From 08e96feca69beff2eed8fbfaac4a8f7ab7b411c4 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 12:50:02 +0530 Subject: [PATCH 01/24] =?UTF-8?q?docs(ai-docs):=20task=20refactor=20migrat?= =?UTF-8?q?ion=20=E2=80=94=20store=20layer=20(PR=202/4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add store layer migration documentation: - 003: Store event wiring migration (30+ event handlers in storeEventsWrapper.ts) - 008: Store task-utils migration (~15 utility functions, findHoldStatus/findHoldTimestamp retention) Made-with: Cursor --- .../003-store-event-wiring-migration.md | 268 ++++++++++++++++++ .../008-store-task-utils-migration.md | 231 +++++++++++++++ 2 files changed, 499 insertions(+) create mode 100644 ai-docs/migration/003-store-event-wiring-migration.md create mode 100644 ai-docs/migration/008-store-task-utils-migration.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..6df3f3a79 --- /dev/null +++ b/ai-docs/migration/003-store-event-wiring-migration.md @@ -0,0 +1,268 @@ +# 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/008-store-task-utils-migration.md b/ai-docs/migration/008-store-task-utils-migration.md new file mode 100644 index 000000000..6e22fdeb4 --- /dev/null +++ b/ai-docs/migration/008-store-task-utils-migration.md @@ -0,0 +1,231 @@ +# 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)_ From c05d0ccba0684f37b16c97b9f4ab7c5ac9c51370 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 20:19:29 +0530 Subject: [PATCH 02/24] docs(ai-docs): move migration docs to packages/contact-center scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move migration docs from ai-docs/migration/ to packages/contact-center/ai-docs/migration/ per reviewer feedback — this migration is specific to contact center. Made-with: Cursor --- .../ai-docs}/migration/003-store-event-wiring-migration.md | 0 .../ai-docs}/migration/008-store-task-utils-migration.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {ai-docs => packages/contact-center/ai-docs}/migration/003-store-event-wiring-migration.md (100%) rename {ai-docs => packages/contact-center/ai-docs}/migration/008-store-task-utils-migration.md (100%) diff --git a/ai-docs/migration/003-store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md similarity index 100% rename from ai-docs/migration/003-store-event-wiring-migration.md rename to packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md diff --git a/ai-docs/migration/008-store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md similarity index 100% rename from ai-docs/migration/008-store-task-utils-migration.md rename to packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md From 87142c5f08bf853f15a982dfe9d85f84350c6bda Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 20:33:16 +0530 Subject: [PATCH 03/24] docs: remove findHoldTimestamp pre-existing issue, fix double separators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove findHoldTimestamp signature mismatch section from 008 — not a task-refactor change. Fix double --- separators in 003 and 008. Made-with: Cursor --- .../migration/003-store-event-wiring-migration.md | 2 -- .../migration/008-store-task-utils-migration.md | 15 --------------- 2 files changed, 17 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md index 6df3f3a79..eace1a714 100644 --- a/packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md @@ -119,8 +119,6 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be --- ---- - ## Refactor Patterns (Before/After) ### Pattern 1: `registerTaskEventListeners()` — Adding UI Controls Handler diff --git a/packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md index 6e22fdeb4..c7e84c255 100644 --- a/packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md @@ -86,8 +86,6 @@ The store's `task-utils.ts` contains ~15 utility functions that inspect raw task --- ---- - ## Before/After: Removing `getConsultStatus` Usage ### Before (consumed in `task-util.ts::getControlsVisibility`) @@ -193,19 +191,6 @@ export function getTaskStatus(task: ITask, agentId: string): string { --- -## `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 | From 81c581c1283d666d39eed6b1b2be94e7195fa161 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 12 Mar 2026 13:17:50 +0530 Subject: [PATCH 04/24] docs(ai-docs): rename store migration docs, add constants/events detail Removed serial numbers from filenames. Added event name rename table, constants to delete/keep, CONSULT_INITIATING gotcha, and fixed findHoldStatus contradiction in store-task-utils doc. Made-with: Cursor --- ...ion.md => store-event-wiring-migration.md} | 26 ++++++++++++-- ...ation.md => store-task-utils-migration.md} | 36 +++++++++++++++---- 2 files changed, 54 insertions(+), 8 deletions(-) rename packages/contact-center/ai-docs/migration/{003-store-event-wiring-migration.md => store-event-wiring-migration.md} (91%) rename packages/contact-center/ai-docs/migration/{008-store-task-utils-migration.md => store-task-utils-migration.md} (87%) diff --git a/packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md similarity index 91% rename from packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md rename to packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index eace1a714..f8d8f1163 100644 --- a/packages/contact-center/ai-docs/migration/003-store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -1,4 +1,4 @@ -# Migration Doc 003: Store Event Wiring Refactor +# Store Event Wiring Migration ## Summary @@ -6,6 +6,28 @@ The store's `storeEventsWrapper.ts` currently registers 30+ individual task even --- +## Event Names — 5 Renamed + +The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-level naming that differs from the SDK's task-level naming. These must be aligned: + +| Old (Widget) | Old Value | New (SDK) | New Value | +|---|---|---|---| +| `AGENT_WRAPPEDUP` | `'AgentWrappedUp'` | `TASK_WRAPPEDUP` | `'task:wrappedup'` | +| `AGENT_CONSULT_CREATED` | `'AgentConsultCreated'` | `TASK_CONSULT_CREATED` | `'task:consultCreated'` | +| `AGENT_OFFER_CONTACT` | `'AgentOfferContact'` | `TASK_OFFER_CONTACT` | `'task:offerContact'` | +| `CONTACT_RECORDING_PAUSED` | `'ContactRecordingPaused'` | `TASK_RECORDING_PAUSED` | `'task:recordingPaused'` | +| `CONTACT_RECORDING_RESUMED` | `'ContactRecordingResumed'` | `TASK_RECORDING_RESUMED` | `'task:recordingResumed'` | + +New event: `TASK_UI_CONTROLS_UPDATED` (`'task:ui-controls-updated'`) — subscribe to this for control updates. + +**Action:** Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from SDK instead. + +### Pre-existing Bug: Event Name Mismatches + +The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. During migration, align these to SDK's exported `TASK_EVENTS` enum. + +--- + ## Old Approach ### Entry Point @@ -263,4 +285,4 @@ handleConferenceEnded(data: any, interactionId: string) { --- -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ +_Parent: [migration-overview.md](./migration-overview.md)_ diff --git a/packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md similarity index 87% rename from packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md rename to packages/contact-center/ai-docs/migration/store-task-utils-migration.md index c7e84c255..f560135f0 100644 --- a/packages/contact-center/ai-docs/migration/008-store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -1,4 +1,4 @@ -# Migration Doc 008: Store Task Utils Migration +# Store Task Utils Migration ## Summary @@ -6,6 +6,27 @@ The store's `task-utils.ts` contains ~15 utility functions that inspect raw task --- +## Constants to Delete + +| Delete | Reason | +|--------|--------| +| Local `TASK_EVENTS` enum (`store/src/store.types.ts`) | SDK exports this — delete local copy | +| `TASK_STATE_CONSULT`, `TASK_STATE_CONSULTING`, `TASK_STATE_CONSULT_COMPLETED` | SDK handles via state machine | +| `INTERACTION_STATE_*` constants | SDK handles via `TaskState` | +| `CONSULT_STATE_*` constants | SDK handles via context | + +## Constants to Keep + +| Keep | Reason | +|------|--------| +| `RELATIONSHIP_TYPE_CONSULT`, `MEDIA_TYPE_CONSULT` | Still used by `findMediaResourceId` | + +## Gotcha: `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`. Do not collapse these when updating `getTaskStatus()` or any consult timer logic. + +--- + ## Old Utilities Inventory **File:** `packages/contact-center/store/src/task-utils.ts` @@ -149,10 +170,13 @@ 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. +// KEPT in store/task-utils.ts — still needed for: +// 1. getTaskStatus() held-state derivation +// 2. Component layer isHeld (cannot derive from controls.hold.isEnabled) +// Usage unchanged — widgets still call findHoldStatus(task, 'mainCall', agentId) +export function findHoldStatus(task: ITask, mType: string, agentId: string): boolean { + // Implementation unchanged — reads from task.data.interaction.participants +} ``` ### Before/After: `getTaskStatus` (KEPT but enhanced) @@ -213,4 +237,4 @@ export function getTaskStatus(task: ITask, agentId: string): string { --- -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ +_Parent: [migration-overview.md](./migration-overview.md)_ From af98aa7af84bfce8735b979a7a3b6c5b5aa840f7 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 12 Mar 2026 18:11:51 +0530 Subject: [PATCH 05/24] docs(ai-docs): comprehensive audit fixes for store layer migration docs Cross-validated against both CC Widgets and CC SDK task-refactor repos: - Fix event count (27 not 30+), add all handler detail (state mutations, side effects) - Document handleTaskRemove cleanup, pre-existing bugs (dead handleConsultEnd, listener leak) - Add store-only enum members to delete, new SDK events to evaluate - Fix contradictions between proposed and mapping tables (consolidated into single table) - Correct renamed event usage in After code examples - Fix function counts (remove 5 not 6), add ConsultStatus enum to deletions - Add participant role constants to keep list, findHoldTimestamp dual-signature note - Replace simplified getTaskStatus Before code with actual implementation - Add all 22 get*ButtonVisibility functions to deletion scope - Add review function dependencies, test file impact, barrel export notes Made-with: Cursor --- .../migration/store-event-wiring-migration.md | 375 +++++++++++------- .../migration/store-task-utils-migration.md | 297 ++++++++------ 2 files changed, 406 insertions(+), 266 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index f8d8f1163..0b0dbdaaa 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -2,13 +2,15 @@ ## 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. +The store's `storeEventsWrapper.ts` currently registers 27 individual task event handlers via `registerTaskEventListeners()` that manually call `refreshTaskList()` and update observables. A companion method `handleTaskRemove()` unregisters them. 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. --- -## Event Names — 5 Renamed +## Event Names — Renamed and Deleted -The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-level naming that differs from the SDK's task-level naming. These must be aligned: +The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-level naming that differs from the SDK's task-level naming. + +### 5 Renamed Events | Old (Widget) | Old Value | New (SDK) | New Value | |---|---|---|---| @@ -18,13 +20,34 @@ The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-le | `CONTACT_RECORDING_PAUSED` | `'ContactRecordingPaused'` | `TASK_RECORDING_PAUSED` | `'task:recordingPaused'` | | `CONTACT_RECORDING_RESUMED` | `'ContactRecordingResumed'` | `TASK_RECORDING_RESUMED` | `'task:recordingResumed'` | -New event: `TASK_UI_CONTROLS_UPDATED` (`'task:ui-controls-updated'`) — subscribe to this for control updates. +### 4 Store-Only Enum Members (no SDK equivalent — delete) + +| Widget Enum Member | Value | Note | +|---|---|---| +| `TASK_UNHOLD` | `'task:unhold'` | SDK uses `TASK_RESUME` instead | +| `TASK_CONSULT` | `'task:consult'` | No SDK equivalent; consult flow uses multiple events | +| `TASK_PAUSE` | `'task:pause'` | No SDK equivalent; SDK uses `TASK_HOLD` | +| `AGENT_CONTACT_ASSIGNED` | `'AgentContactAssigned'` | SDK uses `TASK_ASSIGNED` (`'task:assigned'`) | + +### New SDK Events (not in current widget enum) -**Action:** Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from SDK instead. +| SDK Event | Value | Widget Action Needed | +|---|---|---| +| `TASK_UI_CONTROLS_UPDATED` | `'task:ui-controls-updated'` | **Must subscribe** — triggers widget re-renders | +| `TASK_UNASSIGNED` | `'task:unassigned'` | Evaluate if widget needs to handle | +| `TASK_CONSULT_QUEUE_FAILED` | `'task:consultQueueFailed'` | Evaluate if widget needs to handle | +| `TASK_RECORDING_STARTED` | `'task:recordingStarted'` | Evaluate for recording indicator | +| `TASK_RECORDING_PAUSE_FAILED` | `'task:recordingPauseFailed'` | Evaluate for error handling | +| `TASK_RECORDING_RESUME_FAILED` | `'task:recordingResumeFailed'` | Evaluate for error handling | +| `TASK_EXIT_CONFERENCE` | `'task:exitConference'` | Evaluate for conference flow | +| `TASK_TRANSFER_CONFERENCE` | `'task:transferConference'` | Evaluate for conference flow | +| `TASK_CLEANUP` | `'task:cleanup'` | SDK internal — likely no widget action | + +**Action:** Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from SDK instead. SDK's `TASK_EVENTS` already includes all needed events including `TASK_UI_CONTROLS_UPDATED`. ### Pre-existing Bug: Event Name Mismatches -The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. During migration, align these to SDK's exported `TASK_EVENTS` enum. +The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. During migration, replace the entire local enum with SDK's exported `TASK_EVENTS` enum. --- @@ -32,46 +55,67 @@ The 5 renamed events above are currently hardcoded in `store.types.ts` with a TO ### Entry Point **File:** `packages/contact-center/store/src/storeEventsWrapper.ts` -**Method:** `registerTaskEventListeners(task: ITask)` +**Register:** `registerTaskEventListeners(task: ITask)` — registers 27 event listeners +**Cleanup:** `handleTaskRemove(taskToRemove: ITask)` — unregisters all listeners + resets state ### 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) +1. On task creation, store registers individual listeners for 27 task events +2. Each handler manually updates store observables (`taskList`, `currentTask`, `consultStartTimeStamp`, `isQueueConsultInProgress`, `currentConsultQueueId`) 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 | +### Store Observables Affected by Event Handlers + +These live in `store/src/store.ts` and are mutated via setters in `storeEventsWrapper.ts`: + +| Observable | Type | Mutated By | +|---|---|---| +| `currentTask` | `ITask \| null` | `handleTaskAssigned`, `handleTaskEnd`, `handleTaskRemove` | +| `taskList` | `Record` | `refreshTaskList` | +| `consultStartTimeStamp` | `number \| undefined` | `handleConsultCreated`, `handleConsulting`, `handleConsultAccepted`, `handleConsultEnd`, `handleConsultQueueCancelled`, `handleConferenceStarted` | +| `isQueueConsultInProgress` | `boolean` | `handleConsultEnd`, `handleConsultQueueCancelled`, `handleConferenceStarted` | +| `currentConsultQueueId` | `string` | `handleConsultEnd`, `handleConsultQueueCancelled`, `handleConferenceStarted` | + +### Old Event Handlers (27 events) + +| # | Event | Handler | Action | +|---|-------|---------|--------| +| 1 | `TASK_END` | `handleTaskEnd` | Remove task from list, clear current task | +| 2 | `TASK_ASSIGNED` | `handleTaskAssigned` | Update task list, set current task | +| 3 | `AGENT_OFFER_CONTACT` | `refreshTaskList` | Re-fetch all tasks | +| 4 | `AGENT_CONSULT_CREATED` | `handleConsultCreated` | `refreshTaskList()` + `setConsultStartTimeStamp(Date.now())` | +| 5 | `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | Reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` + `refreshTaskList()` | +| 6 | `TASK_REJECT` | `handleTaskReject` | Remove task, fire callbacks | +| 7 | `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | Remove task, fire callbacks | +| 8 | `AGENT_WRAPPEDUP` | `refreshTaskList` | Re-fetch all tasks | +| 9 | `TASK_CONSULTING` | `handleConsulting` | `refreshTaskList()` + `setConsultStartTimeStamp(Date.now())` | +| 10 | `TASK_CONSULT_ACCEPTED` | `handleConsultAccepted` | `refreshTaskList()` + `setConsultStartTimeStamp(Date.now())` + set ENGAGED state + **registers `TASK_MEDIA` listener on consult task (browser only)** | +| 11 | `TASK_OFFER_CONSULT` | `handleConsultOffer` | `refreshTaskList()` | +| 12 | `TASK_AUTO_ANSWERED` | `handleAutoAnswer` | `setIsDeclineButtonEnabled(true)` + `refreshTaskList()` | +| 13 | `TASK_CONSULT_END` | `refreshTaskList` | Re-fetch all tasks (**Note:** `handleConsultEnd` method exists but is NOT wired — see pre-existing bug below) | +| 14 | `TASK_HOLD` | `refreshTaskList` | Re-fetch all tasks | +| 15 | `TASK_RESUME` | `refreshTaskList` | Re-fetch all tasks | +| 16 | `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | `refreshTaskList()` | +| 17 | `TASK_CONFERENCE_END_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| 18 | `TASK_CONFERENCE_ESTABLISHING` | `refreshTaskList` | Re-fetch all tasks | +| 19 | `TASK_CONFERENCE_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| 20 | `TASK_PARTICIPANT_JOINED` | `handleConferenceStarted` | Reset `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp` + `refreshTaskList()` | +| 21 | `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | `refreshTaskList()` | +| 22 | `TASK_PARTICIPANT_LEFT_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| 23 | `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | (same as #20) | +| 24 | `TASK_CONFERENCE_TRANSFERRED` | `refreshTaskList` | Re-fetch all tasks | +| 25 | `TASK_CONFERENCE_TRANSFER_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| 26 | `TASK_POST_CALL_ACTIVITY` | `refreshTaskList` | Re-fetch all tasks | +| 27 | `TASK_MEDIA` | `handleTaskMedia` | Browser-only: `setCallControlAudio(new MediaStream([track]))` | + +### Pre-existing Bugs in Old Code + +**Bug 1: `handleConsultEnd` is dead code.** +A `handleConsultEnd` method exists (resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`) but `TASK_CONSULT_END` is wired to `refreshTaskList()` instead. The method's consult state cleanup never runs. + +**Bug 2: `handleTaskRemove` listener mismatch.** +`registerTaskEventListeners` wires `TASK_CONFERENCE_TRANSFERRED → this.refreshTaskList`. But `handleTaskRemove` calls `taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.handleConferenceEnded)` — wrong handler reference. This listener is **never actually removed**, causing a listener leak. --- @@ -83,32 +127,42 @@ The 5 renamed events above are currently hardcoded in `store.types.ts` with a TO 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** | +### Definitive New Event Registration + +Many events that currently trigger `refreshTaskList()` will no longer need it because `task.data` is kept in sync by the SDK. Below is the single authoritative table for all event handler changes: + +| # | Event | New Handler | Change | Detail | +|---|-------|-------------|--------|--------| +| 1 | `TASK_END` | `handleTaskEnd` | **Keep** | Remove from task list, clear current task | +| 2 | `TASK_ASSIGNED` | `handleTaskAssigned` | **Keep** | Update task list, set current task | +| 3 | `TASK_REJECT` | `handleTaskReject` | **Keep** | Remove from task list | +| 4 | `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | **Keep** | Remove from task list | +| 5 | `TASK_MEDIA` | `handleTaskMedia` | **Keep** | Browser-only WebRTC setup (conditional registration) | +| 6 | `TASK_UI_CONTROLS_UPDATED` | `handleUIControlsUpdated` | **Add new** | Fire callbacks to trigger widget re-renders | +| 7 | `TASK_WRAPPEDUP` | `handleWrappedup` | **Keep + rename** | Was `AGENT_WRAPPEDUP`. Keep `refreshTaskList()` — task must be removed from list after wrapup. Fire callback. | +| 8 | `TASK_CONSULT_END` | `handleConsultEnd` | **Fix wiring** | Wire the existing (currently dead) `handleConsultEnd` method. Resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`. Remove `refreshTaskList()`. Fire callback. | +| 9 | `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | **Simplify** | Keep consult state reset. Remove `refreshTaskList()`. Fire callback. | +| 10 | `TASK_CONSULTING` | `handleConsulting` | **Simplify** | Keep `setConsultStartTimeStamp(Date.now())`. Remove `refreshTaskList()`. Fire callback. | +| 11 | `TASK_CONSULT_CREATED` | `handleConsultCreated` | **Simplify + rename** | Was `AGENT_CONSULT_CREATED`. Keep `setConsultStartTimeStamp(Date.now())`. Remove `refreshTaskList()`. Fire callback. | +| 12 | `TASK_CONSULT_ACCEPTED` | `handleConsultAccepted` | **Simplify** | Keep `setConsultStartTimeStamp(Date.now())`, keep ENGAGED state, **keep `TASK_MEDIA` listener registration (browser)**. Remove `refreshTaskList()`. Fire callback. | +| 13 | `TASK_AUTO_ANSWERED` | `handleAutoAnswer` | **Simplify** | Keep `setIsDeclineButtonEnabled(true)`. Remove `refreshTaskList()`. Fire callback. | +| 14 | `TASK_OFFER_CONTACT` | Fire callback only | **Simplify + rename** | Was `AGENT_OFFER_CONTACT`. Remove `refreshTaskList()`. | +| 15 | `TASK_OFFER_CONSULT` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 16 | `TASK_PARTICIPANT_JOINED` | `handleConferenceStarted` | **Simplify** | Keep consult state reset (`isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`). Remove `refreshTaskList()`. Fire callback. | +| 17 | `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | **Simplify** | Same as #16 | +| 18 | `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | **Simplify** | Remove `refreshTaskList()`. Fire callback. | +| 19 | `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | **Simplify** | Same as #18 | +| 20 | `TASK_HOLD` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 21 | `TASK_RESUME` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 22 | `TASK_RECORDING_PAUSED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_PAUSED`. | +| 23 | `TASK_RECORDING_RESUMED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_RESUMED`. | +| 24 | `TASK_POST_CALL_ACTIVITY` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 25 | `TASK_CONFERENCE_ESTABLISHING` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 26 | `TASK_CONFERENCE_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 27 | `TASK_CONFERENCE_END_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 28 | `TASK_PARTICIPANT_LEFT_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 29 | `TASK_CONFERENCE_TRANSFERRED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | +| 30 | `TASK_CONFERENCE_TRANSFER_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | ### Key Insight: `refreshTaskList()` Elimination @@ -117,27 +171,7 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be **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** | +- `TASK_WRAPPEDUP` (task must be removed from list — may be replaceable with explicit list removal) --- @@ -147,24 +181,22 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be #### 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() + task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); + task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); + 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, this.handleConferenceStarted); + // ... 19 more event registrations, most calling refreshTaskList() } ``` #### After ```typescript -// storeEventsWrapper.ts — registerTaskEventListeners(task) registerTaskEventListeners(task: ITask) { const interactionId = task.data.interactionId; @@ -174,23 +206,54 @@ registerTaskEventListeners(task: ITask) { }); // 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)); + task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); + task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); + task.on(TASK_EVENTS.TASK_REJECT, (reason) => this.handleTaskReject(task, reason)); + task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, (reason) => this.handleOutdialFailed(reason)); + + // KEEP + FIX WIRING: Wire handleConsultEnd (was dead code) + task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); + + // KEEP: Consult state management (remove refreshTaskList, keep state mutations) + task.on(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); // renamed from AGENT_CONSULT_CREATED + task.on(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting); + task.on(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted); + task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); + + // KEEP: Conference state management (remove refreshTaskList, keep consult state reset) + task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); + + // KEEP: Auto-answer sets decline button state + task.on(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); + + // KEEP: Wrapup completion — task must be removed from list + task.on(TASK_EVENTS.TASK_WRAPPEDUP, (data) => { // renamed from AGENT_WRAPPEDUP + this.refreshTaskList(); + this.fireTaskCallbacks(TASK_EVENTS.TASK_WRAPPEDUP, interactionId, data); + }); // 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() + task.on(TASK_EVENTS.TASK_OFFER_CONTACT, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONTACT, interactionId)); // renamed + task.on(TASK_EVENTS.TASK_OFFER_CONSULT, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONSULT, interactionId)); + task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_PAUSED, interactionId)); // renamed + task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_RESUMED, interactionId)); // renamed + task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, interactionId)); + task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, interactionId)); + task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_FAILED, interactionId)); + task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, interactionId)); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, interactionId)); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, interactionId)); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, interactionId)); + + // Browser-only: WebRTC media setup + if (this.deviceType === DEVICE_TYPE_BROWSER) { + task.on(TASK_EVENTS.TASK_MEDIA, this.handleTaskMedia); + } } ``` @@ -198,7 +261,6 @@ registerTaskEventListeners(task: ITask) { #### 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()); @@ -210,13 +272,12 @@ 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. +// refreshTaskList() only called on initialization/hydration and TASK_WRAPPEDUP. // Individual events just fire callbacks for widget-layer side effects. task.on(TASK_EVENTS.TASK_HOLD, () => { @@ -225,41 +286,75 @@ task.on(TASK_EVENTS.TASK_HOLD, () => { 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); -} +handleConferenceStarted = () => { + runInAction(() => { + this.setIsQueueConsultInProgress(false); + this.setCurrentConsultQueueId(null); + this.setConsultStartTimeStamp(null); + }); + this.refreshTaskList(); +}; -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); -} +handleConferenceEnded = () => { + this.refreshTaskList(); +}; ``` #### 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. +// Keep consult state reset in handleConferenceStarted; remove refreshTaskList() from both. -handleConferenceStarted(data: any, interactionId: string) { - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId, data); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId, data); -} +handleConferenceStarted = () => { + runInAction(() => { + this.setIsQueueConsultInProgress(false); + this.setCurrentConsultQueueId(null); + this.setConsultStartTimeStamp(null); + }); + this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId); + this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId); +}; + +handleConferenceEnded = () => { + this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId); + this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId); +}; +``` -handleConferenceEnded(data: any, interactionId: string) { - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId, data); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId, data); -} +### Pattern 4: `handleTaskRemove()` — Update Cleanup + +#### Before +```typescript +handleTaskRemove = (taskToRemove: ITask) => { + if (taskToRemove) { + taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); + taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); + // ... all 27 .off() calls using OLD event names + // BUG: TASK_CONFERENCE_TRANSFERRED uses wrong handler (handleConferenceEnded instead of refreshTaskList) + } +}; +``` + +#### After +```typescript +handleTaskRemove = (taskToRemove: ITask) => { + if (taskToRemove) { + // Use renamed SDK event names; add TASK_UI_CONTROLS_UPDATED; fix handler references + taskToRemove.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, this.handleUIControlsUpdated); // NEW + taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); + taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); + taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); // FIX: was refreshTaskList + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, ...); // FIX: match registered handler + // ... all other .off() calls matching their .on() counterparts exactly + } +}; ``` --- @@ -268,20 +363,26 @@ handleConferenceEnded(data: any, interactionId: string) { | File | Action | |------|--------| -| `store/src/storeEventsWrapper.ts` | Refactor `registerTaskEventListeners`, simplify/remove handlers | +| `store/src/storeEventsWrapper.ts` | Refactor `registerTaskEventListeners` (see definitive table), update `handleTaskRemove` (fix listener mismatches + add `TASK_UI_CONTROLS_UPDATED`), simplify handlers (remove `refreshTaskList()` from all except `TASK_WRAPPEDUP`), wire `handleConsultEnd` to `TASK_CONSULT_END` | | `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 | +| `store/src/store.types.ts` | Delete local `TASK_EVENTS` enum; import from SDK (which includes `TASK_UI_CONTROLS_UPDATED`) | +| `store/tests/*` | Update tests for renamed events, new `TASK_UI_CONTROLS_UPDATED` handler, simplified handlers | --- ## 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 +- [ ] `refreshTaskList()` only called on init/hydration and `TASK_WRAPPEDUP`, 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 +- [ ] `handleTaskRemove` unregisters all listeners correctly (no listener leaks) +- [ ] `handleConsultEnd` is properly wired and resets consult state on `TASK_CONSULT_END` +- [ ] `handleConsultAccepted` still registers `TASK_MEDIA` listener on consult task (browser) +- [ ] `handleAutoAnswer` still sets `isDeclineButtonEnabled = true` +- [ ] All 5 renamed events use SDK names (`TASK_WRAPPEDUP`, `TASK_CONSULT_CREATED`, `TASK_OFFER_CONTACT`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED`) --- diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index f560135f0..ad4b70559 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -2,24 +2,40 @@ ## 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. +The store's `task-utils.ts` contains 16 exported 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. ---- +**Barrel export:** `store/src/index.ts` has `export * from './task-utils'` — all 16 functions are publicly exported via `@webex/cc-store`. Removing functions will cause compile errors in any downstream consumer still importing them. -## Constants to Delete +--- -| Delete | Reason | -|--------|--------| -| Local `TASK_EVENTS` enum (`store/src/store.types.ts`) | SDK exports this — delete local copy | -| `TASK_STATE_CONSULT`, `TASK_STATE_CONSULTING`, `TASK_STATE_CONSULT_COMPLETED` | SDK handles via state machine | -| `INTERACTION_STATE_*` constants | SDK handles via `TaskState` | -| `CONSULT_STATE_*` constants | SDK handles via context | +## Constants and Types to Delete + +| Delete | File | Reason | +|--------|------|--------| +| Local `TASK_EVENTS` enum | `store/src/store.types.ts` | SDK exports this — delete local copy (covered in detail in [store-event-wiring-migration.md](./store-event-wiring-migration.md)) | +| `ConsultStatus` enum | `store/src/store.types.ts` | All consumers (`getConsultStatus`, `getControlsVisibility`) are being removed | +| `TASK_STATE_CONSULT` | `store/src/constants.ts` | SDK handles via `TaskState.CONSULT_INITIATING` | +| `TASK_STATE_CONSULTING` | `store/src/constants.ts` | SDK handles via `TaskState.CONSULTING` | +| `TASK_STATE_CONSULT_COMPLETED` | `store/src/constants.ts` | SDK handles via context | +| `INTERACTION_STATE_WRAPUP` | `store/src/constants.ts` | SDK handles via `TaskState.WRAPPING_UP` | +| `INTERACTION_STATE_POST_CALL` | `store/src/constants.ts` | SDK handles via `TaskState.POST_CALL` | +| `INTERACTION_STATE_CONNECTED` | `store/src/constants.ts` | SDK handles via `TaskState.CONNECTED` | +| `INTERACTION_STATE_CONFERENCE` | `store/src/constants.ts` | SDK handles via `TaskState.CONFERENCING` | +| `CONSULT_STATE_INITIATED` | `store/src/constants.ts` | SDK handles via context | +| `CONSULT_STATE_COMPLETED` | `store/src/constants.ts` | SDK handles via context | +| `CONSULT_STATE_CONFERENCING` | `store/src/constants.ts` | SDK handles via context | ## Constants to Keep -| Keep | Reason | -|------|--------| -| `RELATIONSHIP_TYPE_CONSULT`, `MEDIA_TYPE_CONSULT` | Still used by `findMediaResourceId` | +| Keep | File | Reason | +|------|------|--------| +| `RELATIONSHIP_TYPE_CONSULT` | `store/src/constants.ts` | Still used by `findMediaResourceId` (KEEP) | +| `MEDIA_TYPE_CONSULT` | `store/src/constants.ts` | Still used by `findMediaResourceId` (KEEP) | +| `AGENT` | `store/src/constants.ts` | Used by `getConferenceParticipants` (KEEP) for participant filtering | +| `CUSTOMER` | `store/src/constants.ts` | Used by `EXCLUDED_PARTICIPANT_TYPES` | +| `SUPERVISOR` | `store/src/constants.ts` | Used by `EXCLUDED_PARTICIPANT_TYPES` | +| `VVA` | `store/src/constants.ts` | Used by `EXCLUDED_PARTICIPANT_TYPES` | +| `EXCLUDED_PARTICIPANT_TYPES` | `store/src/constants.ts` | Used by `getConferenceParticipants` (KEEP) for participant filtering | ## Gotcha: `TaskState.CONSULT_INITIATING` vs `CONSULTING` @@ -29,121 +45,115 @@ The SDK has `CONSULT_INITIATING` (consult requested, async in-progress) and `CON ## 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 | +**File:** `packages/contact-center/store/src/task-utils.ts` (16 exported functions) + +| # | Function | Purpose | Actual Consumers (production code) | +|---|----------|---------|-------------------------------------| +| 1 | `isIncomingTask(task, agentId)` | Check if task is incoming | `storeEventsWrapper.ts` | +| 2 | `getConsultMPCState(task, agentId)` | Consult multi-party conference state string | `getTaskStatus()`, `findHoldStatus()` area logic (internal to `task-utils.ts`) | +| 3 | `isSecondaryAgent(task)` | Whether agent is secondary in consult | `task-utils.ts` (internal) | +| 4 | `isSecondaryEpDnAgent(task)` | Whether agent is secondary EP-DN | `getTaskStatus()`, `getConsultStatus()` (internal to `task-utils.ts`) | +| 5 | `getTaskStatus(task, agentId)` | Human-readable task status string | `getConsultStatus()` (internal — no external consumer currently) | +| 6 | `getConsultStatus(task, agentId)` | `ConsultStatus` enum value | `task/src/Utils/task-util.ts` (`getControlsVisibility`) | +| 7 | `getIsConferenceInProgress(task)` | Boolean conference check | Tests only — `task-util.ts` uses `task?.data?.isConferenceInProgress` directly instead | +| 8 | `getConferenceParticipants(task, agentId)` | Filtered participant list | `task/src/helper.ts` (CallControl hook) | +| 9 | `getConferenceParticipantsCount(task)` | Participant count | `task/src/Utils/task-util.ts` (`getControlsVisibility`) | +| 10 | `getIsCustomerInCall(task)` | Whether customer is connected | `task/src/Utils/task-util.ts` (`getControlsVisibility`) | +| 11 | `getIsConsultInProgress(task)` | Whether consult is active | `task/src/Utils/task-util.ts` (`getControlsVisibility`) | +| 12 | `isInteractionOnHold(task)` | Whether any media is held | Timer utils | +| 13 | `setmTypeForEPDN(task, mType)` | Media type for EP-DN agents | CallControl hook | +| 14 | `findMediaResourceId(task, mType)` | Media resource ID lookup | CallControl hook (switch calls) | +| 15 | `findHoldStatus(task, mType, agentId)` | Hold status by media type | `task/src/Utils/task-util.ts` (`getControlsVisibility`), `getTaskStatus()` (via `getConsultMPCState` chain) | +| 16 | `findHoldTimestamp(task, mType)` | Hold timestamp for timers | Timer utils (store version takes `ITask`; see dual-signature note below) | + +### Important: `findHoldTimestamp` Dual Signatures + +Two different `findHoldTimestamp` functions exist with different signatures: +- **`store/src/task-utils.ts`:** `findHoldTimestamp(task: ITask, mType: string)` — takes full task object +- **`task/src/Utils/task-util.ts`:** `findHoldTimestamp(interaction: Interaction, mType: string)` — takes interaction only + +`timer-utils.ts` imports from `@webex/cc-store` (task version). `useHoldTimer.ts` imports from `task-util` (interaction version). Both are kept but the implementing agent must not confuse them. --- ## 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 | +### Remove — 5 Functions (SDK handles via uiControls) + +| # | Function | Reason | SDK Replacement | Impact on Other Functions | +|---|----------|--------|-----------------|--------------------------| +| 1 | `getConsultStatus(task, agentId)` | Primary consumer `getControlsVisibility` is deleted | `task.uiControls` encodes all consult control states | `getTaskStatus()` calls this — must be updated (see After code below) | +| 2 | `getIsConferenceInProgress(task)` | `task-util.ts` already uses `task?.data?.isConferenceInProgress` directly; function only used in tests | `task.uiControls.exitConference.isVisible` | None | +| 3 | `getConferenceParticipantsCount(task)` | Used only in `getControlsVisibility` | SDK computes max participant check internally | None | +| 4 | `getIsCustomerInCall(task)` | Used only in `getControlsVisibility` | SDK computes internally | None | +| 5 | `getIsConsultInProgress(task)` | Used only in `getControlsVisibility` | SDK computes internally | None | + +### Keep — 7 Functions (Widget-layer concerns) + +| # | Function | Reason | +|---|----------|--------| +| 1 | `isIncomingTask(task, agentId)` | Store needs this for routing incoming tasks | +| 2 | `getTaskStatus(task, agentId)` | Returns human-readable status. Currently only consumed internally by `getConsultStatus()`, but after migration will be the primary status provider for TaskList display. Must be updated to use `task.uiControls` instead of `getConsultStatus()`. | +| 3 | `getConferenceParticipants(task, agentId)` | CallControl UI shows participant list (display, not control visibility). Uses `EXCLUDED_PARTICIPANT_TYPES` from constants. | +| 4 | `isInteractionOnHold(task)` | Timer logic needs this | +| 5 | `findMediaResourceId(task, mType)` | Switch-call actions need media resource IDs. Uses `RELATIONSHIP_TYPE_CONSULT`, `MEDIA_TYPE_CONSULT` from constants. | +| 6 | `findHoldTimestamp(task, mType)` | Hold timer needs timestamp. Note: store version takes `ITask`, task-util version takes `Interaction` (see dual-signature note above). | +| 7 | `findHoldStatus(task, mType, agentId)` | Needed for `getTaskStatus()` held-state derivation and component layer `isHeld` — cannot derive from `controls.hold.isEnabled` | + +### Review — 4 Functions (may simplify or remove) + +| # | Function | Consideration | Dependency | +|---|----------|--------------|------------| +| 1 | `isSecondaryAgent(task)` | May be replaceable by SDK context | Used internally by `task-utils.ts` | +| 2 | `isSecondaryEpDnAgent(task)` | May be replaceable by SDK context | Used by `getTaskStatus()` and `getConsultStatus()` | +| 3 | `getConsultMPCState(task, agentId)` | Review if still needed with SDK handling consult state | **Called by `getTaskStatus()` (line 112) as its return value** — if `getTaskStatus` is rewritten to use `task.uiControls`, this may become removable | +| 4 | `setmTypeForEPDN(task, mType)` | Review if SDK simplifies this | Used by CallControl hook | --- -## 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/After: Removing `getConsultStatus` and `getControlsVisibility` ### Before (consumed in `task-util.ts::getControlsVisibility`) ```typescript -// task-util.ts — old approach +// task/src/Utils/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... + // 22 individual get*ButtonVisibility() functions using these derived states... return { /* 22 controls + 7 state flags */ }; } ``` -### After (all above replaced by `task.uiControls`) +### After (entire `getControlsVisibility` + 22 visibility functions deleted) ```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 +// task/src/Utils/task-util.ts — DELETE getControlsVisibility and all 22 get*ButtonVisibility functions: +// getAcceptButtonVisibility, getDeclineButtonVisibility, getEndButtonVisibility, +// getMuteUnmuteButtonVisibility, getHoldResumeButtonVisibility, +// getPauseResumeRecordingButtonVisibility, getRecordingIndicatorVisibility, +// getTransferButtonVisibility, getConferenceButtonVisibility, +// getExitConferenceButtonVisibility, getMergeConferenceButtonVisibility, +// getConsultButtonVisibility, getEndConsultButtonVisibility, +// getConsultTransferButtonVisibility, getMergeConferenceConsultButtonVisibility, +// getConsultTransferConsultButtonVisibility, getMuteUnmuteConsultButtonVisibility, +// getSwitchToMainCallButtonVisibility, getSwitchToConsultButtonVisibility, +// getWrapupButtonVisibility, getControlsVisibility +// +// KEEP in task-util.ts: findHoldTimestamp(interaction, mType) — different signature from store version // In useCallControl hook — no imports from store task-utils for controls: const controls = currentTask?.uiControls ?? getDefaultUIControls(); @@ -152,18 +162,15 @@ const controls = currentTask?.uiControls ?? getDefaultUIControls(); ### Before/After: `findHoldStatus` — RETAINED (not removed) -#### Before (used in controls computation) +#### Before (used in controls computation and task status) ```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; -} +// store/task-utils.ts — exported function +export const findHoldStatus = (task: ITask, mType: string, agentId: string): boolean => { + // Reads from task.data.interaction.participants to determine hold state + // ... +}; -// task-util.ts — consumed for control visibility +// task-util.ts — consumed for control visibility (BEING DELETED) const isHeld = findHoldStatus(task, 'mainCall', agentId); const consultCallHeld = findHoldStatus(task, 'consult', agentId); ``` @@ -171,46 +178,70 @@ const consultCallHeld = findHoldStatus(task, 'consult', agentId); #### After ```typescript // KEPT in store/task-utils.ts — still needed for: -// 1. getTaskStatus() held-state derivation -// 2. Component layer isHeld (cannot derive from controls.hold.isEnabled) -// Usage unchanged — widgets still call findHoldStatus(task, 'mainCall', agentId) -export function findHoldStatus(task: ITask, mType: string, agentId: string): boolean { - // Implementation unchanged — reads from task.data.interaction.participants -} +// 1. getTaskStatus() held-state derivation (cannot derive from controls.hold.isEnabled) +// 2. Component layer isHeld prop +// Implementation unchanged — reads from task.data.interaction.participants +export const findHoldStatus = (task: ITask, mType: string, agentId: string): boolean => { + // ...unchanged... +}; ``` -### Before/After: `getTaskStatus` (KEPT but enhanced) +### Before/After: `getTaskStatus` (KEPT but rewritten) -#### Before +#### Before (actual implementation) ```typescript -// store/task-utils.ts — returns human-readable status +// store/task-utils.ts — the real code (NOT a simplification) 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'; + const interaction = task.data.interaction; + + // EP-DN secondary agent handling + if (isSecondaryEpDnAgent(task)) { + if (interaction.state === INTERACTION_STATE_CONFERENCE) { + return INTERACTION_STATE_CONFERENCE; + } + return TASK_STATE_CONSULTING; + } + + // Consult-completed wrapup handling + if ( + (interaction.state === INTERACTION_STATE_WRAPUP || interaction.state === INTERACTION_STATE_POST_CALL) && + interaction.participants[agentId]?.consultState === CONSULT_STATE_COMPLETED + ) { + return TASK_STATE_CONSULT_COMPLETED; + } + + // Delegates to getConsultMPCState for all other cases + return getConsultMPCState(task, agentId); } + +// getConsultStatus() calls getTaskStatus() and maps the string to ConsultStatus enum values ``` -#### After (enhanced with SDK controls) +#### After (rewritten to use SDK controls) ```typescript -// store/task-utils.ts — can now derive status from uiControls +// store/task-utils.ts — rewritten to use task.uiControls +// NOTE: getConsultStatus() is deleted, so getTaskStatus() no longer needs to produce +// values that feed into ConsultStatus. It becomes a pure display-status function. 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 + + // 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'; + if (controls.accept.isVisible) return 'Offered'; // NEW: not in old version + return 'Unknown'; // NEW: safe default } +// NOTE: New states 'Offered' and 'Unknown' are additions not present in old code. +// EP-DN secondary agent handling and consultState-based wrapup may need review — +// verify that task.uiControls correctly handles these edge cases in SDK. ``` --- @@ -219,21 +250,29 @@ export function getTaskStatus(task: ITask, agentId: string): string { | 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) | +| `store/src/task-utils.ts` | Remove 5 functions, keep 7 (update `getTaskStatus` to use `task.uiControls`), review 4 | +| `store/src/store.types.ts` | Delete `ConsultStatus` enum (all consumers removed) | +| `store/src/constants.ts` | Delete 9 task/interaction/consult state constants; keep 7 participant/media constants | +| `task/src/Utils/task-util.ts` | Delete `getControlsVisibility` + all 22 `get*ButtonVisibility` functions; keep `findHoldTimestamp(interaction, mType)` | +| `store/tests/task-utils.ts` | Update tests: remove tests for 5 deleted functions, update `getTaskStatus` tests | +| `task/tests/utils/task-util.ts` | Remove tests for deleted visibility functions | | 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 +- [ ] 5 removed functions have no remaining consumers (compile check) +- [ ] 7 kept functions still work correctly +- [ ] `getTaskStatus` produces correct status for all task states (connected, held, consulting, conference, wrapup, offered) +- [ ] `getTaskStatus` handles EP-DN secondary agent edge cases correctly - [ ] Conference participant display unchanged - [ ] Hold timer unchanged - [ ] Switch-call media resource IDs work +- [ ] `ConsultStatus` enum removed with no remaining imports +- [ ] 9 deleted constants have no remaining consumers +- [ ] `findHoldTimestamp` dual-signature (task vs interaction) not confused during migration +- [ ] `task-util.ts` `getControlsVisibility` + 22 visibility functions fully deleted --- From 850b0bacdd1fcbe2e4883521398af1a19797bf79 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 12 Mar 2026 19:19:52 +0530 Subject: [PATCH 06/24] docs(ai-docs): clarify getControlsVisibility is hook layer, not store Add callout note explaining getControlsVisibility lives in task/src/Utils/task-util.ts (hook layer), not the store. It appears in the store utils doc because it is the primary consumer of the 5 store functions being removed. Made-with: Cursor --- .../migration/store-task-utils-migration.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index ad4b70559..167adddc6 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -111,11 +111,16 @@ Two different `findHoldTimestamp` functions exist with different signatures: --- -## Before/After: Removing `getConsultStatus` and `getControlsVisibility` +## Before/After: Downstream Impact — `getControlsVisibility` Deletion -### Before (consumed in `task-util.ts::getControlsVisibility`) +> **Note:** `getControlsVisibility` is NOT in the store. It lives in the **task package** at +> `task/src/Utils/task-util.ts` (hook/widget layer). It is shown here because it is the +> **primary consumer** of the 5 store functions being removed above. When those store +> functions are deleted, this entire function chain becomes deletable — replaced by `task.uiControls`. + +### Before (`task/src/Utils/task-util.ts` — imports 5 store functions) ```typescript -// task/src/Utils/task-util.ts — old approach +// task/src/Utils/task-util.ts (hook layer, NOT store) import { getConsultStatus, ConsultStatus, getIsConsultInProgress, getIsCustomerInCall, getConferenceParticipantsCount, findHoldStatus } from '@webex/cc-store'; @@ -139,9 +144,10 @@ export function getControlsVisibility(deviceType, featureFlags, task, agentId, c } ``` -### After (entire `getControlsVisibility` + 22 visibility functions deleted) +### After (`task/src/Utils/task-util.ts` — entire `getControlsVisibility` + 22 visibility functions deleted) ```typescript -// task/src/Utils/task-util.ts — DELETE getControlsVisibility and all 22 get*ButtonVisibility functions: +// task/src/Utils/task-util.ts (hook layer, NOT store) +// DELETE getControlsVisibility and all 22 get*ButtonVisibility functions: // getAcceptButtonVisibility, getDeclineButtonVisibility, getEndButtonVisibility, // getMuteUnmuteButtonVisibility, getHoldResumeButtonVisibility, // getPauseResumeRecordingButtonVisibility, getRecordingIndicatorVisibility, From 0df8e9c70bfea4f5077daff24ac5336bee761c33 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 12 Mar 2026 19:28:36 +0530 Subject: [PATCH 07/24] fix: address 3 valid Codex review comments on PR #646 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add ordering constraint for consult state constants — TASK_STATE_CONSULT, TASK_STATE_CONSULTING, TASK_STATE_CONSULT_COMPLETED must not be deleted until findHoldStatus/isConsultOnHoldMPC are rewritten to use SDK TaskState. 2. Use stable named handler (handleUIControlsUpdated) for TASK_UI_CONTROLS_UPDATED event instead of inline arrow, so handleTaskRemove can correctly .off() it. 3. Fix conference handlers to accept eventType parameter and fire only the triggering event's callback, preventing duplicate notifications when both TASK_CONFERENCE_STARTED and TASK_PARTICIPANT_JOINED are wired to the same handler. Made-with: Cursor --- .../migration/store-event-wiring-migration.md | 44 ++++++++++++------- .../migration/store-task-utils-migration.md | 15 +++++-- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 0b0dbdaaa..ab75bd9b3 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -201,9 +201,8 @@ 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); - }); + // Use a named method (not inline arrow) so handleTaskRemove can .off() the same reference + task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, this.handleUIControlsUpdated); // KEEP: Task lifecycle events that need store-level management task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); @@ -221,10 +220,11 @@ registerTaskEventListeners(task: ITask) { task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); // KEEP: Conference state management (remove refreshTaskList, keep consult state reset) - task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); - task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); - task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); - task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); + // Pass event type so handler fires only the correct callback (not both) + task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, () => this.handleConferenceStarted(TASK_EVENTS.TASK_PARTICIPANT_JOINED)); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, () => this.handleConferenceStarted(TASK_EVENTS.TASK_CONFERENCE_STARTED)); + task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, () => this.handleConferenceEnded(TASK_EVENTS.TASK_CONFERENCE_ENDED)); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, () => this.handleConferenceEnded(TASK_EVENTS.TASK_PARTICIPANT_LEFT)); // KEEP: Auto-answer sets decline button state task.on(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); @@ -288,7 +288,19 @@ task.on(TASK_EVENTS.TASK_RESUME, () => { }); ``` -### Pattern 3: Conference Handler Simplification +### Pattern 3: New `handleUIControlsUpdated` Method + +```typescript +// NEW method — must be a class property (not inline arrow) so .off() can detach it +handleUIControlsUpdated = (uiControls: TaskUIControls) => { + const interactionId = this.store.currentTask?.data.interactionId; + if (interactionId) { + this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); + } +}; +``` + +### Pattern 4: Conference Handler Simplification #### Before ```typescript @@ -311,24 +323,26 @@ handleConferenceEnded = () => { // SDK state machine handles CONFERENCING state transitions. // task.data and task.uiControls already reflect conference state. // Keep consult state reset in handleConferenceStarted; remove refreshTaskList() from both. +// +// IMPORTANT: Do NOT fire both TASK_CONFERENCE_STARTED and TASK_PARTICIPANT_JOINED from one handler. +// Both events are wired to this same handler, so one incoming event would produce two different +// callback notifications. Instead, accept the event type as a parameter. -handleConferenceStarted = () => { +handleConferenceStarted = (eventType: TASK_EVENTS) => { runInAction(() => { this.setIsQueueConsultInProgress(false); this.setCurrentConsultQueueId(null); this.setConsultStartTimeStamp(null); }); - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId); + this.fireTaskCallbacks(eventType, interactionId); }; -handleConferenceEnded = () => { - this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId); - this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId); +handleConferenceEnded = (eventType: TASK_EVENTS) => { + this.fireTaskCallbacks(eventType, interactionId); }; ``` -### Pattern 4: `handleTaskRemove()` — Update Cleanup +### Pattern 5: `handleTaskRemove()` — Update Cleanup #### Before ```typescript diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index 167adddc6..3494279ad 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -14,9 +14,9 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect |--------|------|--------| | Local `TASK_EVENTS` enum | `store/src/store.types.ts` | SDK exports this — delete local copy (covered in detail in [store-event-wiring-migration.md](./store-event-wiring-migration.md)) | | `ConsultStatus` enum | `store/src/store.types.ts` | All consumers (`getConsultStatus`, `getControlsVisibility`) are being removed | -| `TASK_STATE_CONSULT` | `store/src/constants.ts` | SDK handles via `TaskState.CONSULT_INITIATING` | -| `TASK_STATE_CONSULTING` | `store/src/constants.ts` | SDK handles via `TaskState.CONSULTING` | -| `TASK_STATE_CONSULT_COMPLETED` | `store/src/constants.ts` | SDK handles via context | +| `TASK_STATE_CONSULT` | `store/src/constants.ts` | SDK `TaskState.CONSULT_INITIATING` — **delete ONLY AFTER rewriting `findHoldStatus`** (see ordering note below) | +| `TASK_STATE_CONSULTING` | `store/src/constants.ts` | SDK `TaskState.CONSULTING` — **same ordering constraint** | +| `TASK_STATE_CONSULT_COMPLETED` | `store/src/constants.ts` | SDK handles via context — **same ordering constraint** | | `INTERACTION_STATE_WRAPUP` | `store/src/constants.ts` | SDK handles via `TaskState.WRAPPING_UP` | | `INTERACTION_STATE_POST_CALL` | `store/src/constants.ts` | SDK handles via `TaskState.POST_CALL` | | `INTERACTION_STATE_CONNECTED` | `store/src/constants.ts` | SDK handles via `TaskState.CONNECTED` | @@ -37,6 +37,15 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect | `VVA` | `store/src/constants.ts` | Used by `EXCLUDED_PARTICIPANT_TYPES` | | `EXCLUDED_PARTICIPANT_TYPES` | `store/src/constants.ts` | Used by `getConferenceParticipants` (KEEP) for participant filtering | +## Ordering Constraint: Consult State Constants + +`findHoldStatus` (KEEP) depends on `TASK_STATE_CONSULT`, `TASK_STATE_CONSULTING`, and `TASK_STATE_CONSULT_COMPLETED` via: +- **Direct usage:** Line 328 — `mType === TASK_STATE_CONSULT` +- **Via `isConsultOnHoldMPC`:** Line 303 — `[TASK_STATE_CONSULT, TASK_STATE_CONSULTING].includes(getConsultMPCState(...))` +- **Via `getConsultMPCState`:** Line 321 — `[TASK_STATE_CONSULT_COMPLETED].includes(getConsultMPCState(...))` + +**Do NOT delete these 3 constants until `findHoldStatus` and `isConsultOnHoldMPC` are rewritten** to use SDK `TaskState` equivalents. Deleting them first will break compilation. + ## Gotcha: `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`. Do not collapse these when updating `getTaskStatus()` or any consult timer logic. From 5d29b58c40aabdeef186215a715d4d5be22cb87b Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 12 Mar 2026 21:50:11 +0530 Subject: [PATCH 08/24] fix: introduce bound-handler map pattern to resolve 4 Codex review issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 4 issues stem from the tension between needing per-task context (interactionId) and stable handler references for .off() cleanup: 1. TASK_WRAPPEDUP inline arrow — now a bound handler stored in map 2. Conference handlers unresolved interactionId — now passed as parameter from bound handler wrappers 3. handleUIControlsUpdated wrong task ID — no longer reads currentTask; uses bound interactionId from the emitting task's registration closure 4. Conference inline wrappers not detachable — replaced with stored bound handlers that handleTaskRemove retrieves from the map Pattern: taskBoundHandlers Map> - Created per-task in registerTaskEventListeners - Retrieved in handleTaskRemove for exact .off() reference matching - Deleted after cleanup to prevent memory leaks Made-with: Cursor --- .../migration/store-event-wiring-migration.md | 190 +++++++++++++----- 1 file changed, 135 insertions(+), 55 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index ab75bd9b3..17440dfec 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -177,7 +177,18 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be ## Refactor Patterns (Before/After) -### Pattern 1: `registerTaskEventListeners()` — Adding UI Controls Handler +### Architectural Note: Bound-Handler Map + +Handlers registered via `task.on()` that need per-task context (`interactionId`) **cannot** use inline arrows — `task.off()` in `handleTaskRemove` requires the exact same function reference to detach a listener. Conversely, class-level methods don't have access to the `interactionId` local variable from `registerTaskEventListeners`. + +**Solution:** Store bound handler references in a per-task map at registration time. `handleTaskRemove` retrieves them for cleanup. + +```typescript +// Class property — keyed by interactionId +private taskBoundHandlers = new Map>(); +``` + +### Pattern 1: `registerTaskEventListeners()` — Bound-Handler Registration #### Before ```typescript @@ -200,15 +211,45 @@ registerTaskEventListeners(task: ITask) { registerTaskEventListeners(task: ITask) { const interactionId = task.data.interactionId; - // NEW: Subscribe to SDK-computed UI control updates - // Use a named method (not inline arrow) so handleTaskRemove can .off() the same reference - task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, this.handleUIControlsUpdated); - - // KEEP: Task lifecycle events that need store-level management + // Create bound handlers that close over this task's interactionId. + // Stored in map so handleTaskRemove can .off() the exact same references. + const bound: Record = { + uiControlsUpdated: (uiControls: TaskUIControls) => { + this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); + }, + wrappedup: (data: unknown) => { + this.refreshTaskList(); + this.fireTaskCallbacks(TASK_EVENTS.TASK_WRAPPEDUP, interactionId, data); + }, + confStarted_participantJoined: () => this.handleConferenceStarted(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId), + confStarted_conferenceStarted: () => this.handleConferenceStarted(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId), + confEnded_conferenceEnded: () => this.handleConferenceEnded(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId), + confEnded_participantLeft: () => this.handleConferenceEnded(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId), + // Callback-only events — each bound to this task's interactionId + hold: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_HOLD, interactionId), + resume: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RESUME, interactionId), + offerContact: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONTACT, interactionId), + offerConsult: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONSULT, interactionId), + recordingPaused: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_PAUSED, interactionId), + recordingResumed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_RESUMED, interactionId), + postCallActivity: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, interactionId), + confEstablishing: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, interactionId), + confFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_FAILED, interactionId), + confEndFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, interactionId), + participantLeftFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, interactionId), + confTransferred: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, interactionId), + confTransferFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, interactionId), + }; + this.taskBoundHandlers.set(interactionId, bound); + + // NEW: SDK-computed UI control updates (bound to emitting task's interactionId) + task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, bound.uiControlsUpdated); + + // KEEP: Task lifecycle events that need store-level management (class methods — no interactionId needed) task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - task.on(TASK_EVENTS.TASK_REJECT, (reason) => this.handleTaskReject(task, reason)); - task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, (reason) => this.handleOutdialFailed(reason)); + task.on(TASK_EVENTS.TASK_REJECT, this.handleTaskReject); + task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, this.handleOutdialFailed); // KEEP + FIX WIRING: Wire handleConsultEnd (was dead code) task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); @@ -219,36 +260,32 @@ registerTaskEventListeners(task: ITask) { task.on(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted); task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); - // KEEP: Conference state management (remove refreshTaskList, keep consult state reset) - // Pass event type so handler fires only the correct callback (not both) - task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, () => this.handleConferenceStarted(TASK_EVENTS.TASK_PARTICIPANT_JOINED)); - task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, () => this.handleConferenceStarted(TASK_EVENTS.TASK_CONFERENCE_STARTED)); - task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, () => this.handleConferenceEnded(TASK_EVENTS.TASK_CONFERENCE_ENDED)); - task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, () => this.handleConferenceEnded(TASK_EVENTS.TASK_PARTICIPANT_LEFT)); + // KEEP: Conference state management — bound handlers pass event type + interactionId + task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, bound.confStarted_participantJoined); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, bound.confStarted_conferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, bound.confEnded_conferenceEnded); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, bound.confEnded_participantLeft); // KEEP: Auto-answer sets decline button state task.on(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); - // KEEP: Wrapup completion — task must be removed from list - task.on(TASK_EVENTS.TASK_WRAPPEDUP, (data) => { // renamed from AGENT_WRAPPEDUP - this.refreshTaskList(); - this.fireTaskCallbacks(TASK_EVENTS.TASK_WRAPPEDUP, interactionId, data); - }); - - // 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.TASK_OFFER_CONTACT, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONTACT, interactionId)); // renamed - task.on(TASK_EVENTS.TASK_OFFER_CONSULT, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONSULT, interactionId)); - task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_PAUSED, interactionId)); // renamed - task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_RESUMED, interactionId)); // renamed - task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, interactionId)); - task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, interactionId)); - task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_FAILED, interactionId)); - task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, interactionId)); - task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, interactionId)); - task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, interactionId)); - task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, interactionId)); + // KEEP: Wrapup completion — bound handler retains refreshTaskList + correct interactionId + task.on(TASK_EVENTS.TASK_WRAPPEDUP, bound.wrappedup); // renamed from AGENT_WRAPPEDUP + + // SIMPLIFIED: Callback-only events — all use bound handlers with correct interactionId + task.on(TASK_EVENTS.TASK_HOLD, bound.hold); + task.on(TASK_EVENTS.TASK_RESUME, bound.resume); + task.on(TASK_EVENTS.TASK_OFFER_CONTACT, bound.offerContact); // renamed + task.on(TASK_EVENTS.TASK_OFFER_CONSULT, bound.offerConsult); + task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, bound.recordingPaused); // renamed + task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, bound.recordingResumed); // renamed + task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, bound.postCallActivity); + task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, bound.confEstablishing); + task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, bound.confFailed); + task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, bound.confEndFailed); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, bound.participantLeftFailed); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, bound.confTransferred); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, bound.confTransferFailed); // Browser-only: WebRTC media setup if (this.deviceType === DEVICE_TYPE_BROWSER) { @@ -288,16 +325,23 @@ task.on(TASK_EVENTS.TASK_RESUME, () => { }); ``` -### Pattern 3: New `handleUIControlsUpdated` Method +### Pattern 3: `TASK_UI_CONTROLS_UPDATED` — Bound Handler (Not a Class Method) + +**Why not a class method?** A class-level `handleUIControlsUpdated` would need to derive `interactionId` from `this.store.currentTask`, which is wrong in multi-task/consult scenarios — the emitting task may not be the currently selected one. Using a bound handler (see Pattern 1) captures the correct `interactionId` at registration time. ```typescript -// NEW method — must be a class property (not inline arrow) so .off() can detach it +// WRONG — class method reads currentTask (may be a different task than the emitter): handleUIControlsUpdated = (uiControls: TaskUIControls) => { - const interactionId = this.store.currentTask?.data.interactionId; - if (interactionId) { - this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); - } + const interactionId = this.store.currentTask?.data.interactionId; // ← BUG in multi-task + this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); }; + +// CORRECT — bound handler from Pattern 1 captures emitting task's interactionId: +// (created in registerTaskEventListeners per task) +uiControlsUpdated: (uiControls: TaskUIControls) => { + this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); + // interactionId is from the closure: const interactionId = task.data.interactionId; +}, ``` ### Pattern 4: Conference Handler Simplification @@ -324,11 +368,11 @@ handleConferenceEnded = () => { // task.data and task.uiControls already reflect conference state. // Keep consult state reset in handleConferenceStarted; remove refreshTaskList() from both. // -// IMPORTANT: Do NOT fire both TASK_CONFERENCE_STARTED and TASK_PARTICIPANT_JOINED from one handler. -// Both events are wired to this same handler, so one incoming event would produce two different -// callback notifications. Instead, accept the event type as a parameter. +// Both eventType and interactionId are passed by the bound handlers in Pattern 1. +// This avoids: (a) dual callback firing, (b) unresolved interactionId in class scope, +// and (c) inline arrows that can't be detached by handleTaskRemove. -handleConferenceStarted = (eventType: TASK_EVENTS) => { +handleConferenceStarted = (eventType: TASK_EVENTS, interactionId: string) => { runInAction(() => { this.setIsQueueConsultInProgress(false); this.setCurrentConsultQueueId(null); @@ -337,12 +381,12 @@ handleConferenceStarted = (eventType: TASK_EVENTS) => { this.fireTaskCallbacks(eventType, interactionId); }; -handleConferenceEnded = (eventType: TASK_EVENTS) => { +handleConferenceEnded = (eventType: TASK_EVENTS, interactionId: string) => { this.fireTaskCallbacks(eventType, interactionId); }; ``` -### Pattern 5: `handleTaskRemove()` — Update Cleanup +### Pattern 5: `handleTaskRemove()` — Cleanup via Bound-Handler Map #### Before ```typescript @@ -359,15 +403,50 @@ handleTaskRemove = (taskToRemove: ITask) => { #### After ```typescript handleTaskRemove = (taskToRemove: ITask) => { - if (taskToRemove) { - // Use renamed SDK event names; add TASK_UI_CONTROLS_UPDATED; fix handler references - taskToRemove.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, this.handleUIControlsUpdated); // NEW - taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); - taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); // FIX: was refreshTaskList - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, ...); // FIX: match registered handler - // ... all other .off() calls matching their .on() counterparts exactly + if (!taskToRemove) return; + + const interactionId = taskToRemove.data.interactionId; + const bound = this.taskBoundHandlers.get(interactionId); + + // Class-method handlers — stable references, no map needed + taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); + taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); + taskToRemove.off(TASK_EVENTS.TASK_REJECT, this.handleTaskReject); + taskToRemove.off(TASK_EVENTS.TASK_OUTDIAL_FAILED, this.handleOutdialFailed); + taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); // FIX: was refreshTaskList + taskToRemove.off(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); + taskToRemove.off(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting); + taskToRemove.off(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted); + taskToRemove.off(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); + taskToRemove.off(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); + taskToRemove.off(TASK_EVENTS.TASK_MEDIA, this.handleTaskMedia); + + // Bound handlers — retrieve exact references from map for correct .off() detachment + if (bound) { + taskToRemove.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, bound.uiControlsUpdated); + taskToRemove.off(TASK_EVENTS.TASK_WRAPPEDUP, bound.wrappedup); + taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_JOINED, bound.confStarted_participantJoined); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_STARTED, bound.confStarted_conferenceStarted); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_ENDED, bound.confEnded_conferenceEnded); + taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT, bound.confEnded_participantLeft); + taskToRemove.off(TASK_EVENTS.TASK_HOLD, bound.hold); + taskToRemove.off(TASK_EVENTS.TASK_RESUME, bound.resume); + taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONTACT, bound.offerContact); + taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONSULT, bound.offerConsult); + taskToRemove.off(TASK_EVENTS.TASK_RECORDING_PAUSED, bound.recordingPaused); + taskToRemove.off(TASK_EVENTS.TASK_RECORDING_RESUMED, bound.recordingResumed); + taskToRemove.off(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, bound.postCallActivity); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, bound.confEstablishing); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_FAILED, bound.confFailed); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, bound.confEndFailed); + taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, bound.participantLeftFailed); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, bound.confTransferred); // FIX: was handleConferenceEnded + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, bound.confTransferFailed); + this.taskBoundHandlers.delete(interactionId); } + + // Reset store state + // ... existing currentTask/taskList cleanup logic }; ``` @@ -392,7 +471,8 @@ handleTaskRemove = (taskToRemove: ITask) => { - [ ] `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 -- [ ] `handleTaskRemove` unregisters all listeners correctly (no listener leaks) +- [ ] `handleTaskRemove` unregisters all listeners correctly via bound-handler map (no listener leaks) +- [ ] `taskBoundHandlers` map is cleaned up (`.delete()`) when a task is removed - [ ] `handleConsultEnd` is properly wired and resets consult state on `TASK_CONSULT_END` - [ ] `handleConsultAccepted` still registers `TASK_MEDIA` listener on consult task (browser) - [ ] `handleAutoAnswer` still sets `isDeclineButtonEnabled = true` From b91d8b78eafa85c430197ce1e8e5a751efbec83a Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 11:15:48 +0530 Subject: [PATCH 09/24] fix: add interaction-state ordering constraint, fix Pattern 2 bound handlers --- .../migration/store-event-wiring-migration.md | 14 ++++++-------- .../migration/store-task-utils-migration.md | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 17440dfec..25697f742 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -315,14 +315,12 @@ task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, () => this.refreshTaskList()); ```typescript // SDK keeps task.data in sync via state machine. // refreshTaskList() only called on initialization/hydration and TASK_WRAPPEDUP. -// 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); -}); +// Individual events use bound handlers (from taskBoundHandlers map) so +// handleTaskRemove can .off() the exact same reference. See Pattern 1. + +task.on(TASK_EVENTS.TASK_HOLD, bound.hold); +task.on(TASK_EVENTS.TASK_RESUME, bound.resume); +// ... all other callback-only events use bound.* references ``` ### Pattern 3: `TASK_UI_CONTROLS_UPDATED` — Bound Handler (Not a Class Method) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index 3494279ad..a176ba7c3 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -17,10 +17,10 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect | `TASK_STATE_CONSULT` | `store/src/constants.ts` | SDK `TaskState.CONSULT_INITIATING` — **delete ONLY AFTER rewriting `findHoldStatus`** (see ordering note below) | | `TASK_STATE_CONSULTING` | `store/src/constants.ts` | SDK `TaskState.CONSULTING` — **same ordering constraint** | | `TASK_STATE_CONSULT_COMPLETED` | `store/src/constants.ts` | SDK handles via context — **same ordering constraint** | -| `INTERACTION_STATE_WRAPUP` | `store/src/constants.ts` | SDK handles via `TaskState.WRAPPING_UP` | -| `INTERACTION_STATE_POST_CALL` | `store/src/constants.ts` | SDK handles via `TaskState.POST_CALL` | -| `INTERACTION_STATE_CONNECTED` | `store/src/constants.ts` | SDK handles via `TaskState.CONNECTED` | -| `INTERACTION_STATE_CONFERENCE` | `store/src/constants.ts` | SDK handles via `TaskState.CONFERENCING` | +| `INTERACTION_STATE_WRAPUP` | `store/src/constants.ts` | SDK `TaskState.WRAPPING_UP` — **delete ONLY AFTER rewriting `getTaskStatus`** (see ordering note below) | +| `INTERACTION_STATE_POST_CALL` | `store/src/constants.ts` | SDK `TaskState.POST_CALL` — **same ordering constraint** | +| `INTERACTION_STATE_CONNECTED` | `store/src/constants.ts` | SDK `TaskState.CONNECTED` — **same ordering constraint** | +| `INTERACTION_STATE_CONFERENCE` | `store/src/constants.ts` | SDK `TaskState.CONFERENCING` — **same ordering constraint** | | `CONSULT_STATE_INITIATED` | `store/src/constants.ts` | SDK handles via context | | `CONSULT_STATE_COMPLETED` | `store/src/constants.ts` | SDK handles via context | | `CONSULT_STATE_CONFERENCING` | `store/src/constants.ts` | SDK handles via context | @@ -46,6 +46,17 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect **Do NOT delete these 3 constants until `findHoldStatus` and `isConsultOnHoldMPC` are rewritten** to use SDK `TaskState` equivalents. Deleting them first will break compilation. +## Ordering Constraint: Interaction State Constants + +`getTaskStatus` (KEEP) depends on `INTERACTION_STATE_WRAPUP`, `INTERACTION_STATE_POST_CALL`, `INTERACTION_STATE_CONNECTED`, and `INTERACTION_STATE_CONFERENCE` extensively: +- **`isIncomingTask`:** Line 46 — `task.data.interaction.state !== INTERACTION_STATE_WRAPUP` +- **`getTaskStatus`:** Lines 56–60 — returns `INTERACTION_STATE_CONNECTED` or `INTERACTION_STATE_CONFERENCE` +- **`getTaskStatus`:** Lines 99–100 — conference state check +- **`getTaskStatus`:** Lines 105–106 — wrapup/post-call consult-completed check +- **`getConsultMPCState`:** Lines 137–139 — connected/conference branching + +**Do NOT delete these 4 constants until `getTaskStatus` and `getConsultMPCState` are rewritten** to use SDK `TaskState` equivalents. Deleting them first will break compilation. + ## Gotcha: `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`. Do not collapse these when updating `getTaskStatus()` or any consult timer logic. From 307891e442db2385748b0204aa1e493f97445feb Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 11:36:35 +0530 Subject: [PATCH 10/24] fix: bound handler for TASK_REJECT, ordering constraint for CONSULT_STATE constants --- .../migration/store-event-wiring-migration.md | 14 +++++++++----- .../migration/store-task-utils-migration.md | 16 +++++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 25697f742..2c5966109 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -214,6 +214,8 @@ registerTaskEventListeners(task: ITask) { // Create bound handlers that close over this task's interactionId. // Stored in map so handleTaskRemove can .off() the exact same references. const bound: Record = { + reject: (reason: string) => this.handleTaskReject(task, reason), + outdialFailed: (reason: string) => this.handleOutdialFailed(reason), uiControlsUpdated: (uiControls: TaskUIControls) => { this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); }, @@ -245,11 +247,13 @@ registerTaskEventListeners(task: ITask) { // NEW: SDK-computed UI control updates (bound to emitting task's interactionId) task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, bound.uiControlsUpdated); - // KEEP: Task lifecycle events that need store-level management (class methods — no interactionId needed) + // KEEP: Task lifecycle events that need store-level management (class methods) task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - task.on(TASK_EVENTS.TASK_REJECT, this.handleTaskReject); - task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, this.handleOutdialFailed); + // TASK_REJECT: handleTaskReject(task, reason) needs the emitting task reference — + // must use a bound handler, not a direct class method reference + task.on(TASK_EVENTS.TASK_REJECT, bound.reject); + task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, bound.outdialFailed); // KEEP + FIX WIRING: Wire handleConsultEnd (was dead code) task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); @@ -409,8 +413,6 @@ handleTaskRemove = (taskToRemove: ITask) => { // Class-method handlers — stable references, no map needed taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - taskToRemove.off(TASK_EVENTS.TASK_REJECT, this.handleTaskReject); - taskToRemove.off(TASK_EVENTS.TASK_OUTDIAL_FAILED, this.handleOutdialFailed); taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); // FIX: was refreshTaskList taskToRemove.off(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); taskToRemove.off(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting); @@ -421,6 +423,8 @@ handleTaskRemove = (taskToRemove: ITask) => { // Bound handlers — retrieve exact references from map for correct .off() detachment if (bound) { + taskToRemove.off(TASK_EVENTS.TASK_REJECT, bound.reject); + taskToRemove.off(TASK_EVENTS.TASK_OUTDIAL_FAILED, bound.outdialFailed); taskToRemove.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, bound.uiControlsUpdated); taskToRemove.off(TASK_EVENTS.TASK_WRAPPEDUP, bound.wrappedup); taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_JOINED, bound.confStarted_participantJoined); diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index a176ba7c3..0f7482f18 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -21,9 +21,9 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect | `INTERACTION_STATE_POST_CALL` | `store/src/constants.ts` | SDK `TaskState.POST_CALL` — **same ordering constraint** | | `INTERACTION_STATE_CONNECTED` | `store/src/constants.ts` | SDK `TaskState.CONNECTED` — **same ordering constraint** | | `INTERACTION_STATE_CONFERENCE` | `store/src/constants.ts` | SDK `TaskState.CONFERENCING` — **same ordering constraint** | -| `CONSULT_STATE_INITIATED` | `store/src/constants.ts` | SDK handles via context | -| `CONSULT_STATE_COMPLETED` | `store/src/constants.ts` | SDK handles via context | -| `CONSULT_STATE_CONFERENCING` | `store/src/constants.ts` | SDK handles via context | +| `CONSULT_STATE_INITIATED` | `store/src/constants.ts` | SDK handles via context — **delete ONLY AFTER rewriting `getConsultMPCState`** (see ordering note below) | +| `CONSULT_STATE_COMPLETED` | `store/src/constants.ts` | SDK handles via context — **same ordering constraint** | +| `CONSULT_STATE_CONFERENCING` | `store/src/constants.ts` | SDK handles via context — **same ordering constraint** | ## Constants to Keep @@ -57,6 +57,16 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect **Do NOT delete these 4 constants until `getTaskStatus` and `getConsultMPCState` are rewritten** to use SDK `TaskState` equivalents. Deleting them first will break compilation. +## Ordering Constraint: Consult State Constants (`CONSULT_STATE_*`) + +`getConsultMPCState` (used by `getTaskStatus` and `findHoldStatus`) depends on `CONSULT_STATE_INITIATED`, `CONSULT_STATE_COMPLETED`, and `CONSULT_STATE_CONFERENCING`: +- **Line 53:** `case CONSULT_STATE_INITIATED:` — returns `TASK_STATE_CONSULT` +- **Line 55:** `case CONSULT_STATE_COMPLETED:` — returns connected or consult-completed +- **Line 59:** `case CONSULT_STATE_CONFERENCING:` — returns `INTERACTION_STATE_CONFERENCE` +- **Line 107:** `consultState === CONSULT_STATE_COMPLETED` — wrapup path in `getTaskStatus` + +**Do NOT delete these 3 constants until `getConsultMPCState` is rewritten** to use SDK equivalents. Deleting them first will break compilation. + ## Gotcha: `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`. Do not collapse these when updating `getTaskStatus()` or any consult timer logic. From 708fad0bf4f2ac1936ffa2ee09fa8abcfdbbb0e3 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 12:01:14 +0530 Subject: [PATCH 11/24] fix: add feature-flag gating overlay for getControlsVisibility migration --- .../migration/store-task-utils-migration.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index 0f7482f18..ea95fe8ed 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -196,6 +196,36 @@ const controls = currentTask?.uiControls ?? getDefaultUIControls(); // All 17 controls come pre-computed from SDK. Zero store util calls needed. ``` +> **CRITICAL: Feature-Flag Gating Overlay** +> +> The old `getControlsVisibility` applied integrator-provided widget props (`featureFlags` +> and `conferenceEnabled`) that the SDK has **no knowledge of**. SDK-computed `task.uiControls` +> reflects task-state-only visibility. The widget layer **must** still overlay these gates on +> top of the SDK controls to honour integrator configuration. +> +> | Widget Prop | Controls Affected | Gate Logic | +> |-------------|-------------------|------------| +> | `featureFlags.webRtcEnabled` | accept, decline, muteUnmute, conference, muteUnmuteConsult, + telephony support (holdResume, endConsult) | Hide control when `webRtcEnabled` is `false` and channel is voice in browser | +> | `featureFlags.isEndCallEnabled` | end | Hide end button when `isEndCallEnabled` is `false` (phone device only) | +> | `featureFlags.isEndConsultEnabled` | endConsult | Hide end-consult when `isEndConsultEnabled` is `false` | +> | `conferenceEnabled` (widget prop) | conference, exitConference, mergeConference, consultTransferConsult | Hide all conference-related controls when `conferenceEnabled` is `false` | +> +> **Implementation pattern — apply after reading SDK controls:** +> ```typescript +> const sdkControls = currentTask?.uiControls ?? getDefaultUIControls(); +> +> // Overlay integrator feature-flag gates +> const controls = applyFeatureGates(sdkControls, { +> deviceType, +> featureFlags, // { webRtcEnabled, isEndCallEnabled, isEndConsultEnabled } +> conferenceEnabled, +> channelType, // voice vs digital — needed for webRtc gate +> }); +> ``` +> The `applyFeatureGates` helper is a thin function that sets `isVisible = false` +> on any control whose integrator gate is off. It does **not** re-derive state; it only +> narrows visibility that the SDK already computed. + ### Before/After: `findHoldStatus` — RETAINED (not removed) #### Before (used in controls computation and task status) @@ -309,6 +339,7 @@ export function getTaskStatus(task: ITask, agentId: string): string { - [ ] 9 deleted constants have no remaining consumers - [ ] `findHoldTimestamp` dual-signature (task vs interaction) not confused during migration - [ ] `task-util.ts` `getControlsVisibility` + 22 visibility functions fully deleted +- [ ] Feature-flag overlay (`applyFeatureGates`) preserves `webRtcEnabled`, `isEndCallEnabled`, `isEndConsultEnabled`, and `conferenceEnabled` gating on top of SDK controls --- From 7364f9d11293d615cf7c71df794970d5ce2d31ee Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 12:30:17 +0530 Subject: [PATCH 12/24] fix: preserve getTaskStatus return contract; add task-layer TASK_EVENTS to migration --- .../migration/store-event-wiring-migration.md | 1 + .../migration/store-task-utils-migration.md | 47 +++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 2c5966109..654a2a4ac 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -461,6 +461,7 @@ handleTaskRemove = (taskToRemove: ITask) => { | `store/src/storeEventsWrapper.ts` | Refactor `registerTaskEventListeners` (see definitive table), update `handleTaskRemove` (fix listener mismatches + add `TASK_UI_CONTROLS_UPDATED`), simplify handlers (remove `refreshTaskList()` from all except `TASK_WRAPPEDUP`), wire `handleConsultEnd` to `TASK_CONSULT_END` | | `store/src/store.ts` | No changes expected (observables stay) | | `store/src/store.types.ts` | Delete local `TASK_EVENTS` enum; import from SDK (which includes `TASK_UI_CONTROLS_UPDATED`) | +| **Task-layer consumers of `TASK_EVENTS`** | **Must be updated in the same step** so that removing the store’s local enum does not break the build. `task/src/helper.ts` imports `TASK_EVENTS` from `@webex/cc-store` and uses legacy names: `AGENT_WRAPPEDUP`, `CONTACT_RECORDING_PAUSED`, `CONTACT_RECORDING_RESUMED` (and `TASK_RECORDING_PAUSED` / `TASK_RECORDING_RESUMED` in setTaskCallback). Replace with SDK event names: `TASK_WRAPPEDUP`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED` in both `setTaskCallback` and `removeTaskCallback`. Either update `task/src/helper.ts` (and any other task files using `TASK_EVENTS`) in this PR or sequence the migration so store switches to SDK enum only after task package is updated. | | `store/tests/*` | Update tests for renamed events, new `TASK_UI_CONTROLS_UPDATED` handler, simplified handlers | --- diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index ea95fe8ed..9bcaeaae1 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -283,31 +283,40 @@ export function getTaskStatus(task: ITask, agentId: string): string { // getConsultStatus() calls getTaskStatus() and maps the string to ConsultStatus enum values ``` -#### After (rewritten to use SDK controls) +#### After (rewritten to use SDK controls — preserve return contract) ```typescript // store/task-utils.ts — rewritten to use task.uiControls -// NOTE: getConsultStatus() is deleted, so getTaskStatus() no longer needs to produce -// values that feed into ConsultStatus. It becomes a pure display-status function. +// CONTRACT: getTaskStatus is barrel-exported from @webex/cc-store. Downstream consumers +// may compare return values to INTERACTION_STATE_* / TASK_STATE_* constants. Preserve the +// existing machine-readable return values; do NOT switch to display labels here. +// Display labels ('Wrap Up', 'Conference', etc.) are a UI concern — implement via a +// separate helper or mapping layer if needed. export function getTaskStatus(task: ITask, agentId: string): string { + const interaction = task.data.interaction; 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'; - - // 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) return interaction?.state ?? ''; + // EP-DN secondary agent (same as Before) + if (isSecondaryEpDnAgent(task)) { + if (interaction.state === INTERACTION_STATE_CONFERENCE) return INTERACTION_STATE_CONFERENCE; + return TASK_STATE_CONSULTING; + } + // Wrapup / post-call with consult completed (same as Before) + if ( + (interaction.state === INTERACTION_STATE_WRAPUP || interaction.state === INTERACTION_STATE_POST_CALL) && + interaction.participants[agentId]?.consultState === CONSULT_STATE_COMPLETED + ) { + return TASK_STATE_CONSULT_COMPLETED; + } - if (controls.end.isVisible) return 'Connected'; - if (controls.accept.isVisible) return 'Offered'; // NEW: not in old version - return 'Unknown'; // NEW: safe default + // Map from uiControls to same constant values as old getConsultMPCState / getTaskStatus + if (controls.wrapup.isVisible) return INTERACTION_STATE_WRAPUP; + if (controls.endConsult.isVisible) return TASK_STATE_CONSULTING; + if (controls.exitConference.isVisible) return INTERACTION_STATE_CONFERENCE; + if (findHoldStatus(task, 'mainCall', agentId)) return INTERACTION_STATE_CONNECTED; // held → connected + if (controls.end.isVisible) return INTERACTION_STATE_CONNECTED; + if (controls.accept.isVisible) return interaction?.state ?? 'new'; // offered + return getConsultMPCState(task, agentId); // fallback preserves legacy behaviour } -// NOTE: New states 'Offered' and 'Unknown' are additions not present in old code. -// EP-DN secondary agent handling and consultState-based wrapup may need review — -// verify that task.uiControls correctly handles these edge cases in SDK. ``` --- From d575a7b690e1feed2af50aa5f40c1e1d57f9f63d Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 14:11:16 +0530 Subject: [PATCH 13/24] fix: authoritative table bound handler, parent link note, getConsultStatus dependency direction --- .../ai-docs/migration/store-event-wiring-migration.md | 4 ++-- .../ai-docs/migration/store-task-utils-migration.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 654a2a4ac..35740f097 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -138,7 +138,7 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be | 3 | `TASK_REJECT` | `handleTaskReject` | **Keep** | Remove from task list | | 4 | `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | **Keep** | Remove from task list | | 5 | `TASK_MEDIA` | `handleTaskMedia` | **Keep** | Browser-only WebRTC setup (conditional registration) | -| 6 | `TASK_UI_CONTROLS_UPDATED` | `handleUIControlsUpdated` | **Add new** | Fire callbacks to trigger widget re-renders | +| 6 | `TASK_UI_CONTROLS_UPDATED` | `bound.uiControlsUpdated` (per-task; see Pattern 1 & 3) | **Add new** | Fire callbacks to trigger widget re-renders. Do **not** use a class-level handler — it would resolve the wrong `interactionId` in multi-task scenarios. | | 7 | `TASK_WRAPPEDUP` | `handleWrappedup` | **Keep + rename** | Was `AGENT_WRAPPEDUP`. Keep `refreshTaskList()` — task must be removed from list after wrapup. Fire callback. | | 8 | `TASK_CONSULT_END` | `handleConsultEnd` | **Fix wiring** | Wire the existing (currently dead) `handleConsultEnd` method. Resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`. Remove `refreshTaskList()`. Fire callback. | | 9 | `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | **Simplify** | Keep consult state reset. Remove `refreshTaskList()`. Fire callback. | @@ -483,4 +483,4 @@ handleTaskRemove = (taskToRemove: ITask) => { --- -_Parent: [migration-overview.md](./migration-overview.md)_ +_Parent: [migration-overview.md](./migration-overview.md) — overview doc is added in PR 1/4; link resolves once that PR is merged._ diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index 9bcaeaae1..bf00ddb11 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -112,7 +112,7 @@ Two different `findHoldTimestamp` functions exist with different signatures: | # | Function | Reason | SDK Replacement | Impact on Other Functions | |---|----------|--------|-----------------|--------------------------| -| 1 | `getConsultStatus(task, agentId)` | Primary consumer `getControlsVisibility` is deleted | `task.uiControls` encodes all consult control states | `getTaskStatus()` calls this — must be updated (see After code below) | +| 1 | `getConsultStatus(task, agentId)` | Primary consumer `getControlsVisibility` is deleted | `task.uiControls` encodes all consult control states | `getConsultStatus()` **calls** `getTaskStatus()` (not the reverse). When we delete `getConsultStatus`, update `getTaskStatus` to use `task.uiControls` (see After code below). | | 2 | `getIsConferenceInProgress(task)` | `task-util.ts` already uses `task?.data?.isConferenceInProgress` directly; function only used in tests | `task.uiControls.exitConference.isVisible` | None | | 3 | `getConferenceParticipantsCount(task)` | Used only in `getControlsVisibility` | SDK computes max participant check internally | None | | 4 | `getIsCustomerInCall(task)` | Used only in `getControlsVisibility` | SDK computes internally | None | From df026d84d59cd328432f1ed5fbf910082b860f87 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 14:26:56 +0530 Subject: [PATCH 14/24] fix: preserve legacy getTaskStatus when uiControls missing; use getConsultMPCState for endConsult --- .../migration/store-task-utils-migration.md | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index bf00ddb11..eae17a8de 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -294,7 +294,23 @@ export function getTaskStatus(task: ITask, agentId: string): string { export function getTaskStatus(task: ITask, agentId: string): string { const interaction = task.data.interaction; const controls = task.uiControls; - if (!controls) return interaction?.state ?? ''; + + // When uiControls is missing (hydration/race), run full legacy path so EP-DN and + // consult-completed derivation are preserved; avoid falling back to raw interaction.state only. + if (!controls) { + if (isSecondaryEpDnAgent(task)) { + if (interaction.state === INTERACTION_STATE_CONFERENCE) return INTERACTION_STATE_CONFERENCE; + return TASK_STATE_CONSULTING; + } + if ( + (interaction.state === INTERACTION_STATE_WRAPUP || interaction.state === INTERACTION_STATE_POST_CALL) && + interaction.participants[agentId]?.consultState === CONSULT_STATE_COMPLETED + ) { + return TASK_STATE_CONSULT_COMPLETED; + } + return getConsultMPCState(task, agentId); + } + // EP-DN secondary agent (same as Before) if (isSecondaryEpDnAgent(task)) { if (interaction.state === INTERACTION_STATE_CONFERENCE) return INTERACTION_STATE_CONFERENCE; @@ -310,7 +326,9 @@ export function getTaskStatus(task: ITask, agentId: string): string { // Map from uiControls to same constant values as old getConsultMPCState / getTaskStatus if (controls.wrapup.isVisible) return INTERACTION_STATE_WRAPUP; - if (controls.endConsult.isVisible) return TASK_STATE_CONSULTING; + // endConsult.isVisible is true for both consult-initiating and consulting; use getConsultMPCState + // so we return TASK_STATE_CONSULT vs TASK_STATE_CONSULTING correctly (see gotcha in this doc). + if (controls.endConsult.isVisible) return getConsultMPCState(task, agentId); if (controls.exitConference.isVisible) return INTERACTION_STATE_CONFERENCE; if (findHoldStatus(task, 'mainCall', agentId)) return INTERACTION_STATE_CONNECTED; // held → connected if (controls.end.isVisible) return INTERACTION_STATE_CONNECTED; From 585851545dd3f63f17f655339bb7be3b00ba4dc8 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 14:43:58 +0530 Subject: [PATCH 15/24] fix: point TASK_WRAPPEDUP in authoritative table to bound.wrappedup --- .../ai-docs/migration/store-event-wiring-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 35740f097..a64c75805 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -139,7 +139,7 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be | 4 | `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | **Keep** | Remove from task list | | 5 | `TASK_MEDIA` | `handleTaskMedia` | **Keep** | Browser-only WebRTC setup (conditional registration) | | 6 | `TASK_UI_CONTROLS_UPDATED` | `bound.uiControlsUpdated` (per-task; see Pattern 1 & 3) | **Add new** | Fire callbacks to trigger widget re-renders. Do **not** use a class-level handler — it would resolve the wrong `interactionId` in multi-task scenarios. | -| 7 | `TASK_WRAPPEDUP` | `handleWrappedup` | **Keep + rename** | Was `AGENT_WRAPPEDUP`. Keep `refreshTaskList()` — task must be removed from list after wrapup. Fire callback. | +| 7 | `TASK_WRAPPEDUP` | `bound.wrappedup` (per-task; see Pattern 1) | **Keep + rename** | Was `AGENT_WRAPPEDUP`. Keep `refreshTaskList()` in handler — task must be removed from list after wrapup. Fire callback. Do **not** use class method; use bound handler for correct `.off()` teardown. | | 8 | `TASK_CONSULT_END` | `handleConsultEnd` | **Fix wiring** | Wire the existing (currently dead) `handleConsultEnd` method. Resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`. Remove `refreshTaskList()`. Fire callback. | | 9 | `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | **Simplify** | Keep consult state reset. Remove `refreshTaskList()`. Fire callback. | | 10 | `TASK_CONSULTING` | `handleConsulting` | **Simplify** | Keep `setConsultStartTimeStamp(Date.now())`. Remove `refreshTaskList()`. Fire callback. | From ef05f5aa0beed0e4ce558fcac78a01075326d6d8 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Fri, 13 Mar 2026 15:01:20 +0530 Subject: [PATCH 16/24] fix: add transfer and mergeConferenceConsult to feature-gate overlay table --- .../ai-docs/migration/store-task-utils-migration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index eae17a8de..b33519913 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -205,10 +205,10 @@ const controls = currentTask?.uiControls ?? getDefaultUIControls(); > > | Widget Prop | Controls Affected | Gate Logic | > |-------------|-------------------|------------| -> | `featureFlags.webRtcEnabled` | accept, decline, muteUnmute, conference, muteUnmuteConsult, + telephony support (holdResume, endConsult) | Hide control when `webRtcEnabled` is `false` and channel is voice in browser | +> | `featureFlags.webRtcEnabled` | accept, decline, muteUnmute, conference, muteUnmuteConsult, **transfer** (browser: `isTransferVisibility`), + telephony support (holdResume, endConsult) | Hide control when `webRtcEnabled` is `false` and channel is voice in browser | > | `featureFlags.isEndCallEnabled` | end | Hide end button when `isEndCallEnabled` is `false` (phone device only) | > | `featureFlags.isEndConsultEnabled` | endConsult | Hide end-consult when `isEndConsultEnabled` is `false` | -> | `conferenceEnabled` (widget prop) | conference, exitConference, mergeConference, consultTransferConsult | Hide all conference-related controls when `conferenceEnabled` is `false` | +> | `conferenceEnabled` (widget prop) | conference, exitConference, mergeConference, **mergeConferenceConsult**, consultTransferConsult | Hide all conference-related controls when `conferenceEnabled` is `false` | > > **Implementation pattern — apply after reading SDK controls:** > ```typescript From e1109be968804b9a32f2a8c54637a280b8bfdf93 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 17 Mar 2026 11:32:00 +0530 Subject: [PATCH 17/24] docs(migration): derived-state consumers and webRtc gate map --- .../ai-docs/migration/store-task-utils-migration.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index b33519913..e4e5d66b9 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -196,6 +196,10 @@ const controls = currentTask?.uiControls ?? getDefaultUIControls(); // All 17 controls come pre-computed from SDK. Zero store util calls needed. ``` +> **CRITICAL: Migrate derived state consumers before removing getControlsVisibility** +> +> Many consumers still depend on **derived booleans** that are not part of SDK `uiControls` (e.g. `controlVisibility.consultCallHeld`, `isConsultInitiated` in `task/src/Utils/timer-utils.ts`; `isHeld`, `isConferenceInProgress` in `cc-components/.../CallControl*`). Do **not** follow the replacement above literally without migrating those consumers in the same step: either pass derived values from parent (e.g. `findHoldStatus(task, 'mainCall', agentId)` for `isHeld`) or move derivation to the hook/layer that owns the control. Otherwise removing `getControlsVisibility` / `ControlVisibility` will break timers and button behavior. + > **CRITICAL: Feature-Flag Gating Overlay** > > The old `getControlsVisibility` applied integrator-provided widget props (`featureFlags` @@ -205,7 +209,7 @@ const controls = currentTask?.uiControls ?? getDefaultUIControls(); > > | Widget Prop | Controls Affected | Gate Logic | > |-------------|-------------------|------------| -> | `featureFlags.webRtcEnabled` | accept, decline, muteUnmute, conference, muteUnmuteConsult, **transfer** (browser: `isTransferVisibility`), + telephony support (holdResume, endConsult) | Hide control when `webRtcEnabled` is `false` and channel is voice in browser | +> | `featureFlags.webRtcEnabled` | accept, decline, muteUnmute, conference, muteUnmuteConsult, **transfer** (browser: `isTransferVisibility`), **consult**, **recording** (pause/resume), + telephony support (holdResume, endConsult). (Old logic: `telephonySupported` from `webRtcEnabled` drives `getConsultButtonVisibility`, `getPauseResumeRecordingButtonVisibility` in task-util.) | Hide control when `webRtcEnabled` is `false` and channel is voice in browser | > | `featureFlags.isEndCallEnabled` | end | Hide end button when `isEndCallEnabled` is `false` (phone device only) | > | `featureFlags.isEndConsultEnabled` | endConsult | Hide end-consult when `isEndConsultEnabled` is `false` | > | `conferenceEnabled` (widget prop) | conference, exitConference, mergeConference, **mergeConferenceConsult**, consultTransferConsult | Hide all conference-related controls when `conferenceEnabled` is `false` | From e64d50f0d776b692a709559cffd7ce02962bd02b Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 17 Mar 2026 14:41:05 +0530 Subject: [PATCH 18/24] docs(migration): preserve media-type constant for findHoldStatus consult --- .../ai-docs/migration/store-task-utils-migration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index e4e5d66b9..af5e9df2f 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -14,8 +14,8 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect |--------|------|--------| | Local `TASK_EVENTS` enum | `store/src/store.types.ts` | SDK exports this — delete local copy (covered in detail in [store-event-wiring-migration.md](./store-event-wiring-migration.md)) | | `ConsultStatus` enum | `store/src/store.types.ts` | All consumers (`getConsultStatus`, `getControlsVisibility`) are being removed | -| `TASK_STATE_CONSULT` | `store/src/constants.ts` | SDK `TaskState.CONSULT_INITIATING` — **delete ONLY AFTER rewriting `findHoldStatus`** (see ordering note below) | -| `TASK_STATE_CONSULTING` | `store/src/constants.ts` | SDK `TaskState.CONSULTING` — **same ordering constraint** | +| `TASK_STATE_CONSULT` | `store/src/constants.ts` | **Dual use:** (1) Task state → SDK `TaskState.CONSULT_INITIATING`. (2) **Media-type sentinel** in `findHoldStatus(task, mType, agentId)` — line 328 uses `mType === TASK_STATE_CONSULT` to identify the consult *leg*, not task state. **Delete ONLY AFTER rewriting `findHoldStatus`**; when rewriting, preserve a media-type constant (or use media-type enums) for the consult leg — do **not** replace that branch with `TaskState`. See ordering note below. | +| `TASK_STATE_CONSULTING` | `store/src/constants.ts` | SDK `TaskState.CONSULTING` — **same ordering constraint** (used only as task state, not as mType) | | `TASK_STATE_CONSULT_COMPLETED` | `store/src/constants.ts` | SDK handles via context — **same ordering constraint** | | `INTERACTION_STATE_WRAPUP` | `store/src/constants.ts` | SDK `TaskState.WRAPPING_UP` — **delete ONLY AFTER rewriting `getTaskStatus`** (see ordering note below) | | `INTERACTION_STATE_POST_CALL` | `store/src/constants.ts` | SDK `TaskState.POST_CALL` — **same ordering constraint** | @@ -44,7 +44,7 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect - **Via `isConsultOnHoldMPC`:** Line 303 — `[TASK_STATE_CONSULT, TASK_STATE_CONSULTING].includes(getConsultMPCState(...))` - **Via `getConsultMPCState`:** Line 321 — `[TASK_STATE_CONSULT_COMPLETED].includes(getConsultMPCState(...))` -**Do NOT delete these 3 constants until `findHoldStatus` and `isConsultOnHoldMPC` are rewritten** to use SDK `TaskState` equivalents. Deleting them first will break compilation. +**Do NOT delete these 3 constants until `findHoldStatus` and `isConsultOnHoldMPC` are rewritten** to use SDK `TaskState` equivalents. Deleting them first will break compilation. When rewriting `findHoldStatus`, note that `TASK_STATE_CONSULT` is used there as a **media-type** sentinel (`mType === TASK_STATE_CONSULT`); preserve a media-type constant or media-type enum for the consult leg — do not replace that comparison with `TaskState`. ## Ordering Constraint: Interaction State Constants From 5149061382fba6115529c973e092ab98b038d186 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 17 Mar 2026 15:10:21 +0530 Subject: [PATCH 19/24] docs(migration): do not gate consultTransferConsult on conferenceEnabled --- .../ai-docs/migration/store-task-utils-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index af5e9df2f..b9d222ec0 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -212,7 +212,7 @@ const controls = currentTask?.uiControls ?? getDefaultUIControls(); > | `featureFlags.webRtcEnabled` | accept, decline, muteUnmute, conference, muteUnmuteConsult, **transfer** (browser: `isTransferVisibility`), **consult**, **recording** (pause/resume), + telephony support (holdResume, endConsult). (Old logic: `telephonySupported` from `webRtcEnabled` drives `getConsultButtonVisibility`, `getPauseResumeRecordingButtonVisibility` in task-util.) | Hide control when `webRtcEnabled` is `false` and channel is voice in browser | > | `featureFlags.isEndCallEnabled` | end | Hide end button when `isEndCallEnabled` is `false` (phone device only) | > | `featureFlags.isEndConsultEnabled` | endConsult | Hide end-consult when `isEndConsultEnabled` is `false` | -> | `conferenceEnabled` (widget prop) | conference, exitConference, mergeConference, **mergeConferenceConsult**, consultTransferConsult | Hide all conference-related controls when `conferenceEnabled` is `false` | +> | `conferenceEnabled` (widget prop) | conference, exitConference, mergeConference, **mergeConferenceConsult** | Hide all conference-related controls when `conferenceEnabled` is `false`. **Do not** gate `consultTransferConsult` on `conferenceEnabled` — current code does not; gating it would regress consult-transfer-consult in tenants with conference disabled. | > > **Implementation pattern — apply after reading SDK controls:** > ```typescript From 1b774cb205ae5867327b997888a463313b5de93f Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 17 Mar 2026 15:29:55 +0530 Subject: [PATCH 20/24] docs(migration): use state-based signal for getIsConferenceInProgress --- .../ai-docs/migration/store-task-utils-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index b9d222ec0..d12b3e166 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -113,7 +113,7 @@ Two different `findHoldTimestamp` functions exist with different signatures: | # | Function | Reason | SDK Replacement | Impact on Other Functions | |---|----------|--------|-----------------|--------------------------| | 1 | `getConsultStatus(task, agentId)` | Primary consumer `getControlsVisibility` is deleted | `task.uiControls` encodes all consult control states | `getConsultStatus()` **calls** `getTaskStatus()` (not the reverse). When we delete `getConsultStatus`, update `getTaskStatus` to use `task.uiControls` (see After code below). | -| 2 | `getIsConferenceInProgress(task)` | `task-util.ts` already uses `task?.data?.isConferenceInProgress` directly; function only used in tests | `task.uiControls.exitConference.isVisible` | None | +| 2 | `getIsConferenceInProgress(task)` | `task-util.ts` already uses `task?.data?.isConferenceInProgress` directly; function only used in tests | **State-based:** `task?.data?.isConferenceInProgress`. Do **not** use `task.uiControls.exitConference.isVisible` — that is control visibility and can be false when `conferenceEnabled` hides the button while the task is still in conference, causing false negatives. | None | | 3 | `getConferenceParticipantsCount(task)` | Used only in `getControlsVisibility` | SDK computes max participant check internally | None | | 4 | `getIsCustomerInCall(task)` | Used only in `getControlsVisibility` | SDK computes internally | None | | 5 | `getIsConsultInProgress(task)` | Used only in `getControlsVisibility` | SDK computes internally | None | From 4a6b3cdf6a61e94261cfb4a2c0036c4bafbefbbb Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Tue, 17 Mar 2026 22:16:12 +0530 Subject: [PATCH 21/24] docs: store event wiring migration review fixes, add CAI-7758 for test gaps --- .../migration/store-event-wiring-migration.md | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index a64c75805..aadfbfb42 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -29,6 +29,8 @@ The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-le | `TASK_PAUSE` | `'task:pause'` | No SDK equivalent; SDK uses `TASK_HOLD` | | `AGENT_CONTACT_ASSIGNED` | `'AgentContactAssigned'` | SDK uses `TASK_ASSIGNED` (`'task:assigned'`) | +**Docs to update when migrating event names:** CallControl widget spec references `TASK_CONSULT` (and related consult flow) in its sequence diagram — see `packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md` (consult sequence around line 169). Update that doc to use SDK event names and remove references to store-only enum members (`TASK_CONSULT`, `TASK_UNHOLD`) once the migration is applied. + ### New SDK Events (not in current widget enum) | SDK Event | Value | Widget Action Needed | @@ -43,11 +45,11 @@ The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-le | `TASK_TRANSFER_CONFERENCE` | `'task:transferConference'` | Evaluate for conference flow | | `TASK_CLEANUP` | `'task:cleanup'` | SDK internal — likely no widget action | -**Action:** Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from SDK instead. SDK's `TASK_EVENTS` already includes all needed events including `TASK_UI_CONTROLS_UPDATED`. +**Action:** Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from SDK once the SDK exports it. **As of this migration, the SDK does not yet export `TASK_EVENTS` from its package entry point** (or status is TBD). For current status and the plan to replace the local enum once the SDK exports it, see [migration-overview.md](./migration-overview.md) → "SDK Pending Exports" (or equivalent section). Until then, keep the local enum and align event string values with the SDK where needed. ### Pre-existing Bug: Event Name Mismatches -The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. During migration, replace the entire local enum with SDK's exported `TASK_EVENTS` enum. +The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. During migration, **when the SDK exports `TASK_EVENTS`**, replace the entire local enum with the SDK's exported enum. Until then, update local enum values to match SDK event strings so wiring is correct. --- @@ -114,9 +116,13 @@ These live in `store/src/store.ts` and are mutated via setters in `storeEventsWr **Bug 1: `handleConsultEnd` is dead code.** A `handleConsultEnd` method exists (resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`) but `TASK_CONSULT_END` is wired to `refreshTaskList()` instead. The method's consult state cleanup never runs. +**Migration / test plan for Bug 1:** When wiring `TASK_CONSULT_END` to `handleConsultEnd`, update store unit tests (e.g. `store/tests/storeEventsWrapper.ts` or equivalent) that reference `handleConsultEnd`: assert that `TASK_CONSULT_END` triggers the handler and that consult state is reset. Remove or rewrite tests that only covered the old dead path (e.g. tests that assumed `TASK_CONSULT_END` only triggered `refreshTaskList`). + **Bug 2: `handleTaskRemove` listener mismatch.** `registerTaskEventListeners` wires `TASK_CONFERENCE_TRANSFERRED → this.refreshTaskList`. But `handleTaskRemove` calls `taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.handleConferenceEnded)` — wrong handler reference. This listener is **never actually removed**, causing a listener leak. +**Test gap:** `TASK_CONFERENCE_TRANSFERRED` currently has **no unit test** (registration and cleanup). This gap is tracked by a dedicated Jira ticket. The ticket covers: adding UTs that assert this event is registered in `registerTaskEventListeners` and correctly unregistered in `handleTaskRemove`. When implementing the store migration or the ticket, add tests so this gap is closed. *(Jira: [CAI-7758](https://jira-eng-sjc12.cisco.com/jira/browse/CAI-7758))* + --- ## New Approach @@ -173,6 +179,23 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be - Full page refresh recovery - `TASK_WRAPPEDUP` (task must be removed from list — may be replaceable with explicit list removal) +#### Why we can remove most `refreshTaskList()` + +1. **SDK keeps the same task reference up to date.** The state machine updates `task.data` (and `task.uiControls`) on the **same** `ITask` reference already held in the store's `taskList`. No re-fetch is needed for in-place updates. +2. **Widget re-renders are driven by callbacks.** Widgets that registered via `setTaskCallback(event, cb, taskId)` are notified when the store calls `fireTaskCallbacks(event, interactionId, payload)`. Those callbacks cause the widget to re-run and re-render with the updated task from the store. +3. **Re-fetch is only needed when the list itself changes.** We keep `refreshTaskList()` only where the **list** must change: e.g. initial load, full refresh, or `TASK_WRAPPEDUP` (task removed from the list). For all other events, the existing task reference is already updated by the SDK, and `fireTaskCallbacks` triggers UI updates. + +--- + +### fireTaskCallbacks — Definition + +**Purpose:** Invoke all callbacks that were registered for a given task event via `setTaskCallback(event, cb, taskId)` (or equivalent), so widgets can re-render or react when that event occurs. + +**Signature (conceptual):** +`fireTaskCallbacks(event: TASK_EVENTS, interactionId: string, payload?: unknown): void` + +**Where it lives:** In the store-events layer — `packages/contact-center/store/src/storeEventsWrapper.ts`. After the migration, "After" handlers call `fireTaskCallbacks(...)` instead of (or in addition to) `refreshTaskList()` for events that only need to notify widgets. Implementation pattern: look up callbacks by event and optional `taskId`/`interactionId`, then invoke each with the payload. + --- ## Refactor Patterns (Before/After) From 9ff04cbed464bbf207f88c6cbd6571af657da4ec Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 18 Mar 2026 13:58:28 +0530 Subject: [PATCH 22/24] =?UTF-8?q?docs(migration):=20PR=20#646=20review=20f?= =?UTF-8?q?ixes=20=E2=80=94=20refreshTaskList,=20TASK=5FEVENTS,=20task=20u?= =?UTF-8?q?tils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Event wiring: when refreshTaskList() is required; remove call not handler; no write-back - fireTaskCallbacks note; future refreshTaskList(task?) - TASK_EVENTS: SDK exports from package index; delete local enum, import from @webex/contact-center - Task utils: task as source of truth; keep findHoldStatus/findHoldTimestamp until SDK equivalent - Constants/consult alias and pre-migration checks Made-with: Cursor --- .../migration/store-event-wiring-migration.md | 44 ++++++++++++------- .../migration/store-task-utils-migration.md | 20 +++++++-- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index aadfbfb42..83d0baea1 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -45,11 +45,11 @@ The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-le | `TASK_TRANSFER_CONFERENCE` | `'task:transferConference'` | Evaluate for conference flow | | `TASK_CLEANUP` | `'task:cleanup'` | SDK internal — likely no widget action | -**Action:** Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from SDK once the SDK exports it. **As of this migration, the SDK does not yet export `TASK_EVENTS` from its package entry point** (or status is TBD). For current status and the plan to replace the local enum once the SDK exports it, see [migration-overview.md](./migration-overview.md) → "SDK Pending Exports" (or equivalent section). Until then, keep the local enum and align event string values with the SDK where needed. +**Action:** The SDK **exports `TASK_EVENTS`** from its package entry point (`packages/@webex/contact-center/src/index.ts`): `export {TASK_EVENTS} from './services/task/types';` and `export type {TASK_EVENTS as TaskEvents} from './services/task/types';`. Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from the SDK: `import { TASK_EVENTS } from '@webex/contact-center';`. If the widgets repo currently depends on an older SDK version that does not re-export `TASK_EVENTS` from the package index, keep the local enum and align event string values with the SDK until the dependency is updated. ### Pre-existing Bug: Event Name Mismatches -The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. During migration, **when the SDK exports `TASK_EVENTS`**, replace the entire local enum with the SDK's exported enum. Until then, update local enum values to match SDK event strings so wiring is correct. +The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. The SDK now exports `TASK_EVENTS`; replace the entire local enum with the SDK import and use SDK event names (e.g. `TASK_WRAPPEDUP`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED`) everywhere. --- @@ -135,7 +135,7 @@ A `handleConsultEnd` method exists (resets `isQueueConsultInProgress`, `currentC ### Definitive New Event Registration -Many events that currently trigger `refreshTaskList()` will no longer need it because `task.data` is kept in sync by the SDK. Below is the single authoritative table for all event handler changes: +Many events that currently trigger `refreshTaskList()` will no longer need it because `task.data` is kept in sync by the SDK. For rows marked **Simplify**, we **remove the call to `refreshTaskList()`** from the handler; the **handler is not removed** — it is kept and only fires callbacks so widgets re-render. Below is the single authoritative table for all event handler changes: | # | Event | New Handler | Change | Detail | |---|-------|-------------|--------|--------| @@ -158,10 +158,10 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be | 17 | `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | **Simplify** | Same as #16 | | 18 | `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | **Simplify** | Remove `refreshTaskList()`. Fire callback. | | 19 | `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | **Simplify** | Same as #18 | -| 20 | `TASK_HOLD` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 21 | `TASK_RESUME` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 22 | `TASK_RECORDING_PAUSED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_PAUSED`. | -| 23 | `TASK_RECORDING_RESUMED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_RESUMED`. | +| 20 | `TASK_HOLD` | Fire callback only (e.g. `bound.hold`) | **Simplify** | **Remove only `refreshTaskList()`** from the handler; keep the handler and fire callbacks. | +| 21 | `TASK_RESUME` | Fire callback only (e.g. `bound.resume`) | **Simplify** | **Remove only `refreshTaskList()`** from the handler; keep the handler and fire callbacks. | +| 22 | `TASK_RECORDING_PAUSED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_PAUSED`. Remove `refreshTaskList()`; keep handler, fire callback. | +| 23 | `TASK_RECORDING_RESUMED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_RESUMED`. Remove `refreshTaskList()`; keep handler, fire callback. | | 24 | `TASK_POST_CALL_ACTIVITY` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | | 25 | `TASK_CONFERENCE_ESTABLISHING` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | | 26 | `TASK_CONFERENCE_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | @@ -174,16 +174,27 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be **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 -- `TASK_WRAPPEDUP` (task must be removed from list — may be replaceable with explicit list removal) +**New:** SDK keeps `task.data` updated via state machine actions. The store can read `task.data` directly instead of re-fetching. For most events we **remove only the call to `refreshTaskList()`** from the handler; the **handler itself is kept** and becomes "fire callback only" (e.g. bound hold/resume handlers). -#### Why we can remove most `refreshTaskList()` +#### When `refreshTaskList()` is still required + +**`refreshTaskList()` remains required** for these cases only: + +1. **Initial load / hydration** — populate the store's task list when the app or CC session loads. +2. **Full page refresh recovery** — re-sync the list after a full page reload. +3. **`TASK_WRAPPEDUP`** — the task must be removed from the list after wrap-up completion; today that is done by calling `refreshTaskList()` (or may be replaced later with explicit list removal). + +For all other events (e.g. `TASK_HOLD`, `TASK_RESUME`, `TASK_RECORDING_PAUSED`, consult/conference lifecycle), **do not** call `refreshTaskList()`. The handler stays; it only fires callbacks so widgets re-render. + +#### Why we can remove most `refreshTaskList()` calls 1. **SDK keeps the same task reference up to date.** The state machine updates `task.data` (and `task.uiControls`) on the **same** `ITask` reference already held in the store's `taskList`. No re-fetch is needed for in-place updates. -2. **Widget re-renders are driven by callbacks.** Widgets that registered via `setTaskCallback(event, cb, taskId)` are notified when the store calls `fireTaskCallbacks(event, interactionId, payload)`. Those callbacks cause the widget to re-run and re-render with the updated task from the store. -3. **Re-fetch is only needed when the list itself changes.** We keep `refreshTaskList()` only where the **list** must change: e.g. initial load, full refresh, or `TASK_WRAPPEDUP` (task removed from the list). For all other events, the existing task reference is already updated by the SDK, and `fireTaskCallbacks` triggers UI updates. +2. **Widgets get the latest data by re-reading from the store.** When the store calls `fireTaskCallbacks(event, interactionId, payload)`, widgets that registered via `setTaskCallback(event, cb, taskId)` are notified. Those callbacks cause the widget to re-run and **re-read the same task from the store** (e.g. `store.taskList[interactionId]` or `store.currentTask`). The task reference has already been updated in place by the SDK, so **no write-back of task data into the store is needed** — and `fireTaskCallbacks` does not write task data; it only invokes callbacks. +3. **Re-fetch is only needed when the list itself changes.** We keep `refreshTaskList()` only where the **list** must change: initial load, full refresh, or `TASK_WRAPPEDUP` (task removed from the list). For all other events, the existing task reference is already updated by the SDK, and `fireTaskCallbacks` triggers UI updates. + +#### Future consideration: optional single-task refresh + +As an enhancement, `refreshTaskList()` could be extended to accept an optional task (or `interactionId`) and update only that task in the store instead of re-fetching the full list (e.g. for wrap-up or other single-task updates). This is not required for the current migration. --- @@ -196,6 +207,9 @@ Many events that currently trigger `refreshTaskList()` will no longer need it be **Where it lives:** In the store-events layer — `packages/contact-center/store/src/storeEventsWrapper.ts`. After the migration, "After" handlers call `fireTaskCallbacks(...)` instead of (or in addition to) `refreshTaskList()` for events that only need to notify widgets. Implementation pattern: look up callbacks by event and optional `taskId`/`interactionId`, then invoke each with the payload. +**Implementation note — task data flow (no write-back):** +`fireTaskCallbacks` **does not** write or update task data back into the store. The flow is: (1) The SDK mutates the **same** `ITask` reference already held in the store's `taskList` (updates `task.data` and `task.uiControls` in place). (2) The store calls `fireTaskCallbacks(event, interactionId, payload)`, which only **invokes** the registered widget callbacks. (3) Widgets then **read** from the store (e.g. `store.taskList[interactionId]` or `store.currentTask`) and re-render with the updated task. So widgets get the latest data by re-reading that same reference after the callback; there is no separate "update task data back into the store" step. + --- ## Refactor Patterns (Before/After) @@ -483,7 +497,7 @@ handleTaskRemove = (taskToRemove: ITask) => { |------|--------| | `store/src/storeEventsWrapper.ts` | Refactor `registerTaskEventListeners` (see definitive table), update `handleTaskRemove` (fix listener mismatches + add `TASK_UI_CONTROLS_UPDATED`), simplify handlers (remove `refreshTaskList()` from all except `TASK_WRAPPEDUP`), wire `handleConsultEnd` to `TASK_CONSULT_END` | | `store/src/store.ts` | No changes expected (observables stay) | -| `store/src/store.types.ts` | Delete local `TASK_EVENTS` enum; import from SDK (which includes `TASK_UI_CONTROLS_UPDATED`) | +| `store/src/store.types.ts` | Delete the local `TASK_EVENTS` enum and import from SDK: `import { TASK_EVENTS } from '@webex/contact-center';` (SDK exports it from package index, e.g. `export {TASK_EVENTS} from './services/task/types'`). If the widgets dependency is an older SDK that does not re-export `TASK_EVENTS`, keep the local enum until the dependency is updated. | | **Task-layer consumers of `TASK_EVENTS`** | **Must be updated in the same step** so that removing the store’s local enum does not break the build. `task/src/helper.ts` imports `TASK_EVENTS` from `@webex/cc-store` and uses legacy names: `AGENT_WRAPPEDUP`, `CONTACT_RECORDING_PAUSED`, `CONTACT_RECORDING_RESUMED` (and `TASK_RECORDING_PAUSED` / `TASK_RECORDING_RESUMED` in setTaskCallback). Replace with SDK event names: `TASK_WRAPPEDUP`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED` in both `setTaskCallback` and `removeTaskCallback`. Either update `task/src/helper.ts` (and any other task files using `TASK_EVENTS`) in this PR or sequence the migration so store switches to SDK enum only after task package is updated. | | `store/tests/*` | Update tests for renamed events, new `TASK_UI_CONTROLS_UPDATED` handler, simplified handlers | diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index d12b3e166..5b7418405 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -6,6 +6,10 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect **Barrel export:** `store/src/index.ts` has `export * from './task-utils'` — all 16 functions are publicly exported via `@webex/cc-store`. Removing functions will cause compile errors in any downstream consumer still importing them. +**Pre-migration: confirm downstream usage.** Exported does not mean used. The only known downstream consumer today is Epic. Before removing store task-utils or changing exports, confirm in the space (or with Epic) that these utils are unused. If they are not used, removal is safe and external compile impact can be ignored; if they are used, coordinate the migration or provide an alternative. + +**Migration end state:** The task object (and `task.data` / `task.uiControls`) is the source of truth. The goal is to remove these legacy constants and derived-state helpers once the migration is complete. This document defines the **safe ordering** to remove them (e.g. rewrite `findHoldStatus` before deleting consult-state constants). Helpers such as `findHoldStatus` and `findHoldTimestamp` are kept for now because hold state must be derived from task/participants (SDK `uiControls.hold.isEnabled` is an action-availability flag, not the current held state); they can be removed when the SDK or task layer exposes equivalent hold state. + --- ## Constants and Types to Delete @@ -27,15 +31,19 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect ## Constants to Keep +**All entries in this table are kept** (no deletions). Use them until the corresponding helpers are rewritten or removed per the ordering constraints below. + | Keep | File | Reason | |------|------|--------| -| `RELATIONSHIP_TYPE_CONSULT` | `store/src/constants.ts` | Still used by `findMediaResourceId` (KEEP) | -| `MEDIA_TYPE_CONSULT` | `store/src/constants.ts` | Still used by `findMediaResourceId` (KEEP) | -| `AGENT` | `store/src/constants.ts` | Used by `getConferenceParticipants` (KEEP) for participant filtering | +| `RELATIONSHIP_TYPE_CONSULT` | `store/src/constants.ts` | Used by `findMediaResourceId` | +| `MEDIA_TYPE_CONSULT` | `store/src/constants.ts` | Used by `findMediaResourceId` | +| `AGENT` | `store/src/constants.ts` | Used by `getConferenceParticipants` for participant filtering | | `CUSTOMER` | `store/src/constants.ts` | Used by `EXCLUDED_PARTICIPANT_TYPES` | | `SUPERVISOR` | `store/src/constants.ts` | Used by `EXCLUDED_PARTICIPANT_TYPES` | | `VVA` | `store/src/constants.ts` | Used by `EXCLUDED_PARTICIPANT_TYPES` | -| `EXCLUDED_PARTICIPANT_TYPES` | `store/src/constants.ts` | Used by `getConferenceParticipants` (KEEP) for participant filtering | +| `EXCLUDED_PARTICIPANT_TYPES` | `store/src/constants.ts` | Used by `getConferenceParticipants` for participant filtering | + +**Consult string alias:** `TASK_STATE_CONSULT`, `RELATIONSHIP_TYPE_CONSULT`, and `MEDIA_TYPE_CONSULT` all resolve to the same string `'consult'`. When rewriting `findHoldStatus` or consolidating constants, consider using a single constant (e.g. one media-type constant for the consult leg) or document the alias explicitly to avoid drift. Do not rely on three separate names for the same value long term. ## Ordering Constraint: Consult State Constants @@ -71,6 +79,10 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect 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`. Do not collapse these when updating `getTaskStatus()` or any consult timer logic. +## Decision: `findHoldStatus` and `findHoldTimestamp` retained for now + +Reviewers may suggest removing these because "task is source of truth." They are **kept** in this migration because: (1) SDK `task.uiControls.hold.isEnabled` indicates whether the hold *action* is available, not whether a given leg is *currently held* (e.g. during consult, main call can be held while consult is active). (2) Timers and UI need per-leg hold state and hold timestamp, which are read from `task.data.interaction.media`; the store helpers centralize that derivation. Once the SDK or task layer exposes equivalent hold state (or widgets derive it in a single place from `task.data` only), these helpers can be removed and the "task as source of truth" end state is fully achieved. + --- ## Old Utilities Inventory From aad2f4a32174976ff7c483f290ce36c31992019f Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 18 Mar 2026 15:24:00 +0530 Subject: [PATCH 23/24] docs(migration): docs(migration): clarify widgets get task updates via callback-driven re-render, not MobX observation of cc --- .../ai-docs/migration/store-event-wiring-migration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 83d0baea1..90903bdc8 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -210,6 +210,8 @@ As an enhancement, `refreshTaskList()` could be extended to accept an optional t **Implementation note — task data flow (no write-back):** `fireTaskCallbacks` **does not** write or update task data back into the store. The flow is: (1) The SDK mutates the **same** `ITask` reference already held in the store's `taskList` (updates `task.data` and `task.uiControls` in place). (2) The store calls `fireTaskCallbacks(event, interactionId, payload)`, which only **invokes** the registered widget callbacks. (3) Widgets then **read** from the store (e.g. `store.taskList[interactionId]` or `store.currentTask`) and re-render with the updated task. So widgets get the latest data by re-reading that same reference after the callback; there is no separate "update task data back into the store" step. +**How widgets see updates when the store does not observe the `cc` object:** The store marks the `cc` object (and task internals) as not observed in the constructor, so **MobX does not trigger re-renders when the SDK mutates the task in place**. Widgets do not rely on observation of task data for updates. Instead, **re-renders are callback-driven**: when `fireTaskCallbacks` runs, the widget's registered callback executes (e.g. updates component state or triggers a re-read). The widget then reads `store.currentTask` or `store.taskList[interactionId]` — the same task reference the SDK has already mutated — and renders with the latest `task.data` and `task.uiControls`. So the callback is what causes the widget to re-render and read the updated task; there is no separate observable on task internals. + --- ## Refactor Patterns (Before/After) From b149db4868f0c53280f0773cf9c94d4a3b89be68 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 19 Mar 2026 16:59:52 +0530 Subject: [PATCH 24/24] =?UTF-8?q?docs:=20address=20review=20comments=20?= =?UTF-8?q?=E2=80=94=20restructure=20definitive=20event=20table,=20merge?= =?UTF-8?q?=20Change=20column=20into=20Detail,=20split=20into=20no-change/?= =?UTF-8?q?changes/new-additions=20sections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration/store-event-wiring-migration.md | 661 ++++++++---------- .../migration/store-task-utils-migration.md | 12 +- 2 files changed, 297 insertions(+), 376 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md index 90903bdc8..08ac77fb1 100644 --- a/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md +++ b/packages/contact-center/ai-docs/migration/store-event-wiring-migration.md @@ -2,58 +2,257 @@ ## Summary -The store's `storeEventsWrapper.ts` currently registers 27 individual task event handlers via `registerTaskEventListeners()` that manually call `refreshTaskList()` and update observables. A companion method `handleTaskRemove()` unregisters them. 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. +The store's `storeEventsWrapper.ts` currently registers 27 individual task event handlers via `registerTaskEventListeners()` that manually call `refreshTaskList()` and update observables. A companion method `handleTaskRemove()` unregisters them. With the SDK task-refactor, the SDK handles state transitions internally via a state machine. The core migration is: + +1. **Switch event names** — use SDK `TASK_EVENTS` enum (delete local copy) +2. **Keep `refreshTaskList()`** — the store does not observe `task.data` directly; `refreshTaskList()` is needed so the store re-syncs observables and the UI re-renders +3. **Add `TASK_UI_CONTROLS_UPDATED`** subscription to trigger widget re-renders +4. **Replace `isDeclineButtonEnabled`** store property with `task.uiControls.decline.isEnabled` +5. **Fix `TASK_CONSULT_END` wiring** — wire the existing (dead) `handleConsultEnd` method --- -## Event Names — Renamed and Deleted +## Files to Modify + +| File | Action | +|------|--------| +| `store/src/storeEventsWrapper.ts` | Update event names to SDK names. Keep `refreshTaskList()` in existing handlers. Add `TASK_UI_CONTROLS_UPDATED` listener. Wire `handleConsultEnd` to `TASK_CONSULT_END` (fix dead code). Remove `setIsDeclineButtonEnabled` from `handleAutoAnswer`. | +| `store/src/store.ts` | No changes expected (observables stay). Mark `isDeclineButtonEnabled` for removal once widget layer uses `uiControls.decline.isEnabled`. | +| `store/src/store.types.ts` | Delete local `TASK_EVENTS` enum; import from SDK: `import { TASK_EVENTS } from '@webex/contact-center';`. | +| **Task-layer consumers of `TASK_EVENTS`** | **Must be updated in the same step** so that removing the store's local enum does not break the build. `task/src/helper.ts` imports `TASK_EVENTS` from `@webex/cc-store` and uses legacy names: `AGENT_WRAPPEDUP`, `CONTACT_RECORDING_PAUSED`, `CONTACT_RECORDING_RESUMED`. Replace with SDK names: `TASK_WRAPPEDUP`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED`. | +| `store/tests/*` | Update tests for renamed events, new `TASK_UI_CONTROLS_UPDATED` handler, `handleConsultEnd` wiring fix, `handleAutoAnswer` change | + +--- + +## SDK Event Reference + +The SDK exports `TASK_EVENTS` from its package entry point (`packages/@webex/contact-center/src/index.ts`): +```typescript +export {TASK_EVENTS} from './services/task/types'; +export type {TASK_EVENTS as TaskEvents} from './services/task/types'; +``` + +**Action:** Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from SDK: `import { TASK_EVENTS } from '@webex/contact-center';`. If the widgets repo depends on an older SDK that does not re-export `TASK_EVENTS`, keep the local enum and align event string values until the dependency is updated. + +### Events Emitted by SDK (complete list) + +Sourced from the SDK task-refactor branch. Events are emitted from Task (lifecycle) and Voice (media/telephony) layers. + +**Task lifecycle events (from Task.ts):** + +| SDK Event | String Value | +|-----------|--------------| +| `TASK_INCOMING` | `'task:incoming'` | +| `TASK_HYDRATE` | `'task:hydrate'` | +| `TASK_OFFER_CONTACT` | `'task:offerContact'` | +| `TASK_ASSIGNED` | `'task:assigned'` | +| `TASK_END` | `'task:end'` | +| `TASK_OFFER_CONSULT` | `'task:offerConsult'` | +| `TASK_CONSULT_CREATED` | `'task:consultCreated'` | +| `TASK_CONSULT_ACCEPTED` | `'task:consultAccepted'` | +| `TASK_CONSULT_END` | `'task:consultEnd'` | +| `TASK_CONSULT_QUEUE_CANCELLED` | `'task:consultQueueCancelled'` | +| `TASK_CONSULT_QUEUE_FAILED` | `'task:consultQueueFailed'` | +| `TASK_WRAPPEDUP` | `'task:wrappedup'` | + +**Voice / media events (from Voice.ts):** + +| SDK Event | String Value | +|-----------|--------------| +| `TASK_HOLD` | `'task:hold'` | +| `TASK_RESUME` | `'task:resume'` | +| `TASK_RECORDING_STARTED` | `'task:recordingStarted'` | +| `TASK_RECORDING_PAUSED` | `'task:recordingPaused'` | +| `TASK_RECORDING_PAUSE_FAILED` | `'task:recordingPauseFailed'` | +| `TASK_RECORDING_RESUMED` | `'task:recordingResumed'` | +| `TASK_RECORDING_RESUME_FAILED` | `'task:recordingResumeFailed'` | +| `TASK_PARTICIPANT_JOINED` | `'task:participantJoined'` | +| `TASK_PARTICIPANT_LEFT` | `'task:participantLeft'` | +| `TASK_CONFERENCE_STARTED` | `'task:conferenceStarted'` | +| `TASK_CONFERENCE_ENDED` | `'task:conferenceEnded'` | +| `TASK_CONFERENCE_FAILED` | `'task:conferenceFailed'` | +| `TASK_EXIT_CONFERENCE` | `'task:exitConference'` | +| `TASK_TRANSFER_CONFERENCE` | `'task:transferConference'` | +| `TASK_SWITCH_CALL` | `'task:switchCall'` | +| `TASK_CONFERENCE_TRANSFER_FAILED` | `'task:conferenceTransferFailed'` | +| `TASK_OUTDIAL_FAILED` | `'task:outdialFailed'` | + +**Additional events (in TASK_EVENTS enum, emitted from TaskManager or other layers):** + +| SDK Event | String Value | Note | +|-----------|--------------|------| +| `TASK_MEDIA` | `'task:media'` | Browser WebRTC remote media | +| `TASK_AUTO_ANSWERED` | `'task:autoAnswered'` | Auto-answer notification | +| `TASK_REJECT` | `'task:rejected'` | Task rejected | +| `TASK_MERGED` | `'task:merged'` | Task merged | +| `TASK_POST_CALL_ACTIVITY` | `'task:postCallActivity'` | Post-call activity | +| `TASK_CONFERENCE_ESTABLISHING` | `'task:conferenceEstablishing'` | Conference in progress | +| `TASK_CONFERENCE_END_FAILED` | `'task:conferenceEndFailed'` | Conference end failure | +| `TASK_PARTICIPANT_LEFT_FAILED` | `'task:participantLeftFailed'` | Participant removal failure | +| `TASK_CONFERENCE_TRANSFERRED` | `'task:conferenceTransferred'` | Conference transferred | +| `TASK_UI_CONTROLS_UPDATED` | `'task:ui-controls-updated'` | UI controls recomputed — **must subscribe** | +| `TASK_UNASSIGNED` | `'task:unassigned'` | Evaluate if widget needs to handle | +| `TASK_WRAPUP` | `'task:wrapup'` | Evaluate if widget needs to handle | +| `TASK_CONSULTING` | `'task:consulting'` | Consulting state entered | + +--- + +## Event Names — Widget Local vs SDK The widget's local `TASK_EVENTS` enum (in `store/src/store.types.ts`) uses CC-level naming that differs from the SDK's task-level naming. -### 5 Renamed Events +### 3 Renamed Events (task-refactor specific) | Old (Widget) | Old Value | New (SDK) | New Value | |---|---|---|---| | `AGENT_WRAPPEDUP` | `'AgentWrappedUp'` | `TASK_WRAPPEDUP` | `'task:wrappedup'` | | `AGENT_CONSULT_CREATED` | `'AgentConsultCreated'` | `TASK_CONSULT_CREATED` | `'task:consultCreated'` | | `AGENT_OFFER_CONTACT` | `'AgentOfferContact'` | `TASK_OFFER_CONTACT` | `'task:offerContact'` | + +### Pre-existing Name Mismatches (not task-refactor — fix when switching to SDK enum) + +These are incorrect names in the widget's local enum that already differed from the SDK. They are **not** renames introduced by the task-refactor; the SDK has always used the `task:*` naming for these. + +| Old (Widget) | Old Value | Correct (SDK) | SDK Value | +|---|---|---|---| | `CONTACT_RECORDING_PAUSED` | `'ContactRecordingPaused'` | `TASK_RECORDING_PAUSED` | `'task:recordingPaused'` | | `CONTACT_RECORDING_RESUMED` | `'ContactRecordingResumed'` | `TASK_RECORDING_RESUMED` | `'task:recordingResumed'` | -### 4 Store-Only Enum Members (no SDK equivalent — delete) +### 4 Store-Only Enum Members (delete — no SDK equivalent) | Widget Enum Member | Value | Note | |---|---|---| -| `TASK_UNHOLD` | `'task:unhold'` | SDK uses `TASK_RESUME` instead | +| `TASK_UNHOLD` | `'task:unhold'` | SDK uses `TASK_RESUME` (`'task:resume'`) instead | | `TASK_CONSULT` | `'task:consult'` | No SDK equivalent; consult flow uses multiple events | -| `TASK_PAUSE` | `'task:pause'` | No SDK equivalent; SDK uses `TASK_HOLD` | +| `TASK_PAUSE` | `'task:pause'` | No SDK equivalent; SDK uses `TASK_HOLD` (`'task:hold'`) | | `AGENT_CONTACT_ASSIGNED` | `'AgentContactAssigned'` | SDK uses `TASK_ASSIGNED` (`'task:assigned'`) | **Docs to update when migrating event names:** CallControl widget spec references `TASK_CONSULT` (and related consult flow) in its sequence diagram — see `packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md` (consult sequence around line 169). Update that doc to use SDK event names and remove references to store-only enum members (`TASK_CONSULT`, `TASK_UNHOLD`) once the migration is applied. -### New SDK Events (not in current widget enum) +--- -| SDK Event | Value | Widget Action Needed | -|---|---|---| -| `TASK_UI_CONTROLS_UPDATED` | `'task:ui-controls-updated'` | **Must subscribe** — triggers widget re-renders | -| `TASK_UNASSIGNED` | `'task:unassigned'` | Evaluate if widget needs to handle | -| `TASK_CONSULT_QUEUE_FAILED` | `'task:consultQueueFailed'` | Evaluate if widget needs to handle | -| `TASK_RECORDING_STARTED` | `'task:recordingStarted'` | Evaluate for recording indicator | -| `TASK_RECORDING_PAUSE_FAILED` | `'task:recordingPauseFailed'` | Evaluate for error handling | -| `TASK_RECORDING_RESUME_FAILED` | `'task:recordingResumeFailed'` | Evaluate for error handling | -| `TASK_EXIT_CONFERENCE` | `'task:exitConference'` | Evaluate for conference flow | -| `TASK_TRANSFER_CONFERENCE` | `'task:transferConference'` | Evaluate for conference flow | -| `TASK_CLEANUP` | `'task:cleanup'` | SDK internal — likely no widget action | +## Task-Refactor Migration Changes + +### 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 + +### Definitive New Event Registration + +`refreshTaskList()` is **kept** in all handlers that already call it. Removing `refreshTaskList()` is a future widget optimization, not part of this task-refactor migration (see [Future Optimization](#future-optimization-refreshtasklist-removal) below). + +#### No change required (22 events) + +These handlers are unchanged by the migration. Event names already match the SDK. + +| # | Event | Handler | Detail | +|---|-------|---------|--------| +| 1 | `TASK_END` | `handleTaskEnd` | Remove from task list, clear current task | +| 2 | `TASK_ASSIGNED` | `handleTaskAssigned` | Update task list, set current task | +| 3 | `TASK_REJECT` | `handleTaskReject` | Remove from task list | +| 4 | `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | Remove from task list | +| 5 | `TASK_MEDIA` | `handleTaskMedia` | Browser-only WebRTC setup — not task-refactor related | +| 6 | `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | Consult state reset + `refreshTaskList()` | +| 7 | `TASK_CONSULTING` | `handleConsulting` | `setConsultStartTimeStamp(Date.now())` + `refreshTaskList()` | +| 8 | `TASK_CONSULT_ACCEPTED` | `handleConsultAccepted` | `setConsultStartTimeStamp(Date.now())` + `refreshTaskList()` + `TASK_MEDIA` listener (browser) | +| 9 | `TASK_OFFER_CONSULT` | `handleConsultOffer` | `refreshTaskList()` | +| 10 | `TASK_PARTICIPANT_JOINED` | `handleConferenceStarted` | Consult state reset + `refreshTaskList()` | +| 11 | `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | Same as #10 | +| 12 | `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | `refreshTaskList()` | +| 13 | `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | Same as #12 | +| 14 | `TASK_HOLD` | `refreshTaskList` | Re-fetch all tasks | +| 15 | `TASK_RESUME` | `refreshTaskList` | Re-fetch all tasks | +| 16 | `TASK_POST_CALL_ACTIVITY` | `refreshTaskList` | Re-fetch all tasks | +| 17 | `TASK_CONFERENCE_ESTABLISHING` | `refreshTaskList` | Re-fetch all tasks | +| 18 | `TASK_CONFERENCE_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| 19 | `TASK_CONFERENCE_END_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| 20 | `TASK_PARTICIPANT_LEFT_FAILED` | `refreshTaskList` | Re-fetch all tasks | +| 21 | `TASK_CONFERENCE_TRANSFERRED` | `refreshTaskList` | Re-fetch all tasks | +| 22 | `TASK_CONFERENCE_TRANSFER_FAILED` | `refreshTaskList` | Re-fetch all tasks | + +#### Changes required (7 events) + +| # | Event | Handler | Category | Detail | +|---|-------|---------|----------|--------| +| 23 | `TASK_WRAPPEDUP` | `refreshTaskList` | Task-refactor | **Rename** from `AGENT_WRAPPEDUP`. Handler unchanged. | +| 24 | `TASK_CONSULT_CREATED` | `handleConsultCreated` | Task-refactor | **Rename** from `AGENT_CONSULT_CREATED`. Handler unchanged — `setConsultStartTimeStamp(Date.now())` + `refreshTaskList()`. | +| 25 | `TASK_OFFER_CONTACT` | `refreshTaskList` | Task-refactor | **Rename** from `AGENT_OFFER_CONTACT`. Handler unchanged. | +| 26 | `TASK_RECORDING_PAUSED` | `refreshTaskList` | Pre-existing fix | **Fix name** from `CONTACT_RECORDING_PAUSED`. Handler unchanged. | +| 27 | `TASK_RECORDING_RESUMED` | `refreshTaskList` | Pre-existing fix | **Fix name** from `CONTACT_RECORDING_RESUMED`. Handler unchanged. | +| 28 | `TASK_CONSULT_END` | `handleConsultEnd` | Pre-existing fix | **Fix wiring** — wire the existing (currently dead) `handleConsultEnd` method instead of bare `refreshTaskList`. | +| 29 | `TASK_AUTO_ANSWERED` | `handleAutoAnswer` | Task-refactor | **Remove `setIsDeclineButtonEnabled(true)`** — replace with `task.uiControls.decline.isEnabled`. Keep `refreshTaskList()`. | -**Action:** The SDK **exports `TASK_EVENTS`** from its package entry point (`packages/@webex/contact-center/src/index.ts`): `export {TASK_EVENTS} from './services/task/types';` and `export type {TASK_EVENTS as TaskEvents} from './services/task/types';`. Delete the local `TASK_EVENTS` enum from `store/src/store.types.ts` and import from the SDK: `import { TASK_EVENTS } from '@webex/contact-center';`. If the widgets repo currently depends on an older SDK version that does not re-export `TASK_EVENTS` from the package index, keep the local enum and align event string values with the SDK until the dependency is updated. +#### New additions (1 event) -### Pre-existing Bug: Event Name Mismatches +| # | Event | Handler | Category | Detail | +|---|-------|---------|----------|--------| +| 30 | `TASK_UI_CONTROLS_UPDATED` | New handler | Task-refactor | Fire callbacks to trigger widget re-renders when SDK recomputes `uiControls` | -The 5 renamed events above are currently hardcoded in `store.types.ts` with a TODO comment: `// TODO: remove this once cc sdk exports this enum`. The SDK now exports `TASK_EVENTS`; replace the entire local enum with the SDK import and use SDK event names (e.g. `TASK_WRAPPEDUP`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED`) everywhere. +### `refreshTaskList()` — Retained + +`refreshTaskList()` is **kept in all existing handlers** for this migration. The store does not directly observe `task.data` via MobX, so `refreshTaskList()` is the mechanism that re-syncs the store's observable `taskList` and triggers MobX-driven re-renders. Removing it is a separate widget-layer optimization — not part of the task-refactor migration. + +### Future Optimization: `refreshTaskList()` Removal + +> **Scope:** Widget improvement — not part of this migration. + +Once the widget layer is updated to derive UI state from `task.data` / `task.uiControls` directly (via callbacks or by making the `cc` object MobX-observable), `refreshTaskList()` calls can be removed from most handlers. Until then, the current approach of calling `refreshTaskList()` on every event is safe and correct. + +**Why it could be removed in the future:** +1. **SDK keeps the same task reference up to date.** The state machine updates `task.data` (and `task.uiControls`) on the **same** `ITask` reference already held in the store's `taskList`. +2. **Widget re-renders could be driven by callbacks.** Widgets registered via `setTaskCallback(event, cb, taskId)` are notified when the store fires callbacks. Those callbacks could cause the widget to re-read the latest `task.data` directly. +3. **Re-fetch would only be needed when the list membership changes** (initial load, full refresh, or `TASK_WRAPPEDUP`). + +### Migration: `isDeclineButtonEnabled` → `task.uiControls.decline.isEnabled` + +The store currently has an `isDeclineButtonEnabled` observable set by `handleAutoAnswer`. With the SDK task-refactor, `task.uiControls.decline.isEnabled` is recomputed by the SDK and should be the source of truth for decline button state. + +**Current consumers:** +- `store/src/store.ts` — observable property +- `storeEventsWrapper.ts` — getter + `setIsDeclineButtonEnabled()` setter +- `task/src/helper.ts` — reads `store.isDeclineButtonEnabled` and passes to widget +- `cc-components/.../TaskList/task-list.utils.ts` — `!store.isDeclineButtonEnabled` for auto-answer disable logic +- `cc-components/.../IncomingTask/incoming-task.utils.tsx` — `isDeclineButtonEnabled` prop + +**Migration steps:** +1. Update `handleAutoAnswer` to remove `setIsDeclineButtonEnabled(true)` — no store mutation needed. +2. Update `task/src/helper.ts` to read `task.uiControls.decline.isEnabled` instead of `store.isDeclineButtonEnabled`. +3. Update IncomingTask and TaskList components to read from `task.uiControls.decline.isEnabled`. +4. Once no consumers remain, delete the store property, getter, and setter. + +### Migration: Store Consult State Observables (future removal) + +The store tracks consult state via three observables (`consultStartTimeStamp`, `isQueueConsultInProgress`, `currentConsultQueueId`) that are mutated by `handleConsultCreated`, `handleConsulting`, `handleConsultAccepted`, `handleConsultEnd`, `handleConsultQueueCancelled`, and `handleConferenceStarted`. + +With the SDK task-refactor, consult state is tracked in `task.data` and `task.uiControls`. These store observables are **retained for now** because widget consult timer logic (`timer-utils.ts`, `CallControl/index.tsx`) reads `consultStartTimeStamp` directly. + +**End state:** Once widgets derive consult timing from `task.data` (or the SDK exposes consult start time), these store properties and their setters in `handleConferenceStarted` and other handlers can be removed. + +--- + +## Pre-existing Issues (tracked separately from task-refactor) + +These are bugs in the current widget code, not introduced by the task-refactor. They should be fixed during migration but are not task-refactor changes. + +### Bug 1: `handleConsultEnd` is dead code + +A `handleConsultEnd` method exists (resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`) but `TASK_CONSULT_END` is wired to `refreshTaskList()` instead. The method's consult state cleanup never runs. + +**Migration / test plan:** When wiring `TASK_CONSULT_END` to `handleConsultEnd`, update store unit tests to assert the new wiring and consult state reset. Remove or rewrite tests that only covered the old dead path. + +### Bug 2: `handleTaskRemove` listener mismatch + +`registerTaskEventListeners` wires `TASK_CONFERENCE_TRANSFERRED → this.refreshTaskList`. But `handleTaskRemove` calls `taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.handleConferenceEnded)` — wrong handler reference. This listener is **never actually removed**, causing a listener leak. + +### Test gap + +`TASK_CONFERENCE_TRANSFERRED` currently has **no unit test** (registration and cleanup). Tracked by *(Jira: [CAI-7758](https://jira-eng-sjc12.cisco.com/jira/browse/CAI-7758))*. When implementing the migration, add tests for this event. --- -## Old Approach +## Old Code Reference ### Entry Point **File:** `packages/contact-center/store/src/storeEventsWrapper.ts` @@ -69,8 +268,6 @@ The 5 renamed events above are currently hardcoded in `store.types.ts` with a TO ### Store Observables Affected by Event Handlers -These live in `store/src/store.ts` and are mutated via setters in `storeEventsWrapper.ts`: - | Observable | Type | Mutated By | |---|---|---| | `currentTask` | `ITask \| null` | `handleTaskAssigned`, `handleTaskEnd`, `handleTaskRemove` | @@ -78,6 +275,7 @@ These live in `store/src/store.ts` and are mutated via setters in `storeEventsWr | `consultStartTimeStamp` | `number \| undefined` | `handleConsultCreated`, `handleConsulting`, `handleConsultAccepted`, `handleConsultEnd`, `handleConsultQueueCancelled`, `handleConferenceStarted` | | `isQueueConsultInProgress` | `boolean` | `handleConsultEnd`, `handleConsultQueueCancelled`, `handleConferenceStarted` | | `currentConsultQueueId` | `string` | `handleConsultEnd`, `handleConsultQueueCancelled`, `handleConferenceStarted` | +| `isDeclineButtonEnabled` | `boolean` | `handleAutoAnswer` — **migrate to `task.uiControls.decline.isEnabled`** | ### Old Event Handlers (27 events) @@ -95,7 +293,7 @@ These live in `store/src/store.ts` and are mutated via setters in `storeEventsWr | 10 | `TASK_CONSULT_ACCEPTED` | `handleConsultAccepted` | `refreshTaskList()` + `setConsultStartTimeStamp(Date.now())` + set ENGAGED state + **registers `TASK_MEDIA` listener on consult task (browser only)** | | 11 | `TASK_OFFER_CONSULT` | `handleConsultOffer` | `refreshTaskList()` | | 12 | `TASK_AUTO_ANSWERED` | `handleAutoAnswer` | `setIsDeclineButtonEnabled(true)` + `refreshTaskList()` | -| 13 | `TASK_CONSULT_END` | `refreshTaskList` | Re-fetch all tasks (**Note:** `handleConsultEnd` method exists but is NOT wired — see pre-existing bug below) | +| 13 | `TASK_CONSULT_END` | `refreshTaskList` | Re-fetch all tasks (**Note:** `handleConsultEnd` method exists but is NOT wired — see pre-existing bug) | | 14 | `TASK_HOLD` | `refreshTaskList` | Re-fetch all tasks | | 15 | `TASK_RESUME` | `refreshTaskList` | Re-fetch all tasks | | 16 | `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | `refreshTaskList()` | @@ -111,283 +309,93 @@ These live in `store/src/store.ts` and are mutated via setters in `storeEventsWr | 26 | `TASK_POST_CALL_ACTIVITY` | `refreshTaskList` | Re-fetch all tasks | | 27 | `TASK_MEDIA` | `handleTaskMedia` | Browser-only: `setCallControlAudio(new MediaStream([track]))` | -### Pre-existing Bugs in Old Code - -**Bug 1: `handleConsultEnd` is dead code.** -A `handleConsultEnd` method exists (resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`) but `TASK_CONSULT_END` is wired to `refreshTaskList()` instead. The method's consult state cleanup never runs. - -**Migration / test plan for Bug 1:** When wiring `TASK_CONSULT_END` to `handleConsultEnd`, update store unit tests (e.g. `store/tests/storeEventsWrapper.ts` or equivalent) that reference `handleConsultEnd`: assert that `TASK_CONSULT_END` triggers the handler and that consult state is reset. Remove or rewrite tests that only covered the old dead path (e.g. tests that assumed `TASK_CONSULT_END` only triggered `refreshTaskList`). - -**Bug 2: `handleTaskRemove` listener mismatch.** -`registerTaskEventListeners` wires `TASK_CONFERENCE_TRANSFERRED → this.refreshTaskList`. But `handleTaskRemove` calls `taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.handleConferenceEnded)` — wrong handler reference. This listener is **never actually removed**, causing a listener leak. - -**Test gap:** `TASK_CONFERENCE_TRANSFERRED` currently has **no unit test** (registration and cleanup). This gap is tracked by a dedicated Jira ticket. The ticket covers: adding UTs that assert this event is registered in `registerTaskEventListeners` and correctly unregistered in `handleTaskRemove`. When implementing the store migration or the ticket, add tests so this gap is closed. *(Jira: [CAI-7758](https://jira-eng-sjc12.cisco.com/jira/browse/CAI-7758))* - --- -## New Approach +## Implementation Reference -### 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 +This section provides code-level guidance for the implementing developer. The core migration: switch to SDK event names, keep `refreshTaskList()`, add `TASK_UI_CONTROLS_UPDATED`, fix `TASK_CONSULT_END` wiring, and replace `isDeclineButtonEnabled`. -### Definitive New Event Registration +### Before/After: `registerTaskEventListeners()` -Many events that currently trigger `refreshTaskList()` will no longer need it because `task.data` is kept in sync by the SDK. For rows marked **Simplify**, we **remove the call to `refreshTaskList()`** from the handler; the **handler is not removed** — it is kept and only fires callbacks so widgets re-render. Below is the single authoritative table for all event handler changes: - -| # | Event | New Handler | Change | Detail | -|---|-------|-------------|--------|--------| -| 1 | `TASK_END` | `handleTaskEnd` | **Keep** | Remove from task list, clear current task | -| 2 | `TASK_ASSIGNED` | `handleTaskAssigned` | **Keep** | Update task list, set current task | -| 3 | `TASK_REJECT` | `handleTaskReject` | **Keep** | Remove from task list | -| 4 | `TASK_OUTDIAL_FAILED` | `handleOutdialFailed` | **Keep** | Remove from task list | -| 5 | `TASK_MEDIA` | `handleTaskMedia` | **Keep** | Browser-only WebRTC setup (conditional registration) | -| 6 | `TASK_UI_CONTROLS_UPDATED` | `bound.uiControlsUpdated` (per-task; see Pattern 1 & 3) | **Add new** | Fire callbacks to trigger widget re-renders. Do **not** use a class-level handler — it would resolve the wrong `interactionId` in multi-task scenarios. | -| 7 | `TASK_WRAPPEDUP` | `bound.wrappedup` (per-task; see Pattern 1) | **Keep + rename** | Was `AGENT_WRAPPEDUP`. Keep `refreshTaskList()` in handler — task must be removed from list after wrapup. Fire callback. Do **not** use class method; use bound handler for correct `.off()` teardown. | -| 8 | `TASK_CONSULT_END` | `handleConsultEnd` | **Fix wiring** | Wire the existing (currently dead) `handleConsultEnd` method. Resets `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`. Remove `refreshTaskList()`. Fire callback. | -| 9 | `TASK_CONSULT_QUEUE_CANCELLED` | `handleConsultQueueCancelled` | **Simplify** | Keep consult state reset. Remove `refreshTaskList()`. Fire callback. | -| 10 | `TASK_CONSULTING` | `handleConsulting` | **Simplify** | Keep `setConsultStartTimeStamp(Date.now())`. Remove `refreshTaskList()`. Fire callback. | -| 11 | `TASK_CONSULT_CREATED` | `handleConsultCreated` | **Simplify + rename** | Was `AGENT_CONSULT_CREATED`. Keep `setConsultStartTimeStamp(Date.now())`. Remove `refreshTaskList()`. Fire callback. | -| 12 | `TASK_CONSULT_ACCEPTED` | `handleConsultAccepted` | **Simplify** | Keep `setConsultStartTimeStamp(Date.now())`, keep ENGAGED state, **keep `TASK_MEDIA` listener registration (browser)**. Remove `refreshTaskList()`. Fire callback. | -| 13 | `TASK_AUTO_ANSWERED` | `handleAutoAnswer` | **Simplify** | Keep `setIsDeclineButtonEnabled(true)`. Remove `refreshTaskList()`. Fire callback. | -| 14 | `TASK_OFFER_CONTACT` | Fire callback only | **Simplify + rename** | Was `AGENT_OFFER_CONTACT`. Remove `refreshTaskList()`. | -| 15 | `TASK_OFFER_CONSULT` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 16 | `TASK_PARTICIPANT_JOINED` | `handleConferenceStarted` | **Simplify** | Keep consult state reset (`isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`). Remove `refreshTaskList()`. Fire callback. | -| 17 | `TASK_CONFERENCE_STARTED` | `handleConferenceStarted` | **Simplify** | Same as #16 | -| 18 | `TASK_CONFERENCE_ENDED` | `handleConferenceEnded` | **Simplify** | Remove `refreshTaskList()`. Fire callback. | -| 19 | `TASK_PARTICIPANT_LEFT` | `handleConferenceEnded` | **Simplify** | Same as #18 | -| 20 | `TASK_HOLD` | Fire callback only (e.g. `bound.hold`) | **Simplify** | **Remove only `refreshTaskList()`** from the handler; keep the handler and fire callbacks. | -| 21 | `TASK_RESUME` | Fire callback only (e.g. `bound.resume`) | **Simplify** | **Remove only `refreshTaskList()`** from the handler; keep the handler and fire callbacks. | -| 22 | `TASK_RECORDING_PAUSED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_PAUSED`. Remove `refreshTaskList()`; keep handler, fire callback. | -| 23 | `TASK_RECORDING_RESUMED` | Fire callback only | **Simplify + rename** | Was `CONTACT_RECORDING_RESUMED`. Remove `refreshTaskList()`; keep handler, fire callback. | -| 24 | `TASK_POST_CALL_ACTIVITY` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 25 | `TASK_CONFERENCE_ESTABLISHING` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 26 | `TASK_CONFERENCE_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 27 | `TASK_CONFERENCE_END_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 28 | `TASK_PARTICIPANT_LEFT_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 29 | `TASK_CONFERENCE_TRANSFERRED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | -| 30 | `TASK_CONFERENCE_TRANSFER_FAILED` | Fire callback only | **Simplify** | Remove `refreshTaskList()`. | - -### 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. For most events we **remove only the call to `refreshTaskList()`** from the handler; the **handler itself is kept** and becomes "fire callback only" (e.g. bound hold/resume handlers). - -#### When `refreshTaskList()` is still required - -**`refreshTaskList()` remains required** for these cases only: - -1. **Initial load / hydration** — populate the store's task list when the app or CC session loads. -2. **Full page refresh recovery** — re-sync the list after a full page reload. -3. **`TASK_WRAPPEDUP`** — the task must be removed from the list after wrap-up completion; today that is done by calling `refreshTaskList()` (or may be replaced later with explicit list removal). - -For all other events (e.g. `TASK_HOLD`, `TASK_RESUME`, `TASK_RECORDING_PAUSED`, consult/conference lifecycle), **do not** call `refreshTaskList()`. The handler stays; it only fires callbacks so widgets re-render. - -#### Why we can remove most `refreshTaskList()` calls - -1. **SDK keeps the same task reference up to date.** The state machine updates `task.data` (and `task.uiControls`) on the **same** `ITask` reference already held in the store's `taskList`. No re-fetch is needed for in-place updates. -2. **Widgets get the latest data by re-reading from the store.** When the store calls `fireTaskCallbacks(event, interactionId, payload)`, widgets that registered via `setTaskCallback(event, cb, taskId)` are notified. Those callbacks cause the widget to re-run and **re-read the same task from the store** (e.g. `store.taskList[interactionId]` or `store.currentTask`). The task reference has already been updated in place by the SDK, so **no write-back of task data into the store is needed** — and `fireTaskCallbacks` does not write task data; it only invokes callbacks. -3. **Re-fetch is only needed when the list itself changes.** We keep `refreshTaskList()` only where the **list** must change: initial load, full refresh, or `TASK_WRAPPEDUP` (task removed from the list). For all other events, the existing task reference is already updated by the SDK, and `fireTaskCallbacks` triggers UI updates. - -#### Future consideration: optional single-task refresh - -As an enhancement, `refreshTaskList()` could be extended to accept an optional task (or `interactionId`) and update only that task in the store instead of re-fetching the full list (e.g. for wrap-up or other single-task updates). This is not required for the current migration. - ---- - -### fireTaskCallbacks — Definition - -**Purpose:** Invoke all callbacks that were registered for a given task event via `setTaskCallback(event, cb, taskId)` (or equivalent), so widgets can re-render or react when that event occurs. - -**Signature (conceptual):** -`fireTaskCallbacks(event: TASK_EVENTS, interactionId: string, payload?: unknown): void` - -**Where it lives:** In the store-events layer — `packages/contact-center/store/src/storeEventsWrapper.ts`. After the migration, "After" handlers call `fireTaskCallbacks(...)` instead of (or in addition to) `refreshTaskList()` for events that only need to notify widgets. Implementation pattern: look up callbacks by event and optional `taskId`/`interactionId`, then invoke each with the payload. - -**Implementation note — task data flow (no write-back):** -`fireTaskCallbacks` **does not** write or update task data back into the store. The flow is: (1) The SDK mutates the **same** `ITask` reference already held in the store's `taskList` (updates `task.data` and `task.uiControls` in place). (2) The store calls `fireTaskCallbacks(event, interactionId, payload)`, which only **invokes** the registered widget callbacks. (3) Widgets then **read** from the store (e.g. `store.taskList[interactionId]` or `store.currentTask`) and re-render with the updated task. So widgets get the latest data by re-reading that same reference after the callback; there is no separate "update task data back into the store" step. - -**How widgets see updates when the store does not observe the `cc` object:** The store marks the `cc` object (and task internals) as not observed in the constructor, so **MobX does not trigger re-renders when the SDK mutates the task in place**. Widgets do not rely on observation of task data for updates. Instead, **re-renders are callback-driven**: when `fireTaskCallbacks` runs, the widget's registered callback executes (e.g. updates component state or triggers a re-read). The widget then reads `store.currentTask` or `store.taskList[interactionId]` — the same task reference the SDK has already mutated — and renders with the latest `task.data` and `task.uiControls`. So the callback is what causes the widget to re-render and read the updated task; there is no separate observable on task internals. - ---- - -## Refactor Patterns (Before/After) - -### Architectural Note: Bound-Handler Map - -Handlers registered via `task.on()` that need per-task context (`interactionId`) **cannot** use inline arrows — `task.off()` in `handleTaskRemove` requires the exact same function reference to detach a listener. Conversely, class-level methods don't have access to the `interactionId` local variable from `registerTaskEventListeners`. - -**Solution:** Store bound handler references in a per-task map at registration time. `handleTaskRemove` retrieves them for cleanup. - -```typescript -// Class property — keyed by interactionId -private taskBoundHandlers = new Map>(); -``` - -### Pattern 1: `registerTaskEventListeners()` — Bound-Handler Registration - -#### Before +#### Before (old event names) ```typescript registerTaskEventListeners(task: ITask) { const interactionId = task.data.interactionId; task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - 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, this.handleConferenceStarted); - // ... 19 more event registrations, most calling refreshTaskList() + task.on(TASK_EVENTS.AGENT_OFFER_CONTACT, this.refreshTaskList); // old name + task.on(TASK_EVENTS.AGENT_CONSULT_CREATED, this.handleConsultCreated); // old name + task.on(TASK_EVENTS.AGENT_WRAPPEDUP, this.refreshTaskList); // old name + task.on(TASK_EVENTS.TASK_CONSULT_END, this.refreshTaskList); // BUG: handleConsultEnd exists but not wired + task.on(TASK_EVENTS.CONTACT_RECORDING_PAUSED, this.refreshTaskList); // wrong name + task.on(TASK_EVENTS.CONTACT_RECORDING_RESUMED, this.refreshTaskList);// wrong name + // ... remaining events unchanged } ``` -#### After +#### After (SDK event names, refreshTaskList kept, fixes applied) ```typescript registerTaskEventListeners(task: ITask) { const interactionId = task.data.interactionId; - // Create bound handlers that close over this task's interactionId. - // Stored in map so handleTaskRemove can .off() the exact same references. - const bound: Record = { - reject: (reason: string) => this.handleTaskReject(task, reason), - outdialFailed: (reason: string) => this.handleOutdialFailed(reason), - uiControlsUpdated: (uiControls: TaskUIControls) => { - this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); - }, - wrappedup: (data: unknown) => { - this.refreshTaskList(); - this.fireTaskCallbacks(TASK_EVENTS.TASK_WRAPPEDUP, interactionId, data); - }, - confStarted_participantJoined: () => this.handleConferenceStarted(TASK_EVENTS.TASK_PARTICIPANT_JOINED, interactionId), - confStarted_conferenceStarted: () => this.handleConferenceStarted(TASK_EVENTS.TASK_CONFERENCE_STARTED, interactionId), - confEnded_conferenceEnded: () => this.handleConferenceEnded(TASK_EVENTS.TASK_CONFERENCE_ENDED, interactionId), - confEnded_participantLeft: () => this.handleConferenceEnded(TASK_EVENTS.TASK_PARTICIPANT_LEFT, interactionId), - // Callback-only events — each bound to this task's interactionId - hold: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_HOLD, interactionId), - resume: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RESUME, interactionId), - offerContact: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONTACT, interactionId), - offerConsult: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_OFFER_CONSULT, interactionId), - recordingPaused: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_PAUSED, interactionId), - recordingResumed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_RECORDING_RESUMED, interactionId), - postCallActivity: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, interactionId), - confEstablishing: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, interactionId), - confFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_FAILED, interactionId), - confEndFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, interactionId), - participantLeftFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, interactionId), - confTransferred: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, interactionId), - confTransferFailed: () => this.fireTaskCallbacks(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, interactionId), - }; - this.taskBoundHandlers.set(interactionId, bound); - - // NEW: SDK-computed UI control updates (bound to emitting task's interactionId) - task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, bound.uiControlsUpdated); - - // KEEP: Task lifecycle events that need store-level management (class methods) + // Lifecycle (unchanged handlers) task.on(TASK_EVENTS.TASK_END, this.handleTaskEnd); task.on(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - // TASK_REJECT: handleTaskReject(task, reason) needs the emitting task reference — - // must use a bound handler, not a direct class method reference - task.on(TASK_EVENTS.TASK_REJECT, bound.reject); - task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, bound.outdialFailed); + task.on(TASK_EVENTS.TASK_REJECT, this.handleTaskReject); + task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, this.handleOutdialFailed); - // KEEP + FIX WIRING: Wire handleConsultEnd (was dead code) + // NEW: SDK-computed UI control updates + task.on(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, /* fire callbacks for this task */); + + // RENAMED events (handler unchanged, refreshTaskList kept) + task.on(TASK_EVENTS.TASK_WRAPPEDUP, this.refreshTaskList); // was AGENT_WRAPPEDUP + task.on(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); // was AGENT_CONSULT_CREATED + task.on(TASK_EVENTS.TASK_OFFER_CONTACT, this.refreshTaskList); // was AGENT_OFFER_CONTACT + + // FIX: Wire handleConsultEnd (was dead code — wired to refreshTaskList before) task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); - // KEEP: Consult state management (remove refreshTaskList, keep state mutations) - task.on(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); // renamed from AGENT_CONSULT_CREATED + // FIX: Correct event names (handler unchanged) + task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, this.refreshTaskList); // was CONTACT_RECORDING_PAUSED + task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, this.refreshTaskList); // was CONTACT_RECORDING_RESUMED + + // Auto-answer — remove setIsDeclineButtonEnabled, keep refreshTaskList + task.on(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); + + // Consult/conference handlers (unchanged — keep refreshTaskList + state mutations) task.on(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting); task.on(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted); task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); + task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer); - // KEEP: Conference state management — bound handlers pass event type + interactionId - task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, bound.confStarted_participantJoined); - task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, bound.confStarted_conferenceStarted); - task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, bound.confEnded_conferenceEnded); - task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, bound.confEnded_participantLeft); - - // KEEP: Auto-answer sets decline button state - task.on(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); + // Events wired to refreshTaskList (unchanged) + task.on(TASK_EVENTS.TASK_HOLD, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_RESUME, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, 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); - // KEEP: Wrapup completion — bound handler retains refreshTaskList + correct interactionId - task.on(TASK_EVENTS.TASK_WRAPPEDUP, bound.wrappedup); // renamed from AGENT_WRAPPEDUP - - // SIMPLIFIED: Callback-only events — all use bound handlers with correct interactionId - task.on(TASK_EVENTS.TASK_HOLD, bound.hold); - task.on(TASK_EVENTS.TASK_RESUME, bound.resume); - task.on(TASK_EVENTS.TASK_OFFER_CONTACT, bound.offerContact); // renamed - task.on(TASK_EVENTS.TASK_OFFER_CONSULT, bound.offerConsult); - task.on(TASK_EVENTS.TASK_RECORDING_PAUSED, bound.recordingPaused); // renamed - task.on(TASK_EVENTS.TASK_RECORDING_RESUMED, bound.recordingResumed); // renamed - task.on(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, bound.postCallActivity); - task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, bound.confEstablishing); - task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, bound.confFailed); - task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, bound.confEndFailed); - task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, bound.participantLeftFailed); - task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, bound.confTransferred); - task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, bound.confTransferFailed); - - // Browser-only: WebRTC media setup + // Browser-only: WebRTC media setup (unchanged) if (this.deviceType === DEVICE_TYPE_BROWSER) { task.on(TASK_EVENTS.TASK_MEDIA, this.handleTaskMedia); } } ``` -### Pattern 2: Simplifying `refreshTaskList()` Event Handlers - -#### Before -```typescript -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()); -``` - -#### After -```typescript -// SDK keeps task.data in sync via state machine. -// refreshTaskList() only called on initialization/hydration and TASK_WRAPPEDUP. -// Individual events use bound handlers (from taskBoundHandlers map) so -// handleTaskRemove can .off() the exact same reference. See Pattern 1. - -task.on(TASK_EVENTS.TASK_HOLD, bound.hold); -task.on(TASK_EVENTS.TASK_RESUME, bound.resume); -// ... all other callback-only events use bound.* references -``` - -### Pattern 3: `TASK_UI_CONTROLS_UPDATED` — Bound Handler (Not a Class Method) +> **Note on handler references and `.off()` cleanup:** The implementing developer must ensure that handler references registered via `task.on()` match those used in `handleTaskRemove` for `.off()`. This is a standard listener pattern — see the old code's existing approach for reference. -**Why not a class method?** A class-level `handleUIControlsUpdated` would need to derive `interactionId` from `this.store.currentTask`, which is wrong in multi-task/consult scenarios — the emitting task may not be the currently selected one. Using a bound handler (see Pattern 1) captures the correct `interactionId` at registration time. - -```typescript -// WRONG — class method reads currentTask (may be a different task than the emitter): -handleUIControlsUpdated = (uiControls: TaskUIControls) => { - const interactionId = this.store.currentTask?.data.interactionId; // ← BUG in multi-task - this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); -}; - -// CORRECT — bound handler from Pattern 1 captures emitting task's interactionId: -// (created in registerTaskEventListeners per task) -uiControlsUpdated: (uiControls: TaskUIControls) => { - this.fireTaskCallbacks(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, interactionId, uiControls); - // interactionId is from the closure: const interactionId = task.data.interactionId; -}, -``` +### `handleConferenceStarted` — No Change -### Pattern 4: Conference Handler Simplification +`handleConferenceStarted` is **unchanged** by this migration. Consult state resets and `refreshTaskList()` are both retained. -#### Before ```typescript handleConferenceStarted = () => { runInAction(() => { @@ -397,128 +405,41 @@ handleConferenceStarted = () => { }); this.refreshTaskList(); }; - -handleConferenceEnded = () => { - this.refreshTaskList(); -}; -``` - -#### After -```typescript -// SDK state machine handles CONFERENCING state transitions. -// task.data and task.uiControls already reflect conference state. -// Keep consult state reset in handleConferenceStarted; remove refreshTaskList() from both. -// -// Both eventType and interactionId are passed by the bound handlers in Pattern 1. -// This avoids: (a) dual callback firing, (b) unresolved interactionId in class scope, -// and (c) inline arrows that can't be detached by handleTaskRemove. - -handleConferenceStarted = (eventType: TASK_EVENTS, interactionId: string) => { - runInAction(() => { - this.setIsQueueConsultInProgress(false); - this.setCurrentConsultQueueId(null); - this.setConsultStartTimeStamp(null); - }); - this.fireTaskCallbacks(eventType, interactionId); -}; - -handleConferenceEnded = (eventType: TASK_EVENTS, interactionId: string) => { - this.fireTaskCallbacks(eventType, interactionId); -}; ``` -### Pattern 5: `handleTaskRemove()` — Cleanup via Bound-Handler Map +### Before/After: `handleAutoAnswer` #### Before ```typescript -handleTaskRemove = (taskToRemove: ITask) => { - if (taskToRemove) { - taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); - // ... all 27 .off() calls using OLD event names - // BUG: TASK_CONFERENCE_TRANSFERRED uses wrong handler (handleConferenceEnded instead of refreshTaskList) - } +handleAutoAnswer = () => { + this.setIsDeclineButtonEnabled(true); + this.refreshTaskList(); }; ``` #### After ```typescript -handleTaskRemove = (taskToRemove: ITask) => { - if (!taskToRemove) return; - - const interactionId = taskToRemove.data.interactionId; - const bound = this.taskBoundHandlers.get(interactionId); - - // Class-method handlers — stable references, no map needed - taskToRemove.off(TASK_EVENTS.TASK_END, this.handleTaskEnd); - taskToRemove.off(TASK_EVENTS.TASK_ASSIGNED, this.handleTaskAssigned); - taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); // FIX: was refreshTaskList - taskToRemove.off(TASK_EVENTS.TASK_CONSULT_CREATED, this.handleConsultCreated); - taskToRemove.off(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting); - taskToRemove.off(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted); - taskToRemove.off(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); - taskToRemove.off(TASK_EVENTS.TASK_AUTO_ANSWERED, this.handleAutoAnswer); - taskToRemove.off(TASK_EVENTS.TASK_MEDIA, this.handleTaskMedia); - - // Bound handlers — retrieve exact references from map for correct .off() detachment - if (bound) { - taskToRemove.off(TASK_EVENTS.TASK_REJECT, bound.reject); - taskToRemove.off(TASK_EVENTS.TASK_OUTDIAL_FAILED, bound.outdialFailed); - taskToRemove.off(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, bound.uiControlsUpdated); - taskToRemove.off(TASK_EVENTS.TASK_WRAPPEDUP, bound.wrappedup); - taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_JOINED, bound.confStarted_participantJoined); - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_STARTED, bound.confStarted_conferenceStarted); - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_ENDED, bound.confEnded_conferenceEnded); - taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT, bound.confEnded_participantLeft); - taskToRemove.off(TASK_EVENTS.TASK_HOLD, bound.hold); - taskToRemove.off(TASK_EVENTS.TASK_RESUME, bound.resume); - taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONTACT, bound.offerContact); - taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONSULT, bound.offerConsult); - taskToRemove.off(TASK_EVENTS.TASK_RECORDING_PAUSED, bound.recordingPaused); - taskToRemove.off(TASK_EVENTS.TASK_RECORDING_RESUMED, bound.recordingResumed); - taskToRemove.off(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, bound.postCallActivity); - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, bound.confEstablishing); - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_FAILED, bound.confFailed); - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, bound.confEndFailed); - taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, bound.participantLeftFailed); - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, bound.confTransferred); // FIX: was handleConferenceEnded - taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, bound.confTransferFailed); - this.taskBoundHandlers.delete(interactionId); - } - - // Reset store state - // ... existing currentTask/taskList cleanup logic +handleAutoAnswer = () => { + // setIsDeclineButtonEnabled removed — use task.uiControls.decline.isEnabled instead. + this.refreshTaskList(); }; ``` --- -## Files to Modify - -| File | Action | -|------|--------| -| `store/src/storeEventsWrapper.ts` | Refactor `registerTaskEventListeners` (see definitive table), update `handleTaskRemove` (fix listener mismatches + add `TASK_UI_CONTROLS_UPDATED`), simplify handlers (remove `refreshTaskList()` from all except `TASK_WRAPPEDUP`), wire `handleConsultEnd` to `TASK_CONSULT_END` | -| `store/src/store.ts` | No changes expected (observables stay) | -| `store/src/store.types.ts` | Delete the local `TASK_EVENTS` enum and import from SDK: `import { TASK_EVENTS } from '@webex/contact-center';` (SDK exports it from package index, e.g. `export {TASK_EVENTS} from './services/task/types'`). If the widgets dependency is an older SDK that does not re-export `TASK_EVENTS`, keep the local enum until the dependency is updated. | -| **Task-layer consumers of `TASK_EVENTS`** | **Must be updated in the same step** so that removing the store’s local enum does not break the build. `task/src/helper.ts` imports `TASK_EVENTS` from `@webex/cc-store` and uses legacy names: `AGENT_WRAPPEDUP`, `CONTACT_RECORDING_PAUSED`, `CONTACT_RECORDING_RESUMED` (and `TASK_RECORDING_PAUSED` / `TASK_RECORDING_RESUMED` in setTaskCallback). Replace with SDK event names: `TASK_WRAPPEDUP`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED` in both `setTaskCallback` and `removeTaskCallback`. Either update `task/src/helper.ts` (and any other task files using `TASK_EVENTS`) in this PR or sequence the migration so store switches to SDK enum only after task package is updated. | -| `store/tests/*` | Update tests for renamed events, new `TASK_UI_CONTROLS_UPDATED` handler, simplified handlers | - ---- - ## Validation Criteria -- [ ] Task list stays in sync on all lifecycle events (incoming, assigned, end, reject) -- [ ] `refreshTaskList()` only called on init/hydration and `TASK_WRAPPEDUP`, 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 -- [ ] `handleTaskRemove` unregisters all listeners correctly via bound-handler map (no listener leaks) -- [ ] `taskBoundHandlers` map is cleaned up (`.delete()`) when a task is removed -- [ ] `handleConsultEnd` is properly wired and resets consult state on `TASK_CONSULT_END` +- [ ] All event names switched to SDK `TASK_EVENTS` enum (`TASK_WRAPPEDUP`, `TASK_CONSULT_CREATED`, `TASK_OFFER_CONTACT`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED`) +- [ ] Local `TASK_EVENTS` enum deleted from `store.types.ts`; imported from `@webex/contact-center` +- [ ] `refreshTaskList()` still called in all existing handlers (no removal in this migration) +- [ ] `TASK_UI_CONTROLS_UPDATED` handler added; triggers widget re-renders +- [ ] `handleConsultEnd` is properly wired to `TASK_CONSULT_END` and resets consult state +- [ ] `handleAutoAnswer` no longer calls `setIsDeclineButtonEnabled` — widget layer uses `task.uiControls.decline.isEnabled` - [ ] `handleConsultAccepted` still registers `TASK_MEDIA` listener on consult task (browser) -- [ ] `handleAutoAnswer` still sets `isDeclineButtonEnabled = true` -- [ ] All 5 renamed events use SDK names (`TASK_WRAPPEDUP`, `TASK_CONSULT_CREATED`, `TASK_OFFER_CONTACT`, `TASK_RECORDING_PAUSED`, `TASK_RECORDING_RESUMED`) +- [ ] Task list stays in sync on all lifecycle events (incoming, assigned, end, reject, wrapup) +- [ ] No regression in consult/conference/hold flows +- [ ] `handleTaskRemove` unregisters all listeners correctly (no listener leaks) +- [ ] Task-layer consumers (`task/src/helper.ts`) updated to use SDK event names --- diff --git a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md index 5b7418405..0d88889d8 100644 --- a/packages/contact-center/ai-docs/migration/store-task-utils-migration.md +++ b/packages/contact-center/ai-docs/migration/store-task-utils-migration.md @@ -8,7 +8,7 @@ The store's `task-utils.ts` contains 16 exported utility functions that inspect **Pre-migration: confirm downstream usage.** Exported does not mean used. The only known downstream consumer today is Epic. Before removing store task-utils or changing exports, confirm in the space (or with Epic) that these utils are unused. If they are not used, removal is safe and external compile impact can be ignored; if they are used, coordinate the migration or provide an alternative. -**Migration end state:** The task object (and `task.data` / `task.uiControls`) is the source of truth. The goal is to remove these legacy constants and derived-state helpers once the migration is complete. This document defines the **safe ordering** to remove them (e.g. rewrite `findHoldStatus` before deleting consult-state constants). Helpers such as `findHoldStatus` and `findHoldTimestamp` are kept for now because hold state must be derived from task/participants (SDK `uiControls.hold.isEnabled` is an action-availability flag, not the current held state); they can be removed when the SDK or task layer exposes equivalent hold state. +**Migration end state:** The task object (and `task.data` / `task.uiControls`) is the source of truth. `task.uiControls` is recomputed by the SDK whenever `task.data` changes and should be considered the source for anything and everything in the UI. The goal is to remove these legacy constants and derived-state helpers once the migration is complete. This document defines the **safe ordering** to remove them (e.g. rewrite `findHoldStatus` before deleting consult-state constants). Helpers `findHoldStatus` and `findHoldTimestamp` are kept for now only because per-leg hold state and hold timestamps must be derived from `task.data.interaction.media`; they can be removed when the SDK or task layer exposes equivalent per-leg hold state. --- @@ -81,7 +81,7 @@ The SDK has `CONSULT_INITIATING` (consult requested, async in-progress) and `CON ## Decision: `findHoldStatus` and `findHoldTimestamp` retained for now -Reviewers may suggest removing these because "task is source of truth." They are **kept** in this migration because: (1) SDK `task.uiControls.hold.isEnabled` indicates whether the hold *action* is available, not whether a given leg is *currently held* (e.g. during consult, main call can be held while consult is active). (2) Timers and UI need per-leg hold state and hold timestamp, which are read from `task.data.interaction.media`; the store helpers centralize that derivation. Once the SDK or task layer exposes equivalent hold state (or widgets derive it in a single place from `task.data` only), these helpers can be removed and the "task as source of truth" end state is fully achieved. +Reviewers may suggest removing these because "task is source of truth." The SDK `task.uiControls` is recomputed every time `task.data` changes and should be considered the source for anything and everything in the UI. These helpers are **kept** in this migration only because timers and UI need **per-leg hold state** and **hold timestamp**, which are derived from `task.data.interaction.media`; the store helpers centralize that derivation. Once the SDK or task layer exposes equivalent per-leg hold state (or widgets derive it in a single place from `task.data` only), these helpers can be removed and the "task as source of truth" end state is fully achieved. --- @@ -140,7 +140,7 @@ Two different `findHoldTimestamp` functions exist with different signatures: | 4 | `isInteractionOnHold(task)` | Timer logic needs this | | 5 | `findMediaResourceId(task, mType)` | Switch-call actions need media resource IDs. Uses `RELATIONSHIP_TYPE_CONSULT`, `MEDIA_TYPE_CONSULT` from constants. | | 6 | `findHoldTimestamp(task, mType)` | Hold timer needs timestamp. Note: store version takes `ITask`, task-util version takes `Interaction` (see dual-signature note above). | -| 7 | `findHoldStatus(task, mType, agentId)` | Needed for `getTaskStatus()` held-state derivation and component layer `isHeld` — cannot derive from `controls.hold.isEnabled` | +| 7 | `findHoldStatus(task, mType, agentId)` | Needed for per-leg hold state and hold timers (from `task.data.interaction.media`); `getTaskStatus()` and component layer `isHeld` use this until SDK/task exposes equivalent | ### Review — 4 Functions (may simplify or remove) @@ -260,8 +260,8 @@ const consultCallHeld = findHoldStatus(task, 'consult', agentId); #### After ```typescript // KEPT in store/task-utils.ts — still needed for: -// 1. getTaskStatus() held-state derivation (cannot derive from controls.hold.isEnabled) -// 2. Component layer isHeld prop +// Per-leg hold state and hold timers (from task.data.interaction.media); +// getTaskStatus() and component layer isHeld use this until SDK/task exposes equivalent. // Implementation unchanged — reads from task.data.interaction.participants export const findHoldStatus = (task: ITask, mType: string, agentId: string): boolean => { // ...unchanged... @@ -361,7 +361,7 @@ export function getTaskStatus(task: ITask, agentId: string): string { |------|--------| | `store/src/task-utils.ts` | Remove 5 functions, keep 7 (update `getTaskStatus` to use `task.uiControls`), review 4 | | `store/src/store.types.ts` | Delete `ConsultStatus` enum (all consumers removed) | -| `store/src/constants.ts` | Delete 9 task/interaction/consult state constants; keep 7 participant/media constants | +| `store/src/constants.ts` | **Phased deletion:** Delete 9 task/interaction/consult state constants **only after** dependent functions (`getTaskStatus`, `findHoldStatus`, `getConsultMPCState`) are rewritten to use SDK `TaskState`. The "After" `getTaskStatus` code still returns these constants as values — they must remain until a follow-up step rewrites return values to SDK `TaskState`. Keep 7 participant/media constants. See ordering constraints above. | | `task/src/Utils/task-util.ts` | Delete `getControlsVisibility` + all 22 `get*ButtonVisibility` functions; keep `findHoldTimestamp(interaction, mType)` | | `store/tests/task-utils.ts` | Update tests: remove tests for 5 deleted functions, update `getTaskStatus` tests | | `task/tests/utils/task-util.ts` | Remove tests for deleted visibility functions |