From 439effd4b9495697ad52a9f7472a9bdb540fa32f Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 9 Mar 2026 16:10:37 +0530 Subject: [PATCH 01/18] docs(ai-docs): add task refactor migration documentation (old vs new) --- ai-docs/migration/001-migration-overview.md | 166 +++++ .../migration/002-ui-controls-migration.md | 210 ++++++ .../003-store-event-wiring-migration.md | 259 +++++++ .../004-call-control-hook-migration.md | 415 +++++++++++ .../migration/005-incoming-task-migration.md | 238 +++++++ ai-docs/migration/006-task-list-migration.md | 150 ++++ .../migration/007-outdial-call-migration.md | 39 + .../008-store-task-utils-migration.md | 225 ++++++ .../009-types-and-constants-migration.md | 247 +++++++ .../010-component-layer-migration.md | 561 +++++++++++++++ ai-docs/migration/011-execution-plan.md | 438 ++++++++++++ .../012-task-lifecycle-flows-old-vs-new.md | 664 ++++++++++++++++++ ...3-file-inventory-old-control-references.md | 162 +++++ .../migration/014-task-code-scan-report.md | 356 ++++++++++ 14 files changed, 4130 insertions(+) create mode 100644 ai-docs/migration/001-migration-overview.md create mode 100644 ai-docs/migration/002-ui-controls-migration.md create mode 100644 ai-docs/migration/003-store-event-wiring-migration.md create mode 100644 ai-docs/migration/004-call-control-hook-migration.md create mode 100644 ai-docs/migration/005-incoming-task-migration.md create mode 100644 ai-docs/migration/006-task-list-migration.md create mode 100644 ai-docs/migration/007-outdial-call-migration.md create mode 100644 ai-docs/migration/008-store-task-utils-migration.md create mode 100644 ai-docs/migration/009-types-and-constants-migration.md create mode 100644 ai-docs/migration/010-component-layer-migration.md create mode 100644 ai-docs/migration/011-execution-plan.md create mode 100644 ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md create mode 100644 ai-docs/migration/013-file-inventory-old-control-references.md create mode 100644 ai-docs/migration/014-task-code-scan-report.md diff --git a/ai-docs/migration/001-migration-overview.md b/ai-docs/migration/001-migration-overview.md new file mode 100644 index 000000000..fd9840bf2 --- /dev/null +++ b/ai-docs/migration/001-migration-overview.md @@ -0,0 +1,166 @@ +# Migration Doc 001: Task Refactor Migration Overview + +## Purpose + +This document set guides the migration of CC Widgets from the **old ad-hoc task state management** to the **new state-machine-driven architecture** in CC SDK (`task-refactor` branch). + +--- + +## Migration Document Index + +| # | Document | Scope | Risk | Priority | +|---|----------|-------|------|----------| +| 002 | [002-ui-controls-migration.md](./002-ui-controls-migration.md) | Replace `getControlsVisibility()` with `task.uiControls` | **High** (core UX) | P0 | +| 003 | [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) | Refactor store event handlers to leverage state machine events | **Medium** | P1 | +| 004 | [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) | Refactor `useCallControl` hook, timer utils, fix bugs | **High** (largest widget) | P0 | +| 005 | [005-incoming-task-migration.md](./005-incoming-task-migration.md) | Refactor `useIncomingTask` for state-machine offer/assign flow | **Low** | P2 | +| 006 | [006-task-list-migration.md](./006-task-list-migration.md) | Refactor `useTaskList` for per-task `uiControls` | **Low** | P2 | +| 007 | [007-outdial-call-migration.md](./007-outdial-call-migration.md) | No changes needed (CC-level, not task-level) | **Low** | P3 | +| 008 | [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) | Retire or thin out `task-utils.ts`, fix `findHoldTimestamp` dual signatures | **Medium** | P1 | +| 009 | [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) | Align types/constants with SDK, document `UIControlConfig` | **Medium** | P1 | +| 010 | [010-component-layer-migration.md](./010-component-layer-migration.md) | Update `cc-components` to accept new control shape from SDK | **Medium** | P1 | +| 011 | [011-execution-plan.md](./011-execution-plan.md) | Step-by-step spec-first execution plan with 10 milestones | — | — | +| 012 | [012-task-lifecycle-flows-old-vs-new.md](./012-task-lifecycle-flows-old-vs-new.md) | End-to-end task flows (14 scenarios) with old vs new tracing | — | Reference | +| 013 | [013-file-inventory-old-control-references.md](./013-file-inventory-old-control-references.md) | Complete file-by-file inventory of every old control reference | — | Reference | +| 014 | [014-task-code-scan-report.md](./014-task-code-scan-report.md) | Deep code scan findings across both CC SDK and CC Widgets repos | — | Reference | + +--- + +## Key Architectural Shift + +### Before (Old Approach) +``` +SDK emits 30+ events → Store handlers manually update observables → +Widgets compute UI controls via getControlsVisibility() → +Components receive {isVisible, isEnabled} per control +``` + +**Problems:** +- Control visibility logic duplicated between SDK and widgets +- Ad-hoc state derivation from raw task data (consult status, hold status, conference flags) +- Fragile: every new state requires changes in widgets, store utils, AND component logic +- No single source of truth for "what state is this task in?" + +### After (New Approach) +``` +SDK state machine handles all transitions → +SDK computes task.uiControls automatically → +SDK emits task:ui-controls-updated → +Widgets consume task.uiControls directly → +Components receive {isVisible, isEnabled} per control +``` + +**Benefits:** +- Single source of truth: `task.uiControls` from SDK +- Widget code dramatically simplified (remove ~600 lines of control visibility logic) +- Store utils thinned (most consult/conference/hold status checks no longer needed) +- New states automatically handled by SDK, zero widget changes needed +- Parity with Agent Desktop guaranteed by SDK + +--- + +## Repo Paths Reference + +### CC Widgets (this repo) +| Area | Path | +|------|------| +| Task widgets | `packages/contact-center/task/src/` | +| Task hooks | `packages/contact-center/task/src/helper.ts` | +| Task UI utils (OLD) | `packages/contact-center/task/src/Utils/task-util.ts` | +| Task constants | `packages/contact-center/task/src/Utils/constants.ts` | +| Task timer utils | `packages/contact-center/task/src/Utils/timer-utils.ts` | +| Hold timer hook | `packages/contact-center/task/src/Utils/useHoldTimer.ts` | +| Task types | `packages/contact-center/task/src/task.types.ts` | +| Store | `packages/contact-center/store/src/store.ts` | +| Store event wrapper | `packages/contact-center/store/src/storeEventsWrapper.ts` | +| Store task utils (OLD) | `packages/contact-center/store/src/task-utils.ts` | +| Store constants | `packages/contact-center/store/src/constants.ts` | +| CC Components task | `packages/contact-center/cc-components/src/components/task/` | +| CC Components types | `packages/contact-center/cc-components/src/components/task/task.types.ts` | + +### CC SDK (task-refactor branch) +| Area | Path | +|------|------| +| State machine | `packages/@webex/contact-center/src/services/task/state-machine/` | +| UI controls computer | `.../state-machine/uiControlsComputer.ts` | +| State machine config | `.../state-machine/TaskStateMachine.ts` | +| Guards | `.../state-machine/guards.ts` | +| Actions | `.../state-machine/actions.ts` | +| Constants (TaskState, TaskEvent) | `.../state-machine/constants.ts` | +| Types | `.../state-machine/types.ts` | +| Task service | `.../task/Task.ts` | +| Task manager | `.../task/TaskManager.ts` | +| Task types | `.../task/types.ts` | +| Sample app | `docs/samples/contact-center/app.js` | + +--- + +## SDK Version Requirements + +The CC Widgets migration depends on the CC SDK `task-refactor` branch being merged and released. Key new APIs: + +| API | Type | Description | +|-----|------|-------------| +| `task.uiControls` | Property (getter) | Pre-computed `TaskUIControls` object | +| `task:ui-controls-updated` | Event | Emitted when any control's visibility/enabled state changes | +| `TaskUIControls` | Type | `{ [controlName]: { isVisible: boolean, isEnabled: boolean } }` | +| `TaskState` | Enum | Explicit task states (IDLE, OFFERED, CONNECTED, HELD, etc.) | + +--- + +## Pre-existing Bugs Found During Analysis + +These bugs exist in the current codebase and should be fixed during migration: + +### 1. Recording Callback Cleanup Mismatch +**File:** `task/src/helper.ts` (useCallControl), lines 634-653 + +Setup uses `TASK_EVENTS.TASK_RECORDING_PAUSED` / `TASK_EVENTS.TASK_RECORDING_RESUMED`, but cleanup uses `TASK_EVENTS.CONTACT_RECORDING_PAUSED` / `TASK_EVENTS.CONTACT_RECORDING_RESUMED`. Callbacks are never properly removed. + +### 2. `findHoldTimestamp` Dual Signatures +Two `findHoldTimestamp` functions with different signatures: +- `store/src/task-utils.ts`: `findHoldTimestamp(task: ITask, mType: string)` +- `task/src/Utils/task-util.ts`: `findHoldTimestamp(interaction: Interaction, mType: string)` + +Should be consolidated to one function during migration. + +--- + +## Critical Migration Notes + +### UIControlConfig Is Built by SDK (Not by Widgets) + +Widgets do NOT need to provide `UIControlConfig`. The SDK builds it internally from agent profile, `callProcessingDetails`, media type, and voice variant. This means `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` **can be removed** from `useCallControlProps` — they are only used for `getControlsVisibility()` which is being eliminated. + +### Timer Utils Dependency on `controlVisibility` + +`calculateStateTimerData()` and `calculateConsultTimerData()` in `timer-utils.ts` accept `controlVisibility` as a parameter with old control names. These functions must be migrated to accept `TaskUIControls` (new control names). + +### `task:wrapup` Race Condition + +The SDK sample app uses `setTimeout(..., 0)` before updating UI after `task:wrapup`. Consider adding a similar guard in the hook to avoid control flickering during wrapup transition. + +### Sample App Reference Pattern + +The CC SDK sample app (`docs/samples/contact-center/app.js`) demonstrates the canonical pattern: + +```javascript +task.on('task:ui-controls-updated', () => { + updateCallControlUI(task); +}); + +function updateCallControlUI(task) { + const uiControls = task.uiControls || {}; + applyAllControlsFromUIControls(uiControls); +} + +function applyControlState(element, control) { + element.style.display = control?.isVisible ? 'inline-block' : 'none'; + element.disabled = !control?.isEnabled; +} +``` + +--- + +_Created: 2026-03-09_ +_Updated: 2026-03-09 (added deep scan findings, before/after examples, bug reports)_ diff --git a/ai-docs/migration/002-ui-controls-migration.md b/ai-docs/migration/002-ui-controls-migration.md new file mode 100644 index 000000000..0e5f1886f --- /dev/null +++ b/ai-docs/migration/002-ui-controls-migration.md @@ -0,0 +1,210 @@ +# Migration Doc 002: UI Controls — `getControlsVisibility()` → `task.uiControls` + +## Summary + +The largest single change in this migration. CC Widgets currently computes all call control button visibility/enabled states in `task-util.ts::getControlsVisibility()` (~650 lines). The new SDK provides `task.uiControls` as a pre-computed `TaskUIControls` object driven by the state machine, making the widget-side computation redundant. + +--- + +## Old Approach + +### Entry Point +**File:** `packages/contact-center/task/src/Utils/task-util.ts` +**Function:** `getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled, logger)` + +### How It Works (Old) +1. Widget calls `getControlsVisibility()` on every render/task update +2. Function inspects raw `task.data.interaction` to derive: + - Media type (telephony, chat, email) + - Device type (browser, agentDN, extension) + - Consult status (via `getConsultStatus()` from store utils) + - Hold status (via `findHoldStatus()` from store utils) + - Conference status (via `task.data.isConferenceInProgress`) + - Participant counts (via `getConferenceParticipantsCount()`) +3. Each control has a dedicated function that returns `{ isVisible, isEnabled }` +4. Result includes both control visibility AND state flags (e.g., `isHeld`, `consultCallHeld`) + +### Old Control Names (22 controls + 7 state flags) +| Old Control Name | Type | +|------------------|------| +| `accept` | `Visibility` | +| `decline` | `Visibility` | +| `end` | `Visibility` | +| `muteUnmute` | `Visibility` | +| `holdResume` | `Visibility` | +| `pauseResumeRecording` | `Visibility` | +| `recordingIndicator` | `Visibility` | +| `transfer` | `Visibility` | +| `conference` | `Visibility` | +| `exitConference` | `Visibility` | +| `mergeConference` | `Visibility` | +| `consult` | `Visibility` | +| `endConsult` | `Visibility` | +| `consultTransfer` | `Visibility` | +| `consultTransferConsult` | `Visibility` | +| `mergeConferenceConsult` | `Visibility` | +| `muteUnmuteConsult` | `Visibility` | +| `switchToMainCall` | `Visibility` | +| `switchToConsult` | `Visibility` | +| `wrapup` | `Visibility` | +| **State flags** | | +| `isConferenceInProgress` | `boolean` | +| `isConsultInitiated` | `boolean` | +| `isConsultInitiatedAndAccepted` | `boolean` | +| `isConsultReceived` | `boolean` | +| `isConsultInitiatedOrAccepted` | `boolean` | +| `isHeld` | `boolean` | +| `consultCallHeld` | `boolean` | + +--- + +## New Approach + +### Entry Point +**SDK Property:** `task.uiControls` (getter on `ITask`) +**SDK Event:** `task:ui-controls-updated` (emitted when controls change) +**SDK File:** `packages/@webex/contact-center/src/services/task/state-machine/uiControlsComputer.ts` + +### How It Works (New) +1. SDK state machine transitions on every event (hold, consult, conference, etc.) +2. After each transition, `computeUIControls(currentState, context)` is called +3. If controls changed (`haveUIControlsChanged()`), emits `task:ui-controls-updated` +4. Widget reads `task.uiControls` — no computation needed on widget side + +### New Control Names (17 controls, no state flags) +| New Control Name | Type | +|------------------|------| +| `accept` | `{ isVisible, isEnabled }` | +| `decline` | `{ isVisible, isEnabled }` | +| `hold` | `{ isVisible, isEnabled }` | +| `mute` | `{ isVisible, isEnabled }` | +| `end` | `{ isVisible, isEnabled }` | +| `transfer` | `{ isVisible, isEnabled }` | +| `consult` | `{ isVisible, isEnabled }` | +| `consultTransfer` | `{ isVisible, isEnabled }` | +| `endConsult` | `{ isVisible, isEnabled }` | +| `recording` | `{ isVisible, isEnabled }` | +| `conference` | `{ isVisible, isEnabled }` | +| `wrapup` | `{ isVisible, isEnabled }` | +| `exitConference` | `{ isVisible, isEnabled }` | +| `transferConference` | `{ isVisible, isEnabled }` | +| `mergeToConference` | `{ isVisible, isEnabled }` | +| `switchToMainCall` | `{ isVisible, isEnabled }` | +| `switchToConsult` | `{ isVisible, isEnabled }` | + +--- + +## Old → New Control Name Mapping + +| Old Widget Control | New SDK Control | Notes | +|--------------------|-----------------|-------| +| `accept` | `accept` | Same | +| `decline` | `decline` | Same | +| `end` | `end` | Same | +| `muteUnmute` | `mute` | **Renamed** | +| `holdResume` | `hold` | **Renamed** (hold state still togglable) | +| `pauseResumeRecording` | `recording` | **Renamed** | +| `recordingIndicator` | `recording` | **Merged** into `recording` control | +| `transfer` | `transfer` | Same | +| `conference` | `conference` | Same | +| `exitConference` | `exitConference` | Same | +| `mergeConference` | `mergeToConference` | **Renamed** | +| `consult` | `consult` | Same | +| `endConsult` | `endConsult` | Same | +| `consultTransfer` | `consultTransfer` | Same (always hidden in new SDK) | +| `consultTransferConsult` | `transfer` | **Removed** — transfer button handles consult transfer | +| `mergeConferenceConsult` | `mergeToConference` | **Merged** into `mergeToConference` | +| `muteUnmuteConsult` | `mute` | **Merged** into `mute` | +| `switchToMainCall` | `switchToMainCall` | Same | +| `switchToConsult` | `switchToConsult` | Same | +| `wrapup` | `wrapup` | Same | + +### Removed State Flags +The following state flags were returned by `getControlsVisibility()` but are no longer needed: + +| Old State Flag | Replacement | +|----------------|-------------| +| `isConferenceInProgress` | Derive from `task.uiControls.exitConference.isVisible` if needed | +| `isConsultInitiated` | Derive from `task.uiControls.endConsult.isVisible` if needed | +| `isConsultInitiatedAndAccepted` | No longer needed — SDK handles via controls | +| `isConsultReceived` | No longer needed — SDK handles via controls | +| `isConsultInitiatedOrAccepted` | No longer needed — SDK handles via controls | +| `isHeld` | Derive from `task.uiControls.hold` state or SDK task state | +| `consultCallHeld` | No longer needed — SDK handles switch controls | + +--- + +## Refactor Pattern (Before/After) + +### Before (in `useCallControl` hook) +```typescript +// helper.ts — old approach +const controls = getControlsVisibility( + store.deviceType, + store.featureFlags, + store.currentTask, + store.agentId, + conferenceEnabled, + store.logger +); + +// Pass 22 controls + 7 state flags to component +return { + ...controls, + // additional hook state +}; +``` + +### After (in `useCallControl` hook) +```typescript +// helper.ts — new approach +const task = store.currentTask; +const uiControls = task?.uiControls ?? getDefaultUIControls(); + +// Subscribe to UI control updates +useEffect(() => { + if (!task) return; + const handler = () => { + // MobX or setState to trigger re-render + }; + task.on('task:ui-controls-updated', handler); + return () => task.off('task:ui-controls-updated', handler); +}, [task]); + +// Pass SDK-computed controls directly to component +return { + controls: uiControls, + // additional hook state (timers, mute state, etc.) +}; +``` + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `task/src/Utils/task-util.ts` | **DELETE** or reduce to `findHoldTimestamp()` only | +| `task/src/helper.ts` (`useCallControl`) | Remove `getControlsVisibility()` call, use `task.uiControls` | +| `task/src/task.types.ts` | Import `TaskUIControls` from SDK, remove old control types | +| `cc-components/src/components/task/task.types.ts` | Align `ControlProps` with new control names | +| `cc-components/src/components/task/CallControl/call-control.tsx` | Update prop names (`holdResume` → `hold`, etc.) | +| `cc-components/src/components/task/CallControl/call-control.utils.ts` | Simplify/remove old control derivation | +| `store/src/task-utils.ts` | Remove `getConsultStatus`, `findHoldStatus` if no longer consumed | +| All test files for above | Update to test new contract | + +--- + +## Validation Criteria + +- [ ] All 17 SDK controls map correctly to widget UI buttons +- [ ] No widget-side computation of control visibility remains +- [ ] `task:ui-controls-updated` event drives re-renders +- [ ] All existing call control scenarios work identically (hold, consult, transfer, conference, wrapup) +- [ ] Digital channel controls work (accept, end, transfer, wrapup only) +- [ ] Default controls shown when no task is active +- [ ] Error boundary still catches failures gracefully + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/003-store-event-wiring-migration.md b/ai-docs/migration/003-store-event-wiring-migration.md new file mode 100644 index 000000000..05bf58330 --- /dev/null +++ b/ai-docs/migration/003-store-event-wiring-migration.md @@ -0,0 +1,259 @@ +# Migration Doc 003: Store Event Wiring Refactor + +## Summary + +The store's `storeEventsWrapper.ts` currently registers 30+ individual task event handlers that manually call `refreshTaskList()` and update observables. With the state machine, the SDK handles state transitions internally. Many event handlers can be simplified or removed, and the new `task:ui-controls-updated` event replaces manual state derivation. + +--- + +## Old Approach + +### Entry Point +**File:** `packages/contact-center/store/src/storeEventsWrapper.ts` +**Method:** `registerTaskEventListeners(task: ITask)` + +### How It Works (Old) +1. On task creation, store registers individual listeners for 30+ task events +2. Each handler manually updates store observables (taskList, currentTask) +3. Many handlers simply call `refreshTaskList()` to re-fetch task state +4. Some handlers have specialized logic (consult, conference lifecycle) +5. Widgets subscribe to store callbacks via `setTaskCallback(event, cb, taskId)` + +### Old Event Handlers + +| Event | Handler | Action | +|-------|---------|--------| +| `TASK_END` | `handleTaskEnd` | Remove task from list, clear current task | +| `TASK_ASSIGNED` | `handleTaskAssigned` | Update task list, set current task | +| `AGENT_OFFER_CONTACT` | `refreshTaskList` | Re-fetch all tasks | +| `AGENT_CONSULT_CREATED` | `handleConsultCreated` | Update task list, fire callbacks | +| `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | Refresh + fire callbacks | +| `TASK_REJECT` | `handleTaskReject` | Remove task, fire callbacks | +| `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | Remove task, fire callbacks | +| `AGENT_WRAPPEDUP` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_CONSULTING` | `handleConsulting` | Refresh + fire callbacks | +| `TASK_CONSULT_ACCEPTED` | `handleConsultAccepted` | Refresh + fire callbacks | +| `TASK_OFFER_CONSULT` | `handleConsultOffer` | Refresh + fire callbacks | +| `TASK_AUTO_ANSWERED` | `handleAutoAnswer` | Refresh + fire callbacks | +| `TASK_CONSULT_END` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_HOLD` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_RESUME` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | Refresh + fire callbacks | +| `TASK_CONFERENCE_END_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_CONFERENCE_ESTABLISHING` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_CONFERENCE_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_PARTICIPANT_JOINED` | `handleConferenceStarted` | Refresh + fire callbacks | +| `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | Refresh + fire callbacks | +| `TASK_PARTICIPANT_LEFT_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | Refresh + fire callbacks | +| `TASK_CONFERENCE_TRANSFERRED` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_CONFERENCE_TRANSFER_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_POST_CALL_ACTIVITY` | `refreshTaskList` | Re-fetch all tasks | +| `TASK_MEDIA` | `handleTaskMedia` | Browser-only media setup | + +--- + +## New Approach + +### What Changes in SDK +1. SDK state machine handles all transitions internally +2. `task.data` is updated by the state machine's `updateTaskData` action on every event +3. `task.uiControls` is recomputed after every state transition +4. `task:ui-controls-updated` is emitted when controls change + +### Proposed New Event Registration + +Many events that currently trigger `refreshTaskList()` will no longer need it because `task.data` is kept in sync by the SDK. The store should: + +| Event | New Handler | Change | +|-------|-------------|--------| +| `TASK_END` | `handleTaskEnd` | **Keep** (need to remove from task list) | +| `TASK_ASSIGNED` | `handleTaskAssigned` | **Keep** (need to update current task) | +| `TASK_REJECT` | `handleTaskReject` | **Keep** (need to remove from task list) | +| `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | **Keep** (need to remove from task list) | +| `TASK_MEDIA` | `handleTaskMedia` | **Keep** (browser WebRTC setup) | +| `TASK_UI_CONTROLS_UPDATED` | **NEW** — `handleUIControlsUpdated` | **Add** — trigger widget re-renders | +| `TASK_WRAPUP` | `handleWrapup` | **Simplify** — no need to refresh | +| `TASK_WRAPPEDUP` | `handleWrappedup` | **Simplify** — no need to refresh | +| `TASK_HOLD` | Fire callback only | **Simplify** — no `refreshTaskList()` | +| `TASK_RESUME` | Fire callback only | **Simplify** — no `refreshTaskList()` | +| `TASK_CONSULT_*` | Fire callback only | **Simplify** — SDK manages state | +| `TASK_CONFERENCE_*` | Fire callback only | **Simplify** — SDK manages state | +| `AGENT_OFFER_CONTACT` | Fire callback only | **Simplify** — SDK updates task.data | +| `TASK_POST_CALL_ACTIVITY` | Fire callback only | **Simplify** | +| All other `refreshTaskList()` handlers | Remove or fire callback only | **Simplify** | + +### Key Insight: `refreshTaskList()` Elimination + +**Old:** 15+ events trigger `refreshTaskList()` → `cc.taskManager.getAllTasks()` → update store observables. + +**New:** SDK keeps `task.data` updated via state machine actions. The store can read `task.data` directly instead of re-fetching. `refreshTaskList()` should only be called for: +- Initial load / hydration +- Full page refresh recovery +- Edge cases where task data is stale + +--- + +## Old → New Event Handler Mapping + +| Old Handler | Old Action | New Action | +|-------------|-----------|------------| +| `refreshTaskList` (15+ events) | Re-fetch ALL tasks from SDK | **Remove** — task.data is live; fire callbacks only | +| `handleTaskEnd` | Remove from list + cleanup | **Keep** — still need list management | +| `handleTaskAssigned` | Set current task | **Keep** — still need list management | +| `handleConsultCreated` | Refresh + callbacks | **Simplify** — callbacks only | +| `handleConsulting` | Refresh + callbacks | **Simplify** — callbacks only | +| `handleConsultAccepted` | Refresh + callbacks | **Simplify** — callbacks only | +| `handleConsultOffer` | Refresh + callbacks | **Simplify** — callbacks only | +| `handleConferenceStarted` | Refresh + callbacks | **Simplify** — callbacks only | +| `handleConferenceEnded` | Refresh + callbacks | **Simplify** — callbacks only | +| `handleAutoAnswer` | Refresh + callbacks | **Simplify** — callbacks only | +| `handleTaskReject` | Remove from list | **Keep** | +| `handleOutdialFailed` | Remove from list | **Keep** | +| `handleTaskMedia` | WebRTC media setup | **Keep** | + +--- + +--- + +## Refactor Patterns (Before/After) + +### Pattern 1: `registerTaskEventListeners()` — Adding UI Controls Handler + +#### Before +```typescript +// storeEventsWrapper.ts — registerTaskEventListeners(task) +registerTaskEventListeners(task: ITask) { + const interactionId = task.data.interactionId; + task.on(TASK_EVENTS.TASK_END, (data) => this.handleTaskEnd(data, interactionId)); + task.on(TASK_EVENTS.TASK_ASSIGNED, (data) => this.handleTaskAssigned(data, interactionId)); + task.on(TASK_EVENTS.AGENT_OFFER_CONTACT, () => this.refreshTaskList()); + task.on(TASK_EVENTS.TASK_HOLD, () => this.refreshTaskList()); + task.on(TASK_EVENTS.TASK_RESUME, () => this.refreshTaskList()); + task.on(TASK_EVENTS.TASK_CONSULT_END, () => this.refreshTaskList()); + task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, () => this.refreshTaskList()); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, (data) => this.handleConferenceStarted(data, interactionId)); + // ... 20+ more event registrations, most calling refreshTaskList() +} +``` + +#### After +```typescript +// storeEventsWrapper.ts — registerTaskEventListeners(task) +registerTaskEventListeners(task: ITask) { + const interactionId = task.data.interactionId; + + // NEW: Subscribe to SDK-computed UI control updates + task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, (uiControls) => { + this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); + }); + + // KEEP: Task lifecycle events that need store-level management + task.on(TASK_EVENTS.TASK_END, (data) => this.handleTaskEnd(data, interactionId)); + task.on(TASK_EVENTS.TASK_ASSIGNED, (data) => this.handleTaskAssigned(data, interactionId)); + task.on(TASK_EVENTS.TASK_REJECT, (data) => this.handleTaskReject(data, interactionId)); + task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, (data) => this.handleOutdialFailed(data, interactionId)); + task.on(TASK_EVENTS.TASK_MEDIA, (data) => this.handleTaskMedia(data, interactionId)); + + // SIMPLIFIED: Events that only need callback firing (SDK keeps task.data in sync) + task.on(TASK_EVENTS.TASK_HOLD, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_HOLD, interactionId)); + task.on(TASK_EVENTS.TASK_RESUME, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RESUME, interactionId)); + task.on(TASK_EVENTS.AGENT_WRAPPEDUP, (data) => this.fireTaskCallbacks(TASK_EVENTS.AGENT_WRAPPEDUP, interactionId, data)); + task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_PAUSED, interactionId)); + task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_RESUMED, interactionId)); + // ... consult/conference events: fire callbacks only, no refreshTaskList() +} +``` + +### Pattern 2: Simplifying `refreshTaskList()` Event Handlers + +#### Before +```typescript +// 15+ events all trigger a full re-fetch +task.on(TASK_EVENTS.TASK_HOLD, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_RESUME, () => this.refreshTaskList()); +task.on(TASK_EVENTS.AGENT_WRAPPEDUP, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_CONSULT_END, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, () => this.refreshTaskList()); +task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, () => this.refreshTaskList()); +// refreshTaskList() does: cc.taskManager.getAllTasks() → update store.taskList +``` + +#### After +```typescript +// SDK keeps task.data in sync via state machine. +// refreshTaskList() only called on initialization/hydration. +// Individual events just fire callbacks for widget-layer side effects. + +task.on(TASK_EVENTS.TASK_HOLD, () => { + this.fireTaskCallbacks(TASK_EVENTS.TASK_HOLD, interactionId); +}); +task.on(TASK_EVENTS.TASK_RESUME, () => { + this.fireTaskCallbacks(TASK_EVENTS.TASK_RESUME, interactionId); +}); +// No refreshTaskList() — task.data already updated by SDK +``` + +### Pattern 3: Conference Handler Simplification + +#### Before +```typescript +handleConferenceStarted(data: any, interactionId: string) { + this.refreshTaskList(); // Re-fetch all tasks from SDK + this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId, data); + this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId, data); +} + +handleConferenceEnded(data: any, interactionId: string) { + this.refreshTaskList(); // Re-fetch all tasks from SDK + this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId, data); + this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId, data); +} +``` + +#### After +```typescript +// SDK state machine handles CONFERENCING state transitions. +// task.data and task.uiControls already reflect conference state. +// Store just fires callbacks for widget-layer side effects. + +handleConferenceStarted(data: any, interactionId: string) { + this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId, data); + this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId, data); +} + +handleConferenceEnded(data: any, interactionId: string) { + this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId, data); + this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId, data); +} +``` + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `store/src/storeEventsWrapper.ts` | Refactor `registerTaskEventListeners`, simplify/remove handlers | +| `store/src/store.ts` | No changes expected (observables stay) | +| `store/src/store.types.ts` | Add `TASK_UI_CONTROLS_UPDATED` if not re-exported from SDK | + +--- + +## Validation Criteria + +- [ ] Task list stays in sync on all lifecycle events (incoming, assigned, end, reject) +- [ ] `refreshTaskList()` only called on init/hydration, not on every event +- [ ] Widget callbacks still fire correctly for events that require UI updates +- [ ] `task:ui-controls-updated` triggers re-renders in widgets +- [ ] No regression in consult/conference/hold flows +- [ ] Task removal from list on end/reject works correctly + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/004-call-control-hook-migration.md b/ai-docs/migration/004-call-control-hook-migration.md new file mode 100644 index 000000000..6a60f63c6 --- /dev/null +++ b/ai-docs/migration/004-call-control-hook-migration.md @@ -0,0 +1,415 @@ +# Migration Doc 004: CallControl Hook (`useCallControl`) Migration + +## Summary + +`useCallControl` is the largest and most complex hook in CC Widgets. It orchestrates hold, mute, recording, consult, transfer, conference, wrapup, and auto-wrapup flows. This migration replaces widget-side control computation with `task.uiControls` and simplifies event-driven state updates. + +--- + +## Old Approach + +### Entry Point +**File:** `packages/contact-center/task/src/helper.ts` +**Hook:** `useCallControl(props: useCallControlProps)` + +### Current Responsibilities +1. **Control visibility**: Calls `getControlsVisibility()` → 22 controls + 7 state flags +2. **Hold/Resume**: `toggleHold()` → `task.hold()` / `task.resume()` / `task.hold(mediaResourceId)` / `task.resume(mediaResourceId)` +3. **Mute**: `toggleMute()` → `task.toggleMute()` (local state tracking) +4. **Recording**: `toggleRecording()` → `task.pauseRecording()` / `task.resumeRecording()` +5. **End call**: `endCall()` → `task.end()` +6. **Wrapup**: `wrapupCall()` → `task.wrapup()` +7. **Transfer**: `transferCall()` → `task.transfer()` +8. **Consult**: `consultCall()` → `task.consult()`, `endConsultCall()` → `task.endConsult()` +9. **Consult transfer**: `consultTransfer()` → `task.consultTransfer()` / `task.transferConference()` +10. **Conference**: `consultConference()` → `task.consultConference()`, `exitConference()` → `task.exitConference()` +11. **Switch calls**: `switchToConsult()` → `task.hold(mainMedia)` + `task.resume(consultMedia)`, `switchToMainCall()` → reverse +12. **Auto-wrapup timer**: `cancelAutoWrapup()` → `task.cancelAutoWrapupTimer()` +13. **Hold timer**: via `useHoldTimer(currentTask)` hook +14. **Event callbacks**: Registers hold/resume/end/wrapup/recording callbacks via `setTaskCallback` + +### Old Hook Return Shape (abbreviated) +```typescript +{ + // Controls (from getControlsVisibility) + accept, decline, end, muteUnmute, holdResume, + pauseResumeRecording, recordingIndicator, + transfer, conference, exitConference, mergeConference, + consult, endConsult, consultTransfer, consultTransferConsult, + mergeConferenceConsult, muteUnmuteConsult, + switchToMainCall, switchToConsult, wrapup, + // State flags (from getControlsVisibility) + isConferenceInProgress, isConsultInitiated, isConsultInitiatedAndAccepted, + isConsultReceived, isConsultInitiatedOrAccepted, isHeld, consultCallHeld, + // Hook state + isMuted, isRecording, holdTime, buddyAgents, + consultAgentName, lastTargetType, secondsUntilAutoWrapup, + // Actions + toggleHold, toggleMute, toggleRecording, endCall, wrapupCall, + transferCall, consultCall, endConsultCall, consultTransfer, + consultConference, exitConference, switchToConsult, switchToMainCall, + cancelAutoWrapup, +} +``` + +--- + +## New Approach + +### Key Changes + +1. **Remove `getControlsVisibility()` call entirely** +2. **Read `task.uiControls` directly** for all control states +3. **Subscribe to `task:ui-controls-updated`** for re-renders +4. **Keep all action methods** (hold, mute, end, etc.) — SDK methods unchanged +5. **Simplify state flags** — derive from `uiControls` or remove entirely +6. **Keep hold timer, auto-wrapup, mute state** — these are widget-layer concerns + +### New Hook Return Shape (proposed) +```typescript +{ + // Controls (directly from task.uiControls) + controls: TaskUIControls, // { accept, decline, hold, mute, end, transfer, ... } + // Hook state (kept) + isMuted: boolean, + isRecording: boolean, + holdTime: number, + buddyAgents: Agent[], + consultAgentName: string, + lastTargetType: string, + secondsUntilAutoWrapup: number, + // Actions (kept — SDK methods unchanged) + toggleHold, toggleMute, toggleRecording, endCall, wrapupCall, + transferCall, consultCall, endConsultCall, consultTransfer, + consultConference, exitConference, switchToConsult, switchToMainCall, + cancelAutoWrapup, +} +``` + +--- + +## Old → New Mapping Table + +### Control Properties + +| Old Property | New Property | Change | +|-------------|-------------|--------| +| `accept` | `controls.accept` | Nested under `controls` | +| `decline` | `controls.decline` | Nested under `controls` | +| `end` | `controls.end` | Nested under `controls` | +| `muteUnmute` | `controls.mute` | **Renamed** + nested | +| `holdResume` | `controls.hold` | **Renamed** + nested | +| `pauseResumeRecording` | `controls.recording` | **Renamed** + nested | +| `recordingIndicator` | `controls.recording` | **Merged** with recording | +| `transfer` | `controls.transfer` | Nested | +| `conference` | `controls.conference` | Nested | +| `exitConference` | `controls.exitConference` | Nested | +| `mergeConference` | `controls.mergeToConference` | **Renamed** + nested | +| `consult` | `controls.consult` | Nested | +| `endConsult` | `controls.endConsult` | Nested | +| `consultTransfer` | `controls.consultTransfer` | Nested (always hidden in new) | +| `consultTransferConsult` | `controls.transfer` | **Removed** — use `transfer` | +| `mergeConferenceConsult` | `controls.mergeToConference` | **Merged** | +| `muteUnmuteConsult` | `controls.mute` | **Merged** | +| `switchToMainCall` | `controls.switchToMainCall` | Nested | +| `switchToConsult` | `controls.switchToConsult` | Nested | +| `wrapup` | `controls.wrapup` | Nested | + +### State Flags + +| Old Flag | New Approach | +|----------|-------------| +| `isConferenceInProgress` | `controls.exitConference.isVisible` | +| `isConsultInitiated` | `controls.endConsult.isVisible` | +| `isConsultInitiatedAndAccepted` | Removed — SDK handles | +| `isConsultReceived` | Removed — SDK handles | +| `isConsultInitiatedOrAccepted` | `controls.endConsult.isVisible` | +| `isHeld` | `controls.hold` state (visible + disabled = held) | +| `consultCallHeld` | `controls.switchToConsult.isVisible` | + +### Actions (Unchanged) + +| Action | SDK Method | Change | +|--------|-----------|--------| +| `toggleHold` | `task.hold()` / `task.resume()` | None | +| `toggleMute` | `task.toggleMute()` | None | +| `toggleRecording` | `task.pauseRecording()` / `task.resumeRecording()` | None | +| `endCall` | `task.end()` | None | +| `wrapupCall` | `task.wrapup()` | None | +| `transferCall` | `task.transfer()` | None | +| `consultCall` | `task.consult()` | None | +| `endConsultCall` | `task.endConsult()` | None | +| `consultTransfer` | `task.consultTransfer()` / `task.transferConference()` | None | +| `consultConference` | `task.consultConference()` | None | +| `exitConference` | `task.exitConference()` | None | +| `switchToConsult` | `task.hold(mainMediaId)` + `task.resume(consultMediaId)` | None | +| `switchToMainCall` | `task.hold(consultMediaId)` + `task.resume(mainMediaId)` | None | +| `cancelAutoWrapup` | `task.cancelAutoWrapupTimer()` | None | + +--- + +## Refactor Pattern + +### Before +```typescript +export function useCallControl(props: useCallControlProps) { + const task = store.currentTask; + + // OLD: Widget computes controls + const controls = getControlsVisibility( + store.deviceType, + store.featureFlags, + task, + store.agentId, + conferenceEnabled, + store.logger + ); + + // Event callbacks for hold, resume, end, wrapup, recording + useEffect(() => { + if (!task) return; + store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, task.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, task.data.interactionId); + // ... 4 more callbacks + return () => { + store.removeTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, task.data.interactionId); + // ... cleanup + }; + }, [task]); + + return { ...controls, isMuted, isRecording, /* ... actions */ }; +} +``` + +### After +```typescript +export function useCallControl(props: useCallControlProps) { + const task = store.currentTask; + + // NEW: Read SDK-computed controls directly + const [controls, setControls] = useState( + task?.uiControls ?? getDefaultUIControls() + ); + + // Subscribe to UI control updates + useEffect(() => { + if (!task) { + setControls(getDefaultUIControls()); + return; + } + setControls(task.uiControls); + const onControlsUpdated = (updatedControls: TaskUIControls) => { + setControls(updatedControls); + }; + task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, onControlsUpdated); + return () => { + task.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, onControlsUpdated); + }; + }, [task]); + + // Keep event callbacks for actions that need hook-level side effects + // (hold timer, mute state, recording state) + useEffect(() => { + if (!task) return; + store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, task.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, task.data.interactionId); + // ... recording callbacks + return () => { /* cleanup */ }; + }, [task]); + + return { controls, isMuted, isRecording, holdTime, /* ... actions */ }; +} +``` + +--- + +--- + +## Newly Discovered Items (Deep Scan) + +### 1. Pre-existing Bug: Recording Callback Cleanup Mismatch + +**File:** `task/src/helper.ts`, lines 634-653 + +```typescript +// SETUP uses TASK_EVENTS: +store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_PAUSED, pauseRecordingCallback, interactionId); +store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_RESUMED, resumeRecordingCallback, interactionId); + +// CLEANUP uses CONTACT_RECORDING (different event name!): +store.removeTaskCallback(TASK_EVENTS.CONTACT_RECORDING_PAUSED, pauseRecordingCallback, interactionId); +store.removeTaskCallback(TASK_EVENTS.CONTACT_RECORDING_RESUMED, resumeRecordingCallback, interactionId); +``` + +**Impact:** Callbacks are never properly removed on cleanup. Fix during migration by using consistent event names. + +### 2. `controlVisibility` Used as `useMemo` + Timer Effect Dependencies + +```typescript +// Line 930-933: controlVisibility is a useMemo +const controlVisibility = useMemo( + () => getControlsVisibility(deviceType, featureFlags, currentTask, agentId, conferenceEnabled, logger), + [deviceType, featureFlags, currentTask, agentId, conferenceEnabled, logger] +); + +// Line 939: Auto-wrapup timer depends on controlVisibility.wrapup +useEffect(() => { + if (currentTask?.autoWrapup && controlVisibility?.wrapup) { ... } +}, [currentTask?.autoWrapup, controlVisibility?.wrapup]); + +// Line 974: State timer depends on controlVisibility +useEffect(() => { + const stateTimerData = calculateStateTimerData(currentTask, controlVisibility, agentId); + ... +}, [currentTask, controlVisibility, agentId]); + +// Line 982: Consult timer depends on controlVisibility +useEffect(() => { + const consultTimerData = calculateConsultTimerData(currentTask, controlVisibility, agentId); + ... +}, [currentTask, controlVisibility, agentId]); +``` + +**Migration impact:** `calculateStateTimerData()` and `calculateConsultTimerData()` in `timer-utils.ts` accept `controlVisibility` as a parameter. These must be updated to accept `TaskUIControls` instead (with new control names). + +### 3. `toggleMute` References Old Control Name + +```typescript +// Line 704-705: +if (!controlVisibility?.muteUnmute) { + logger.warn('Mute control not available', ...); + return; +} +``` + +**Migration:** Change to `controls.mute`. + +### 4. `wrapupCall` Post-Action State Management + +```typescript +// Lines 766-773: After wrapup, sets next task as current and updates agent state +.then(() => { + const taskKeys = Object.keys(store.taskList); + if (taskKeys.length > 0) { + store.setCurrentTask(store.taskList[taskKeys[0]]); + store.setState({ developerName: ENGAGED_LABEL, name: ENGAGED_USERNAME }); + } +}) +``` + +**Migration:** This logic stays. Post-wrapup task selection is a widget-layer concern. + +### 5. `consultTransfer` Uses `currentTask.data.isConferenceInProgress` + +```typescript +// Line 898: Decides between consultTransfer vs transferConference +if (currentTask.data.isConferenceInProgress) { + await currentTask.transferConference(); +} else { + await currentTask.consultTransfer(); +} +``` + +**Migration:** Can replace with `controls.transferConference.isVisible` to decide. But since SDK action methods are unchanged, keeping `data.isConferenceInProgress` is also fine. + +### 6. `extractConsultingAgent` — Complex Display Logic (KEEP) + +Lines 326-446: ~120 lines of logic to find the consulting agent's name from `interaction.participants` and `callProcessingDetails.consultDestinationAgentName`. This is display-only logic and NOT related to control visibility. **Keep as-is.** + +### 7. `useOutdialCall` — `isTelephonyTaskActive` Check + +```typescript +const isTelephonyTaskActive = useMemo(() => { + return Object.values(store.taskList).some( + (task) => task?.data?.interaction?.mediaType === MEDIA_TYPE_TELEPHONY_LOWER + ); +}, [store.taskList]); +``` + +**Migration:** Unaffected — this checks media type for outdial gating, not control visibility. + +### 8. UIControlConfig — SDK Builds It Internally + +Widgets do NOT need to provide UIControlConfig. The SDK builds it from: +- Agent profile → `isEndTaskEnabled`, `isEndConsultEnabled` +- `callProcessingDetails.pauseResumeEnabled` → `isRecordingEnabled` +- `interaction.mediaType` → `channelType` (voice/digital) +- Voice/WebRTC layer → `voiceVariant` (pstn/webrtc) +- `taskManager.setAgentId()` → `agentId` + +This means `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` props can be removed from `useCallControlProps`. + +### 9. `task:wrapup` Race Condition + +SDK sample app uses `setTimeout(..., 0)` before updating UI after `task:wrapup`. Consider adding similar guard in hook if wrapup controls flicker. + +--- + +## Timer Utils Migration + +**File:** `task/src/Utils/timer-utils.ts` + +The `calculateStateTimerData()` and `calculateConsultTimerData()` functions accept `controlVisibility` as a parameter with old control names. These must be migrated: + +### Before +```typescript +export function calculateStateTimerData( + task: ITask, + controlVisibility: ReturnType, + agentId: string +) { + if (controlVisibility?.wrapup?.isVisible) { + return { label: 'Wrap Up', timestamp: task.data.wrapUpTimestamp }; + } + // Uses controlVisibility.isConsultInitiatedOrAccepted, controlVisibility.isHeld, etc. +} +``` + +### After +```typescript +export function calculateStateTimerData( + task: ITask, + controls: TaskUIControls, + agentId: string +) { + if (controls.wrapup.isVisible) { + return { label: 'Wrap Up', timestamp: task.data.wrapUpTimestamp }; + } + const isConsulting = controls.endConsult.isVisible; + // Use controls.hold, controls.endConsult, etc. +} +``` + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `task/src/helper.ts` | Refactor `useCallControl` as described above | +| `task/src/Utils/task-util.ts` | Remove or reduce (only keep `findHoldTimestamp`) | +| `task/src/Utils/timer-utils.ts` | Update to accept `TaskUIControls` instead of `controlVisibility` | +| `task/src/task.types.ts` | Update `useCallControlProps` return type | +| `task/tests/helper.ts` | Update all `useCallControl` tests | +| `cc-components/.../CallControl/call-control.tsx` | Update to accept new `controls` prop shape | +| `cc-components/.../CallControl/call-control.utils.ts` | Simplify (remove old control mapping) | + +--- + +## Validation Criteria + +- [ ] All 17 SDK controls render correctly in CallControl UI +- [ ] Hold toggle works (CONNECTED ↔ HELD) +- [ ] Mute toggle works (local WebRTC state) +- [ ] Recording toggle works (pause/resume) +- [ ] Consult flow: initiate → switch calls → end/transfer/conference +- [ ] Conference flow: merge → exit → transfer conference +- [ ] Wrapup flow: end → wrapup → complete +- [ ] Auto-wrapup timer works +- [ ] Hold timer displays correctly +- [ ] Digital channel shows only accept/end/transfer/wrapup +- [ ] All action methods still call correct SDK methods + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/005-incoming-task-migration.md b/ai-docs/migration/005-incoming-task-migration.md new file mode 100644 index 000000000..ffaaf0dd5 --- /dev/null +++ b/ai-docs/migration/005-incoming-task-migration.md @@ -0,0 +1,238 @@ +# Migration Doc 005: IncomingTask Widget Migration + +## Summary + +The IncomingTask widget handles task offer/accept/reject flows. The state machine changes are minimal here since accept/decline SDK methods are unchanged. The main change is that the OFFERED → CONNECTED/TERMINATED transitions are now explicit state machine states, and `task.uiControls.accept`/`decline` can drive button visibility instead of widget-side logic. + +--- + +## Old Approach + +### Entry Point +**File:** `packages/contact-center/task/src/helper.ts` +**Hook:** `useIncomingTask(props: UseTaskProps)` + +### How It Works (Old) +1. Store sets `incomingTask` observable on `TASK_INCOMING` event +2. Widget (observer) re-renders when `incomingTask` changes +3. Hook registers per-task callbacks: `TASK_ASSIGNED`, `TASK_CONSULT_ACCEPTED`, `TASK_END`, `TASK_REJECT`, `TASK_CONSULT_END` +4. Accept → `task.accept()` → SDK → `TASK_ASSIGNED` → `onAccepted` callback +5. Reject → `task.decline()` → SDK → `TASK_REJECT` → `onRejected` callback +6. Timer expiry (RONA) → `reject()` → `task.decline()` +7. Accept/Decline button visibility computed by `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` in task-util.ts + +--- + +## New Approach + +### What Changes +1. **Accept/Decline visibility** → now available via `task.uiControls.accept` / `task.uiControls.decline` +2. **State machine states**: IDLE → OFFERED → (CONNECTED on accept | TERMINATED on reject/RONA) +3. **SDK methods unchanged**: `task.accept()`, `task.decline()` still work the same +4. **Events unchanged**: `TASK_ASSIGNED`, `TASK_REJECT` still emitted + +### Minimal Changes Required +- Replace `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` with `task.uiControls.accept` / `task.uiControls.decline` +- Optionally subscribe to `task:ui-controls-updated` for reactive updates +- Keep all callback registration as-is (accept/reject lifecycle callbacks) + +--- + +## Old → New Mapping + +| Aspect | Old | New | +|--------|-----|-----| +| Accept visible | `getAcceptButtonVisibility(isBrowser, isPhone, webRtc, isCall, isDigital)` | `task.uiControls.accept.isVisible` | +| Decline visible | `getDeclineButtonVisibility(isBrowser, webRtc, isCall)` | `task.uiControls.decline.isVisible` | +| Accept action | `task.accept()` | `task.accept()` (unchanged) | +| Decline action | `task.decline()` | `task.decline()` (unchanged) | +| Task assigned event | `TASK_EVENTS.TASK_ASSIGNED` | `TASK_EVENTS.TASK_ASSIGNED` (unchanged) | +| Task rejected event | `TASK_EVENTS.TASK_REJECT` | `TASK_EVENTS.TASK_REJECT` (unchanged) | +| Timer/RONA | Widget-managed timer | Widget-managed timer (unchanged) | + +--- + +## Refactor Pattern + +### Before +```typescript +// In IncomingTask component or hook +const { isBrowser, isPhoneDevice } = getDeviceTypeFlags(store.deviceType); +const acceptVisibility = getAcceptButtonVisibility( + isBrowser, isPhoneDevice, webRtcEnabled, isCall, isDigitalChannel +); +const declineVisibility = getDeclineButtonVisibility(isBrowser, webRtcEnabled, isCall); +``` + +### After +```typescript +// In IncomingTask component or hook +const task = store.incomingTask; +const acceptVisibility = task?.uiControls?.accept ?? { isVisible: false, isEnabled: false }; +const declineVisibility = task?.uiControls?.decline ?? { isVisible: false, isEnabled: false }; +``` + +--- + +--- + +## Full Before/After: `useIncomingTask` Hook + +### Before (current code in `helper.ts`) +```typescript +export const useIncomingTask = (props: UseTaskProps) => { + const {onAccepted, onRejected, deviceType, incomingTask, logger} = props; + const isBrowser = deviceType === 'BROWSER'; + const isDeclineButtonEnabled = store.isDeclineButtonEnabled; + + // Event callbacks registered per-task for accept/reject lifecycle + useEffect(() => { + if (!incomingTask) return; + store.setTaskCallback(TASK_EVENTS.TASK_ASSIGNED, () => { + if (onAccepted) onAccepted({task: incomingTask}); + }, incomingTask.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); + + return () => { + store.removeTaskCallback(TASK_EVENTS.TASK_ASSIGNED, taskAssignCallback, incomingTask?.data.interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); + }; + }, [incomingTask]); + + const accept = () => { + if (!incomingTask?.data.interactionId) return; + incomingTask.accept().catch((error) => { /* log */ }); + }; + + const reject = () => { + if (!incomingTask?.data.interactionId) return; + incomingTask.decline().catch((error) => { /* log */ }); + }; + + return { + incomingTask, + accept, + reject, + isBrowser, // Used to determine accept/decline button visibility + isDeclineButtonEnabled, // Feature flag for decline button + }; +}; +``` + +**Note:** The `isBrowser` and `isDeclineButtonEnabled` flags are passed to the component, which uses them to decide whether to show accept/decline buttons. This duplicates what `task.uiControls.accept/decline` now provides. + +### After (migrated) +```typescript +export const useIncomingTask = (props: UseTaskProps) => { + const {onAccepted, onRejected, incomingTask, logger} = props; + + // NEW: Read accept/decline visibility from SDK + const acceptControl = incomingTask?.uiControls?.accept ?? {isVisible: false, isEnabled: false}; + const declineControl = incomingTask?.uiControls?.decline ?? {isVisible: false, isEnabled: false}; + + // Event callbacks — UNCHANGED (still need lifecycle callbacks for onAccepted/onRejected) + useEffect(() => { + if (!incomingTask) return; + store.setTaskCallback(TASK_EVENTS.TASK_ASSIGNED, () => { + if (onAccepted) onAccepted({task: incomingTask}); + }, incomingTask.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); + + return () => { /* cleanup — same as before */ }; + }, [incomingTask]); + + // Actions — UNCHANGED + const accept = () => { + if (!incomingTask?.data.interactionId) return; + incomingTask.accept().catch((error) => { /* log */ }); + }; + + const reject = () => { + if (!incomingTask?.data.interactionId) return; + incomingTask.decline().catch((error) => { /* log */ }); + }; + + return { + incomingTask, + accept, + reject, + acceptControl, // NEW: { isVisible, isEnabled } from SDK + declineControl, // NEW: { isVisible, isEnabled } from SDK + // REMOVED: isBrowser, isDeclineButtonEnabled (no longer needed) + }; +}; +``` + +### Component-Level Before/After + +#### Before (IncomingTaskComponent) +```tsx +// incoming-task.tsx — old approach +const IncomingTaskComponent = ({ isBrowser, isDeclineButtonEnabled, onAccept, onReject, ... }) => { + // Widget computes visibility from device type and feature flags + const showAccept = isBrowser; // simplified — actual logic in getAcceptButtonVisibility() + const showDecline = isBrowser && isDeclineButtonEnabled; + + return ( +
+ {showAccept && } + {showDecline && } +
+ ); +}; +``` + +#### After (IncomingTaskComponent) +```tsx +// incoming-task.tsx — new approach +const IncomingTaskComponent = ({ acceptControl, declineControl, onAccept, onReject, ... }) => { + // SDK provides exact visibility and enabled state + return ( +
+ {acceptControl.isVisible && ( + + )} + {declineControl.isVisible && ( + + )} +
+ ); +}; +``` + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `task/src/helper.ts` (`useIncomingTask`) | Use `task.uiControls.accept/decline` instead of visibility functions | +| `task/src/IncomingTask/index.tsx` | Minor: pass new control shape to component | +| `cc-components/.../IncomingTask/incoming-task.tsx` | Update accept/decline prop names if needed | +| `task/tests/IncomingTask/index.tsx` | Update tests | + +--- + +## Validation Criteria + +- [ ] Accept button visible for WebRTC voice tasks +- [ ] Accept button visible for digital channel tasks (chat/email) +- [ ] Decline button visible for WebRTC voice tasks only +- [ ] Accept action calls `task.accept()` and triggers `TASK_ASSIGNED` +- [ ] Decline action calls `task.decline()` and triggers `TASK_REJECT` +- [ ] RONA timer triggers reject correctly +- [ ] Consult incoming (OFFER_CONSULT) shows accept/decline correctly +- [ ] Cleanup on unmount removes callbacks + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/006-task-list-migration.md b/ai-docs/migration/006-task-list-migration.md new file mode 100644 index 000000000..3c51afae9 --- /dev/null +++ b/ai-docs/migration/006-task-list-migration.md @@ -0,0 +1,150 @@ +# Migration Doc 006: TaskList Widget Migration + +## Summary + +The TaskList widget displays all active tasks and allows accept/decline/select. Changes are minimal since task list management (add/remove tasks) stays in the store, and SDK methods are unchanged. The main opportunity is to simplify how task status is derived for display. + +--- + +## Old Approach + +### Entry Point +**File:** `packages/contact-center/task/src/helper.ts` +**Hook:** `useTaskList(props: UseTaskListProps)` + +### How It Works (Old) +1. Store maintains `taskList: Record` observable +2. Store maintains `currentTask: ITask | null` observable +3. Hook provides `acceptTask(task)`, `declineTask(task)`, `onTaskSelect(task)` actions +4. Task status derived via `getTaskStatus(task, agentId)` from store's `task-utils.ts` +5. Task display data extracted by `cc-components/task/Task/task.utils.ts` + +--- + +## New Approach + +### What Changes +1. **Task status derivation** can potentially use state machine state instead of `getTaskStatus()` +2. **Task list management** (add/remove) stays the same — store-managed +3. **SDK methods unchanged**: `task.accept()`, `task.decline()` +4. **Store callbacks unchanged**: `setTaskAssigned`, `setTaskRejected`, `setTaskSelected` + +### Minimal Changes Required +- If `getTaskStatus()` is used for display, consider using SDK task state info +- Accept/decline button visibility per task can use `task.uiControls.accept` +- Task selection logic unchanged + +--- + +## Old → New Mapping + +| Aspect | Old | New | +|--------|-----|-----| +| Task list source | `store.taskList` observable | `store.taskList` observable (unchanged) | +| Current task | `store.currentTask` observable | `store.currentTask` observable (unchanged) | +| Task status | `getTaskStatus(task, agentId)` from store utils | SDK task state or `task.uiControls` for button states | +| Accept action | `task.accept()` | `task.accept()` (unchanged) | +| Decline action | `task.decline()` | `task.decline()` (unchanged) | +| Select action | `store.setCurrentTask(task, isClicked)` | Unchanged | + +--- + +--- + +## Before/After: Per-Task Accept/Decline in TaskList + +### Before (TaskList component renders accept/decline per task) +```tsx +// task-list.tsx — old approach +const TaskListComponent = ({ taskList, isBrowser, onAccept, onDecline, onSelect }) => { + return taskList.map((task) => { + // Accept/decline visibility computed per-task from device type + const showAccept = isBrowser; // simplified + return ( + onSelect(task)}> + {showAccept && } + {showAccept && } + + ); + }); +}; +``` + +### After (use per-task `uiControls`) +```tsx +// task-list.tsx — new approach +const TaskListComponent = ({ taskList, onAccept, onDecline, onSelect }) => { + return taskList.map((task) => { + // SDK provides per-task control visibility + const acceptControl = task.uiControls?.accept ?? {isVisible: false, isEnabled: false}; + const declineControl = task.uiControls?.decline ?? {isVisible: false, isEnabled: false}; + return ( + onSelect(task)}> + {acceptControl.isVisible && ( + + )} + {declineControl.isVisible && ( + + )} + + ); + }); +}; +``` + +### Before/After: `useTaskList` Hook + +#### Before +```typescript +// helper.ts — useTaskList (abbreviated) +export const useTaskList = (props: UseTaskListProps) => { + const {deviceType, onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; + const isBrowser = deviceType === 'BROWSER'; // Used for accept/decline visibility + + // ... store callbacks and actions unchanged ... + + return {taskList, acceptTask, declineTask, onTaskSelect, isBrowser}; + // ^^^^^^^^^ passed to component +}; +``` + +#### After +```typescript +// helper.ts — useTaskList (migrated) +export const useTaskList = (props: UseTaskListProps) => { + const {onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; + // REMOVED: deviceType, isBrowser — no longer needed, SDK handles per-task visibility + + // ... store callbacks and actions unchanged ... + + return {taskList, acceptTask, declineTask, onTaskSelect}; + // REMOVED: isBrowser — each task.uiControls.accept/decline provides visibility +}; +``` + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `task/src/helper.ts` (`useTaskList`) | Remove `isBrowser`, use per-task `uiControls` for accept/decline | +| `task/src/TaskList/index.tsx` | Remove `isBrowser` prop pass-through | +| `cc-components/.../TaskList/task-list.tsx` | Use `task.uiControls.accept/decline` per task | +| `cc-components/.../Task/task.utils.ts` | Update task data extraction if status source changes | +| `store/src/task-utils.ts` (`getTaskStatus`) | Consider deprecation if SDK provides equivalent | + +--- + +## Validation Criteria + +- [ ] Task list displays all active tasks +- [ ] Task selection works (sets `currentTask`) +- [ ] Accept/decline per task works +- [ ] Task status displays correctly (connected, held, wrapup, etc.) +- [ ] Tasks removed from list on end/reject +- [ ] New incoming tasks appear in list + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/007-outdial-call-migration.md b/ai-docs/migration/007-outdial-call-migration.md new file mode 100644 index 000000000..79e3f9bef --- /dev/null +++ b/ai-docs/migration/007-outdial-call-migration.md @@ -0,0 +1,39 @@ +# Migration Doc 007: OutdialCall Widget Migration + +## Summary + +OutdialCall is largely **unaffected** by the task state machine refactor. It initiates outbound calls via `cc.startOutdial()` (a CC-level method, not a task method) and fetches ANI entries. The resulting task, once created, is handled by IncomingTask/TaskList/CallControl. No state machine integration needed here. + +--- + +## Old Approach + +### Entry Point +**File:** `packages/contact-center/task/src/helper.ts` +**Hook:** `useOutdialCall(props: useOutdialCallProps)` + +### How It Works +1. Fetches ANI entries via `cc.getOutdialAniEntries()` +2. Validates phone number format +3. Initiates outbound call via `cc.startOutdial(destination, origin)` +4. Does NOT subscribe to any task events +5. Resulting task surfaces via `TASK_INCOMING` → IncomingTask widget + +--- + +## New Approach + +**No changes required.** The `cc.startOutdial()` method is a CC-level API, not a task-level API. The state machine is activated when the resulting task is created. + +--- + +## Validation Criteria + +- [ ] Outdial initiation works +- [ ] ANI entry fetching works +- [ ] Resulting task appears in task list via existing flow +- [ ] Phone number validation unchanged + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/008-store-task-utils-migration.md b/ai-docs/migration/008-store-task-utils-migration.md new file mode 100644 index 000000000..f83d8be71 --- /dev/null +++ b/ai-docs/migration/008-store-task-utils-migration.md @@ -0,0 +1,225 @@ +# Migration Doc 008: Store Task Utils Migration + +## Summary + +The store's `task-utils.ts` contains ~15 utility functions that inspect raw task data to derive state flags (consult status, hold status, conference state, participant info). Many of these become redundant when `task.uiControls` is the source of truth. This document maps which utils to keep, simplify, or remove. + +--- + +## Old Utilities Inventory + +**File:** `packages/contact-center/store/src/task-utils.ts` + +| Function | Purpose | Used By | +|----------|---------|---------| +| `isIncomingTask(task, agentId)` | Check if task is incoming | Store event wrapper | +| `getConsultMPCState(task, agentId)` | Consult multi-party conference state | Store, helpers | +| `isSecondaryAgent(task)` | Whether agent is secondary in consult | Store | +| `isSecondaryEpDnAgent(task)` | Whether agent is secondary EP-DN | Store | +| `getTaskStatus(task, agentId)` | Human-readable task status string | TaskList component | +| `getConsultStatus(task, agentId)` | `ConsultStatus` enum value | task-util.ts (controls), CallControl | +| `getIsConferenceInProgress(task)` | Boolean conference check | task-util.ts (controls) | +| `getConferenceParticipants(task, agentId)` | Filtered participant list | CallControl component | +| `getConferenceParticipantsCount(task)` | Participant count | task-util.ts (controls) | +| `getIsCustomerInCall(task)` | Whether customer is connected | task-util.ts (controls) | +| `getIsConsultInProgress(task)` | Whether consult is active | task-util.ts (controls) | +| `isInteractionOnHold(task)` | Whether any media is held | Task timer utils | +| `setmTypeForEPDN(task, mType)` | Media type for EP-DN agents | CallControl hook | +| `findMediaResourceId(task, mType)` | Media resource ID lookup | CallControl hook (switch calls) | +| `findHoldStatus(task, mType, agentId)` | Hold status by media type | task-util.ts (controls) | +| `findHoldTimestamp(task, mType)` | Hold timestamp for timers | Task timer, hold timer | + +--- + +## Migration Decisions + +### Remove (SDK handles via uiControls) + +| Function | Reason | SDK Replacement | +|----------|--------|-----------------| +| `getConsultStatus(task, agentId)` | Used only for control visibility computation | `task.uiControls` encodes all consult control states | +| `getIsConferenceInProgress(task)` | Used only for control visibility computation | `task.uiControls.exitConference.isVisible` | +| `getConferenceParticipantsCount(task)` | Used only for control visibility computation | SDK computes max participant check internally | +| `getIsCustomerInCall(task)` | Used only for control visibility computation | SDK computes internally | +| `getIsConsultInProgress(task)` | Used only for control visibility computation | SDK computes internally | +| `findHoldStatus(task, mType, agentId)` | Used for control visibility | SDK tracks hold state in context | + +### Keep (Widget-layer concerns) + +| Function | Reason | +|----------|--------| +| `isIncomingTask(task, agentId)` | Store needs this for routing incoming tasks | +| `getTaskStatus(task, agentId)` | TaskList display needs human-readable status | +| `getConferenceParticipants(task, agentId)` | CallControl UI shows participant list (display, not control visibility) | +| `isInteractionOnHold(task)` | Timer logic needs this | +| `findMediaResourceId(task, mType)` | Switch-call actions need media resource IDs | +| `findHoldTimestamp(task, mType)` | Hold timer needs timestamp | + +### Review (may simplify) + +| Function | Consideration | +|----------|--------------| +| `isSecondaryAgent(task)` | May be replaceable by SDK context | +| `isSecondaryEpDnAgent(task)` | May be replaceable by SDK context | +| `getConsultMPCState(task, agentId)` | Review if still needed with SDK handling consult state | +| `setmTypeForEPDN(task, mType)` | Review if SDK simplifies this | + +--- + +## Old → New Mapping + +| Old Function | Status | New Equivalent | +|-------------|--------|---------------| +| `getConsultStatus()` | **REMOVE** | `task.uiControls.endConsult`, `task.uiControls.switchToMainCall` etc. | +| `getIsConferenceInProgress()` | **REMOVE** | `task.uiControls.exitConference.isVisible` | +| `getConferenceParticipantsCount()` | **REMOVE** | SDK internal check | +| `getIsCustomerInCall()` | **REMOVE** | SDK internal check | +| `getIsConsultInProgress()` | **REMOVE** | SDK internal check | +| `findHoldStatus()` | **REMOVE** | SDK tracks in `TaskContext` | +| `isIncomingTask()` | **KEEP** | — | +| `getTaskStatus()` | **KEEP** | Could enhance with SDK TaskState | +| `getConferenceParticipants()` | **KEEP** | Display only | +| `isInteractionOnHold()` | **KEEP** | Timer display | +| `findMediaResourceId()` | **KEEP** | Action parameter | +| `findHoldTimestamp()` | **KEEP** | Timer display | + +--- + +--- + +## Before/After: Removing `getConsultStatus` Usage + +### Before (consumed in `task-util.ts::getControlsVisibility`) +```typescript +// task-util.ts — old approach +import { getConsultStatus, ConsultStatus, getIsConsultInProgress, getIsCustomerInCall, + getConferenceParticipantsCount, findHoldStatus } from '@webex/cc-store'; + +export function getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled) { + // Derive consult status by inspecting raw task data + const taskConsultStatus = getConsultStatus(task, agentId); + const isConsultInitiated = taskConsultStatus === ConsultStatus.CONSULT_INITIATED; + const isConsultAccepted = taskConsultStatus === ConsultStatus.CONSULT_ACCEPTED; + const isBeingConsulted = taskConsultStatus === ConsultStatus.BEING_CONSULTED_ACCEPTED; + const isConsultCompleted = taskConsultStatus === ConsultStatus.CONSULT_COMPLETED; + + // Derive hold status from raw task media + const isHeld = findHoldStatus(task, 'mainCall', agentId); + const consultCallHeld = findHoldStatus(task, 'consult', agentId); + + // Derive conference state from raw task data + const isConferenceInProgress = task?.data?.isConferenceInProgress ?? false; + const isConsultInProgress = getIsConsultInProgress(task); + const isCustomerInCall = getIsCustomerInCall(task); + const conferenceParticipantsCount = getConferenceParticipantsCount(task); + + // 20+ individual visibility functions using these derived states... + return { /* 22 controls + 7 state flags */ }; +} +``` + +### After (all above replaced by `task.uiControls`) +```typescript +// task-util.ts — DELETED or reduced to: +export { findHoldTimestamp } from './task-util'; // Only keep for timer display + +// In useCallControl hook — no imports from store task-utils for controls: +const controls = currentTask?.uiControls ?? getDefaultUIControls(); +// All 17 controls come pre-computed from SDK. Zero store util calls needed. +``` + +### Before/After: `findHoldStatus` Removal + +#### Before (used in controls computation) +```typescript +// store/task-utils.ts +export function findHoldStatus(task: ITask, mType: string, agentId: string): boolean { + if (!task?.data?.interaction?.media) return false; + const media = Object.values(task.data.interaction.media).find(m => m.mType === mType); + if (!media?.participants) return false; + const participant = task.data.interaction.participants[agentId]; + return participant?.isHold ?? false; +} + +// task-util.ts — consumed for control visibility +const isHeld = findHoldStatus(task, 'mainCall', agentId); +const consultCallHeld = findHoldStatus(task, 'consult', agentId); +``` + +#### After +```typescript +// REMOVED from store/task-utils.ts — SDK tracks hold state in TaskContext: +// - task.uiControls.hold.isEnabled indicates holdable +// - task.uiControls.switchToConsult.isVisible indicates consult call is held +// No widget-side derivation needed. +``` + +### Before/After: `getTaskStatus` (KEPT but enhanced) + +#### Before +```typescript +// store/task-utils.ts — returns human-readable status +export function getTaskStatus(task: ITask, agentId: string): string { + if (task.data.interaction?.isTerminated) return 'Wrap Up'; + const consultStatus = getConsultStatus(task, agentId); + if (consultStatus === ConsultStatus.CONSULT_INITIATED) return 'Consulting'; + if (task.data.isConferenceInProgress) return 'Conference'; + if (findHoldStatus(task, 'mainCall', agentId)) return 'Held'; + return 'Connected'; +} +``` + +#### After (enhanced with SDK controls) +```typescript +// store/task-utils.ts — can now derive status from uiControls +export function getTaskStatus(task: ITask): string { + const controls = task.uiControls; + if (!controls) return 'Unknown'; + if (controls.wrapup.isVisible) return 'Wrap Up'; + if (controls.endConsult.isVisible) return 'Consulting'; + if (controls.exitConference.isVisible) return 'Conference'; + if (controls.hold.isVisible && !controls.hold.isEnabled) return 'Held'; + if (controls.end.isVisible) return 'Connected'; + if (controls.accept.isVisible) return 'Offered'; + return 'Unknown'; +} +``` + +--- + +## `findHoldTimestamp` Signature Mismatch (Pre-existing Issue) + +**Discovery:** Two different `findHoldTimestamp` functions exist with different signatures: + +| Location | Signature | Used By | +|----------|-----------|---------| +| `store/src/task-utils.ts` | `findHoldTimestamp(task: ITask, mType: string)` | `timer-utils.ts` | +| `task/src/Utils/task-util.ts` | `findHoldTimestamp(interaction: Interaction, mType: string)` | `useHoldTimer.ts` | + +Both should be consolidated during migration. Recommend keeping only the store version (accepts `ITask`) for consistency. + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `store/src/task-utils.ts` | Remove 6 functions, keep 6, review 4 | +| `store/src/constants.ts` | Remove consult state constants if unused | +| `task/src/Utils/task-util.ts` | Major reduction (imports from store utils) | +| All consumers of removed functions | Update imports, switch to `task.uiControls` | + +--- + +## Validation Criteria + +- [ ] Removed functions have no remaining consumers +- [ ] Kept functions still work correctly +- [ ] TaskList status display unchanged +- [ ] Conference participant display unchanged +- [ ] Hold timer unchanged +- [ ] Switch-call media resource IDs work + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/009-types-and-constants-migration.md b/ai-docs/migration/009-types-and-constants-migration.md new file mode 100644 index 000000000..7646ef5b8 --- /dev/null +++ b/ai-docs/migration/009-types-and-constants-migration.md @@ -0,0 +1,247 @@ +# Migration Doc 009: Types and Constants Alignment + +## Summary + +CC Widgets defines its own types for control visibility, task state, and constants. These must be aligned with the new SDK types (`TaskUIControls`, `TaskState`, etc.) to ensure type safety and avoid duplication. + +--- + +## Type Mapping: Old → New + +### Control Visibility Type + +| Old (CC Widgets) | New (CC SDK) | +|------------------|--------------| +| `Visibility` from `@webex/cc-components` = `{ isVisible: boolean; isEnabled: boolean }` | `TaskUIControlState` = `{ isVisible: boolean; isEnabled: boolean }` | + +**Same shape, different name.** Can either: +- (A) Import `TaskUIControlState` from SDK and alias +- (B) Keep `Visibility` as-is since shape is identical +- **Recommendation:** Keep `Visibility` in `cc-components` for UI-layer independence; accept `TaskUIControls` from SDK in hooks. + +### Controls Object Type + +| Old (CC Widgets) | New (CC SDK) | +|------------------|--------------| +| No unified type — `getControlsVisibility()` returns ad-hoc object with 22 controls + 7 flags | `TaskUIControls` = `{ [17 control names]: { isVisible, isEnabled } }` | + +**Action:** Import and use `TaskUIControls` from SDK. Map to component props. + +### Task State Constants + +| Old (CC Widgets Store) | New (CC SDK) | +|------------------------|--------------| +| `TASK_STATE_CONSULT` | `TaskState.CONSULTING` | +| `TASK_STATE_CONSULTING` | `TaskState.CONSULTING` | +| `TASK_STATE_CONSULT_COMPLETED` | `TaskState.CONNECTED` (consult ended, back to connected) | +| `INTERACTION_STATE_WRAPUP` | `TaskState.WRAPPING_UP` | +| `POST_CALL` | `TaskState.POST_CALL` (not implemented) | +| `CONNECTED` | `TaskState.CONNECTED` | +| `CONFERENCE` | `TaskState.CONFERENCING` | + +### Consult Status Constants + +| Old (CC Widgets Store) | New (CC SDK Context) | +|------------------------|---------------------| +| `CONSULT_STATE_INITIATED` | `context.consultInitiator` = true | +| `CONSULT_STATE_COMPLETED` | SDK transitions back to CONNECTED/CONFERENCING | +| `CONSULT_STATE_CONFERENCING` | `TaskState.CONFERENCING` | +| `ConsultStatus.CONSULT_INITIATED` | `TaskState.CONSULT_INITIATING` | +| `ConsultStatus.CONSULT_ACCEPTED` | `context.consultDestinationAgentJoined` = true | +| `ConsultStatus.BEING_CONSULTED` | `context.isConsultedAgent` (derived by SDK) | +| `ConsultStatus.BEING_CONSULTED_ACCEPTED` | `context.isConsultedAgent` + CONSULTING state | +| `ConsultStatus.CONSULT_COMPLETED` | SDK clears consult context on CONSULT_END | + +### Event Constants + +| Old (CC Widgets) | New (CC SDK) | Change | +|------------------|--------------|--------| +| `TASK_EVENTS.TASK_INCOMING` | `TASK_EVENTS.TASK_INCOMING` | Same | +| `TASK_EVENTS.TASK_ASSIGNED` | `TASK_EVENTS.TASK_ASSIGNED` | Same | +| `TASK_EVENTS.TASK_HOLD` | `TASK_EVENTS.TASK_HOLD` | Same | +| `TASK_EVENTS.TASK_RESUME` | `TASK_EVENTS.TASK_RESUME` | Same | +| `TASK_EVENTS.TASK_END` | `TASK_EVENTS.TASK_END` | Same | +| — | `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | **NEW** | +| `TASK_EVENTS.TASK_WRAPUP` | `TASK_EVENTS.TASK_WRAPUP` | Same | +| `TASK_EVENTS.TASK_WRAPPEDUP` | `TASK_EVENTS.TASK_WRAPPEDUP` | Same | +| All consult/conference events | Same event names | Same | + +### Media Type Constants + +| Old (CC Widgets) | New (CC SDK) | +|------------------|--------------| +| `MEDIA_TYPE_TELEPHONY` = `'telephony'` | `TASK_CHANNEL_TYPE.VOICE` = `'voice'` | +| `MEDIA_TYPE_CHAT` = `'chat'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | +| `MEDIA_TYPE_EMAIL` = `'email'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | + +**Note:** SDK uses channel type (`VOICE`/`DIGITAL`) for UI control computation. Widget media types may still be needed for display purposes. + +--- + +## New Types to Import from SDK + +| Type | Source | Purpose | +|------|--------|---------| +| `TaskUIControls` | `@webex/contact-center` | Pre-computed control states | +| `TaskUIControlState` | `@webex/contact-center` | Single control `{ isVisible, isEnabled }` | +| `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | `@webex/contact-center` | New event | + +**Note:** `TaskState` and `TaskEvent` enums are internal to SDK and NOT exported to consumers. Widgets should not depend on them directly — use `task.uiControls` instead. + +--- + +--- + +## Before/After: Type Imports + +### Before (task.types.ts) +```typescript +// task/src/task.types.ts — old approach +import {ITask, Interaction} from '@webex/contact-center'; +import {Visibility, ControlProps} from '@webex/cc-components'; + +export interface useCallControlProps { + currentTask: ITask; + deviceType: string; // Used for control visibility computation + featureFlags: {[key: string]: boolean}; // Used for control visibility + agentId: string; // Used for control visibility + conferenceEnabled: boolean; // Used for control visibility + isMuted: boolean; + logger: ILogger; + onHoldResume?: (data: any) => void; + onEnd?: (data: any) => void; + onWrapUp?: (data: any) => void; + onRecordingToggle?: (data: any) => void; + onToggleMute?: (data: any) => void; +} +// Return type is ad-hoc — includes 22 controls + 7 flags + hook state + actions +``` + +### After (task.types.ts) +```typescript +// task/src/task.types.ts — new approach +import {ITask, TaskUIControls} from '@webex/contact-center'; + +export interface useCallControlProps { + currentTask: ITask; + // REMOVED: deviceType, featureFlags, agentId, conferenceEnabled + // (SDK computes controls via UIControlConfig, set at task creation) + isMuted: boolean; + logger: ILogger; + onHoldResume?: (data: any) => void; + onEnd?: (data: any) => void; + onWrapUp?: (data: any) => void; + onRecordingToggle?: (data: any) => void; + onToggleMute?: (data: any) => void; +} + +export interface CallControlHookResult { + controls: TaskUIControls; // NEW: all 17 controls from SDK + currentTask: ITask; + isMuted: boolean; + isRecording: boolean; + holdTime: number; + startTimestamp: number; + stateTimerLabel: string | null; + stateTimerTimestamp: number; + consultTimerLabel: string; + consultTimerTimestamp: number; + secondsUntilAutoWrapup: number | null; + buddyAgents: BuddyDetails[]; + loadingBuddyAgents: boolean; + consultAgentName: string; + conferenceParticipants: Participant[]; + lastTargetType: TargetType; + // Actions + toggleHold: (hold: boolean) => void; + toggleMute: () => Promise; + toggleRecording: () => void; + endCall: () => void; + wrapupCall: (reason: string, auxCodeId: string) => void; + transferCall: (to: string, type: DestinationType) => Promise; + consultCall: (dest: string, type: DestinationType, interact: boolean) => Promise; + endConsultCall: () => Promise; + consultTransfer: () => Promise; + consultConference: () => Promise; + switchToMainCall: () => Promise; + switchToConsult: () => Promise; + exitConference: () => Promise; + cancelAutoWrapup: () => void; + loadBuddyAgents: () => Promise; + getAddressBookEntries: (params: PaginatedListParams) => Promise; + getEntryPoints: (params: PaginatedListParams) => Promise; + getQueuesFetcher: (params: PaginatedListParams) => Promise; + setLastTargetType: (type: TargetType) => void; + setConsultAgentName: (name: string) => void; +} +``` + +### Before/After: Constants + +#### Before (store/constants.ts) +```typescript +// Used throughout for consult status derivation +export const TASK_STATE_CONSULT = 'consult'; +export const TASK_STATE_CONSULTING = 'consulting'; +export const TASK_STATE_CONSULT_COMPLETED = 'consultCompleted'; +export const INTERACTION_STATE_WRAPUP = 'wrapup'; +export const POST_CALL = 'postCall'; +export const CONNECTED = 'connected'; +export const CONFERENCE = 'conference'; +export const CONSULT_STATE_INITIATED = 'initiated'; +export const CONSULT_STATE_COMPLETED = 'completed'; +export const CONSULT_STATE_CONFERENCING = 'conferencing'; +export const RELATIONSHIP_TYPE_CONSULT = 'consult'; +export const MEDIA_TYPE_CONSULT = 'consult'; +``` + +#### After +```typescript +// REMOVE these (no longer needed for control computation): +// TASK_STATE_CONSULT, TASK_STATE_CONSULTING, TASK_STATE_CONSULT_COMPLETED +// INTERACTION_STATE_WRAPUP, POST_CALL, CONNECTED, CONFERENCE +// CONSULT_STATE_INITIATED, CONSULT_STATE_COMPLETED, CONSULT_STATE_CONFERENCING + +// KEEP these (still used for display or action logic): +export const RELATIONSHIP_TYPE_CONSULT = 'consult'; +export const MEDIA_TYPE_CONSULT = 'consult'; // Used by findMediaResourceId +``` + +--- + +## UIControlConfig Note + +**Important:** Widgets do NOT need to provide `UIControlConfig`. The SDK builds it internally: +- `channelType` — resolved from `task.data.interaction.mediaType` (telephony → voice, else digital) +- `voiceVariant` — set by Voice/WebRTC layer (PSTN vs WebRTC) +- `isEndTaskEnabled` / `isEndConsultEnabled` — from agent profile config flags +- `isRecordingEnabled` — from `callProcessingDetails.pauseResumeEnabled` +- `agentId` — from `taskManager.setAgentId()` + +This means the widget no longer needs to pass `deviceType`, `featureFlags`, or `agentId` for control computation. + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `task/src/task.types.ts` | Import `TaskUIControls` from SDK; update hook return types | +| `cc-components/.../task/task.types.ts` | Add `TaskUIControls` prop type for CallControl | +| `store/src/store.types.ts` | Ensure `TASK_UI_CONTROLS_UPDATED` is available | +| `store/src/constants.ts` | Review/remove consult state constants | +| `task/src/Utils/constants.ts` | Review/remove media type constants used only for controls | + +--- + +## Validation Criteria + +- [ ] `TaskUIControls` type imported from SDK compiles correctly +- [ ] No type mismatches between SDK controls and component props +- [ ] `TASK_UI_CONTROLS_UPDATED` event constant available +- [ ] Old constants still available where needed (display purposes) +- [ ] No unused type imports remain + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/010-component-layer-migration.md b/ai-docs/migration/010-component-layer-migration.md new file mode 100644 index 000000000..edb49cf98 --- /dev/null +++ b/ai-docs/migration/010-component-layer-migration.md @@ -0,0 +1,561 @@ +# Migration Doc 010: Component Layer (`cc-components`) Migration + +## Summary + +The `cc-components` package contains the presentational React components for task widgets. These components receive control visibility as props. The prop interface must be updated to match the new `TaskUIControls` shape from SDK (renamed controls, merged controls, removed state flags). + +--- + +## Components to Update + +### CallControlComponent +**File:** `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` + +#### Old Prop Names → New Prop Names + +| Old Prop | New Prop | Change | +|----------|----------|--------| +| `holdResume` | `hold` | **Rename** | +| `muteUnmute` | `mute` | **Rename** | +| `pauseResumeRecording` | `recording` | **Rename** | +| `recordingIndicator` | `recording` | **Merge** with recording control | +| `mergeConference` | `mergeToConference` | **Rename** | +| `consultTransferConsult` | — | **Remove** (use `transfer`) | +| `mergeConferenceConsult` | — | **Remove** (use `mergeToConference`) | +| `muteUnmuteConsult` | — | **Remove** (use `mute`) | +| `isConferenceInProgress` | — | **Remove** (derive from controls) | +| `isConsultInitiated` | — | **Remove** (derive from controls) | +| `isConsultInitiatedAndAccepted` | — | **Remove** | +| `isConsultReceived` | — | **Remove** | +| `isConsultInitiatedOrAccepted` | — | **Remove** | +| `isHeld` | — | **Remove** (derive from controls) | +| `consultCallHeld` | — | **Remove** | + +#### Proposed New Interface + +```typescript +interface CallControlComponentProps { + controls: TaskUIControls; // All 17 controls from SDK + // Widget-layer state (not from SDK) + isMuted: boolean; + isRecording: boolean; + holdTime: number; + secondsUntilAutoWrapup: number; + buddyAgents: Agent[]; + consultAgentName: string; + // Actions + onToggleHold: () => void; + onToggleMute: () => void; + onToggleRecording: () => void; + onEndCall: () => void; + onWrapupCall: (reason: string, auxCodeId: string) => void; + onTransferCall: (payload: TransferPayLoad) => void; + onConsultCall: (payload: ConsultPayload) => void; + onEndConsultCall: () => void; + onConsultTransfer: () => void; + onConsultConference: () => void; + onExitConference: () => void; + onSwitchToConsult: () => void; + onSwitchToMainCall: () => void; + onCancelAutoWrapup: () => void; +} +``` + +### CallControlConsult +**File:** `packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx` + +- Update to use `controls.endConsult`, `controls.mergeToConference`, `controls.switchToMainCall`, `controls.switchToConsult` +- Remove separate `consultTransferConsult`, `mergeConferenceConsult`, `muteUnmuteConsult` props + +### IncomingTaskComponent +**File:** `packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx` + +- Accept: `controls.accept.isVisible` / `controls.accept.isEnabled` +- Decline: `controls.decline.isVisible` / `controls.decline.isEnabled` +- Minimal changes — shape is compatible + +### TaskListComponent +**File:** `packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx` + +- Per-task accept/decline: use `task.uiControls.accept` / `task.uiControls.decline` +- Task status display: may use existing `getTaskStatus()` or enhance + +### OutdialCallComponent +**File:** `packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx` + +- **No changes needed** — OutdialCall does not use task controls + +--- + +--- + +## Full Before/After: CallControlComponent + +### Before +```tsx +// call-control.tsx — old approach +const CallControlComponent = ({ + // 22 individual control props + accept, decline, end, muteUnmute, holdResume, + pauseResumeRecording, recordingIndicator, + transfer, conference, exitConference, mergeConference, + consult, endConsult, consultTransfer, consultTransferConsult, + mergeConferenceConsult, muteUnmuteConsult, + switchToMainCall, switchToConsult, wrapup, + // 7 state flags + isConferenceInProgress, isConsultInitiated, + isConsultInitiatedAndAccepted, isConsultReceived, + isConsultInitiatedOrAccepted, isHeld, consultCallHeld, + // Actions and hook state + isMuted, isRecording, holdTime, onToggleHold, onToggleMute, ... +}) => { + return ( +
+ {holdResume.isVisible && ( + + )} + {muteUnmute.isVisible && ( + + )} + {end.isVisible && ( + + )} + {/* Consult sub-controls */} + {isConsultInitiatedOrAccepted && ( +
+ {endConsult.isVisible && } + {consultTransferConsult.isVisible && } + {mergeConferenceConsult.isVisible && } + {muteUnmuteConsult.isVisible && } +
+ )} + {/* Conference sub-controls */} + {isConferenceInProgress && ( +
+ {exitConference.isVisible && } + {mergeConference.isVisible && } +
+ )} +
+ ); +}; +``` + +### After +```tsx +// call-control.tsx — new approach +const CallControlComponent = ({ + controls, // TaskUIControls — all 17 controls from SDK + isMuted, isRecording, holdTime, + onToggleHold, onToggleMute, onEndCall, onEndConsult, + onConsultTransfer, onConsultConference, onExitConference, + onSwitchToMainCall, onSwitchToConsult, ... +}: CallControlComponentProps) => { + // Derive display-only flags from controls (replaces old state flag props) + const isConsulting = controls.endConsult.isVisible; + const isConferencing = controls.exitConference.isVisible; + + return ( +
+ {controls.hold.isVisible && ( + + )} + {controls.mute.isVisible && ( + + )} + {controls.end.isVisible && ( + + )} + {/* Transfer and Consult initiation */} + {controls.transfer.isVisible && ( + + )} + {controls.consult.isVisible && ( + + )} + {/* Active consult controls */} + {controls.endConsult.isVisible && ( + + )} + {controls.mergeToConference.isVisible && ( + + )} + {controls.switchToMainCall.isVisible && ( + + )} + {controls.switchToConsult.isVisible && ( + + )} + {/* Conference controls */} + {controls.exitConference.isVisible && ( + + )} + {controls.transferConference.isVisible && ( + + )} + {/* Recording */} + {controls.recording.isVisible && ( + + )} + {/* Wrapup */} + {controls.wrapup.isVisible && ( + + )} +
+ ); +}; +``` + +--- + +## Deriving State Flags from Controls + +Components that previously relied on state flags can derive them: + +```typescript +// Old: isConferenceInProgress (boolean prop) +// New: derive from controls +const isConferenceInProgress = controls.exitConference.isVisible; + +// Old: isConsultInitiatedOrAccepted (boolean prop) +// New: derive from controls +const isConsulting = controls.endConsult.isVisible; + +// Old: isHeld (boolean prop) +// New: derive from hold control +const isHeld = controls.hold.isVisible && !controls.hold.isEnabled; +// (Note: hold is visible but disabled when held + in conference) +``` + +--- + +## Newly Discovered Files (Deep Scan) + +### 1. `ControlVisibility` Type — The Central Type to Replace + +**File:** `cc-components/src/components/task/task.types.ts` (lines 702-730) + +```typescript +// OLD: ControlVisibility interface (22 controls + 7 state flags) +export interface ControlVisibility { + accept: Visibility; + decline: Visibility; + end: Visibility; + muteUnmute: Visibility; // → mute + muteUnmuteConsult: Visibility; // → REMOVE (use mute) + holdResume: Visibility; // → hold + consult: Visibility; + transfer: Visibility; + conference: Visibility; + wrapup: Visibility; + pauseResumeRecording: Visibility; // → recording + endConsult: Visibility; + recordingIndicator: Visibility; // → REMOVE (merged into recording) + exitConference: Visibility; + mergeConference: Visibility; // → mergeToConference + consultTransfer: Visibility; + mergeConferenceConsult: Visibility; // → REMOVE (use mergeToConference) + consultTransferConsult: Visibility; // → REMOVE (use transfer) + switchToMainCall: Visibility; + switchToConsult: Visibility; + isConferenceInProgress: boolean; // → derive from controls.exitConference.isVisible + isConsultInitiated: boolean; // → derive from controls.endConsult.isVisible + isConsultInitiatedAndAccepted: boolean; // → REMOVE + isConsultReceived: boolean; // → REMOVE + isConsultInitiatedOrAccepted: boolean; // → REMOVE + isHeld: boolean; // → derive from controls.hold + consultCallHeld: boolean; // → derive from controls.switchToConsult.isVisible +} +``` + +**Migration:** Replace with `TaskUIControls` import from SDK. The `ControlVisibility` interface and all its consumers must be updated. + +### 2. `buildCallControlButtons()` — CRITICAL File + +**File:** `cc-components/.../CallControl/call-control.utils.ts` (line 192) + +This function builds the main call control button array. It references **12 old control names and 2 state flags**: + +| Old Reference | New Equivalent | +|--------------|---------------| +| `controlVisibility.muteUnmute.isVisible` | `controls.mute.isVisible` | +| `controlVisibility.switchToConsult.isEnabled` | `controls.switchToConsult.isEnabled` | +| `controlVisibility.isHeld` | Derive: `controls.hold.isVisible && !controls.hold.isEnabled` | +| `controlVisibility.holdResume.isEnabled` | `controls.hold.isEnabled` | +| `controlVisibility.holdResume.isVisible` | `controls.hold.isVisible` | +| `controlVisibility.consult.isEnabled` | `controls.consult.isEnabled` | +| `controlVisibility.consult.isVisible` | `controls.consult.isVisible` | +| `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | +| `controlVisibility.consultTransfer.isEnabled` | `controls.consultTransfer.isEnabled` | +| `controlVisibility.consultTransfer.isVisible` | `controls.consultTransfer.isVisible` | +| `controlVisibility.mergeConference.isEnabled` | `controls.mergeToConference.isEnabled` | +| `controlVisibility.mergeConference.isVisible` | `controls.mergeToConference.isVisible` | +| `controlVisibility.transfer.isEnabled` | `controls.transfer.isEnabled` | +| `controlVisibility.transfer.isVisible` | `controls.transfer.isVisible` | +| `controlVisibility.pauseResumeRecording.isEnabled` | `controls.recording.isEnabled` | +| `controlVisibility.pauseResumeRecording.isVisible` | `controls.recording.isVisible` | +| `controlVisibility.exitConference.isEnabled` | `controls.exitConference.isEnabled` | +| `controlVisibility.exitConference.isVisible` | `controls.exitConference.isVisible` | +| `controlVisibility.end.isEnabled` | `controls.end.isEnabled` | +| `controlVisibility.end.isVisible` | `controls.end.isVisible` | + +#### Before +```typescript +export const buildCallControlButtons = ( + isMuted, isRecording, isMuteButtonDisabled, currentMediaType, + controlVisibility: ControlVisibility, // OLD type + ...handlers +): CallControlButton[] => { + return [ + { id: 'mute', isVisible: controlVisibility.muteUnmute.isVisible, ... }, + { id: 'hold', icon: controlVisibility.isHeld ? 'play-bold' : 'pause-bold', + disabled: !controlVisibility.holdResume.isEnabled, + isVisible: controlVisibility.holdResume.isVisible, ... }, + { id: 'record', isVisible: controlVisibility.pauseResumeRecording.isVisible, ... }, + // ... 10 more buttons using old names + ]; +}; +``` + +#### After +```typescript +export const buildCallControlButtons = ( + isMuted, isRecording, isMuteButtonDisabled, currentMediaType, + controls: TaskUIControls, // NEW type from SDK + ...handlers +): CallControlButton[] => { + const isHeld = controls.hold.isVisible && !controls.hold.isEnabled; + const isConferencing = controls.exitConference.isVisible; + return [ + { id: 'mute', isVisible: controls.mute.isVisible, ... }, + { id: 'hold', icon: isHeld ? 'play-bold' : 'pause-bold', + disabled: !controls.hold.isEnabled, + isVisible: controls.hold.isVisible, ... }, + { id: 'record', isVisible: controls.recording.isVisible, ... }, + // ... 10 more buttons using new names + ]; +}; +``` + +### 3. `createConsultButtons()` — CRITICAL File + +**File:** `cc-components/.../CallControlCustom/call-control-custom.utils.ts` (line 16) + +This function builds the consult-mode button array. It references **5 old control names and 1 state flag**: + +| Old Reference | New Equivalent | +|--------------|---------------| +| `controlVisibility.muteUnmuteConsult` | `controls.mute` | +| `controlVisibility.switchToMainCall` | `controls.switchToMainCall` | +| `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | +| `controlVisibility.consultTransferConsult` | `controls.transfer` | +| `controlVisibility.mergeConferenceConsult` | `controls.mergeToConference` | +| `controlVisibility.endConsult` | `controls.endConsult` | + +#### Before +```typescript +export const createConsultButtons = ( + isMuted, controlVisibility: ControlVisibility, ...handlers +): ButtonConfig[] => { + return [ + { key: 'mute', isVisible: controlVisibility.muteUnmuteConsult.isVisible, + disabled: !controlVisibility.muteUnmuteConsult.isEnabled }, + { key: 'switchToMainCall', tooltip: controlVisibility.isConferenceInProgress ? 'Switch to Conference Call' : 'Switch to Call', + isVisible: controlVisibility.switchToMainCall.isVisible }, + { key: 'transfer', isVisible: controlVisibility.consultTransferConsult.isVisible }, + { key: 'conference', isVisible: controlVisibility.mergeConferenceConsult.isVisible }, + { key: 'cancel', isVisible: controlVisibility.endConsult.isVisible }, + ]; +}; +``` + +#### After +```typescript +export const createConsultButtons = ( + isMuted, controls: TaskUIControls, ...handlers +): ButtonConfig[] => { + const isConferencing = controls.exitConference.isVisible; + return [ + { key: 'mute', isVisible: controls.mute.isVisible, + disabled: !controls.mute.isEnabled }, + { key: 'switchToMainCall', tooltip: isConferencing ? 'Switch to Conference Call' : 'Switch to Call', + isVisible: controls.switchToMainCall.isVisible }, + { key: 'transfer', isVisible: controls.transfer.isVisible }, + { key: 'conference', isVisible: controls.mergeToConference.isVisible }, + { key: 'cancel', isVisible: controls.endConsult.isVisible }, + ]; +}; +``` + +### 4. `CallControlCAD` Widget — Props to Remove + +**File:** `task/src/CallControlCAD/index.tsx` + +This is an **alternative CallControl widget** (CAD = Call Associated Data). It passes `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` from the store to `useCallControl`. These will all be removed when `useCallControlProps` is updated. + +#### Before +```tsx +const { deviceType, featureFlags, isMuted, agentId } = store; +const result = { + ...useCallControl({ + currentTask, onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, + logger, deviceType, featureFlags, isMuted, conferenceEnabled, agentId + }), + // ... +}; +return ; +``` + +#### After +```tsx +const { isMuted } = store; +const result = { + ...useCallControl({ + currentTask, onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, + logger, isMuted + // REMOVED: deviceType, featureFlags, conferenceEnabled, agentId + }), + // ... +}; +return ; +``` + +### 5. `CallControlConsultComponentsProps` — Uses `controlVisibility` + +**File:** `cc-components/.../task/task.types.ts` (line 640) + +```typescript +// OLD +export interface CallControlConsultComponentsProps { + // ... + controlVisibility: ControlVisibility; // → controls: TaskUIControls + // ... +} +``` + +### 6. `ConsultTransferPopoverComponentProps` — Uses `isConferenceInProgress` + +**File:** `cc-components/.../task/task.types.ts` (line 617) + +```typescript +// OLD +export interface ConsultTransferPopoverComponentProps { + // ... + isConferenceInProgress?: boolean; // → derive from controls.exitConference.isVisible + // ... +} +``` + +### 7. `ControlProps` — The Master Interface + +**File:** `cc-components/.../task/task.types.ts` (line 159) + +Has these migration-affected fields: +- `controlVisibility: ControlVisibility` (line 441) → `controls: TaskUIControls` +- `isHeld: boolean` (line 256) → derive from `controls.hold` +- `deviceType: string` (line 251) → REMOVE (SDK handles) +- `featureFlags: {[key: string]: boolean}` (line 389) → REMOVE (SDK handles) +- `conferenceEnabled: boolean` (line 429) → REMOVE (SDK handles) +- `agentId: string` (line 472) → REMOVE from controls (keep for display uses) + +### 8. `CallControlComponentProps` — Picks `controlVisibility` + +**File:** `cc-components/.../task/task.types.ts` (line 475) + +```typescript +// OLD: Picks 'controlVisibility' from ControlProps +export type CallControlComponentProps = Pick; + +// NEW: Replace 'controlVisibility' with 'controls' +// Also can remove some picked props that came from the old approach +``` + +### 9. `filterButtonsForConsultation()` — Uses `consultInitiated` Flag + +**File:** `cc-components/.../call-control.utils.ts` (line 324) + +```typescript +// OLD +export const filterButtonsForConsultation = (buttons, consultInitiated, isTelephony) => { + return consultInitiated && isTelephony + ? buttons.filter((button) => !['hold', 'consult'].includes(button.id)) + : buttons; +}; + +// NEW: derive consultInitiated from controls.endConsult.isVisible +``` + +### 10. `getConsultStatusText()` — Uses `consultInitiated` Flag + +**File:** `cc-components/.../call-control-custom.utils.ts` (line 126) + +```typescript +// OLD +export const getConsultStatusText = (consultInitiated: boolean) => { + return consultInitiated ? 'Consult requested' : 'Consulting'; +}; + +// NEW: derive from controls.endConsult.isVisible && !controls.mergeToConference.isEnabled +``` + +### 11. Files NOT Impacted (Confirmed) + +| File | Reason | +|------|--------| +| `AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only — no control refs | +| `AutoWrapupTimer.utils.ts` | Pure timer formatting — no control refs | +| `consult-transfer-popover-hooks.ts` | Pagination/search logic — no control refs | +| `consult-transfer-list-item.tsx` | Display only — no control refs | +| `consult-transfer-dial-number.tsx` | Input handling — no control refs | +| `consult-transfer-empty-state.tsx` | Display only — no control refs | +| `TaskTimer/index.tsx` | Timer display — no control refs | +| `Task/index.tsx` | Task card display — no control refs | +| `OutdialCall/outdial-call.tsx` | No task controls used | + +--- + +## Files to Modify (Updated) + +| File | Action | Impact | +|------|--------|--------| +| `cc-components/.../task/task.types.ts` | Replace `ControlVisibility` with `TaskUIControls`; update `ControlProps`, `CallControlComponentProps`, `CallControlConsultComponentsProps`, `ConsultTransferPopoverComponentProps` | **HIGH** — central type file | +| `cc-components/.../CallControl/call-control.tsx` | Update to use `controls` prop | **HIGH** | +| `cc-components/.../CallControl/call-control.utils.ts` | Update `buildCallControlButtons()` and `filterButtonsForConsultation()` to use new control names | **HIGH** — 20+ old control references | +| `cc-components/.../CallControlCustom/call-control-custom.utils.ts` | Update `createConsultButtons()` and `getConsultStatusText()` to use new control names | **HIGH** — 6 old control references | +| `cc-components/.../CallControlCustom/call-control-consult.tsx` | Update consult control props | **MEDIUM** | +| `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Update `isConferenceInProgress` prop | **LOW** | +| `cc-components/.../IncomingTask/incoming-task.tsx` | Minor prop updates | **LOW** | +| `cc-components/.../TaskList/task-list.tsx` | Minor prop updates | **LOW** | +| `task/src/CallControlCAD/index.tsx` | Remove `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` from `useCallControl` call | **MEDIUM** | +| All test files for above | Update mocks and assertions | **HIGH** | + +--- + +## Validation Criteria + +- [ ] CallControl renders all 17 controls correctly +- [ ] Consult sub-controls (endConsult, merge, switch) render correctly +- [ ] Conference sub-controls (exit, transfer conference) render correctly +- [ ] State flag derivation works for conditional rendering +- [ ] IncomingTask accept/decline render correctly +- [ ] TaskList per-task controls render correctly +- [ ] CallControlCAD works with simplified props +- [ ] `buildCallControlButtons()` returns correct buttons for all states +- [ ] `createConsultButtons()` returns correct buttons for consult state +- [ ] No TypeScript compilation errors +- [ ] All component tests pass + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/011-execution-plan.md b/ai-docs/migration/011-execution-plan.md new file mode 100644 index 000000000..336d7b280 --- /dev/null +++ b/ai-docs/migration/011-execution-plan.md @@ -0,0 +1,438 @@ +# Migration Doc 011: Execution Plan (Spec-First) + +## Overview + +This plan uses the **spec-driven development** approach already established in CC Widgets. Each milestone starts with writing specs (tests), then implementing to make specs pass. This ensures parity between old and new behavior. + +--- + +## Prerequisites + +- [ ] CC SDK `task-refactor` branch merged and released (or linked locally) +- [ ] `TaskUIControls` type and `task:ui-controls-updated` event available in `@webex/contact-center` +- [ ] `task.uiControls` getter available on `ITask` +- [ ] Team alignment on migration approach + +--- + +## Milestone Overview + +| # | Milestone | Scope | Est. Effort | Risk | Depends On | +|---|-----------|-------|-------------|------|------------| +| M0 | SDK integration setup | Link SDK, verify types | 1 day | Low | SDK release | +| M1 | Types & constants alignment | Import new types, add adapters | 1-2 days | Low | M0 | +| M2 | Store event wiring simplification | Simplify event handlers, add `ui-controls-updated` | 2-3 days | Medium | M0 | +| M3 | Store task-utils thinning | Remove redundant utils | 1-2 days | Low | M2 | +| M3.5 | Timer utils migration | Update timer-utils to accept `TaskUIControls` | 1 day | Low | M3 | +| M4 | CallControl hook refactor | Core: replace `getControlsVisibility` with `task.uiControls` | 3-5 days | **High** | M1, M2, M3, M3.5 | +| M5 | Component layer update | Update `cc-components` prop interfaces | 2-3 days | Medium | M4 | +| M6 | IncomingTask migration | Use `task.uiControls.accept/decline` | 1 day | Low | M1 | +| M7 | TaskList migration | Optional status enhancement | 1 day | Low | M1 | +| M8 | Integration testing & cleanup | E2E, remove dead code, docs | 2-3 days | Medium | All | + +**Total estimated effort: 15–23 days** + +--- + +## Detailed Milestone Plans + +### M0: SDK Integration Setup (1 day) + +**Goal:** Verify the new SDK API is available and types compile. + +**Steps:** +1. Update `@webex/contact-center` dependency to task-refactor version +2. Verify `TaskUIControls` type is importable +3. Verify `task.uiControls` getter exists on `ITask` +4. Verify `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` constant exists +5. Run `yarn build` to confirm no type errors + +**Spec:** Write a minimal integration test that creates a mock task and reads `uiControls`. + +**Validation:** `yarn build` passes with new SDK version. + +--- + +### M1: Types & Constants Alignment (1-2 days) + +**Ref:** [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) + +**Goal:** Import new SDK types into widget packages without changing runtime behavior. + +**Spec first:** +```typescript +// Test: TaskUIControls type compatibility +import { TaskUIControls } from '@webex/contact-center'; +const controls: TaskUIControls = task.uiControls; +expect(controls.hold).toHaveProperty('isVisible'); +expect(controls.hold).toHaveProperty('isEnabled'); +``` + +**Steps:** +1. Add `TaskUIControls` import to `task/src/task.types.ts` +2. Create adapter type mapping old control names → new (for gradual migration) +3. Add `TASK_UI_CONTROLS_UPDATED` to store event constants +4. Review and annotate constants for deprecation + +**Validation:** All existing tests still pass. New types compile. + +--- + +### M2: Store Event Wiring Simplification (2-3 days) + +**Ref:** [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) + +**Goal:** Simplify store event handlers; add `task:ui-controls-updated` subscription. + +**Spec first:** +```typescript +// Test: Store registers ui-controls-updated listener +describe('registerTaskEventListeners', () => { + it('should register TASK_UI_CONTROLS_UPDATED handler', () => { + store.registerTaskEventListeners(mockTask); + expect(mockTask.on).toHaveBeenCalledWith( + TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, expect.any(Function) + ); + }); + + it('should NOT call refreshTaskList on TASK_HOLD', () => { + // Verify simplified handler + }); +}); +``` + +**Steps:** +1. Add `TASK_UI_CONTROLS_UPDATED` handler in `registerTaskEventListeners()` +2. Replace `refreshTaskList()` calls with callback-only for: TASK_HOLD, TASK_RESUME, TASK_CONSULT_END, all conference events +3. Keep `refreshTaskList()` only for: initialization, hydration +4. Update tests for each modified handler + +**Order (low risk → high risk):** +1. Add new `TASK_UI_CONTROLS_UPDATED` handler (additive, no breakage) +2. Simplify conference event handlers (less critical) +3. Simplify hold/resume handlers (medium impact) +4. Simplify consult handlers (medium impact) +5. Remove unnecessary `refreshTaskList()` calls (highest impact) + +**Validation:** All existing widget tests pass. Store correctly fires callbacks on events. + +--- + +### M3: Store Task-Utils Thinning (1-2 days) + +**Ref:** [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) + +**Goal:** Remove utility functions that are now handled by SDK. + +**Spec first:** +```typescript +// Test: Verify no consumers remain for removed functions +// (Static analysis — ensure no import of getConsultStatus, getIsConferenceInProgress, etc.) +``` + +**Steps:** +1. Search codebase for each function to verify consumers +2. Remove functions with zero consumers after M2 changes +3. Mark functions with remaining consumers for later removal (after M4/M5) +4. Keep display-only functions (`getTaskStatus`, `getConferenceParticipants`, etc.) + +**Validation:** Build succeeds. No runtime errors. + +--- + +### M3.5: Timer Utils Migration (1 day) + +**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md#timer-utils-migration) + +**Goal:** Update `calculateStateTimerData()` and `calculateConsultTimerData()` to accept `TaskUIControls`. + +**Why:** These functions accept `controlVisibility` (old shape) as a parameter and derive timer labels from it. They must be migrated before M4 since `useCallControl` depends on them. + +**Spec first:** +```typescript +describe('calculateStateTimerData with TaskUIControls', () => { + it('should return Wrap Up label when controls.wrapup.isVisible', () => { + const controls = { ...getDefaultUIControls(), wrapup: { isVisible: true, isEnabled: true } }; + const result = calculateStateTimerData(mockTask, controls, agentId); + expect(result.label).toBe('Wrap Up'); + }); +}); +``` + +**Steps:** +1. Update `calculateStateTimerData(task, controls, agentId)` signature +2. Replace `controlVisibility.isConsultInitiatedOrAccepted` → `controls.endConsult.isVisible` +3. Replace `controlVisibility.isHeld` → derive from `controls.hold` +4. Update `calculateConsultTimerData(task, controls, agentId)` similarly +5. Update all test cases + +**Also fix during this milestone:** +- Consolidate `findHoldTimestamp` dual signatures (store vs task-util versions) + +--- + +### M4: CallControl Hook Refactor (3-5 days) — CRITICAL PATH + +**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) + +**Goal:** Replace `getControlsVisibility()` with `task.uiControls` in `useCallControl`. + +**Spec first (write ALL specs before implementation):** + +```typescript +describe('useCallControl with task.uiControls', () => { + // Parity specs: each scenario must produce identical control states + + describe('connected voice call', () => { + it('should show hold, mute, end, transfer, consult controls', () => { + mockTask.uiControls = { + hold: { isVisible: true, isEnabled: true }, + mute: { isVisible: true, isEnabled: true }, + end: { isVisible: true, isEnabled: true }, + transfer: { isVisible: true, isEnabled: true }, + consult: { isVisible: true, isEnabled: true }, + // ... all other controls disabled + }; + const { result } = renderHook(() => useCallControl(props)); + expect(result.current.controls.hold).toEqual({ isVisible: true, isEnabled: true }); + }); + }); + + describe('held voice call', () => { + it('should show hold (enabled=true for resume), disable end/mute', () => { /* ... */ }); + }); + + describe('consulting', () => { + it('should show endConsult, switchToMainCall, switchToConsult, mergeToConference', () => { /* ... */ }); + }); + + describe('conferencing', () => { + it('should show exitConference, disable hold', () => { /* ... */ }); + }); + + describe('wrapping up', () => { + it('should show only wrapup control', () => { /* ... */ }); + }); + + describe('digital channel', () => { + it('should show only accept, end, transfer, wrapup', () => { /* ... */ }); + }); + + describe('ui-controls-updated event', () => { + it('should re-render when task emits ui-controls-updated', () => { /* ... */ }); + }); + + describe('no task', () => { + it('should return default controls when no task', () => { /* ... */ }); + }); +}); +``` + +**Steps:** +1. Write comprehensive parity specs (30+ test cases covering all states) +2. Create `adaptSDKControls()` adapter function (maps SDK names to old names if needed during transition) +3. Replace `getControlsVisibility()` call in `useCallControl` with `task.uiControls` +4. Add `task:ui-controls-updated` subscription with `useEffect` +5. Update hook return type to use new control names +6. Remove old state flags from return +7. Run parity specs — fix any mismatches + +**Parity verification approach:** +- For each state (connected, held, consulting, conferencing, wrapping-up, offered): + - Mock task with known data + - Call old `getControlsVisibility()` → capture result + - Read `task.uiControls` → capture result + - Compare: every control must have same `isVisible`/`isEnabled` + - Document and resolve any differences (old bug vs new behavior) + +**Validation:** All 30+ parity specs pass. All existing hook tests pass (with updated assertions). + +--- + +### M5: Component Layer Update (2-3 days) + +**Ref:** [010-component-layer-migration.md](./010-component-layer-migration.md) + +**Goal:** Update `cc-components` to accept new control prop shape. + +**Spec first:** +```typescript +describe('CallControlComponent', () => { + it('should render hold button from controls.hold', () => { + render(); + expect(screen.getByTestId('hold-button')).toBeVisible(); + }); + + it('should hide mute button when controls.mute.isVisible=false', () => { + const controls = { ...mockControls, mute: { isVisible: false, isEnabled: false } }; + render(); + expect(screen.queryByTestId('mute-button')).not.toBeInTheDocument(); + }); +}); +``` + +**Steps:** +1. Update `CallControlComponentProps` to accept `controls: TaskUIControls` +2. Update `CallControlComponent` to read from `controls.*` +3. Update `CallControlConsult` component +4. Update `IncomingTaskComponent` if needed +5. Update all component tests + +**Validation:** All component tests pass. Visual output identical. + +--- + +### M6: IncomingTask Migration (1 day) + +**Ref:** [005-incoming-task-migration.md](./005-incoming-task-migration.md) + +**Goal:** Use `task.uiControls.accept/decline` in IncomingTask. + +**Spec first:** +```typescript +describe('useIncomingTask with uiControls', () => { + it('should derive accept visibility from task.uiControls.accept', () => { /* ... */ }); + it('should derive decline visibility from task.uiControls.decline', () => { /* ... */ }); +}); +``` + +**Steps:** +1. Replace `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` with `task.uiControls` +2. Update component props +3. Update tests + +**Validation:** IncomingTask tests pass. Accept/decline work for voice and digital. + +--- + +### M7: TaskList Migration (1 day) + +**Ref:** [006-task-list-migration.md](./006-task-list-migration.md) + +**Goal:** Optionally enhance task status display; verify compatibility. + +**Steps:** +1. Verify `useTaskList` works with new SDK (should be compatible) +2. Optionally enhance `getTaskStatus()` to use SDK state info +3. Update tests if any changes made + +**Validation:** TaskList renders correctly with all task states. + +--- + +### M8: Integration Testing & Cleanup (2-3 days) + +**Goal:** End-to-end verification, dead code removal, documentation update. + +**Steps:** + +1. **E2E Test Matrix:** + +| Scenario | Widgets Involved | Verify | +|----------|-----------------|--------| +| Incoming voice call → accept → end | IncomingTask, CallControl | Accept button, end button | +| Incoming voice call → reject | IncomingTask | Decline button, RONA timer | +| Connected → hold → resume | CallControl | Hold toggle, timer | +| Connected → consult → end consult | CallControl | Consult flow controls | +| Connected → consult → conference | CallControl | Merge, conference controls | +| Conference → exit | CallControl | Exit conference | +| Conference → transfer conference | CallControl | Transfer conference | +| Connected → transfer (blind) | CallControl | Transfer popover | +| Connected → end → wrapup | CallControl | Wrapup button | +| Outdial → connected → end | OutdialCall, CallControl | Full outdial flow | +| Digital task → accept → end → wrapup | IncomingTask, CallControl | Digital controls | +| Multiple tasks in list | TaskList | Task selection, per-task controls | +| Page refresh → hydrate | All | Restore state correctly | + +2. **Bug fixes (found during analysis):** + - Fix recording callback cleanup mismatch (`TASK_RECORDING_PAUSED` vs `CONTACT_RECORDING_PAUSED`) + - Consolidate `findHoldTimestamp` dual signatures (store vs task-util) + - Add `task:wrapup` race guard if needed + +3. **Dead code removal:** + - Delete `task/src/Utils/task-util.ts` (or reduce to `findHoldTimestamp` only) + - Remove unused store utils + - Remove unused constants + - Remove unused type definitions + +4. **Documentation updates:** + - Update `task/ai-docs/widgets/CallControl/AGENTS.md` and `ARCHITECTURE.md` + - Update `task/ai-docs/widgets/IncomingTask/AGENTS.md` and `ARCHITECTURE.md` + - Update `store/ai-docs/AGENTS.md` and `ARCHITECTURE.md` + - Update `cc-components/ai-docs/AGENTS.md` + +5. **Final validation:** + - `yarn build` — no errors + - `yarn test:unit` — all pass + - `yarn test:styles` — no lint errors + - Sample apps (React + WC) work correctly + +--- + +## Risk Mitigation + +### High-Risk Areas +1. **CallControl hook refactor (M4)** — largest change, most complex logic + - **Mitigation:** Comprehensive parity specs written BEFORE implementation + - **Rollback:** Old `getControlsVisibility()` stays in codebase until M8 cleanup + +2. **Store event wiring (M2)** — removing `refreshTaskList()` could cause stale data + - **Mitigation:** Gradual removal; keep `refreshTaskList()` as fallback initially + - **Rollback:** Re-add `refreshTaskList()` calls if data staleness detected + +3. **Consult/Conference flows** — most complex state transitions + - **Mitigation:** Dedicated parity specs for every consult/conference scenario + - **Mitigation:** Test with Agent Desktop to verify identical behavior + +### Low-Risk Areas +- OutdialCall (no changes needed) +- IncomingTask (minimal changes) +- TaskList (minimal changes) +- Types alignment (additive, no runtime changes) + +--- + +## Spec-First Checklist + +For each milestone, complete these in order: + +1. [ ] Write spec file with test cases for NEW behavior +2. [ ] Write parity tests (old behavior == new behavior) where applicable +3. [ ] Run specs — verify they FAIL (red) +4. [ ] Implement changes +5. [ ] Run specs — verify they PASS (green) +6. [ ] Run ALL existing tests — verify no regressions +7. [ ] `yarn build` — verify compilation +8. [ ] Code review with team +9. [ ] Mark milestone complete + +--- + +## Recommended Order of Execution + +``` +M0 (SDK setup) + │ + ├── M1 (types) ─┐ + └── M2 (store events) ─┤ + │ + M3 (store utils) + │ + M3.5 (timer utils) + │ + M4 (CallControl hook) ← CRITICAL PATH + │ + M5 (components) + │ + ├── M6 (IncomingTask) + └── M7 (TaskList) + │ + M8 (integration + cleanup + bug fixes) +``` + +**M0 → M1 + M2 (parallel) → M3 → M3.5 → M4 → M5 → M6 + M7 (parallel) → M8** + +--- + +_Created: 2026-03-09_ +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md new file mode 100644 index 000000000..09173b25a --- /dev/null +++ b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md @@ -0,0 +1,664 @@ +# Migration Doc 012: Task Lifecycle Flows — Complete Old vs New + +## Purpose + +This document traces **every task scenario from start to finish**, showing exactly what happens at each step in both the old and new approach. Each flow maps: +- User/system action +- SDK event chain +- Widget/store layer behavior +- UI controls shown +- State machine state (new only) + +--- + +## Flow 1: Incoming Voice Call → Accept → Connected + +### Old Flow +``` +1. WebSocket: AgentContactReserved +2. SDK emits: task:incoming (with ITask) +3. Store: handleIncomingTask() → refreshTaskList() → cc.taskManager.getAllTasks() + → runInAction: store.taskList updated, store.incomingTask set +4. Widget: IncomingTask (observer) re-renders +5. Hook: useIncomingTask registers callbacks (TASK_ASSIGNED, TASK_REJECT, etc.) +6. UI Controls: getAcceptButtonVisibility(isBrowser, isPhoneDevice, webRtcEnabled, isCall, isDigitalChannel) + getDeclineButtonVisibility(isBrowser, webRtcEnabled, isCall) +7. User clicks Accept +8. Hook: incomingTask.accept() → SDK API call +9. WebSocket: AgentContactAssigned +10. SDK emits: task:assigned +11. Store: handleTaskAssigned() → refreshTaskList() → update taskList, set currentTask +12. Hook: TASK_ASSIGNED callback fires → onAccepted({task}) +13. Widget: CallControl appears +14. UI Controls: getControlsVisibility() computes all 22 controls from raw task data + → hold, mute, end, transfer, consult visible and enabled +``` + +### New Flow +``` +1. WebSocket: AgentContactReserved +2. SDK: TaskManager maps to TaskEvent.TASK_INCOMING +3. SDK: task.sendStateMachineEvent(TASK_INCOMING) → State: IDLE → OFFERED +4. SDK: computeUIControls(OFFERED, context) → accept/decline visible (WebRTC) +5. SDK emits: task:incoming, task:ui-controls-updated +6. Store: handleIncomingTask() → store.incomingTask set +7. Widget: IncomingTask (observer) re-renders +8. Hook: useIncomingTask reads task.uiControls.accept / task.uiControls.decline +9. User clicks Accept +10. Hook: incomingTask.accept() → SDK API call +11. WebSocket: AgentContactAssigned +12. SDK: TaskManager maps to TaskEvent.ASSIGN +13. SDK: task.sendStateMachineEvent(ASSIGN) → State: OFFERED → CONNECTED +14. SDK: computeUIControls(CONNECTED, context) → hold, mute, end, transfer, consult +15. SDK emits: task:assigned, task:ui-controls-updated +16. Store: handleTaskAssigned() → set currentTask +17. Widget: CallControl appears +18. Hook: useCallControl reads task.uiControls directly (no computation) +``` + +### Key Difference +| Step | Old | New | +|------|-----|-----| +| Controls computation | Widget runs `getControlsVisibility()` on every render | SDK pre-computes `task.uiControls` on every state transition | +| Data freshness | `refreshTaskList()` re-fetches all tasks | SDK updates `task.data` in state machine action | +| Re-render trigger | MobX observable change after `refreshTaskList()` | `task:ui-controls-updated` event | + +--- + +## Flow 2: Incoming Voice Call → Reject / RONA (Timeout) + +### Old Flow +``` +1-6. Same as Flow 1 (incoming → show accept/decline) +7. User clicks Decline (or timer expires → auto-reject) +8. Hook: incomingTask.decline() → SDK API call +9. WebSocket: AgentContactReservedTimeout (RONA) or rejection +10. SDK emits: task:rejected +11. Store: handleTaskReject() → refreshTaskList() → remove task from list +12. Hook: TASK_REJECT callback fires → onRejected({task}) +13. Widget: IncomingTask unmounts +``` + +### New Flow +``` +1-8. Same as Flow 1 new (incoming → OFFERED state) +7. User clicks Decline (or timer expires → auto-reject) +8. Hook: incomingTask.decline() → SDK API call +9. WebSocket: RONA or rejection +10. SDK: task.sendStateMachineEvent(RONA) → State: OFFERED → TERMINATED +11. SDK: computeUIControls(TERMINATED) → all controls disabled +12. SDK emits: task:rejected, task:ui-controls-updated +13. Store: handleTaskReject() → remove task from list +14. Hook: TASK_REJECT callback fires → onRejected({task}) +15. Widget: IncomingTask unmounts +``` + +--- + +## Flow 3: Connected → Hold → Resume + +### Old Flow +``` +1. User clicks Hold +2. Hook: toggleHold(true) → currentTask.hold() +3. SDK: API call to backend +4. WebSocket: AgentContactHeld +5. SDK emits: task:hold +6. Store: refreshTaskList() → cc.taskManager.getAllTasks() → update store.taskList +7. Hook: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) +8. UI Controls: getControlsVisibility() recalculates: + → findHoldStatus(task, 'mainCall', agentId) returns true + → holdResume: { isVisible: true, isEnabled: true } (for resume) + → end: { isVisible: true, isEnabled: false } (disabled while held) + → mute: same +9. User clicks Resume +10. Hook: toggleHold(false) → currentTask.resume() +11. WebSocket: AgentContactUnheld +12. SDK emits: task:resume +13. Store: refreshTaskList() → update store.taskList +14. Hook: TASK_RESUME callback fires → onHoldResume({ isHeld: false }) +15. UI Controls: getControlsVisibility() recalculates → controls back to connected state +``` + +### New Flow +``` +1. User clicks Hold +2. Hook: toggleHold(true) → currentTask.hold() +3. SDK: sends TaskEvent.HOLD_INITIATED → State: CONNECTED → HOLD_INITIATING +4. SDK: computeUIControls(HOLD_INITIATING) → hold visible but transitioning +5. SDK emits: task:ui-controls-updated (optimistic) +6. SDK: API call to backend +7. WebSocket: AgentContactHeld +8. SDK: sends TaskEvent.HOLD_SUCCESS → State: HOLD_INITIATING → HELD +9. SDK: computeUIControls(HELD) → hold visible (for resume), end/mute disabled +10. SDK emits: task:hold, task:ui-controls-updated +11. Store: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) +12. Hook: controls state updated via task:ui-controls-updated listener +13. User clicks Resume +14. Hook: toggleHold(false) → currentTask.resume() +15. SDK: sends TaskEvent.UNHOLD_INITIATED → State: HELD → RESUME_INITIATING +16. SDK: API call to backend +17. WebSocket: AgentContactUnheld +18. SDK: sends TaskEvent.UNHOLD_SUCCESS → State: RESUME_INITIATING → CONNECTED +19. SDK: computeUIControls(CONNECTED) → all active controls enabled +20. SDK emits: task:resume, task:ui-controls-updated +21. Store: TASK_RESUME callback fires +22. Hook: controls state updated +``` + +### Key Difference +| Step | Old | New | +|------|-----|-----| +| Hold initiation | Immediate API call, wait for response | Optimistic: HOLD_INITIATING state before API call | +| Intermediate states | None (binary: held or not) | HOLD_INITIATING, RESUME_INITIATING (UI can show spinner) | +| Controls update | After `refreshTaskList()` + `getControlsVisibility()` | After each state transition via `task:ui-controls-updated` | + +--- + +## Flow 4: Connected → Consult → End Consult + +### Old Flow +``` +1. User initiates consult +2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) +3. SDK: API call to backend +4. WebSocket: AgentConsultCreated +5. SDK emits: task:consultCreated +6. Store: handleConsultCreated() → refreshTaskList() → update taskList +7. UI Controls: getControlsVisibility() recalculates: + → getConsultStatus() returns CONSULT_INITIATED + → endConsult visible, consultTransfer visible, switchToMainCall visible + → hold disabled, transfer hidden +8. WebSocket: AgentConsulting (consult agent answered) +9. SDK emits: task:consulting +10. Store: handleConsulting() → refreshTaskList() +11. UI Controls: getControlsVisibility() recalculates: + → getConsultStatus() returns CONSULT_ACCEPTED + → mergeConference enabled, consultTransfer enabled + → switchToMainCall/switchToConsult available +12. User clicks End Consult +13. Hook: endConsultCall() → currentTask.endConsult(payload) +14. WebSocket: AgentConsultEnded +15. SDK emits: task:consultEnd +16. Store: refreshTaskList() +17. UI Controls: getControlsVisibility() → back to connected state controls +``` + +### New Flow +``` +1. User initiates consult +2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) +3. SDK: sends TaskEvent.CONSULT → State: CONNECTED → CONSULT_INITIATING +4. SDK: computeUIControls(CONSULT_INITIATING) → consult controls transitioning +5. SDK emits: task:ui-controls-updated +6. SDK: API call → success +7. SDK: sends TaskEvent.CONSULT_SUCCESS → stays CONSULT_INITIATING (waiting for agent) +8. WebSocket: AgentConsultCreated → TaskEvent.CONSULT_CREATED +9. SDK: task data updated +10. WebSocket: AgentConsulting → TaskEvent.CONSULTING_ACTIVE +11. SDK: State: CONSULT_INITIATING → CONSULTING +12. SDK: context.consultDestinationAgentJoined = true +13. SDK: computeUIControls(CONSULTING): + → endConsult visible+enabled, mergeToConference visible+enabled + → switchToMainCall visible, switchToConsult visible + → transfer visible (for consult transfer) + → hold disabled (in consult) +14. SDK emits: task:consulting, task:ui-controls-updated +15. Hook: controls updated via listener +16. User clicks End Consult +17. Hook: endConsultCall() → currentTask.endConsult(payload) +18. WebSocket: AgentConsultEnded → TaskEvent.CONSULT_END +19. SDK: State: CONSULTING → CONNECTED (or CONFERENCING if from conference) +20. SDK: context cleared (consultInitiator=false, consultDestinationAgentJoined=false) +21. SDK: computeUIControls(CONNECTED) → back to normal connected controls +22. SDK emits: task:consultEnd, task:ui-controls-updated +23. Hook: controls updated +``` + +### Key Difference +| Step | Old | New | +|------|-----|-----| +| Consult state tracking | `getConsultStatus()` inspects participants | State machine: CONSULT_INITIATING → CONSULTING | +| Agent joined detection | `ConsultStatus.CONSULT_ACCEPTED` from participant flags | `context.consultDestinationAgentJoined` set by action | +| Controls | Computed from raw data every render | Pre-computed on each state transition | + +--- + +## Flow 5: Consulting → Merge to Conference → Exit Conference + +### Old Flow +``` +1. In consulting state (Flow 4 steps 1-11) +2. User clicks Merge Conference +3. Hook: consultConference() → currentTask.consultConference() +4. SDK: API call +5. WebSocket: AgentConsultConferenced / ParticipantJoinedConference +6. SDK emits: task:conferenceStarted / task:participantJoined +7. Store: handleConferenceStarted() → refreshTaskList() +8. UI Controls: getControlsVisibility(): + → task.data.isConferenceInProgress = true + → exitConference visible+enabled + → consult visible+enabled (can add more agents) + → hold disabled (in conference) + → mergeConference hidden (already in conference) +9. User clicks Exit Conference +10. Hook: exitConference() → currentTask.exitConference() +11. WebSocket: ParticipantLeftConference / AgentConsultConferenceEnded +12. SDK emits: task:conferenceEnded / task:participantLeft +13. Store: handleConferenceEnded() → refreshTaskList() +14. UI Controls: getControlsVisibility() → may go to wrapup or connected +``` + +### New Flow +``` +1. In CONSULTING state (Flow 4 new steps 1-15) +2. User clicks Merge Conference +3. Hook: consultConference() → currentTask.consultConference() +4. SDK: sends TaskEvent.MERGE_TO_CONFERENCE → State: CONSULTING → CONF_INITIATING +5. SDK: computeUIControls(CONF_INITIATING) → transitioning controls +6. SDK emits: task:ui-controls-updated +7. WebSocket: AgentConsultConferenced → TaskEvent.CONFERENCE_START +8. SDK: State: CONF_INITIATING → CONFERENCING +9. SDK: computeUIControls(CONFERENCING): + → exitConference visible+enabled + → consult visible (can add more) + → hold disabled + → mergeToConference hidden +10. SDK emits: task:conferenceStarted, task:ui-controls-updated +11. User clicks Exit Conference +12. Hook: exitConference() → currentTask.exitConference() +13. WebSocket: ParticipantLeftConference → TaskEvent.PARTICIPANT_LEAVE +14. SDK: guards check: didCurrentAgentLeaveConference? shouldWrapUp? +15. SDK: State: CONFERENCING → WRAPPING_UP (if wrapup required) or CONNECTED +16. SDK: computeUIControls for new state +17. SDK emits: task:conferenceEnded, task:ui-controls-updated +``` + +--- + +## Flow 6: Connected → End → Wrapup → Complete + +### Old Flow +``` +1. User clicks End +2. Hook: endCall() → currentTask.end() +3. SDK: API call +4. WebSocket: ContactEnded + AgentWrapup +5. SDK emits: task:end, task:wrapup +6. Store: handleTaskEnd() may refresh; handleWrapup → refreshTaskList() +7. UI Controls: getControlsVisibility(): + → getWrapupButtonVisibility(task) → { isVisible: task.data.wrapUpRequired } + → all other controls hidden/disabled +8. Widget: Wrapup form appears +9. User selects reason, clicks Submit +10. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) +11. SDK: API call +12. WebSocket: AgentWrappedup +13. SDK emits: task:wrappedup +14. Store: refreshTaskList() → task removed or updated +15. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) +16. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) +``` + +### New Flow +``` +1. User clicks End +2. Hook: endCall() → currentTask.end() +3. SDK: API call +4. WebSocket: ContactEnded → TaskEvent.CONTACT_ENDED +5. SDK: guards: shouldWrapUp? → State: CONNECTED → WRAPPING_UP +6. SDK: computeUIControls(WRAPPING_UP) → only wrapup visible+enabled +7. SDK emits: task:end, task:wrapup, task:ui-controls-updated +8. Hook: controls updated → only wrapup control shown +9. Widget: Wrapup form appears +10. User selects reason, clicks Submit +11. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) +12. SDK: API call +13. WebSocket: AgentWrappedup → TaskEvent.WRAPUP_COMPLETE +14. SDK: State: WRAPPING_UP → COMPLETED +15. SDK: computeUIControls(COMPLETED) → all controls disabled +16. SDK: emitTaskWrappedup action → cleanupResources action +17. SDK emits: task:wrappedup, task:ui-controls-updated, task:cleanup +18. Store: handleTaskEnd/cleanup → remove task from list +19. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) +20. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) +``` + +--- + +## Flow 7: Auto-Wrapup Timer + +### Old Flow +``` +1. Task enters wrapup state (Flow 6 steps 1-7) +2. Hook: useEffect detects currentTask.autoWrapup && controlVisibility.wrapup +3. Hook: reads currentTask.autoWrapup.getTimeLeftSeconds() +4. Hook: setInterval every 1s → decrements secondsUntilAutoWrapup +5. Widget: AutoWrapupTimer component shows countdown +6. If user clicks Cancel: cancelAutoWrapup() → currentTask.cancelAutoWrapupTimer() +7. If timer reaches 0: SDK auto-submits wrapup with default reason +``` + +### New Flow +``` +1. Task enters WRAPPING_UP state (Flow 6 new steps 1-8) +2. Hook: useEffect detects currentTask.autoWrapup && controls.wrapup.isVisible + (changed: controlVisibility.wrapup → controls.wrapup) +3-7. Same as old — auto-wrapup is a widget-layer timer concern, SDK unchanged +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Timer trigger condition | `controlVisibility.wrapup` (computed by widget) | `controls.wrapup.isVisible` (from SDK) | +| Timer behavior | Unchanged | Unchanged | +| Cancel action | Unchanged | Unchanged | + +--- + +## Flow 8: Recording Pause/Resume + +### Old Flow +``` +1. User clicks Pause Recording +2. Hook: toggleRecording() → currentTask.pauseRecording() +3. SDK: API call +4. WebSocket: ContactRecordingPaused +5. SDK emits: task:recordingPaused +6. Store: fires callback +7. Hook: pauseRecordingCallback() → setIsRecording(false), onRecordingToggle({isRecording: false}) +8. UI Controls: getPauseResumeRecordingButtonVisibility() unchanged (still visible) +9. Widget: Button label changes to "Resume Recording" +``` + +### New Flow +``` +1. User clicks Pause Recording +2. Hook: toggleRecording() → currentTask.pauseRecording() +3. SDK: sends TaskEvent.PAUSE_RECORDING → context.recordingInProgress = false +4. SDK: computeUIControls → recording: { isVisible: true, isEnabled: false } + (visible but disabled = recording paused) +5. SDK: API call +6. WebSocket: ContactRecordingPaused +7. SDK emits: task:recordingPaused, task:ui-controls-updated +8. Hook: setIsRecording(false), controls updated +9. Widget: Button label changes to "Resume Recording" +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Recording state tracking | Widget local state (`isRecording`) | SDK context (`recordingInProgress`) + widget local state | +| Recording button visibility | `getPauseResumeRecordingButtonVisibility()` | `controls.recording` from SDK | +| Recording indicator | Separate `recordingIndicator` control | Merged into `recording` control | + +--- + +## Flow 9: Blind Transfer + +### Old Flow +``` +1. User clicks Transfer, selects destination +2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) +3. SDK: API call +4. WebSocket: AgentBlindTransferred +5. SDK emits: (leads to task:end or task:wrapup depending on config) +6. Store: refreshTaskList() +7. UI Controls: getControlsVisibility() → wrapup or all disabled +``` + +### New Flow +``` +1. User clicks Transfer, selects destination +2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) +3. SDK: API call +4. WebSocket: AgentBlindTransferred → TaskEvent.TRANSFER_SUCCESS +5. SDK: guards: shouldWrapUpOrIsInitiator? +6. SDK: State → WRAPPING_UP (if wrapup required) or stays CONNECTED +7. SDK: computeUIControls for new state +8. SDK emits: task:end/task:wrapup, task:ui-controls-updated +``` + +--- + +## Flow 10: Digital Task (Chat/Email) — Accept → End → Wrapup + +### Old Flow +``` +1. WebSocket: AgentContactReserved (mediaType: chat) +2. SDK emits: task:incoming +3. Store: handleIncomingTask() +4. UI Controls: getAcceptButtonVisibility(): + → isBrowser && isDigitalChannel → accept visible + → decline NOT visible (digital channels) +5. User clicks Accept +6. incomingTask.accept() → task:assigned +7. UI Controls: getControlsVisibility(): + → end visible, transfer visible, wrapup hidden + → hold/mute/consult/conference/recording: all hidden (digital) +8. User clicks End +9. currentTask.end() → task:end, task:wrapup +10. UI Controls: wrapup visible (if wrapUpRequired) +``` + +### New Flow +``` +1. WebSocket: AgentContactReserved (mediaType: chat) +2. SDK: State: IDLE → OFFERED, channelType: DIGITAL +3. SDK: computeDigitalUIControls(OFFERED) → accept visible, decline hidden +4. SDK emits: task:incoming, task:ui-controls-updated +5. User clicks Accept +6. incomingTask.accept() → ASSIGN → State: OFFERED → CONNECTED +7. SDK: computeDigitalUIControls(CONNECTED) → end visible, transfer visible + → hold/mute/consult/conference/recording: all disabled (digital) +8. User clicks End +9. currentTask.end() → CONTACT_ENDED → WRAPPING_UP +10. SDK: computeDigitalUIControls(WRAPPING_UP) → wrapup visible +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Channel detection | Widget checks `mediaType === 'telephony'` vs chat/email | SDK checks `channelType: VOICE` vs `DIGITAL` | +| Digital controls | `getControlsVisibility()` with `isCall=false, isDigitalChannel=true` | `computeDigitalUIControls()` — much simpler logic | +| Controls shown | Same end result | Same end result (accept, end, transfer, wrapup only) | + +--- + +## Flow 11: Page Refresh → Hydration + +### Old Flow +``` +1. Agent refreshes browser page +2. SDK reconnects, receives AgentContact for active task +3. SDK emits: task:hydrate +4. Store: handleTaskHydrate() → refreshTaskList() → cc.taskManager.getAllTasks() +5. Store: sets currentTask, taskList from fetched data +6. Widgets: observer re-renders with restored task +7. UI Controls: getControlsVisibility() computes from raw task data + → Must correctly derive: held state, consult state, conference state + → Error-prone: depends on raw interaction data being complete +``` + +### New Flow +``` +1. Agent refreshes browser page +2. SDK reconnects, receives AgentContact for active task +3. SDK: TaskManager sends TaskEvent.HYDRATE with task data +4. SDK: State machine guards determine correct state: + → isInteractionTerminated? → WRAPPING_UP + → isInteractionConsulting? → CONSULTING + → isInteractionHeld? → HELD + → isInteractionConnected? → CONNECTED + → isConferencingByParticipants? → CONFERENCING + → default → IDLE (data update only) +5. SDK: computeUIControls for resolved state → correct controls for restored state +6. SDK emits: task:hydrate, task:ui-controls-updated +7. Store: handleTaskHydrate() → set currentTask, taskList +8. Widgets: observer re-renders; controls from task.uiControls are correct +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| State recovery | `refreshTaskList()` + `getControlsVisibility()` from raw data | State machine guards determine correct state | +| Reliability | Can show wrong controls if interaction data is incomplete | Guards explicitly check each condition; predictable | +| Conference recovery | Depends on `isConferenceInProgress` flag in data | Guard: `isConferencingByParticipants` counts agents | + +--- + +## Flow 12: Outdial → New Task + +### Old Flow +``` +1. User enters number, clicks Dial +2. Hook: startOutdial(destination, origin) → cc.startOutdial(destination, origin) +3. SDK: API call (CC-level, not task-level) +4. Backend creates task, sends AgentContactReserved +5. Flow continues as Flow 1 (Incoming → Accept → Connected) +``` + +### New Flow +``` +Identical — outdial initiation is CC-level. Once the task is created, +the state machine takes over and flows follow Flow 1 new approach. +No changes needed. +``` + +--- + +## Flow 13: Consult Transfer (from Consulting State) + +### Old Flow +``` +1. In consulting state (Flow 4 steps 1-11) +2. User clicks Consult Transfer +3. Hook: consultTransfer() checks currentTask.data.isConferenceInProgress: + → false: currentTask.consultTransfer() + → true: currentTask.transferConference() +4. SDK: API call +5. WebSocket: AgentConsultTransferred / AgentConferenceTransferred +6. SDK emits: task events (leads to end/wrapup) +7. Store: refreshTaskList() +8. UI Controls: wrapup or all disabled +``` + +### New Flow +``` +1. In CONSULTING state (Flow 4 new steps 1-15) +2. User clicks Transfer (transfer control now handles consult transfer) +3. Hook: consultTransfer() checks controls.transferConference.isVisible: + → false: currentTask.consultTransfer() + → true: currentTask.transferConference() +4. SDK: API call +5. WebSocket: → TaskEvent.TRANSFER_SUCCESS or TRANSFER_CONFERENCE_SUCCESS +6. SDK: State → WRAPPING_UP or TERMINATED +7. SDK: computeUIControls for new state +8. SDK emits: events + task:ui-controls-updated +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Conference check | `currentTask.data.isConferenceInProgress` | `controls.transferConference.isVisible` (or keep data check) | +| Transfer button | Separate `consultTransferConsult` control | Unified `transfer` control handles all transfer types | + +--- + +## Flow 14: Switch Between Main Call and Consult Call + +### Old Flow +``` +1. In consulting state, agent is on consult leg +2. User clicks "Switch to Main Call" +3. Hook: switchToMainCall(): + → currentTask.resume(findMediaResourceId(task, 'consult')) + (resumes consult media → puts consult on hold → main call active) +4. WebSocket: AgentContactHeld (consult) + AgentContactUnheld (main) +5. SDK emits: task:hold, task:resume +6. Store: refreshTaskList() (twice) +7. UI Controls: getControlsVisibility(): + → consultCallHeld = true → switchToConsult visible + → switchToMainCall hidden +``` + +### New Flow +``` +1. In CONSULTING state, agent is on consult leg +2. User clicks "Switch to Main Call" +3. Hook: switchToMainCall(): + → currentTask.resume(findMediaResourceId(task, 'consult')) +4. SDK: HOLD_INITIATED / UNHOLD_INITIATED → state machine tracks +5. SDK: context.consultCallHeld updated +6. SDK: computeUIControls(CONSULTING, updated context): + → switchToConsult visible (consult is now held) + → switchToMainCall hidden +7. SDK emits: task:hold, task:resume, task:ui-controls-updated +8. Hook: controls updated via listener +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| consultCallHeld tracking | `findHoldStatus(task, 'consult', agentId)` | `context.consultCallHeld` in state machine | +| Controls update | After 2x `refreshTaskList()` | Single `task:ui-controls-updated` after state settles | + +--- + +## State Machine States → Widget Controls Summary + +| TaskState (New) | Old Equivalent | Controls Visible | +|-----------------|---------------|-----------------| +| `IDLE` | No task / before incoming | All disabled | +| `OFFERED` | Incoming task shown | accept, decline (WebRTC voice); accept only (digital) | +| `CONNECTED` | Active call | hold, mute, end, transfer, consult, recording | +| `HOLD_INITIATING` | (no equivalent) | hold visible (transitioning) | +| `HELD` | `isHeld = true` | hold (for resume), transfer, consult | +| `RESUME_INITIATING` | (no equivalent) | hold visible (transitioning) | +| `CONSULT_INITIATING` | `ConsultStatus.CONSULT_INITIATED` | endConsult, switchToMainCall, switchToConsult | +| `CONSULTING` | `ConsultStatus.CONSULT_ACCEPTED` | endConsult, mergeToConference, transfer, switchToMainCall/Consult | +| `CONF_INITIATING` | (no equivalent) | conference transitioning | +| `CONFERENCING` | `isConferenceInProgress = true` | exitConference, consult, transferConference | +| `WRAPPING_UP` | `wrapUpRequired && interaction terminated` | wrapup only | +| `COMPLETED` | Task removed after wrapup | All disabled | +| `TERMINATED` | Task rejected / ended without wrapup | All disabled | + +--- + +## Event Chain Mapping: Old Widget Events → New SDK State Machine + +| Widget Action | Old Event Chain | New Event Chain | +|--------------|----------------|-----------------| +| Accept task | `accept()` → `task:assigned` | `accept()` → `ASSIGN` → `task:assigned` + `task:ui-controls-updated` | +| Decline task | `decline()` → `task:rejected` | `decline()` → `RONA` → `task:rejected` + `task:ui-controls-updated` | +| Hold | `hold()` → `task:hold` | `hold()` → `HOLD_INITIATED` → `HOLD_SUCCESS` → `task:hold` + `task:ui-controls-updated` | +| Resume | `resume()` → `task:resume` | `resume()` → `UNHOLD_INITIATED` → `UNHOLD_SUCCESS` → `task:resume` + `task:ui-controls-updated` | +| End call | `end()` → `task:end` | `end()` → `CONTACT_ENDED` → `task:end` + `task:ui-controls-updated` | +| Wrapup | `wrapup()` → `task:wrappedup` | `wrapup()` → `WRAPUP_COMPLETE` → `task:wrappedup` + `task:ui-controls-updated` | +| Transfer | `transfer()` → `task:end` | `transfer()` → `TRANSFER_SUCCESS` → `task:end` + `task:ui-controls-updated` | +| Start consult | `consult()` → `task:consultCreated` | `consult()` → `CONSULT` → `CONSULT_SUCCESS` → `CONSULTING_ACTIVE` → `task:consulting` + `task:ui-controls-updated` | +| End consult | `endConsult()` → `task:consultEnd` | `endConsult()` → `CONSULT_END` → `task:consultEnd` + `task:ui-controls-updated` | +| Merge conference | `consultConference()` → `task:conferenceStarted` | `consultConference()` → `MERGE_TO_CONFERENCE` → `CONFERENCE_START` → `task:conferenceStarted` + `task:ui-controls-updated` | +| Exit conference | `exitConference()` → `task:conferenceEnded` | `exitConference()` → `PARTICIPANT_LEAVE` / `CONFERENCE_END` → `task:conferenceEnded` + `task:ui-controls-updated` | +| Pause recording | `pauseRecording()` → `task:recordingPaused` | `pauseRecording()` → `PAUSE_RECORDING` → `task:recordingPaused` + `task:ui-controls-updated` | +| Resume recording | `resumeRecording()` → `task:recordingResumed` | `resumeRecording()` → `RESUME_RECORDING` → `task:recordingResumed` + `task:ui-controls-updated` | + +### Universal New Pattern +Every action now follows: **User action → SDK method → State machine event(s) → task.uiControls recomputed → `task:ui-controls-updated` emitted**. Widgets never need to compute controls themselves. + +--- + +## Timer Utils: Old vs New Control References + +**File:** `packages/contact-center/task/src/Utils/timer-utils.ts` + +| Old Reference in Timer Utils | New Equivalent | +|-----------------------------|---------------| +| `controlVisibility.wrapup?.isVisible` | `controls.wrapup.isVisible` | +| `controlVisibility.consultCallHeld` | `controls.switchToConsult.isVisible` | +| `controlVisibility.isConsultInitiated` | `controls.endConsult.isVisible && !controls.mergeToConference.isEnabled` | + +--- + +_Created: 2026-03-09_ +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/013-file-inventory-old-control-references.md b/ai-docs/migration/013-file-inventory-old-control-references.md new file mode 100644 index 000000000..9e33abe18 --- /dev/null +++ b/ai-docs/migration/013-file-inventory-old-control-references.md @@ -0,0 +1,162 @@ +# Migration Doc 013: Complete File Inventory — Old Control References + +## Purpose + +This is the definitive inventory of **every file** in CC Widgets that references old task control names, state flags, or the `ControlVisibility` type. Use this as a checklist during migration to ensure nothing is missed. + +--- + +## Summary + +| Category | Files with Old Refs | Files Unaffected | +|----------|-------------------|-----------------| +| Widget hooks (`task/src/`) | 4 | 1 (`index.ts`) | +| Widget utils (`task/src/Utils/`) | 4 | 0 | +| Widget entry points (`task/src/*/index.tsx`) | 3 | 2 (`OutdialCall`, `IncomingTask`) | +| cc-components types | 1 (central type file) | 0 | +| cc-components utils | 2 | 3+ (unaffected) | +| cc-components components | 4 | 8+ (unaffected) | +| Store | 3 | 1 (`store.ts`) | +| **Total files to modify** | **~21** | **~15 unaffected** | + +--- + +## Files WITH Old Control References (Must Migrate) + +### Tier 1: Core Logic Files (Highest Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 1 | `task/src/Utils/task-util.ts` | `getControlsVisibility()` — the entire function (~650 lines). Computes all 22 controls + 7 state flags. Calls `getConsultStatus`, `findHoldStatus`, `getIsConferenceInProgress`, etc. | [Doc 002](./002-ui-controls-migration.md) | +| 2 | `task/src/helper.ts` (`useCallControl`) | `controlVisibility = useMemo(() => getControlsVisibility(...))`, references `controlVisibility.muteUnmute`, `controlVisibility.wrapup`, passes to `calculateStateTimerData`, `calculateConsultTimerData` | [Doc 004](./004-call-control-hook-migration.md) | +| 3 | `store/src/storeEventsWrapper.ts` | `refreshTaskList()` in 15+ event handlers; missing `TASK_UI_CONTROLS_UPDATED` subscription | [Doc 003](./003-store-event-wiring-migration.md) | +| 4 | `store/src/task-utils.ts` | `getConsultStatus()`, `findHoldStatus()`, `getIsConferenceInProgress()`, `getConferenceParticipantsCount()`, `getIsCustomerInCall()`, `getIsConsultInProgress()` — all used by `getControlsVisibility()` | [Doc 008](./008-store-task-utils-migration.md) | + +### Tier 2: Component Utility Files (High Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 5 | `cc-components/.../task/task.types.ts` | `ControlVisibility` interface (22 controls + 7 flags), `ControlProps.controlVisibility`, `CallControlComponentProps` picks `controlVisibility`, `CallControlConsultComponentsProps.controlVisibility`, `ConsultTransferPopoverComponentProps.isConferenceInProgress`, `ControlProps.isHeld`, `ControlProps.deviceType`, `ControlProps.featureFlags` | [Doc 009](./009-types-and-constants-migration.md), [Doc 010](./010-component-layer-migration.md) | +| 6 | `cc-components/.../call-control.utils.ts` | `buildCallControlButtons()` — 20+ references: `controlVisibility.muteUnmute`, `.holdResume`, `.isHeld`, `.consult`, `.transfer`, `.isConferenceInProgress`, `.consultTransfer`, `.mergeConference`, `.pauseResumeRecording`, `.exitConference`, `.end`, `.switchToConsult`. Also `filterButtonsForConsultation(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | +| 7 | `cc-components/.../call-control-custom.utils.ts` | `createConsultButtons()` — `controlVisibility.muteUnmuteConsult`, `.switchToMainCall`, `.isConferenceInProgress`, `.consultTransferConsult`, `.mergeConferenceConsult`, `.endConsult`. Also `getConsultStatusText(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | + +### Tier 3: Component Files (Medium Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 8 | `cc-components/.../CallControl/call-control.tsx` | Receives `controlVisibility` as prop, passes to `buildCallControlButtons()`, `createConsultButtons()`, `filterButtonsForConsultation()` | [Doc 010](./010-component-layer-migration.md) | +| 9 | `cc-components/.../CallControlCustom/call-control-consult.tsx` | Receives `controlVisibility` from `CallControlConsultComponentsProps`, passes to `createConsultButtons()` | [Doc 010](./010-component-layer-migration.md) | +| 10 | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Receives `isConferenceInProgress` prop | [Doc 010](./010-component-layer-migration.md) | + +### Tier 4: Widget Entry Points (Medium Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 11 | `task/src/CallControl/index.tsx` | Passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` from store to `useCallControl` | [Doc 004](./004-call-control-hook-migration.md) | +| 12 | `task/src/CallControlCAD/index.tsx` | Same as #11 — passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 010](./010-component-layer-migration.md) | +| 13 | `task/src/TaskList/index.tsx` | Passes `deviceType` from store for `isBrowser` computation | [Doc 006](./006-task-list-migration.md) | + +### Tier 5: Utility Files (Low-Medium Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 14 | `task/src/Utils/timer-utils.ts` | `calculateStateTimerData(task, controlVisibility, agentId)` — uses `controlVisibility.wrapup`, `.consultCallHeld`, `.isConsultInitiated` | [Doc 004](./004-call-control-hook-migration.md#timer-utils-migration) | +| 15 | `task/src/Utils/useHoldTimer.ts` | Uses `findHoldTimestamp` from task-util.ts (dual signature issue) — NOT a control visibility reference, but part of consolidation | [Doc 008](./008-store-task-utils-migration.md) | +| 16 | `task/src/task.types.ts` | `useCallControlProps` interface — includes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 009](./009-types-and-constants-migration.md) | +| 17 | `task/src/Utils/constants.ts` | Timer label constants — no control refs, but check for unused consult state constants | [Doc 009](./009-types-and-constants-migration.md) | + +### Tier 6: Store Constants (Low Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 18 | `store/src/constants.ts` | `TASK_STATE_CONSULT`, `TASK_STATE_CONSULTING`, `CONSULT_STATE_INITIATED`, `CONSULT_STATE_COMPLETED`, etc. — used by `getConsultStatus()` | [Doc 009](./009-types-and-constants-migration.md) | +| 19 | `store/src/store.ts` | `refreshTaskList()` method, `isDeclineButtonEnabled` observable | [Doc 003](./003-store-event-wiring-migration.md) | + +### Tier 7: Test Files (Must Update After Implementation) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 20 | `task/tests/**` | All `useCallControl` tests mock `getControlsVisibility()` return | [Doc 011](./011-execution-plan.md) | +| 21 | `cc-components/tests/**` | All CallControl component tests mock `controlVisibility` prop | [Doc 011](./011-execution-plan.md) | +| 22 | `store/tests/**` | Tests for event handlers, `refreshTaskList()`, task-utils | [Doc 011](./011-execution-plan.md) | + +--- + +## Files WITHOUT Old Control References (No Migration Needed) + +| File | Reason | +|------|--------| +| `task/src/OutdialCall/index.tsx` | CC-level API, no task controls | +| `task/src/IncomingTask/index.tsx` | Minimal — accept/decline passed through, not computed here | +| `task/src/index.ts` | Re-exports only | +| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only | +| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.utils.ts` | Pure timer formatting | +| `cc-components/.../CallControlCustom/consult-transfer-popover-hooks.ts` | Pagination/search logic | +| `cc-components/.../CallControlCustom/consult-transfer-list-item.tsx` | Display only | +| `cc-components/.../CallControlCustom/consult-transfer-dial-number.tsx` | Input handling | +| `cc-components/.../CallControlCustom/consult-transfer-empty-state.tsx` | Display only | +| `cc-components/.../TaskTimer/index.tsx` | Timer display | +| `cc-components/.../Task/index.tsx` | Task card display | +| `cc-components/.../Task/task.utils.ts` | Task data extraction for display | +| `cc-components/.../TaskList/task-list.utils.ts` | Task list data formatting | +| `cc-components/.../OutdialCall/outdial-call.tsx` | No task controls | +| `cc-components/.../IncomingTask/incoming-task.utils.tsx` | Incoming task display utils | +| `cc-components/.../constants.ts` | UI string constants | +| `cc-components/.../OutdialCall/constants.ts` | Outdial constants | + +--- + +## Old Control Name → File Reference Matrix + +This shows exactly which files reference each old control name: + +| Old Control Name | Files That Reference It | +|------------------|------------------------| +| `muteUnmute` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `muteUnmuteConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | +| `holdResume` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `pauseResumeRecording` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `recordingIndicator` | `task-util.ts`, `task.types.ts` | +| `mergeConference` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `consultTransferConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | +| `mergeConferenceConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | +| `isConferenceInProgress` | `task-util.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `task.types.ts`, `consult-transfer-popover.tsx` | +| `isConsultInitiated` | `task-util.ts`, `call-control.utils.ts`, `timer-utils.ts`, `task.types.ts` | +| `isConsultInitiatedAndAccepted` | `task-util.ts`, `task.types.ts` | +| `isConsultReceived` | `task-util.ts`, `task.types.ts` | +| `isConsultInitiatedOrAccepted` | `task-util.ts`, `helper.ts`, `timer-utils.ts`, `task.types.ts` | +| `isHeld` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `consultCallHeld` | `task-util.ts`, `timer-utils.ts`, `task.types.ts` | +| `controlVisibility` (param name) | `helper.ts`, `timer-utils.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `call-control.tsx`, `call-control-consult.tsx`, `task.types.ts` | +| `ControlVisibility` (type) | `task.types.ts` (definition), `call-control.utils.ts`, `call-control-custom.utils.ts` (imports) | + +--- + +## Migration Execution Order by File + +Based on dependencies: + +``` +1. task.types.ts (cc-components) — Define new TaskUIControls prop, keep ControlVisibility during transition +2. task/src/task.types.ts — Import TaskUIControls, update useCallControlProps +3. store/src/constants.ts — Mark deprecated constants +4. store/src/task-utils.ts — Remove redundant functions +5. store/src/storeEventsWrapper.ts — Add TASK_UI_CONTROLS_UPDATED, simplify handlers +6. task/src/Utils/timer-utils.ts — Accept TaskUIControls instead of ControlVisibility +7. task/src/Utils/task-util.ts — DELETE or reduce to findHoldTimestamp only +8. task/src/helper.ts — Replace getControlsVisibility() with task.uiControls +9. call-control.utils.ts — Update buildCallControlButtons() to new control names +10. call-control-custom.utils.ts — Update createConsultButtons() to new control names +11. call-control.tsx — Update to accept controls: TaskUIControls +12. call-control-consult.tsx — Update consult component props +13. consult-transfer-popover.tsx — Update isConferenceInProgress derivation +14. CallControl/index.tsx — Remove old props from useCallControl call +15. CallControlCAD/index.tsx — Remove old props from useCallControl call +16. TaskList/index.tsx — Remove deviceType usage +17. All test files — Update mocks and assertions +``` + +--- + +_Created: 2026-03-09_ +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/014-task-code-scan-report.md b/ai-docs/migration/014-task-code-scan-report.md new file mode 100644 index 000000000..584f69e27 --- /dev/null +++ b/ai-docs/migration/014-task-code-scan-report.md @@ -0,0 +1,356 @@ +# Migration Doc 014: Task-Related Code Scan Report + +**Generated:** 2025-03-09 +**Scope:** Complete scan of CC Widgets repository for task-related code + +--- + +## 1. `packages/contact-center/task/src/helper.ts` — ALL HOOKS + +### useTaskList (lines 30–145) +- **Store callbacks:** `setTaskAssigned`, `setTaskRejected`, `setTaskSelected` +- **SDK methods:** `task.accept()`, `task.decline()` +- **Store methods:** `store.setCurrentTask(task, true)` +- **Migration:** Callbacks registered in useEffect with empty deps; task methods called directly on ITask + +### useIncomingTask (lines 147–281) +- **setTaskCallback usage:** TASK_ASSIGNED, TASK_CONSULT_ACCEPTED, TASK_END, TASK_REJECT, TASK_CONSULT_END +- **removeTaskCallback:** Same events in cleanup +- **SDK methods:** `incomingTask.accept()`, `incomingTask.decline()` +- **Migration:** Per-task event subscriptions; must migrate to new event model + +### useCallControl (lines 283–728) +- **setTaskCallback usage:** TASK_HOLD, TASK_RESUME, TASK_END, AGENT_WRAPPEDUP, TASK_RECORDING_PAUSED, TASK_RECORDING_RESUMED +- **removeTaskCallback:** TASK_HOLD, TASK_RESUME, TASK_END, AGENT_WRAPPEDUP, CONTACT_RECORDING_PAUSED, CONTACT_RECORDING_RESUMED (note: mismatch — uses CONTACT_* in cleanup but TASK_* in setup) +- **SDK methods on currentTask:** + - `hold()`, `resume()`, `hold(mediaResourceId)`, `resume(mediaResourceId)` + - `end()`, `wrapup({wrapUpReason, auxCodeId})` + - `pauseRecording()`, `resumeRecording({autoResumed})` + - `toggleMute()` + - `transfer({to, destinationType})` + - `consult(consultPayload)` + - `consultConference()`, `exitConference()` + - `endConsult(consultEndPayload)` + - `consultTransfer()`, `transferConference()` + - `cancelAutoWrapupTimer()` +- **Store imports:** `getConferenceParticipants`, `findMediaResourceId` from @webex/cc-store +- **Migration:** Heavy task API usage; all task methods and event subscriptions need migration + +### useOutdialCall (lines 731–813) +- **Store:** `store.taskList`, `store.cc.startOutdial()`, `cc.getOutdialAniEntries()`, `cc.addressBook.getEntries()` +- **Task check:** `Object.values(taskList).some(task => task?.data?.interaction?.mediaType === MEDIA_TYPE_TELEPHONY_LOWER)` +- **Migration:** Minimal task usage; mainly checks taskList for telephony presence + +--- + +## 2. `packages/contact-center/store/src/storeEventsWrapper.ts` — TASK EVENT HANDLERS + +### registerTaskEventListeners (lines 416–451) +Registers on each task: +- TASK_END → handleTaskEnd +- TASK_ASSIGNED → handleTaskAssigned +- AGENT_OFFER_CONTACT → refreshTaskList +- AGENT_CONSULT_CREATED → handleConsultCreated +- TASK_CONSULT_QUEUE_CANCELLED → handleConsultQueueCancelled +- TASK_REJECT → handleTaskReject (with task) +- TASK_OUTDIAL_FAILED → handleOutdialFailed +- AGENT_WRAPPEDUP → refreshTaskList +- TASK_CONSULTING → handleConsulting +- TASK_CONSULT_ACCEPTED → handleConsultAccepted +- TASK_OFFER_CONSULT → handleConsultOffer +- TASK_AUTO_ANSWERED → handleAutoAnswer +- TASK_CONSULT_END → refreshTaskList +- TASK_HOLD, TASK_RESUME → refreshTaskList +- TASK_CONFERENCE_ENDED → handleConferenceEnded +- TASK_CONFERENCE_END_FAILED, TASK_CONFERENCE_ESTABLISHING, TASK_CONFERENCE_FAILED → refreshTaskList +- TASK_PARTICIPANT_JOINED → handleConferenceStarted +- TASK_PARTICIPANT_LEFT → handleConferenceEnded +- TASK_PARTICIPANT_LEFT_FAILED → refreshTaskList +- TASK_CONFERENCE_STARTED → handleConferenceStarted +- TASK_CONFERENCE_TRANSFERRED → handleConferenceEnded +- TASK_CONFERENCE_TRANSFER_FAILED → refreshTaskList +- TASK_POST_CALL_ACTIVITY → refreshTaskList +- TASK_MEDIA (browser only) → handleTaskMedia + +### handleTaskEnd (lines 344–347) +- setIsDeclineButtonEnabled(false) +- refreshTaskList() + +### handleTaskAssigned (lines 349–359) +- Calls onTaskAssigned if set +- setCurrentTask(task) +- setState({developerName: ENGAGED_LABEL, name: ENGAGED_USERNAME}) + +### handleIncomingTask (lines 453–467) +- Calls registerTaskEventListeners(task) +- If onIncomingTask && !taskList[task.data.interactionId]: onIncomingTask({task}), handleTaskMuteState(task) +- refreshTaskList() + +### handleConsultCreated (lines 368–371) +- refreshTaskList() +- setConsultStartTimeStamp(Date.now()) + +### handleConsulting (lines 373–376) +- refreshTaskList() +- setConsultStartTimeStamp(Date.now()) + +### handleConsultAccepted (lines 393–406) +- refreshTaskList() +- setConsultStartTimeStamp(Date.now()) +- setState(ENGAGED) +- If browser: task.on(TASK_EVENTS.TASK_MEDIA, handleTaskMedia) + +### handleConsultOffer (lines 378–380) +- refreshTaskList() + +### handleConferenceStarted (lines 412–419) +- setIsQueueConsultInProgress(false) +- setCurrentConsultQueueId(null) +- setConsultStartTimeStamp(null) +- refreshTaskList() + +### handleConferenceEnded (lines 421–423) +- refreshTaskList() + +### refreshTaskList (lines 262–281) +- taskList = cc.taskManager.getAllTasks() +- If empty: handleTaskRemove(currentTask), setCurrentTask(null), setState({reset: true}) +- Else if currentTask in list: setCurrentTask(taskList[currentTask.data.interactionId]) +- Else: handleTaskRemove(currentTask), setCurrentTask(taskList[taskListKeys[0]]) + +### setTaskCallback / removeTaskCallback (lines 354–384) +- setTaskCallback: task.on(event, callback) — task from taskList[taskId] +- removeTaskCallback: task.off(event, callback) + +### setupIncomingTaskHandler (lines 603–668) +- CC events: TASK_HYDRATE, TASK_INCOMING, TASK_MERGED +- handleTaskHydrate, handleIncomingTask, handleTaskMerged + +### handleTaskHydrate (lines 494–528) +- registerTaskEventListeners(task) +- refreshTaskList() +- setCurrentTask(task) +- Consult/wrapup state handling + +### handleTaskMerged (lines 488–492) +- registerTaskEventListeners(task) +- refreshTaskList() + +--- + +## 3. `packages/contact-center/store/src/task-utils.ts` — UTILITY FUNCTIONS + +| Function | Purpose | +|----------|---------| +| `isIncomingTask(task, agentId)` | Determines if task is incoming (new/consult/connected/conference, !wrapUpRequired, !hasJoined) | +| `getConsultMPCState(task, agentId)` | Consult state derivation | +| `isSecondaryAgent(task)` | Secondary agent in consult | +| `isSecondaryEpDnAgent(task)` | Secondary EP-DN agent | +| `getTaskStatus(task, agentId)` | Task status string | +| `getConsultStatus(task, agentId)` | ConsultStatus enum | +| `getIsConferenceInProgress(task)` | Conference in progress check | +| `getConferenceParticipants(task, agentId)` | Active conference participants (excl. agent) | +| `getConferenceParticipantsCount(task)` | Participant count | +| `getIsCustomerInCall(task)` | Customer in call check | +| `getIsConsultInProgress(task)` | Consult in progress | +| `isInteractionOnHold(task)` | Any media on hold | +| `setmTypeForEPDN(task, mType)` | mType adjustment for EP-DN | +| `findMediaResourceId(task, mType)` | Media resource ID lookup | +| `findHoldStatus(task, mType, agentId)` | Hold status for media | +| `findHoldTimestamp(task, mType)` | Hold timestamp (task, mType) | + +**Note:** Store `findHoldTimestamp(task, mType)` vs task package `findHoldTimestamp(interaction, mType)` — different signatures. + +--- + +## 4. `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` + +- **Props:** currentTask, toggleHold, toggleRecording, toggleMute, wrapupCall, controlVisibility, secondsUntilAutoWrapup, cancelAutoWrapup, etc. +- **Task usage:** `currentTask.data.interaction`, `currentTask.autoWrapup`, `updateCallStateFromTask(currentTask, setIsRecording)` +- **Pattern:** Presentational; receives all handlers from hook + +--- + +## 5. `packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts` + +- **updateCallStateFromTask(currentTask, setIsRecording):** Reads `currentTask.data.interaction.callProcessingDetails.isPaused` +- **buildCallControlButtons:** Uses controlVisibility, no direct task access +- **filterButtonsForConsultation:** Filters hold/consult when consult initiated + telephony + +--- + +## 6. `packages/contact-center/cc-components/src/components/task/task.types.ts` + +- **TaskProps, ControlProps, CallControlComponentProps:** ITask, currentTask, incomingTask, taskList +- **TaskListItemData, TaskComponentData:** Task-derived display types +- **TASK_EVENTS** not in this file (in store.types.ts) + +--- + +## 7. `packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx` + +- Uses `extractIncomingTaskData(incomingTask, ...)` +- Renders Task with accept/reject callbacks +- No direct SDK/store access + +--- + +## 8. `packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx` + +- Uses `extractTaskListItemData`, `getTasksArray`, `createTaskSelectHandler`, `isCurrentTaskSelected` +- Imports `isIncomingTask` from @webex/cc-store +- Renders Task for each task in taskList + +--- + +## 9. `packages/contact-center/task/src/Utils/timer-utils.ts` + +- **calculateStateTimerData(currentTask, controlVisibility, agentId):** Wrap-up/post-call timer from participant +- **calculateConsultTimerData(currentTask, controlVisibility, agentId):** Consult timer; uses `findHoldTimestamp(currentTask, 'consult')` from @webex/cc-store +- **Imports:** ITask, findHoldTimestamp from @webex/cc-store + +--- + +## 10. `packages/contact-center/task/src/Utils/useHoldTimer.ts` + +- **Imports:** findHoldTimestamp from `./task-util` (task package) +- **Signature:** findHoldTimestamp(interaction, mType) — passes `currentTask.data.interaction` +- **Uses:** Web Worker for hold elapsed time +- **Note:** Task package findHoldTimestamp(interaction, mType) vs store findHoldTimestamp(task, mType) + +--- + +## 11. CallControlCustom/ + +| File | Task Usage | +|------|------------| +| consult-transfer-popover.tsx | No direct task; receives buddyAgents, getQueues, etc. | +| consult-transfer-popover-hooks.ts | Paginated data; no task | +| call-control-consult.tsx | consultTimerLabel, consultTimerTimestamp, controlVisibility — from props | +| call-control-custom.utils.ts | ControlVisibility, ButtonConfig; no task | +| consult-transfer-list-item.tsx | No task | +| consult-transfer-dial-number.tsx | No task | +| consult-transfer-empty-state.tsx | No task | + +--- + +## 12. Task/ + +| File | Task Usage | +|------|------------| +| index.tsx | Props: interactionId, title, state, startTimeStamp, ronaTimeout, acceptTask, declineTask, onTaskSelect | +| task.utils.ts | extractTaskComponentData; no direct task object | + +--- + +## 13. TaskTimer/ + +- **Props:** startTimeStamp, countdown, ronaTimeout +- Web Worker for elapsed/countdown display +- No ITask reference + +--- + +## 14. AutoWrapupTimer/ + +- **Props:** secondsUntilAutoWrapup, allowCancelAutoWrapup, handleCancelWrapup +- getTimerUIState(secondsUntilAutoWrapup) +- No ITask reference + +--- + +## 15. `packages/contact-center/store/src/store.ts` + +- **Observables:** currentTask, taskList +- **init(options, setupEventListeners):** Passes setupIncomingTaskHandler +- No direct task logic + +--- + +## 16. `packages/contact-center/store/src/store.types.ts` + +- **TASK_EVENTS** enum (lines 168–207) +- **CC_EVENTS** enum +- **IStore:** currentTask, taskList +- **ITask** from @webex/contact-center + +--- + +## 17. `packages/contact-center/store/src/constants.ts` + +- Task/consult state constants: TASK_STATE_CONSULT, TASK_STATE_CONSULTING, INTERACTION_STATE_*, CONSULT_STATE_*, etc. +- EXCLUDED_PARTICIPANT_TYPES, MEDIA_TYPE_CONSULT + +--- + +## 18. `packages/contact-center/task/src/Utils/task-util.ts` + +- **findHoldTimestamp(interaction, mType):** Task package version — takes interaction +- **getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled):** Full control visibility +- Imports from @webex/cc-store: getConsultStatus, getIsConsultInProgress, getIsCustomerInCall, getConferenceParticipantsCount, findHoldStatus + +--- + +## 19. `widgets-samples/cc/samples-cc-react-app/` + +- **App.tsx:** IncomingTask, TaskList, CallControl; onIncomingTaskCB, onAccepted, onRejected, onTaskAccepted, onTaskDeclined, onTaskSelected; store.currentTask, store.setIncomingTaskCb +- **EngageWidget.tsx:** store.currentTask, mediaType, isSupportedTask +- **Task usage:** currentTask, taskList, incomingTasks state, task.data.interactionId + +--- + +## CRITICAL MIGRATION PATTERNS + +### 1. findHoldTimestamp Signature Mismatch +- **Store (task-utils.ts):** `findHoldTimestamp(task: ITask, mType: string)` +- **Task package (task-util.ts):** `findHoldTimestamp(interaction: Interaction, mType: string)` +- **useHoldTimer** uses task package version with `currentTask.data.interaction` +- **timer-utils.ts** uses store version with `currentTask` + +### 2. useCallControl Event Cleanup Mismatch (BUG) +- Setup: TASK_RECORDING_PAUSED, TASK_RECORDING_RESUMED +- Cleanup: CONTACT_RECORDING_PAUSED, CONTACT_RECORDING_RESUMED +- **Bug:** Cleanup uses wrong event names — callbacks are never removed; should use TASK_RECORDING_* in cleanup to match setup + +### 3. SDK Method Calls (require migration) +- task.accept(), task.decline() +- task.hold(), task.resume(), task.hold(id), task.resume(id) +- task.end(), task.wrapup(), task.pauseRecording(), task.resumeRecording() +- task.toggleMute() +- task.transfer(), task.consult(), task.endConsult() +- task.consultConference(), task.exitConference(), task.consultTransfer(), task.transferConference() +- task.cancelAutoWrapupTimer() + +### 4. Event Subscriptions +- Per-task: task.on(TASK_EVENTS.*, callback) +- CC-level: ccSDK.on(TASK_EVENTS.TASK_INCOMING, ...), TASK_HYDRATE, TASK_MERGED + +### 5. Store Task Flow +- refreshTaskList → cc.taskManager.getAllTasks() +- setCurrentTask uses isIncomingTask(task, agentId) to skip incoming +- handleTaskRemove unregisters all task listeners + +--- + +## FILES SUMMARY + +| Location | Task-Related | +|----------|--------------| +| task/src/helper.ts | ✅ All 4 hooks | +| store/storeEventsWrapper.ts | ✅ All handlers | +| store/task-utils.ts | ✅ All utilities | +| store/store.ts | ✅ currentTask, taskList | +| store/store.types.ts | ✅ TASK_EVENTS, ITask | +| store/constants.ts | ✅ Task state constants | +| task/src/Utils/task-util.ts | ✅ findHoldTimestamp, getControlsVisibility | +| task/src/Utils/timer-utils.ts | ✅ Timer computation | +| task/src/Utils/useHoldTimer.ts | ✅ Hold timer | +| cc-components CallControl/* | ✅ Props, utils | +| cc-components Task/* | ✅ Display | +| cc-components TaskList/* | ✅ Utils, isIncomingTask | +| cc-components IncomingTask/* | ✅ Utils | +| samples-cc-react-app | ✅ Full usage | + +--- + +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ From 45a1bd66e35a56bbf729dce7d60c836551c6b70a Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 10 Mar 2026 12:52:39 +0530 Subject: [PATCH 02/18] docs(ai-docs): address codex review comments --- ai-docs/migration/001-migration-overview.md | 2 +- ai-docs/migration/002-ui-controls-migration.md | 6 +++--- ai-docs/migration/003-store-event-wiring-migration.md | 2 +- ai-docs/migration/004-call-control-hook-migration.md | 8 ++++---- ai-docs/migration/009-types-and-constants-migration.md | 9 +++++---- ai-docs/migration/010-component-layer-migration.md | 8 ++++---- ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md | 2 +- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/ai-docs/migration/001-migration-overview.md b/ai-docs/migration/001-migration-overview.md index fd9840bf2..1dac577c5 100644 --- a/ai-docs/migration/001-migration-overview.md +++ b/ai-docs/migration/001-migration-overview.md @@ -130,7 +130,7 @@ Should be consolidated to one function during migration. ### UIControlConfig Is Built by SDK (Not by Widgets) -Widgets do NOT need to provide `UIControlConfig`. The SDK builds it internally from agent profile, `callProcessingDetails`, media type, and voice variant. This means `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` **can be removed** from `useCallControlProps` — they are only used for `getControlsVisibility()` which is being eliminated. +Widgets do NOT need to provide `UIControlConfig`. The SDK builds it internally from agent profile, `callProcessingDetails`, media type, and voice variant. This means `deviceType`, `featureFlags`, and `conferenceEnabled` **can be removed** from `useCallControlProps` — they are only used for `getControlsVisibility()` which is being eliminated. **Note:** `agentId` must be retained because it is also used by timer utilities (`calculateStateTimerData`, `calculateConsultTimerData`) to look up the agent's participant record from `interaction.participants`. ### Timer Utils Dependency on `controlVisibility` diff --git a/ai-docs/migration/002-ui-controls-migration.md b/ai-docs/migration/002-ui-controls-migration.md index 0e5f1886f..b6511256d 100644 --- a/ai-docs/migration/002-ui-controls-migration.md +++ b/ai-docs/migration/002-ui-controls-migration.md @@ -103,8 +103,8 @@ The largest single change in this migration. CC Widgets currently computes all c | `end` | `end` | Same | | `muteUnmute` | `mute` | **Renamed** | | `holdResume` | `hold` | **Renamed** (hold state still togglable) | -| `pauseResumeRecording` | `recording` | **Renamed** | -| `recordingIndicator` | `recording` | **Merged** into `recording` control | +| `pauseResumeRecording` | `recording` | **Renamed** — toggle button (pause/resume) | +| `recordingIndicator` | `recording` | **Maps to same SDK control** — but widget must keep a separate UI indicator (status badge). Use `recording.isVisible` for the indicator badge and `recording.isEnabled` for the toggle button's interactive state. See note below. | | `transfer` | `transfer` | Same | | `conference` | `conference` | Same | | `exitConference` | `exitConference` | Same | @@ -112,7 +112,7 @@ The largest single change in this migration. CC Widgets currently computes all c | `consult` | `consult` | Same | | `endConsult` | `endConsult` | Same | | `consultTransfer` | `consultTransfer` | Same (always hidden in new SDK) | -| `consultTransferConsult` | `transfer` | **Removed** — transfer button handles consult transfer | +| `consultTransferConsult` | `transfer` / `transferConference` | **Split** — `transfer` for consult transfer, `transferConference` for conference transfer | | `mergeConferenceConsult` | `mergeToConference` | **Merged** into `mergeToConference` | | `muteUnmuteConsult` | `mute` | **Merged** into `mute` | | `switchToMainCall` | `switchToMainCall` | Same | diff --git a/ai-docs/migration/003-store-event-wiring-migration.md b/ai-docs/migration/003-store-event-wiring-migration.md index 05bf58330..33231eb43 100644 --- a/ai-docs/migration/003-store-event-wiring-migration.md +++ b/ai-docs/migration/003-store-event-wiring-migration.md @@ -74,7 +74,7 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be | `TASK_MEDIA` | `handleTaskMedia` | **Keep** (browser WebRTC setup) | | `TASK_UI_CONTROLS_UPDATED` | **NEW** — `handleUIControlsUpdated` | **Add** — trigger widget re-renders | | `TASK_WRAPUP` | `handleWrapup` | **Simplify** — no need to refresh | -| `TASK_WRAPPEDUP` | `handleWrappedup` | **Simplify** — no need to refresh | +| `AGENT_WRAPPEDUP` | `handleWrappedup` | **Simplify** — no need to refresh | | `TASK_HOLD` | Fire callback only | **Simplify** — no `refreshTaskList()` | | `TASK_RESUME` | Fire callback only | **Simplify** — no `refreshTaskList()` | | `TASK_CONSULT_*` | Fire callback only | **Simplify** — SDK manages state | diff --git a/ai-docs/migration/004-call-control-hook-migration.md b/ai-docs/migration/004-call-control-hook-migration.md index 6a60f63c6..3fb5b33c5 100644 --- a/ai-docs/migration/004-call-control-hook-migration.md +++ b/ai-docs/migration/004-call-control-hook-migration.md @@ -99,8 +99,8 @@ | `end` | `controls.end` | Nested under `controls` | | `muteUnmute` | `controls.mute` | **Renamed** + nested | | `holdResume` | `controls.hold` | **Renamed** + nested | -| `pauseResumeRecording` | `controls.recording` | **Renamed** + nested | -| `recordingIndicator` | `controls.recording` | **Merged** with recording | +| `pauseResumeRecording` | `controls.recording` | **Renamed** — toggle button (pause/resume) | +| `recordingIndicator` | `controls.recording` | **Same SDK control** — widget must keep separate UI for recording status badge vs toggle. Use `recording.isVisible` for badge, `recording.isEnabled` for toggle interactivity | | `transfer` | `controls.transfer` | Nested | | `conference` | `controls.conference` | Nested | | `exitConference` | `controls.exitConference` | Nested | @@ -108,7 +108,7 @@ | `consult` | `controls.consult` | Nested | | `endConsult` | `controls.endConsult` | Nested | | `consultTransfer` | `controls.consultTransfer` | Nested (always hidden in new) | -| `consultTransferConsult` | `controls.transfer` | **Removed** — use `transfer` | +| `consultTransferConsult` | `controls.transfer` / `controls.transferConference` | **Split** — `transfer` for consult transfer, `transferConference` for conference transfer | | `mergeConferenceConsult` | `controls.mergeToConference` | **Merged** | | `muteUnmuteConsult` | `controls.mute` | **Merged** | | `switchToMainCall` | `controls.switchToMainCall` | Nested | @@ -337,7 +337,7 @@ Widgets do NOT need to provide UIControlConfig. The SDK builds it from: - Voice/WebRTC layer → `voiceVariant` (pstn/webrtc) - `taskManager.setAgentId()` → `agentId` -This means `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` props can be removed from `useCallControlProps`. +This means `deviceType`, `featureFlags`, and `conferenceEnabled` props can be removed from `useCallControlProps`. **Note:** `agentId` must be retained — it is still required by `calculateStateTimerData()` and `calculateConsultTimerData()` to look up the agent's participant record from `interaction.participants`. ### 9. `task:wrapup` Race Condition diff --git a/ai-docs/migration/009-types-and-constants-migration.md b/ai-docs/migration/009-types-and-constants-migration.md index 7646ef5b8..121741906 100644 --- a/ai-docs/migration/009-types-and-constants-migration.md +++ b/ai-docs/migration/009-types-and-constants-migration.md @@ -63,7 +63,7 @@ CC Widgets defines its own types for control visibility, task state, and constan | `TASK_EVENTS.TASK_END` | `TASK_EVENTS.TASK_END` | Same | | — | `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | **NEW** | | `TASK_EVENTS.TASK_WRAPUP` | `TASK_EVENTS.TASK_WRAPUP` | Same | -| `TASK_EVENTS.TASK_WRAPPEDUP` | `TASK_EVENTS.TASK_WRAPPEDUP` | Same | +| `TASK_EVENTS.AGENT_WRAPPEDUP` | `TASK_EVENTS.AGENT_WRAPPEDUP` | Same | | All consult/conference events | Same event names | Same | ### Media Type Constants @@ -104,7 +104,7 @@ export interface useCallControlProps { currentTask: ITask; deviceType: string; // Used for control visibility computation featureFlags: {[key: string]: boolean}; // Used for control visibility - agentId: string; // Used for control visibility + agentId: string; // Used for control visibility AND timer participant lookup conferenceEnabled: boolean; // Used for control visibility isMuted: boolean; logger: ILogger; @@ -124,8 +124,9 @@ import {ITask, TaskUIControls} from '@webex/contact-center'; export interface useCallControlProps { currentTask: ITask; - // REMOVED: deviceType, featureFlags, agentId, conferenceEnabled + // REMOVED: deviceType, featureFlags, conferenceEnabled // (SDK computes controls via UIControlConfig, set at task creation) + agentId: string; // RETAINED — still needed by timer utils for participant lookup isMuted: boolean; logger: ILogger; onHoldResume?: (data: any) => void; @@ -218,7 +219,7 @@ export const MEDIA_TYPE_CONSULT = 'consult'; // Used by findMediaResourceId - `isRecordingEnabled` — from `callProcessingDetails.pauseResumeEnabled` - `agentId` — from `taskManager.setAgentId()` -This means the widget no longer needs to pass `deviceType`, `featureFlags`, or `agentId` for control computation. +This means the widget no longer needs to pass `deviceType`, `featureFlags`, or `conferenceEnabled` for control computation. **Note:** `agentId` is retained — it is still needed by timer utilities for participant lookup. --- diff --git a/ai-docs/migration/010-component-layer-migration.md b/ai-docs/migration/010-component-layer-migration.md index edb49cf98..e4fe26733 100644 --- a/ai-docs/migration/010-component-layer-migration.md +++ b/ai-docs/migration/010-component-layer-migration.md @@ -17,10 +17,10 @@ The `cc-components` package contains the presentational React components for tas |----------|----------|--------| | `holdResume` | `hold` | **Rename** | | `muteUnmute` | `mute` | **Rename** | -| `pauseResumeRecording` | `recording` | **Rename** | -| `recordingIndicator` | `recording` | **Merge** with recording control | +| `pauseResumeRecording` | `recording` | **Rename** — toggle button (pause/resume) | +| `recordingIndicator` | `recording` | **Same SDK control** — widget must preserve separate recording status badge UI. Use `recording.isVisible` for badge, `recording.isEnabled` for toggle | | `mergeConference` | `mergeToConference` | **Rename** | -| `consultTransferConsult` | — | **Remove** (use `transfer`) | +| `consultTransferConsult` | `transfer` / `transferConference` | **Split** — use `transfer` for consult transfer, `transferConference` for conference transfer | | `mergeConferenceConsult` | — | **Remove** (use `mergeToConference`) | | `muteUnmuteConsult` | — | **Remove** (use `mute`) | | `isConferenceInProgress` | — | **Remove** (derive from controls) | @@ -359,7 +359,7 @@ This function builds the consult-mode button array. It references **5 old contro | `controlVisibility.muteUnmuteConsult` | `controls.mute` | | `controlVisibility.switchToMainCall` | `controls.switchToMainCall` | | `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | -| `controlVisibility.consultTransferConsult` | `controls.transfer` | +| `controlVisibility.consultTransferConsult` | `controls.transfer` (consult) / `controls.transferConference` (conference) | | `controlVisibility.mergeConferenceConsult` | `controls.mergeToConference` | | `controlVisibility.endConsult` | `controls.endConsult` | diff --git a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md index 09173b25a..72a403a78 100644 --- a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md +++ b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md @@ -561,7 +561,7 @@ No changes needed. | Aspect | Old | New | |--------|-----|-----| | Conference check | `currentTask.data.isConferenceInProgress` | `controls.transferConference.isVisible` (or keep data check) | -| Transfer button | Separate `consultTransferConsult` control | Unified `transfer` control handles all transfer types | +| Transfer button | Separate `consultTransferConsult` control | `controls.transfer` (consult transfer) / `controls.transferConference` (conference transfer) | --- From 28397c24b4e1b8961d833863159f727fea1de26a Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 10 Mar 2026 14:54:34 +0530 Subject: [PATCH 03/18] docs(ai-docs): address codex second review comments --- .../008-store-task-utils-migration.md | 5 +++- .../010-component-layer-migration.md | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/ai-docs/migration/008-store-task-utils-migration.md b/ai-docs/migration/008-store-task-utils-migration.md index f83d8be71..fd654c091 100644 --- a/ai-docs/migration/008-store-task-utils-migration.md +++ b/ai-docs/migration/008-store-task-utils-migration.md @@ -178,7 +178,10 @@ export function getTaskStatus(task: ITask): string { if (controls.wrapup.isVisible) return 'Wrap Up'; if (controls.endConsult.isVisible) return 'Consulting'; if (controls.exitConference.isVisible) return 'Conference'; - if (controls.hold.isVisible && !controls.hold.isEnabled) return 'Held'; + // NOTE: Do NOT derive held state from controls.hold.isEnabled — hold can be + // disabled in consult/transition states even when call is not held. + // Use task data instead: + if (findHoldStatus(task, 'mainCall', agentId)) return 'Held'; if (controls.end.isVisible) return 'Connected'; if (controls.accept.isVisible) return 'Offered'; return 'Unknown'; diff --git a/ai-docs/migration/010-component-layer-migration.md b/ai-docs/migration/010-component-layer-migration.md index e4fe26733..453d533a3 100644 --- a/ai-docs/migration/010-component-layer-migration.md +++ b/ai-docs/migration/010-component-layer-migration.md @@ -232,10 +232,12 @@ const isConferenceInProgress = controls.exitConference.isVisible; // New: derive from controls const isConsulting = controls.endConsult.isVisible; -// Old: isHeld (boolean prop) -// New: derive from hold control -const isHeld = controls.hold.isVisible && !controls.hold.isEnabled; -// (Note: hold is visible but disabled when held + in conference) +// Old: isHeld (boolean state flag from getControlsVisibility) +// New: derive from task data, NOT from control enabled state +// IMPORTANT: Do NOT use `controls.hold.isEnabled` to determine held state — +// hold can be disabled in consult/transition states even when call is not held. +const isHeld = findHoldStatus(currentTask, 'mainCall', agentId); +// (Uses task.data.interaction.participants to check actual hold state) ``` --- @@ -291,7 +293,7 @@ This function builds the main call control button array. It references **12 old |--------------|---------------| | `controlVisibility.muteUnmute.isVisible` | `controls.mute.isVisible` | | `controlVisibility.switchToConsult.isEnabled` | `controls.switchToConsult.isEnabled` | -| `controlVisibility.isHeld` | Derive: `controls.hold.isVisible && !controls.hold.isEnabled` | +| `controlVisibility.isHeld` | Derive from task data: `findHoldStatus(task, 'mainCall', agentId)` — do NOT derive from `controls.hold.isEnabled` | | `controlVisibility.holdResume.isEnabled` | `controls.hold.isEnabled` | | `controlVisibility.holdResume.isVisible` | `controls.hold.isVisible` | | `controlVisibility.consult.isEnabled` | `controls.consult.isEnabled` | @@ -335,7 +337,7 @@ export const buildCallControlButtons = ( controls: TaskUIControls, // NEW type from SDK ...handlers ): CallControlButton[] => { - const isHeld = controls.hold.isVisible && !controls.hold.isEnabled; + const isHeld = findHoldStatus(currentTask, 'mainCall', agentId); // from task data, NOT control state const isConferencing = controls.exitConference.isVisible; return [ { id: 'mute', isVisible: controls.mute.isVisible, ... }, @@ -402,7 +404,7 @@ export const createConsultButtons = ( **File:** `task/src/CallControlCAD/index.tsx` -This is an **alternative CallControl widget** (CAD = Call Associated Data). It passes `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` from the store to `useCallControl`. These will all be removed when `useCallControlProps` is updated. +This is an **alternative CallControl widget** (CAD = Call Associated Data). It passes `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` from the store to `useCallControl`. When `useCallControlProps` is updated, `deviceType`, `featureFlags`, and `conferenceEnabled` will be removed. `agentId` is retained — it is still needed for timer participant lookup. #### Before ```tsx @@ -423,8 +425,8 @@ const { isMuted } = store; const result = { ...useCallControl({ currentTask, onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, - logger, isMuted - // REMOVED: deviceType, featureFlags, conferenceEnabled, agentId + logger, isMuted, agentId // agentId RETAINED — needed for timer participant lookup + // REMOVED: deviceType, featureFlags, conferenceEnabled }), // ... }; @@ -467,7 +469,7 @@ Has these migration-affected fields: - `deviceType: string` (line 251) → REMOVE (SDK handles) - `featureFlags: {[key: string]: boolean}` (line 389) → REMOVE (SDK handles) - `conferenceEnabled: boolean` (line 429) → REMOVE (SDK handles) -- `agentId: string` (line 472) → REMOVE from controls (keep for display uses) +- `agentId: string` (line 472) → RETAIN (needed for timer participant lookup, not just controls) ### 8. `CallControlComponentProps` — Picks `controlVisibility` @@ -537,7 +539,7 @@ export const getConsultStatusText = (consultInitiated: boolean) => { | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Update `isConferenceInProgress` prop | **LOW** | | `cc-components/.../IncomingTask/incoming-task.tsx` | Minor prop updates | **LOW** | | `cc-components/.../TaskList/task-list.tsx` | Minor prop updates | **LOW** | -| `task/src/CallControlCAD/index.tsx` | Remove `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` from `useCallControl` call | **MEDIUM** | +| `task/src/CallControlCAD/index.tsx` | Remove `deviceType`, `featureFlags`, `conferenceEnabled` from `useCallControl` call (retain `agentId` for timers) | **MEDIUM** | | All test files for above | Update mocks and assertions | **HIGH** | --- From 3bd613f97f0cf0e8bf312f7f4ed01b88db179088 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 10 Mar 2026 16:34:01 +0530 Subject: [PATCH 04/18] docs(ai-docs): address codex third review comments --- ai-docs/migration/003-store-event-wiring-migration.md | 8 ++++++-- ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md | 4 ++-- .../013-file-inventory-old-control-references.md | 7 ++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ai-docs/migration/003-store-event-wiring-migration.md b/ai-docs/migration/003-store-event-wiring-migration.md index 33231eb43..49ec16b03 100644 --- a/ai-docs/migration/003-store-event-wiring-migration.md +++ b/ai-docs/migration/003-store-event-wiring-migration.md @@ -74,7 +74,7 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be | `TASK_MEDIA` | `handleTaskMedia` | **Keep** (browser WebRTC setup) | | `TASK_UI_CONTROLS_UPDATED` | **NEW** — `handleUIControlsUpdated` | **Add** — trigger widget re-renders | | `TASK_WRAPUP` | `handleWrapup` | **Simplify** — no need to refresh | -| `AGENT_WRAPPEDUP` | `handleWrappedup` | **Simplify** — no need to refresh | +| `AGENT_WRAPPEDUP` | `handleWrappedup` | **Keep refresh or add explicit task removal** — task must be removed from `taskList`/`currentTask` after wrapup completion to prevent stale UI | | `TASK_HOLD` | Fire callback only | **Simplify** — no `refreshTaskList()` | | `TASK_RESUME` | Fire callback only | **Simplify** — no `refreshTaskList()` | | `TASK_CONSULT_*` | Fire callback only | **Simplify** — SDK manages state | @@ -158,7 +158,11 @@ registerTaskEventListeners(task: ITask) { // SIMPLIFIED: Events that only need callback firing (SDK keeps task.data in sync) task.on(TASK_EVENTS.TASK_HOLD, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_HOLD, interactionId)); task.on(TASK_EVENTS.TASK_RESUME, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RESUME, interactionId)); - task.on(TASK_EVENTS.AGENT_WRAPPEDUP, (data) => this.fireTaskCallbacks(TASK_EVENTS.AGENT_WRAPPEDUP, interactionId, data)); + // AGENT_WRAPPEDUP: still needs task cleanup — refresh or explicitly remove from taskList/currentTask + task.on(TASK_EVENTS.AGENT_WRAPPEDUP, (data) => { + this.refreshTaskList(); // retain: task must be removed from list after wrapup completion + this.fireTaskCallbacks(TASK_EVENTS.AGENT_WRAPPEDUP, interactionId, data); + }); task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_PAUSED, interactionId)); task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_RESUMED, interactionId)); // ... consult/conference events: fire callbacks only, no refreshTaskList() diff --git a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md index 72a403a78..5bd1d25b8 100644 --- a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md +++ b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md @@ -376,8 +376,8 @@ This document traces **every task scenario from start to finish**, showing exact 1. User clicks Pause Recording 2. Hook: toggleRecording() → currentTask.pauseRecording() 3. SDK: sends TaskEvent.PAUSE_RECORDING → context.recordingInProgress = false -4. SDK: computeUIControls → recording: { isVisible: true, isEnabled: false } - (visible but disabled = recording paused) +4. SDK: computeUIControls → recording: { isVisible: true, isEnabled: true } + (visible and enabled = agent can click to resume recording) 5. SDK: API call 6. WebSocket: ContactRecordingPaused 7. SDK emits: task:recordingPaused, task:ui-controls-updated diff --git a/ai-docs/migration/013-file-inventory-old-control-references.md b/ai-docs/migration/013-file-inventory-old-control-references.md index 9e33abe18..5579486ee 100644 --- a/ai-docs/migration/013-file-inventory-old-control-references.md +++ b/ai-docs/migration/013-file-inventory-old-control-references.md @@ -12,7 +12,7 @@ This is the definitive inventory of **every file** in CC Widgets that references |----------|-------------------|-----------------| | Widget hooks (`task/src/`) | 4 | 1 (`index.ts`) | | Widget utils (`task/src/Utils/`) | 4 | 0 | -| Widget entry points (`task/src/*/index.tsx`) | 3 | 2 (`OutdialCall`, `IncomingTask`) | +| Widget entry points (`task/src/*/index.tsx`) | 4 | 1 (`OutdialCall`) | | cc-components types | 1 (central type file) | 0 | | cc-components utils | 2 | 3+ (unaffected) | | cc-components components | 4 | 8+ (unaffected) | @@ -55,6 +55,7 @@ This is the definitive inventory of **every file** in CC Widgets that references | 11 | `task/src/CallControl/index.tsx` | Passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` from store to `useCallControl` | [Doc 004](./004-call-control-hook-migration.md) | | 12 | `task/src/CallControlCAD/index.tsx` | Same as #11 — passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 010](./010-component-layer-migration.md) | | 13 | `task/src/TaskList/index.tsx` | Passes `deviceType` from store for `isBrowser` computation | [Doc 006](./006-task-list-migration.md) | +| 13b | `task/src/IncomingTask/index.tsx` | Passes `deviceType` from store to `useIncomingTask` — migrate to `task.uiControls.accept`/`decline` | [Doc 005](./005-incoming-task-migration.md) | ### Tier 5: Utility Files (Low-Medium Impact) @@ -87,7 +88,6 @@ This is the definitive inventory of **every file** in CC Widgets that references | File | Reason | |------|--------| | `task/src/OutdialCall/index.tsx` | CC-level API, no task controls | -| `task/src/IncomingTask/index.tsx` | Minimal — accept/decline passed through, not computed here | | `task/src/index.ts` | Re-exports only | | `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only | | `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.utils.ts` | Pure timer formatting | @@ -153,7 +153,8 @@ Based on dependencies: 14. CallControl/index.tsx — Remove old props from useCallControl call 15. CallControlCAD/index.tsx — Remove old props from useCallControl call 16. TaskList/index.tsx — Remove deviceType usage -17. All test files — Update mocks and assertions +17. IncomingTask/index.tsx — Remove deviceType, migrate to task.uiControls +18. All test files — Update mocks and assertions ``` --- From 606dcbfd213401dba891b8a936667196a5a14055 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 10 Mar 2026 17:32:30 +0530 Subject: [PATCH 05/18] docs(ai-docs): address codex fourth review comments --- ai-docs/migration/003-store-event-wiring-migration.md | 9 +++++++-- ai-docs/migration/008-store-task-utils-migration.md | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ai-docs/migration/003-store-event-wiring-migration.md b/ai-docs/migration/003-store-event-wiring-migration.md index 49ec16b03..6df3f3a79 100644 --- a/ai-docs/migration/003-store-event-wiring-migration.md +++ b/ai-docs/migration/003-store-event-wiring-migration.md @@ -77,8 +77,13 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be | `AGENT_WRAPPEDUP` | `handleWrappedup` | **Keep refresh or add explicit task removal** — task must be removed from `taskList`/`currentTask` after wrapup completion to prevent stale UI | | `TASK_HOLD` | Fire callback only | **Simplify** — no `refreshTaskList()` | | `TASK_RESUME` | Fire callback only | **Simplify** — no `refreshTaskList()` | -| `TASK_CONSULT_*` | Fire callback only | **Simplify** — SDK manages state | -| `TASK_CONFERENCE_*` | Fire callback only | **Simplify** — SDK manages state | +| `TASK_CONSULT_END` | `handleConsultEnd` | **Keep handler** — must reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` + fire callback | +| `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | **Keep handler** — must reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` + fire callback | +| `TASK_CONSULTING` | `handleConsulting` | **Keep handler** — sets `consultStartTimeStamp` + fire callback | +| Other `TASK_CONSULT_*` | Fire callback only | **Simplify** — SDK manages task state | +| `TASK_PARTICIPANT_JOINED` / `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | **Keep handler** — must reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` | +| `TASK_CONFERENCE_ENDED` / `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | **Keep handler** — conference cleanup logic | +| Other `TASK_CONFERENCE_*` | Fire callback only | **Simplify** — SDK manages task state | | `AGENT_OFFER_CONTACT` | Fire callback only | **Simplify** — SDK updates task.data | | `TASK_POST_CALL_ACTIVITY` | Fire callback only | **Simplify** | | All other `refreshTaskList()` handlers | Remove or fire callback only | **Simplify** | diff --git a/ai-docs/migration/008-store-task-utils-migration.md b/ai-docs/migration/008-store-task-utils-migration.md index fd654c091..2389ac9ec 100644 --- a/ai-docs/migration/008-store-task-utils-migration.md +++ b/ai-docs/migration/008-store-task-utils-migration.md @@ -172,7 +172,7 @@ export function getTaskStatus(task: ITask, agentId: string): string { #### After (enhanced with SDK controls) ```typescript // store/task-utils.ts — can now derive status from uiControls -export function getTaskStatus(task: ITask): string { +export function getTaskStatus(task: ITask, agentId: string): string { const controls = task.uiControls; if (!controls) return 'Unknown'; if (controls.wrapup.isVisible) return 'Wrap Up'; @@ -180,7 +180,7 @@ export function getTaskStatus(task: ITask): string { if (controls.exitConference.isVisible) return 'Conference'; // NOTE: Do NOT derive held state from controls.hold.isEnabled — hold can be // disabled in consult/transition states even when call is not held. - // Use task data instead: + // Use task data instead (agentId needed for participant lookup): if (findHoldStatus(task, 'mainCall', agentId)) return 'Held'; if (controls.end.isVisible) return 'Connected'; if (controls.accept.isVisible) return 'Offered'; From 23c1fab1bfba3f48ebe96dccc4681ab1f1f134a0 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 10 Mar 2026 19:38:57 +0530 Subject: [PATCH 06/18] docs(ai-docs): address codex fifth review comments - Fix isHeld derivation in execution plan to use findHoldStatus(task) instead of controls.hold - Add CallControlCAD/call-control-cad.tsx and wc.ts to file inventory and migration checklist - Update cross-reference matrix with call-control-cad.tsx references Made-with: Cursor --- ai-docs/migration/011-execution-plan.md | 2 +- .../013-file-inventory-old-control-references.md | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ai-docs/migration/011-execution-plan.md b/ai-docs/migration/011-execution-plan.md index 336d7b280..9588b141c 100644 --- a/ai-docs/migration/011-execution-plan.md +++ b/ai-docs/migration/011-execution-plan.md @@ -162,7 +162,7 @@ describe('calculateStateTimerData with TaskUIControls', () => { **Steps:** 1. Update `calculateStateTimerData(task, controls, agentId)` signature 2. Replace `controlVisibility.isConsultInitiatedOrAccepted` → `controls.endConsult.isVisible` -3. Replace `controlVisibility.isHeld` → derive from `controls.hold` +3. Replace `controlVisibility.isHeld` → derive from task data via `findHoldStatus(task, 'mainCall', agentId)` (do NOT derive from `controls.hold.isEnabled` — hold can be disabled in consult/transition states even when call is not held) 4. Update `calculateConsultTimerData(task, controls, agentId)` similarly 5. Update all test cases diff --git a/ai-docs/migration/013-file-inventory-old-control-references.md b/ai-docs/migration/013-file-inventory-old-control-references.md index 5579486ee..6d7a000e7 100644 --- a/ai-docs/migration/013-file-inventory-old-control-references.md +++ b/ai-docs/migration/013-file-inventory-old-control-references.md @@ -17,7 +17,7 @@ This is the definitive inventory of **every file** in CC Widgets that references | cc-components utils | 2 | 3+ (unaffected) | | cc-components components | 4 | 8+ (unaffected) | | Store | 3 | 1 (`store.ts`) | -| **Total files to modify** | **~21** | **~15 unaffected** | +| **Total files to modify** | **~23** | **~15 unaffected** | --- @@ -47,6 +47,8 @@ This is the definitive inventory of **every file** in CC Widgets that references | 8 | `cc-components/.../CallControl/call-control.tsx` | Receives `controlVisibility` as prop, passes to `buildCallControlButtons()`, `createConsultButtons()`, `filterButtonsForConsultation()` | [Doc 010](./010-component-layer-migration.md) | | 9 | `cc-components/.../CallControlCustom/call-control-consult.tsx` | Receives `controlVisibility` from `CallControlConsultComponentsProps`, passes to `createConsultButtons()` | [Doc 010](./010-component-layer-migration.md) | | 10 | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Receives `isConferenceInProgress` prop | [Doc 010](./010-component-layer-migration.md) | +| 10b | `cc-components/.../CallControlCAD/call-control-cad.tsx` | Directly references `controlVisibility.isConferenceInProgress`, `controlVisibility.isHeld`, `controlVisibility.isConsultReceived`, `controlVisibility.consultCallHeld`, `controlVisibility.recordingIndicator`, `controlVisibility.wrapup`, `controlVisibility.isConsultInitiatedOrAccepted` | [Doc 010](./010-component-layer-migration.md) | +| 10c | `cc-components/src/wc.ts` | Registers `WebCallControlCADComponent` with `commonPropsForCallControl` — props must align with new `TaskUIControls` shape | [Doc 010](./010-component-layer-migration.md) | ### Tier 4: Widget Entry Points (Medium Impact) @@ -116,18 +118,18 @@ This shows exactly which files reference each old control name: | `muteUnmuteConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | | `holdResume` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | | `pauseResumeRecording` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `recordingIndicator` | `task-util.ts`, `task.types.ts` | +| `recordingIndicator` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | | `mergeConference` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | | `consultTransferConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | | `mergeConferenceConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | | `isConferenceInProgress` | `task-util.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `task.types.ts`, `consult-transfer-popover.tsx` | | `isConsultInitiated` | `task-util.ts`, `call-control.utils.ts`, `timer-utils.ts`, `task.types.ts` | | `isConsultInitiatedAndAccepted` | `task-util.ts`, `task.types.ts` | -| `isConsultReceived` | `task-util.ts`, `task.types.ts` | +| `isConsultReceived` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | | `isConsultInitiatedOrAccepted` | `task-util.ts`, `helper.ts`, `timer-utils.ts`, `task.types.ts` | -| `isHeld` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `consultCallHeld` | `task-util.ts`, `timer-utils.ts`, `task.types.ts` | -| `controlVisibility` (param name) | `helper.ts`, `timer-utils.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `call-control.tsx`, `call-control-consult.tsx`, `task.types.ts` | +| `isHeld` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts`, `call-control-cad.tsx` | +| `consultCallHeld` | `task-util.ts`, `timer-utils.ts`, `task.types.ts`, `call-control-cad.tsx` | +| `controlVisibility` (param name) | `helper.ts`, `timer-utils.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `call-control.tsx`, `call-control-consult.tsx`, `call-control-cad.tsx`, `task.types.ts` | | `ControlVisibility` (type) | `task.types.ts` (definition), `call-control.utils.ts`, `call-control-custom.utils.ts` (imports) | --- @@ -150,6 +152,8 @@ Based on dependencies: 11. call-control.tsx — Update to accept controls: TaskUIControls 12. call-control-consult.tsx — Update consult component props 13. consult-transfer-popover.tsx — Update isConferenceInProgress derivation +13b. call-control-cad.tsx — Replace all controlVisibility refs (isHeld, isConferenceInProgress, recordingIndicator, wrapup, isConsultInitiatedOrAccepted, isConsultReceived, consultCallHeld) +13c. wc.ts — Update commonPropsForCallControl to align with TaskUIControls shape 14. CallControl/index.tsx — Remove old props from useCallControl call 15. CallControlCAD/index.tsx — Remove old props from useCallControl call 16. TaskList/index.tsx — Remove deviceType usage From e4ea922f37ec3cf01008a6dce984d82d6bad0671 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 10:36:50 +0530 Subject: [PATCH 07/18] docs(ai-docs): address codex sixth review comments --- ai-docs/migration/004-call-control-hook-migration.md | 6 +++--- ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ai-docs/migration/004-call-control-hook-migration.md b/ai-docs/migration/004-call-control-hook-migration.md index 3fb5b33c5..6bad91600 100644 --- a/ai-docs/migration/004-call-control-hook-migration.md +++ b/ai-docs/migration/004-call-control-hook-migration.md @@ -23,7 +23,7 @@ 8. **Consult**: `consultCall()` → `task.consult()`, `endConsultCall()` → `task.endConsult()` 9. **Consult transfer**: `consultTransfer()` → `task.consultTransfer()` / `task.transferConference()` 10. **Conference**: `consultConference()` → `task.consultConference()`, `exitConference()` → `task.exitConference()` -11. **Switch calls**: `switchToConsult()` → `task.hold(mainMedia)` + `task.resume(consultMedia)`, `switchToMainCall()` → reverse +11. **Switch calls**: `switchToConsult()` → `task.hold(mainMediaId)` (single call), `switchToMainCall()` → `task.resume(consultMediaId)` (single call) 12. **Auto-wrapup timer**: `cancelAutoWrapup()` → `task.cancelAutoWrapupTimer()` 13. **Hold timer**: via `useHoldTimer(currentTask)` hook 14. **Event callbacks**: Registers hold/resume/end/wrapup/recording callbacks via `setTaskCallback` @@ -142,8 +142,8 @@ | `consultTransfer` | `task.consultTransfer()` / `task.transferConference()` | None | | `consultConference` | `task.consultConference()` | None | | `exitConference` | `task.exitConference()` | None | -| `switchToConsult` | `task.hold(mainMediaId)` + `task.resume(consultMediaId)` | None | -| `switchToMainCall` | `task.hold(consultMediaId)` + `task.resume(mainMediaId)` | None | +| `switchToConsult` | `task.hold(mainMediaId)` | Single SDK call — holds main call; SDK auto-switches to consult leg | +| `switchToMainCall` | `task.resume(consultMediaId)` | Single SDK call — resumes consult leg; SDK auto-switches to main call | | `cancelAutoWrapup` | `task.cancelAutoWrapupTimer()` | None | --- diff --git a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md index 5bd1d25b8..0337952a6 100644 --- a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md +++ b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md @@ -318,7 +318,7 @@ This document traces **every task scenario from start to finish**, showing exact 14. SDK: State: WRAPPING_UP → COMPLETED 15. SDK: computeUIControls(COMPLETED) → all controls disabled 16. SDK: emitTaskWrappedup action → cleanupResources action -17. SDK emits: task:wrappedup, task:ui-controls-updated, task:cleanup +17. SDK emits: AGENT_WRAPPEDUP (AgentWrappedUp), task:ui-controls-updated, task:cleanup 18. Store: handleTaskEnd/cleanup → remove task from list 19. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) 20. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) @@ -634,7 +634,7 @@ No changes needed. | Hold | `hold()` → `task:hold` | `hold()` → `HOLD_INITIATED` → `HOLD_SUCCESS` → `task:hold` + `task:ui-controls-updated` | | Resume | `resume()` → `task:resume` | `resume()` → `UNHOLD_INITIATED` → `UNHOLD_SUCCESS` → `task:resume` + `task:ui-controls-updated` | | End call | `end()` → `task:end` | `end()` → `CONTACT_ENDED` → `task:end` + `task:ui-controls-updated` | -| Wrapup | `wrapup()` → `task:wrappedup` | `wrapup()` → `WRAPUP_COMPLETE` → `task:wrappedup` + `task:ui-controls-updated` | +| Wrapup | `wrapup()` → `task:wrappedup` | `wrapup()` → `WRAPUP_COMPLETE` → `AGENT_WRAPPEDUP` (AgentWrappedUp) + `task:ui-controls-updated` | | Transfer | `transfer()` → `task:end` | `transfer()` → `TRANSFER_SUCCESS` → `task:end` + `task:ui-controls-updated` | | Start consult | `consult()` → `task:consultCreated` | `consult()` → `CONSULT` → `CONSULT_SUCCESS` → `CONSULTING_ACTIVE` → `task:consulting` + `task:ui-controls-updated` | | End consult | `endConsult()` → `task:consultEnd` | `endConsult()` → `CONSULT_END` → `task:consultEnd` + `task:ui-controls-updated` | From 2313f1a26660087ed93c7180d7649f7f6960c769 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 12:48:49 +0530 Subject: [PATCH 08/18] docs(ai-docs): address codex seventh review comments - Fix self-reexport in 008: replace circular export with clear retention note for findHoldTimestamp and findHoldStatus - Move findHoldStatus from REMOVE to KEEP in 008: still needed for getTaskStatus() and component layer isHeld - Move task-list.utils.ts and incoming-task.utils.tsx from unaffected to affected in 013: both use isBrowser/isDeclineButtonEnabled - Update cross-reference matrix with isBrowser and isDeclineButtonEnabled entries Made-with: Cursor --- ai-docs/migration/008-store-task-utils-migration.md | 13 ++++++++----- .../013-file-inventory-old-control-references.md | 10 +++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ai-docs/migration/008-store-task-utils-migration.md b/ai-docs/migration/008-store-task-utils-migration.md index 2389ac9ec..6e22fdeb4 100644 --- a/ai-docs/migration/008-store-task-utils-migration.md +++ b/ai-docs/migration/008-store-task-utils-migration.md @@ -42,7 +42,7 @@ The store's `task-utils.ts` contains ~15 utility functions that inspect raw task | `getConferenceParticipantsCount(task)` | Used only for control visibility computation | SDK computes max participant check internally | | `getIsCustomerInCall(task)` | Used only for control visibility computation | SDK computes internally | | `getIsConsultInProgress(task)` | Used only for control visibility computation | SDK computes internally | -| `findHoldStatus(task, mType, agentId)` | Used for control visibility | SDK tracks hold state in context | +| ~~`findHoldStatus(task, mType, agentId)`~~ | ~~Used for control visibility~~ | **MOVED TO KEEP** — still needed for `getTaskStatus()` held-state derivation and component layer `isHeld` (see below) | ### Keep (Widget-layer concerns) @@ -54,6 +54,7 @@ The store's `task-utils.ts` contains ~15 utility functions that inspect raw task | `isInteractionOnHold(task)` | Timer logic needs this | | `findMediaResourceId(task, mType)` | Switch-call actions need media resource IDs | | `findHoldTimestamp(task, mType)` | Hold timer needs timestamp | +| `findHoldStatus(task, mType, agentId)` | Needed for `getTaskStatus()` held-state derivation and component layer `isHeld` — cannot derive from `controls.hold.isEnabled` | ### Review (may simplify) @@ -75,7 +76,7 @@ The store's `task-utils.ts` contains ~15 utility functions that inspect raw task | `getConferenceParticipantsCount()` | **REMOVE** | SDK internal check | | `getIsCustomerInCall()` | **REMOVE** | SDK internal check | | `getIsConsultInProgress()` | **REMOVE** | SDK internal check | -| `findHoldStatus()` | **REMOVE** | SDK tracks in `TaskContext` | +| `findHoldStatus()` | **KEEP** | Still needed for `getTaskStatus()` held-state derivation and component layer `isHeld` — do NOT derive from `controls.hold` | | `isIncomingTask()` | **KEEP** | — | | `getTaskStatus()` | **KEEP** | Could enhance with SDK TaskState | | `getConferenceParticipants()` | **KEEP** | Display only | @@ -120,15 +121,17 @@ export function getControlsVisibility(deviceType, featureFlags, task, agentId, c ### After (all above replaced by `task.uiControls`) ```typescript -// task-util.ts — DELETED or reduced to: -export { findHoldTimestamp } from './task-util'; // Only keep for timer display +// task-util.ts — DELETED or reduced to only keep timer/hold helpers: +// findHoldTimestamp() — retained in task-util.ts for hold timer display +// findHoldStatus() — retained in task-util.ts for isHeld derivation (used by getTaskStatus, component layer) +// All other functions (getConsultStatus, getIsConsultInProgress, getConferenceParticipantsCount, etc.) — DELETED // In useCallControl hook — no imports from store task-utils for controls: const controls = currentTask?.uiControls ?? getDefaultUIControls(); // All 17 controls come pre-computed from SDK. Zero store util calls needed. ``` -### Before/After: `findHoldStatus` Removal +### Before/After: `findHoldStatus` — RETAINED (not removed) #### Before (used in controls computation) ```typescript diff --git a/ai-docs/migration/013-file-inventory-old-control-references.md b/ai-docs/migration/013-file-inventory-old-control-references.md index 6d7a000e7..728155129 100644 --- a/ai-docs/migration/013-file-inventory-old-control-references.md +++ b/ai-docs/migration/013-file-inventory-old-control-references.md @@ -17,7 +17,7 @@ This is the definitive inventory of **every file** in CC Widgets that references | cc-components utils | 2 | 3+ (unaffected) | | cc-components components | 4 | 8+ (unaffected) | | Store | 3 | 1 (`store.ts`) | -| **Total files to modify** | **~23** | **~15 unaffected** | +| **Total files to modify** | **~25** | **~13 unaffected** | --- @@ -49,6 +49,8 @@ This is the definitive inventory of **every file** in CC Widgets that references | 10 | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Receives `isConferenceInProgress` prop | [Doc 010](./010-component-layer-migration.md) | | 10b | `cc-components/.../CallControlCAD/call-control-cad.tsx` | Directly references `controlVisibility.isConferenceInProgress`, `controlVisibility.isHeld`, `controlVisibility.isConsultReceived`, `controlVisibility.consultCallHeld`, `controlVisibility.recordingIndicator`, `controlVisibility.wrapup`, `controlVisibility.isConsultInitiatedOrAccepted` | [Doc 010](./010-component-layer-migration.md) | | 10c | `cc-components/src/wc.ts` | Registers `WebCallControlCADComponent` with `commonPropsForCallControl` — props must align with new `TaskUIControls` shape | [Doc 010](./010-component-layer-migration.md) | +| 10d | `cc-components/.../TaskList/task-list.utils.ts` | `extractTaskListItemData(task, isBrowser, agentId)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation; `store.isDeclineButtonEnabled` | [Doc 006](./006-task-list-migration.md) | +| 10e | `cc-components/.../IncomingTask/incoming-task.utils.tsx` | `extractIncomingTaskData(incomingTask, isBrowser, logger, isDeclineButtonEnabled)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation | [Doc 005](./005-incoming-task-migration.md) | ### Tier 4: Widget Entry Points (Medium Impact) @@ -100,9 +102,7 @@ This is the definitive inventory of **every file** in CC Widgets that references | `cc-components/.../TaskTimer/index.tsx` | Timer display | | `cc-components/.../Task/index.tsx` | Task card display | | `cc-components/.../Task/task.utils.ts` | Task data extraction for display | -| `cc-components/.../TaskList/task-list.utils.ts` | Task list data formatting | | `cc-components/.../OutdialCall/outdial-call.tsx` | No task controls | -| `cc-components/.../IncomingTask/incoming-task.utils.tsx` | Incoming task display utils | | `cc-components/.../constants.ts` | UI string constants | | `cc-components/.../OutdialCall/constants.ts` | Outdial constants | @@ -131,6 +131,8 @@ This shows exactly which files reference each old control name: | `consultCallHeld` | `task-util.ts`, `timer-utils.ts`, `task.types.ts`, `call-control-cad.tsx` | | `controlVisibility` (param name) | `helper.ts`, `timer-utils.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `call-control.tsx`, `call-control-consult.tsx`, `call-control-cad.tsx`, `task.types.ts` | | `ControlVisibility` (type) | `task.types.ts` (definition), `call-control.utils.ts`, `call-control-custom.utils.ts` (imports) | +| `isBrowser` (legacy flag) | `task-list.utils.ts`, `incoming-task.utils.tsx`, `task-list.tsx`, `incoming-task.tsx` — replace with `task.uiControls.accept`/`decline` | +| `isDeclineButtonEnabled` (legacy flag) | `incoming-task.utils.tsx`, `incoming-task.tsx`, `task-list.utils.ts` — replace with `task.uiControls.decline.isEnabled` | --- @@ -154,6 +156,8 @@ Based on dependencies: 13. consult-transfer-popover.tsx — Update isConferenceInProgress derivation 13b. call-control-cad.tsx — Replace all controlVisibility refs (isHeld, isConferenceInProgress, recordingIndicator, wrapup, isConsultInitiatedOrAccepted, isConsultReceived, consultCallHeld) 13c. wc.ts — Update commonPropsForCallControl to align with TaskUIControls shape +13d. task-list.utils.ts — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic +13e. incoming-task.utils.tsx — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic 14. CallControl/index.tsx — Remove old props from useCallControl call 15. CallControlCAD/index.tsx — Remove old props from useCallControl call 16. TaskList/index.tsx — Remove deviceType usage From 0024a37302b06e96900ef03b8960891024167fd6 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 12:52:56 +0530 Subject: [PATCH 09/18] =?UTF-8?q?docs(ai-docs):=20split=20PR=20=E2=80=94?= =?UTF-8?q?=20retain=20only=20foundation=20docs=20(PR=201/4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove files moved to separate PRs: - PR 2 (Store Layer): 003, 008 - PR 3 (Widget Layer): 004, 005, 006, 007, 010 - PR 4 (Planning & Reference): 011, 012, 013 This PR now contains only the foundation documents: - 001: Migration overview (master index) - 002: UI controls migration (core old→new mapping) - 009: Types and constants migration - 014: Task code scan report Made-with: Cursor --- .../003-store-event-wiring-migration.md | 268 ------- .../004-call-control-hook-migration.md | 415 ----------- .../migration/005-incoming-task-migration.md | 238 ------- ai-docs/migration/006-task-list-migration.md | 150 ---- .../migration/007-outdial-call-migration.md | 39 - .../008-store-task-utils-migration.md | 231 ------ .../010-component-layer-migration.md | 563 --------------- ai-docs/migration/011-execution-plan.md | 438 ------------ .../012-task-lifecycle-flows-old-vs-new.md | 664 ------------------ ...3-file-inventory-old-control-references.md | 171 ----- 10 files changed, 3177 deletions(-) delete mode 100644 ai-docs/migration/003-store-event-wiring-migration.md delete mode 100644 ai-docs/migration/004-call-control-hook-migration.md delete mode 100644 ai-docs/migration/005-incoming-task-migration.md delete mode 100644 ai-docs/migration/006-task-list-migration.md delete mode 100644 ai-docs/migration/007-outdial-call-migration.md delete mode 100644 ai-docs/migration/008-store-task-utils-migration.md delete mode 100644 ai-docs/migration/010-component-layer-migration.md delete mode 100644 ai-docs/migration/011-execution-plan.md delete mode 100644 ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md delete mode 100644 ai-docs/migration/013-file-inventory-old-control-references.md diff --git a/ai-docs/migration/003-store-event-wiring-migration.md b/ai-docs/migration/003-store-event-wiring-migration.md deleted file mode 100644 index 6df3f3a79..000000000 --- a/ai-docs/migration/003-store-event-wiring-migration.md +++ /dev/null @@ -1,268 +0,0 @@ -# Migration Doc 003: Store Event Wiring Refactor - -## Summary - -The store's `storeEventsWrapper.ts` currently registers 30+ individual task event handlers that manually call `refreshTaskList()` and update observables. With the state machine, the SDK handles state transitions internally. Many event handlers can be simplified or removed, and the new `task:ui-controls-updated` event replaces manual state derivation. - ---- - -## Old Approach - -### Entry Point -**File:** `packages/contact-center/store/src/storeEventsWrapper.ts` -**Method:** `registerTaskEventListeners(task: ITask)` - -### How It Works (Old) -1. On task creation, store registers individual listeners for 30+ task events -2. Each handler manually updates store observables (taskList, currentTask) -3. Many handlers simply call `refreshTaskList()` to re-fetch task state -4. Some handlers have specialized logic (consult, conference lifecycle) -5. Widgets subscribe to store callbacks via `setTaskCallback(event, cb, taskId)` - -### Old Event Handlers - -| Event | Handler | Action | -|-------|---------|--------| -| `TASK_END` | `handleTaskEnd` | Remove task from list, clear current task | -| `TASK_ASSIGNED` | `handleTaskAssigned` | Update task list, set current task | -| `AGENT_OFFER_CONTACT` | `refreshTaskList` | Re-fetch all tasks | -| `AGENT_CONSULT_CREATED` | `handleConsultCreated` | Update task list, fire callbacks | -| `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | Refresh + fire callbacks | -| `TASK_REJECT` | `handleTaskReject` | Remove task, fire callbacks | -| `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | Remove task, fire callbacks | -| `AGENT_WRAPPEDUP` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_CONSULTING` | `handleConsulting` | Refresh + fire callbacks | -| `TASK_CONSULT_ACCEPTED` | `handleConsultAccepted` | Refresh + fire callbacks | -| `TASK_OFFER_CONSULT` | `handleConsultOffer` | Refresh + fire callbacks | -| `TASK_AUTO_ANSWERED` | `handleAutoAnswer` | Refresh + fire callbacks | -| `TASK_CONSULT_END` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_HOLD` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_RESUME` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | Refresh + fire callbacks | -| `TASK_CONFERENCE_END_FAILED` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_CONFERENCE_ESTABLISHING` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_CONFERENCE_FAILED` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_PARTICIPANT_JOINED` | `handleConferenceStarted` | Refresh + fire callbacks | -| `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | Refresh + fire callbacks | -| `TASK_PARTICIPANT_LEFT_FAILED` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | Refresh + fire callbacks | -| `TASK_CONFERENCE_TRANSFERRED` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_CONFERENCE_TRANSFER_FAILED` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_POST_CALL_ACTIVITY` | `refreshTaskList` | Re-fetch all tasks | -| `TASK_MEDIA` | `handleTaskMedia` | Browser-only media setup | - ---- - -## New Approach - -### What Changes in SDK -1. SDK state machine handles all transitions internally -2. `task.data` is updated by the state machine's `updateTaskData` action on every event -3. `task.uiControls` is recomputed after every state transition -4. `task:ui-controls-updated` is emitted when controls change - -### Proposed New Event Registration - -Many events that currently trigger `refreshTaskList()` will no longer need it because `task.data` is kept in sync by the SDK. The store should: - -| Event | New Handler | Change | -|-------|-------------|--------| -| `TASK_END` | `handleTaskEnd` | **Keep** (need to remove from task list) | -| `TASK_ASSIGNED` | `handleTaskAssigned` | **Keep** (need to update current task) | -| `TASK_REJECT` | `handleTaskReject` | **Keep** (need to remove from task list) | -| `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | **Keep** (need to remove from task list) | -| `TASK_MEDIA` | `handleTaskMedia` | **Keep** (browser WebRTC setup) | -| `TASK_UI_CONTROLS_UPDATED` | **NEW** — `handleUIControlsUpdated` | **Add** — trigger widget re-renders | -| `TASK_WRAPUP` | `handleWrapup` | **Simplify** — no need to refresh | -| `AGENT_WRAPPEDUP` | `handleWrappedup` | **Keep refresh or add explicit task removal** — task must be removed from `taskList`/`currentTask` after wrapup completion to prevent stale UI | -| `TASK_HOLD` | Fire callback only | **Simplify** — no `refreshTaskList()` | -| `TASK_RESUME` | Fire callback only | **Simplify** — no `refreshTaskList()` | -| `TASK_CONSULT_END` | `handleConsultEnd` | **Keep handler** — must reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` + fire callback | -| `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | **Keep handler** — must reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` + fire callback | -| `TASK_CONSULTING` | `handleConsulting` | **Keep handler** — sets `consultStartTimeStamp` + fire callback | -| Other `TASK_CONSULT_*` | Fire callback only | **Simplify** — SDK manages task state | -| `TASK_PARTICIPANT_JOINED` / `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | **Keep handler** — must reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` | -| `TASK_CONFERENCE_ENDED` / `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | **Keep handler** — conference cleanup logic | -| Other `TASK_CONFERENCE_*` | Fire callback only | **Simplify** — SDK manages task state | -| `AGENT_OFFER_CONTACT` | Fire callback only | **Simplify** — SDK updates task.data | -| `TASK_POST_CALL_ACTIVITY` | Fire callback only | **Simplify** | -| All other `refreshTaskList()` handlers | Remove or fire callback only | **Simplify** | - -### Key Insight: `refreshTaskList()` Elimination - -**Old:** 15+ events trigger `refreshTaskList()` → `cc.taskManager.getAllTasks()` → update store observables. - -**New:** SDK keeps `task.data` updated via state machine actions. The store can read `task.data` directly instead of re-fetching. `refreshTaskList()` should only be called for: -- Initial load / hydration -- Full page refresh recovery -- Edge cases where task data is stale - ---- - -## Old → New Event Handler Mapping - -| Old Handler | Old Action | New Action | -|-------------|-----------|------------| -| `refreshTaskList` (15+ events) | Re-fetch ALL tasks from SDK | **Remove** — task.data is live; fire callbacks only | -| `handleTaskEnd` | Remove from list + cleanup | **Keep** — still need list management | -| `handleTaskAssigned` | Set current task | **Keep** — still need list management | -| `handleConsultCreated` | Refresh + callbacks | **Simplify** — callbacks only | -| `handleConsulting` | Refresh + callbacks | **Simplify** — callbacks only | -| `handleConsultAccepted` | Refresh + callbacks | **Simplify** — callbacks only | -| `handleConsultOffer` | Refresh + callbacks | **Simplify** — callbacks only | -| `handleConferenceStarted` | Refresh + callbacks | **Simplify** — callbacks only | -| `handleConferenceEnded` | Refresh + callbacks | **Simplify** — callbacks only | -| `handleAutoAnswer` | Refresh + callbacks | **Simplify** — callbacks only | -| `handleTaskReject` | Remove from list | **Keep** | -| `handleOutdialFailed` | Remove from list | **Keep** | -| `handleTaskMedia` | WebRTC media setup | **Keep** | - ---- - ---- - -## Refactor Patterns (Before/After) - -### Pattern 1: `registerTaskEventListeners()` — Adding UI Controls Handler - -#### Before -```typescript -// storeEventsWrapper.ts — registerTaskEventListeners(task) -registerTaskEventListeners(task: ITask) { - const interactionId = task.data.interactionId; - task.on(TASK_EVENTS.TASK_END, (data) => this.handleTaskEnd(data, interactionId)); - task.on(TASK_EVENTS.TASK_ASSIGNED, (data) => this.handleTaskAssigned(data, interactionId)); - task.on(TASK_EVENTS.AGENT_OFFER_CONTACT, () => this.refreshTaskList()); - task.on(TASK_EVENTS.TASK_HOLD, () => this.refreshTaskList()); - task.on(TASK_EVENTS.TASK_RESUME, () => this.refreshTaskList()); - task.on(TASK_EVENTS.TASK_CONSULT_END, () => this.refreshTaskList()); - task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, () => this.refreshTaskList()); - task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, (data) => this.handleConferenceStarted(data, interactionId)); - // ... 20+ more event registrations, most calling refreshTaskList() -} -``` - -#### After -```typescript -// storeEventsWrapper.ts — registerTaskEventListeners(task) -registerTaskEventListeners(task: ITask) { - const interactionId = task.data.interactionId; - - // NEW: Subscribe to SDK-computed UI control updates - task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, (uiControls) => { - this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); - }); - - // KEEP: Task lifecycle events that need store-level management - task.on(TASK_EVENTS.TASK_END, (data) => this.handleTaskEnd(data, interactionId)); - task.on(TASK_EVENTS.TASK_ASSIGNED, (data) => this.handleTaskAssigned(data, interactionId)); - task.on(TASK_EVENTS.TASK_REJECT, (data) => this.handleTaskReject(data, interactionId)); - task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, (data) => this.handleOutdialFailed(data, interactionId)); - task.on(TASK_EVENTS.TASK_MEDIA, (data) => this.handleTaskMedia(data, interactionId)); - - // SIMPLIFIED: Events that only need callback firing (SDK keeps task.data in sync) - task.on(TASK_EVENTS.TASK_HOLD, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_HOLD, interactionId)); - task.on(TASK_EVENTS.TASK_RESUME, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RESUME, interactionId)); - // AGENT_WRAPPEDUP: still needs task cleanup — refresh or explicitly remove from taskList/currentTask - task.on(TASK_EVENTS.AGENT_WRAPPEDUP, (data) => { - this.refreshTaskList(); // retain: task must be removed from list after wrapup completion - this.fireTaskCallbacks(TASK_EVENTS.AGENT_WRAPPEDUP, interactionId, data); - }); - task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_PAUSED, interactionId)); - task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_RESUMED, interactionId)); - // ... consult/conference events: fire callbacks only, no refreshTaskList() -} -``` - -### Pattern 2: Simplifying `refreshTaskList()` Event Handlers - -#### Before -```typescript -// 15+ events all trigger a full re-fetch -task.on(TASK_EVENTS.TASK_HOLD, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_RESUME, () => this.refreshTaskList()); -task.on(TASK_EVENTS.AGENT_WRAPPEDUP, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_CONSULT_END, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, () => this.refreshTaskList()); -task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, () => this.refreshTaskList()); -// refreshTaskList() does: cc.taskManager.getAllTasks() → update store.taskList -``` - -#### After -```typescript -// SDK keeps task.data in sync via state machine. -// refreshTaskList() only called on initialization/hydration. -// Individual events just fire callbacks for widget-layer side effects. - -task.on(TASK_EVENTS.TASK_HOLD, () => { - this.fireTaskCallbacks(TASK_EVENTS.TASK_HOLD, interactionId); -}); -task.on(TASK_EVENTS.TASK_RESUME, () => { - this.fireTaskCallbacks(TASK_EVENTS.TASK_RESUME, interactionId); -}); -// No refreshTaskList() — task.data already updated by SDK -``` - -### Pattern 3: Conference Handler Simplification - -#### Before -```typescript -handleConferenceStarted(data: any, interactionId: string) { - this.refreshTaskList(); // Re-fetch all tasks from SDK - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId, data); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId, data); -} - -handleConferenceEnded(data: any, interactionId: string) { - this.refreshTaskList(); // Re-fetch all tasks from SDK - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId, data); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId, data); -} -``` - -#### After -```typescript -// SDK state machine handles CONFERENCING state transitions. -// task.data and task.uiControls already reflect conference state. -// Store just fires callbacks for widget-layer side effects. - -handleConferenceStarted(data: any, interactionId: string) { - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId, data); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId, data); -} - -handleConferenceEnded(data: any, interactionId: string) { - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId, data); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId, data); -} -``` - ---- - -## Files to Modify - -| File | Action | -|------|--------| -| `store/src/storeEventsWrapper.ts` | Refactor `registerTaskEventListeners`, simplify/remove handlers | -| `store/src/store.ts` | No changes expected (observables stay) | -| `store/src/store.types.ts` | Add `TASK_UI_CONTROLS_UPDATED` if not re-exported from SDK | - ---- - -## Validation Criteria - -- [ ] Task list stays in sync on all lifecycle events (incoming, assigned, end, reject) -- [ ] `refreshTaskList()` only called on init/hydration, not on every event -- [ ] Widget callbacks still fire correctly for events that require UI updates -- [ ] `task:ui-controls-updated` triggers re-renders in widgets -- [ ] No regression in consult/conference/hold flows -- [ ] Task removal from list on end/reject works correctly - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/004-call-control-hook-migration.md b/ai-docs/migration/004-call-control-hook-migration.md deleted file mode 100644 index 6bad91600..000000000 --- a/ai-docs/migration/004-call-control-hook-migration.md +++ /dev/null @@ -1,415 +0,0 @@ -# Migration Doc 004: CallControl Hook (`useCallControl`) Migration - -## Summary - -`useCallControl` is the largest and most complex hook in CC Widgets. It orchestrates hold, mute, recording, consult, transfer, conference, wrapup, and auto-wrapup flows. This migration replaces widget-side control computation with `task.uiControls` and simplifies event-driven state updates. - ---- - -## Old Approach - -### Entry Point -**File:** `packages/contact-center/task/src/helper.ts` -**Hook:** `useCallControl(props: useCallControlProps)` - -### Current Responsibilities -1. **Control visibility**: Calls `getControlsVisibility()` → 22 controls + 7 state flags -2. **Hold/Resume**: `toggleHold()` → `task.hold()` / `task.resume()` / `task.hold(mediaResourceId)` / `task.resume(mediaResourceId)` -3. **Mute**: `toggleMute()` → `task.toggleMute()` (local state tracking) -4. **Recording**: `toggleRecording()` → `task.pauseRecording()` / `task.resumeRecording()` -5. **End call**: `endCall()` → `task.end()` -6. **Wrapup**: `wrapupCall()` → `task.wrapup()` -7. **Transfer**: `transferCall()` → `task.transfer()` -8. **Consult**: `consultCall()` → `task.consult()`, `endConsultCall()` → `task.endConsult()` -9. **Consult transfer**: `consultTransfer()` → `task.consultTransfer()` / `task.transferConference()` -10. **Conference**: `consultConference()` → `task.consultConference()`, `exitConference()` → `task.exitConference()` -11. **Switch calls**: `switchToConsult()` → `task.hold(mainMediaId)` (single call), `switchToMainCall()` → `task.resume(consultMediaId)` (single call) -12. **Auto-wrapup timer**: `cancelAutoWrapup()` → `task.cancelAutoWrapupTimer()` -13. **Hold timer**: via `useHoldTimer(currentTask)` hook -14. **Event callbacks**: Registers hold/resume/end/wrapup/recording callbacks via `setTaskCallback` - -### Old Hook Return Shape (abbreviated) -```typescript -{ - // Controls (from getControlsVisibility) - accept, decline, end, muteUnmute, holdResume, - pauseResumeRecording, recordingIndicator, - transfer, conference, exitConference, mergeConference, - consult, endConsult, consultTransfer, consultTransferConsult, - mergeConferenceConsult, muteUnmuteConsult, - switchToMainCall, switchToConsult, wrapup, - // State flags (from getControlsVisibility) - isConferenceInProgress, isConsultInitiated, isConsultInitiatedAndAccepted, - isConsultReceived, isConsultInitiatedOrAccepted, isHeld, consultCallHeld, - // Hook state - isMuted, isRecording, holdTime, buddyAgents, - consultAgentName, lastTargetType, secondsUntilAutoWrapup, - // Actions - toggleHold, toggleMute, toggleRecording, endCall, wrapupCall, - transferCall, consultCall, endConsultCall, consultTransfer, - consultConference, exitConference, switchToConsult, switchToMainCall, - cancelAutoWrapup, -} -``` - ---- - -## New Approach - -### Key Changes - -1. **Remove `getControlsVisibility()` call entirely** -2. **Read `task.uiControls` directly** for all control states -3. **Subscribe to `task:ui-controls-updated`** for re-renders -4. **Keep all action methods** (hold, mute, end, etc.) — SDK methods unchanged -5. **Simplify state flags** — derive from `uiControls` or remove entirely -6. **Keep hold timer, auto-wrapup, mute state** — these are widget-layer concerns - -### New Hook Return Shape (proposed) -```typescript -{ - // Controls (directly from task.uiControls) - controls: TaskUIControls, // { accept, decline, hold, mute, end, transfer, ... } - // Hook state (kept) - isMuted: boolean, - isRecording: boolean, - holdTime: number, - buddyAgents: Agent[], - consultAgentName: string, - lastTargetType: string, - secondsUntilAutoWrapup: number, - // Actions (kept — SDK methods unchanged) - toggleHold, toggleMute, toggleRecording, endCall, wrapupCall, - transferCall, consultCall, endConsultCall, consultTransfer, - consultConference, exitConference, switchToConsult, switchToMainCall, - cancelAutoWrapup, -} -``` - ---- - -## Old → New Mapping Table - -### Control Properties - -| Old Property | New Property | Change | -|-------------|-------------|--------| -| `accept` | `controls.accept` | Nested under `controls` | -| `decline` | `controls.decline` | Nested under `controls` | -| `end` | `controls.end` | Nested under `controls` | -| `muteUnmute` | `controls.mute` | **Renamed** + nested | -| `holdResume` | `controls.hold` | **Renamed** + nested | -| `pauseResumeRecording` | `controls.recording` | **Renamed** — toggle button (pause/resume) | -| `recordingIndicator` | `controls.recording` | **Same SDK control** — widget must keep separate UI for recording status badge vs toggle. Use `recording.isVisible` for badge, `recording.isEnabled` for toggle interactivity | -| `transfer` | `controls.transfer` | Nested | -| `conference` | `controls.conference` | Nested | -| `exitConference` | `controls.exitConference` | Nested | -| `mergeConference` | `controls.mergeToConference` | **Renamed** + nested | -| `consult` | `controls.consult` | Nested | -| `endConsult` | `controls.endConsult` | Nested | -| `consultTransfer` | `controls.consultTransfer` | Nested (always hidden in new) | -| `consultTransferConsult` | `controls.transfer` / `controls.transferConference` | **Split** — `transfer` for consult transfer, `transferConference` for conference transfer | -| `mergeConferenceConsult` | `controls.mergeToConference` | **Merged** | -| `muteUnmuteConsult` | `controls.mute` | **Merged** | -| `switchToMainCall` | `controls.switchToMainCall` | Nested | -| `switchToConsult` | `controls.switchToConsult` | Nested | -| `wrapup` | `controls.wrapup` | Nested | - -### State Flags - -| Old Flag | New Approach | -|----------|-------------| -| `isConferenceInProgress` | `controls.exitConference.isVisible` | -| `isConsultInitiated` | `controls.endConsult.isVisible` | -| `isConsultInitiatedAndAccepted` | Removed — SDK handles | -| `isConsultReceived` | Removed — SDK handles | -| `isConsultInitiatedOrAccepted` | `controls.endConsult.isVisible` | -| `isHeld` | `controls.hold` state (visible + disabled = held) | -| `consultCallHeld` | `controls.switchToConsult.isVisible` | - -### Actions (Unchanged) - -| Action | SDK Method | Change | -|--------|-----------|--------| -| `toggleHold` | `task.hold()` / `task.resume()` | None | -| `toggleMute` | `task.toggleMute()` | None | -| `toggleRecording` | `task.pauseRecording()` / `task.resumeRecording()` | None | -| `endCall` | `task.end()` | None | -| `wrapupCall` | `task.wrapup()` | None | -| `transferCall` | `task.transfer()` | None | -| `consultCall` | `task.consult()` | None | -| `endConsultCall` | `task.endConsult()` | None | -| `consultTransfer` | `task.consultTransfer()` / `task.transferConference()` | None | -| `consultConference` | `task.consultConference()` | None | -| `exitConference` | `task.exitConference()` | None | -| `switchToConsult` | `task.hold(mainMediaId)` | Single SDK call — holds main call; SDK auto-switches to consult leg | -| `switchToMainCall` | `task.resume(consultMediaId)` | Single SDK call — resumes consult leg; SDK auto-switches to main call | -| `cancelAutoWrapup` | `task.cancelAutoWrapupTimer()` | None | - ---- - -## Refactor Pattern - -### Before -```typescript -export function useCallControl(props: useCallControlProps) { - const task = store.currentTask; - - // OLD: Widget computes controls - const controls = getControlsVisibility( - store.deviceType, - store.featureFlags, - task, - store.agentId, - conferenceEnabled, - store.logger - ); - - // Event callbacks for hold, resume, end, wrapup, recording - useEffect(() => { - if (!task) return; - store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, task.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, task.data.interactionId); - // ... 4 more callbacks - return () => { - store.removeTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, task.data.interactionId); - // ... cleanup - }; - }, [task]); - - return { ...controls, isMuted, isRecording, /* ... actions */ }; -} -``` - -### After -```typescript -export function useCallControl(props: useCallControlProps) { - const task = store.currentTask; - - // NEW: Read SDK-computed controls directly - const [controls, setControls] = useState( - task?.uiControls ?? getDefaultUIControls() - ); - - // Subscribe to UI control updates - useEffect(() => { - if (!task) { - setControls(getDefaultUIControls()); - return; - } - setControls(task.uiControls); - const onControlsUpdated = (updatedControls: TaskUIControls) => { - setControls(updatedControls); - }; - task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, onControlsUpdated); - return () => { - task.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, onControlsUpdated); - }; - }, [task]); - - // Keep event callbacks for actions that need hook-level side effects - // (hold timer, mute state, recording state) - useEffect(() => { - if (!task) return; - store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, task.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, task.data.interactionId); - // ... recording callbacks - return () => { /* cleanup */ }; - }, [task]); - - return { controls, isMuted, isRecording, holdTime, /* ... actions */ }; -} -``` - ---- - ---- - -## Newly Discovered Items (Deep Scan) - -### 1. Pre-existing Bug: Recording Callback Cleanup Mismatch - -**File:** `task/src/helper.ts`, lines 634-653 - -```typescript -// SETUP uses TASK_EVENTS: -store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_PAUSED, pauseRecordingCallback, interactionId); -store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_RESUMED, resumeRecordingCallback, interactionId); - -// CLEANUP uses CONTACT_RECORDING (different event name!): -store.removeTaskCallback(TASK_EVENTS.CONTACT_RECORDING_PAUSED, pauseRecordingCallback, interactionId); -store.removeTaskCallback(TASK_EVENTS.CONTACT_RECORDING_RESUMED, resumeRecordingCallback, interactionId); -``` - -**Impact:** Callbacks are never properly removed on cleanup. Fix during migration by using consistent event names. - -### 2. `controlVisibility` Used as `useMemo` + Timer Effect Dependencies - -```typescript -// Line 930-933: controlVisibility is a useMemo -const controlVisibility = useMemo( - () => getControlsVisibility(deviceType, featureFlags, currentTask, agentId, conferenceEnabled, logger), - [deviceType, featureFlags, currentTask, agentId, conferenceEnabled, logger] -); - -// Line 939: Auto-wrapup timer depends on controlVisibility.wrapup -useEffect(() => { - if (currentTask?.autoWrapup && controlVisibility?.wrapup) { ... } -}, [currentTask?.autoWrapup, controlVisibility?.wrapup]); - -// Line 974: State timer depends on controlVisibility -useEffect(() => { - const stateTimerData = calculateStateTimerData(currentTask, controlVisibility, agentId); - ... -}, [currentTask, controlVisibility, agentId]); - -// Line 982: Consult timer depends on controlVisibility -useEffect(() => { - const consultTimerData = calculateConsultTimerData(currentTask, controlVisibility, agentId); - ... -}, [currentTask, controlVisibility, agentId]); -``` - -**Migration impact:** `calculateStateTimerData()` and `calculateConsultTimerData()` in `timer-utils.ts` accept `controlVisibility` as a parameter. These must be updated to accept `TaskUIControls` instead (with new control names). - -### 3. `toggleMute` References Old Control Name - -```typescript -// Line 704-705: -if (!controlVisibility?.muteUnmute) { - logger.warn('Mute control not available', ...); - return; -} -``` - -**Migration:** Change to `controls.mute`. - -### 4. `wrapupCall` Post-Action State Management - -```typescript -// Lines 766-773: After wrapup, sets next task as current and updates agent state -.then(() => { - const taskKeys = Object.keys(store.taskList); - if (taskKeys.length > 0) { - store.setCurrentTask(store.taskList[taskKeys[0]]); - store.setState({ developerName: ENGAGED_LABEL, name: ENGAGED_USERNAME }); - } -}) -``` - -**Migration:** This logic stays. Post-wrapup task selection is a widget-layer concern. - -### 5. `consultTransfer` Uses `currentTask.data.isConferenceInProgress` - -```typescript -// Line 898: Decides between consultTransfer vs transferConference -if (currentTask.data.isConferenceInProgress) { - await currentTask.transferConference(); -} else { - await currentTask.consultTransfer(); -} -``` - -**Migration:** Can replace with `controls.transferConference.isVisible` to decide. But since SDK action methods are unchanged, keeping `data.isConferenceInProgress` is also fine. - -### 6. `extractConsultingAgent` — Complex Display Logic (KEEP) - -Lines 326-446: ~120 lines of logic to find the consulting agent's name from `interaction.participants` and `callProcessingDetails.consultDestinationAgentName`. This is display-only logic and NOT related to control visibility. **Keep as-is.** - -### 7. `useOutdialCall` — `isTelephonyTaskActive` Check - -```typescript -const isTelephonyTaskActive = useMemo(() => { - return Object.values(store.taskList).some( - (task) => task?.data?.interaction?.mediaType === MEDIA_TYPE_TELEPHONY_LOWER - ); -}, [store.taskList]); -``` - -**Migration:** Unaffected — this checks media type for outdial gating, not control visibility. - -### 8. UIControlConfig — SDK Builds It Internally - -Widgets do NOT need to provide UIControlConfig. The SDK builds it from: -- Agent profile → `isEndTaskEnabled`, `isEndConsultEnabled` -- `callProcessingDetails.pauseResumeEnabled` → `isRecordingEnabled` -- `interaction.mediaType` → `channelType` (voice/digital) -- Voice/WebRTC layer → `voiceVariant` (pstn/webrtc) -- `taskManager.setAgentId()` → `agentId` - -This means `deviceType`, `featureFlags`, and `conferenceEnabled` props can be removed from `useCallControlProps`. **Note:** `agentId` must be retained — it is still required by `calculateStateTimerData()` and `calculateConsultTimerData()` to look up the agent's participant record from `interaction.participants`. - -### 9. `task:wrapup` Race Condition - -SDK sample app uses `setTimeout(..., 0)` before updating UI after `task:wrapup`. Consider adding similar guard in hook if wrapup controls flicker. - ---- - -## Timer Utils Migration - -**File:** `task/src/Utils/timer-utils.ts` - -The `calculateStateTimerData()` and `calculateConsultTimerData()` functions accept `controlVisibility` as a parameter with old control names. These must be migrated: - -### Before -```typescript -export function calculateStateTimerData( - task: ITask, - controlVisibility: ReturnType, - agentId: string -) { - if (controlVisibility?.wrapup?.isVisible) { - return { label: 'Wrap Up', timestamp: task.data.wrapUpTimestamp }; - } - // Uses controlVisibility.isConsultInitiatedOrAccepted, controlVisibility.isHeld, etc. -} -``` - -### After -```typescript -export function calculateStateTimerData( - task: ITask, - controls: TaskUIControls, - agentId: string -) { - if (controls.wrapup.isVisible) { - return { label: 'Wrap Up', timestamp: task.data.wrapUpTimestamp }; - } - const isConsulting = controls.endConsult.isVisible; - // Use controls.hold, controls.endConsult, etc. -} -``` - ---- - -## Files to Modify - -| File | Action | -|------|--------| -| `task/src/helper.ts` | Refactor `useCallControl` as described above | -| `task/src/Utils/task-util.ts` | Remove or reduce (only keep `findHoldTimestamp`) | -| `task/src/Utils/timer-utils.ts` | Update to accept `TaskUIControls` instead of `controlVisibility` | -| `task/src/task.types.ts` | Update `useCallControlProps` return type | -| `task/tests/helper.ts` | Update all `useCallControl` tests | -| `cc-components/.../CallControl/call-control.tsx` | Update to accept new `controls` prop shape | -| `cc-components/.../CallControl/call-control.utils.ts` | Simplify (remove old control mapping) | - ---- - -## Validation Criteria - -- [ ] All 17 SDK controls render correctly in CallControl UI -- [ ] Hold toggle works (CONNECTED ↔ HELD) -- [ ] Mute toggle works (local WebRTC state) -- [ ] Recording toggle works (pause/resume) -- [ ] Consult flow: initiate → switch calls → end/transfer/conference -- [ ] Conference flow: merge → exit → transfer conference -- [ ] Wrapup flow: end → wrapup → complete -- [ ] Auto-wrapup timer works -- [ ] Hold timer displays correctly -- [ ] Digital channel shows only accept/end/transfer/wrapup -- [ ] All action methods still call correct SDK methods - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/005-incoming-task-migration.md b/ai-docs/migration/005-incoming-task-migration.md deleted file mode 100644 index ffaaf0dd5..000000000 --- a/ai-docs/migration/005-incoming-task-migration.md +++ /dev/null @@ -1,238 +0,0 @@ -# Migration Doc 005: IncomingTask Widget Migration - -## Summary - -The IncomingTask widget handles task offer/accept/reject flows. The state machine changes are minimal here since accept/decline SDK methods are unchanged. The main change is that the OFFERED → CONNECTED/TERMINATED transitions are now explicit state machine states, and `task.uiControls.accept`/`decline` can drive button visibility instead of widget-side logic. - ---- - -## Old Approach - -### Entry Point -**File:** `packages/contact-center/task/src/helper.ts` -**Hook:** `useIncomingTask(props: UseTaskProps)` - -### How It Works (Old) -1. Store sets `incomingTask` observable on `TASK_INCOMING` event -2. Widget (observer) re-renders when `incomingTask` changes -3. Hook registers per-task callbacks: `TASK_ASSIGNED`, `TASK_CONSULT_ACCEPTED`, `TASK_END`, `TASK_REJECT`, `TASK_CONSULT_END` -4. Accept → `task.accept()` → SDK → `TASK_ASSIGNED` → `onAccepted` callback -5. Reject → `task.decline()` → SDK → `TASK_REJECT` → `onRejected` callback -6. Timer expiry (RONA) → `reject()` → `task.decline()` -7. Accept/Decline button visibility computed by `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` in task-util.ts - ---- - -## New Approach - -### What Changes -1. **Accept/Decline visibility** → now available via `task.uiControls.accept` / `task.uiControls.decline` -2. **State machine states**: IDLE → OFFERED → (CONNECTED on accept | TERMINATED on reject/RONA) -3. **SDK methods unchanged**: `task.accept()`, `task.decline()` still work the same -4. **Events unchanged**: `TASK_ASSIGNED`, `TASK_REJECT` still emitted - -### Minimal Changes Required -- Replace `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` with `task.uiControls.accept` / `task.uiControls.decline` -- Optionally subscribe to `task:ui-controls-updated` for reactive updates -- Keep all callback registration as-is (accept/reject lifecycle callbacks) - ---- - -## Old → New Mapping - -| Aspect | Old | New | -|--------|-----|-----| -| Accept visible | `getAcceptButtonVisibility(isBrowser, isPhone, webRtc, isCall, isDigital)` | `task.uiControls.accept.isVisible` | -| Decline visible | `getDeclineButtonVisibility(isBrowser, webRtc, isCall)` | `task.uiControls.decline.isVisible` | -| Accept action | `task.accept()` | `task.accept()` (unchanged) | -| Decline action | `task.decline()` | `task.decline()` (unchanged) | -| Task assigned event | `TASK_EVENTS.TASK_ASSIGNED` | `TASK_EVENTS.TASK_ASSIGNED` (unchanged) | -| Task rejected event | `TASK_EVENTS.TASK_REJECT` | `TASK_EVENTS.TASK_REJECT` (unchanged) | -| Timer/RONA | Widget-managed timer | Widget-managed timer (unchanged) | - ---- - -## Refactor Pattern - -### Before -```typescript -// In IncomingTask component or hook -const { isBrowser, isPhoneDevice } = getDeviceTypeFlags(store.deviceType); -const acceptVisibility = getAcceptButtonVisibility( - isBrowser, isPhoneDevice, webRtcEnabled, isCall, isDigitalChannel -); -const declineVisibility = getDeclineButtonVisibility(isBrowser, webRtcEnabled, isCall); -``` - -### After -```typescript -// In IncomingTask component or hook -const task = store.incomingTask; -const acceptVisibility = task?.uiControls?.accept ?? { isVisible: false, isEnabled: false }; -const declineVisibility = task?.uiControls?.decline ?? { isVisible: false, isEnabled: false }; -``` - ---- - ---- - -## Full Before/After: `useIncomingTask` Hook - -### Before (current code in `helper.ts`) -```typescript -export const useIncomingTask = (props: UseTaskProps) => { - const {onAccepted, onRejected, deviceType, incomingTask, logger} = props; - const isBrowser = deviceType === 'BROWSER'; - const isDeclineButtonEnabled = store.isDeclineButtonEnabled; - - // Event callbacks registered per-task for accept/reject lifecycle - useEffect(() => { - if (!incomingTask) return; - store.setTaskCallback(TASK_EVENTS.TASK_ASSIGNED, () => { - if (onAccepted) onAccepted({task: incomingTask}); - }, incomingTask.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); - - return () => { - store.removeTaskCallback(TASK_EVENTS.TASK_ASSIGNED, taskAssignCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); - store.removeTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); - }; - }, [incomingTask]); - - const accept = () => { - if (!incomingTask?.data.interactionId) return; - incomingTask.accept().catch((error) => { /* log */ }); - }; - - const reject = () => { - if (!incomingTask?.data.interactionId) return; - incomingTask.decline().catch((error) => { /* log */ }); - }; - - return { - incomingTask, - accept, - reject, - isBrowser, // Used to determine accept/decline button visibility - isDeclineButtonEnabled, // Feature flag for decline button - }; -}; -``` - -**Note:** The `isBrowser` and `isDeclineButtonEnabled` flags are passed to the component, which uses them to decide whether to show accept/decline buttons. This duplicates what `task.uiControls.accept/decline` now provides. - -### After (migrated) -```typescript -export const useIncomingTask = (props: UseTaskProps) => { - const {onAccepted, onRejected, incomingTask, logger} = props; - - // NEW: Read accept/decline visibility from SDK - const acceptControl = incomingTask?.uiControls?.accept ?? {isVisible: false, isEnabled: false}; - const declineControl = incomingTask?.uiControls?.decline ?? {isVisible: false, isEnabled: false}; - - // Event callbacks — UNCHANGED (still need lifecycle callbacks for onAccepted/onRejected) - useEffect(() => { - if (!incomingTask) return; - store.setTaskCallback(TASK_EVENTS.TASK_ASSIGNED, () => { - if (onAccepted) onAccepted({task: incomingTask}); - }, incomingTask.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_ACCEPTED, taskAssignCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_END, taskRejectCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_REJECT, taskRejectCallback, incomingTask?.data.interactionId); - store.setTaskCallback(TASK_EVENTS.TASK_CONSULT_END, taskRejectCallback, incomingTask?.data.interactionId); - - return () => { /* cleanup — same as before */ }; - }, [incomingTask]); - - // Actions — UNCHANGED - const accept = () => { - if (!incomingTask?.data.interactionId) return; - incomingTask.accept().catch((error) => { /* log */ }); - }; - - const reject = () => { - if (!incomingTask?.data.interactionId) return; - incomingTask.decline().catch((error) => { /* log */ }); - }; - - return { - incomingTask, - accept, - reject, - acceptControl, // NEW: { isVisible, isEnabled } from SDK - declineControl, // NEW: { isVisible, isEnabled } from SDK - // REMOVED: isBrowser, isDeclineButtonEnabled (no longer needed) - }; -}; -``` - -### Component-Level Before/After - -#### Before (IncomingTaskComponent) -```tsx -// incoming-task.tsx — old approach -const IncomingTaskComponent = ({ isBrowser, isDeclineButtonEnabled, onAccept, onReject, ... }) => { - // Widget computes visibility from device type and feature flags - const showAccept = isBrowser; // simplified — actual logic in getAcceptButtonVisibility() - const showDecline = isBrowser && isDeclineButtonEnabled; - - return ( -
- {showAccept && } - {showDecline && } -
- ); -}; -``` - -#### After (IncomingTaskComponent) -```tsx -// incoming-task.tsx — new approach -const IncomingTaskComponent = ({ acceptControl, declineControl, onAccept, onReject, ... }) => { - // SDK provides exact visibility and enabled state - return ( -
- {acceptControl.isVisible && ( - - )} - {declineControl.isVisible && ( - - )} -
- ); -}; -``` - ---- - -## Files to Modify - -| File | Action | -|------|--------| -| `task/src/helper.ts` (`useIncomingTask`) | Use `task.uiControls.accept/decline` instead of visibility functions | -| `task/src/IncomingTask/index.tsx` | Minor: pass new control shape to component | -| `cc-components/.../IncomingTask/incoming-task.tsx` | Update accept/decline prop names if needed | -| `task/tests/IncomingTask/index.tsx` | Update tests | - ---- - -## Validation Criteria - -- [ ] Accept button visible for WebRTC voice tasks -- [ ] Accept button visible for digital channel tasks (chat/email) -- [ ] Decline button visible for WebRTC voice tasks only -- [ ] Accept action calls `task.accept()` and triggers `TASK_ASSIGNED` -- [ ] Decline action calls `task.decline()` and triggers `TASK_REJECT` -- [ ] RONA timer triggers reject correctly -- [ ] Consult incoming (OFFER_CONSULT) shows accept/decline correctly -- [ ] Cleanup on unmount removes callbacks - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/006-task-list-migration.md b/ai-docs/migration/006-task-list-migration.md deleted file mode 100644 index 3c51afae9..000000000 --- a/ai-docs/migration/006-task-list-migration.md +++ /dev/null @@ -1,150 +0,0 @@ -# Migration Doc 006: TaskList Widget Migration - -## Summary - -The TaskList widget displays all active tasks and allows accept/decline/select. Changes are minimal since task list management (add/remove tasks) stays in the store, and SDK methods are unchanged. The main opportunity is to simplify how task status is derived for display. - ---- - -## Old Approach - -### Entry Point -**File:** `packages/contact-center/task/src/helper.ts` -**Hook:** `useTaskList(props: UseTaskListProps)` - -### How It Works (Old) -1. Store maintains `taskList: Record` observable -2. Store maintains `currentTask: ITask | null` observable -3. Hook provides `acceptTask(task)`, `declineTask(task)`, `onTaskSelect(task)` actions -4. Task status derived via `getTaskStatus(task, agentId)` from store's `task-utils.ts` -5. Task display data extracted by `cc-components/task/Task/task.utils.ts` - ---- - -## New Approach - -### What Changes -1. **Task status derivation** can potentially use state machine state instead of `getTaskStatus()` -2. **Task list management** (add/remove) stays the same — store-managed -3. **SDK methods unchanged**: `task.accept()`, `task.decline()` -4. **Store callbacks unchanged**: `setTaskAssigned`, `setTaskRejected`, `setTaskSelected` - -### Minimal Changes Required -- If `getTaskStatus()` is used for display, consider using SDK task state info -- Accept/decline button visibility per task can use `task.uiControls.accept` -- Task selection logic unchanged - ---- - -## Old → New Mapping - -| Aspect | Old | New | -|--------|-----|-----| -| Task list source | `store.taskList` observable | `store.taskList` observable (unchanged) | -| Current task | `store.currentTask` observable | `store.currentTask` observable (unchanged) | -| Task status | `getTaskStatus(task, agentId)` from store utils | SDK task state or `task.uiControls` for button states | -| Accept action | `task.accept()` | `task.accept()` (unchanged) | -| Decline action | `task.decline()` | `task.decline()` (unchanged) | -| Select action | `store.setCurrentTask(task, isClicked)` | Unchanged | - ---- - ---- - -## Before/After: Per-Task Accept/Decline in TaskList - -### Before (TaskList component renders accept/decline per task) -```tsx -// task-list.tsx — old approach -const TaskListComponent = ({ taskList, isBrowser, onAccept, onDecline, onSelect }) => { - return taskList.map((task) => { - // Accept/decline visibility computed per-task from device type - const showAccept = isBrowser; // simplified - return ( - onSelect(task)}> - {showAccept && } - {showAccept && } - - ); - }); -}; -``` - -### After (use per-task `uiControls`) -```tsx -// task-list.tsx — new approach -const TaskListComponent = ({ taskList, onAccept, onDecline, onSelect }) => { - return taskList.map((task) => { - // SDK provides per-task control visibility - const acceptControl = task.uiControls?.accept ?? {isVisible: false, isEnabled: false}; - const declineControl = task.uiControls?.decline ?? {isVisible: false, isEnabled: false}; - return ( - onSelect(task)}> - {acceptControl.isVisible && ( - - )} - {declineControl.isVisible && ( - - )} - - ); - }); -}; -``` - -### Before/After: `useTaskList` Hook - -#### Before -```typescript -// helper.ts — useTaskList (abbreviated) -export const useTaskList = (props: UseTaskListProps) => { - const {deviceType, onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; - const isBrowser = deviceType === 'BROWSER'; // Used for accept/decline visibility - - // ... store callbacks and actions unchanged ... - - return {taskList, acceptTask, declineTask, onTaskSelect, isBrowser}; - // ^^^^^^^^^ passed to component -}; -``` - -#### After -```typescript -// helper.ts — useTaskList (migrated) -export const useTaskList = (props: UseTaskListProps) => { - const {onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; - // REMOVED: deviceType, isBrowser — no longer needed, SDK handles per-task visibility - - // ... store callbacks and actions unchanged ... - - return {taskList, acceptTask, declineTask, onTaskSelect}; - // REMOVED: isBrowser — each task.uiControls.accept/decline provides visibility -}; -``` - ---- - -## Files to Modify - -| File | Action | -|------|--------| -| `task/src/helper.ts` (`useTaskList`) | Remove `isBrowser`, use per-task `uiControls` for accept/decline | -| `task/src/TaskList/index.tsx` | Remove `isBrowser` prop pass-through | -| `cc-components/.../TaskList/task-list.tsx` | Use `task.uiControls.accept/decline` per task | -| `cc-components/.../Task/task.utils.ts` | Update task data extraction if status source changes | -| `store/src/task-utils.ts` (`getTaskStatus`) | Consider deprecation if SDK provides equivalent | - ---- - -## Validation Criteria - -- [ ] Task list displays all active tasks -- [ ] Task selection works (sets `currentTask`) -- [ ] Accept/decline per task works -- [ ] Task status displays correctly (connected, held, wrapup, etc.) -- [ ] Tasks removed from list on end/reject -- [ ] New incoming tasks appear in list - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/007-outdial-call-migration.md b/ai-docs/migration/007-outdial-call-migration.md deleted file mode 100644 index 79e3f9bef..000000000 --- a/ai-docs/migration/007-outdial-call-migration.md +++ /dev/null @@ -1,39 +0,0 @@ -# Migration Doc 007: OutdialCall Widget Migration - -## Summary - -OutdialCall is largely **unaffected** by the task state machine refactor. It initiates outbound calls via `cc.startOutdial()` (a CC-level method, not a task method) and fetches ANI entries. The resulting task, once created, is handled by IncomingTask/TaskList/CallControl. No state machine integration needed here. - ---- - -## Old Approach - -### Entry Point -**File:** `packages/contact-center/task/src/helper.ts` -**Hook:** `useOutdialCall(props: useOutdialCallProps)` - -### How It Works -1. Fetches ANI entries via `cc.getOutdialAniEntries()` -2. Validates phone number format -3. Initiates outbound call via `cc.startOutdial(destination, origin)` -4. Does NOT subscribe to any task events -5. Resulting task surfaces via `TASK_INCOMING` → IncomingTask widget - ---- - -## New Approach - -**No changes required.** The `cc.startOutdial()` method is a CC-level API, not a task-level API. The state machine is activated when the resulting task is created. - ---- - -## Validation Criteria - -- [ ] Outdial initiation works -- [ ] ANI entry fetching works -- [ ] Resulting task appears in task list via existing flow -- [ ] Phone number validation unchanged - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/008-store-task-utils-migration.md b/ai-docs/migration/008-store-task-utils-migration.md deleted file mode 100644 index 6e22fdeb4..000000000 --- a/ai-docs/migration/008-store-task-utils-migration.md +++ /dev/null @@ -1,231 +0,0 @@ -# Migration Doc 008: Store Task Utils Migration - -## Summary - -The store's `task-utils.ts` contains ~15 utility functions that inspect raw task data to derive state flags (consult status, hold status, conference state, participant info). Many of these become redundant when `task.uiControls` is the source of truth. This document maps which utils to keep, simplify, or remove. - ---- - -## Old Utilities Inventory - -**File:** `packages/contact-center/store/src/task-utils.ts` - -| Function | Purpose | Used By | -|----------|---------|---------| -| `isIncomingTask(task, agentId)` | Check if task is incoming | Store event wrapper | -| `getConsultMPCState(task, agentId)` | Consult multi-party conference state | Store, helpers | -| `isSecondaryAgent(task)` | Whether agent is secondary in consult | Store | -| `isSecondaryEpDnAgent(task)` | Whether agent is secondary EP-DN | Store | -| `getTaskStatus(task, agentId)` | Human-readable task status string | TaskList component | -| `getConsultStatus(task, agentId)` | `ConsultStatus` enum value | task-util.ts (controls), CallControl | -| `getIsConferenceInProgress(task)` | Boolean conference check | task-util.ts (controls) | -| `getConferenceParticipants(task, agentId)` | Filtered participant list | CallControl component | -| `getConferenceParticipantsCount(task)` | Participant count | task-util.ts (controls) | -| `getIsCustomerInCall(task)` | Whether customer is connected | task-util.ts (controls) | -| `getIsConsultInProgress(task)` | Whether consult is active | task-util.ts (controls) | -| `isInteractionOnHold(task)` | Whether any media is held | Task timer utils | -| `setmTypeForEPDN(task, mType)` | Media type for EP-DN agents | CallControl hook | -| `findMediaResourceId(task, mType)` | Media resource ID lookup | CallControl hook (switch calls) | -| `findHoldStatus(task, mType, agentId)` | Hold status by media type | task-util.ts (controls) | -| `findHoldTimestamp(task, mType)` | Hold timestamp for timers | Task timer, hold timer | - ---- - -## Migration Decisions - -### Remove (SDK handles via uiControls) - -| Function | Reason | SDK Replacement | -|----------|--------|-----------------| -| `getConsultStatus(task, agentId)` | Used only for control visibility computation | `task.uiControls` encodes all consult control states | -| `getIsConferenceInProgress(task)` | Used only for control visibility computation | `task.uiControls.exitConference.isVisible` | -| `getConferenceParticipantsCount(task)` | Used only for control visibility computation | SDK computes max participant check internally | -| `getIsCustomerInCall(task)` | Used only for control visibility computation | SDK computes internally | -| `getIsConsultInProgress(task)` | Used only for control visibility computation | SDK computes internally | -| ~~`findHoldStatus(task, mType, agentId)`~~ | ~~Used for control visibility~~ | **MOVED TO KEEP** — still needed for `getTaskStatus()` held-state derivation and component layer `isHeld` (see below) | - -### Keep (Widget-layer concerns) - -| Function | Reason | -|----------|--------| -| `isIncomingTask(task, agentId)` | Store needs this for routing incoming tasks | -| `getTaskStatus(task, agentId)` | TaskList display needs human-readable status | -| `getConferenceParticipants(task, agentId)` | CallControl UI shows participant list (display, not control visibility) | -| `isInteractionOnHold(task)` | Timer logic needs this | -| `findMediaResourceId(task, mType)` | Switch-call actions need media resource IDs | -| `findHoldTimestamp(task, mType)` | Hold timer needs timestamp | -| `findHoldStatus(task, mType, agentId)` | Needed for `getTaskStatus()` held-state derivation and component layer `isHeld` — cannot derive from `controls.hold.isEnabled` | - -### Review (may simplify) - -| Function | Consideration | -|----------|--------------| -| `isSecondaryAgent(task)` | May be replaceable by SDK context | -| `isSecondaryEpDnAgent(task)` | May be replaceable by SDK context | -| `getConsultMPCState(task, agentId)` | Review if still needed with SDK handling consult state | -| `setmTypeForEPDN(task, mType)` | Review if SDK simplifies this | - ---- - -## Old → New Mapping - -| Old Function | Status | New Equivalent | -|-------------|--------|---------------| -| `getConsultStatus()` | **REMOVE** | `task.uiControls.endConsult`, `task.uiControls.switchToMainCall` etc. | -| `getIsConferenceInProgress()` | **REMOVE** | `task.uiControls.exitConference.isVisible` | -| `getConferenceParticipantsCount()` | **REMOVE** | SDK internal check | -| `getIsCustomerInCall()` | **REMOVE** | SDK internal check | -| `getIsConsultInProgress()` | **REMOVE** | SDK internal check | -| `findHoldStatus()` | **KEEP** | Still needed for `getTaskStatus()` held-state derivation and component layer `isHeld` — do NOT derive from `controls.hold` | -| `isIncomingTask()` | **KEEP** | — | -| `getTaskStatus()` | **KEEP** | Could enhance with SDK TaskState | -| `getConferenceParticipants()` | **KEEP** | Display only | -| `isInteractionOnHold()` | **KEEP** | Timer display | -| `findMediaResourceId()` | **KEEP** | Action parameter | -| `findHoldTimestamp()` | **KEEP** | Timer display | - ---- - ---- - -## Before/After: Removing `getConsultStatus` Usage - -### Before (consumed in `task-util.ts::getControlsVisibility`) -```typescript -// task-util.ts — old approach -import { getConsultStatus, ConsultStatus, getIsConsultInProgress, getIsCustomerInCall, - getConferenceParticipantsCount, findHoldStatus } from '@webex/cc-store'; - -export function getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled) { - // Derive consult status by inspecting raw task data - const taskConsultStatus = getConsultStatus(task, agentId); - const isConsultInitiated = taskConsultStatus === ConsultStatus.CONSULT_INITIATED; - const isConsultAccepted = taskConsultStatus === ConsultStatus.CONSULT_ACCEPTED; - const isBeingConsulted = taskConsultStatus === ConsultStatus.BEING_CONSULTED_ACCEPTED; - const isConsultCompleted = taskConsultStatus === ConsultStatus.CONSULT_COMPLETED; - - // Derive hold status from raw task media - const isHeld = findHoldStatus(task, 'mainCall', agentId); - const consultCallHeld = findHoldStatus(task, 'consult', agentId); - - // Derive conference state from raw task data - const isConferenceInProgress = task?.data?.isConferenceInProgress ?? false; - const isConsultInProgress = getIsConsultInProgress(task); - const isCustomerInCall = getIsCustomerInCall(task); - const conferenceParticipantsCount = getConferenceParticipantsCount(task); - - // 20+ individual visibility functions using these derived states... - return { /* 22 controls + 7 state flags */ }; -} -``` - -### After (all above replaced by `task.uiControls`) -```typescript -// task-util.ts — DELETED or reduced to only keep timer/hold helpers: -// findHoldTimestamp() — retained in task-util.ts for hold timer display -// findHoldStatus() — retained in task-util.ts for isHeld derivation (used by getTaskStatus, component layer) -// All other functions (getConsultStatus, getIsConsultInProgress, getConferenceParticipantsCount, etc.) — DELETED - -// In useCallControl hook — no imports from store task-utils for controls: -const controls = currentTask?.uiControls ?? getDefaultUIControls(); -// All 17 controls come pre-computed from SDK. Zero store util calls needed. -``` - -### Before/After: `findHoldStatus` — RETAINED (not removed) - -#### Before (used in controls computation) -```typescript -// store/task-utils.ts -export function findHoldStatus(task: ITask, mType: string, agentId: string): boolean { - if (!task?.data?.interaction?.media) return false; - const media = Object.values(task.data.interaction.media).find(m => m.mType === mType); - if (!media?.participants) return false; - const participant = task.data.interaction.participants[agentId]; - return participant?.isHold ?? false; -} - -// task-util.ts — consumed for control visibility -const isHeld = findHoldStatus(task, 'mainCall', agentId); -const consultCallHeld = findHoldStatus(task, 'consult', agentId); -``` - -#### After -```typescript -// REMOVED from store/task-utils.ts — SDK tracks hold state in TaskContext: -// - task.uiControls.hold.isEnabled indicates holdable -// - task.uiControls.switchToConsult.isVisible indicates consult call is held -// No widget-side derivation needed. -``` - -### Before/After: `getTaskStatus` (KEPT but enhanced) - -#### Before -```typescript -// store/task-utils.ts — returns human-readable status -export function getTaskStatus(task: ITask, agentId: string): string { - if (task.data.interaction?.isTerminated) return 'Wrap Up'; - const consultStatus = getConsultStatus(task, agentId); - if (consultStatus === ConsultStatus.CONSULT_INITIATED) return 'Consulting'; - if (task.data.isConferenceInProgress) return 'Conference'; - if (findHoldStatus(task, 'mainCall', agentId)) return 'Held'; - return 'Connected'; -} -``` - -#### After (enhanced with SDK controls) -```typescript -// store/task-utils.ts — can now derive status from uiControls -export function getTaskStatus(task: ITask, agentId: string): string { - const controls = task.uiControls; - if (!controls) return 'Unknown'; - if (controls.wrapup.isVisible) return 'Wrap Up'; - if (controls.endConsult.isVisible) return 'Consulting'; - if (controls.exitConference.isVisible) return 'Conference'; - // NOTE: Do NOT derive held state from controls.hold.isEnabled — hold can be - // disabled in consult/transition states even when call is not held. - // Use task data instead (agentId needed for participant lookup): - if (findHoldStatus(task, 'mainCall', agentId)) return 'Held'; - if (controls.end.isVisible) return 'Connected'; - if (controls.accept.isVisible) return 'Offered'; - return 'Unknown'; -} -``` - ---- - -## `findHoldTimestamp` Signature Mismatch (Pre-existing Issue) - -**Discovery:** Two different `findHoldTimestamp` functions exist with different signatures: - -| Location | Signature | Used By | -|----------|-----------|---------| -| `store/src/task-utils.ts` | `findHoldTimestamp(task: ITask, mType: string)` | `timer-utils.ts` | -| `task/src/Utils/task-util.ts` | `findHoldTimestamp(interaction: Interaction, mType: string)` | `useHoldTimer.ts` | - -Both should be consolidated during migration. Recommend keeping only the store version (accepts `ITask`) for consistency. - ---- - -## Files to Modify - -| File | Action | -|------|--------| -| `store/src/task-utils.ts` | Remove 6 functions, keep 6, review 4 | -| `store/src/constants.ts` | Remove consult state constants if unused | -| `task/src/Utils/task-util.ts` | Major reduction (imports from store utils) | -| All consumers of removed functions | Update imports, switch to `task.uiControls` | - ---- - -## Validation Criteria - -- [ ] Removed functions have no remaining consumers -- [ ] Kept functions still work correctly -- [ ] TaskList status display unchanged -- [ ] Conference participant display unchanged -- [ ] Hold timer unchanged -- [ ] Switch-call media resource IDs work - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/010-component-layer-migration.md b/ai-docs/migration/010-component-layer-migration.md deleted file mode 100644 index 453d533a3..000000000 --- a/ai-docs/migration/010-component-layer-migration.md +++ /dev/null @@ -1,563 +0,0 @@ -# Migration Doc 010: Component Layer (`cc-components`) Migration - -## Summary - -The `cc-components` package contains the presentational React components for task widgets. These components receive control visibility as props. The prop interface must be updated to match the new `TaskUIControls` shape from SDK (renamed controls, merged controls, removed state flags). - ---- - -## Components to Update - -### CallControlComponent -**File:** `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` - -#### Old Prop Names → New Prop Names - -| Old Prop | New Prop | Change | -|----------|----------|--------| -| `holdResume` | `hold` | **Rename** | -| `muteUnmute` | `mute` | **Rename** | -| `pauseResumeRecording` | `recording` | **Rename** — toggle button (pause/resume) | -| `recordingIndicator` | `recording` | **Same SDK control** — widget must preserve separate recording status badge UI. Use `recording.isVisible` for badge, `recording.isEnabled` for toggle | -| `mergeConference` | `mergeToConference` | **Rename** | -| `consultTransferConsult` | `transfer` / `transferConference` | **Split** — use `transfer` for consult transfer, `transferConference` for conference transfer | -| `mergeConferenceConsult` | — | **Remove** (use `mergeToConference`) | -| `muteUnmuteConsult` | — | **Remove** (use `mute`) | -| `isConferenceInProgress` | — | **Remove** (derive from controls) | -| `isConsultInitiated` | — | **Remove** (derive from controls) | -| `isConsultInitiatedAndAccepted` | — | **Remove** | -| `isConsultReceived` | — | **Remove** | -| `isConsultInitiatedOrAccepted` | — | **Remove** | -| `isHeld` | — | **Remove** (derive from controls) | -| `consultCallHeld` | — | **Remove** | - -#### Proposed New Interface - -```typescript -interface CallControlComponentProps { - controls: TaskUIControls; // All 17 controls from SDK - // Widget-layer state (not from SDK) - isMuted: boolean; - isRecording: boolean; - holdTime: number; - secondsUntilAutoWrapup: number; - buddyAgents: Agent[]; - consultAgentName: string; - // Actions - onToggleHold: () => void; - onToggleMute: () => void; - onToggleRecording: () => void; - onEndCall: () => void; - onWrapupCall: (reason: string, auxCodeId: string) => void; - onTransferCall: (payload: TransferPayLoad) => void; - onConsultCall: (payload: ConsultPayload) => void; - onEndConsultCall: () => void; - onConsultTransfer: () => void; - onConsultConference: () => void; - onExitConference: () => void; - onSwitchToConsult: () => void; - onSwitchToMainCall: () => void; - onCancelAutoWrapup: () => void; -} -``` - -### CallControlConsult -**File:** `packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx` - -- Update to use `controls.endConsult`, `controls.mergeToConference`, `controls.switchToMainCall`, `controls.switchToConsult` -- Remove separate `consultTransferConsult`, `mergeConferenceConsult`, `muteUnmuteConsult` props - -### IncomingTaskComponent -**File:** `packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx` - -- Accept: `controls.accept.isVisible` / `controls.accept.isEnabled` -- Decline: `controls.decline.isVisible` / `controls.decline.isEnabled` -- Minimal changes — shape is compatible - -### TaskListComponent -**File:** `packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx` - -- Per-task accept/decline: use `task.uiControls.accept` / `task.uiControls.decline` -- Task status display: may use existing `getTaskStatus()` or enhance - -### OutdialCallComponent -**File:** `packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx` - -- **No changes needed** — OutdialCall does not use task controls - ---- - ---- - -## Full Before/After: CallControlComponent - -### Before -```tsx -// call-control.tsx — old approach -const CallControlComponent = ({ - // 22 individual control props - accept, decline, end, muteUnmute, holdResume, - pauseResumeRecording, recordingIndicator, - transfer, conference, exitConference, mergeConference, - consult, endConsult, consultTransfer, consultTransferConsult, - mergeConferenceConsult, muteUnmuteConsult, - switchToMainCall, switchToConsult, wrapup, - // 7 state flags - isConferenceInProgress, isConsultInitiated, - isConsultInitiatedAndAccepted, isConsultReceived, - isConsultInitiatedOrAccepted, isHeld, consultCallHeld, - // Actions and hook state - isMuted, isRecording, holdTime, onToggleHold, onToggleMute, ... -}) => { - return ( -
- {holdResume.isVisible && ( - - )} - {muteUnmute.isVisible && ( - - )} - {end.isVisible && ( - - )} - {/* Consult sub-controls */} - {isConsultInitiatedOrAccepted && ( -
- {endConsult.isVisible && } - {consultTransferConsult.isVisible && } - {mergeConferenceConsult.isVisible && } - {muteUnmuteConsult.isVisible && } -
- )} - {/* Conference sub-controls */} - {isConferenceInProgress && ( -
- {exitConference.isVisible && } - {mergeConference.isVisible && } -
- )} -
- ); -}; -``` - -### After -```tsx -// call-control.tsx — new approach -const CallControlComponent = ({ - controls, // TaskUIControls — all 17 controls from SDK - isMuted, isRecording, holdTime, - onToggleHold, onToggleMute, onEndCall, onEndConsult, - onConsultTransfer, onConsultConference, onExitConference, - onSwitchToMainCall, onSwitchToConsult, ... -}: CallControlComponentProps) => { - // Derive display-only flags from controls (replaces old state flag props) - const isConsulting = controls.endConsult.isVisible; - const isConferencing = controls.exitConference.isVisible; - - return ( -
- {controls.hold.isVisible && ( - - )} - {controls.mute.isVisible && ( - - )} - {controls.end.isVisible && ( - - )} - {/* Transfer and Consult initiation */} - {controls.transfer.isVisible && ( - - )} - {controls.consult.isVisible && ( - - )} - {/* Active consult controls */} - {controls.endConsult.isVisible && ( - - )} - {controls.mergeToConference.isVisible && ( - - )} - {controls.switchToMainCall.isVisible && ( - - )} - {controls.switchToConsult.isVisible && ( - - )} - {/* Conference controls */} - {controls.exitConference.isVisible && ( - - )} - {controls.transferConference.isVisible && ( - - )} - {/* Recording */} - {controls.recording.isVisible && ( - - )} - {/* Wrapup */} - {controls.wrapup.isVisible && ( - - )} -
- ); -}; -``` - ---- - -## Deriving State Flags from Controls - -Components that previously relied on state flags can derive them: - -```typescript -// Old: isConferenceInProgress (boolean prop) -// New: derive from controls -const isConferenceInProgress = controls.exitConference.isVisible; - -// Old: isConsultInitiatedOrAccepted (boolean prop) -// New: derive from controls -const isConsulting = controls.endConsult.isVisible; - -// Old: isHeld (boolean state flag from getControlsVisibility) -// New: derive from task data, NOT from control enabled state -// IMPORTANT: Do NOT use `controls.hold.isEnabled` to determine held state — -// hold can be disabled in consult/transition states even when call is not held. -const isHeld = findHoldStatus(currentTask, 'mainCall', agentId); -// (Uses task.data.interaction.participants to check actual hold state) -``` - ---- - -## Newly Discovered Files (Deep Scan) - -### 1. `ControlVisibility` Type — The Central Type to Replace - -**File:** `cc-components/src/components/task/task.types.ts` (lines 702-730) - -```typescript -// OLD: ControlVisibility interface (22 controls + 7 state flags) -export interface ControlVisibility { - accept: Visibility; - decline: Visibility; - end: Visibility; - muteUnmute: Visibility; // → mute - muteUnmuteConsult: Visibility; // → REMOVE (use mute) - holdResume: Visibility; // → hold - consult: Visibility; - transfer: Visibility; - conference: Visibility; - wrapup: Visibility; - pauseResumeRecording: Visibility; // → recording - endConsult: Visibility; - recordingIndicator: Visibility; // → REMOVE (merged into recording) - exitConference: Visibility; - mergeConference: Visibility; // → mergeToConference - consultTransfer: Visibility; - mergeConferenceConsult: Visibility; // → REMOVE (use mergeToConference) - consultTransferConsult: Visibility; // → REMOVE (use transfer) - switchToMainCall: Visibility; - switchToConsult: Visibility; - isConferenceInProgress: boolean; // → derive from controls.exitConference.isVisible - isConsultInitiated: boolean; // → derive from controls.endConsult.isVisible - isConsultInitiatedAndAccepted: boolean; // → REMOVE - isConsultReceived: boolean; // → REMOVE - isConsultInitiatedOrAccepted: boolean; // → REMOVE - isHeld: boolean; // → derive from controls.hold - consultCallHeld: boolean; // → derive from controls.switchToConsult.isVisible -} -``` - -**Migration:** Replace with `TaskUIControls` import from SDK. The `ControlVisibility` interface and all its consumers must be updated. - -### 2. `buildCallControlButtons()` — CRITICAL File - -**File:** `cc-components/.../CallControl/call-control.utils.ts` (line 192) - -This function builds the main call control button array. It references **12 old control names and 2 state flags**: - -| Old Reference | New Equivalent | -|--------------|---------------| -| `controlVisibility.muteUnmute.isVisible` | `controls.mute.isVisible` | -| `controlVisibility.switchToConsult.isEnabled` | `controls.switchToConsult.isEnabled` | -| `controlVisibility.isHeld` | Derive from task data: `findHoldStatus(task, 'mainCall', agentId)` — do NOT derive from `controls.hold.isEnabled` | -| `controlVisibility.holdResume.isEnabled` | `controls.hold.isEnabled` | -| `controlVisibility.holdResume.isVisible` | `controls.hold.isVisible` | -| `controlVisibility.consult.isEnabled` | `controls.consult.isEnabled` | -| `controlVisibility.consult.isVisible` | `controls.consult.isVisible` | -| `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | -| `controlVisibility.consultTransfer.isEnabled` | `controls.consultTransfer.isEnabled` | -| `controlVisibility.consultTransfer.isVisible` | `controls.consultTransfer.isVisible` | -| `controlVisibility.mergeConference.isEnabled` | `controls.mergeToConference.isEnabled` | -| `controlVisibility.mergeConference.isVisible` | `controls.mergeToConference.isVisible` | -| `controlVisibility.transfer.isEnabled` | `controls.transfer.isEnabled` | -| `controlVisibility.transfer.isVisible` | `controls.transfer.isVisible` | -| `controlVisibility.pauseResumeRecording.isEnabled` | `controls.recording.isEnabled` | -| `controlVisibility.pauseResumeRecording.isVisible` | `controls.recording.isVisible` | -| `controlVisibility.exitConference.isEnabled` | `controls.exitConference.isEnabled` | -| `controlVisibility.exitConference.isVisible` | `controls.exitConference.isVisible` | -| `controlVisibility.end.isEnabled` | `controls.end.isEnabled` | -| `controlVisibility.end.isVisible` | `controls.end.isVisible` | - -#### Before -```typescript -export const buildCallControlButtons = ( - isMuted, isRecording, isMuteButtonDisabled, currentMediaType, - controlVisibility: ControlVisibility, // OLD type - ...handlers -): CallControlButton[] => { - return [ - { id: 'mute', isVisible: controlVisibility.muteUnmute.isVisible, ... }, - { id: 'hold', icon: controlVisibility.isHeld ? 'play-bold' : 'pause-bold', - disabled: !controlVisibility.holdResume.isEnabled, - isVisible: controlVisibility.holdResume.isVisible, ... }, - { id: 'record', isVisible: controlVisibility.pauseResumeRecording.isVisible, ... }, - // ... 10 more buttons using old names - ]; -}; -``` - -#### After -```typescript -export const buildCallControlButtons = ( - isMuted, isRecording, isMuteButtonDisabled, currentMediaType, - controls: TaskUIControls, // NEW type from SDK - ...handlers -): CallControlButton[] => { - const isHeld = findHoldStatus(currentTask, 'mainCall', agentId); // from task data, NOT control state - const isConferencing = controls.exitConference.isVisible; - return [ - { id: 'mute', isVisible: controls.mute.isVisible, ... }, - { id: 'hold', icon: isHeld ? 'play-bold' : 'pause-bold', - disabled: !controls.hold.isEnabled, - isVisible: controls.hold.isVisible, ... }, - { id: 'record', isVisible: controls.recording.isVisible, ... }, - // ... 10 more buttons using new names - ]; -}; -``` - -### 3. `createConsultButtons()` — CRITICAL File - -**File:** `cc-components/.../CallControlCustom/call-control-custom.utils.ts` (line 16) - -This function builds the consult-mode button array. It references **5 old control names and 1 state flag**: - -| Old Reference | New Equivalent | -|--------------|---------------| -| `controlVisibility.muteUnmuteConsult` | `controls.mute` | -| `controlVisibility.switchToMainCall` | `controls.switchToMainCall` | -| `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | -| `controlVisibility.consultTransferConsult` | `controls.transfer` (consult) / `controls.transferConference` (conference) | -| `controlVisibility.mergeConferenceConsult` | `controls.mergeToConference` | -| `controlVisibility.endConsult` | `controls.endConsult` | - -#### Before -```typescript -export const createConsultButtons = ( - isMuted, controlVisibility: ControlVisibility, ...handlers -): ButtonConfig[] => { - return [ - { key: 'mute', isVisible: controlVisibility.muteUnmuteConsult.isVisible, - disabled: !controlVisibility.muteUnmuteConsult.isEnabled }, - { key: 'switchToMainCall', tooltip: controlVisibility.isConferenceInProgress ? 'Switch to Conference Call' : 'Switch to Call', - isVisible: controlVisibility.switchToMainCall.isVisible }, - { key: 'transfer', isVisible: controlVisibility.consultTransferConsult.isVisible }, - { key: 'conference', isVisible: controlVisibility.mergeConferenceConsult.isVisible }, - { key: 'cancel', isVisible: controlVisibility.endConsult.isVisible }, - ]; -}; -``` - -#### After -```typescript -export const createConsultButtons = ( - isMuted, controls: TaskUIControls, ...handlers -): ButtonConfig[] => { - const isConferencing = controls.exitConference.isVisible; - return [ - { key: 'mute', isVisible: controls.mute.isVisible, - disabled: !controls.mute.isEnabled }, - { key: 'switchToMainCall', tooltip: isConferencing ? 'Switch to Conference Call' : 'Switch to Call', - isVisible: controls.switchToMainCall.isVisible }, - { key: 'transfer', isVisible: controls.transfer.isVisible }, - { key: 'conference', isVisible: controls.mergeToConference.isVisible }, - { key: 'cancel', isVisible: controls.endConsult.isVisible }, - ]; -}; -``` - -### 4. `CallControlCAD` Widget — Props to Remove - -**File:** `task/src/CallControlCAD/index.tsx` - -This is an **alternative CallControl widget** (CAD = Call Associated Data). It passes `deviceType`, `featureFlags`, `agentId`, and `conferenceEnabled` from the store to `useCallControl`. When `useCallControlProps` is updated, `deviceType`, `featureFlags`, and `conferenceEnabled` will be removed. `agentId` is retained — it is still needed for timer participant lookup. - -#### Before -```tsx -const { deviceType, featureFlags, isMuted, agentId } = store; -const result = { - ...useCallControl({ - currentTask, onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, - logger, deviceType, featureFlags, isMuted, conferenceEnabled, agentId - }), - // ... -}; -return ; -``` - -#### After -```tsx -const { isMuted } = store; -const result = { - ...useCallControl({ - currentTask, onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, - logger, isMuted, agentId // agentId RETAINED — needed for timer participant lookup - // REMOVED: deviceType, featureFlags, conferenceEnabled - }), - // ... -}; -return ; -``` - -### 5. `CallControlConsultComponentsProps` — Uses `controlVisibility` - -**File:** `cc-components/.../task/task.types.ts` (line 640) - -```typescript -// OLD -export interface CallControlConsultComponentsProps { - // ... - controlVisibility: ControlVisibility; // → controls: TaskUIControls - // ... -} -``` - -### 6. `ConsultTransferPopoverComponentProps` — Uses `isConferenceInProgress` - -**File:** `cc-components/.../task/task.types.ts` (line 617) - -```typescript -// OLD -export interface ConsultTransferPopoverComponentProps { - // ... - isConferenceInProgress?: boolean; // → derive from controls.exitConference.isVisible - // ... -} -``` - -### 7. `ControlProps` — The Master Interface - -**File:** `cc-components/.../task/task.types.ts` (line 159) - -Has these migration-affected fields: -- `controlVisibility: ControlVisibility` (line 441) → `controls: TaskUIControls` -- `isHeld: boolean` (line 256) → derive from `controls.hold` -- `deviceType: string` (line 251) → REMOVE (SDK handles) -- `featureFlags: {[key: string]: boolean}` (line 389) → REMOVE (SDK handles) -- `conferenceEnabled: boolean` (line 429) → REMOVE (SDK handles) -- `agentId: string` (line 472) → RETAIN (needed for timer participant lookup, not just controls) - -### 8. `CallControlComponentProps` — Picks `controlVisibility` - -**File:** `cc-components/.../task/task.types.ts` (line 475) - -```typescript -// OLD: Picks 'controlVisibility' from ControlProps -export type CallControlComponentProps = Pick; - -// NEW: Replace 'controlVisibility' with 'controls' -// Also can remove some picked props that came from the old approach -``` - -### 9. `filterButtonsForConsultation()` — Uses `consultInitiated` Flag - -**File:** `cc-components/.../call-control.utils.ts` (line 324) - -```typescript -// OLD -export const filterButtonsForConsultation = (buttons, consultInitiated, isTelephony) => { - return consultInitiated && isTelephony - ? buttons.filter((button) => !['hold', 'consult'].includes(button.id)) - : buttons; -}; - -// NEW: derive consultInitiated from controls.endConsult.isVisible -``` - -### 10. `getConsultStatusText()` — Uses `consultInitiated` Flag - -**File:** `cc-components/.../call-control-custom.utils.ts` (line 126) - -```typescript -// OLD -export const getConsultStatusText = (consultInitiated: boolean) => { - return consultInitiated ? 'Consult requested' : 'Consulting'; -}; - -// NEW: derive from controls.endConsult.isVisible && !controls.mergeToConference.isEnabled -``` - -### 11. Files NOT Impacted (Confirmed) - -| File | Reason | -|------|--------| -| `AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only — no control refs | -| `AutoWrapupTimer.utils.ts` | Pure timer formatting — no control refs | -| `consult-transfer-popover-hooks.ts` | Pagination/search logic — no control refs | -| `consult-transfer-list-item.tsx` | Display only — no control refs | -| `consult-transfer-dial-number.tsx` | Input handling — no control refs | -| `consult-transfer-empty-state.tsx` | Display only — no control refs | -| `TaskTimer/index.tsx` | Timer display — no control refs | -| `Task/index.tsx` | Task card display — no control refs | -| `OutdialCall/outdial-call.tsx` | No task controls used | - ---- - -## Files to Modify (Updated) - -| File | Action | Impact | -|------|--------|--------| -| `cc-components/.../task/task.types.ts` | Replace `ControlVisibility` with `TaskUIControls`; update `ControlProps`, `CallControlComponentProps`, `CallControlConsultComponentsProps`, `ConsultTransferPopoverComponentProps` | **HIGH** — central type file | -| `cc-components/.../CallControl/call-control.tsx` | Update to use `controls` prop | **HIGH** | -| `cc-components/.../CallControl/call-control.utils.ts` | Update `buildCallControlButtons()` and `filterButtonsForConsultation()` to use new control names | **HIGH** — 20+ old control references | -| `cc-components/.../CallControlCustom/call-control-custom.utils.ts` | Update `createConsultButtons()` and `getConsultStatusText()` to use new control names | **HIGH** — 6 old control references | -| `cc-components/.../CallControlCustom/call-control-consult.tsx` | Update consult control props | **MEDIUM** | -| `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Update `isConferenceInProgress` prop | **LOW** | -| `cc-components/.../IncomingTask/incoming-task.tsx` | Minor prop updates | **LOW** | -| `cc-components/.../TaskList/task-list.tsx` | Minor prop updates | **LOW** | -| `task/src/CallControlCAD/index.tsx` | Remove `deviceType`, `featureFlags`, `conferenceEnabled` from `useCallControl` call (retain `agentId` for timers) | **MEDIUM** | -| All test files for above | Update mocks and assertions | **HIGH** | - ---- - -## Validation Criteria - -- [ ] CallControl renders all 17 controls correctly -- [ ] Consult sub-controls (endConsult, merge, switch) render correctly -- [ ] Conference sub-controls (exit, transfer conference) render correctly -- [ ] State flag derivation works for conditional rendering -- [ ] IncomingTask accept/decline render correctly -- [ ] TaskList per-task controls render correctly -- [ ] CallControlCAD works with simplified props -- [ ] `buildCallControlButtons()` returns correct buttons for all states -- [ ] `createConsultButtons()` returns correct buttons for consult state -- [ ] No TypeScript compilation errors -- [ ] All component tests pass - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/011-execution-plan.md b/ai-docs/migration/011-execution-plan.md deleted file mode 100644 index 9588b141c..000000000 --- a/ai-docs/migration/011-execution-plan.md +++ /dev/null @@ -1,438 +0,0 @@ -# Migration Doc 011: Execution Plan (Spec-First) - -## Overview - -This plan uses the **spec-driven development** approach already established in CC Widgets. Each milestone starts with writing specs (tests), then implementing to make specs pass. This ensures parity between old and new behavior. - ---- - -## Prerequisites - -- [ ] CC SDK `task-refactor` branch merged and released (or linked locally) -- [ ] `TaskUIControls` type and `task:ui-controls-updated` event available in `@webex/contact-center` -- [ ] `task.uiControls` getter available on `ITask` -- [ ] Team alignment on migration approach - ---- - -## Milestone Overview - -| # | Milestone | Scope | Est. Effort | Risk | Depends On | -|---|-----------|-------|-------------|------|------------| -| M0 | SDK integration setup | Link SDK, verify types | 1 day | Low | SDK release | -| M1 | Types & constants alignment | Import new types, add adapters | 1-2 days | Low | M0 | -| M2 | Store event wiring simplification | Simplify event handlers, add `ui-controls-updated` | 2-3 days | Medium | M0 | -| M3 | Store task-utils thinning | Remove redundant utils | 1-2 days | Low | M2 | -| M3.5 | Timer utils migration | Update timer-utils to accept `TaskUIControls` | 1 day | Low | M3 | -| M4 | CallControl hook refactor | Core: replace `getControlsVisibility` with `task.uiControls` | 3-5 days | **High** | M1, M2, M3, M3.5 | -| M5 | Component layer update | Update `cc-components` prop interfaces | 2-3 days | Medium | M4 | -| M6 | IncomingTask migration | Use `task.uiControls.accept/decline` | 1 day | Low | M1 | -| M7 | TaskList migration | Optional status enhancement | 1 day | Low | M1 | -| M8 | Integration testing & cleanup | E2E, remove dead code, docs | 2-3 days | Medium | All | - -**Total estimated effort: 15–23 days** - ---- - -## Detailed Milestone Plans - -### M0: SDK Integration Setup (1 day) - -**Goal:** Verify the new SDK API is available and types compile. - -**Steps:** -1. Update `@webex/contact-center` dependency to task-refactor version -2. Verify `TaskUIControls` type is importable -3. Verify `task.uiControls` getter exists on `ITask` -4. Verify `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` constant exists -5. Run `yarn build` to confirm no type errors - -**Spec:** Write a minimal integration test that creates a mock task and reads `uiControls`. - -**Validation:** `yarn build` passes with new SDK version. - ---- - -### M1: Types & Constants Alignment (1-2 days) - -**Ref:** [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) - -**Goal:** Import new SDK types into widget packages without changing runtime behavior. - -**Spec first:** -```typescript -// Test: TaskUIControls type compatibility -import { TaskUIControls } from '@webex/contact-center'; -const controls: TaskUIControls = task.uiControls; -expect(controls.hold).toHaveProperty('isVisible'); -expect(controls.hold).toHaveProperty('isEnabled'); -``` - -**Steps:** -1. Add `TaskUIControls` import to `task/src/task.types.ts` -2. Create adapter type mapping old control names → new (for gradual migration) -3. Add `TASK_UI_CONTROLS_UPDATED` to store event constants -4. Review and annotate constants for deprecation - -**Validation:** All existing tests still pass. New types compile. - ---- - -### M2: Store Event Wiring Simplification (2-3 days) - -**Ref:** [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) - -**Goal:** Simplify store event handlers; add `task:ui-controls-updated` subscription. - -**Spec first:** -```typescript -// Test: Store registers ui-controls-updated listener -describe('registerTaskEventListeners', () => { - it('should register TASK_UI_CONTROLS_UPDATED handler', () => { - store.registerTaskEventListeners(mockTask); - expect(mockTask.on).toHaveBeenCalledWith( - TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, expect.any(Function) - ); - }); - - it('should NOT call refreshTaskList on TASK_HOLD', () => { - // Verify simplified handler - }); -}); -``` - -**Steps:** -1. Add `TASK_UI_CONTROLS_UPDATED` handler in `registerTaskEventListeners()` -2. Replace `refreshTaskList()` calls with callback-only for: TASK_HOLD, TASK_RESUME, TASK_CONSULT_END, all conference events -3. Keep `refreshTaskList()` only for: initialization, hydration -4. Update tests for each modified handler - -**Order (low risk → high risk):** -1. Add new `TASK_UI_CONTROLS_UPDATED` handler (additive, no breakage) -2. Simplify conference event handlers (less critical) -3. Simplify hold/resume handlers (medium impact) -4. Simplify consult handlers (medium impact) -5. Remove unnecessary `refreshTaskList()` calls (highest impact) - -**Validation:** All existing widget tests pass. Store correctly fires callbacks on events. - ---- - -### M3: Store Task-Utils Thinning (1-2 days) - -**Ref:** [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) - -**Goal:** Remove utility functions that are now handled by SDK. - -**Spec first:** -```typescript -// Test: Verify no consumers remain for removed functions -// (Static analysis — ensure no import of getConsultStatus, getIsConferenceInProgress, etc.) -``` - -**Steps:** -1. Search codebase for each function to verify consumers -2. Remove functions with zero consumers after M2 changes -3. Mark functions with remaining consumers for later removal (after M4/M5) -4. Keep display-only functions (`getTaskStatus`, `getConferenceParticipants`, etc.) - -**Validation:** Build succeeds. No runtime errors. - ---- - -### M3.5: Timer Utils Migration (1 day) - -**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md#timer-utils-migration) - -**Goal:** Update `calculateStateTimerData()` and `calculateConsultTimerData()` to accept `TaskUIControls`. - -**Why:** These functions accept `controlVisibility` (old shape) as a parameter and derive timer labels from it. They must be migrated before M4 since `useCallControl` depends on them. - -**Spec first:** -```typescript -describe('calculateStateTimerData with TaskUIControls', () => { - it('should return Wrap Up label when controls.wrapup.isVisible', () => { - const controls = { ...getDefaultUIControls(), wrapup: { isVisible: true, isEnabled: true } }; - const result = calculateStateTimerData(mockTask, controls, agentId); - expect(result.label).toBe('Wrap Up'); - }); -}); -``` - -**Steps:** -1. Update `calculateStateTimerData(task, controls, agentId)` signature -2. Replace `controlVisibility.isConsultInitiatedOrAccepted` → `controls.endConsult.isVisible` -3. Replace `controlVisibility.isHeld` → derive from task data via `findHoldStatus(task, 'mainCall', agentId)` (do NOT derive from `controls.hold.isEnabled` — hold can be disabled in consult/transition states even when call is not held) -4. Update `calculateConsultTimerData(task, controls, agentId)` similarly -5. Update all test cases - -**Also fix during this milestone:** -- Consolidate `findHoldTimestamp` dual signatures (store vs task-util versions) - ---- - -### M4: CallControl Hook Refactor (3-5 days) — CRITICAL PATH - -**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) - -**Goal:** Replace `getControlsVisibility()` with `task.uiControls` in `useCallControl`. - -**Spec first (write ALL specs before implementation):** - -```typescript -describe('useCallControl with task.uiControls', () => { - // Parity specs: each scenario must produce identical control states - - describe('connected voice call', () => { - it('should show hold, mute, end, transfer, consult controls', () => { - mockTask.uiControls = { - hold: { isVisible: true, isEnabled: true }, - mute: { isVisible: true, isEnabled: true }, - end: { isVisible: true, isEnabled: true }, - transfer: { isVisible: true, isEnabled: true }, - consult: { isVisible: true, isEnabled: true }, - // ... all other controls disabled - }; - const { result } = renderHook(() => useCallControl(props)); - expect(result.current.controls.hold).toEqual({ isVisible: true, isEnabled: true }); - }); - }); - - describe('held voice call', () => { - it('should show hold (enabled=true for resume), disable end/mute', () => { /* ... */ }); - }); - - describe('consulting', () => { - it('should show endConsult, switchToMainCall, switchToConsult, mergeToConference', () => { /* ... */ }); - }); - - describe('conferencing', () => { - it('should show exitConference, disable hold', () => { /* ... */ }); - }); - - describe('wrapping up', () => { - it('should show only wrapup control', () => { /* ... */ }); - }); - - describe('digital channel', () => { - it('should show only accept, end, transfer, wrapup', () => { /* ... */ }); - }); - - describe('ui-controls-updated event', () => { - it('should re-render when task emits ui-controls-updated', () => { /* ... */ }); - }); - - describe('no task', () => { - it('should return default controls when no task', () => { /* ... */ }); - }); -}); -``` - -**Steps:** -1. Write comprehensive parity specs (30+ test cases covering all states) -2. Create `adaptSDKControls()` adapter function (maps SDK names to old names if needed during transition) -3. Replace `getControlsVisibility()` call in `useCallControl` with `task.uiControls` -4. Add `task:ui-controls-updated` subscription with `useEffect` -5. Update hook return type to use new control names -6. Remove old state flags from return -7. Run parity specs — fix any mismatches - -**Parity verification approach:** -- For each state (connected, held, consulting, conferencing, wrapping-up, offered): - - Mock task with known data - - Call old `getControlsVisibility()` → capture result - - Read `task.uiControls` → capture result - - Compare: every control must have same `isVisible`/`isEnabled` - - Document and resolve any differences (old bug vs new behavior) - -**Validation:** All 30+ parity specs pass. All existing hook tests pass (with updated assertions). - ---- - -### M5: Component Layer Update (2-3 days) - -**Ref:** [010-component-layer-migration.md](./010-component-layer-migration.md) - -**Goal:** Update `cc-components` to accept new control prop shape. - -**Spec first:** -```typescript -describe('CallControlComponent', () => { - it('should render hold button from controls.hold', () => { - render(); - expect(screen.getByTestId('hold-button')).toBeVisible(); - }); - - it('should hide mute button when controls.mute.isVisible=false', () => { - const controls = { ...mockControls, mute: { isVisible: false, isEnabled: false } }; - render(); - expect(screen.queryByTestId('mute-button')).not.toBeInTheDocument(); - }); -}); -``` - -**Steps:** -1. Update `CallControlComponentProps` to accept `controls: TaskUIControls` -2. Update `CallControlComponent` to read from `controls.*` -3. Update `CallControlConsult` component -4. Update `IncomingTaskComponent` if needed -5. Update all component tests - -**Validation:** All component tests pass. Visual output identical. - ---- - -### M6: IncomingTask Migration (1 day) - -**Ref:** [005-incoming-task-migration.md](./005-incoming-task-migration.md) - -**Goal:** Use `task.uiControls.accept/decline` in IncomingTask. - -**Spec first:** -```typescript -describe('useIncomingTask with uiControls', () => { - it('should derive accept visibility from task.uiControls.accept', () => { /* ... */ }); - it('should derive decline visibility from task.uiControls.decline', () => { /* ... */ }); -}); -``` - -**Steps:** -1. Replace `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` with `task.uiControls` -2. Update component props -3. Update tests - -**Validation:** IncomingTask tests pass. Accept/decline work for voice and digital. - ---- - -### M7: TaskList Migration (1 day) - -**Ref:** [006-task-list-migration.md](./006-task-list-migration.md) - -**Goal:** Optionally enhance task status display; verify compatibility. - -**Steps:** -1. Verify `useTaskList` works with new SDK (should be compatible) -2. Optionally enhance `getTaskStatus()` to use SDK state info -3. Update tests if any changes made - -**Validation:** TaskList renders correctly with all task states. - ---- - -### M8: Integration Testing & Cleanup (2-3 days) - -**Goal:** End-to-end verification, dead code removal, documentation update. - -**Steps:** - -1. **E2E Test Matrix:** - -| Scenario | Widgets Involved | Verify | -|----------|-----------------|--------| -| Incoming voice call → accept → end | IncomingTask, CallControl | Accept button, end button | -| Incoming voice call → reject | IncomingTask | Decline button, RONA timer | -| Connected → hold → resume | CallControl | Hold toggle, timer | -| Connected → consult → end consult | CallControl | Consult flow controls | -| Connected → consult → conference | CallControl | Merge, conference controls | -| Conference → exit | CallControl | Exit conference | -| Conference → transfer conference | CallControl | Transfer conference | -| Connected → transfer (blind) | CallControl | Transfer popover | -| Connected → end → wrapup | CallControl | Wrapup button | -| Outdial → connected → end | OutdialCall, CallControl | Full outdial flow | -| Digital task → accept → end → wrapup | IncomingTask, CallControl | Digital controls | -| Multiple tasks in list | TaskList | Task selection, per-task controls | -| Page refresh → hydrate | All | Restore state correctly | - -2. **Bug fixes (found during analysis):** - - Fix recording callback cleanup mismatch (`TASK_RECORDING_PAUSED` vs `CONTACT_RECORDING_PAUSED`) - - Consolidate `findHoldTimestamp` dual signatures (store vs task-util) - - Add `task:wrapup` race guard if needed - -3. **Dead code removal:** - - Delete `task/src/Utils/task-util.ts` (or reduce to `findHoldTimestamp` only) - - Remove unused store utils - - Remove unused constants - - Remove unused type definitions - -4. **Documentation updates:** - - Update `task/ai-docs/widgets/CallControl/AGENTS.md` and `ARCHITECTURE.md` - - Update `task/ai-docs/widgets/IncomingTask/AGENTS.md` and `ARCHITECTURE.md` - - Update `store/ai-docs/AGENTS.md` and `ARCHITECTURE.md` - - Update `cc-components/ai-docs/AGENTS.md` - -5. **Final validation:** - - `yarn build` — no errors - - `yarn test:unit` — all pass - - `yarn test:styles` — no lint errors - - Sample apps (React + WC) work correctly - ---- - -## Risk Mitigation - -### High-Risk Areas -1. **CallControl hook refactor (M4)** — largest change, most complex logic - - **Mitigation:** Comprehensive parity specs written BEFORE implementation - - **Rollback:** Old `getControlsVisibility()` stays in codebase until M8 cleanup - -2. **Store event wiring (M2)** — removing `refreshTaskList()` could cause stale data - - **Mitigation:** Gradual removal; keep `refreshTaskList()` as fallback initially - - **Rollback:** Re-add `refreshTaskList()` calls if data staleness detected - -3. **Consult/Conference flows** — most complex state transitions - - **Mitigation:** Dedicated parity specs for every consult/conference scenario - - **Mitigation:** Test with Agent Desktop to verify identical behavior - -### Low-Risk Areas -- OutdialCall (no changes needed) -- IncomingTask (minimal changes) -- TaskList (minimal changes) -- Types alignment (additive, no runtime changes) - ---- - -## Spec-First Checklist - -For each milestone, complete these in order: - -1. [ ] Write spec file with test cases for NEW behavior -2. [ ] Write parity tests (old behavior == new behavior) where applicable -3. [ ] Run specs — verify they FAIL (red) -4. [ ] Implement changes -5. [ ] Run specs — verify they PASS (green) -6. [ ] Run ALL existing tests — verify no regressions -7. [ ] `yarn build` — verify compilation -8. [ ] Code review with team -9. [ ] Mark milestone complete - ---- - -## Recommended Order of Execution - -``` -M0 (SDK setup) - │ - ├── M1 (types) ─┐ - └── M2 (store events) ─┤ - │ - M3 (store utils) - │ - M3.5 (timer utils) - │ - M4 (CallControl hook) ← CRITICAL PATH - │ - M5 (components) - │ - ├── M6 (IncomingTask) - └── M7 (TaskList) - │ - M8 (integration + cleanup + bug fixes) -``` - -**M0 → M1 + M2 (parallel) → M3 → M3.5 → M4 → M5 → M6 + M7 (parallel) → M8** - ---- - -_Created: 2026-03-09_ -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md deleted file mode 100644 index 0337952a6..000000000 --- a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md +++ /dev/null @@ -1,664 +0,0 @@ -# Migration Doc 012: Task Lifecycle Flows — Complete Old vs New - -## Purpose - -This document traces **every task scenario from start to finish**, showing exactly what happens at each step in both the old and new approach. Each flow maps: -- User/system action -- SDK event chain -- Widget/store layer behavior -- UI controls shown -- State machine state (new only) - ---- - -## Flow 1: Incoming Voice Call → Accept → Connected - -### Old Flow -``` -1. WebSocket: AgentContactReserved -2. SDK emits: task:incoming (with ITask) -3. Store: handleIncomingTask() → refreshTaskList() → cc.taskManager.getAllTasks() - → runInAction: store.taskList updated, store.incomingTask set -4. Widget: IncomingTask (observer) re-renders -5. Hook: useIncomingTask registers callbacks (TASK_ASSIGNED, TASK_REJECT, etc.) -6. UI Controls: getAcceptButtonVisibility(isBrowser, isPhoneDevice, webRtcEnabled, isCall, isDigitalChannel) - getDeclineButtonVisibility(isBrowser, webRtcEnabled, isCall) -7. User clicks Accept -8. Hook: incomingTask.accept() → SDK API call -9. WebSocket: AgentContactAssigned -10. SDK emits: task:assigned -11. Store: handleTaskAssigned() → refreshTaskList() → update taskList, set currentTask -12. Hook: TASK_ASSIGNED callback fires → onAccepted({task}) -13. Widget: CallControl appears -14. UI Controls: getControlsVisibility() computes all 22 controls from raw task data - → hold, mute, end, transfer, consult visible and enabled -``` - -### New Flow -``` -1. WebSocket: AgentContactReserved -2. SDK: TaskManager maps to TaskEvent.TASK_INCOMING -3. SDK: task.sendStateMachineEvent(TASK_INCOMING) → State: IDLE → OFFERED -4. SDK: computeUIControls(OFFERED, context) → accept/decline visible (WebRTC) -5. SDK emits: task:incoming, task:ui-controls-updated -6. Store: handleIncomingTask() → store.incomingTask set -7. Widget: IncomingTask (observer) re-renders -8. Hook: useIncomingTask reads task.uiControls.accept / task.uiControls.decline -9. User clicks Accept -10. Hook: incomingTask.accept() → SDK API call -11. WebSocket: AgentContactAssigned -12. SDK: TaskManager maps to TaskEvent.ASSIGN -13. SDK: task.sendStateMachineEvent(ASSIGN) → State: OFFERED → CONNECTED -14. SDK: computeUIControls(CONNECTED, context) → hold, mute, end, transfer, consult -15. SDK emits: task:assigned, task:ui-controls-updated -16. Store: handleTaskAssigned() → set currentTask -17. Widget: CallControl appears -18. Hook: useCallControl reads task.uiControls directly (no computation) -``` - -### Key Difference -| Step | Old | New | -|------|-----|-----| -| Controls computation | Widget runs `getControlsVisibility()` on every render | SDK pre-computes `task.uiControls` on every state transition | -| Data freshness | `refreshTaskList()` re-fetches all tasks | SDK updates `task.data` in state machine action | -| Re-render trigger | MobX observable change after `refreshTaskList()` | `task:ui-controls-updated` event | - ---- - -## Flow 2: Incoming Voice Call → Reject / RONA (Timeout) - -### Old Flow -``` -1-6. Same as Flow 1 (incoming → show accept/decline) -7. User clicks Decline (or timer expires → auto-reject) -8. Hook: incomingTask.decline() → SDK API call -9. WebSocket: AgentContactReservedTimeout (RONA) or rejection -10. SDK emits: task:rejected -11. Store: handleTaskReject() → refreshTaskList() → remove task from list -12. Hook: TASK_REJECT callback fires → onRejected({task}) -13. Widget: IncomingTask unmounts -``` - -### New Flow -``` -1-8. Same as Flow 1 new (incoming → OFFERED state) -7. User clicks Decline (or timer expires → auto-reject) -8. Hook: incomingTask.decline() → SDK API call -9. WebSocket: RONA or rejection -10. SDK: task.sendStateMachineEvent(RONA) → State: OFFERED → TERMINATED -11. SDK: computeUIControls(TERMINATED) → all controls disabled -12. SDK emits: task:rejected, task:ui-controls-updated -13. Store: handleTaskReject() → remove task from list -14. Hook: TASK_REJECT callback fires → onRejected({task}) -15. Widget: IncomingTask unmounts -``` - ---- - -## Flow 3: Connected → Hold → Resume - -### Old Flow -``` -1. User clicks Hold -2. Hook: toggleHold(true) → currentTask.hold() -3. SDK: API call to backend -4. WebSocket: AgentContactHeld -5. SDK emits: task:hold -6. Store: refreshTaskList() → cc.taskManager.getAllTasks() → update store.taskList -7. Hook: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) -8. UI Controls: getControlsVisibility() recalculates: - → findHoldStatus(task, 'mainCall', agentId) returns true - → holdResume: { isVisible: true, isEnabled: true } (for resume) - → end: { isVisible: true, isEnabled: false } (disabled while held) - → mute: same -9. User clicks Resume -10. Hook: toggleHold(false) → currentTask.resume() -11. WebSocket: AgentContactUnheld -12. SDK emits: task:resume -13. Store: refreshTaskList() → update store.taskList -14. Hook: TASK_RESUME callback fires → onHoldResume({ isHeld: false }) -15. UI Controls: getControlsVisibility() recalculates → controls back to connected state -``` - -### New Flow -``` -1. User clicks Hold -2. Hook: toggleHold(true) → currentTask.hold() -3. SDK: sends TaskEvent.HOLD_INITIATED → State: CONNECTED → HOLD_INITIATING -4. SDK: computeUIControls(HOLD_INITIATING) → hold visible but transitioning -5. SDK emits: task:ui-controls-updated (optimistic) -6. SDK: API call to backend -7. WebSocket: AgentContactHeld -8. SDK: sends TaskEvent.HOLD_SUCCESS → State: HOLD_INITIATING → HELD -9. SDK: computeUIControls(HELD) → hold visible (for resume), end/mute disabled -10. SDK emits: task:hold, task:ui-controls-updated -11. Store: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) -12. Hook: controls state updated via task:ui-controls-updated listener -13. User clicks Resume -14. Hook: toggleHold(false) → currentTask.resume() -15. SDK: sends TaskEvent.UNHOLD_INITIATED → State: HELD → RESUME_INITIATING -16. SDK: API call to backend -17. WebSocket: AgentContactUnheld -18. SDK: sends TaskEvent.UNHOLD_SUCCESS → State: RESUME_INITIATING → CONNECTED -19. SDK: computeUIControls(CONNECTED) → all active controls enabled -20. SDK emits: task:resume, task:ui-controls-updated -21. Store: TASK_RESUME callback fires -22. Hook: controls state updated -``` - -### Key Difference -| Step | Old | New | -|------|-----|-----| -| Hold initiation | Immediate API call, wait for response | Optimistic: HOLD_INITIATING state before API call | -| Intermediate states | None (binary: held or not) | HOLD_INITIATING, RESUME_INITIATING (UI can show spinner) | -| Controls update | After `refreshTaskList()` + `getControlsVisibility()` | After each state transition via `task:ui-controls-updated` | - ---- - -## Flow 4: Connected → Consult → End Consult - -### Old Flow -``` -1. User initiates consult -2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) -3. SDK: API call to backend -4. WebSocket: AgentConsultCreated -5. SDK emits: task:consultCreated -6. Store: handleConsultCreated() → refreshTaskList() → update taskList -7. UI Controls: getControlsVisibility() recalculates: - → getConsultStatus() returns CONSULT_INITIATED - → endConsult visible, consultTransfer visible, switchToMainCall visible - → hold disabled, transfer hidden -8. WebSocket: AgentConsulting (consult agent answered) -9. SDK emits: task:consulting -10. Store: handleConsulting() → refreshTaskList() -11. UI Controls: getControlsVisibility() recalculates: - → getConsultStatus() returns CONSULT_ACCEPTED - → mergeConference enabled, consultTransfer enabled - → switchToMainCall/switchToConsult available -12. User clicks End Consult -13. Hook: endConsultCall() → currentTask.endConsult(payload) -14. WebSocket: AgentConsultEnded -15. SDK emits: task:consultEnd -16. Store: refreshTaskList() -17. UI Controls: getControlsVisibility() → back to connected state controls -``` - -### New Flow -``` -1. User initiates consult -2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) -3. SDK: sends TaskEvent.CONSULT → State: CONNECTED → CONSULT_INITIATING -4. SDK: computeUIControls(CONSULT_INITIATING) → consult controls transitioning -5. SDK emits: task:ui-controls-updated -6. SDK: API call → success -7. SDK: sends TaskEvent.CONSULT_SUCCESS → stays CONSULT_INITIATING (waiting for agent) -8. WebSocket: AgentConsultCreated → TaskEvent.CONSULT_CREATED -9. SDK: task data updated -10. WebSocket: AgentConsulting → TaskEvent.CONSULTING_ACTIVE -11. SDK: State: CONSULT_INITIATING → CONSULTING -12. SDK: context.consultDestinationAgentJoined = true -13. SDK: computeUIControls(CONSULTING): - → endConsult visible+enabled, mergeToConference visible+enabled - → switchToMainCall visible, switchToConsult visible - → transfer visible (for consult transfer) - → hold disabled (in consult) -14. SDK emits: task:consulting, task:ui-controls-updated -15. Hook: controls updated via listener -16. User clicks End Consult -17. Hook: endConsultCall() → currentTask.endConsult(payload) -18. WebSocket: AgentConsultEnded → TaskEvent.CONSULT_END -19. SDK: State: CONSULTING → CONNECTED (or CONFERENCING if from conference) -20. SDK: context cleared (consultInitiator=false, consultDestinationAgentJoined=false) -21. SDK: computeUIControls(CONNECTED) → back to normal connected controls -22. SDK emits: task:consultEnd, task:ui-controls-updated -23. Hook: controls updated -``` - -### Key Difference -| Step | Old | New | -|------|-----|-----| -| Consult state tracking | `getConsultStatus()` inspects participants | State machine: CONSULT_INITIATING → CONSULTING | -| Agent joined detection | `ConsultStatus.CONSULT_ACCEPTED` from participant flags | `context.consultDestinationAgentJoined` set by action | -| Controls | Computed from raw data every render | Pre-computed on each state transition | - ---- - -## Flow 5: Consulting → Merge to Conference → Exit Conference - -### Old Flow -``` -1. In consulting state (Flow 4 steps 1-11) -2. User clicks Merge Conference -3. Hook: consultConference() → currentTask.consultConference() -4. SDK: API call -5. WebSocket: AgentConsultConferenced / ParticipantJoinedConference -6. SDK emits: task:conferenceStarted / task:participantJoined -7. Store: handleConferenceStarted() → refreshTaskList() -8. UI Controls: getControlsVisibility(): - → task.data.isConferenceInProgress = true - → exitConference visible+enabled - → consult visible+enabled (can add more agents) - → hold disabled (in conference) - → mergeConference hidden (already in conference) -9. User clicks Exit Conference -10. Hook: exitConference() → currentTask.exitConference() -11. WebSocket: ParticipantLeftConference / AgentConsultConferenceEnded -12. SDK emits: task:conferenceEnded / task:participantLeft -13. Store: handleConferenceEnded() → refreshTaskList() -14. UI Controls: getControlsVisibility() → may go to wrapup or connected -``` - -### New Flow -``` -1. In CONSULTING state (Flow 4 new steps 1-15) -2. User clicks Merge Conference -3. Hook: consultConference() → currentTask.consultConference() -4. SDK: sends TaskEvent.MERGE_TO_CONFERENCE → State: CONSULTING → CONF_INITIATING -5. SDK: computeUIControls(CONF_INITIATING) → transitioning controls -6. SDK emits: task:ui-controls-updated -7. WebSocket: AgentConsultConferenced → TaskEvent.CONFERENCE_START -8. SDK: State: CONF_INITIATING → CONFERENCING -9. SDK: computeUIControls(CONFERENCING): - → exitConference visible+enabled - → consult visible (can add more) - → hold disabled - → mergeToConference hidden -10. SDK emits: task:conferenceStarted, task:ui-controls-updated -11. User clicks Exit Conference -12. Hook: exitConference() → currentTask.exitConference() -13. WebSocket: ParticipantLeftConference → TaskEvent.PARTICIPANT_LEAVE -14. SDK: guards check: didCurrentAgentLeaveConference? shouldWrapUp? -15. SDK: State: CONFERENCING → WRAPPING_UP (if wrapup required) or CONNECTED -16. SDK: computeUIControls for new state -17. SDK emits: task:conferenceEnded, task:ui-controls-updated -``` - ---- - -## Flow 6: Connected → End → Wrapup → Complete - -### Old Flow -``` -1. User clicks End -2. Hook: endCall() → currentTask.end() -3. SDK: API call -4. WebSocket: ContactEnded + AgentWrapup -5. SDK emits: task:end, task:wrapup -6. Store: handleTaskEnd() may refresh; handleWrapup → refreshTaskList() -7. UI Controls: getControlsVisibility(): - → getWrapupButtonVisibility(task) → { isVisible: task.data.wrapUpRequired } - → all other controls hidden/disabled -8. Widget: Wrapup form appears -9. User selects reason, clicks Submit -10. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) -11. SDK: API call -12. WebSocket: AgentWrappedup -13. SDK emits: task:wrappedup -14. Store: refreshTaskList() → task removed or updated -15. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) -16. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) -``` - -### New Flow -``` -1. User clicks End -2. Hook: endCall() → currentTask.end() -3. SDK: API call -4. WebSocket: ContactEnded → TaskEvent.CONTACT_ENDED -5. SDK: guards: shouldWrapUp? → State: CONNECTED → WRAPPING_UP -6. SDK: computeUIControls(WRAPPING_UP) → only wrapup visible+enabled -7. SDK emits: task:end, task:wrapup, task:ui-controls-updated -8. Hook: controls updated → only wrapup control shown -9. Widget: Wrapup form appears -10. User selects reason, clicks Submit -11. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) -12. SDK: API call -13. WebSocket: AgentWrappedup → TaskEvent.WRAPUP_COMPLETE -14. SDK: State: WRAPPING_UP → COMPLETED -15. SDK: computeUIControls(COMPLETED) → all controls disabled -16. SDK: emitTaskWrappedup action → cleanupResources action -17. SDK emits: AGENT_WRAPPEDUP (AgentWrappedUp), task:ui-controls-updated, task:cleanup -18. Store: handleTaskEnd/cleanup → remove task from list -19. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) -20. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) -``` - ---- - -## Flow 7: Auto-Wrapup Timer - -### Old Flow -``` -1. Task enters wrapup state (Flow 6 steps 1-7) -2. Hook: useEffect detects currentTask.autoWrapup && controlVisibility.wrapup -3. Hook: reads currentTask.autoWrapup.getTimeLeftSeconds() -4. Hook: setInterval every 1s → decrements secondsUntilAutoWrapup -5. Widget: AutoWrapupTimer component shows countdown -6. If user clicks Cancel: cancelAutoWrapup() → currentTask.cancelAutoWrapupTimer() -7. If timer reaches 0: SDK auto-submits wrapup with default reason -``` - -### New Flow -``` -1. Task enters WRAPPING_UP state (Flow 6 new steps 1-8) -2. Hook: useEffect detects currentTask.autoWrapup && controls.wrapup.isVisible - (changed: controlVisibility.wrapup → controls.wrapup) -3-7. Same as old — auto-wrapup is a widget-layer timer concern, SDK unchanged -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Timer trigger condition | `controlVisibility.wrapup` (computed by widget) | `controls.wrapup.isVisible` (from SDK) | -| Timer behavior | Unchanged | Unchanged | -| Cancel action | Unchanged | Unchanged | - ---- - -## Flow 8: Recording Pause/Resume - -### Old Flow -``` -1. User clicks Pause Recording -2. Hook: toggleRecording() → currentTask.pauseRecording() -3. SDK: API call -4. WebSocket: ContactRecordingPaused -5. SDK emits: task:recordingPaused -6. Store: fires callback -7. Hook: pauseRecordingCallback() → setIsRecording(false), onRecordingToggle({isRecording: false}) -8. UI Controls: getPauseResumeRecordingButtonVisibility() unchanged (still visible) -9. Widget: Button label changes to "Resume Recording" -``` - -### New Flow -``` -1. User clicks Pause Recording -2. Hook: toggleRecording() → currentTask.pauseRecording() -3. SDK: sends TaskEvent.PAUSE_RECORDING → context.recordingInProgress = false -4. SDK: computeUIControls → recording: { isVisible: true, isEnabled: true } - (visible and enabled = agent can click to resume recording) -5. SDK: API call -6. WebSocket: ContactRecordingPaused -7. SDK emits: task:recordingPaused, task:ui-controls-updated -8. Hook: setIsRecording(false), controls updated -9. Widget: Button label changes to "Resume Recording" -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Recording state tracking | Widget local state (`isRecording`) | SDK context (`recordingInProgress`) + widget local state | -| Recording button visibility | `getPauseResumeRecordingButtonVisibility()` | `controls.recording` from SDK | -| Recording indicator | Separate `recordingIndicator` control | Merged into `recording` control | - ---- - -## Flow 9: Blind Transfer - -### Old Flow -``` -1. User clicks Transfer, selects destination -2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) -3. SDK: API call -4. WebSocket: AgentBlindTransferred -5. SDK emits: (leads to task:end or task:wrapup depending on config) -6. Store: refreshTaskList() -7. UI Controls: getControlsVisibility() → wrapup or all disabled -``` - -### New Flow -``` -1. User clicks Transfer, selects destination -2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) -3. SDK: API call -4. WebSocket: AgentBlindTransferred → TaskEvent.TRANSFER_SUCCESS -5. SDK: guards: shouldWrapUpOrIsInitiator? -6. SDK: State → WRAPPING_UP (if wrapup required) or stays CONNECTED -7. SDK: computeUIControls for new state -8. SDK emits: task:end/task:wrapup, task:ui-controls-updated -``` - ---- - -## Flow 10: Digital Task (Chat/Email) — Accept → End → Wrapup - -### Old Flow -``` -1. WebSocket: AgentContactReserved (mediaType: chat) -2. SDK emits: task:incoming -3. Store: handleIncomingTask() -4. UI Controls: getAcceptButtonVisibility(): - → isBrowser && isDigitalChannel → accept visible - → decline NOT visible (digital channels) -5. User clicks Accept -6. incomingTask.accept() → task:assigned -7. UI Controls: getControlsVisibility(): - → end visible, transfer visible, wrapup hidden - → hold/mute/consult/conference/recording: all hidden (digital) -8. User clicks End -9. currentTask.end() → task:end, task:wrapup -10. UI Controls: wrapup visible (if wrapUpRequired) -``` - -### New Flow -``` -1. WebSocket: AgentContactReserved (mediaType: chat) -2. SDK: State: IDLE → OFFERED, channelType: DIGITAL -3. SDK: computeDigitalUIControls(OFFERED) → accept visible, decline hidden -4. SDK emits: task:incoming, task:ui-controls-updated -5. User clicks Accept -6. incomingTask.accept() → ASSIGN → State: OFFERED → CONNECTED -7. SDK: computeDigitalUIControls(CONNECTED) → end visible, transfer visible - → hold/mute/consult/conference/recording: all disabled (digital) -8. User clicks End -9. currentTask.end() → CONTACT_ENDED → WRAPPING_UP -10. SDK: computeDigitalUIControls(WRAPPING_UP) → wrapup visible -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Channel detection | Widget checks `mediaType === 'telephony'` vs chat/email | SDK checks `channelType: VOICE` vs `DIGITAL` | -| Digital controls | `getControlsVisibility()` with `isCall=false, isDigitalChannel=true` | `computeDigitalUIControls()` — much simpler logic | -| Controls shown | Same end result | Same end result (accept, end, transfer, wrapup only) | - ---- - -## Flow 11: Page Refresh → Hydration - -### Old Flow -``` -1. Agent refreshes browser page -2. SDK reconnects, receives AgentContact for active task -3. SDK emits: task:hydrate -4. Store: handleTaskHydrate() → refreshTaskList() → cc.taskManager.getAllTasks() -5. Store: sets currentTask, taskList from fetched data -6. Widgets: observer re-renders with restored task -7. UI Controls: getControlsVisibility() computes from raw task data - → Must correctly derive: held state, consult state, conference state - → Error-prone: depends on raw interaction data being complete -``` - -### New Flow -``` -1. Agent refreshes browser page -2. SDK reconnects, receives AgentContact for active task -3. SDK: TaskManager sends TaskEvent.HYDRATE with task data -4. SDK: State machine guards determine correct state: - → isInteractionTerminated? → WRAPPING_UP - → isInteractionConsulting? → CONSULTING - → isInteractionHeld? → HELD - → isInteractionConnected? → CONNECTED - → isConferencingByParticipants? → CONFERENCING - → default → IDLE (data update only) -5. SDK: computeUIControls for resolved state → correct controls for restored state -6. SDK emits: task:hydrate, task:ui-controls-updated -7. Store: handleTaskHydrate() → set currentTask, taskList -8. Widgets: observer re-renders; controls from task.uiControls are correct -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| State recovery | `refreshTaskList()` + `getControlsVisibility()` from raw data | State machine guards determine correct state | -| Reliability | Can show wrong controls if interaction data is incomplete | Guards explicitly check each condition; predictable | -| Conference recovery | Depends on `isConferenceInProgress` flag in data | Guard: `isConferencingByParticipants` counts agents | - ---- - -## Flow 12: Outdial → New Task - -### Old Flow -``` -1. User enters number, clicks Dial -2. Hook: startOutdial(destination, origin) → cc.startOutdial(destination, origin) -3. SDK: API call (CC-level, not task-level) -4. Backend creates task, sends AgentContactReserved -5. Flow continues as Flow 1 (Incoming → Accept → Connected) -``` - -### New Flow -``` -Identical — outdial initiation is CC-level. Once the task is created, -the state machine takes over and flows follow Flow 1 new approach. -No changes needed. -``` - ---- - -## Flow 13: Consult Transfer (from Consulting State) - -### Old Flow -``` -1. In consulting state (Flow 4 steps 1-11) -2. User clicks Consult Transfer -3. Hook: consultTransfer() checks currentTask.data.isConferenceInProgress: - → false: currentTask.consultTransfer() - → true: currentTask.transferConference() -4. SDK: API call -5. WebSocket: AgentConsultTransferred / AgentConferenceTransferred -6. SDK emits: task events (leads to end/wrapup) -7. Store: refreshTaskList() -8. UI Controls: wrapup or all disabled -``` - -### New Flow -``` -1. In CONSULTING state (Flow 4 new steps 1-15) -2. User clicks Transfer (transfer control now handles consult transfer) -3. Hook: consultTransfer() checks controls.transferConference.isVisible: - → false: currentTask.consultTransfer() - → true: currentTask.transferConference() -4. SDK: API call -5. WebSocket: → TaskEvent.TRANSFER_SUCCESS or TRANSFER_CONFERENCE_SUCCESS -6. SDK: State → WRAPPING_UP or TERMINATED -7. SDK: computeUIControls for new state -8. SDK emits: events + task:ui-controls-updated -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Conference check | `currentTask.data.isConferenceInProgress` | `controls.transferConference.isVisible` (or keep data check) | -| Transfer button | Separate `consultTransferConsult` control | `controls.transfer` (consult transfer) / `controls.transferConference` (conference transfer) | - ---- - -## Flow 14: Switch Between Main Call and Consult Call - -### Old Flow -``` -1. In consulting state, agent is on consult leg -2. User clicks "Switch to Main Call" -3. Hook: switchToMainCall(): - → currentTask.resume(findMediaResourceId(task, 'consult')) - (resumes consult media → puts consult on hold → main call active) -4. WebSocket: AgentContactHeld (consult) + AgentContactUnheld (main) -5. SDK emits: task:hold, task:resume -6. Store: refreshTaskList() (twice) -7. UI Controls: getControlsVisibility(): - → consultCallHeld = true → switchToConsult visible - → switchToMainCall hidden -``` - -### New Flow -``` -1. In CONSULTING state, agent is on consult leg -2. User clicks "Switch to Main Call" -3. Hook: switchToMainCall(): - → currentTask.resume(findMediaResourceId(task, 'consult')) -4. SDK: HOLD_INITIATED / UNHOLD_INITIATED → state machine tracks -5. SDK: context.consultCallHeld updated -6. SDK: computeUIControls(CONSULTING, updated context): - → switchToConsult visible (consult is now held) - → switchToMainCall hidden -7. SDK emits: task:hold, task:resume, task:ui-controls-updated -8. Hook: controls updated via listener -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| consultCallHeld tracking | `findHoldStatus(task, 'consult', agentId)` | `context.consultCallHeld` in state machine | -| Controls update | After 2x `refreshTaskList()` | Single `task:ui-controls-updated` after state settles | - ---- - -## State Machine States → Widget Controls Summary - -| TaskState (New) | Old Equivalent | Controls Visible | -|-----------------|---------------|-----------------| -| `IDLE` | No task / before incoming | All disabled | -| `OFFERED` | Incoming task shown | accept, decline (WebRTC voice); accept only (digital) | -| `CONNECTED` | Active call | hold, mute, end, transfer, consult, recording | -| `HOLD_INITIATING` | (no equivalent) | hold visible (transitioning) | -| `HELD` | `isHeld = true` | hold (for resume), transfer, consult | -| `RESUME_INITIATING` | (no equivalent) | hold visible (transitioning) | -| `CONSULT_INITIATING` | `ConsultStatus.CONSULT_INITIATED` | endConsult, switchToMainCall, switchToConsult | -| `CONSULTING` | `ConsultStatus.CONSULT_ACCEPTED` | endConsult, mergeToConference, transfer, switchToMainCall/Consult | -| `CONF_INITIATING` | (no equivalent) | conference transitioning | -| `CONFERENCING` | `isConferenceInProgress = true` | exitConference, consult, transferConference | -| `WRAPPING_UP` | `wrapUpRequired && interaction terminated` | wrapup only | -| `COMPLETED` | Task removed after wrapup | All disabled | -| `TERMINATED` | Task rejected / ended without wrapup | All disabled | - ---- - -## Event Chain Mapping: Old Widget Events → New SDK State Machine - -| Widget Action | Old Event Chain | New Event Chain | -|--------------|----------------|-----------------| -| Accept task | `accept()` → `task:assigned` | `accept()` → `ASSIGN` → `task:assigned` + `task:ui-controls-updated` | -| Decline task | `decline()` → `task:rejected` | `decline()` → `RONA` → `task:rejected` + `task:ui-controls-updated` | -| Hold | `hold()` → `task:hold` | `hold()` → `HOLD_INITIATED` → `HOLD_SUCCESS` → `task:hold` + `task:ui-controls-updated` | -| Resume | `resume()` → `task:resume` | `resume()` → `UNHOLD_INITIATED` → `UNHOLD_SUCCESS` → `task:resume` + `task:ui-controls-updated` | -| End call | `end()` → `task:end` | `end()` → `CONTACT_ENDED` → `task:end` + `task:ui-controls-updated` | -| Wrapup | `wrapup()` → `task:wrappedup` | `wrapup()` → `WRAPUP_COMPLETE` → `AGENT_WRAPPEDUP` (AgentWrappedUp) + `task:ui-controls-updated` | -| Transfer | `transfer()` → `task:end` | `transfer()` → `TRANSFER_SUCCESS` → `task:end` + `task:ui-controls-updated` | -| Start consult | `consult()` → `task:consultCreated` | `consult()` → `CONSULT` → `CONSULT_SUCCESS` → `CONSULTING_ACTIVE` → `task:consulting` + `task:ui-controls-updated` | -| End consult | `endConsult()` → `task:consultEnd` | `endConsult()` → `CONSULT_END` → `task:consultEnd` + `task:ui-controls-updated` | -| Merge conference | `consultConference()` → `task:conferenceStarted` | `consultConference()` → `MERGE_TO_CONFERENCE` → `CONFERENCE_START` → `task:conferenceStarted` + `task:ui-controls-updated` | -| Exit conference | `exitConference()` → `task:conferenceEnded` | `exitConference()` → `PARTICIPANT_LEAVE` / `CONFERENCE_END` → `task:conferenceEnded` + `task:ui-controls-updated` | -| Pause recording | `pauseRecording()` → `task:recordingPaused` | `pauseRecording()` → `PAUSE_RECORDING` → `task:recordingPaused` + `task:ui-controls-updated` | -| Resume recording | `resumeRecording()` → `task:recordingResumed` | `resumeRecording()` → `RESUME_RECORDING` → `task:recordingResumed` + `task:ui-controls-updated` | - -### Universal New Pattern -Every action now follows: **User action → SDK method → State machine event(s) → task.uiControls recomputed → `task:ui-controls-updated` emitted**. Widgets never need to compute controls themselves. - ---- - -## Timer Utils: Old vs New Control References - -**File:** `packages/contact-center/task/src/Utils/timer-utils.ts` - -| Old Reference in Timer Utils | New Equivalent | -|-----------------------------|---------------| -| `controlVisibility.wrapup?.isVisible` | `controls.wrapup.isVisible` | -| `controlVisibility.consultCallHeld` | `controls.switchToConsult.isVisible` | -| `controlVisibility.isConsultInitiated` | `controls.endConsult.isVisible && !controls.mergeToConference.isEnabled` | - ---- - -_Created: 2026-03-09_ -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/013-file-inventory-old-control-references.md b/ai-docs/migration/013-file-inventory-old-control-references.md deleted file mode 100644 index 728155129..000000000 --- a/ai-docs/migration/013-file-inventory-old-control-references.md +++ /dev/null @@ -1,171 +0,0 @@ -# Migration Doc 013: Complete File Inventory — Old Control References - -## Purpose - -This is the definitive inventory of **every file** in CC Widgets that references old task control names, state flags, or the `ControlVisibility` type. Use this as a checklist during migration to ensure nothing is missed. - ---- - -## Summary - -| Category | Files with Old Refs | Files Unaffected | -|----------|-------------------|-----------------| -| Widget hooks (`task/src/`) | 4 | 1 (`index.ts`) | -| Widget utils (`task/src/Utils/`) | 4 | 0 | -| Widget entry points (`task/src/*/index.tsx`) | 4 | 1 (`OutdialCall`) | -| cc-components types | 1 (central type file) | 0 | -| cc-components utils | 2 | 3+ (unaffected) | -| cc-components components | 4 | 8+ (unaffected) | -| Store | 3 | 1 (`store.ts`) | -| **Total files to modify** | **~25** | **~13 unaffected** | - ---- - -## Files WITH Old Control References (Must Migrate) - -### Tier 1: Core Logic Files (Highest Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 1 | `task/src/Utils/task-util.ts` | `getControlsVisibility()` — the entire function (~650 lines). Computes all 22 controls + 7 state flags. Calls `getConsultStatus`, `findHoldStatus`, `getIsConferenceInProgress`, etc. | [Doc 002](./002-ui-controls-migration.md) | -| 2 | `task/src/helper.ts` (`useCallControl`) | `controlVisibility = useMemo(() => getControlsVisibility(...))`, references `controlVisibility.muteUnmute`, `controlVisibility.wrapup`, passes to `calculateStateTimerData`, `calculateConsultTimerData` | [Doc 004](./004-call-control-hook-migration.md) | -| 3 | `store/src/storeEventsWrapper.ts` | `refreshTaskList()` in 15+ event handlers; missing `TASK_UI_CONTROLS_UPDATED` subscription | [Doc 003](./003-store-event-wiring-migration.md) | -| 4 | `store/src/task-utils.ts` | `getConsultStatus()`, `findHoldStatus()`, `getIsConferenceInProgress()`, `getConferenceParticipantsCount()`, `getIsCustomerInCall()`, `getIsConsultInProgress()` — all used by `getControlsVisibility()` | [Doc 008](./008-store-task-utils-migration.md) | - -### Tier 2: Component Utility Files (High Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 5 | `cc-components/.../task/task.types.ts` | `ControlVisibility` interface (22 controls + 7 flags), `ControlProps.controlVisibility`, `CallControlComponentProps` picks `controlVisibility`, `CallControlConsultComponentsProps.controlVisibility`, `ConsultTransferPopoverComponentProps.isConferenceInProgress`, `ControlProps.isHeld`, `ControlProps.deviceType`, `ControlProps.featureFlags` | [Doc 009](./009-types-and-constants-migration.md), [Doc 010](./010-component-layer-migration.md) | -| 6 | `cc-components/.../call-control.utils.ts` | `buildCallControlButtons()` — 20+ references: `controlVisibility.muteUnmute`, `.holdResume`, `.isHeld`, `.consult`, `.transfer`, `.isConferenceInProgress`, `.consultTransfer`, `.mergeConference`, `.pauseResumeRecording`, `.exitConference`, `.end`, `.switchToConsult`. Also `filterButtonsForConsultation(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | -| 7 | `cc-components/.../call-control-custom.utils.ts` | `createConsultButtons()` — `controlVisibility.muteUnmuteConsult`, `.switchToMainCall`, `.isConferenceInProgress`, `.consultTransferConsult`, `.mergeConferenceConsult`, `.endConsult`. Also `getConsultStatusText(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | - -### Tier 3: Component Files (Medium Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 8 | `cc-components/.../CallControl/call-control.tsx` | Receives `controlVisibility` as prop, passes to `buildCallControlButtons()`, `createConsultButtons()`, `filterButtonsForConsultation()` | [Doc 010](./010-component-layer-migration.md) | -| 9 | `cc-components/.../CallControlCustom/call-control-consult.tsx` | Receives `controlVisibility` from `CallControlConsultComponentsProps`, passes to `createConsultButtons()` | [Doc 010](./010-component-layer-migration.md) | -| 10 | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Receives `isConferenceInProgress` prop | [Doc 010](./010-component-layer-migration.md) | -| 10b | `cc-components/.../CallControlCAD/call-control-cad.tsx` | Directly references `controlVisibility.isConferenceInProgress`, `controlVisibility.isHeld`, `controlVisibility.isConsultReceived`, `controlVisibility.consultCallHeld`, `controlVisibility.recordingIndicator`, `controlVisibility.wrapup`, `controlVisibility.isConsultInitiatedOrAccepted` | [Doc 010](./010-component-layer-migration.md) | -| 10c | `cc-components/src/wc.ts` | Registers `WebCallControlCADComponent` with `commonPropsForCallControl` — props must align with new `TaskUIControls` shape | [Doc 010](./010-component-layer-migration.md) | -| 10d | `cc-components/.../TaskList/task-list.utils.ts` | `extractTaskListItemData(task, isBrowser, agentId)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation; `store.isDeclineButtonEnabled` | [Doc 006](./006-task-list-migration.md) | -| 10e | `cc-components/.../IncomingTask/incoming-task.utils.tsx` | `extractIncomingTaskData(incomingTask, isBrowser, logger, isDeclineButtonEnabled)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation | [Doc 005](./005-incoming-task-migration.md) | - -### Tier 4: Widget Entry Points (Medium Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 11 | `task/src/CallControl/index.tsx` | Passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` from store to `useCallControl` | [Doc 004](./004-call-control-hook-migration.md) | -| 12 | `task/src/CallControlCAD/index.tsx` | Same as #11 — passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 010](./010-component-layer-migration.md) | -| 13 | `task/src/TaskList/index.tsx` | Passes `deviceType` from store for `isBrowser` computation | [Doc 006](./006-task-list-migration.md) | -| 13b | `task/src/IncomingTask/index.tsx` | Passes `deviceType` from store to `useIncomingTask` — migrate to `task.uiControls.accept`/`decline` | [Doc 005](./005-incoming-task-migration.md) | - -### Tier 5: Utility Files (Low-Medium Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 14 | `task/src/Utils/timer-utils.ts` | `calculateStateTimerData(task, controlVisibility, agentId)` — uses `controlVisibility.wrapup`, `.consultCallHeld`, `.isConsultInitiated` | [Doc 004](./004-call-control-hook-migration.md#timer-utils-migration) | -| 15 | `task/src/Utils/useHoldTimer.ts` | Uses `findHoldTimestamp` from task-util.ts (dual signature issue) — NOT a control visibility reference, but part of consolidation | [Doc 008](./008-store-task-utils-migration.md) | -| 16 | `task/src/task.types.ts` | `useCallControlProps` interface — includes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 009](./009-types-and-constants-migration.md) | -| 17 | `task/src/Utils/constants.ts` | Timer label constants — no control refs, but check for unused consult state constants | [Doc 009](./009-types-and-constants-migration.md) | - -### Tier 6: Store Constants (Low Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 18 | `store/src/constants.ts` | `TASK_STATE_CONSULT`, `TASK_STATE_CONSULTING`, `CONSULT_STATE_INITIATED`, `CONSULT_STATE_COMPLETED`, etc. — used by `getConsultStatus()` | [Doc 009](./009-types-and-constants-migration.md) | -| 19 | `store/src/store.ts` | `refreshTaskList()` method, `isDeclineButtonEnabled` observable | [Doc 003](./003-store-event-wiring-migration.md) | - -### Tier 7: Test Files (Must Update After Implementation) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 20 | `task/tests/**` | All `useCallControl` tests mock `getControlsVisibility()` return | [Doc 011](./011-execution-plan.md) | -| 21 | `cc-components/tests/**` | All CallControl component tests mock `controlVisibility` prop | [Doc 011](./011-execution-plan.md) | -| 22 | `store/tests/**` | Tests for event handlers, `refreshTaskList()`, task-utils | [Doc 011](./011-execution-plan.md) | - ---- - -## Files WITHOUT Old Control References (No Migration Needed) - -| File | Reason | -|------|--------| -| `task/src/OutdialCall/index.tsx` | CC-level API, no task controls | -| `task/src/index.ts` | Re-exports only | -| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only | -| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.utils.ts` | Pure timer formatting | -| `cc-components/.../CallControlCustom/consult-transfer-popover-hooks.ts` | Pagination/search logic | -| `cc-components/.../CallControlCustom/consult-transfer-list-item.tsx` | Display only | -| `cc-components/.../CallControlCustom/consult-transfer-dial-number.tsx` | Input handling | -| `cc-components/.../CallControlCustom/consult-transfer-empty-state.tsx` | Display only | -| `cc-components/.../TaskTimer/index.tsx` | Timer display | -| `cc-components/.../Task/index.tsx` | Task card display | -| `cc-components/.../Task/task.utils.ts` | Task data extraction for display | -| `cc-components/.../OutdialCall/outdial-call.tsx` | No task controls | -| `cc-components/.../constants.ts` | UI string constants | -| `cc-components/.../OutdialCall/constants.ts` | Outdial constants | - ---- - -## Old Control Name → File Reference Matrix - -This shows exactly which files reference each old control name: - -| Old Control Name | Files That Reference It | -|------------------|------------------------| -| `muteUnmute` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `muteUnmuteConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | -| `holdResume` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `pauseResumeRecording` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `recordingIndicator` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `mergeConference` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `consultTransferConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | -| `mergeConferenceConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | -| `isConferenceInProgress` | `task-util.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `task.types.ts`, `consult-transfer-popover.tsx` | -| `isConsultInitiated` | `task-util.ts`, `call-control.utils.ts`, `timer-utils.ts`, `task.types.ts` | -| `isConsultInitiatedAndAccepted` | `task-util.ts`, `task.types.ts` | -| `isConsultReceived` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `isConsultInitiatedOrAccepted` | `task-util.ts`, `helper.ts`, `timer-utils.ts`, `task.types.ts` | -| `isHeld` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `consultCallHeld` | `task-util.ts`, `timer-utils.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `controlVisibility` (param name) | `helper.ts`, `timer-utils.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `call-control.tsx`, `call-control-consult.tsx`, `call-control-cad.tsx`, `task.types.ts` | -| `ControlVisibility` (type) | `task.types.ts` (definition), `call-control.utils.ts`, `call-control-custom.utils.ts` (imports) | -| `isBrowser` (legacy flag) | `task-list.utils.ts`, `incoming-task.utils.tsx`, `task-list.tsx`, `incoming-task.tsx` — replace with `task.uiControls.accept`/`decline` | -| `isDeclineButtonEnabled` (legacy flag) | `incoming-task.utils.tsx`, `incoming-task.tsx`, `task-list.utils.ts` — replace with `task.uiControls.decline.isEnabled` | - ---- - -## Migration Execution Order by File - -Based on dependencies: - -``` -1. task.types.ts (cc-components) — Define new TaskUIControls prop, keep ControlVisibility during transition -2. task/src/task.types.ts — Import TaskUIControls, update useCallControlProps -3. store/src/constants.ts — Mark deprecated constants -4. store/src/task-utils.ts — Remove redundant functions -5. store/src/storeEventsWrapper.ts — Add TASK_UI_CONTROLS_UPDATED, simplify handlers -6. task/src/Utils/timer-utils.ts — Accept TaskUIControls instead of ControlVisibility -7. task/src/Utils/task-util.ts — DELETE or reduce to findHoldTimestamp only -8. task/src/helper.ts — Replace getControlsVisibility() with task.uiControls -9. call-control.utils.ts — Update buildCallControlButtons() to new control names -10. call-control-custom.utils.ts — Update createConsultButtons() to new control names -11. call-control.tsx — Update to accept controls: TaskUIControls -12. call-control-consult.tsx — Update consult component props -13. consult-transfer-popover.tsx — Update isConferenceInProgress derivation -13b. call-control-cad.tsx — Replace all controlVisibility refs (isHeld, isConferenceInProgress, recordingIndicator, wrapup, isConsultInitiatedOrAccepted, isConsultReceived, consultCallHeld) -13c. wc.ts — Update commonPropsForCallControl to align with TaskUIControls shape -13d. task-list.utils.ts — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic -13e. incoming-task.utils.tsx — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic -14. CallControl/index.tsx — Remove old props from useCallControl call -15. CallControlCAD/index.tsx — Remove old props from useCallControl call -16. TaskList/index.tsx — Remove deviceType usage -17. IncomingTask/index.tsx — Remove deviceType, migrate to task.uiControls -18. All test files — Update mocks and assertions -``` - ---- - -_Created: 2026-03-09_ -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ From abbbaf35b1a3e1f148bce4c7b6fbac907b003156 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 13:34:38 +0530 Subject: [PATCH 10/18] =?UTF-8?q?docs(ai-docs):=20address=20codex=20eighth?= =?UTF-8?q?=20review=20=E2=80=94=20fix=20conference=20state=20derivation,?= =?UTF-8?q?=20consult=20state=20mapping,=20and=20callback=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-docs/migration/002-ui-controls-migration.md | 2 +- ai-docs/migration/009-types-and-constants-migration.md | 4 ++-- ai-docs/migration/014-task-code-scan-report.md | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ai-docs/migration/002-ui-controls-migration.md b/ai-docs/migration/002-ui-controls-migration.md index b6511256d..0985028de 100644 --- a/ai-docs/migration/002-ui-controls-migration.md +++ b/ai-docs/migration/002-ui-controls-migration.md @@ -124,7 +124,7 @@ The following state flags were returned by `getControlsVisibility()` but are no | Old State Flag | Replacement | |----------------|-------------| -| `isConferenceInProgress` | Derive from `task.uiControls.exitConference.isVisible` if needed | +| `isConferenceInProgress` | **Do NOT derive from `exitConference.isVisible`** — exit-conference is hidden during conference + active consult (`isConferenceInProgress && !isConsultInitiatedOrAccepted`). Use `task.data.isConferenceInProgress` from task data instead, or check if SDK exposes this as a dedicated flag | | `isConsultInitiated` | Derive from `task.uiControls.endConsult.isVisible` if needed | | `isConsultInitiatedAndAccepted` | No longer needed — SDK handles via controls | | `isConsultReceived` | No longer needed — SDK handles via controls | diff --git a/ai-docs/migration/009-types-and-constants-migration.md b/ai-docs/migration/009-types-and-constants-migration.md index 121741906..f0fa5fa00 100644 --- a/ai-docs/migration/009-types-and-constants-migration.md +++ b/ai-docs/migration/009-types-and-constants-migration.md @@ -31,8 +31,8 @@ CC Widgets defines its own types for control visibility, task state, and constan | Old (CC Widgets Store) | New (CC SDK) | |------------------------|--------------| -| `TASK_STATE_CONSULT` | `TaskState.CONSULTING` | -| `TASK_STATE_CONSULTING` | `TaskState.CONSULTING` | +| `TASK_STATE_CONSULT` | **Not a 1:1 map** — represents `CONSULT_STATE_INITIATED` specifically (consult requested, not yet accepted). SDK may use a distinct initiating state; verify with SDK `TaskState` enum. Do NOT collapse with `TASK_STATE_CONSULTING` | +| `TASK_STATE_CONSULTING` | `TaskState.CONSULTING` (consult accepted, actively consulting) | | `TASK_STATE_CONSULT_COMPLETED` | `TaskState.CONNECTED` (consult ended, back to connected) | | `INTERACTION_STATE_WRAPUP` | `TaskState.WRAPPING_UP` | | `POST_CALL` | `TaskState.POST_CALL` (not implemented) | diff --git a/ai-docs/migration/014-task-code-scan-report.md b/ai-docs/migration/014-task-code-scan-report.md index 584f69e27..787abc9bf 100644 --- a/ai-docs/migration/014-task-code-scan-report.md +++ b/ai-docs/migration/014-task-code-scan-report.md @@ -16,6 +16,11 @@ ### useIncomingTask (lines 147–281) - **setTaskCallback usage:** TASK_ASSIGNED, TASK_CONSULT_ACCEPTED, TASK_END, TASK_REJECT, TASK_CONSULT_END - **removeTaskCallback:** Same events in cleanup +- **⚠️ Pre-existing bug — TASK_ASSIGNED callback mismatch:** + - `setTaskCallback(TASK_ASSIGNED, ...)` registers an **inline anonymous function** (line ~180) + - `removeTaskCallback(TASK_ASSIGNED, taskAssignCallback, ...)` removes with the **named `taskAssignCallback`** reference (line ~200) + - These are different function references, so the inline listener is **never removed** — potential listener leak / duplicate callback on task reassignment + - **Migration action:** Fix during migration by using the same function reference for both register and cleanup, or consolidate to SDK event subscription model - **SDK methods:** `incomingTask.accept()`, `incomingTask.decline()` - **Migration:** Per-task event subscriptions; must migrate to new event model From eabc80db496d5a81cab62cf63ab38e0c0bbe60f3 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 13:53:58 +0530 Subject: [PATCH 11/18] docs(ai-docs): add CC SDK task-refactor branch reference to 001-migration-overview --- ai-docs/migration/001-migration-overview.md | 36 +++++++++++++++++++ .../migration/002-ui-controls-migration.md | 2 +- .../009-types-and-constants-migration.md | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/ai-docs/migration/001-migration-overview.md b/ai-docs/migration/001-migration-overview.md index 1dac577c5..5b780b9ee 100644 --- a/ai-docs/migration/001-migration-overview.md +++ b/ai-docs/migration/001-migration-overview.md @@ -95,6 +95,42 @@ Components receive {isVisible, isEnabled} per control --- +## CC SDK Task-Refactor Branch Reference + +> **Repo:** [webex/webex-js-sdk (task-refactor)](https://github.com/webex/webex-js-sdk/tree/task-refactor) +> **Local path:** `/Users/akulakum/Documents/CC_SDK/webex-js-sdk` (branch: `task-refactor`) + +### Key SDK Source Files + +| File | Purpose | +|------|---------| +| `uiControlsComputer.ts` | Computes `TaskUIControls` from `TaskState` + `TaskContext` — the single source of truth for all control visibility/enabled states | +| `constants.ts` | `TaskState` enum (IDLE, OFFERED, CONNECTED, HELD, CONSULT_INITIATING, CONSULTING, CONF_INITIATING, CONFERENCING, WRAPPING_UP, COMPLETED, TERMINATED, etc.) and `TaskEvent` enum | +| `types.ts` | `TaskContext`, `UIControlConfig`, `TaskStateMachineConfig` | +| `TaskStateMachine.ts` | State machine configuration with transitions, guards, and actions | +| `actions.ts` | State machine action implementations | +| `guards.ts` | Transition guard conditions | +| `../Task.ts` | Task service exposing `task.uiControls` getter and `task:ui-controls-updated` event | +| `../TaskUtils.ts` | Shared utility functions used by `uiControlsComputer.ts` (e.g., `getIsConferenceInProgress`, `getIsCustomerInCall`) | + +### Key SDK Architectural Decisions + +These decisions in the SDK directly impact how the migration docs should be interpreted: + +1. **`exitConference` visibility:** In the SDK, `exitConference` is `VISIBLE_DISABLED` (not hidden) during consulting-from-conference. This differs from the old widget logic where it was hidden. `exitConference.isVisible` is therefore more reliable in the new SDK for detecting conference state, but consulted agents not in conferencing state still see `DISABLED`. + +2. **`TaskState.CONSULT_INITIATING` vs `CONSULTING`:** The SDK has `CONSULT_INITIATING` (consult requested, async in-progress) and `CONSULTING` (consult accepted, actively consulting) as distinct states. The old widget constant `TASK_STATE_CONSULT` ('consult') maps to `CONSULT_INITIATING`, NOT `CONSULTING`. `TaskState.CONSULT_INITIATED` exists in the enum but is marked "NOT IMPLEMENTED". + +3. **Recording control:** `recording.isEnabled = true` when recording is in progress (allows pause/resume toggle). `recording.isEnabled = false` when recording is not active (allows starting). This means paused recordings show `{ isVisible: true, isEnabled: true }` to allow resumption. + +4. **`isHeld` derivation:** The SDK computes `isHeld` from `serverHold ?? state === TaskState.HELD` (line 81 of `uiControlsComputer.ts`). Hold control can be `VISIBLE_DISABLED` in conference/consulting states without meaning the call is held. Widgets must derive `isHeld` from task data (`findHoldStatus`), not from `controls.hold.isEnabled`. + +5. **`UIControlConfig` built internally:** The SDK builds `UIControlConfig` from agent profile, `callProcessingDetails`, media type, and voice variant. Widgets do NOT need to provide it. + +6. **Conference state (`inConference`):** The SDK computes `inConference` as `conferenceActive && (isConferencing || selfInMainCall || consultInitiator)` (line 97). This is broader than `isConferencing` state alone, accounting for backend conference flags and consult-from-conference flows. + +--- + ## SDK Version Requirements The CC Widgets migration depends on the CC SDK `task-refactor` branch being merged and released. Key new APIs: diff --git a/ai-docs/migration/002-ui-controls-migration.md b/ai-docs/migration/002-ui-controls-migration.md index 0985028de..9773799d5 100644 --- a/ai-docs/migration/002-ui-controls-migration.md +++ b/ai-docs/migration/002-ui-controls-migration.md @@ -124,7 +124,7 @@ The following state flags were returned by `getControlsVisibility()` but are no | Old State Flag | Replacement | |----------------|-------------| -| `isConferenceInProgress` | **Do NOT derive from `exitConference.isVisible`** — exit-conference is hidden during conference + active consult (`isConferenceInProgress && !isConsultInitiatedOrAccepted`). Use `task.data.isConferenceInProgress` from task data instead, or check if SDK exposes this as a dedicated flag | +| `isConferenceInProgress` | **Caution with `exitConference.isVisible`** — In the old widget code, exit-conference was hidden during conference + active consult. In the **new SDK** (`uiControlsComputer.ts`), `exitConference` is `VISIBLE_DISABLED` (not hidden) during consulting-from-conference, making `isVisible` more reliable. However, for consulted agents not in conferencing state, `exitConference` is `DISABLED`. If you need a definitive conference-in-progress flag independent of agent role, use `task.data` (e.g., `getIsConferenceInProgress(taskData)` which the SDK itself uses internally) rather than relying solely on `exitConference.isVisible` | | `isConsultInitiated` | Derive from `task.uiControls.endConsult.isVisible` if needed | | `isConsultInitiatedAndAccepted` | No longer needed — SDK handles via controls | | `isConsultReceived` | No longer needed — SDK handles via controls | diff --git a/ai-docs/migration/009-types-and-constants-migration.md b/ai-docs/migration/009-types-and-constants-migration.md index f0fa5fa00..182061ca0 100644 --- a/ai-docs/migration/009-types-and-constants-migration.md +++ b/ai-docs/migration/009-types-and-constants-migration.md @@ -31,7 +31,7 @@ CC Widgets defines its own types for control visibility, task state, and constan | Old (CC Widgets Store) | New (CC SDK) | |------------------------|--------------| -| `TASK_STATE_CONSULT` | **Not a 1:1 map** — represents `CONSULT_STATE_INITIATED` specifically (consult requested, not yet accepted). SDK may use a distinct initiating state; verify with SDK `TaskState` enum. Do NOT collapse with `TASK_STATE_CONSULTING` | +| `TASK_STATE_CONSULT` | **Not a 1:1 map** — represents `CONSULT_STATE_INITIATED` specifically (consult requested, not yet accepted). SDK equivalent is `TaskState.CONSULT_INITIATING` (intermediate async state). Note: SDK also has `TaskState.CONSULT_INITIATED` but it is marked "NOT IMPLEMENTED". Do NOT collapse with `TASK_STATE_CONSULTING` which maps to `TaskState.CONSULTING` | | `TASK_STATE_CONSULTING` | `TaskState.CONSULTING` (consult accepted, actively consulting) | | `TASK_STATE_CONSULT_COMPLETED` | `TaskState.CONNECTED` (consult ended, back to connected) | | `INTERACTION_STATE_WRAPUP` | `TaskState.WRAPPING_UP` | From 6e50dc5fc8b9d064aa86d4f496e0fbff601ccb8d Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 14:41:06 +0530 Subject: [PATCH 12/18] =?UTF-8?q?docs(ai-docs):=20address=20codex=20ninth?= =?UTF-8?q?=20review=20=E2=80=94=20fix=20consult=20mapping,=20recording=20?= =?UTF-8?q?semantics,=20isHeld,=20constants,=20and=20listener=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-docs/migration/001-migration-overview.md | 2 +- ai-docs/migration/002-ui-controls-migration.md | 4 ++-- ai-docs/migration/009-types-and-constants-migration.md | 2 +- ai-docs/migration/014-task-code-scan-report.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ai-docs/migration/001-migration-overview.md b/ai-docs/migration/001-migration-overview.md index 5b780b9ee..adf58cf3f 100644 --- a/ai-docs/migration/001-migration-overview.md +++ b/ai-docs/migration/001-migration-overview.md @@ -121,7 +121,7 @@ These decisions in the SDK directly impact how the migration docs should be inte 2. **`TaskState.CONSULT_INITIATING` vs `CONSULTING`:** The SDK has `CONSULT_INITIATING` (consult requested, async in-progress) and `CONSULTING` (consult accepted, actively consulting) as distinct states. The old widget constant `TASK_STATE_CONSULT` ('consult') maps to `CONSULT_INITIATING`, NOT `CONSULTING`. `TaskState.CONSULT_INITIATED` exists in the enum but is marked "NOT IMPLEMENTED". -3. **Recording control:** `recording.isEnabled = true` when recording is in progress (allows pause/resume toggle). `recording.isEnabled = false` when recording is not active (allows starting). This means paused recordings show `{ isVisible: true, isEnabled: true }` to allow resumption. +3. **Recording control:** SDK computes: `recordingInProgress ? VISIBLE_ENABLED : VISIBLE_DISABLED` (line 228 of `uiControlsComputer.ts`). So: `recording.isEnabled = true` when recording is active (button clickable to pause). `recording.isEnabled = false` when recording is NOT active (button visible but disabled — nothing to pause/resume). Recording start is handled separately, not via this control's `isEnabled` flag. Widget button wiring (`disabled: !isEnabled`) is correct with this semantic. 4. **`isHeld` derivation:** The SDK computes `isHeld` from `serverHold ?? state === TaskState.HELD` (line 81 of `uiControlsComputer.ts`). Hold control can be `VISIBLE_DISABLED` in conference/consulting states without meaning the call is held. Widgets must derive `isHeld` from task data (`findHoldStatus`), not from `controls.hold.isEnabled`. diff --git a/ai-docs/migration/002-ui-controls-migration.md b/ai-docs/migration/002-ui-controls-migration.md index 9773799d5..0d5ea5d60 100644 --- a/ai-docs/migration/002-ui-controls-migration.md +++ b/ai-docs/migration/002-ui-controls-migration.md @@ -125,11 +125,11 @@ The following state flags were returned by `getControlsVisibility()` but are no | Old State Flag | Replacement | |----------------|-------------| | `isConferenceInProgress` | **Caution with `exitConference.isVisible`** — In the old widget code, exit-conference was hidden during conference + active consult. In the **new SDK** (`uiControlsComputer.ts`), `exitConference` is `VISIBLE_DISABLED` (not hidden) during consulting-from-conference, making `isVisible` more reliable. However, for consulted agents not in conferencing state, `exitConference` is `DISABLED`. If you need a definitive conference-in-progress flag independent of agent role, use `task.data` (e.g., `getIsConferenceInProgress(taskData)` which the SDK itself uses internally) rather than relying solely on `exitConference.isVisible` | -| `isConsultInitiated` | Derive from `task.uiControls.endConsult.isVisible` if needed | +| `isConsultInitiated` | **Do NOT derive from `endConsult.isVisible`** — SDK shows `endConsult` for all consulting states (`CONSULT_INITIATING`, `CONSULTING`, `CONF_INITIATING`), not just initiated. If initiated-only semantics are needed (e.g., consult timer labeling in `calculateConsultTimerData`), use SDK `TaskState` directly: `state === TaskState.CONSULT_INITIATING` | | `isConsultInitiatedAndAccepted` | No longer needed — SDK handles via controls | | `isConsultReceived` | No longer needed — SDK handles via controls | | `isConsultInitiatedOrAccepted` | No longer needed — SDK handles via controls | -| `isHeld` | Derive from `task.uiControls.hold` state or SDK task state | +| `isHeld` | **Do NOT derive from `controls.hold`** — hold control is `VISIBLE_DISABLED` in consult/conference states even when call is not held. Derive from task data using `findHoldStatus(task, 'mainCall', agentId)` which checks `interaction.media[mediaId].isHold` directly | | `consultCallHeld` | No longer needed — SDK handles switch controls | --- diff --git a/ai-docs/migration/009-types-and-constants-migration.md b/ai-docs/migration/009-types-and-constants-migration.md index 182061ca0..bccdb06ce 100644 --- a/ai-docs/migration/009-types-and-constants-migration.md +++ b/ai-docs/migration/009-types-and-constants-migration.md @@ -200,7 +200,7 @@ export const MEDIA_TYPE_CONSULT = 'consult'; ```typescript // REMOVE these (no longer needed for control computation): // TASK_STATE_CONSULT, TASK_STATE_CONSULTING, TASK_STATE_CONSULT_COMPLETED -// INTERACTION_STATE_WRAPUP, POST_CALL, CONNECTED, CONFERENCE +// INTERACTION_STATE_WRAPUP, INTERACTION_STATE_POST_CALL, INTERACTION_STATE_CONNECTED, INTERACTION_STATE_CONFERENCE // CONSULT_STATE_INITIATED, CONSULT_STATE_COMPLETED, CONSULT_STATE_CONFERENCING // KEEP these (still used for display or action logic): diff --git a/ai-docs/migration/014-task-code-scan-report.md b/ai-docs/migration/014-task-code-scan-report.md index 787abc9bf..e75fe4897 100644 --- a/ai-docs/migration/014-task-code-scan-report.md +++ b/ai-docs/migration/014-task-code-scan-report.md @@ -333,7 +333,7 @@ Registers on each task: ### 5. Store Task Flow - refreshTaskList → cc.taskManager.getAllTasks() - setCurrentTask uses isIncomingTask(task, agentId) to skip incoming -- handleTaskRemove unregisters all task listeners +- handleTaskRemove attempts to unregister task listeners, but has a **pre-existing listener leak bug**: `TASK_REJECT` and `TASK_OUTDIAL_FAILED` are registered with inline lambdas (`(reason) => this.handleTaskReject(task, reason)`) in `registerTaskEventListeners` but removed with *different* inline lambdas in `handleTaskRemove`, so those listeners are never actually removed. Fix during migration by using stored function references --- From 0d08a09773917f40fc1684ac9109f58ed4a01de9 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 15:41:51 +0530 Subject: [PATCH 13/18] =?UTF-8?q?docs(ai-docs):=20address=20codex=20tenth?= =?UTF-8?q?=20review=20=E2=80=94=20add=20TaskState=20export,=20fix=20confe?= =?UTF-8?q?rence=20handler=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-docs/migration/001-migration-overview.md | 67 ++++++++ .../migration/002-ui-controls-migration.md | 17 +- .../009-types-and-constants-migration.md | 149 +++++++++++++----- .../migration/014-task-code-scan-report.md | 38 ++++- 4 files changed, 226 insertions(+), 45 deletions(-) diff --git a/ai-docs/migration/001-migration-overview.md b/ai-docs/migration/001-migration-overview.md index adf58cf3f..a942d8968 100644 --- a/ai-docs/migration/001-migration-overview.md +++ b/ai-docs/migration/001-migration-overview.md @@ -144,6 +144,62 @@ The CC Widgets migration depends on the CC SDK `task-refactor` branch being merg --- +## SDK Package Entry Point — Pending Additions + +> **As of the `task-refactor` branch snapshot reviewed,** the items below are properly exported from their individual source files within the SDK but are **not yet re-exported** from the package-level entry point (`src/index.ts`). This means widgets cannot `import { ... } from '@webex/contact-center'` for these items until the SDK team adds them. +> +> **A Jira ticket is being created** to track adding these missing exports to the SDK `src/index.ts` before the widget migration begins. + +### 1. Add `uiControls` to `ITask` interface + +The `uiControls` getter is defined on the **concrete `Task` class** and works at runtime on all task instances (Voice, Digital, WebRTC). However, it is not yet declared on the `ITask` interface — only on `IDigital`. Since widgets import `ITask`, TypeScript won't recognize `task.uiControls` until the interface is updated. + +**SDK change:** Add `uiControls: TaskUIControls` to `ITask` interface in `services/task/types.ts`. + +### 2. Add `TaskUIControls` type to package exports + +`TaskUIControls` is exported from `services/task/types.ts` but not re-exported from `src/index.ts`. Similarly, `TaskUIControlState` (the `{ isVisible, isEnabled }` shape) is a local type — should be exported if widgets need it for prop typing. + +**SDK change:** Add to the "Task related types" export block in `src/index.ts`: +```typescript +export type { TaskUIControls, TaskUIControlState } from './services/task/types'; +``` + +### 3. Add `getDefaultUIControls()` to package exports + +`getDefaultUIControls()` is exported from `uiControlsComputer.ts` and the state-machine `index.ts`, but not from `src/index.ts`. Widgets need it as a fallback: `task?.uiControls ?? getDefaultUIControls()`. + +**SDK change:** Add to `src/index.ts`: +```typescript +export { getDefaultUIControls } from './services/task/state-machine/uiControlsComputer'; +``` + +### 4. Add `TaskState` enum to package exports + +`TaskState` is exported from the state-machine internal module but not from the package entry point. Widgets need it for consult timer labeling — `calculateConsultTimerData` must distinguish `CONSULT_INITIATING` (consult requested) from `CONSULTING` (consult accepted) for correct timer labels. + +**SDK change:** Add to `src/index.ts`: +```typescript +export { TaskState } from './services/task/state-machine/constants'; +``` + +### 5. Add `IVoice`, `IDigital`, `IWebRTC` to package exports + +These task subtype interfaces are defined but not re-exported. Widgets may need them for type narrowing (e.g., to access `holdResume()` on voice tasks). + +**SDK change:** Add to `src/index.ts`: +```typescript +export type { IVoice, IDigital, IWebRTC } from './services/task/types'; +``` + +### 6. `holdResume()` only on Voice tasks (informational — no SDK change needed) + +The base `Task` class defines `hold()` and `resume()` that throw `unsupportedMethodError`. **Voice** tasks override both to delegate to `holdResume()` — a single toggle. The `ITask` interface exposes `hold(mediaResourceId?)` and `resume(mediaResourceId?)`, but voice tasks actually use `holdResume()` internally (from `IVoice`). + +**Widget impact:** Widgets calling `task.hold()` / `task.resume()` will work correctly on voice tasks (they delegate to `holdResume`). No widget change needed unless widgets want to call `holdResume()` directly — in which case they need `IVoice` typing (covered in item 4 above). + +--- + ## Pre-existing Bugs Found During Analysis These bugs exist in the current codebase and should be fixed during migration: @@ -160,6 +216,16 @@ Two `findHoldTimestamp` functions with different signatures: Should be consolidated to one function during migration. +### 3. Event Name Mismatches Between Widget and SDK +Widget `store.types.ts` declares a local `TASK_EVENTS` enum (line 210: `TODO: remove this once cc sdk exports this enum`) with 5 events using CC-level naming that differ from SDK task-level naming: +- `AGENT_WRAPPEDUP = 'AgentWrappedUp'` → SDK: `TASK_WRAPPEDUP = 'task:wrappedup'` +- `AGENT_CONSULT_CREATED = 'AgentConsultCreated'` → SDK: `TASK_CONSULT_CREATED = 'task:consultCreated'` +- `AGENT_OFFER_CONTACT = 'AgentOfferContact'` → SDK: `TASK_OFFER_CONTACT = 'task:offerContact'` +- `CONTACT_RECORDING_PAUSED = 'ContactRecordingPaused'` → SDK: `TASK_RECORDING_PAUSED = 'task:recordingPaused'` +- `CONTACT_RECORDING_RESUMED = 'ContactRecordingResumed'` → SDK: `TASK_RECORDING_RESUMED = 'task:recordingResumed'` + +See [009-types-and-constants-migration.md § Event Constants](./009-types-and-constants-migration.md) for the complete mapping. + --- ## Critical Migration Notes @@ -200,3 +266,4 @@ function applyControlState(element, control) { _Created: 2026-03-09_ _Updated: 2026-03-09 (added deep scan findings, before/after examples, bug reports)_ +_Updated: 2026-03-11 (SDK export gaps, holdResume, event name mismatches — from deep SDK comparison)_ diff --git a/ai-docs/migration/002-ui-controls-migration.md b/ai-docs/migration/002-ui-controls-migration.md index 0d5ea5d60..96bf49f41 100644 --- a/ai-docs/migration/002-ui-controls-migration.md +++ b/ai-docs/migration/002-ui-controls-migration.md @@ -125,7 +125,7 @@ The following state flags were returned by `getControlsVisibility()` but are no | Old State Flag | Replacement | |----------------|-------------| | `isConferenceInProgress` | **Caution with `exitConference.isVisible`** — In the old widget code, exit-conference was hidden during conference + active consult. In the **new SDK** (`uiControlsComputer.ts`), `exitConference` is `VISIBLE_DISABLED` (not hidden) during consulting-from-conference, making `isVisible` more reliable. However, for consulted agents not in conferencing state, `exitConference` is `DISABLED`. If you need a definitive conference-in-progress flag independent of agent role, use `task.data` (e.g., `getIsConferenceInProgress(taskData)` which the SDK itself uses internally) rather than relying solely on `exitConference.isVisible` | -| `isConsultInitiated` | **Do NOT derive from `endConsult.isVisible`** — SDK shows `endConsult` for all consulting states (`CONSULT_INITIATING`, `CONSULTING`, `CONF_INITIATING`), not just initiated. If initiated-only semantics are needed (e.g., consult timer labeling in `calculateConsultTimerData`), use SDK `TaskState` directly: `state === TaskState.CONSULT_INITIATING` | +| `isConsultInitiated` | **Do NOT derive from `endConsult.isVisible`** — SDK shows `endConsult` for all consulting states (`CONSULT_INITIATING`, `CONSULTING`, `CONF_INITIATING`), not just initiated. If initiated-only semantics are needed (e.g., consult timer labeling in `calculateConsultTimerData`), use SDK `TaskState` directly: `state === TaskState.CONSULT_INITIATING`. **Note:** This requires `TaskState` to be exported from SDK — tracked in the [SDK missing items Confluence page](./confluence-sdk-missing-items.md) and 009 pending exports. | | `isConsultInitiatedAndAccepted` | No longer needed — SDK handles via controls | | `isConsultReceived` | No longer needed — SDK handles via controls | | `isConsultInitiatedOrAccepted` | No longer needed — SDK handles via controls | @@ -158,20 +158,22 @@ return { ### After (in `useCallControl` hook) ```typescript // helper.ts — new approach +// These imports require pending SDK entry point additions (tracked via Jira). +// See 001-migration-overview.md § SDK Package Entry Point — Pending Additions. +import {TaskUIControls, TASK_EVENTS, getDefaultUIControls} from '@webex/contact-center'; + const task = store.currentTask; -const uiControls = task?.uiControls ?? getDefaultUIControls(); +const uiControls: TaskUIControls = task?.uiControls ?? getDefaultUIControls(); -// Subscribe to UI control updates useEffect(() => { if (!task) return; const handler = () => { // MobX or setState to trigger re-render }; - task.on('task:ui-controls-updated', handler); - return () => task.off('task:ui-controls-updated', handler); + task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, handler); + return () => task.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, handler); }, [task]); -// Pass SDK-computed controls directly to component return { controls: uiControls, // additional hook state (timers, mute state, etc.) @@ -185,7 +187,7 @@ return { | File | Action | |------|--------| | `task/src/Utils/task-util.ts` | **DELETE** or reduce to `findHoldTimestamp()` only | -| `task/src/helper.ts` (`useCallControl`) | Remove `getControlsVisibility()` call, use `task.uiControls` | +| `task/src/helper.ts` (`useCallControl`) | Remove `getControlsVisibility()` call, use `task.uiControls`. Note: voice tasks use `holdResume()` toggle internally (see 001 § SDK Export Gaps), but `task.hold()`/`task.resume()` delegate correctly, so existing widget calls are safe. | | `task/src/task.types.ts` | Import `TaskUIControls` from SDK, remove old control types | | `cc-components/src/components/task/task.types.ts` | Align `ControlProps` with new control names | | `cc-components/src/components/task/CallControl/call-control.tsx` | Update prop names (`holdResume` → `hold`, etc.) | @@ -208,3 +210,4 @@ return { --- _Parent: [001-migration-overview.md](./001-migration-overview.md)_ +_Updated: 2026-03-11 (SDK export gap notes, import examples, holdResume clarification)_ diff --git a/ai-docs/migration/009-types-and-constants-migration.md b/ai-docs/migration/009-types-and-constants-migration.md index bccdb06ce..9b28df7d0 100644 --- a/ai-docs/migration/009-types-and-constants-migration.md +++ b/ai-docs/migration/009-types-and-constants-migration.md @@ -29,15 +29,28 @@ CC Widgets defines its own types for control visibility, task state, and constan ### Task State Constants -| Old (CC Widgets Store) | New (CC SDK) | -|------------------------|--------------| -| `TASK_STATE_CONSULT` | **Not a 1:1 map** — represents `CONSULT_STATE_INITIATED` specifically (consult requested, not yet accepted). SDK equivalent is `TaskState.CONSULT_INITIATING` (intermediate async state). Note: SDK also has `TaskState.CONSULT_INITIATED` but it is marked "NOT IMPLEMENTED". Do NOT collapse with `TASK_STATE_CONSULTING` which maps to `TaskState.CONSULTING` | -| `TASK_STATE_CONSULTING` | `TaskState.CONSULTING` (consult accepted, actively consulting) | -| `TASK_STATE_CONSULT_COMPLETED` | `TaskState.CONNECTED` (consult ended, back to connected) | -| `INTERACTION_STATE_WRAPUP` | `TaskState.WRAPPING_UP` | -| `POST_CALL` | `TaskState.POST_CALL` (not implemented) | -| `CONNECTED` | `TaskState.CONNECTED` | -| `CONFERENCE` | `TaskState.CONFERENCING` | +| Old (CC Widgets Store) | New (CC SDK) | Notes | +|------------------------|--------------|-------| +| `TASK_STATE_CONSULT` | `TaskState.CONSULT_INITIATING` | **Not a 1:1 map** — old constant represents consult requested, not yet accepted. SDK `CONSULT_INITIATING` is the intermediate async state. SDK also has `TaskState.CONSULT_INITIATED` but it is **"NOT IMPLEMENTED"**. Do NOT collapse with `TASK_STATE_CONSULTING` | +| `TASK_STATE_CONSULTING` | `TaskState.CONSULTING` | Consult accepted, actively consulting | +| `TASK_STATE_CONSULT_COMPLETED` | `TaskState.CONNECTED` | Consult ended, back to connected state | +| `INTERACTION_STATE_WRAPUP` | `TaskState.WRAPPING_UP` | | +| `INTERACTION_STATE_POST_CALL` | `TaskState.POST_CALL` | SDK marks this **"NOT IMPLEMENTED"** | +| `INTERACTION_STATE_CONNECTED` | `TaskState.CONNECTED` | | +| `INTERACTION_STATE_CONFERENCE` | `TaskState.CONFERENCING` | | +| *(no old equivalent)* | `TaskState.IDLE` | New — task created but not yet offered | +| *(no old equivalent)* | `TaskState.OFFERED` | New — task offered to agent | +| *(no old equivalent)* | `TaskState.HOLD_INITIATING` | New — intermediate async state for hold request | +| *(no old equivalent)* | `TaskState.HELD` | New — task is on hold | +| *(no old equivalent)* | `TaskState.RESUME_INITIATING` | New — intermediate async state for resume request | +| *(no old equivalent)* | `TaskState.CONF_INITIATING` | New — intermediate async state for conference merge | +| *(no old equivalent)* | `TaskState.COMPLETED` | New — task completed | +| *(no old equivalent)* | `TaskState.TERMINATED` | New — task terminated | +| *(no old equivalent)* | `TaskState.CONSULT_COMPLETED` | **"NOT IMPLEMENTED"** in SDK | +| *(no old equivalent)* | `TaskState.PARKED` | **"NOT IMPLEMENTED"** in SDK | +| *(no old equivalent)* | `TaskState.MONITORING` | **"NOT IMPLEMENTED"** in SDK | + +**Full `TaskState` enum (SDK):** `IDLE`, `OFFERED`, `CONNECTED`, `HOLD_INITIATING`, `HELD`, `RESUME_INITIATING`, `CONSULT_INITIATING`, `CONSULTING`, `CONF_INITIATING`, `CONFERENCING`, `WRAPPING_UP`, `COMPLETED`, `TERMINATED`, `CONSULT_INITIATED` (not impl), `CONSULT_COMPLETED` (not impl), `POST_CALL` (not impl), `PARKED` (not impl), `MONITORING` (not impl) ### Consult Status Constants @@ -54,39 +67,101 @@ CC Widgets defines its own types for control visibility, task state, and constan ### Event Constants -| Old (CC Widgets) | New (CC SDK) | Change | -|------------------|--------------|--------| -| `TASK_EVENTS.TASK_INCOMING` | `TASK_EVENTS.TASK_INCOMING` | Same | -| `TASK_EVENTS.TASK_ASSIGNED` | `TASK_EVENTS.TASK_ASSIGNED` | Same | -| `TASK_EVENTS.TASK_HOLD` | `TASK_EVENTS.TASK_HOLD` | Same | -| `TASK_EVENTS.TASK_RESUME` | `TASK_EVENTS.TASK_RESUME` | Same | -| `TASK_EVENTS.TASK_END` | `TASK_EVENTS.TASK_END` | Same | -| — | `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | **NEW** | -| `TASK_EVENTS.TASK_WRAPUP` | `TASK_EVENTS.TASK_WRAPUP` | Same | -| `TASK_EVENTS.AGENT_WRAPPEDUP` | `TASK_EVENTS.AGENT_WRAPPEDUP` | Same | -| All consult/conference events | Same event names | Same | +| Old (CC Widgets) | Old Value | New (CC SDK) | New Value | Change | +|------------------|-----------|--------------|-----------|--------| +| `TASK_EVENTS.TASK_INCOMING` | `'task:incoming'` | `TASK_EVENTS.TASK_INCOMING` | `'task:incoming'` | Same | +| `TASK_EVENTS.TASK_ASSIGNED` | `'task:assigned'` | `TASK_EVENTS.TASK_ASSIGNED` | `'task:assigned'` | Same | +| `TASK_EVENTS.TASK_HOLD` | `'task:hold'` | `TASK_EVENTS.TASK_HOLD` | `'task:hold'` | Same | +| `TASK_EVENTS.TASK_RESUME` | `'task:resume'` | `TASK_EVENTS.TASK_RESUME` | `'task:resume'` | Same | +| `TASK_EVENTS.TASK_END` | `'task:end'` | `TASK_EVENTS.TASK_END` | `'task:end'` | Same | +| `TASK_EVENTS.TASK_WRAPUP` | `'task:wrapup'` | `TASK_EVENTS.TASK_WRAPUP` | `'task:wrapup'` | Same | +| `TASK_EVENTS.AGENT_WRAPPEDUP` | `'AgentWrappedUp'` | `TASK_EVENTS.TASK_WRAPPEDUP` | `'task:wrappedup'` | **Renamed + value changed** — widget uses CC-level event name, SDK uses task-level | +| `TASK_EVENTS.AGENT_CONSULT_CREATED` | `'AgentConsultCreated'` | `TASK_EVENTS.TASK_CONSULT_CREATED` | `'task:consultCreated'` | **Renamed + value changed** | +| `TASK_EVENTS.AGENT_OFFER_CONTACT` | `'AgentOfferContact'` | `TASK_EVENTS.TASK_OFFER_CONTACT` | `'task:offerContact'` | **Renamed + value changed** | +| `TASK_EVENTS.CONTACT_RECORDING_PAUSED` | `'ContactRecordingPaused'` | `TASK_EVENTS.TASK_RECORDING_PAUSED` | `'task:recordingPaused'` | **Renamed + value changed** | +| `TASK_EVENTS.CONTACT_RECORDING_RESUMED` | `'ContactRecordingResumed'` | `TASK_EVENTS.TASK_RECORDING_RESUMED` | `'task:recordingResumed'` | **Renamed + value changed** | +| — | — | `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | `'task:ui-controls-updated'` | **NEW** | +| — | — | `TASK_EVENTS.TASK_UNASSIGNED` | `'task:unassigned'` | **NEW** | +| — | — | `TASK_EVENTS.TASK_CLEANUP` | `'task:cleanup'` | **NEW** — state machine terminal state cleanup | +| — | — | `TASK_EVENTS.TASK_RECORDING_STARTED` | `'task:recordingStarted'` | **NEW** | +| — | — | `TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED` | `'task:recordingPauseFailed'` | **NEW** | +| — | — | `TASK_EVENTS.TASK_RECORDING_RESUME_FAILED` | `'task:recordingResumeFailed'` | **NEW** | +| — | — | `TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED` | `'task:consultQueueFailed'` | **NEW** | +| — | — | `TASK_EVENTS.TASK_EXIT_CONFERENCE` | `'task:exitConference'` | **NEW** | +| — | — | `TASK_EVENTS.TASK_TRANSFER_CONFERENCE` | `'task:transferConference'` | **NEW** | +| `TASK_EVENTS.TASK_CONSULT_END` | Same | `TASK_EVENTS.TASK_CONSULT_END` | Same | Same | +| `TASK_EVENTS.TASK_CONSULT_ACCEPTED` | Same | `TASK_EVENTS.TASK_CONSULT_ACCEPTED` | Same | Same | +| `TASK_EVENTS.TASK_CONSULTING` | Same | `TASK_EVENTS.TASK_CONSULTING` | Same | Same | +| `TASK_EVENTS.TASK_OFFER_CONSULT` | Same | `TASK_EVENTS.TASK_OFFER_CONSULT` | Same | Same | +| All conference events | Same | Same | Same | Same | + +> **Critical:** Five widget event names (`AGENT_WRAPPEDUP`, `AGENT_CONSULT_CREATED`, `AGENT_OFFER_CONTACT`, `CONTACT_RECORDING_PAUSED`, `CONTACT_RECORDING_RESUMED`) use CC-level naming convention (`'AgentWrappedUp'`, etc.) but the SDK uses task-level naming (`'task:wrappedup'`, etc.). Widget `store.types.ts` re-declares these and must be updated to match SDK values. + +**Widget-only events (no SDK equivalent — must verify or remove):** + +| Widget Event | Widget Value | SDK Status | +|-------------|-------------|------------| +| `TASK_UNHOLD` | `'task:unhold'` | SDK uses `TASK_RESUME` (`'task:resume'`) instead — no separate unhold event | +| `TASK_CONSULT` | `'task:consult'` | Not in SDK `TASK_EVENTS` — SDK uses `TASK_CONSULT_CREATED` / `TASK_CONSULTING` | +| `TASK_PAUSE` | `'task:pause'` | Not in SDK `TASK_EVENTS` — SDK uses recording events instead | +| `AGENT_CONTACT_ASSIGNED` | `'AgentContactAssigned'` | CC-level event — may still be needed for `cc.on()` subscriptions | + +**Note:** Widget `store.types.ts` line 210 has `TODO: remove this once cc sdk exports this enum`. The SDK now exports `TASK_EVENTS` from `@webex/contact-center`. During migration, **delete the local `TASK_EVENTS` enum** and import from SDK directly. + +### Media Type / Channel Type Constants + +| Old (CC Widgets) | New (CC SDK) | SDK Source | +|------------------|--------------|------------| +| `MEDIA_TYPE_TELEPHONY` = `'telephony'` | `TASK_CHANNEL_TYPE.VOICE` = `'voice'` | `services/task/types.ts` | +| `MEDIA_TYPE_CHAT` = `'chat'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | `services/task/types.ts` | +| `MEDIA_TYPE_EMAIL` = `'email'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | `services/task/types.ts` | + +**Note:** SDK uses `TASK_CHANNEL_TYPE` (`VOICE`/`DIGITAL`) for UI control computation. Widget media types may still be needed for display purposes. + +**Type:** `TaskChannelType = 'voice' | 'digital'` (derived from `typeof TASK_CHANNEL_TYPE`) + +### Voice Variant Constants (NEW) + +| Constant | Value | Purpose | +|----------|-------|---------| +| `VOICE_VARIANT.PSTN` | `'pstn'` | PSTN telephony — no `decline`/`toggleMute` in UI controls | +| `VOICE_VARIANT.WEBRTC` | `'webrtc'` | WebRTC browser — `decline` and `toggleMute` available | + +**Type:** `VoiceVariant = 'pstn' | 'webrtc'` (derived from `typeof VOICE_VARIANT`) + +**Widget impact:** Widgets do NOT set voice variant directly — the SDK resolves it internally when creating the task. But widgets should understand this affects which controls appear (e.g., PSTN tasks won't show decline button). -### Media Type Constants +--- -| Old (CC Widgets) | New (CC SDK) | -|------------------|--------------| -| `MEDIA_TYPE_TELEPHONY` = `'telephony'` | `TASK_CHANNEL_TYPE.VOICE` = `'voice'` | -| `MEDIA_TYPE_CHAT` = `'chat'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | -| `MEDIA_TYPE_EMAIL` = `'email'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | +## New Types to Import from SDK -**Note:** SDK uses channel type (`VOICE`/`DIGITAL`) for UI control computation. Widget media types may still be needed for display purposes. +| Type | Source | Purpose | Package Entry Point Status | +|------|--------|---------|---------------------------| +| `TaskUIControls` | `@webex/contact-center` | Pre-computed control states (17 controls) | Exported from source file; pending addition to `src/index.ts` (Jira tracked) | +| `TaskUIControlState` | `@webex/contact-center` | Single control `{ isVisible, isEnabled }` | Local type in source file; pending export + addition to `src/index.ts` | +| `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | `@webex/contact-center` | New event | Available — part of `TASK_EVENTS` enum already exported | +| `TASK_CHANNEL_TYPE` | `@webex/contact-center` | `{ VOICE, DIGITAL }` constant | Exported from source file; pending addition to `src/index.ts` | +| `VoiceVariant` | `@webex/contact-center` | `'pstn' \| 'webrtc'` | Exported from source file; pending addition to `src/index.ts` | +| `Participant` | `@webex/contact-center` | `{ id, name?, pType? }` for conference UI | Exported from source file; pending addition to `src/index.ts` | +| `getDefaultUIControls` | `@webex/contact-center` | Default controls fallback (all disabled) | Exported from source file; pending addition to `src/index.ts` | +| `TaskState` | `@webex/contact-center` | Enum for explicit task states — needed for consult timer labeling (`CONSULT_INITIATING` vs `CONSULTING`) | Exported from state-machine module; pending addition to `src/index.ts` | ---- +> **See [001-migration-overview.md § SDK Package Entry Point — Pending Additions](./001-migration-overview.md)** for the full list and exact `src/index.ts` changes needed. A Jira ticket is being created to track these SDK-side additions. -## New Types to Import from SDK +**Note:** `TaskState` and `TaskEvent` enums are exported from the state-machine internal module but NOT from the package-level `index.ts`. Widgets should use `task.uiControls` for control state. However, widgets **do need `TaskState`** for consult timer labeling (`calculateConsultTimerData` needs to distinguish `CONSULT_INITIATING` from `CONSULTING`). `TaskState` must be added to SDK package exports — tracked in the [SDK missing items Confluence page](./confluence-sdk-missing-items.md). + +### SDK Task Subtype Interfaces + +The SDK defines three task subtype interfaces. Widgets currently use `ITask` but may need these for type narrowing: -| Type | Source | Purpose | -|------|--------|---------| -| `TaskUIControls` | `@webex/contact-center` | Pre-computed control states | -| `TaskUIControlState` | `@webex/contact-center` | Single control `{ isVisible, isEnabled }` | -| `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | `@webex/contact-center` | New event | +| Interface | Extends | Additional Members | Package Entry Point Status | +|-----------|---------|-------------------|---------------------------| +| `ITask` | `EventEmitter` | `data`, `webCallMap`, `autoWrapup`, `accept()`, `decline()`, `hold()`, `resume()`, `end()`, `wrapup()`, `pauseRecording()`, `resumeRecording()`, `consult()`, `endConsult()`, `transfer()`, `consultConference()`, `exitConference()`, `transferConference()`, `toggleMute()`, `consultTransfer()`, `cancelAutoWrapupTimer()` | Available in `src/index.ts` | +| `IVoice` | `ITask` | `holdResume()` — single hold/resume toggle for voice | Defined in source; pending addition to `src/index.ts` | +| `IDigital` | `Omit` | `uiControls: TaskUIControls`, `updateTaskData()` returns `IDigital` | Defined in source; pending addition to `src/index.ts` | +| `IWebRTC` | `IVoice` | `toggleMute()`, `decline()`, `unregisterWebCallListeners()` | Defined in source; pending addition to `src/index.ts` | -**Note:** `TaskState` and `TaskEvent` enums are internal to SDK and NOT exported to consumers. Widgets should not depend on them directly — use `task.uiControls` instead. +**Important:** `uiControls` is currently only declared on `IDigital`, not on `ITask`. The concrete `Task` class has a `public get uiControls()` getter inherited by all subclasses (Voice, Digital, WebRTC). Adding `uiControls` to `ITask` is tracked in the Jira ticket — see 001 for details. --- @@ -229,9 +304,10 @@ This means the widget no longer needs to pass `deviceType`, `featureFlags`, or ` |------|--------| | `task/src/task.types.ts` | Import `TaskUIControls` from SDK; update hook return types | | `cc-components/.../task/task.types.ts` | Add `TaskUIControls` prop type for CallControl | -| `store/src/store.types.ts` | Ensure `TASK_UI_CONTROLS_UPDATED` is available | +| `store/src/store.types.ts` | **Delete local `TASK_EVENTS` enum** — import from SDK `@webex/contact-center` instead. Update all 5 CC-level event names to SDK task-level names. Delete local `CC_EVENTS` if SDK exports it. | | `store/src/constants.ts` | Review/remove consult state constants | | `task/src/Utils/constants.ts` | Review/remove media type constants used only for controls | +| All files importing from `store.types.ts` | Update imports to use SDK `TASK_EVENTS` | --- @@ -246,3 +322,4 @@ This means the widget no longer needs to pass `deviceType`, `featureFlags`, or ` --- _Parent: [001-migration-overview.md](./001-migration-overview.md)_ +_Updated: 2026-03-11 (complete TaskState enum, event name mapping with values, SDK interfaces, VoiceVariant, TASK_CHANNEL_TYPE, SDK export gaps)_ diff --git a/ai-docs/migration/014-task-code-scan-report.md b/ai-docs/migration/014-task-code-scan-report.md index e75fe4897..74b5f5372 100644 --- a/ai-docs/migration/014-task-code-scan-report.md +++ b/ai-docs/migration/014-task-code-scan-report.md @@ -72,7 +72,7 @@ Registers on each task: - TASK_PARTICIPANT_LEFT → handleConferenceEnded - TASK_PARTICIPANT_LEFT_FAILED → refreshTaskList - TASK_CONFERENCE_STARTED → handleConferenceStarted -- TASK_CONFERENCE_TRANSFERRED → handleConferenceEnded +- TASK_CONFERENCE_TRANSFERRED → **refreshTaskList** (registration) / **handleConferenceEnded** (cleanup) — ⚠️ callback reference mismatch, listener never removed - TASK_CONFERENCE_TRANSFER_FAILED → refreshTaskList - TASK_POST_CALL_ACTIVITY → refreshTaskList - TASK_MEDIA (browser only) → handleTaskMedia @@ -166,6 +166,36 @@ Registers on each task: **Note:** Store `findHoldTimestamp(task, mType)` vs task package `findHoldTimestamp(interaction, mType)` — different signatures. +### SDK TaskUtils — Overlapping Functions with Signature Differences + +The SDK (`TaskUtils.ts`) provides utility functions used internally by `uiControlsComputer.ts`. Some overlap with widget `task-utils.ts` but have **different signatures** (SDK takes `Interaction`/`TaskData` instead of `ITask`): + +| Widget Function | Widget Signature | SDK Function | SDK Signature | Signature Diff | +|----------------|-----------------|--------------|---------------|----------------| +| `getIsConferenceInProgress` | `(task: ITask): boolean` | `getIsConferenceInProgress` | `(data: TaskData): boolean` | Takes `TaskData` not `ITask` | +| `getIsCustomerInCall` | `(task: ITask): boolean` | `getIsCustomerInCall` | `(interaction: Interaction, interactionId: string): boolean` | Takes `Interaction` + `interactionId` | +| `getConferenceParticipantsCount` | `(task: ITask): number` | `getConferenceParticipantsCount` | `(data: TaskData, agentId: string): number` | Takes `TaskData` + `agentId` | +| `isSecondaryAgent` | `(task: ITask): boolean` | `isSecondaryAgent` | `(interaction: Interaction): boolean` | Takes `Interaction` not `ITask` | +| `isSecondaryEpDnAgent` | `(task: ITask): boolean` | `isSecondaryEpDnAgent` | `(interaction: Interaction): boolean` | Takes `Interaction` not `ITask` | + +SDK-only utilities (no widget equivalent): + +| SDK Function | Signature | Purpose | +|-------------|-----------|---------| +| `isPrimary(task, agentId)` | `(task: ITask, agentId: string): boolean` | Checks if agent is primary owner | +| `isParticipantInMainInteraction(task, agentId)` | `(task: ITask, agentId: string): boolean` | Agent is in main interaction | +| `checkParticipantNotInInteraction(task, agentId)` | `(task: ITask, agentId: string): boolean` | Agent not in any interaction media | +| `getIsConsultInProgressForConferenceControls(...)` | `(data, agentId): boolean` | Consult check for conference | +| `getIsConsultedAgentForControls(...)` | `(data, agentId): boolean` | Is consulted agent | +| `getServerHoldStateForControls(...)` | `(data, agentId): boolean\|undefined` | Server-side hold state | +| `isAutoAnswerEnabled(interaction, agentId)` | `(interaction, agentId): boolean` | Auto-answer check | +| `isWebRTCCall(interaction, voiceVariant)` | `(interaction, voiceVariant?): boolean` | WebRTC detection | +| `isDigitalOutbound(interaction)` | `(interaction): boolean` | Digital outbound check | +| `hasAgentInitiatedOutdial(interaction, agentId)` | `(interaction, agentId): boolean` | Agent initiated outdial | +| `shouldAutoAnswerTask(interaction, agentId)` | `(interaction, agentId): boolean` | Should auto-answer | + +**Migration impact:** Widget `task-utils.ts` functions used for control computation will be deleted (SDK handles this). Functions like `findHoldStatus`, `findHoldTimestamp`, `findMediaResourceId` that serve widget-specific purposes (timers, hold indicator) will be **retained**. SDK utility functions are used internally by `uiControlsComputer.ts` and are NOT exported to consumers. + --- ## 4. `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` @@ -333,7 +363,10 @@ Registers on each task: ### 5. Store Task Flow - refreshTaskList → cc.taskManager.getAllTasks() - setCurrentTask uses isIncomingTask(task, agentId) to skip incoming -- handleTaskRemove attempts to unregister task listeners, but has a **pre-existing listener leak bug**: `TASK_REJECT` and `TASK_OUTDIAL_FAILED` are registered with inline lambdas (`(reason) => this.handleTaskReject(task, reason)`) in `registerTaskEventListeners` but removed with *different* inline lambdas in `handleTaskRemove`, so those listeners are never actually removed. Fix during migration by using stored function references +- handleTaskRemove attempts to unregister task listeners, but has **pre-existing listener leak bugs**: + - `TASK_REJECT` and `TASK_OUTDIAL_FAILED`: registered with inline lambdas (`(reason) => this.handleTaskReject(task, reason)`) in `registerTaskEventListeners` but removed with *different* inline lambdas in `handleTaskRemove` — listeners never removed + - `TASK_CONFERENCE_TRANSFERRED`: registered with `this.refreshTaskList` (line 599) but removed with `this.handleConferenceEnded` (line 445) — different function references, listener never removed + - **Fix during migration:** Use stored function references for all event registrations --- @@ -359,3 +392,4 @@ Registers on each task: --- _Parent: [001-migration-overview.md](./001-migration-overview.md)_ +_Updated: 2026-03-11 (SDK TaskUtils comparison table, signature differences, SDK-only utilities)_ From d57667611d90dab4708b8347c9b9675fa25c4b0d Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 16:04:17 +0530 Subject: [PATCH 14/18] =?UTF-8?q?docs(ai-docs):=20address=20codex=20eleven?= =?UTF-8?q?th=20review=20=E2=80=94=20keep=20findHoldStatus=20in=20002=20fi?= =?UTF-8?q?les=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-docs/migration/002-ui-controls-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-docs/migration/002-ui-controls-migration.md b/ai-docs/migration/002-ui-controls-migration.md index 96bf49f41..2086b77b6 100644 --- a/ai-docs/migration/002-ui-controls-migration.md +++ b/ai-docs/migration/002-ui-controls-migration.md @@ -192,7 +192,7 @@ return { | `cc-components/src/components/task/task.types.ts` | Align `ControlProps` with new control names | | `cc-components/src/components/task/CallControl/call-control.tsx` | Update prop names (`holdResume` → `hold`, etc.) | | `cc-components/src/components/task/CallControl/call-control.utils.ts` | Simplify/remove old control derivation | -| `store/src/task-utils.ts` | Remove `getConsultStatus`, `findHoldStatus` if no longer consumed | +| `store/src/task-utils.ts` | Remove `getConsultStatus` if no longer consumed. **KEEP `findHoldStatus`** — still needed for `isHeld` derivation (see Removed State Flags table above) | | All test files for above | Update to test new contract | --- From 4a28a430e0ffa73e8a3d2cb56f4c335ff36a5385 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 20:00:40 +0530 Subject: [PATCH 15/18] =?UTF-8?q?docs(ai-docs):=20address=20reviewer=20fee?= =?UTF-8?q?dback=20=E2=80=94=20restructure=20001=20overview,=20move=20migr?= =?UTF-8?q?ation=20docs=20to=20contact-center=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + .../migration/001-migration-overview.md | 185 +++++++----------- .../migration/002-ui-controls-migration.md | 0 .../009-types-and-constants-migration.md | 0 .../migration/014-task-code-scan-report.md | 26 ++- 5 files changed, 100 insertions(+), 113 deletions(-) rename {ai-docs => packages/contact-center/ai-docs}/migration/001-migration-overview.md (61%) rename {ai-docs => packages/contact-center/ai-docs}/migration/002-ui-controls-migration.md (100%) rename {ai-docs => packages/contact-center/ai-docs}/migration/009-types-and-constants-migration.md (100%) rename {ai-docs => packages/contact-center/ai-docs}/migration/014-task-code-scan-report.md (92%) diff --git a/AGENTS.md b/AGENTS.md index 70963b493..f50e1299c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -532,6 +532,7 @@ yarn build ``` ccWidgets/ ├── packages/contact-center/ +│ ├── ai-docs/migration/ # Task refactor migration docs (old → new) │ ├── station-login/ # Widget with ai-docs/ │ ├── user-state/ # Widget with ai-docs/ │ ├── task/ # Widget package @@ -613,6 +614,7 @@ ccWidgets/ - **Repository Rules:** [RULES.md](./RULES.md) - **Templates Overview:** [templates/README.md](./ai-docs/templates/README.md) +- **Task Refactor Migration (Contact Center):** [packages/contact-center/ai-docs/migration/001-migration-overview.md](./packages/contact-center/ai-docs/migration/001-migration-overview.md) — master index for CC SDK task-refactor migration docs --- diff --git a/ai-docs/migration/001-migration-overview.md b/packages/contact-center/ai-docs/migration/001-migration-overview.md similarity index 61% rename from ai-docs/migration/001-migration-overview.md rename to packages/contact-center/ai-docs/migration/001-migration-overview.md index a942d8968..7a48da3b2 100644 --- a/ai-docs/migration/001-migration-overview.md +++ b/packages/contact-center/ai-docs/migration/001-migration-overview.md @@ -8,91 +8,97 @@ This document set guides the migration of CC Widgets from the **old ad-hoc task ## Migration Document Index -| # | Document | Scope | Risk | Priority | -|---|----------|-------|------|----------| -| 002 | [002-ui-controls-migration.md](./002-ui-controls-migration.md) | Replace `getControlsVisibility()` with `task.uiControls` | **High** (core UX) | P0 | -| 003 | [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) | Refactor store event handlers to leverage state machine events | **Medium** | P1 | -| 004 | [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) | Refactor `useCallControl` hook, timer utils, fix bugs | **High** (largest widget) | P0 | -| 005 | [005-incoming-task-migration.md](./005-incoming-task-migration.md) | Refactor `useIncomingTask` for state-machine offer/assign flow | **Low** | P2 | -| 006 | [006-task-list-migration.md](./006-task-list-migration.md) | Refactor `useTaskList` for per-task `uiControls` | **Low** | P2 | -| 007 | [007-outdial-call-migration.md](./007-outdial-call-migration.md) | No changes needed (CC-level, not task-level) | **Low** | P3 | -| 008 | [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) | Retire or thin out `task-utils.ts`, fix `findHoldTimestamp` dual signatures | **Medium** | P1 | -| 009 | [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) | Align types/constants with SDK, document `UIControlConfig` | **Medium** | P1 | -| 010 | [010-component-layer-migration.md](./010-component-layer-migration.md) | Update `cc-components` to accept new control shape from SDK | **Medium** | P1 | -| 011 | [011-execution-plan.md](./011-execution-plan.md) | Step-by-step spec-first execution plan with 10 milestones | — | — | -| 012 | [012-task-lifecycle-flows-old-vs-new.md](./012-task-lifecycle-flows-old-vs-new.md) | End-to-end task flows (14 scenarios) with old vs new tracing | — | Reference | -| 013 | [013-file-inventory-old-control-references.md](./013-file-inventory-old-control-references.md) | Complete file-by-file inventory of every old control reference | — | Reference | -| 014 | [014-task-code-scan-report.md](./014-task-code-scan-report.md) | Deep code scan findings across both CC SDK and CC Widgets repos | — | Reference | +| # | Document | Scope | +|---|----------|-------| +| 002 | [002-ui-controls-migration.md](./002-ui-controls-migration.md) | Replace `getControlsVisibility()` with `task.uiControls` | +| 003 | [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) | Update store event handlers — same events, now emitted via state machine; task state updates handled by SDK (e.g., remove `refreshTaskList` calls) | +| 004 | [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) | Refactor `useCallControl` hook and timer utils to consume `task.uiControls` | +| 005 | [005-incoming-task-migration.md](./005-incoming-task-migration.md) | Refactor `useIncomingTask` for state-machine offer/assign flow | +| 006 | [006-task-list-migration.md](./006-task-list-migration.md) | Refactor `useTaskList` for per-task `uiControls` | +| 008 | [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) | Retire control-computation utils from `task-utils.ts` — SDK handles these now | +| 009 | [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) | Align types/constants with SDK `TaskUIControls` and `TaskState` | +| 010 | [010-component-layer-migration.md](./010-component-layer-migration.md) | Update `cc-components` to accept `TaskUIControls` shape | +| 011 | [011-execution-plan.md](./011-execution-plan.md) | Step-by-step execution plan with milestones | +| 012 | [012-task-lifecycle-flows-old-vs-new.md](./012-task-lifecycle-flows-old-vs-new.md) | End-to-end task flows (14 scenarios) with old vs new tracing | +| 013 | [013-file-inventory-old-control-references.md](./013-file-inventory-old-control-references.md) | File-by-file inventory of every old control reference | +| 014 | [014-task-code-scan-report.md](./014-task-code-scan-report.md) | Deep code scan findings across both CC SDK and CC Widgets repos | --- -## Key Architectural Shift +## Architectural Change: Old vs New + +### Old Approach + +Widgets derive control visibility from raw task data using `getControlsVisibility()`: -### Before (Old Approach) ``` -SDK emits 30+ events → Store handlers manually update observables → -Widgets compute UI controls via getControlsVisibility() → -Components receive {isVisible, isEnabled} per control +SDK emits 30+ task events → +Store handlers manually call refreshTaskList() and update observables → +Hooks call getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled) → +Hooks derive state flags (isHeld, isConsultInitiated, isConferenceInProgress, etc.) → +Components receive a flat ControlVisibility object ``` -**Problems:** -- Control visibility logic duplicated between SDK and widgets -- Ad-hoc state derivation from raw task data (consult status, hold status, conference flags) -- Fragile: every new state requires changes in widgets, store utils, AND component logic -- No single source of truth for "what state is this task in?" +Each hook and utility function independently interprets raw task data to decide which buttons to show/enable. Control logic is spread across `task-util.ts`, `task-utils.ts`, `timer-utils.ts`, and component utils. + +### New Approach + +SDK computes all control states internally via a state machine and exposes `task.uiControls`: -### After (New Approach) ``` -SDK state machine handles all transitions → -SDK computes task.uiControls automatically → -SDK emits task:ui-controls-updated → -Widgets consume task.uiControls directly → -Components receive {isVisible, isEnabled} per control +SDK state machine transitions on task events → +SDK computes TaskUIControls from (TaskState + TaskContext) in uiControlsComputer.ts → +SDK emits 'task:ui-controls-updated' event → +Widgets read task.uiControls directly → +Components receive TaskUIControls (structured per-control object) ``` -**Benefits:** -- Single source of truth: `task.uiControls` from SDK -- Widget code dramatically simplified (remove ~600 lines of control visibility logic) -- Store utils thinned (most consult/conference/hold status checks no longer needed) -- New states automatically handled by SDK, zero widget changes needed -- Parity with Agent Desktop guaranteed by SDK +The `TaskUIControls` type provides a per-control `{ isVisible, isEnabled }` shape for 17 controls: + +```typescript +type TaskUIControlState = { isVisible: boolean; isEnabled: boolean }; + +type TaskUIControls = { + accept: TaskUIControlState; + decline: TaskUIControlState; + hold: TaskUIControlState; + transfer: TaskUIControlState; + consult: TaskUIControlState; + end: TaskUIControlState; + recording: TaskUIControlState; + mute: TaskUIControlState; + consultTransfer: TaskUIControlState; + endConsult: TaskUIControlState; + conference: TaskUIControlState; + exitConference: TaskUIControlState; + transferConference: TaskUIControlState; + mergeToConference: TaskUIControlState; + wrapup: TaskUIControlState; + switchToMainCall: TaskUIControlState; + switchToConsult: TaskUIControlState; +}; +``` + +Widgets no longer derive control visibility — they consume `task.uiControls` as the single source of truth. --- -## Repo Paths Reference +## CC Widgets Files Affected -### CC Widgets (this repo) | Area | Path | |------|------| -| Task widgets | `packages/contact-center/task/src/` | | Task hooks | `packages/contact-center/task/src/helper.ts` | -| Task UI utils (OLD) | `packages/contact-center/task/src/Utils/task-util.ts` | -| Task constants | `packages/contact-center/task/src/Utils/constants.ts` | +| Task UI utils (OLD — to be removed) | `packages/contact-center/task/src/Utils/task-util.ts` | | Task timer utils | `packages/contact-center/task/src/Utils/timer-utils.ts` | | Hold timer hook | `packages/contact-center/task/src/Utils/useHoldTimer.ts` | | Task types | `packages/contact-center/task/src/task.types.ts` | -| Store | `packages/contact-center/store/src/store.ts` | | Store event wrapper | `packages/contact-center/store/src/storeEventsWrapper.ts` | -| Store task utils (OLD) | `packages/contact-center/store/src/task-utils.ts` | +| Store task utils | `packages/contact-center/store/src/task-utils.ts` | | Store constants | `packages/contact-center/store/src/constants.ts` | -| CC Components task | `packages/contact-center/cc-components/src/components/task/` | +| CC Components — CallControl | `packages/contact-center/cc-components/src/components/task/CallControl/` | +| CC Components — CallControlCAD | `packages/contact-center/cc-components/src/components/task/CallControlCAD/` | | CC Components types | `packages/contact-center/cc-components/src/components/task/task.types.ts` | -### CC SDK (task-refactor branch) -| Area | Path | -|------|------| -| State machine | `packages/@webex/contact-center/src/services/task/state-machine/` | -| UI controls computer | `.../state-machine/uiControlsComputer.ts` | -| State machine config | `.../state-machine/TaskStateMachine.ts` | -| Guards | `.../state-machine/guards.ts` | -| Actions | `.../state-machine/actions.ts` | -| Constants (TaskState, TaskEvent) | `.../state-machine/constants.ts` | -| Types | `.../state-machine/types.ts` | -| Task service | `.../task/Task.ts` | -| Task manager | `.../task/TaskManager.ts` | -| Task types | `.../task/types.ts` | -| Sample app | `docs/samples/contact-center/app.js` | - --- ## CC SDK Task-Refactor Branch Reference @@ -131,19 +137,6 @@ These decisions in the SDK directly impact how the migration docs should be inte --- -## SDK Version Requirements - -The CC Widgets migration depends on the CC SDK `task-refactor` branch being merged and released. Key new APIs: - -| API | Type | Description | -|-----|------|-------------| -| `task.uiControls` | Property (getter) | Pre-computed `TaskUIControls` object | -| `task:ui-controls-updated` | Event | Emitted when any control's visibility/enabled state changes | -| `TaskUIControls` | Type | `{ [controlName]: { isVisible: boolean, isEnabled: boolean } }` | -| `TaskState` | Enum | Explicit task states (IDLE, OFFERED, CONNECTED, HELD, etc.) | - ---- - ## SDK Package Entry Point — Pending Additions > **As of the `task-refactor` branch snapshot reviewed,** the items below are properly exported from their individual source files within the SDK but are **not yet re-exported** from the package-level entry point (`src/index.ts`). This means widgets cannot `import { ... } from '@webex/contact-center'` for these items until the SDK team adds them. @@ -209,14 +202,7 @@ These bugs exist in the current codebase and should be fixed during migration: Setup uses `TASK_EVENTS.TASK_RECORDING_PAUSED` / `TASK_EVENTS.TASK_RECORDING_RESUMED`, but cleanup uses `TASK_EVENTS.CONTACT_RECORDING_PAUSED` / `TASK_EVENTS.CONTACT_RECORDING_RESUMED`. Callbacks are never properly removed. -### 2. `findHoldTimestamp` Dual Signatures -Two `findHoldTimestamp` functions with different signatures: -- `store/src/task-utils.ts`: `findHoldTimestamp(task: ITask, mType: string)` -- `task/src/Utils/task-util.ts`: `findHoldTimestamp(interaction: Interaction, mType: string)` - -Should be consolidated to one function during migration. - -### 3. Event Name Mismatches Between Widget and SDK +### 2. Event Name Mismatches Between Widget and SDK Widget `store.types.ts` declares a local `TASK_EVENTS` enum (line 210: `TODO: remove this once cc sdk exports this enum`) with 5 events using CC-level naming that differ from SDK task-level naming: - `AGENT_WRAPPEDUP = 'AgentWrappedUp'` → SDK: `TASK_WRAPPEDUP = 'task:wrappedup'` - `AGENT_CONSULT_CREATED = 'AgentConsultCreated'` → SDK: `TASK_CONSULT_CREATED = 'task:consultCreated'` @@ -228,42 +214,17 @@ See [009-types-and-constants-migration.md § Event Constants](./009-types-and-co --- -## Critical Migration Notes - -### UIControlConfig Is Built by SDK (Not by Widgets) - -Widgets do NOT need to provide `UIControlConfig`. The SDK builds it internally from agent profile, `callProcessingDetails`, media type, and voice variant. This means `deviceType`, `featureFlags`, and `conferenceEnabled` **can be removed** from `useCallControlProps` — they are only used for `getControlsVisibility()` which is being eliminated. **Note:** `agentId` must be retained because it is also used by timer utilities (`calculateStateTimerData`, `calculateConsultTimerData`) to look up the agent's participant record from `interaction.participants`. +## Migration Notes -### Timer Utils Dependency on `controlVisibility` +These are specific implementation details that migration work must account for: -`calculateStateTimerData()` and `calculateConsultTimerData()` in `timer-utils.ts` accept `controlVisibility` as a parameter with old control names. These functions must be migrated to accept `TaskUIControls` (new control names). +1. **`UIControlConfig` is built by SDK:** Widgets do NOT provide it. `deviceType`, `featureFlags`, `conferenceEnabled` can be removed from `useCallControlProps`. **`agentId` must be retained** — timer utilities need it for participant lookup. -### `task:wrapup` Race Condition +2. **Timer utils depend on old `controlVisibility`:** `calculateStateTimerData()` and `calculateConsultTimerData()` in `timer-utils.ts` must be updated to accept `TaskUIControls` instead. -The SDK sample app uses `setTimeout(..., 0)` before updating UI after `task:wrapup`. Consider adding a similar guard in the hook to avoid control flickering during wrapup transition. - -### Sample App Reference Pattern - -The CC SDK sample app (`docs/samples/contact-center/app.js`) demonstrates the canonical pattern: - -```javascript -task.on('task:ui-controls-updated', () => { - updateCallControlUI(task); -}); - -function updateCallControlUI(task) { - const uiControls = task.uiControls || {}; - applyAllControlsFromUIControls(uiControls); -} - -function applyControlState(element, control) { - element.style.display = control?.isVisible ? 'inline-block' : 'none'; - element.disabled = !control?.isEnabled; -} -``` +3. **`task:wrapup` timing:** The SDK sample app uses `setTimeout(..., 0)` before UI update after `task:wrapup` to avoid control flickering during the transition. --- _Created: 2026-03-09_ -_Updated: 2026-03-09 (added deep scan findings, before/after examples, bug reports)_ -_Updated: 2026-03-11 (SDK export gaps, holdResume, event name mismatches — from deep SDK comparison)_ +_Updated: 2026-03-11 (restructured per reviewer feedback: simplified architecture section, removed Priority column and doc 007 row, removed SDK Version Requirements, simplified migration notes, added CallControlCAD to file list)_ diff --git a/ai-docs/migration/002-ui-controls-migration.md b/packages/contact-center/ai-docs/migration/002-ui-controls-migration.md similarity index 100% rename from ai-docs/migration/002-ui-controls-migration.md rename to packages/contact-center/ai-docs/migration/002-ui-controls-migration.md diff --git a/ai-docs/migration/009-types-and-constants-migration.md b/packages/contact-center/ai-docs/migration/009-types-and-constants-migration.md similarity index 100% rename from ai-docs/migration/009-types-and-constants-migration.md rename to packages/contact-center/ai-docs/migration/009-types-and-constants-migration.md diff --git a/ai-docs/migration/014-task-code-scan-report.md b/packages/contact-center/ai-docs/migration/014-task-code-scan-report.md similarity index 92% rename from ai-docs/migration/014-task-code-scan-report.md rename to packages/contact-center/ai-docs/migration/014-task-code-scan-report.md index 74b5f5372..da860a944 100644 --- a/ai-docs/migration/014-task-code-scan-report.md +++ b/packages/contact-center/ai-docs/migration/014-task-code-scan-report.md @@ -326,7 +326,28 @@ SDK-only utilities (no widget equivalent): --- -## 19. `widgets-samples/cc/samples-cc-react-app/` +## 19. `packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx` + +- **Props:** Uses `controlVisibility` from props (same shape as `call-control.tsx`) +- **Old references:** + - `controlVisibility.isConferenceInProgress` — conference participant display + - `controlVisibility.wrapup.isVisible` — conditional rendering + - `controlVisibility.isHeld`, `controlVisibility.isConsultReceived`, `controlVisibility.consultCallHeld` — hold status indicator + - `controlVisibility.recordingIndicator.isVisible` — recording badge display + - `controlVisibility.isConsultInitiatedOrAccepted` — consult panel toggle +- **Migration:** Must update to accept `TaskUIControls` prop and map old control names to new. Same migration pattern as `call-control.tsx` (Doc 010). + +--- + +## 20. `widgets-samples/cc/samples-cc-wc-app/app.js` + +- **Web Component usage:** Creates `widget-cc-incoming-task`, `widget-cc-task-list`, `widget-cc-call-control`, `widget-cc-call-control-cad`, `widget-cc-outdial-call` +- **Callback wiring:** `ccCallControl.onHoldResume`, `ccCallControl.onEnd`, `ccCallControl.onWrapUp`, `ccCallControlCAD.onHoldResume`, etc. +- **Migration:** Callback prop names may change if WC attribute names are updated to match new SDK control names. Review after component migration. + +--- + +## 21. `widgets-samples/cc/samples-cc-react-app/` - **App.tsx:** IncomingTask, TaskList, CallControl; onIncomingTaskCB, onAccepted, onRejected, onTaskAccepted, onTaskDeclined, onTaskSelected; store.currentTask, store.setIncomingTaskCb - **EngageWidget.tsx:** store.currentTask, mediaType, isSupportedTask @@ -384,12 +405,15 @@ SDK-only utilities (no widget equivalent): | task/src/Utils/timer-utils.ts | ✅ Timer computation | | task/src/Utils/useHoldTimer.ts | ✅ Hold timer | | cc-components CallControl/* | ✅ Props, utils | +| cc-components CallControlCAD/* | ✅ controlVisibility, isHeld, recordingIndicator | | cc-components Task/* | ✅ Display | | cc-components TaskList/* | ✅ Utils, isIncomingTask | | cc-components IncomingTask/* | ✅ Utils | +| samples-cc-wc-app | ✅ WC callback wiring | | samples-cc-react-app | ✅ Full usage | --- _Parent: [001-migration-overview.md](./001-migration-overview.md)_ _Updated: 2026-03-11 (SDK TaskUtils comparison table, signature differences, SDK-only utilities)_ +_Updated: 2026-03-11 (added CallControlCAD, WC sample app — per reviewer feedback)_ From 91dd40532f81e228d1633c4dca845b4bd5085482 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 12 Mar 2026 13:00:34 +0530 Subject: [PATCH 16/18] docs(ai-docs): consolidate PR 1 migration docs into single overview Replaced 4 files (001-migration-overview, 002-ui-controls-migration, 009-types-and-constants-migration, 014-task-code-scan-report) with a single migration-overview.md. Reordered sections for spec-driven execution flow. Moved detailed constants, event mappings, and gotchas to PR 2/3 docs where they are actionable. Made-with: Cursor --- AGENTS.md | 2 +- .../migration/001-migration-overview.md | 230 ---------- .../migration/002-ui-controls-migration.md | 213 --------- .../009-types-and-constants-migration.md | 325 -------------- .../migration/014-task-code-scan-report.md | 419 ------------------ .../ai-docs/migration/migration-overview.md | 131 ++++++ 6 files changed, 132 insertions(+), 1188 deletions(-) delete mode 100644 packages/contact-center/ai-docs/migration/001-migration-overview.md delete mode 100644 packages/contact-center/ai-docs/migration/002-ui-controls-migration.md delete mode 100644 packages/contact-center/ai-docs/migration/009-types-and-constants-migration.md delete mode 100644 packages/contact-center/ai-docs/migration/014-task-code-scan-report.md create mode 100644 packages/contact-center/ai-docs/migration/migration-overview.md diff --git a/AGENTS.md b/AGENTS.md index f50e1299c..193649a3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -614,7 +614,7 @@ ccWidgets/ - **Repository Rules:** [RULES.md](./RULES.md) - **Templates Overview:** [templates/README.md](./ai-docs/templates/README.md) -- **Task Refactor Migration (Contact Center):** [packages/contact-center/ai-docs/migration/001-migration-overview.md](./packages/contact-center/ai-docs/migration/001-migration-overview.md) — master index for CC SDK task-refactor migration docs +- **Task Refactor Migration (Contact Center):** [packages/contact-center/ai-docs/migration/migration-overview.md](./packages/contact-center/ai-docs/migration/migration-overview.md) — overview and entry point for CC SDK task-refactor migration docs --- diff --git a/packages/contact-center/ai-docs/migration/001-migration-overview.md b/packages/contact-center/ai-docs/migration/001-migration-overview.md deleted file mode 100644 index 7a48da3b2..000000000 --- a/packages/contact-center/ai-docs/migration/001-migration-overview.md +++ /dev/null @@ -1,230 +0,0 @@ -# Migration Doc 001: Task Refactor Migration Overview - -## Purpose - -This document set guides the migration of CC Widgets from the **old ad-hoc task state management** to the **new state-machine-driven architecture** in CC SDK (`task-refactor` branch). - ---- - -## Migration Document Index - -| # | Document | Scope | -|---|----------|-------| -| 002 | [002-ui-controls-migration.md](./002-ui-controls-migration.md) | Replace `getControlsVisibility()` with `task.uiControls` | -| 003 | [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) | Update store event handlers — same events, now emitted via state machine; task state updates handled by SDK (e.g., remove `refreshTaskList` calls) | -| 004 | [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) | Refactor `useCallControl` hook and timer utils to consume `task.uiControls` | -| 005 | [005-incoming-task-migration.md](./005-incoming-task-migration.md) | Refactor `useIncomingTask` for state-machine offer/assign flow | -| 006 | [006-task-list-migration.md](./006-task-list-migration.md) | Refactor `useTaskList` for per-task `uiControls` | -| 008 | [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) | Retire control-computation utils from `task-utils.ts` — SDK handles these now | -| 009 | [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) | Align types/constants with SDK `TaskUIControls` and `TaskState` | -| 010 | [010-component-layer-migration.md](./010-component-layer-migration.md) | Update `cc-components` to accept `TaskUIControls` shape | -| 011 | [011-execution-plan.md](./011-execution-plan.md) | Step-by-step execution plan with milestones | -| 012 | [012-task-lifecycle-flows-old-vs-new.md](./012-task-lifecycle-flows-old-vs-new.md) | End-to-end task flows (14 scenarios) with old vs new tracing | -| 013 | [013-file-inventory-old-control-references.md](./013-file-inventory-old-control-references.md) | File-by-file inventory of every old control reference | -| 014 | [014-task-code-scan-report.md](./014-task-code-scan-report.md) | Deep code scan findings across both CC SDK and CC Widgets repos | - ---- - -## Architectural Change: Old vs New - -### Old Approach - -Widgets derive control visibility from raw task data using `getControlsVisibility()`: - -``` -SDK emits 30+ task events → -Store handlers manually call refreshTaskList() and update observables → -Hooks call getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled) → -Hooks derive state flags (isHeld, isConsultInitiated, isConferenceInProgress, etc.) → -Components receive a flat ControlVisibility object -``` - -Each hook and utility function independently interprets raw task data to decide which buttons to show/enable. Control logic is spread across `task-util.ts`, `task-utils.ts`, `timer-utils.ts`, and component utils. - -### New Approach - -SDK computes all control states internally via a state machine and exposes `task.uiControls`: - -``` -SDK state machine transitions on task events → -SDK computes TaskUIControls from (TaskState + TaskContext) in uiControlsComputer.ts → -SDK emits 'task:ui-controls-updated' event → -Widgets read task.uiControls directly → -Components receive TaskUIControls (structured per-control object) -``` - -The `TaskUIControls` type provides a per-control `{ isVisible, isEnabled }` shape for 17 controls: - -```typescript -type TaskUIControlState = { isVisible: boolean; isEnabled: boolean }; - -type TaskUIControls = { - accept: TaskUIControlState; - decline: TaskUIControlState; - hold: TaskUIControlState; - transfer: TaskUIControlState; - consult: TaskUIControlState; - end: TaskUIControlState; - recording: TaskUIControlState; - mute: TaskUIControlState; - consultTransfer: TaskUIControlState; - endConsult: TaskUIControlState; - conference: TaskUIControlState; - exitConference: TaskUIControlState; - transferConference: TaskUIControlState; - mergeToConference: TaskUIControlState; - wrapup: TaskUIControlState; - switchToMainCall: TaskUIControlState; - switchToConsult: TaskUIControlState; -}; -``` - -Widgets no longer derive control visibility — they consume `task.uiControls` as the single source of truth. - ---- - -## CC Widgets Files Affected - -| Area | Path | -|------|------| -| Task hooks | `packages/contact-center/task/src/helper.ts` | -| Task UI utils (OLD — to be removed) | `packages/contact-center/task/src/Utils/task-util.ts` | -| Task timer utils | `packages/contact-center/task/src/Utils/timer-utils.ts` | -| Hold timer hook | `packages/contact-center/task/src/Utils/useHoldTimer.ts` | -| Task types | `packages/contact-center/task/src/task.types.ts` | -| Store event wrapper | `packages/contact-center/store/src/storeEventsWrapper.ts` | -| Store task utils | `packages/contact-center/store/src/task-utils.ts` | -| Store constants | `packages/contact-center/store/src/constants.ts` | -| CC Components — CallControl | `packages/contact-center/cc-components/src/components/task/CallControl/` | -| CC Components — CallControlCAD | `packages/contact-center/cc-components/src/components/task/CallControlCAD/` | -| CC Components types | `packages/contact-center/cc-components/src/components/task/task.types.ts` | - ---- - -## CC SDK Task-Refactor Branch Reference - -> **Repo:** [webex/webex-js-sdk (task-refactor)](https://github.com/webex/webex-js-sdk/tree/task-refactor) -> **Local path:** `/Users/akulakum/Documents/CC_SDK/webex-js-sdk` (branch: `task-refactor`) - -### Key SDK Source Files - -| File | Purpose | -|------|---------| -| `uiControlsComputer.ts` | Computes `TaskUIControls` from `TaskState` + `TaskContext` — the single source of truth for all control visibility/enabled states | -| `constants.ts` | `TaskState` enum (IDLE, OFFERED, CONNECTED, HELD, CONSULT_INITIATING, CONSULTING, CONF_INITIATING, CONFERENCING, WRAPPING_UP, COMPLETED, TERMINATED, etc.) and `TaskEvent` enum | -| `types.ts` | `TaskContext`, `UIControlConfig`, `TaskStateMachineConfig` | -| `TaskStateMachine.ts` | State machine configuration with transitions, guards, and actions | -| `actions.ts` | State machine action implementations | -| `guards.ts` | Transition guard conditions | -| `../Task.ts` | Task service exposing `task.uiControls` getter and `task:ui-controls-updated` event | -| `../TaskUtils.ts` | Shared utility functions used by `uiControlsComputer.ts` (e.g., `getIsConferenceInProgress`, `getIsCustomerInCall`) | - -### Key SDK Architectural Decisions - -These decisions in the SDK directly impact how the migration docs should be interpreted: - -1. **`exitConference` visibility:** In the SDK, `exitConference` is `VISIBLE_DISABLED` (not hidden) during consulting-from-conference. This differs from the old widget logic where it was hidden. `exitConference.isVisible` is therefore more reliable in the new SDK for detecting conference state, but consulted agents not in conferencing state still see `DISABLED`. - -2. **`TaskState.CONSULT_INITIATING` vs `CONSULTING`:** The SDK has `CONSULT_INITIATING` (consult requested, async in-progress) and `CONSULTING` (consult accepted, actively consulting) as distinct states. The old widget constant `TASK_STATE_CONSULT` ('consult') maps to `CONSULT_INITIATING`, NOT `CONSULTING`. `TaskState.CONSULT_INITIATED` exists in the enum but is marked "NOT IMPLEMENTED". - -3. **Recording control:** SDK computes: `recordingInProgress ? VISIBLE_ENABLED : VISIBLE_DISABLED` (line 228 of `uiControlsComputer.ts`). So: `recording.isEnabled = true` when recording is active (button clickable to pause). `recording.isEnabled = false` when recording is NOT active (button visible but disabled — nothing to pause/resume). Recording start is handled separately, not via this control's `isEnabled` flag. Widget button wiring (`disabled: !isEnabled`) is correct with this semantic. - -4. **`isHeld` derivation:** The SDK computes `isHeld` from `serverHold ?? state === TaskState.HELD` (line 81 of `uiControlsComputer.ts`). Hold control can be `VISIBLE_DISABLED` in conference/consulting states without meaning the call is held. Widgets must derive `isHeld` from task data (`findHoldStatus`), not from `controls.hold.isEnabled`. - -5. **`UIControlConfig` built internally:** The SDK builds `UIControlConfig` from agent profile, `callProcessingDetails`, media type, and voice variant. Widgets do NOT need to provide it. - -6. **Conference state (`inConference`):** The SDK computes `inConference` as `conferenceActive && (isConferencing || selfInMainCall || consultInitiator)` (line 97). This is broader than `isConferencing` state alone, accounting for backend conference flags and consult-from-conference flows. - ---- - -## SDK Package Entry Point — Pending Additions - -> **As of the `task-refactor` branch snapshot reviewed,** the items below are properly exported from their individual source files within the SDK but are **not yet re-exported** from the package-level entry point (`src/index.ts`). This means widgets cannot `import { ... } from '@webex/contact-center'` for these items until the SDK team adds them. -> -> **A Jira ticket is being created** to track adding these missing exports to the SDK `src/index.ts` before the widget migration begins. - -### 1. Add `uiControls` to `ITask` interface - -The `uiControls` getter is defined on the **concrete `Task` class** and works at runtime on all task instances (Voice, Digital, WebRTC). However, it is not yet declared on the `ITask` interface — only on `IDigital`. Since widgets import `ITask`, TypeScript won't recognize `task.uiControls` until the interface is updated. - -**SDK change:** Add `uiControls: TaskUIControls` to `ITask` interface in `services/task/types.ts`. - -### 2. Add `TaskUIControls` type to package exports - -`TaskUIControls` is exported from `services/task/types.ts` but not re-exported from `src/index.ts`. Similarly, `TaskUIControlState` (the `{ isVisible, isEnabled }` shape) is a local type — should be exported if widgets need it for prop typing. - -**SDK change:** Add to the "Task related types" export block in `src/index.ts`: -```typescript -export type { TaskUIControls, TaskUIControlState } from './services/task/types'; -``` - -### 3. Add `getDefaultUIControls()` to package exports - -`getDefaultUIControls()` is exported from `uiControlsComputer.ts` and the state-machine `index.ts`, but not from `src/index.ts`. Widgets need it as a fallback: `task?.uiControls ?? getDefaultUIControls()`. - -**SDK change:** Add to `src/index.ts`: -```typescript -export { getDefaultUIControls } from './services/task/state-machine/uiControlsComputer'; -``` - -### 4. Add `TaskState` enum to package exports - -`TaskState` is exported from the state-machine internal module but not from the package entry point. Widgets need it for consult timer labeling — `calculateConsultTimerData` must distinguish `CONSULT_INITIATING` (consult requested) from `CONSULTING` (consult accepted) for correct timer labels. - -**SDK change:** Add to `src/index.ts`: -```typescript -export { TaskState } from './services/task/state-machine/constants'; -``` - -### 5. Add `IVoice`, `IDigital`, `IWebRTC` to package exports - -These task subtype interfaces are defined but not re-exported. Widgets may need them for type narrowing (e.g., to access `holdResume()` on voice tasks). - -**SDK change:** Add to `src/index.ts`: -```typescript -export type { IVoice, IDigital, IWebRTC } from './services/task/types'; -``` - -### 6. `holdResume()` only on Voice tasks (informational — no SDK change needed) - -The base `Task` class defines `hold()` and `resume()` that throw `unsupportedMethodError`. **Voice** tasks override both to delegate to `holdResume()` — a single toggle. The `ITask` interface exposes `hold(mediaResourceId?)` and `resume(mediaResourceId?)`, but voice tasks actually use `holdResume()` internally (from `IVoice`). - -**Widget impact:** Widgets calling `task.hold()` / `task.resume()` will work correctly on voice tasks (they delegate to `holdResume`). No widget change needed unless widgets want to call `holdResume()` directly — in which case they need `IVoice` typing (covered in item 4 above). - ---- - -## Pre-existing Bugs Found During Analysis - -These bugs exist in the current codebase and should be fixed during migration: - -### 1. Recording Callback Cleanup Mismatch -**File:** `task/src/helper.ts` (useCallControl), lines 634-653 - -Setup uses `TASK_EVENTS.TASK_RECORDING_PAUSED` / `TASK_EVENTS.TASK_RECORDING_RESUMED`, but cleanup uses `TASK_EVENTS.CONTACT_RECORDING_PAUSED` / `TASK_EVENTS.CONTACT_RECORDING_RESUMED`. Callbacks are never properly removed. - -### 2. Event Name Mismatches Between Widget and SDK -Widget `store.types.ts` declares a local `TASK_EVENTS` enum (line 210: `TODO: remove this once cc sdk exports this enum`) with 5 events using CC-level naming that differ from SDK task-level naming: -- `AGENT_WRAPPEDUP = 'AgentWrappedUp'` → SDK: `TASK_WRAPPEDUP = 'task:wrappedup'` -- `AGENT_CONSULT_CREATED = 'AgentConsultCreated'` → SDK: `TASK_CONSULT_CREATED = 'task:consultCreated'` -- `AGENT_OFFER_CONTACT = 'AgentOfferContact'` → SDK: `TASK_OFFER_CONTACT = 'task:offerContact'` -- `CONTACT_RECORDING_PAUSED = 'ContactRecordingPaused'` → SDK: `TASK_RECORDING_PAUSED = 'task:recordingPaused'` -- `CONTACT_RECORDING_RESUMED = 'ContactRecordingResumed'` → SDK: `TASK_RECORDING_RESUMED = 'task:recordingResumed'` - -See [009-types-and-constants-migration.md § Event Constants](./009-types-and-constants-migration.md) for the complete mapping. - ---- - -## Migration Notes - -These are specific implementation details that migration work must account for: - -1. **`UIControlConfig` is built by SDK:** Widgets do NOT provide it. `deviceType`, `featureFlags`, `conferenceEnabled` can be removed from `useCallControlProps`. **`agentId` must be retained** — timer utilities need it for participant lookup. - -2. **Timer utils depend on old `controlVisibility`:** `calculateStateTimerData()` and `calculateConsultTimerData()` in `timer-utils.ts` must be updated to accept `TaskUIControls` instead. - -3. **`task:wrapup` timing:** The SDK sample app uses `setTimeout(..., 0)` before UI update after `task:wrapup` to avoid control flickering during the transition. - ---- - -_Created: 2026-03-09_ -_Updated: 2026-03-11 (restructured per reviewer feedback: simplified architecture section, removed Priority column and doc 007 row, removed SDK Version Requirements, simplified migration notes, added CallControlCAD to file list)_ diff --git a/packages/contact-center/ai-docs/migration/002-ui-controls-migration.md b/packages/contact-center/ai-docs/migration/002-ui-controls-migration.md deleted file mode 100644 index 2086b77b6..000000000 --- a/packages/contact-center/ai-docs/migration/002-ui-controls-migration.md +++ /dev/null @@ -1,213 +0,0 @@ -# Migration Doc 002: UI Controls — `getControlsVisibility()` → `task.uiControls` - -## Summary - -The largest single change in this migration. CC Widgets currently computes all call control button visibility/enabled states in `task-util.ts::getControlsVisibility()` (~650 lines). The new SDK provides `task.uiControls` as a pre-computed `TaskUIControls` object driven by the state machine, making the widget-side computation redundant. - ---- - -## Old Approach - -### Entry Point -**File:** `packages/contact-center/task/src/Utils/task-util.ts` -**Function:** `getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled, logger)` - -### How It Works (Old) -1. Widget calls `getControlsVisibility()` on every render/task update -2. Function inspects raw `task.data.interaction` to derive: - - Media type (telephony, chat, email) - - Device type (browser, agentDN, extension) - - Consult status (via `getConsultStatus()` from store utils) - - Hold status (via `findHoldStatus()` from store utils) - - Conference status (via `task.data.isConferenceInProgress`) - - Participant counts (via `getConferenceParticipantsCount()`) -3. Each control has a dedicated function that returns `{ isVisible, isEnabled }` -4. Result includes both control visibility AND state flags (e.g., `isHeld`, `consultCallHeld`) - -### Old Control Names (22 controls + 7 state flags) -| Old Control Name | Type | -|------------------|------| -| `accept` | `Visibility` | -| `decline` | `Visibility` | -| `end` | `Visibility` | -| `muteUnmute` | `Visibility` | -| `holdResume` | `Visibility` | -| `pauseResumeRecording` | `Visibility` | -| `recordingIndicator` | `Visibility` | -| `transfer` | `Visibility` | -| `conference` | `Visibility` | -| `exitConference` | `Visibility` | -| `mergeConference` | `Visibility` | -| `consult` | `Visibility` | -| `endConsult` | `Visibility` | -| `consultTransfer` | `Visibility` | -| `consultTransferConsult` | `Visibility` | -| `mergeConferenceConsult` | `Visibility` | -| `muteUnmuteConsult` | `Visibility` | -| `switchToMainCall` | `Visibility` | -| `switchToConsult` | `Visibility` | -| `wrapup` | `Visibility` | -| **State flags** | | -| `isConferenceInProgress` | `boolean` | -| `isConsultInitiated` | `boolean` | -| `isConsultInitiatedAndAccepted` | `boolean` | -| `isConsultReceived` | `boolean` | -| `isConsultInitiatedOrAccepted` | `boolean` | -| `isHeld` | `boolean` | -| `consultCallHeld` | `boolean` | - ---- - -## New Approach - -### Entry Point -**SDK Property:** `task.uiControls` (getter on `ITask`) -**SDK Event:** `task:ui-controls-updated` (emitted when controls change) -**SDK File:** `packages/@webex/contact-center/src/services/task/state-machine/uiControlsComputer.ts` - -### How It Works (New) -1. SDK state machine transitions on every event (hold, consult, conference, etc.) -2. After each transition, `computeUIControls(currentState, context)` is called -3. If controls changed (`haveUIControlsChanged()`), emits `task:ui-controls-updated` -4. Widget reads `task.uiControls` — no computation needed on widget side - -### New Control Names (17 controls, no state flags) -| New Control Name | Type | -|------------------|------| -| `accept` | `{ isVisible, isEnabled }` | -| `decline` | `{ isVisible, isEnabled }` | -| `hold` | `{ isVisible, isEnabled }` | -| `mute` | `{ isVisible, isEnabled }` | -| `end` | `{ isVisible, isEnabled }` | -| `transfer` | `{ isVisible, isEnabled }` | -| `consult` | `{ isVisible, isEnabled }` | -| `consultTransfer` | `{ isVisible, isEnabled }` | -| `endConsult` | `{ isVisible, isEnabled }` | -| `recording` | `{ isVisible, isEnabled }` | -| `conference` | `{ isVisible, isEnabled }` | -| `wrapup` | `{ isVisible, isEnabled }` | -| `exitConference` | `{ isVisible, isEnabled }` | -| `transferConference` | `{ isVisible, isEnabled }` | -| `mergeToConference` | `{ isVisible, isEnabled }` | -| `switchToMainCall` | `{ isVisible, isEnabled }` | -| `switchToConsult` | `{ isVisible, isEnabled }` | - ---- - -## Old → New Control Name Mapping - -| Old Widget Control | New SDK Control | Notes | -|--------------------|-----------------|-------| -| `accept` | `accept` | Same | -| `decline` | `decline` | Same | -| `end` | `end` | Same | -| `muteUnmute` | `mute` | **Renamed** | -| `holdResume` | `hold` | **Renamed** (hold state still togglable) | -| `pauseResumeRecording` | `recording` | **Renamed** — toggle button (pause/resume) | -| `recordingIndicator` | `recording` | **Maps to same SDK control** — but widget must keep a separate UI indicator (status badge). Use `recording.isVisible` for the indicator badge and `recording.isEnabled` for the toggle button's interactive state. See note below. | -| `transfer` | `transfer` | Same | -| `conference` | `conference` | Same | -| `exitConference` | `exitConference` | Same | -| `mergeConference` | `mergeToConference` | **Renamed** | -| `consult` | `consult` | Same | -| `endConsult` | `endConsult` | Same | -| `consultTransfer` | `consultTransfer` | Same (always hidden in new SDK) | -| `consultTransferConsult` | `transfer` / `transferConference` | **Split** — `transfer` for consult transfer, `transferConference` for conference transfer | -| `mergeConferenceConsult` | `mergeToConference` | **Merged** into `mergeToConference` | -| `muteUnmuteConsult` | `mute` | **Merged** into `mute` | -| `switchToMainCall` | `switchToMainCall` | Same | -| `switchToConsult` | `switchToConsult` | Same | -| `wrapup` | `wrapup` | Same | - -### Removed State Flags -The following state flags were returned by `getControlsVisibility()` but are no longer needed: - -| Old State Flag | Replacement | -|----------------|-------------| -| `isConferenceInProgress` | **Caution with `exitConference.isVisible`** — In the old widget code, exit-conference was hidden during conference + active consult. In the **new SDK** (`uiControlsComputer.ts`), `exitConference` is `VISIBLE_DISABLED` (not hidden) during consulting-from-conference, making `isVisible` more reliable. However, for consulted agents not in conferencing state, `exitConference` is `DISABLED`. If you need a definitive conference-in-progress flag independent of agent role, use `task.data` (e.g., `getIsConferenceInProgress(taskData)` which the SDK itself uses internally) rather than relying solely on `exitConference.isVisible` | -| `isConsultInitiated` | **Do NOT derive from `endConsult.isVisible`** — SDK shows `endConsult` for all consulting states (`CONSULT_INITIATING`, `CONSULTING`, `CONF_INITIATING`), not just initiated. If initiated-only semantics are needed (e.g., consult timer labeling in `calculateConsultTimerData`), use SDK `TaskState` directly: `state === TaskState.CONSULT_INITIATING`. **Note:** This requires `TaskState` to be exported from SDK — tracked in the [SDK missing items Confluence page](./confluence-sdk-missing-items.md) and 009 pending exports. | -| `isConsultInitiatedAndAccepted` | No longer needed — SDK handles via controls | -| `isConsultReceived` | No longer needed — SDK handles via controls | -| `isConsultInitiatedOrAccepted` | No longer needed — SDK handles via controls | -| `isHeld` | **Do NOT derive from `controls.hold`** — hold control is `VISIBLE_DISABLED` in consult/conference states even when call is not held. Derive from task data using `findHoldStatus(task, 'mainCall', agentId)` which checks `interaction.media[mediaId].isHold` directly | -| `consultCallHeld` | No longer needed — SDK handles switch controls | - ---- - -## Refactor Pattern (Before/After) - -### Before (in `useCallControl` hook) -```typescript -// helper.ts — old approach -const controls = getControlsVisibility( - store.deviceType, - store.featureFlags, - store.currentTask, - store.agentId, - conferenceEnabled, - store.logger -); - -// Pass 22 controls + 7 state flags to component -return { - ...controls, - // additional hook state -}; -``` - -### After (in `useCallControl` hook) -```typescript -// helper.ts — new approach -// These imports require pending SDK entry point additions (tracked via Jira). -// See 001-migration-overview.md § SDK Package Entry Point — Pending Additions. -import {TaskUIControls, TASK_EVENTS, getDefaultUIControls} from '@webex/contact-center'; - -const task = store.currentTask; -const uiControls: TaskUIControls = task?.uiControls ?? getDefaultUIControls(); - -useEffect(() => { - if (!task) return; - const handler = () => { - // MobX or setState to trigger re-render - }; - task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, handler); - return () => task.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, handler); -}, [task]); - -return { - controls: uiControls, - // additional hook state (timers, mute state, etc.) -}; -``` - ---- - -## Files to Modify - -| File | Action | -|------|--------| -| `task/src/Utils/task-util.ts` | **DELETE** or reduce to `findHoldTimestamp()` only | -| `task/src/helper.ts` (`useCallControl`) | Remove `getControlsVisibility()` call, use `task.uiControls`. Note: voice tasks use `holdResume()` toggle internally (see 001 § SDK Export Gaps), but `task.hold()`/`task.resume()` delegate correctly, so existing widget calls are safe. | -| `task/src/task.types.ts` | Import `TaskUIControls` from SDK, remove old control types | -| `cc-components/src/components/task/task.types.ts` | Align `ControlProps` with new control names | -| `cc-components/src/components/task/CallControl/call-control.tsx` | Update prop names (`holdResume` → `hold`, etc.) | -| `cc-components/src/components/task/CallControl/call-control.utils.ts` | Simplify/remove old control derivation | -| `store/src/task-utils.ts` | Remove `getConsultStatus` if no longer consumed. **KEEP `findHoldStatus`** — still needed for `isHeld` derivation (see Removed State Flags table above) | -| All test files for above | Update to test new contract | - ---- - -## Validation Criteria - -- [ ] All 17 SDK controls map correctly to widget UI buttons -- [ ] No widget-side computation of control visibility remains -- [ ] `task:ui-controls-updated` event drives re-renders -- [ ] All existing call control scenarios work identically (hold, consult, transfer, conference, wrapup) -- [ ] Digital channel controls work (accept, end, transfer, wrapup only) -- [ ] Default controls shown when no task is active -- [ ] Error boundary still catches failures gracefully - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ -_Updated: 2026-03-11 (SDK export gap notes, import examples, holdResume clarification)_ diff --git a/packages/contact-center/ai-docs/migration/009-types-and-constants-migration.md b/packages/contact-center/ai-docs/migration/009-types-and-constants-migration.md deleted file mode 100644 index 9b28df7d0..000000000 --- a/packages/contact-center/ai-docs/migration/009-types-and-constants-migration.md +++ /dev/null @@ -1,325 +0,0 @@ -# Migration Doc 009: Types and Constants Alignment - -## Summary - -CC Widgets defines its own types for control visibility, task state, and constants. These must be aligned with the new SDK types (`TaskUIControls`, `TaskState`, etc.) to ensure type safety and avoid duplication. - ---- - -## Type Mapping: Old → New - -### Control Visibility Type - -| Old (CC Widgets) | New (CC SDK) | -|------------------|--------------| -| `Visibility` from `@webex/cc-components` = `{ isVisible: boolean; isEnabled: boolean }` | `TaskUIControlState` = `{ isVisible: boolean; isEnabled: boolean }` | - -**Same shape, different name.** Can either: -- (A) Import `TaskUIControlState` from SDK and alias -- (B) Keep `Visibility` as-is since shape is identical -- **Recommendation:** Keep `Visibility` in `cc-components` for UI-layer independence; accept `TaskUIControls` from SDK in hooks. - -### Controls Object Type - -| Old (CC Widgets) | New (CC SDK) | -|------------------|--------------| -| No unified type — `getControlsVisibility()` returns ad-hoc object with 22 controls + 7 flags | `TaskUIControls` = `{ [17 control names]: { isVisible, isEnabled } }` | - -**Action:** Import and use `TaskUIControls` from SDK. Map to component props. - -### Task State Constants - -| Old (CC Widgets Store) | New (CC SDK) | Notes | -|------------------------|--------------|-------| -| `TASK_STATE_CONSULT` | `TaskState.CONSULT_INITIATING` | **Not a 1:1 map** — old constant represents consult requested, not yet accepted. SDK `CONSULT_INITIATING` is the intermediate async state. SDK also has `TaskState.CONSULT_INITIATED` but it is **"NOT IMPLEMENTED"**. Do NOT collapse with `TASK_STATE_CONSULTING` | -| `TASK_STATE_CONSULTING` | `TaskState.CONSULTING` | Consult accepted, actively consulting | -| `TASK_STATE_CONSULT_COMPLETED` | `TaskState.CONNECTED` | Consult ended, back to connected state | -| `INTERACTION_STATE_WRAPUP` | `TaskState.WRAPPING_UP` | | -| `INTERACTION_STATE_POST_CALL` | `TaskState.POST_CALL` | SDK marks this **"NOT IMPLEMENTED"** | -| `INTERACTION_STATE_CONNECTED` | `TaskState.CONNECTED` | | -| `INTERACTION_STATE_CONFERENCE` | `TaskState.CONFERENCING` | | -| *(no old equivalent)* | `TaskState.IDLE` | New — task created but not yet offered | -| *(no old equivalent)* | `TaskState.OFFERED` | New — task offered to agent | -| *(no old equivalent)* | `TaskState.HOLD_INITIATING` | New — intermediate async state for hold request | -| *(no old equivalent)* | `TaskState.HELD` | New — task is on hold | -| *(no old equivalent)* | `TaskState.RESUME_INITIATING` | New — intermediate async state for resume request | -| *(no old equivalent)* | `TaskState.CONF_INITIATING` | New — intermediate async state for conference merge | -| *(no old equivalent)* | `TaskState.COMPLETED` | New — task completed | -| *(no old equivalent)* | `TaskState.TERMINATED` | New — task terminated | -| *(no old equivalent)* | `TaskState.CONSULT_COMPLETED` | **"NOT IMPLEMENTED"** in SDK | -| *(no old equivalent)* | `TaskState.PARKED` | **"NOT IMPLEMENTED"** in SDK | -| *(no old equivalent)* | `TaskState.MONITORING` | **"NOT IMPLEMENTED"** in SDK | - -**Full `TaskState` enum (SDK):** `IDLE`, `OFFERED`, `CONNECTED`, `HOLD_INITIATING`, `HELD`, `RESUME_INITIATING`, `CONSULT_INITIATING`, `CONSULTING`, `CONF_INITIATING`, `CONFERENCING`, `WRAPPING_UP`, `COMPLETED`, `TERMINATED`, `CONSULT_INITIATED` (not impl), `CONSULT_COMPLETED` (not impl), `POST_CALL` (not impl), `PARKED` (not impl), `MONITORING` (not impl) - -### Consult Status Constants - -| Old (CC Widgets Store) | New (CC SDK Context) | -|------------------------|---------------------| -| `CONSULT_STATE_INITIATED` | `context.consultInitiator` = true | -| `CONSULT_STATE_COMPLETED` | SDK transitions back to CONNECTED/CONFERENCING | -| `CONSULT_STATE_CONFERENCING` | `TaskState.CONFERENCING` | -| `ConsultStatus.CONSULT_INITIATED` | `TaskState.CONSULT_INITIATING` | -| `ConsultStatus.CONSULT_ACCEPTED` | `context.consultDestinationAgentJoined` = true | -| `ConsultStatus.BEING_CONSULTED` | `context.isConsultedAgent` (derived by SDK) | -| `ConsultStatus.BEING_CONSULTED_ACCEPTED` | `context.isConsultedAgent` + CONSULTING state | -| `ConsultStatus.CONSULT_COMPLETED` | SDK clears consult context on CONSULT_END | - -### Event Constants - -| Old (CC Widgets) | Old Value | New (CC SDK) | New Value | Change | -|------------------|-----------|--------------|-----------|--------| -| `TASK_EVENTS.TASK_INCOMING` | `'task:incoming'` | `TASK_EVENTS.TASK_INCOMING` | `'task:incoming'` | Same | -| `TASK_EVENTS.TASK_ASSIGNED` | `'task:assigned'` | `TASK_EVENTS.TASK_ASSIGNED` | `'task:assigned'` | Same | -| `TASK_EVENTS.TASK_HOLD` | `'task:hold'` | `TASK_EVENTS.TASK_HOLD` | `'task:hold'` | Same | -| `TASK_EVENTS.TASK_RESUME` | `'task:resume'` | `TASK_EVENTS.TASK_RESUME` | `'task:resume'` | Same | -| `TASK_EVENTS.TASK_END` | `'task:end'` | `TASK_EVENTS.TASK_END` | `'task:end'` | Same | -| `TASK_EVENTS.TASK_WRAPUP` | `'task:wrapup'` | `TASK_EVENTS.TASK_WRAPUP` | `'task:wrapup'` | Same | -| `TASK_EVENTS.AGENT_WRAPPEDUP` | `'AgentWrappedUp'` | `TASK_EVENTS.TASK_WRAPPEDUP` | `'task:wrappedup'` | **Renamed + value changed** — widget uses CC-level event name, SDK uses task-level | -| `TASK_EVENTS.AGENT_CONSULT_CREATED` | `'AgentConsultCreated'` | `TASK_EVENTS.TASK_CONSULT_CREATED` | `'task:consultCreated'` | **Renamed + value changed** | -| `TASK_EVENTS.AGENT_OFFER_CONTACT` | `'AgentOfferContact'` | `TASK_EVENTS.TASK_OFFER_CONTACT` | `'task:offerContact'` | **Renamed + value changed** | -| `TASK_EVENTS.CONTACT_RECORDING_PAUSED` | `'ContactRecordingPaused'` | `TASK_EVENTS.TASK_RECORDING_PAUSED` | `'task:recordingPaused'` | **Renamed + value changed** | -| `TASK_EVENTS.CONTACT_RECORDING_RESUMED` | `'ContactRecordingResumed'` | `TASK_EVENTS.TASK_RECORDING_RESUMED` | `'task:recordingResumed'` | **Renamed + value changed** | -| — | — | `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | `'task:ui-controls-updated'` | **NEW** | -| — | — | `TASK_EVENTS.TASK_UNASSIGNED` | `'task:unassigned'` | **NEW** | -| — | — | `TASK_EVENTS.TASK_CLEANUP` | `'task:cleanup'` | **NEW** — state machine terminal state cleanup | -| — | — | `TASK_EVENTS.TASK_RECORDING_STARTED` | `'task:recordingStarted'` | **NEW** | -| — | — | `TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED` | `'task:recordingPauseFailed'` | **NEW** | -| — | — | `TASK_EVENTS.TASK_RECORDING_RESUME_FAILED` | `'task:recordingResumeFailed'` | **NEW** | -| — | — | `TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED` | `'task:consultQueueFailed'` | **NEW** | -| — | — | `TASK_EVENTS.TASK_EXIT_CONFERENCE` | `'task:exitConference'` | **NEW** | -| — | — | `TASK_EVENTS.TASK_TRANSFER_CONFERENCE` | `'task:transferConference'` | **NEW** | -| `TASK_EVENTS.TASK_CONSULT_END` | Same | `TASK_EVENTS.TASK_CONSULT_END` | Same | Same | -| `TASK_EVENTS.TASK_CONSULT_ACCEPTED` | Same | `TASK_EVENTS.TASK_CONSULT_ACCEPTED` | Same | Same | -| `TASK_EVENTS.TASK_CONSULTING` | Same | `TASK_EVENTS.TASK_CONSULTING` | Same | Same | -| `TASK_EVENTS.TASK_OFFER_CONSULT` | Same | `TASK_EVENTS.TASK_OFFER_CONSULT` | Same | Same | -| All conference events | Same | Same | Same | Same | - -> **Critical:** Five widget event names (`AGENT_WRAPPEDUP`, `AGENT_CONSULT_CREATED`, `AGENT_OFFER_CONTACT`, `CONTACT_RECORDING_PAUSED`, `CONTACT_RECORDING_RESUMED`) use CC-level naming convention (`'AgentWrappedUp'`, etc.) but the SDK uses task-level naming (`'task:wrappedup'`, etc.). Widget `store.types.ts` re-declares these and must be updated to match SDK values. - -**Widget-only events (no SDK equivalent — must verify or remove):** - -| Widget Event | Widget Value | SDK Status | -|-------------|-------------|------------| -| `TASK_UNHOLD` | `'task:unhold'` | SDK uses `TASK_RESUME` (`'task:resume'`) instead — no separate unhold event | -| `TASK_CONSULT` | `'task:consult'` | Not in SDK `TASK_EVENTS` — SDK uses `TASK_CONSULT_CREATED` / `TASK_CONSULTING` | -| `TASK_PAUSE` | `'task:pause'` | Not in SDK `TASK_EVENTS` — SDK uses recording events instead | -| `AGENT_CONTACT_ASSIGNED` | `'AgentContactAssigned'` | CC-level event — may still be needed for `cc.on()` subscriptions | - -**Note:** Widget `store.types.ts` line 210 has `TODO: remove this once cc sdk exports this enum`. The SDK now exports `TASK_EVENTS` from `@webex/contact-center`. During migration, **delete the local `TASK_EVENTS` enum** and import from SDK directly. - -### Media Type / Channel Type Constants - -| Old (CC Widgets) | New (CC SDK) | SDK Source | -|------------------|--------------|------------| -| `MEDIA_TYPE_TELEPHONY` = `'telephony'` | `TASK_CHANNEL_TYPE.VOICE` = `'voice'` | `services/task/types.ts` | -| `MEDIA_TYPE_CHAT` = `'chat'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | `services/task/types.ts` | -| `MEDIA_TYPE_EMAIL` = `'email'` | `TASK_CHANNEL_TYPE.DIGITAL` = `'digital'` | `services/task/types.ts` | - -**Note:** SDK uses `TASK_CHANNEL_TYPE` (`VOICE`/`DIGITAL`) for UI control computation. Widget media types may still be needed for display purposes. - -**Type:** `TaskChannelType = 'voice' | 'digital'` (derived from `typeof TASK_CHANNEL_TYPE`) - -### Voice Variant Constants (NEW) - -| Constant | Value | Purpose | -|----------|-------|---------| -| `VOICE_VARIANT.PSTN` | `'pstn'` | PSTN telephony — no `decline`/`toggleMute` in UI controls | -| `VOICE_VARIANT.WEBRTC` | `'webrtc'` | WebRTC browser — `decline` and `toggleMute` available | - -**Type:** `VoiceVariant = 'pstn' | 'webrtc'` (derived from `typeof VOICE_VARIANT`) - -**Widget impact:** Widgets do NOT set voice variant directly — the SDK resolves it internally when creating the task. But widgets should understand this affects which controls appear (e.g., PSTN tasks won't show decline button). - ---- - -## New Types to Import from SDK - -| Type | Source | Purpose | Package Entry Point Status | -|------|--------|---------|---------------------------| -| `TaskUIControls` | `@webex/contact-center` | Pre-computed control states (17 controls) | Exported from source file; pending addition to `src/index.ts` (Jira tracked) | -| `TaskUIControlState` | `@webex/contact-center` | Single control `{ isVisible, isEnabled }` | Local type in source file; pending export + addition to `src/index.ts` | -| `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` | `@webex/contact-center` | New event | Available — part of `TASK_EVENTS` enum already exported | -| `TASK_CHANNEL_TYPE` | `@webex/contact-center` | `{ VOICE, DIGITAL }` constant | Exported from source file; pending addition to `src/index.ts` | -| `VoiceVariant` | `@webex/contact-center` | `'pstn' \| 'webrtc'` | Exported from source file; pending addition to `src/index.ts` | -| `Participant` | `@webex/contact-center` | `{ id, name?, pType? }` for conference UI | Exported from source file; pending addition to `src/index.ts` | -| `getDefaultUIControls` | `@webex/contact-center` | Default controls fallback (all disabled) | Exported from source file; pending addition to `src/index.ts` | -| `TaskState` | `@webex/contact-center` | Enum for explicit task states — needed for consult timer labeling (`CONSULT_INITIATING` vs `CONSULTING`) | Exported from state-machine module; pending addition to `src/index.ts` | - -> **See [001-migration-overview.md § SDK Package Entry Point — Pending Additions](./001-migration-overview.md)** for the full list and exact `src/index.ts` changes needed. A Jira ticket is being created to track these SDK-side additions. - -**Note:** `TaskState` and `TaskEvent` enums are exported from the state-machine internal module but NOT from the package-level `index.ts`. Widgets should use `task.uiControls` for control state. However, widgets **do need `TaskState`** for consult timer labeling (`calculateConsultTimerData` needs to distinguish `CONSULT_INITIATING` from `CONSULTING`). `TaskState` must be added to SDK package exports — tracked in the [SDK missing items Confluence page](./confluence-sdk-missing-items.md). - -### SDK Task Subtype Interfaces - -The SDK defines three task subtype interfaces. Widgets currently use `ITask` but may need these for type narrowing: - -| Interface | Extends | Additional Members | Package Entry Point Status | -|-----------|---------|-------------------|---------------------------| -| `ITask` | `EventEmitter` | `data`, `webCallMap`, `autoWrapup`, `accept()`, `decline()`, `hold()`, `resume()`, `end()`, `wrapup()`, `pauseRecording()`, `resumeRecording()`, `consult()`, `endConsult()`, `transfer()`, `consultConference()`, `exitConference()`, `transferConference()`, `toggleMute()`, `consultTransfer()`, `cancelAutoWrapupTimer()` | Available in `src/index.ts` | -| `IVoice` | `ITask` | `holdResume()` — single hold/resume toggle for voice | Defined in source; pending addition to `src/index.ts` | -| `IDigital` | `Omit` | `uiControls: TaskUIControls`, `updateTaskData()` returns `IDigital` | Defined in source; pending addition to `src/index.ts` | -| `IWebRTC` | `IVoice` | `toggleMute()`, `decline()`, `unregisterWebCallListeners()` | Defined in source; pending addition to `src/index.ts` | - -**Important:** `uiControls` is currently only declared on `IDigital`, not on `ITask`. The concrete `Task` class has a `public get uiControls()` getter inherited by all subclasses (Voice, Digital, WebRTC). Adding `uiControls` to `ITask` is tracked in the Jira ticket — see 001 for details. - ---- - ---- - -## Before/After: Type Imports - -### Before (task.types.ts) -```typescript -// task/src/task.types.ts — old approach -import {ITask, Interaction} from '@webex/contact-center'; -import {Visibility, ControlProps} from '@webex/cc-components'; - -export interface useCallControlProps { - currentTask: ITask; - deviceType: string; // Used for control visibility computation - featureFlags: {[key: string]: boolean}; // Used for control visibility - agentId: string; // Used for control visibility AND timer participant lookup - conferenceEnabled: boolean; // Used for control visibility - isMuted: boolean; - logger: ILogger; - onHoldResume?: (data: any) => void; - onEnd?: (data: any) => void; - onWrapUp?: (data: any) => void; - onRecordingToggle?: (data: any) => void; - onToggleMute?: (data: any) => void; -} -// Return type is ad-hoc — includes 22 controls + 7 flags + hook state + actions -``` - -### After (task.types.ts) -```typescript -// task/src/task.types.ts — new approach -import {ITask, TaskUIControls} from '@webex/contact-center'; - -export interface useCallControlProps { - currentTask: ITask; - // REMOVED: deviceType, featureFlags, conferenceEnabled - // (SDK computes controls via UIControlConfig, set at task creation) - agentId: string; // RETAINED — still needed by timer utils for participant lookup - isMuted: boolean; - logger: ILogger; - onHoldResume?: (data: any) => void; - onEnd?: (data: any) => void; - onWrapUp?: (data: any) => void; - onRecordingToggle?: (data: any) => void; - onToggleMute?: (data: any) => void; -} - -export interface CallControlHookResult { - controls: TaskUIControls; // NEW: all 17 controls from SDK - currentTask: ITask; - isMuted: boolean; - isRecording: boolean; - holdTime: number; - startTimestamp: number; - stateTimerLabel: string | null; - stateTimerTimestamp: number; - consultTimerLabel: string; - consultTimerTimestamp: number; - secondsUntilAutoWrapup: number | null; - buddyAgents: BuddyDetails[]; - loadingBuddyAgents: boolean; - consultAgentName: string; - conferenceParticipants: Participant[]; - lastTargetType: TargetType; - // Actions - toggleHold: (hold: boolean) => void; - toggleMute: () => Promise; - toggleRecording: () => void; - endCall: () => void; - wrapupCall: (reason: string, auxCodeId: string) => void; - transferCall: (to: string, type: DestinationType) => Promise; - consultCall: (dest: string, type: DestinationType, interact: boolean) => Promise; - endConsultCall: () => Promise; - consultTransfer: () => Promise; - consultConference: () => Promise; - switchToMainCall: () => Promise; - switchToConsult: () => Promise; - exitConference: () => Promise; - cancelAutoWrapup: () => void; - loadBuddyAgents: () => Promise; - getAddressBookEntries: (params: PaginatedListParams) => Promise; - getEntryPoints: (params: PaginatedListParams) => Promise; - getQueuesFetcher: (params: PaginatedListParams) => Promise; - setLastTargetType: (type: TargetType) => void; - setConsultAgentName: (name: string) => void; -} -``` - -### Before/After: Constants - -#### Before (store/constants.ts) -```typescript -// Used throughout for consult status derivation -export const TASK_STATE_CONSULT = 'consult'; -export const TASK_STATE_CONSULTING = 'consulting'; -export const TASK_STATE_CONSULT_COMPLETED = 'consultCompleted'; -export const INTERACTION_STATE_WRAPUP = 'wrapup'; -export const POST_CALL = 'postCall'; -export const CONNECTED = 'connected'; -export const CONFERENCE = 'conference'; -export const CONSULT_STATE_INITIATED = 'initiated'; -export const CONSULT_STATE_COMPLETED = 'completed'; -export const CONSULT_STATE_CONFERENCING = 'conferencing'; -export const RELATIONSHIP_TYPE_CONSULT = 'consult'; -export const MEDIA_TYPE_CONSULT = 'consult'; -``` - -#### After -```typescript -// REMOVE these (no longer needed for control computation): -// TASK_STATE_CONSULT, TASK_STATE_CONSULTING, TASK_STATE_CONSULT_COMPLETED -// INTERACTION_STATE_WRAPUP, INTERACTION_STATE_POST_CALL, INTERACTION_STATE_CONNECTED, INTERACTION_STATE_CONFERENCE -// CONSULT_STATE_INITIATED, CONSULT_STATE_COMPLETED, CONSULT_STATE_CONFERENCING - -// KEEP these (still used for display or action logic): -export const RELATIONSHIP_TYPE_CONSULT = 'consult'; -export const MEDIA_TYPE_CONSULT = 'consult'; // Used by findMediaResourceId -``` - ---- - -## UIControlConfig Note - -**Important:** Widgets do NOT need to provide `UIControlConfig`. The SDK builds it internally: -- `channelType` — resolved from `task.data.interaction.mediaType` (telephony → voice, else digital) -- `voiceVariant` — set by Voice/WebRTC layer (PSTN vs WebRTC) -- `isEndTaskEnabled` / `isEndConsultEnabled` — from agent profile config flags -- `isRecordingEnabled` — from `callProcessingDetails.pauseResumeEnabled` -- `agentId` — from `taskManager.setAgentId()` - -This means the widget no longer needs to pass `deviceType`, `featureFlags`, or `conferenceEnabled` for control computation. **Note:** `agentId` is retained — it is still needed by timer utilities for participant lookup. - ---- - -## Files to Modify - -| File | Action | -|------|--------| -| `task/src/task.types.ts` | Import `TaskUIControls` from SDK; update hook return types | -| `cc-components/.../task/task.types.ts` | Add `TaskUIControls` prop type for CallControl | -| `store/src/store.types.ts` | **Delete local `TASK_EVENTS` enum** — import from SDK `@webex/contact-center` instead. Update all 5 CC-level event names to SDK task-level names. Delete local `CC_EVENTS` if SDK exports it. | -| `store/src/constants.ts` | Review/remove consult state constants | -| `task/src/Utils/constants.ts` | Review/remove media type constants used only for controls | -| All files importing from `store.types.ts` | Update imports to use SDK `TASK_EVENTS` | - ---- - -## Validation Criteria - -- [ ] `TaskUIControls` type imported from SDK compiles correctly -- [ ] No type mismatches between SDK controls and component props -- [ ] `TASK_UI_CONTROLS_UPDATED` event constant available -- [ ] Old constants still available where needed (display purposes) -- [ ] No unused type imports remain - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ -_Updated: 2026-03-11 (complete TaskState enum, event name mapping with values, SDK interfaces, VoiceVariant, TASK_CHANNEL_TYPE, SDK export gaps)_ diff --git a/packages/contact-center/ai-docs/migration/014-task-code-scan-report.md b/packages/contact-center/ai-docs/migration/014-task-code-scan-report.md deleted file mode 100644 index da860a944..000000000 --- a/packages/contact-center/ai-docs/migration/014-task-code-scan-report.md +++ /dev/null @@ -1,419 +0,0 @@ -# Migration Doc 014: Task-Related Code Scan Report - -**Generated:** 2025-03-09 -**Scope:** Complete scan of CC Widgets repository for task-related code - ---- - -## 1. `packages/contact-center/task/src/helper.ts` — ALL HOOKS - -### useTaskList (lines 30–145) -- **Store callbacks:** `setTaskAssigned`, `setTaskRejected`, `setTaskSelected` -- **SDK methods:** `task.accept()`, `task.decline()` -- **Store methods:** `store.setCurrentTask(task, true)` -- **Migration:** Callbacks registered in useEffect with empty deps; task methods called directly on ITask - -### useIncomingTask (lines 147–281) -- **setTaskCallback usage:** TASK_ASSIGNED, TASK_CONSULT_ACCEPTED, TASK_END, TASK_REJECT, TASK_CONSULT_END -- **removeTaskCallback:** Same events in cleanup -- **⚠️ Pre-existing bug — TASK_ASSIGNED callback mismatch:** - - `setTaskCallback(TASK_ASSIGNED, ...)` registers an **inline anonymous function** (line ~180) - - `removeTaskCallback(TASK_ASSIGNED, taskAssignCallback, ...)` removes with the **named `taskAssignCallback`** reference (line ~200) - - These are different function references, so the inline listener is **never removed** — potential listener leak / duplicate callback on task reassignment - - **Migration action:** Fix during migration by using the same function reference for both register and cleanup, or consolidate to SDK event subscription model -- **SDK methods:** `incomingTask.accept()`, `incomingTask.decline()` -- **Migration:** Per-task event subscriptions; must migrate to new event model - -### useCallControl (lines 283–728) -- **setTaskCallback usage:** TASK_HOLD, TASK_RESUME, TASK_END, AGENT_WRAPPEDUP, TASK_RECORDING_PAUSED, TASK_RECORDING_RESUMED -- **removeTaskCallback:** TASK_HOLD, TASK_RESUME, TASK_END, AGENT_WRAPPEDUP, CONTACT_RECORDING_PAUSED, CONTACT_RECORDING_RESUMED (note: mismatch — uses CONTACT_* in cleanup but TASK_* in setup) -- **SDK methods on currentTask:** - - `hold()`, `resume()`, `hold(mediaResourceId)`, `resume(mediaResourceId)` - - `end()`, `wrapup({wrapUpReason, auxCodeId})` - - `pauseRecording()`, `resumeRecording({autoResumed})` - - `toggleMute()` - - `transfer({to, destinationType})` - - `consult(consultPayload)` - - `consultConference()`, `exitConference()` - - `endConsult(consultEndPayload)` - - `consultTransfer()`, `transferConference()` - - `cancelAutoWrapupTimer()` -- **Store imports:** `getConferenceParticipants`, `findMediaResourceId` from @webex/cc-store -- **Migration:** Heavy task API usage; all task methods and event subscriptions need migration - -### useOutdialCall (lines 731–813) -- **Store:** `store.taskList`, `store.cc.startOutdial()`, `cc.getOutdialAniEntries()`, `cc.addressBook.getEntries()` -- **Task check:** `Object.values(taskList).some(task => task?.data?.interaction?.mediaType === MEDIA_TYPE_TELEPHONY_LOWER)` -- **Migration:** Minimal task usage; mainly checks taskList for telephony presence - ---- - -## 2. `packages/contact-center/store/src/storeEventsWrapper.ts` — TASK EVENT HANDLERS - -### registerTaskEventListeners (lines 416–451) -Registers on each task: -- TASK_END → handleTaskEnd -- TASK_ASSIGNED → handleTaskAssigned -- AGENT_OFFER_CONTACT → refreshTaskList -- AGENT_CONSULT_CREATED → handleConsultCreated -- TASK_CONSULT_QUEUE_CANCELLED → handleConsultQueueCancelled -- TASK_REJECT → handleTaskReject (with task) -- TASK_OUTDIAL_FAILED → handleOutdialFailed -- AGENT_WRAPPEDUP → refreshTaskList -- TASK_CONSULTING → handleConsulting -- TASK_CONSULT_ACCEPTED → handleConsultAccepted -- TASK_OFFER_CONSULT → handleConsultOffer -- TASK_AUTO_ANSWERED → handleAutoAnswer -- TASK_CONSULT_END → refreshTaskList -- TASK_HOLD, TASK_RESUME → refreshTaskList -- TASK_CONFERENCE_ENDED → handleConferenceEnded -- TASK_CONFERENCE_END_FAILED, TASK_CONFERENCE_ESTABLISHING, TASK_CONFERENCE_FAILED → refreshTaskList -- TASK_PARTICIPANT_JOINED → handleConferenceStarted -- TASK_PARTICIPANT_LEFT → handleConferenceEnded -- TASK_PARTICIPANT_LEFT_FAILED → refreshTaskList -- TASK_CONFERENCE_STARTED → handleConferenceStarted -- TASK_CONFERENCE_TRANSFERRED → **refreshTaskList** (registration) / **handleConferenceEnded** (cleanup) — ⚠️ callback reference mismatch, listener never removed -- TASK_CONFERENCE_TRANSFER_FAILED → refreshTaskList -- TASK_POST_CALL_ACTIVITY → refreshTaskList -- TASK_MEDIA (browser only) → handleTaskMedia - -### handleTaskEnd (lines 344–347) -- setIsDeclineButtonEnabled(false) -- refreshTaskList() - -### handleTaskAssigned (lines 349–359) -- Calls onTaskAssigned if set -- setCurrentTask(task) -- setState({developerName: ENGAGED_LABEL, name: ENGAGED_USERNAME}) - -### handleIncomingTask (lines 453–467) -- Calls registerTaskEventListeners(task) -- If onIncomingTask && !taskList[task.data.interactionId]: onIncomingTask({task}), handleTaskMuteState(task) -- refreshTaskList() - -### handleConsultCreated (lines 368–371) -- refreshTaskList() -- setConsultStartTimeStamp(Date.now()) - -### handleConsulting (lines 373–376) -- refreshTaskList() -- setConsultStartTimeStamp(Date.now()) - -### handleConsultAccepted (lines 393–406) -- refreshTaskList() -- setConsultStartTimeStamp(Date.now()) -- setState(ENGAGED) -- If browser: task.on(TASK_EVENTS.TASK_MEDIA, handleTaskMedia) - -### handleConsultOffer (lines 378–380) -- refreshTaskList() - -### handleConferenceStarted (lines 412–419) -- setIsQueueConsultInProgress(false) -- setCurrentConsultQueueId(null) -- setConsultStartTimeStamp(null) -- refreshTaskList() - -### handleConferenceEnded (lines 421–423) -- refreshTaskList() - -### refreshTaskList (lines 262–281) -- taskList = cc.taskManager.getAllTasks() -- If empty: handleTaskRemove(currentTask), setCurrentTask(null), setState({reset: true}) -- Else if currentTask in list: setCurrentTask(taskList[currentTask.data.interactionId]) -- Else: handleTaskRemove(currentTask), setCurrentTask(taskList[taskListKeys[0]]) - -### setTaskCallback / removeTaskCallback (lines 354–384) -- setTaskCallback: task.on(event, callback) — task from taskList[taskId] -- removeTaskCallback: task.off(event, callback) - -### setupIncomingTaskHandler (lines 603–668) -- CC events: TASK_HYDRATE, TASK_INCOMING, TASK_MERGED -- handleTaskHydrate, handleIncomingTask, handleTaskMerged - -### handleTaskHydrate (lines 494–528) -- registerTaskEventListeners(task) -- refreshTaskList() -- setCurrentTask(task) -- Consult/wrapup state handling - -### handleTaskMerged (lines 488–492) -- registerTaskEventListeners(task) -- refreshTaskList() - ---- - -## 3. `packages/contact-center/store/src/task-utils.ts` — UTILITY FUNCTIONS - -| Function | Purpose | -|----------|---------| -| `isIncomingTask(task, agentId)` | Determines if task is incoming (new/consult/connected/conference, !wrapUpRequired, !hasJoined) | -| `getConsultMPCState(task, agentId)` | Consult state derivation | -| `isSecondaryAgent(task)` | Secondary agent in consult | -| `isSecondaryEpDnAgent(task)` | Secondary EP-DN agent | -| `getTaskStatus(task, agentId)` | Task status string | -| `getConsultStatus(task, agentId)` | ConsultStatus enum | -| `getIsConferenceInProgress(task)` | Conference in progress check | -| `getConferenceParticipants(task, agentId)` | Active conference participants (excl. agent) | -| `getConferenceParticipantsCount(task)` | Participant count | -| `getIsCustomerInCall(task)` | Customer in call check | -| `getIsConsultInProgress(task)` | Consult in progress | -| `isInteractionOnHold(task)` | Any media on hold | -| `setmTypeForEPDN(task, mType)` | mType adjustment for EP-DN | -| `findMediaResourceId(task, mType)` | Media resource ID lookup | -| `findHoldStatus(task, mType, agentId)` | Hold status for media | -| `findHoldTimestamp(task, mType)` | Hold timestamp (task, mType) | - -**Note:** Store `findHoldTimestamp(task, mType)` vs task package `findHoldTimestamp(interaction, mType)` — different signatures. - -### SDK TaskUtils — Overlapping Functions with Signature Differences - -The SDK (`TaskUtils.ts`) provides utility functions used internally by `uiControlsComputer.ts`. Some overlap with widget `task-utils.ts` but have **different signatures** (SDK takes `Interaction`/`TaskData` instead of `ITask`): - -| Widget Function | Widget Signature | SDK Function | SDK Signature | Signature Diff | -|----------------|-----------------|--------------|---------------|----------------| -| `getIsConferenceInProgress` | `(task: ITask): boolean` | `getIsConferenceInProgress` | `(data: TaskData): boolean` | Takes `TaskData` not `ITask` | -| `getIsCustomerInCall` | `(task: ITask): boolean` | `getIsCustomerInCall` | `(interaction: Interaction, interactionId: string): boolean` | Takes `Interaction` + `interactionId` | -| `getConferenceParticipantsCount` | `(task: ITask): number` | `getConferenceParticipantsCount` | `(data: TaskData, agentId: string): number` | Takes `TaskData` + `agentId` | -| `isSecondaryAgent` | `(task: ITask): boolean` | `isSecondaryAgent` | `(interaction: Interaction): boolean` | Takes `Interaction` not `ITask` | -| `isSecondaryEpDnAgent` | `(task: ITask): boolean` | `isSecondaryEpDnAgent` | `(interaction: Interaction): boolean` | Takes `Interaction` not `ITask` | - -SDK-only utilities (no widget equivalent): - -| SDK Function | Signature | Purpose | -|-------------|-----------|---------| -| `isPrimary(task, agentId)` | `(task: ITask, agentId: string): boolean` | Checks if agent is primary owner | -| `isParticipantInMainInteraction(task, agentId)` | `(task: ITask, agentId: string): boolean` | Agent is in main interaction | -| `checkParticipantNotInInteraction(task, agentId)` | `(task: ITask, agentId: string): boolean` | Agent not in any interaction media | -| `getIsConsultInProgressForConferenceControls(...)` | `(data, agentId): boolean` | Consult check for conference | -| `getIsConsultedAgentForControls(...)` | `(data, agentId): boolean` | Is consulted agent | -| `getServerHoldStateForControls(...)` | `(data, agentId): boolean\|undefined` | Server-side hold state | -| `isAutoAnswerEnabled(interaction, agentId)` | `(interaction, agentId): boolean` | Auto-answer check | -| `isWebRTCCall(interaction, voiceVariant)` | `(interaction, voiceVariant?): boolean` | WebRTC detection | -| `isDigitalOutbound(interaction)` | `(interaction): boolean` | Digital outbound check | -| `hasAgentInitiatedOutdial(interaction, agentId)` | `(interaction, agentId): boolean` | Agent initiated outdial | -| `shouldAutoAnswerTask(interaction, agentId)` | `(interaction, agentId): boolean` | Should auto-answer | - -**Migration impact:** Widget `task-utils.ts` functions used for control computation will be deleted (SDK handles this). Functions like `findHoldStatus`, `findHoldTimestamp`, `findMediaResourceId` that serve widget-specific purposes (timers, hold indicator) will be **retained**. SDK utility functions are used internally by `uiControlsComputer.ts` and are NOT exported to consumers. - ---- - -## 4. `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` - -- **Props:** currentTask, toggleHold, toggleRecording, toggleMute, wrapupCall, controlVisibility, secondsUntilAutoWrapup, cancelAutoWrapup, etc. -- **Task usage:** `currentTask.data.interaction`, `currentTask.autoWrapup`, `updateCallStateFromTask(currentTask, setIsRecording)` -- **Pattern:** Presentational; receives all handlers from hook - ---- - -## 5. `packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts` - -- **updateCallStateFromTask(currentTask, setIsRecording):** Reads `currentTask.data.interaction.callProcessingDetails.isPaused` -- **buildCallControlButtons:** Uses controlVisibility, no direct task access -- **filterButtonsForConsultation:** Filters hold/consult when consult initiated + telephony - ---- - -## 6. `packages/contact-center/cc-components/src/components/task/task.types.ts` - -- **TaskProps, ControlProps, CallControlComponentProps:** ITask, currentTask, incomingTask, taskList -- **TaskListItemData, TaskComponentData:** Task-derived display types -- **TASK_EVENTS** not in this file (in store.types.ts) - ---- - -## 7. `packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx` - -- Uses `extractIncomingTaskData(incomingTask, ...)` -- Renders Task with accept/reject callbacks -- No direct SDK/store access - ---- - -## 8. `packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx` - -- Uses `extractTaskListItemData`, `getTasksArray`, `createTaskSelectHandler`, `isCurrentTaskSelected` -- Imports `isIncomingTask` from @webex/cc-store -- Renders Task for each task in taskList - ---- - -## 9. `packages/contact-center/task/src/Utils/timer-utils.ts` - -- **calculateStateTimerData(currentTask, controlVisibility, agentId):** Wrap-up/post-call timer from participant -- **calculateConsultTimerData(currentTask, controlVisibility, agentId):** Consult timer; uses `findHoldTimestamp(currentTask, 'consult')` from @webex/cc-store -- **Imports:** ITask, findHoldTimestamp from @webex/cc-store - ---- - -## 10. `packages/contact-center/task/src/Utils/useHoldTimer.ts` - -- **Imports:** findHoldTimestamp from `./task-util` (task package) -- **Signature:** findHoldTimestamp(interaction, mType) — passes `currentTask.data.interaction` -- **Uses:** Web Worker for hold elapsed time -- **Note:** Task package findHoldTimestamp(interaction, mType) vs store findHoldTimestamp(task, mType) - ---- - -## 11. CallControlCustom/ - -| File | Task Usage | -|------|------------| -| consult-transfer-popover.tsx | No direct task; receives buddyAgents, getQueues, etc. | -| consult-transfer-popover-hooks.ts | Paginated data; no task | -| call-control-consult.tsx | consultTimerLabel, consultTimerTimestamp, controlVisibility — from props | -| call-control-custom.utils.ts | ControlVisibility, ButtonConfig; no task | -| consult-transfer-list-item.tsx | No task | -| consult-transfer-dial-number.tsx | No task | -| consult-transfer-empty-state.tsx | No task | - ---- - -## 12. Task/ - -| File | Task Usage | -|------|------------| -| index.tsx | Props: interactionId, title, state, startTimeStamp, ronaTimeout, acceptTask, declineTask, onTaskSelect | -| task.utils.ts | extractTaskComponentData; no direct task object | - ---- - -## 13. TaskTimer/ - -- **Props:** startTimeStamp, countdown, ronaTimeout -- Web Worker for elapsed/countdown display -- No ITask reference - ---- - -## 14. AutoWrapupTimer/ - -- **Props:** secondsUntilAutoWrapup, allowCancelAutoWrapup, handleCancelWrapup -- getTimerUIState(secondsUntilAutoWrapup) -- No ITask reference - ---- - -## 15. `packages/contact-center/store/src/store.ts` - -- **Observables:** currentTask, taskList -- **init(options, setupEventListeners):** Passes setupIncomingTaskHandler -- No direct task logic - ---- - -## 16. `packages/contact-center/store/src/store.types.ts` - -- **TASK_EVENTS** enum (lines 168–207) -- **CC_EVENTS** enum -- **IStore:** currentTask, taskList -- **ITask** from @webex/contact-center - ---- - -## 17. `packages/contact-center/store/src/constants.ts` - -- Task/consult state constants: TASK_STATE_CONSULT, TASK_STATE_CONSULTING, INTERACTION_STATE_*, CONSULT_STATE_*, etc. -- EXCLUDED_PARTICIPANT_TYPES, MEDIA_TYPE_CONSULT - ---- - -## 18. `packages/contact-center/task/src/Utils/task-util.ts` - -- **findHoldTimestamp(interaction, mType):** Task package version — takes interaction -- **getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled):** Full control visibility -- Imports from @webex/cc-store: getConsultStatus, getIsConsultInProgress, getIsCustomerInCall, getConferenceParticipantsCount, findHoldStatus - ---- - -## 19. `packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx` - -- **Props:** Uses `controlVisibility` from props (same shape as `call-control.tsx`) -- **Old references:** - - `controlVisibility.isConferenceInProgress` — conference participant display - - `controlVisibility.wrapup.isVisible` — conditional rendering - - `controlVisibility.isHeld`, `controlVisibility.isConsultReceived`, `controlVisibility.consultCallHeld` — hold status indicator - - `controlVisibility.recordingIndicator.isVisible` — recording badge display - - `controlVisibility.isConsultInitiatedOrAccepted` — consult panel toggle -- **Migration:** Must update to accept `TaskUIControls` prop and map old control names to new. Same migration pattern as `call-control.tsx` (Doc 010). - ---- - -## 20. `widgets-samples/cc/samples-cc-wc-app/app.js` - -- **Web Component usage:** Creates `widget-cc-incoming-task`, `widget-cc-task-list`, `widget-cc-call-control`, `widget-cc-call-control-cad`, `widget-cc-outdial-call` -- **Callback wiring:** `ccCallControl.onHoldResume`, `ccCallControl.onEnd`, `ccCallControl.onWrapUp`, `ccCallControlCAD.onHoldResume`, etc. -- **Migration:** Callback prop names may change if WC attribute names are updated to match new SDK control names. Review after component migration. - ---- - -## 21. `widgets-samples/cc/samples-cc-react-app/` - -- **App.tsx:** IncomingTask, TaskList, CallControl; onIncomingTaskCB, onAccepted, onRejected, onTaskAccepted, onTaskDeclined, onTaskSelected; store.currentTask, store.setIncomingTaskCb -- **EngageWidget.tsx:** store.currentTask, mediaType, isSupportedTask -- **Task usage:** currentTask, taskList, incomingTasks state, task.data.interactionId - ---- - -## CRITICAL MIGRATION PATTERNS - -### 1. findHoldTimestamp Signature Mismatch -- **Store (task-utils.ts):** `findHoldTimestamp(task: ITask, mType: string)` -- **Task package (task-util.ts):** `findHoldTimestamp(interaction: Interaction, mType: string)` -- **useHoldTimer** uses task package version with `currentTask.data.interaction` -- **timer-utils.ts** uses store version with `currentTask` - -### 2. useCallControl Event Cleanup Mismatch (BUG) -- Setup: TASK_RECORDING_PAUSED, TASK_RECORDING_RESUMED -- Cleanup: CONTACT_RECORDING_PAUSED, CONTACT_RECORDING_RESUMED -- **Bug:** Cleanup uses wrong event names — callbacks are never removed; should use TASK_RECORDING_* in cleanup to match setup - -### 3. SDK Method Calls (require migration) -- task.accept(), task.decline() -- task.hold(), task.resume(), task.hold(id), task.resume(id) -- task.end(), task.wrapup(), task.pauseRecording(), task.resumeRecording() -- task.toggleMute() -- task.transfer(), task.consult(), task.endConsult() -- task.consultConference(), task.exitConference(), task.consultTransfer(), task.transferConference() -- task.cancelAutoWrapupTimer() - -### 4. Event Subscriptions -- Per-task: task.on(TASK_EVENTS.*, callback) -- CC-level: ccSDK.on(TASK_EVENTS.TASK_INCOMING, ...), TASK_HYDRATE, TASK_MERGED - -### 5. Store Task Flow -- refreshTaskList → cc.taskManager.getAllTasks() -- setCurrentTask uses isIncomingTask(task, agentId) to skip incoming -- handleTaskRemove attempts to unregister task listeners, but has **pre-existing listener leak bugs**: - - `TASK_REJECT` and `TASK_OUTDIAL_FAILED`: registered with inline lambdas (`(reason) => this.handleTaskReject(task, reason)`) in `registerTaskEventListeners` but removed with *different* inline lambdas in `handleTaskRemove` — listeners never removed - - `TASK_CONFERENCE_TRANSFERRED`: registered with `this.refreshTaskList` (line 599) but removed with `this.handleConferenceEnded` (line 445) — different function references, listener never removed - - **Fix during migration:** Use stored function references for all event registrations - ---- - -## FILES SUMMARY - -| Location | Task-Related | -|----------|--------------| -| task/src/helper.ts | ✅ All 4 hooks | -| store/storeEventsWrapper.ts | ✅ All handlers | -| store/task-utils.ts | ✅ All utilities | -| store/store.ts | ✅ currentTask, taskList | -| store/store.types.ts | ✅ TASK_EVENTS, ITask | -| store/constants.ts | ✅ Task state constants | -| task/src/Utils/task-util.ts | ✅ findHoldTimestamp, getControlsVisibility | -| task/src/Utils/timer-utils.ts | ✅ Timer computation | -| task/src/Utils/useHoldTimer.ts | ✅ Hold timer | -| cc-components CallControl/* | ✅ Props, utils | -| cc-components CallControlCAD/* | ✅ controlVisibility, isHeld, recordingIndicator | -| cc-components Task/* | ✅ Display | -| cc-components TaskList/* | ✅ Utils, isIncomingTask | -| cc-components IncomingTask/* | ✅ Utils | -| samples-cc-wc-app | ✅ WC callback wiring | -| samples-cc-react-app | ✅ Full usage | - ---- - -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ -_Updated: 2026-03-11 (SDK TaskUtils comparison table, signature differences, SDK-only utilities)_ -_Updated: 2026-03-11 (added CallControlCAD, WC sample app — per reviewer feedback)_ diff --git a/packages/contact-center/ai-docs/migration/migration-overview.md b/packages/contact-center/ai-docs/migration/migration-overview.md new file mode 100644 index 000000000..fcf1153ad --- /dev/null +++ b/packages/contact-center/ai-docs/migration/migration-overview.md @@ -0,0 +1,131 @@ +# Task Refactor Migration Overview + +## Purpose + +Guide for migrating CC Widgets from ad-hoc task state management to the new SDK state-machine-driven architecture (`task-refactor` branch). This is the single entry point — it tells you what changed, which docs to follow in what order, and what to watch out for. + +--- + +## Architectural Change: Old vs New + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ OLD (Current Widgets) │ NEW (After Migration) │ +│ │ │ +│ SDK emits 30+ task events │ SDK state machine transitions │ +│ │ │ │ │ +│ ▼ │ ▼ │ +│ Store: refreshTaskList() │ SDK: computes TaskUIControls │ +│ + update observables manually │ from (TaskState + TaskContext) │ +│ │ │ │ │ +│ ▼ │ ▼ │ +│ Hooks: getControlsVisibility( │ SDK emits │ +│ deviceType, featureFlags, │ 'task:ui-controls-updated' │ +│ task, agentId, conferenceEnabled) │ │ │ +│ │ │ ▼ │ +│ ▼ │ Widgets read task.uiControls │ +│ Components: flat ControlVisibility │ │ │ +│ (22 controls + 7 state flags) │ ▼ │ +│ │ Components: TaskUIControls │ +│ Logic spread across: │ (17 controls, each │ +│ task-util.ts, task-utils.ts, │ { isVisible, isEnabled }) │ +│ timer-utils.ts, component utils │ │ +│ │ Single source of truth: │ +│ │ task.uiControls │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### `TaskUIControls` Structure + +```typescript +type TaskUIControlState = { isVisible: boolean; isEnabled: boolean }; + +type TaskUIControls = { + accept, decline, hold, transfer, consult, end, recording, mute, + consultTransfer, endConsult, conference, exitConference, + transferConference, mergeToConference, wrapup, + switchToMainCall, switchToConsult + // each: TaskUIControlState +}; +``` + +Widgets no longer compute control visibility — `task.uiControls` is the single source of truth. + +--- + +## Execution Order + +Follow these docs in order. Each doc has old vs new code, before/after examples, and files to modify. + +| Order | Document | What to Do | +|-------|----------|------------| +| 1 | [store-event-wiring-migration.md](./store-event-wiring-migration.md) | Simplify 30+ event handlers — remove `refreshTaskList()`, add `TASK_UI_CONTROLS_UPDATED` subscription | +| 2 | [store-task-utils-migration.md](./store-task-utils-migration.md) | Remove redundant utils (SDK handles), keep display/timer utils | +| 3 | [call-control-hook-migration.md](./call-control-hook-migration.md) | Replace `getControlsVisibility()` with `task.uiControls` in `useCallControl` + update timer utils | +| 4 | [incoming-task-migration.md](./incoming-task-migration.md) | Use `task.uiControls.accept/decline` instead of visibility functions | +| 5 | [task-list-migration.md](./task-list-migration.md) | Per-task `uiControls` for accept/decline | +| 6 | [component-layer-migration.md](./component-layer-migration.md) | Update `cc-components` props — `ControlVisibility` → `TaskUIControls`, rename control props | + +--- + +## SDK Pending Exports (Prerequisites) + +> **Status:** This section will be updated once the SDK team adds these exports. Migration can begin on items that don't depend on these, but full completion requires them. + +These items are exported from SDK source files but not yet from the package entry point (`src/index.ts`): + +| Item | SDK Change Needed | +|------|---| +| `TaskUIControls` type | Add to `src/index.ts` | +| `getDefaultUIControls()` | Add to `src/index.ts` | +| `TaskState` enum | Add to `src/index.ts` (needed for consult timer labeling) | +| `uiControls` on `ITask` | Add getter to `ITask` interface (currently only on concrete `Task` class) | +| `IVoice`, `IDigital`, `IWebRTC` | Add to `src/index.ts` (optional — for type narrowing) | + +--- + +## Key Types from SDK + +| Type | Purpose | +|------|---------| +| `TaskUIControls` | Pre-computed control states (17 controls) | +| `getDefaultUIControls()` | Fallback when no task: `task?.uiControls ?? getDefaultUIControls()` | +| `TASK_EVENTS` | Import from SDK — delete local enum in `store.types.ts` | + +> Constants to delete/keep, event name mappings, and migration gotchas are documented in each migration doc above (PRs 2 and 3). + +--- + +## CC Widgets Files Affected + +| Area | Path | +|------|------| +| Task hooks | `packages/contact-center/task/src/helper.ts` | +| Task UI utils (OLD — to be removed) | `packages/contact-center/task/src/Utils/task-util.ts` | +| Task timer utils | `packages/contact-center/task/src/Utils/timer-utils.ts` | +| Hold timer hook | `packages/contact-center/task/src/Utils/useHoldTimer.ts` | +| Task types | `packages/contact-center/task/src/task.types.ts` | +| Store event wrapper | `packages/contact-center/store/src/storeEventsWrapper.ts` | +| Store task utils | `packages/contact-center/store/src/task-utils.ts` | +| Store constants | `packages/contact-center/store/src/constants.ts` | +| CC Components — CallControl | `packages/contact-center/cc-components/src/components/task/CallControl/` | +| CC Components — CallControlCAD | `packages/contact-center/cc-components/src/components/task/CallControlCAD/` | +| CC Components types | `packages/contact-center/cc-components/src/components/task/task.types.ts` | + +--- + +## CC SDK Reference + +> **Repo:** [webex/webex-js-sdk (task-refactor)](https://github.com/webex/webex-js-sdk/tree/task-refactor) +> **Local path:** `/Users/akulakum/Documents/CC_SDK/webex-js-sdk` (branch: `task-refactor`) + +| File | Purpose | +|------|---------| +| `uiControlsComputer.ts` | Computes `TaskUIControls` from `TaskState` + `TaskContext` — the single source of truth | +| `Task.ts` | Task service exposing `task.uiControls` getter and `task:ui-controls-updated` event | +| `constants.ts` | `TaskState` and `TaskEvent` enums | + +--- + +_Created: 2026-03-09_ +_Updated: 2026-03-12 (consolidated and reordered per reviewer feedback)_ From 86ada3604016c0de91e43e7c15fa3ba805c61db4 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 17 Mar 2026 19:01:55 +0530 Subject: [PATCH 17/18] docs(migration): SDK exports list/timeline, define gotcha --- .../contact-center/ai-docs/migration/migration-overview.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/migration-overview.md b/packages/contact-center/ai-docs/migration/migration-overview.md index fcf1153ad..c195ccce6 100644 --- a/packages/contact-center/ai-docs/migration/migration-overview.md +++ b/packages/contact-center/ai-docs/migration/migration-overview.md @@ -70,7 +70,9 @@ Follow these docs in order. Each doc has old vs new code, before/after examples, ## SDK Pending Exports (Prerequisites) -> **Status:** This section will be updated once the SDK team adds these exports. Migration can begin on items that don't depend on these, but full completion requires them. +**What the SDK does not export today** (from the package entry point `src/index.ts`): the items in the table below. They exist in SDK source but are not re-exported from the public package, so widget code cannot import them until they are added to the package. + +**Before implementing:** Identify whether each required export is available from the SDK — i.e. whether you can import it from the package. If an item is not yet exported, either delay the work that depends on it or implement only the parts that do not need it. Full completion of the migration requires these exports to be available. These items are exported from SDK source files but not yet from the package entry point (`src/index.ts`): @@ -92,7 +94,7 @@ These items are exported from SDK source files but not yet from the package entr | `getDefaultUIControls()` | Fallback when no task: `task?.uiControls ?? getDefaultUIControls()` | | `TASK_EVENTS` | Import from SDK — delete local enum in `store.types.ts` | -> Constants to delete/keep, event name mappings, and migration gotchas are documented in each migration doc above (PRs 2 and 3). +> Constants to delete/keep, event name mappings, and **migration gotchas** (non-obvious pitfalls or ordering constraints — e.g. “do not delete constant X until helper Y is rewritten”) are documented in each of the migration docs listed in the [Execution Order](#execution-order) table above (e.g. [store-event-wiring-migration.md](./store-event-wiring-migration.md), [store-task-utils-migration.md](./store-task-utils-migration.md), [call-control-hook-migration.md](./call-control-hook-migration.md), and the rest). --- From 3c5f27493ce303784f277c4351d4df8e9febeaa5 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 19 Mar 2026 17:41:47 +0530 Subject: [PATCH 18/18] =?UTF-8?q?docs(migration):=20address=20=E2=80=94=20?= =?UTF-8?q?reorder=20sections,=20add=20consultTransfer=20method=20change,?= =?UTF-8?q?=20remove=20local=20SDK=20path,=20align=20with=20PR=20#646=20de?= =?UTF-8?q?cisions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-docs/migration/migration-overview.md | 95 ++++++++++++------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/migration-overview.md b/packages/contact-center/ai-docs/migration/migration-overview.md index c195ccce6..6c85ec5db 100644 --- a/packages/contact-center/ai-docs/migration/migration-overview.md +++ b/packages/contact-center/ai-docs/migration/migration-overview.md @@ -12,7 +12,7 @@ Guide for migrating CC Widgets from ad-hoc task state management to the new SDK ┌─────────────────────────────────────────────────────────────────────────────────┐ │ OLD (Current Widgets) │ NEW (After Migration) │ │ │ │ -│ SDK emits 30+ task events │ SDK state machine transitions │ +│ SDK emits 27 task events │ SDK state machine transitions │ │ │ │ │ │ │ ▼ │ ▼ │ │ Store: refreshTaskList() │ SDK: computes TaskUIControls │ @@ -35,21 +35,27 @@ Guide for migrating CC Widgets from ad-hoc task state management to the new SDK └─────────────────────────────────────────────────────────────────────────────────┘ ``` -### `TaskUIControls` Structure +> The events themselves have not changed — they are the same events, now emitted via the SDK state machine. The key difference is that task state updates (including UI control computation) are handled by the SDK, not by widgets. -```typescript -type TaskUIControlState = { isVisible: boolean; isEnabled: boolean }; +--- -type TaskUIControls = { - accept, decline, hold, transfer, consult, end, recording, mute, - consultTransfer, endConsult, conference, exitConference, - transferConference, mergeToConference, wrapup, - switchToMainCall, switchToConsult - // each: TaskUIControlState -}; -``` +## CC Widgets Files Affected -Widgets no longer compute control visibility — `task.uiControls` is the single source of truth. +| Area | Path | +|------|------| +| Store event wrapper | `packages/contact-center/store/src/storeEventsWrapper.ts` | +| Store task utils | `packages/contact-center/store/src/task-utils.ts` | +| Store constants | `packages/contact-center/store/src/constants.ts` | +| Store types | `packages/contact-center/store/src/store.types.ts` | +| Task hooks | `packages/contact-center/task/src/helper.ts` | +| Task UI utils (to be removed) | `packages/contact-center/task/src/Utils/task-util.ts` | +| Task types | `packages/contact-center/task/src/task.types.ts` | +| CC Components — CallControl | `packages/contact-center/cc-components/src/components/task/CallControl/` | +| CC Components — CallControlCAD | `packages/contact-center/cc-components/src/components/task/CallControlCAD/` | +| CC Components types | `packages/contact-center/cc-components/src/components/task/task.types.ts` | +| CC Components — WC wrapper | `packages/contact-center/cc-components/src/wc.ts` | + +> **Not listed:** `timer-utils.ts` and `useHoldTimer.ts` are not directly affected by the task-refactor SDK changes. Timer signature updates (if any) are tracked separately in the hook migration doc. --- @@ -59,7 +65,7 @@ Follow these docs in order. Each doc has old vs new code, before/after examples, | Order | Document | What to Do | |-------|----------|------------| -| 1 | [store-event-wiring-migration.md](./store-event-wiring-migration.md) | Simplify 30+ event handlers — remove `refreshTaskList()`, add `TASK_UI_CONTROLS_UPDATED` subscription | +| 1 | [store-event-wiring-migration.md](./store-event-wiring-migration.md) | Update 27 event handlers — switch to SDK `TASK_EVENTS` enum, keep `refreshTaskList()`, add `TASK_UI_CONTROLS_UPDATED` subscription, fix `handleConsultEnd` wiring, replace `isDeclineButtonEnabled` with `task.uiControls.decline.isEnabled` | | 2 | [store-task-utils-migration.md](./store-task-utils-migration.md) | Remove redundant utils (SDK handles), keep display/timer utils | | 3 | [call-control-hook-migration.md](./call-control-hook-migration.md) | Replace `getControlsVisibility()` with `task.uiControls` in `useCallControl` + update timer utils | | 4 | [incoming-task-migration.md](./incoming-task-migration.md) | Use `task.uiControls.accept/decline` instead of visibility functions | @@ -72,9 +78,7 @@ Follow these docs in order. Each doc has old vs new code, before/after examples, **What the SDK does not export today** (from the package entry point `src/index.ts`): the items in the table below. They exist in SDK source but are not re-exported from the public package, so widget code cannot import them until they are added to the package. -**Before implementing:** Identify whether each required export is available from the SDK — i.e. whether you can import it from the package. If an item is not yet exported, either delay the work that depends on it or implement only the parts that do not need it. Full completion of the migration requires these exports to be available. - -These items are exported from SDK source files but not yet from the package entry point (`src/index.ts`): +**Before implementing:** Check whether each required export is available from the SDK — i.e. whether you can import it from the package. If an item is not yet exported, delay the work that depends on it or implement only the parts that do not need it. Full completion of the migration requires these exports. | Item | SDK Change Needed | |------|---| @@ -90,36 +94,57 @@ These items are exported from SDK source files but not yet from the package entr | Type | Purpose | |------|---------| -| `TaskUIControls` | Pre-computed control states (17 controls) | +| `TaskUIControls` | Pre-computed control states (17 controls, each `{ isVisible, isEnabled }`) | +| `TaskUIControlState` | Shape: `{ isVisible: boolean; isEnabled: boolean }` | | `getDefaultUIControls()` | Fallback when no task: `task?.uiControls ?? getDefaultUIControls()` | | `TASK_EVENTS` | Import from SDK — delete local enum in `store.types.ts` | +| `TaskState` | SDK state machine states — needed for consult timer labeling | + +### `TaskUIControls` Structure -> Constants to delete/keep, event name mappings, and **migration gotchas** (non-obvious pitfalls or ordering constraints — e.g. “do not delete constant X until helper Y is rewritten”) are documented in each of the migration docs listed in the [Execution Order](#execution-order) table above (e.g. [store-event-wiring-migration.md](./store-event-wiring-migration.md), [store-task-utils-migration.md](./store-task-utils-migration.md), [call-control-hook-migration.md](./call-control-hook-migration.md), and the rest). +```typescript +type TaskUIControlState = { isVisible: boolean; isEnabled: boolean }; + +type TaskUIControls = { + accept: TaskUIControlState; + decline: TaskUIControlState; + hold: TaskUIControlState; + transfer: TaskUIControlState; + consult: TaskUIControlState; + end: TaskUIControlState; + recording: TaskUIControlState; + mute: TaskUIControlState; + consultTransfer: TaskUIControlState; + endConsult: TaskUIControlState; + conference: TaskUIControlState; + exitConference: TaskUIControlState; + transferConference: TaskUIControlState; + mergeToConference: TaskUIControlState; + wrapup: TaskUIControlState; + switchToMainCall: TaskUIControlState; + switchToConsult: TaskUIControlState; +}; +``` + +Widgets no longer compute control visibility — `task.uiControls` is the single source of truth. + +> Specific constants to delete/keep, event name mappings, and ordering constraints (e.g. "do not delete constant X until helper Y is rewritten") are documented in each migration doc listed in the [Execution Order](#execution-order) table. --- -## CC Widgets Files Affected +## SDK Public Method Changes -| Area | Path | -|------|------| -| Task hooks | `packages/contact-center/task/src/helper.ts` | -| Task UI utils (OLD — to be removed) | `packages/contact-center/task/src/Utils/task-util.ts` | -| Task timer utils | `packages/contact-center/task/src/Utils/timer-utils.ts` | -| Hold timer hook | `packages/contact-center/task/src/Utils/useHoldTimer.ts` | -| Task types | `packages/contact-center/task/src/task.types.ts` | -| Store event wrapper | `packages/contact-center/store/src/storeEventsWrapper.ts` | -| Store task utils | `packages/contact-center/store/src/task-utils.ts` | -| Store constants | `packages/contact-center/store/src/constants.ts` | -| CC Components — CallControl | `packages/contact-center/cc-components/src/components/task/CallControl/` | -| CC Components — CallControlCAD | `packages/contact-center/cc-components/src/components/task/CallControlCAD/` | -| CC Components types | `packages/contact-center/cc-components/src/components/task/task.types.ts` | +| Old | New | Notes | +|-----|-----|-------| +| `task.consultTransfer()` | `task.transfer()` | `consultTransfer` is no longer a separate public method; a single `.transfer()` is used for all transfer types | --- ## CC SDK Reference > **Repo:** [webex/webex-js-sdk (task-refactor)](https://github.com/webex/webex-js-sdk/tree/task-refactor) -> **Local path:** `/Users/akulakum/Documents/CC_SDK/webex-js-sdk` (branch: `task-refactor`) + + | File | Purpose | |------|---------| @@ -130,4 +155,4 @@ These items are exported from SDK source files but not yet from the package entr --- _Created: 2026-03-09_ -_Updated: 2026-03-12 (consolidated and reordered per reviewer feedback)_ +_Updated: 2026-03-19 (addressed Kesari3008/mkesavan feedback, aligned with PR #646 decisions)_