This plan uses the spec-driven development approach already established in CC Widgets. Each milestone starts with writing specs (tests), then implementing to make specs pass. This ensures parity between old and new behavior.
- CC SDK
task-refactorbranch merged and released (or linked locally) -
TaskUIControlstype andtask:ui-controls-updatedevent available in@webex/contact-center -
task.uiControlsgetter available onITask - Team alignment on migration approach
| # | Milestone | Scope | Est. Effort | Risk | Depends On |
|---|---|---|---|---|---|
| M0 | SDK integration setup | Link SDK, verify types | 1 day | Low | SDK release |
| M1 | Types & constants alignment | Import new types, add adapters | 1-2 days | Low | M0 |
| M2 | Store event wiring simplification | Simplify event handlers, add ui-controls-updated |
2-3 days | Medium | M0 |
| M3 | Store task-utils thinning | Remove redundant utils | 1-2 days | Low | M2 |
| M3.5 | Timer utils migration | Update timer-utils to accept TaskUIControls |
1 day | Low | M3 |
| M4 | CallControl hook refactor | Core: replace getControlsVisibility with task.uiControls |
3-5 days | High | M1, M2, M3, M3.5 |
| M5 | Component layer update | Update cc-components prop interfaces |
2-3 days | Medium | M4 |
| M6 | IncomingTask migration | Use task.uiControls.accept/decline |
1 day | Low | M1 |
| M7 | TaskList migration | Optional status enhancement | 1 day | Low | M1 |
| M8 | Integration testing & cleanup | E2E, remove dead code, docs | 2-3 days | Medium | All |
Total estimated effort: 15–23 days
Goal: Verify the new SDK API is available and types compile.
Steps:
- Update
@webex/contact-centerdependency to task-refactor version - Verify
TaskUIControlstype is importable - Verify
task.uiControlsgetter exists onITask - Verify
TASK_EVENTS.TASK_UI_CONTROLS_UPDATEDconstant exists - Run
yarn buildto confirm no type errors
Spec: Write a minimal integration test that creates a mock task and reads uiControls.
Validation: yarn build passes with new SDK version.
Ref: 009-types-and-constants-migration.md
Goal: Import new SDK types into widget packages without changing runtime behavior.
Spec first:
// Test: TaskUIControls type compatibility
import { TaskUIControls } from '@webex/contact-center';
const controls: TaskUIControls = task.uiControls;
expect(controls.hold).toHaveProperty('isVisible');
expect(controls.hold).toHaveProperty('isEnabled');Steps:
- Add
TaskUIControlsimport totask/src/task.types.ts - Create adapter type mapping old control names → new (for gradual migration)
- Add
TASK_UI_CONTROLS_UPDATEDto store event constants - Review and annotate constants for deprecation
Validation: All existing tests still pass. New types compile.
Ref: 003-store-event-wiring-migration.md
Goal: Simplify store event handlers; add task:ui-controls-updated subscription.
Spec first:
// Test: Store registers ui-controls-updated listener
describe('registerTaskEventListeners', () => {
it('should register TASK_UI_CONTROLS_UPDATED handler', () => {
store.registerTaskEventListeners(mockTask);
expect(mockTask.on).toHaveBeenCalledWith(
TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, expect.any(Function)
);
});
it('should NOT call refreshTaskList on TASK_HOLD', () => {
// Verify simplified handler
});
});Steps:
- Add
TASK_UI_CONTROLS_UPDATEDhandler inregisterTaskEventListeners() - Replace
refreshTaskList()calls with callback-only for: TASK_HOLD, TASK_RESUME, TASK_CONSULT_END, all conference events - Keep
refreshTaskList()only for: initialization, hydration - Update tests for each modified handler
Order (low risk → high risk):
- Add new
TASK_UI_CONTROLS_UPDATEDhandler (additive, no breakage) - Simplify conference event handlers (less critical)
- Simplify hold/resume handlers (medium impact)
- Simplify consult handlers (medium impact)
- Remove unnecessary
refreshTaskList()calls (highest impact)
Validation: All existing widget tests pass. Store correctly fires callbacks on events.
Ref: 008-store-task-utils-migration.md
Goal: Remove utility functions that are now handled by SDK.
Spec first:
// Test: Verify no consumers remain for removed functions
// (Static analysis — ensure no import of getConsultStatus, getIsConferenceInProgress, etc.)Steps:
- Search codebase for each function to verify consumers
- Remove functions with zero consumers after M2 changes
- Mark functions with remaining consumers for later removal (after M4/M5)
- Keep display-only functions (
getTaskStatus,getConferenceParticipants, etc.)
Validation: Build succeeds. No runtime errors.
Ref: 004-call-control-hook-migration.md
Goal: Update calculateStateTimerData() and calculateConsultTimerData() to accept TaskUIControls.
Why: These functions accept controlVisibility (old shape) as a parameter and derive timer labels from it. They must be migrated before M4 since useCallControl depends on them.
Spec first:
describe('calculateStateTimerData with TaskUIControls', () => {
it('should return Wrap Up label when controls.wrapup.isVisible', () => {
const controls = { ...getDefaultUIControls(), wrapup: { isVisible: true, isEnabled: true } };
const result = calculateStateTimerData(mockTask, controls, agentId);
expect(result.label).toBe('Wrap Up');
});
});Steps:
- Update
calculateStateTimerData(task, controls, agentId)signature - Replace
controlVisibility.isConsultInitiatedOrAccepted→controls.endConsult.isVisible - Replace
controlVisibility.isHeld→ derive fromcontrols.hold - Update
calculateConsultTimerData(task, controls, agentId)similarly - Update all test cases
Also fix during this milestone:
- Consolidate
findHoldTimestampdual signatures (store vs task-util versions)
Ref: 004-call-control-hook-migration.md
Goal: Replace getControlsVisibility() with task.uiControls in useCallControl.
Spec first (write ALL specs before implementation):
describe('useCallControl with task.uiControls', () => {
// Parity specs: each scenario must produce identical control states
describe('connected voice call', () => {
it('should show hold, mute, end, transfer, consult controls', () => {
mockTask.uiControls = {
hold: { isVisible: true, isEnabled: true },
mute: { isVisible: true, isEnabled: true },
end: { isVisible: true, isEnabled: true },
transfer: { isVisible: true, isEnabled: true },
consult: { isVisible: true, isEnabled: true },
// ... all other controls disabled
};
const { result } = renderHook(() => useCallControl(props));
expect(result.current.controls.hold).toEqual({ isVisible: true, isEnabled: true });
});
});
describe('held voice call', () => {
it('should show hold (enabled=true for resume), disable end/mute', () => { /* ... */ });
});
describe('consulting', () => {
it('should show endConsult, switchToMainCall, switchToConsult, mergeToConference', () => { /* ... */ });
});
describe('conferencing', () => {
it('should show exitConference, disable hold', () => { /* ... */ });
});
describe('wrapping up', () => {
it('should show only wrapup control', () => { /* ... */ });
});
describe('digital channel', () => {
it('should show only accept, end, transfer, wrapup', () => { /* ... */ });
});
describe('ui-controls-updated event', () => {
it('should re-render when task emits ui-controls-updated', () => { /* ... */ });
});
describe('no task', () => {
it('should return default controls when no task', () => { /* ... */ });
});
});Steps:
- Write comprehensive parity specs (30+ test cases covering all states)
- Create
adaptSDKControls()adapter function (maps SDK names to old names if needed during transition) - Replace
getControlsVisibility()call inuseCallControlwithtask.uiControls - Add
task:ui-controls-updatedsubscription withuseEffect - Update hook return type to use new control names
- Remove old state flags from return
- Run parity specs — fix any mismatches
Parity verification approach:
- For each state (connected, held, consulting, conferencing, wrapping-up, offered):
- Mock task with known data
- Call old
getControlsVisibility()→ capture result - Read
task.uiControls→ capture result - Compare: every control must have same
isVisible/isEnabled - Document and resolve any differences (old bug vs new behavior)
Validation: All 30+ parity specs pass. All existing hook tests pass (with updated assertions).
Ref: 010-component-layer-migration.md
Goal: Update cc-components to accept new control prop shape.
Spec first:
describe('CallControlComponent', () => {
it('should render hold button from controls.hold', () => {
render(<CallControlComponent controls={mockControls} {...otherProps} />);
expect(screen.getByTestId('hold-button')).toBeVisible();
});
it('should hide mute button when controls.mute.isVisible=false', () => {
const controls = { ...mockControls, mute: { isVisible: false, isEnabled: false } };
render(<CallControlComponent controls={controls} {...otherProps} />);
expect(screen.queryByTestId('mute-button')).not.toBeInTheDocument();
});
});Steps:
- Update
CallControlComponentPropsto acceptcontrols: TaskUIControls - Update
CallControlComponentto read fromcontrols.* - Update
CallControlConsultcomponent - Update
IncomingTaskComponentif needed - Update all component tests
Validation: All component tests pass. Visual output identical.
Ref: 005-incoming-task-migration.md
Goal: Use task.uiControls.accept/decline in IncomingTask.
Spec first:
describe('useIncomingTask with uiControls', () => {
it('should derive accept visibility from task.uiControls.accept', () => { /* ... */ });
it('should derive decline visibility from task.uiControls.decline', () => { /* ... */ });
});Steps:
- Replace
getAcceptButtonVisibility()/getDeclineButtonVisibility()withtask.uiControls - Update component props
- Update tests
Validation: IncomingTask tests pass. Accept/decline work for voice and digital.
Ref: 006-task-list-migration.md
Goal: Optionally enhance task status display; verify compatibility.
Steps:
- Verify
useTaskListworks with new SDK (should be compatible) - Optionally enhance
getTaskStatus()to use SDK state info - Update tests if any changes made
Validation: TaskList renders correctly with all task states.
Goal: End-to-end verification, dead code removal, documentation update.
Steps:
- E2E Test Matrix:
| Scenario | Widgets Involved | Verify |
|---|---|---|
| Incoming voice call → accept → end | IncomingTask, CallControl | Accept button, end button |
| Incoming voice call → reject | IncomingTask | Decline button, RONA timer |
| Connected → hold → resume | CallControl | Hold toggle, timer |
| Connected → consult → end consult | CallControl | Consult flow controls |
| Connected → consult → conference | CallControl | Merge, conference controls |
| Conference → exit | CallControl | Exit conference |
| Conference → transfer conference | CallControl | Transfer conference |
| Connected → transfer (blind) | CallControl | Transfer popover |
| Connected → end → wrapup | CallControl | Wrapup button |
| Outdial → connected → end | OutdialCall, CallControl | Full outdial flow |
| Digital task → accept → end → wrapup | IncomingTask, CallControl | Digital controls |
| Multiple tasks in list | TaskList | Task selection, per-task controls |
| Page refresh → hydrate | All | Restore state correctly |
-
Bug fixes (found during analysis):
- Fix recording callback cleanup mismatch (
TASK_RECORDING_PAUSEDvsCONTACT_RECORDING_PAUSED) - Consolidate
findHoldTimestampdual signatures (store vs task-util) - Add
task:wrapuprace guard if needed
- Fix recording callback cleanup mismatch (
-
Dead code removal:
- Delete
task/src/Utils/task-util.ts(or reduce tofindHoldTimestamponly) - Remove unused store utils
- Remove unused constants
- Remove unused type definitions
- Delete
-
Documentation updates:
- Update
task/ai-docs/widgets/CallControl/AGENTS.mdandARCHITECTURE.md - Update
task/ai-docs/widgets/IncomingTask/AGENTS.mdandARCHITECTURE.md - Update
store/ai-docs/AGENTS.mdandARCHITECTURE.md - Update
cc-components/ai-docs/AGENTS.md
- Update
-
Final validation:
yarn build— no errorsyarn test:unit— all passyarn test:styles— no lint errors- Sample apps (React + WC) work correctly
-
CallControl hook refactor (M4) — largest change, most complex logic
- Mitigation: Comprehensive parity specs written BEFORE implementation
- Rollback: Old
getControlsVisibility()stays in codebase until M8 cleanup
-
Store event wiring (M2) — removing
refreshTaskList()could cause stale data- Mitigation: Gradual removal; keep
refreshTaskList()as fallback initially - Rollback: Re-add
refreshTaskList()calls if data staleness detected
- Mitigation: Gradual removal; keep
-
Consult/Conference flows — most complex state transitions
- Mitigation: Dedicated parity specs for every consult/conference scenario
- Mitigation: Test with Agent Desktop to verify identical behavior
- OutdialCall (no changes needed)
- IncomingTask (minimal changes)
- TaskList (minimal changes)
- Types alignment (additive, no runtime changes)
For each milestone, complete these in order:
- Write spec file with test cases for NEW behavior
- Write parity tests (old behavior == new behavior) where applicable
- Run specs — verify they FAIL (red)
- Implement changes
- Run specs — verify they PASS (green)
- Run ALL existing tests — verify no regressions
-
yarn build— verify compilation - Code review with team
- Mark milestone complete
M0 (SDK setup)
│
├── M1 (types) ─┐
└── M2 (store events) ─┤
│
M3 (store utils)
│
M3.5 (timer utils)
│
M4 (CallControl hook) ← CRITICAL PATH
│
M5 (components)
│
├── M6 (IncomingTask)
└── M7 (TaskList)
│
M8 (integration + cleanup + bug fixes)
M0 → M1 + M2 (parallel) → M3 → M3.5 → M4 → M5 → M6 + M7 (parallel) → M8
Created: 2026-03-09 Parent: 001-migration-overview.md