diff --git a/packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md b/packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md new file mode 100644 index 000000000..d49f28b89 --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md @@ -0,0 +1,154 @@ +# CallControl Widget + +## Overview + +Provides call control functionality (hold, mute, transfer, consult, conference, end, wrapup) for active telephony tasks. Includes both standard and CAD (Customer Attached Data) variants. + +## Why This Widget? + +**Problem:** Agents need comprehensive call control during active conversations. + +**Solution:** Unified interface for all call operations with two variants: +- **CallControl:** Standard call controls +- **CallControlCAD:** Call controls + CAD panel for customer data + +## What It Does + +- Hold/Resume active call +- Mute/Unmute microphone +- Transfer call (to agent/queue/number) +- Consult with agent before transfer +- Conference multiple parties +- Recording controls (pause/resume) +- End call +- Wrapup with codes +- Auto-wrapup timer +- CAD panel (CallControlCAD variant only) + +## Usage + +### React + +```tsx +import { CallControl, CallControlCAD } from '@webex/cc-widgets'; + +function App() { + return ( + <> + {/* Standard call controls */} + console.log('Hold:', isHeld)} + onEnd={() => console.log('Call ended')} + onWrapUp={() => console.log('Wrapup complete')} + onRecordingToggle={({ isRecording }) => console.log('Recording:', isRecording)} + onToggleMute={(isMuted) => console.log('Muted:', isMuted)} + conferenceEnabled={true} + consultTransferOptions={{ showAgents: true, showQueues: true }} + /> + + {/* With CAD panel */} + console.log('Hold:', isHeld)} + onEnd={() => console.log('Call ended')} + callControlClassName="custom-class" + /> + + ); +} +``` + +### Web Component + +```html + + + + +``` + +## Props API + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `onHoldResume` | `(isHeld: boolean) => void` | - | Callback when hold state changes | +| `onEnd` | `() => void` | - | Callback when call ends | +| `onWrapUp` | `() => void` | - | Callback when wrapup completes | +| `onRecordingToggle` | `({ isRecording: boolean, task: ITask }) => void` | - | Callback when recording toggled | +| `onToggleMute` | `(isMuted: boolean) => void` | - | Callback when mute toggled | +| `conferenceEnabled` | `boolean` | `true` | Enable conference functionality | +| `consultTransferOptions` | `{ showAgents?: boolean, showQueues?: boolean, showAddressBook?: boolean }` | - | Configure transfer options | +| `callControlClassName` | `string` | - | Custom CSS class (CAD variant) | +| `callControlConsultClassName` | `string` | - | Custom CSS class for consult (CAD variant) | + +## Examples + +### With Transfer Options + +```tsx + +``` + +### With Conference Disabled + +```tsx + { + console.log('Call ended without conference option'); + }} +/> +``` + +### CAD Variant with Custom Styling + +```tsx + { + console.log('Wrapup complete, CAD data saved'); + }} +/> +``` + +## Differences: CallControl vs CallControlCAD + +| Feature | CallControl | CallControlCAD | +|---------|-------------|----------------| +| Call controls | ✅ | ✅ | +| CAD panel | ❌ | ✅ | +| Customer data display | ❌ | ✅ | +| Layout | Compact | Extended with CAD sidebar | +| Use case | Simple call handling | CRM integration scenarios | + +**Note:** Both use the same `useCallControl` hook and share 90% of logic. + +## Dependencies + +```json +{ + "@webex/cc-components": "workspace:*", + "@webex/cc-store": "workspace:*", + "@webex/cc-ui-logging": "workspace:*", + "mobx-react-lite": "^4.1.0", + "react-error-boundary": "^6.0.0" +} +``` + +See [package.json](../../package.json) for versions. + +## Additional Resources + +- [Architecture Details](architecture.md) - Component internals, data flows, diagrams + diff --git a/packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md b/packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md new file mode 100644 index 000000000..94c551b54 --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md @@ -0,0 +1,433 @@ +# CallControl Widget - Architecture + +## Component Overview + +| Layer | File | Purpose | Key Responsibilities | +|-------|------|---------|---------------------| +| **Widget** | `src/CallControl/index.tsx`
`src/CallControlCAD/index.tsx` | Smart container | - Observer HOC
- Error boundary
- Delegates to hook
- Props: callbacks, options | +| **Hook** | `src/helper.ts` (useCallControl) | Business logic | - All call operations
- Task event subscriptions
- State management (hold, mute, recording)
- Buddy agents, transfer, consult
- Auto-wrapup timer | +| **Component** | `@webex/cc-components` (CallControlComponent) | Presentation | - Call control UI
- Buttons (hold, mute, transfer, etc.)
- Transfer/consult modals
- Wrapup dropdown
- Auto-wrapup timer display | +| **Store** | `@webex/cc-store` | State/SDK | - currentTask observable
- Task event callbacks
- wrapupCodes
- Task SDK methods (hold, resume, etc.) | + +## File Structure + +``` +task/src/ +├── CallControl/ +│ └── index.tsx # CallControl widget +├── CallControlCAD/ +│ └── index.tsx # CallControl + CAD variant +├── helper.ts # useCallControl hook (lines 270-945) +├── task.types.ts # CallControlProps, useCallControlProps +└── index.ts # Exports + +cc-components/src/components/task/CallControl/ +├── call-control.tsx # CallControlComponent (main UI) +├── call-control.utils.ts # Utility functions +├── call-control.styles.scss # Styles +├── CallControlCustom/ +│ └── consult-transfer-popover.tsx +└── AutoWrapupTimer/ + └── AutoWrapupTimer.tsx +``` + +## Data Flows + +### Overview + +```mermaid +graph TD + A[CallControl Widget] --> B[useCallControl Hook] + B --> C[Store] + C --> D[currentTask] + B --> E[CallControlComponent] + E --> F[User Actions] + F --> E + E --> B + D --> G[Task SDK Methods] + G --> H[Backend] +``` + +### Hook: useCallControl + +**Inputs:** +- `currentTask` - Active ITask from store +- `onHoldResume` - Hold state change callback +- `onEnd` - Call end callback +- `onWrapUp` - Wrapup callback +- `onRecordingToggle` - Recording toggle callback +- `onToggleMute` - Mute toggle callback +- `conferenceEnabled` - Enable conference features +- `consultTransferOptions` - Transfer UI options +- `logger` - Logger instance + +**Manages State:** +- `isMuted` - Microphone mute state +- `isRecording` - Recording state +- `holdTime` - Duration of current hold +- `buddyAgents` - List of agents for transfer +- `consultAgentName` - Selected consult agent name +- `lastTargetType` - Last transfer target type +- `secondsUntilAutoWrapup` - Auto-wrapup countdown + +**Subscribes to Task Events:** +- `TASK_HOLD` - Task put on hold +- `TASK_RESUME` - Task resumed from hold +- `TASK_END` - Task ended +- `AGENT_WRAPPEDUP` - Wrapup completed +- `TASK_RECORDING_PAUSED` - Recording paused +- `TASK_RECORDING_RESUMED` - Recording resumed + +**Returns:** All functions and state for call control operations + +## Sequence Diagrams + +### Hold/Resume Call + +```mermaid +sequenceDiagram + participant U as User + participant C as CallControlComponent + participant H as useCallControl Hook + participant T as Task Object + participant S as Store + participant B as Backend + + U->>C: Click Hold button + C->>H: toggleHold(true) + H->>T: task.hold() + T->>B: POST /task/hold + alt Success + B-->>T: Success + T-->>S: Emit TASK_HOLD event + S->>H: holdCallback() + H->>H: Start holdTime timer + H->>H: onHoldResume(true) + H-->>C: Update UI (show Resume) + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Hold failed') + end + + Note over U,B: Resume flow similar with task.resume() +``` + +### Transfer Call + +```mermaid +sequenceDiagram + participant U as User + participant C as CallControlComponent + participant H as useCallControl Hook + participant T as Task Object + participant B as Backend + + U->>C: Click Transfer button + C->>C: Show transfer modal + U->>C: Select agent from list + C->>H: transferCall(agentId, name, 'AGENT') + H->>H: logger.info('transferCall') + H->>T: task.transfer({targetAgentId, destinationType}) + T->>B: POST /task/transfer + alt Success + B-->>T: Transfer initiated + T-->>H: Promise resolved + H->>H: logger.info('Transfer success') + Note over C: Task will end via TASK_END event + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Transfer failed') + end +``` + +### Consult Then Transfer + +```mermaid +sequenceDiagram + participant U as User + participant C as CallControlComponent + participant H as useCallControl Hook + participant T as Task Object + participant S as Store + participant B as Backend + + U->>C: Click Consult button + C->>C: Show consult modal + U->>C: Select agent + C->>H: consultCall(agentId, name, 'AGENT') + H->>T: task.consultCall({targetAgentId, destinationType}) + T->>B: POST /task/consult + B-->>T: Consult call created + T-->>S: Emit TASK_CONSULT_STARTED + S-->>C: Update UI (show consult controls) + + Note over U,B: Agent talks with consultant + + U->>C: Click "Complete Transfer" + C->>H: consultTransfer() + H->>T: task.consultTransfer() + T->>B: POST /task/consultTransfer + alt Success + B-->>T: Transfer completed + T-->>S: Emit TASK_END + H->>H: logger.info('Consult transfer complete') + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Consult transfer failed') + end +``` + +### Conference Call + +```mermaid +sequenceDiagram + participant U as User + participant C as CallControlComponent + participant H as useCallControl Hook + participant T as Task Object + participant B as Backend + + U->>C: Click Conference button + C->>C: Show conference modal + U->>C: Select agent + C->>H: consultCall(agentId, name, 'AGENT') + H->>T: task.consultCall() + T->>B: POST /task/consult + B-->>T: Consult created + + U->>C: Click "Add to Conference" + C->>H: consultConference() + H->>T: task.consultConference() + T->>B: POST /task/consultConference + alt Success + B-->>T: Conference created + T-->>H: Promise resolved + H->>H: Update conferenceParticipants + C->>U: Display all participants + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Conference failed') + end +``` + +### Wrapup with Codes + +```mermaid +sequenceDiagram + participant U as User + participant C as CallControlComponent + participant H as useCallControl Hook + participant S as Store + participant T as Task Object + participant B as Backend + + Note over U: Call ended + + S->>S: Fetch wrapupCodes from config + S-->>C: Display wrapup dropdown + C->>U: Show wrapup codes + + U->>C: Select wrapup code + C->>C: setSelectedWrapupReason(code) + + U->>C: Click Submit Wrapup + C->>H: wrapupCall(reason, id, auxCode) + H->>T: task.wrapup({wrapupReason, auxCodeId}) + T->>B: POST /task/wrapup + alt Success + B-->>T: Wrapup saved + T-->>S: Emit AGENT_WRAPPEDUP + S->>H: wrapupCallCallback() + H->>H: onWrapUp() + H->>U: Parent notified + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Wrapup failed') + end +``` + +### Auto-Wrapup Timer + +```mermaid +sequenceDiagram + participant S as Store + participant H as useCallControl Hook + participant C as CallControlComponent + participant U as User + + Note over S: Task ends, auto-wrapup configured + + S->>S: currentTask.autoWrapup = {enabled: true, timeout: 60} + S-->>H: Observable update + H->>H: Start auto-wrapup interval (1 second) + H->>H: secondsUntilAutoWrapup = 60 + + loop Every 1 second + H->>H: secondsUntilAutoWrapup-- + H-->>C: Re-render with new countdown + C->>U: Display "Auto-wrapup in 59s..." + end + + alt User clicks Cancel + U->>C: Click Cancel Auto-Wrapup + C->>H: cancelAutoWrapup() + H->>H: Clear interval + H-->>C: Hide timer + else Timer reaches 0 + H->>H: Auto-wrapup triggered + H->>H: wrapupCall(null, null, null) + Note over H: Proceeds with default wrapup + end +``` + +## Call Control Operations + +### Button Actions + +| Button | Hook Function | Task Method | Description | +|--------|---------------|-------------|-------------| +| Hold | `toggleHold(true)` | `task.hold()` | Put call on hold | +| Resume | `toggleHold(false)` | `task.resume()` | Resume from hold | +| Mute | `toggleMute()` | N/A (local) | Mute microphone | +| Transfer | `transferCall(...)` | `task.transfer(...)` | Direct transfer | +| Consult | `consultCall(...)` | `task.consultCall(...)` | Initiate consult | +| Conference | `consultConference()` | `task.consultConference()` | Add to conference | +| End Call | `endCall()` | `task.end()` | End the call | +| Wrapup | `wrapupCall(...)` | `task.wrapup(...)` | Submit wrapup | +| Recording | `toggleRecording()` | `task.pauseRecording()` / `task.resumeRecording()` | Toggle recording | + +### Transfer/Consult Options + +**Destination Types:** +- `AGENT` - Transfer to buddy agent +- `QUEUE` - Transfer to queue/entry point +- `DN` - Transfer to phone number +- `ADDRESS_BOOK` - Transfer to address book entry + +**Configured via `consultTransferOptions` prop:** +- `showAgents` - Show buddy agents list +- `showQueues` - Show queues/entry points +- `showAddressBook` - Show address book entries + +## Error Handling + +| Error | Source | Handled By | Action | +|-------|--------|------------|--------| +| Hold failed | Task SDK | Hook catch | Log error | +| Transfer failed | Task SDK | Hook catch | Log error, show alert | +| Consult failed | Task SDK | Hook catch | Log error, show alert | +| Wrapup failed | Task SDK | Hook catch | Log error | +| Recording failed | Task SDK | Hook catch | Log error | +| Component crash | React | ErrorBoundary | Call store.onErrorCallback | + +## Troubleshooting + +### Issue: Hold button disabled/doesn't work + +**Possible Causes:** +1. Task not in active state +2. Task type doesn't support hold +3. SDK error + +**Solution:** +- Check `task.data.state` +- Verify task media type is TELEPHONY +- Check console for "Hold failed" logs + +### Issue: Transfer options not showing + +**Possible Causes:** +1. `consultTransferOptions` not configured +2. No buddy agents loaded +3. No queues configured + +**Solution:** +- Pass `consultTransferOptions` prop +- Check `buddyAgents` array in hook +- Verify `loadBuddyAgents()` was called +- Check agent permissions for transfer + +### Issue: Consult call fails + +**Possible Causes:** +1. Invalid target agent +2. Agent not available +3. Insufficient permissions + +**Solution:** +- Verify agent exists and is logged in +- Check agent state (Available) +- Check console for SDK error details + +### Issue: Auto-wrapup timer not showing + +**Possible Causes:** +1. Auto-wrapup not configured in backend +2. `controlVisibility.wrapup` is false +3. Task not in wrapup state + +**Solution:** +- Check `currentTask.autoWrapup` object +- Verify wrapup is visible: `controlVisibility.wrapup === true` +- Check task state is WRAP_UP + +### Issue: Recording button doesn't work + +**Possible Causes:** +1. Recording not enabled for tenant +2. Agent doesn't have recording permissions +3. Task type doesn't support recording + +**Solution:** +- Check tenant recording configuration +- Verify agent profile has recording permission +- Check `task.data.isRecordingEnabled` + +## Performance Considerations + +- **Task Event Subscriptions:** Registered per task, cleaned up on task change/unmount +- **Hold Timer:** Interval running while on hold (cleared on resume/unmount) +- **Auto-Wrapup Timer:** 1-second interval during wrapup countdown +- **Buddy Agents:** Fetched once on mount (cached) +- **Re-renders:** Only on currentTask or specific state changes (MobX observer) + +## Testing + +### Unit Tests + +**Widget Tests:** +- Renders without crashing +- Passes props to hook correctly +- Error boundary catches errors + +**Hook Tests:** +- toggleHold() calls task.hold()/resume() +- toggleMute() updates isMuted state +- transferCall() calls task.transfer() +- consultCall() calls task.consultCall() +- wrapupCall() calls task.wrapup() +- Task event callbacks fire correctly +- Auto-wrapup timer counts down +- Cleanup removes all callbacks + +**Component Tests:** +- All buttons render correctly +- Buttons call correct handlers +- Transfer modal opens/closes +- Wrapup dropdown populates +- Auto-wrapup timer displays countdown + +### E2E Tests + +- Active call → Hold → Resume → Success +- Active call → Transfer to agent → Success +- Active call → Consult → Transfer → Success +- Active call → Consult → Conference → Success +- Active call → End → Wrapup with code → Success +- Active call → Auto-wrapup countdown → Cancel → Success + diff --git a/packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md b/packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md new file mode 100644 index 000000000..483da35fc --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md @@ -0,0 +1,108 @@ +# IncomingTask Widget + +## Overview + +Displays incoming contact center tasks with accept/reject actions. + +## Why This Widget? + +**Problem:** Agents need to be notified of incoming tasks (calls, chats, emails) and respond quickly. + +**Solution:** Displays incoming task details with countdown timer and accept/reject buttons. + +## What It Does + +- Shows incoming task notification +- Displays caller/contact information +- Shows queue and media type +- Countdown timer (RONA - Redirection On No Answer) +- Accept button to handle the task +- Reject button to decline the task +- Auto-hides after acceptance/rejection + +## Usage + +### React + +```tsx +import { IncomingTask } from '@webex/cc-widgets'; + +function App() { + return ( + console.log('Accepted:', task)} + onRejected={({ task }) => console.log('Rejected:', task)} + /> + ); +} +``` + +### Web Component + +```html + + + +``` + +## Props API + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `incomingTask` | `ITask` | - | Incoming task object from store | +| `onAccepted` | `({ task: ITask }) => void` | - | Callback when task accepted | +| `onRejected` | `({ task: ITask }) => void` | - | Callback when task rejected/timed out | + +## Examples + +### With Callbacks + +```tsx + { + console.log('Task accepted:', task.data.interactionId); + // Navigate to task details + }} + onRejected={({ task }) => { + console.log('Task rejected:', task.data.interactionId); + // Show notification + }} +/> +``` + +### Auto-managed (via Store) + +```tsx +// Widget automatically subscribes to store.incomingTask +// No need to pass incomingTask prop if using store directly + +``` + +## Dependencies + +```json +{ + "@webex/cc-components": "workspace:*", + "@webex/cc-store": "workspace:*", + "@webex/cc-ui-logging": "workspace:*", + "mobx-react-lite": "^4.1.0", + "react-error-boundary": "^6.0.0" +} +``` + +See [package.json](../../package.json) for versions. + +## Additional Resources + +- [Architecture Details](architecture.md) - Component internals, data flows, diagrams + diff --git a/packages/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md b/packages/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md new file mode 100644 index 000000000..dac3decf7 --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md @@ -0,0 +1,269 @@ +# IncomingTask Widget - Architecture + +## Component Overview + +| Layer | File | Purpose | Key Responsibilities | +|-------|------|---------|---------------------| +| **Widget** | `src/IncomingTask/index.tsx` | Smart container | - Observer HOC
- Error boundary
- Delegates to hook
- Props: incomingTask, callbacks | +| **Hook** | `src/helper.ts` (useIncomingTask) | Business logic | - Task event subscriptions
- Accept/reject handlers
- Callback management
- Cleanup on unmount | +| **Component** | `@webex/cc-components` (IncomingTaskComponent) | Presentation | - Task card UI
- Timer display
- Accept/reject buttons
- Media type icons | +| **Store** | `@webex/cc-store` | State/SDK | - incomingTask observable
- Task event callbacks
- setTaskCallback/removeTaskCallback | + +## File Structure + +``` +task/src/ +├── IncomingTask/ +│ └── index.tsx # Widget (observer + ErrorBoundary) +├── helper.ts # useIncomingTask hook (lines 138-269) +├── task.types.ts # UseTaskProps, IncomingTaskProps +└── index.ts # Exports + +cc-components/src/components/task/IncomingTask/ +├── incoming-task.tsx # IncomingTaskComponent +└── incoming-task.utils.ts # extractIncomingTaskData +``` + +## Data Flows + +### Overview + +```mermaid +graph LR + A[IncomingTask Widget] --> B[useIncomingTask Hook] + B --> C[Store] + C --> D[Task Object] + B --> E[IncomingTaskComponent] + E --> F[User Actions] + F --> B + D --> G[Task SDK Methods] +``` + +### Hook: useIncomingTask + +**Inputs:** +- `incomingTask` - ITask object (from props or store) +- `deviceType` - 'BROWSER' | 'EXTENSION' | 'AGENT_DN' +- `onAccepted` - Callback when task accepted +- `onRejected` - Callback when task rejected +- `logger` - Logger instance + +**Subscribes to Task Events:** +- `TASK_ASSIGNED` - Task accepted successfully +- `TASK_CONSULT_ACCEPTED` - Consult accepted +- `TASK_END` - Task ended +- `TASK_REJECT` - Task rejected +- `TASK_CONSULT_END` - Consult ended + +**Returns:** +- `incomingTask` - Task object +- `accept()` - Accept task handler +- `reject()` - Reject task handler +- `isBrowser` - Boolean flag + +## Sequence Diagrams + +### Incoming Task Flow + +```mermaid +sequenceDiagram + participant B as Backend + participant S as Store + participant W as IncomingTask Widget + participant H as useIncomingTask Hook + participant C as IncomingTaskComponent + participant U as User + + B->>S: TASK_INCOMING event + S->>S: setIncomingTask(task) + S->>W: Observable update + W->>H: Initialize with incomingTask + H->>S: setTaskCallback(TASK_ASSIGNED) + H->>S: setTaskCallback(TASK_REJECT) + H->>S: setTaskCallback(TASK_END) + H-->>C: Pass {incomingTask, accept, reject} + C->>C: extractIncomingTaskData(task) + C->>U: Display task card with timer +``` + +### Accept Task + +```mermaid +sequenceDiagram + participant U as User + participant C as IncomingTaskComponent + participant H as useIncomingTask Hook + participant T as Task Object + participant S as Store + participant B as Backend + + U->>C: Click Accept button + C->>H: accept() + H->>H: logger.info('accept called') + H->>T: task.accept() + T->>B: POST /task/accept + alt Success + B-->>T: Success response + T-->>S: Emit TASK_ASSIGNED event + S->>H: TASK_ASSIGNED callback + H->>H: onAccepted({ task }) + H->>U: Parent callback notified + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Error accepting') + end +``` + +### Reject Task + +```mermaid +sequenceDiagram + participant U as User + participant C as IncomingTaskComponent + participant H as useIncomingTask Hook + participant T as Task Object + participant S as Store + participant B as Backend + + U->>C: Click Reject button OR Timer expires + C->>H: reject() + H->>H: logger.info('reject called') + H->>T: task.decline() + T->>B: POST /task/decline + alt Success + B-->>T: Success response + T-->>S: Emit TASK_REJECT event + S->>H: TASK_REJECT callback + H->>H: onRejected({ task }) + H->>U: Parent callback notified + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Error rejecting') + end +``` + +### Task Event Cleanup + +```mermaid +sequenceDiagram + participant C as Component + participant H as useIncomingTask Hook + participant S as Store + + Note over C: Component unmount OR
incomingTask changes + + C->>H: useEffect cleanup function + H->>S: removeTaskCallback(TASK_ASSIGNED) + H->>S: removeTaskCallback(TASK_CONSULT_ACCEPTED) + H->>S: removeTaskCallback(TASK_END) + H->>S: removeTaskCallback(TASK_REJECT) + H->>S: removeTaskCallback(TASK_CONSULT_END) + H-->>C: Cleanup complete +``` + +## Task Events Lifecycle + +**Subscribed Events:** +1. `TASK_ASSIGNED` → Calls `onAccepted` callback +2. `TASK_CONSULT_ACCEPTED` → Calls `onAccepted` callback (consult scenario) +3. `TASK_END` → Calls `onRejected` callback (task ended) +4. `TASK_REJECT` → Calls `onRejected` callback (explicitly rejected) +5. `TASK_CONSULT_END` → Calls `onRejected` callback (consult ended) + +**Cleanup:** All callbacks removed on component unmount or when incomingTask changes. + +## Error Handling + +| Error | Source | Handled By | Action | +|-------|--------|------------|--------| +| Task accept failed | Task SDK | Hook catch block | Log error via logger | +| Task reject failed | Task SDK | Hook catch block | Log error via logger | +| Callback execution error | Hook | try/catch in callback | Log error, continue | +| Component crash | React | ErrorBoundary | Call store.onErrorCallback | +| Missing interactionId | Hook | Early return | No action taken | + +## Troubleshooting + +### Issue: Widget not showing + +**Possible Causes:** +1. No incoming task in store +2. incomingTask prop is null/undefined + +**Solution:** +- Check `store.incomingTask` in console +- Verify task events are being received +- Check if agent is in Available state + +### Issue: Accept button doesn't work + +**Possible Causes:** +1. Button disabled (Browser mode restriction) +2. Task already accepted/rejected +3. SDK error + +**Solution:** +- Check `deviceType` (BROWSER mode may have restrictions) +- Check console for "Error accepting task" logs +- Verify task.accept() method is available + +### Issue: Reject button doesn't work + +**Possible Causes:** +1. Task already ended +2. SDK error + +**Solution:** +- Check task state in console +- Check console for "Error rejecting task" logs +- Verify task.decline() method is available + +### Issue: Callbacks not firing + +**Possible Causes:** +1. Event subscription failed +2. interactionId missing +3. Callback execution error + +**Solution:** +- Check useEffect subscription logs +- Verify task.data.interactionId exists +- Check console for callback errors + +## Performance Considerations + +- **Event Subscriptions:** Set up once per task, cleaned up on unmount +- **Re-renders:** Only when incomingTask observable changes (MobX observer) +- **Timer:** Handled by Component layer (not in hook) +- **No polling:** Event-driven architecture + +## Testing + +### Unit Tests + +**Widget Tests** (`tests/IncomingTask/index.tsx`): +- Renders without crashing +- Passes props to hook correctly +- Error boundary catches errors + +**Hook Tests** (`tests/helper.ts`): +- accept() calls task.accept() +- reject() calls task.decline() +- Task event callbacks registered +- Cleanup removes all callbacks +- onAccepted/onRejected fire correctly + +**Component Tests** (`cc-components tests`): +- Displays task information +- Countdown timer works +- Accept button calls accept handler +- Reject button calls reject handler + +### E2E Tests + +- Login → Incoming call → Accept → Task assigned +- Login → Incoming call → Reject → Task cleared +- Login → Incoming call → Timeout (RONA) → Rejected callback fires + diff --git a/packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md b/packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md new file mode 100644 index 000000000..353295670 --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md @@ -0,0 +1,80 @@ +# OutdialCall Widget + +## Overview + +A dialpad widget for agents to make outbound calls with ANI (Automatic Number Identification) selection. + +## Why This Widget? + +**Problem:** Agents need to initiate outbound calls to contacts with proper caller ID selection. + +**Solution:** Provides a dialpad interface with number validation and ANI selection. + +## What It Does + +- Displays numeric keypad for entering destination number +- Validates phone number format (E.164 and special chars) +- Fetches and displays available ANI options for caller ID +- Initiates outbound call via SDK +- Shows validation errors for invalid numbers + +## Usage + +### React + +```tsx +import { OutdialCall } from '@webex/cc-widgets'; + +function App() { + return ; +} +``` + +### Web Component + +```html + +``` + +## Props API + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| *(No props)* | - | - | All functionality handled through store | + +**Note:** OutdialCall reads `store.cc` and `store.logger` directly. No props needed. + +## Examples + +### Basic Usage + +```tsx +// Simply render - no configuration needed + +``` + +### Error Handling + +```tsx +// Widget handles errors internally via store.onErrorCallback +// Logs are available via store.logger +``` + +## Dependencies + +```json +{ + "@webex/cc-components": "workspace:*", + "@webex/cc-store": "workspace:*", + "@webex/cc-ui-logging": "workspace:*", + "mobx-react-lite": "^4.1.0", + "react-error-boundary": "^6.0.0" +} +``` + +See [package.json](../../package.json) for versions. + +## Additional Resources + +- [Architecture Details](architecture.md) - Component internals, data flows, diagrams + diff --git a/packages/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md b/packages/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md new file mode 100644 index 000000000..f5d3e5f58 --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md @@ -0,0 +1,284 @@ +# OutdialCall Widget - Architecture + +## Component Overview + +| Layer | File | Purpose | Key Responsibilities | +|-------|------|---------|---------------------| +| **Widget** | `src/OutdialCall/index.tsx` | Smart container | - Observer HOC
- Error boundary
- Delegates to hook
- No props (uses store) | +| **Hook** | `src/helper.ts` (useOutdialCall) | Business logic | - startOutdial() wrapper
- getOutdialANIEntries() fetcher
- Error handling
- Logging | +| **Component** | `@webex/cc-components` (OutdialCallComponent) | Presentation | - Dialpad UI
- Number validation
- ANI selector
- Input handling | +| **Store** | `@webex/cc-store` | State/SDK | - cc instance (SDK)
- logger instance
- agentConfig (outdialANIId) | + +## File Structure + +``` +task/src/ +├── OutdialCall/ +│ └── index.tsx # Widget (observer + ErrorBoundary) +├── helper.ts # useOutdialCall hook (lines 947-1003) +├── task.types.ts # useOutdialCallProps +└── index.ts # Exports + +cc-components/src/components/task/OutdialCall/ +├── outdial-call.tsx # OutdialCallComponent (dialpad UI) +├── outdial-call.style.scss # Styles +└── constants.ts # KEY_LIST, OutdialStrings +``` + +## Data Flows + +### Overview + +```mermaid +graph LR + A[OutdialCall Widget] --> B[useOutdialCall Hook] + B --> C[Store] + C --> D[SDK cc instance] + B --> E[OutdialCallComponent] + E --> F[User Input] + F --> E + E --> B + D --> G[Backend API] +``` + +### Hook: useOutdialCall + +**Reads from Store:** +- `store.cc` - SDK instance +- `store.logger` - Logger instance +- `store.cc.agentConfig.outdialANIId` - ANI configuration + +**Calls SDK:** +- `cc.startOutdial(destination, origin)` - Initiate outbound call +- `cc.getOutdialAniEntries({outdialANI})` - Fetch ANI options + +**Returns:** +- `startOutdial(destination, origin)` - Start outdial function +- `getOutdialANIEntries()` - Fetch ANI entries async function + +## Sequence Diagrams + +### Initial Load & Fetch ANI Entries + +```mermaid +sequenceDiagram + participant U as User + participant W as OutdialCall Widget + participant H as useOutdialCall Hook + participant C as OutdialCallComponent + participant S as Store/SDK + participant B as Backend + + U->>W: Render widget + W->>H: Initialize hook + H-->>W: Return {startOutdial, getOutdialANIEntries} + W->>C: Pass props + C->>C: useEffect - mount + C->>H: getOutdialANIEntries() + H->>S: cc.agentConfig.outdialANIId + S-->>H: "ANI123" + H->>S: cc.getOutdialAniEntries({outdialANI: "ANI123"}) + S->>B: GET /outdialANI/entries + B-->>S: OutdialAniEntry[] + S-->>H: OutdialAniEntry[] + H-->>C: Return entries + C->>C: setOutdialANIList(entries) + C->>U: Display ANI dropdown +``` + +### Make Outbound Call + +```mermaid +sequenceDiagram + participant U as User + participant C as OutdialCallComponent + participant H as useOutdialCall Hook + participant S as Store/SDK + participant B as Backend + + U->>C: Enter number "1234567890" + C->>C: validateOutboundNumber(value) + C->>C: Check regEx: ^[+1][0-9]{3,18}$ + alt Valid + C->>C: setIsValidNumber('') + C->>U: Enable dial button + else Invalid + C->>C: setIsValidNumber('Incorrect format') + C->>U: Show error, disable button + end + + U->>C: Select ANI from dropdown + C->>C: setSelectedANI(value) + + U->>C: Click dial button + C->>H: startOutdial(destination, selectedANI) + H->>H: Validate destination not empty + H->>S: cc.startOutdial(destination, origin) + S->>B: POST /outdial + alt Success + B-->>S: TaskResponse + S-->>H: Promise resolved + H->>H: logger.info('Outdial call started') + else Error + B-->>S: Error + S-->>H: Promise rejected + H->>H: logger.error('Outdial failed') + end +``` + +### Number Validation + +```mermaid +sequenceDiagram + participant U as User + participant C as OutdialCallComponent + + U->>C: Type/Click key + C->>C: handleOnClick(key) OR onChange(e) + C->>C: setDestination(newValue) + C->>C: validateOutboundNumber(newValue) + C->>C: Test regEx: ^[+1][0-9]{3,18}$ + alt Valid Format + C->>C: setIsValidNumber('') + C->>U: Clear error, enable button + else Invalid Format + C->>C: setIsValidNumber('Incorrect DN format') + C->>U: Show error text, disable button + end +``` + +## Validation Logic + +### Phone Number RegEx + +```regex +^[+1][0-9]{3,18}$ // Standard: +1234567890 (3-18 digits) +^[*#][+1][0-9*#:]{3,18}$ // Special chars start +^[0-9*#]{3,18}$ // No country code +``` + +### Validation Rules + +| Input | Valid? | Reason | +|-------|--------|--------| +| `+1234567890` | ✅ | E.164 format | +| `1234567890` | ✅ | Digits only (3-18 chars) | +| `*12#456` | ✅ | Special chars allowed | +| `12` | ❌ | Too short (< 3 digits) | +| `abc123` | ❌ | Contains letters | +| ` ` (empty) | ❌ | Empty/whitespace | + +## SDK Integration + +### Methods Used + +**1. startOutdial(destination, origin)** +- **Purpose:** Initiate outbound call +- **Parameters:** + - `destination` (string, required) - Phone number to dial + - `origin` (string, optional) - Selected ANI for caller ID +- **Returns:** Promise +- **Errors:** Empty number, invalid format, agent not available + +**2. getOutdialAniEntries({outdialANI})** +- **Purpose:** Fetch available ANI options for caller ID +- **Parameters:** + - `outdialANI` (string, required) - ANI ID from agentConfig +- **Returns:** Promise +- **Errors:** No ANI ID, fetch failed + +## Error Handling + +| Error | Source | Handled By | Action | +|-------|--------|------------|--------| +| Empty destination | Hook | Alert + early return | Show alert to user | +| Invalid format | Component | Validation state | Show error text, disable button | +| startOutdial failed | SDK | Hook catch block | Log error via logger | +| ANI fetch failed | SDK | Component catch | Set empty ANI list, log error | +| Component crash | React | ErrorBoundary | Call store.onErrorCallback | + +## Troubleshooting + +### Issue: Dial button disabled + +**Possible Causes:** +1. Destination number empty +2. Invalid number format (fails regex) + +**Solution:** +- Check validation error message below input +- Ensure number matches accepted formats +- Minimum 3 digits required + +### Issue: No ANI options in dropdown + +**Possible Causes:** +1. Agent has no outdialANIId configured +2. ANI fetch failed +3. No ANI entries exist for this agent + +**Solution:** +- Check `store.cc.agentConfig.outdialANIId` in console +- Check console for "Error fetching outdial ANI entries" +- Verify agent is configured for outbound calls + +### Issue: Outdial fails with error + +**Possible Causes:** +1. Agent not in Available state +2. Agent not configured for outdial +3. Invalid destination format +4. Network/backend error + +**Solution:** +- Check agent state (must be Available) +- Check `store.cc.agentConfig.isOutboundEnabledForAgent` +- Verify phone number format +- Check console logs for SDK error details + +### Issue: Call starts but no task appears + +**Possible Causes:** +1. Task event listeners not set up +2. IncomingTask or TaskList widget not rendered + +**Solution:** +- Ensure IncomingTask or TaskList widget is active +- Check task event subscriptions in store +- Monitor console for TASK_ASSIGNED events + +## Performance Considerations + +- **ANI Fetch:** Happens once on mount, cached in component state +- **Validation:** Runs on every keystroke, but is simple regex (fast) +- **Dial Action:** Async, user must wait for SDK response +- **No polling:** Widget doesn't poll for state changes + +## Testing + +### Unit Tests + +**Widget Tests** (`tests/OutdialCall/index.tsx`): +- Renders without crashing +- Hook called with correct params (cc, logger) +- Error boundary catches component crashes + +**Hook Tests** (`tests/helper.ts`): +- startOutdial validates empty destination +- startOutdial calls SDK with correct params +- getOutdialANIEntries fetches from SDK +- Error handling for SDK failures + +**Component Tests** (`cc-components tests`): +- Number validation works +- Keypad input appends digits +- ANI dropdown populates +- Dial button enables/disables correctly +- Validation error displays + +### E2E Tests + +- Login → OutdialCall → Enter number → Dial → Task appears +- Validate invalid number shows error +- ANI selection works + diff --git a/packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md b/packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md new file mode 100644 index 000000000..6e73f0e78 --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md @@ -0,0 +1,108 @@ +# TaskList Widget + +## Overview + +Displays all active tasks (calls, chats, emails) assigned to the agent with accept/decline/select actions. + +## Why This Widget? + +**Problem:** Agents need to view and manage multiple simultaneous tasks in a multi-session environment. + +**Solution:** Shows all active tasks in a list with quick actions for acceptance, rejection, and selection. + +## What It Does + +- Displays list of all active tasks +- Shows task details (caller, queue, media type, timestamp) +- Accept button for pending tasks +- Decline button to reject tasks +- Click to select/focus a task +- Auto-updates when tasks change +- Highlights currently selected task + +## Usage + +### React + +```tsx +import { TaskList } from '@webex/cc-widgets'; + +function App() { + return ( + console.log('Accepted:', task)} + onTaskDeclined={(task, reason) => console.log('Declined:', task, reason)} + onTaskSelected={({ task, isClicked }) => console.log('Selected:', task)} + /> + ); +} +``` + +### Web Component + +```html + + + +``` + +## Props API + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `onTaskAccepted` | `(task: ITask) => void` | - | Callback when task accepted | +| `onTaskDeclined` | `(task: ITask, reason: string) => void` | - | Callback when task declined | +| `onTaskSelected` | `({ task: ITask, isClicked: boolean }) => void` | - | Callback when task selected/clicked | + +## Examples + +### With All Callbacks + +```tsx + { + console.log('Task accepted:', task.data.interactionId); + // Show call controls for this task + }} + onTaskDeclined={(task, reason) => { + console.log('Task declined:', task.data.interactionId, reason); + // Show notification + }} + onTaskSelected={({ task, isClicked }) => { + console.log('Task selected:', task.data.interactionId); + // Focus on this task's call controls + }} +/> +``` + +### Minimal Usage + +```tsx +// Widget works without callbacks +// Automatically manages task list via store + +``` + +## Dependencies + +```json +{ + "@webex/cc-components": "workspace:*", + "@webex/cc-store": "workspace:*", + "@webex/cc-ui-logging": "workspace:*", + "mobx-react-lite": "^4.1.0", + "react-error-boundary": "^6.0.0" +} +``` + +See [package.json](../../package.json) for versions. + +## Additional Resources + +- [Architecture Details](architecture.md) - Component internals, data flows, diagrams + diff --git a/packages/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md b/packages/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md new file mode 100644 index 000000000..84d357f9e --- /dev/null +++ b/packages/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md @@ -0,0 +1,274 @@ +# TaskList Widget - Architecture + +## Component Overview + +| Layer | File | Purpose | Key Responsibilities | +|-------|------|---------|---------------------| +| **Widget** | `src/TaskList/index.tsx` | Smart container | - Observer HOC
- Error boundary
- Delegates to hook
- Props: callbacks | +| **Hook** | `src/helper.ts` (useTaskList) | Business logic | - Task event subscriptions
- Accept/decline/select handlers
- Callback management | +| **Component** | `@webex/cc-components` (TaskListComponent) | Presentation | - Task list UI
- Task cards
- Accept/decline buttons
- Selection highlighting | +| **Store** | `@webex/cc-store` | State/SDK | - taskList observable
- currentTask observable
- Task event callbacks
- setCurrentTask() | + +## File Structure + +``` +task/src/ +├── TaskList/ +│ └── index.tsx # Widget (observer + ErrorBoundary) +├── helper.ts # useTaskList hook (lines 20-136) +├── task.types.ts # UseTaskListProps, TaskListProps +└── index.ts # Exports + +cc-components/src/components/task/TaskList/ +├── task-list.tsx # TaskListComponent +├── task-list.utils.ts # Utility functions +└── styles.scss # Styles +``` + +## Data Flows + +### Overview + +```mermaid +graph LR + A[TaskList Widget] --> B[useTaskList Hook] + B --> C[Store] + C --> D[taskList Observable] + B --> E[TaskListComponent] + E --> F[User Actions] + F --> B + B --> G[Task Objects] + G --> H[Task SDK Methods] +``` + +### Hook: useTaskList + +**Inputs:** +- `cc` - SDK instance +- `taskList` - Map from store +- `deviceType` - 'BROWSER' | 'EXTENSION' | 'AGENT_DN' +- `onTaskAccepted` - Callback when task accepted +- `onTaskDeclined` - Callback when task declined +- `onTaskSelected` - Callback when task selected +- `logger` - Logger instance + +**Subscribes to Store Callbacks:** +- `store.setTaskAssigned(callback)` - Task accepted +- `store.setTaskRejected(callback, reason)` - Task rejected +- `store.setTaskSelected(callback)` - Task selected + +**Returns:** +- `taskList` - Map of all active tasks +- `acceptTask(task)` - Accept task handler +- `declineTask(task)` - Decline task handler +- `onTaskSelect(task)` - Select task handler +- `isBrowser` - Boolean flag + +## Sequence Diagrams + +### Initial Load & Display Tasks + +```mermaid +sequenceDiagram + participant S as Store + participant W as TaskList Widget + participant H as useTaskList Hook + participant C as TaskListComponent + participant U as User + + S->>S: taskList observable updated + S->>W: Observable change (MobX) + W->>H: Initialize hook + H->>S: setTaskAssigned(callback) + H->>S: setTaskRejected(callback) + H->>S: setTaskSelected(callback) + H->>S: Read store.taskList + S-->>H: Map + H-->>C: Pass {taskList, accept, decline, select} + C->>C: Convert Map to Array + C->>C: Map over tasks → Render Task cards + C->>U: Display task list +``` + +### Accept Task from List + +```mermaid +sequenceDiagram + participant U as User + participant C as TaskListComponent + participant H as useTaskList Hook + participant T as Task Object + participant S as Store + participant B as Backend + + U->>C: Click Accept on task card + C->>H: acceptTask(task) + H->>H: logger.info('acceptTask called') + H->>T: task.accept() + T->>B: POST /task/accept + alt Success + B-->>T: Success + T-->>S: Emit TASK_ASSIGNED event + S->>H: store callback: taskAssigned + H->>H: onTaskAccepted(task) + H->>U: Parent notified + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Error accepting') + end +``` + +### Decline Task from List + +```mermaid +sequenceDiagram + participant U as User + participant C as TaskListComponent + participant H as useTaskList Hook + participant T as Task Object + participant S as Store + participant B as Backend + + U->>C: Click Decline on task card + C->>H: declineTask(task) + H->>H: logger.info('declineTask called') + H->>T: task.decline() + T->>B: POST /task/decline + alt Success + B-->>T: Success + T-->>S: Emit TASK_REJECT event + S->>H: store callback: taskRejected + H->>H: onTaskDeclined(task, reason) + H->>U: Parent notified + else Error + B-->>T: Error + T-->>H: Promise rejected + H->>H: logger.error('Error declining') + end +``` + +### Select Task (Switch Focus) + +```mermaid +sequenceDiagram + participant U as User + participant C as TaskListComponent + participant H as useTaskList Hook + participant S as Store + + U->>C: Click on task card + C->>H: onTaskSelect(task) + H->>S: store.setCurrentTask(task, isClicked=true) + S->>S: Update currentTask observable + S-->>C: Re-render with updated currentTask + C->>C: Highlight selected task + H->>H: onTaskSelected({ task, isClicked: true }) + H->>U: Parent notified +``` + +## Store Callbacks + +**Set in Hook (one-time):** +1. `setTaskAssigned(callback)` - Triggered when task accepted +2. `setTaskRejected(callback)` - Triggered when task rejected +3. `setTaskSelected(callback)` - Triggered when task selected + +**Callbacks persist:** Unlike task-specific subscriptions, these are widget-level callbacks that handle all tasks. + +## Error Handling + +| Error | Source | Handled By | Action | +|-------|--------|------------|--------| +| Task accept failed | Task SDK | Hook catch block | Log error via logger | +| Task decline failed | Task SDK | Hook catch block | Log error via logger | +| Callback execution error | Hook | try/catch in callback | Log error, continue | +| Component crash | React | ErrorBoundary | Call store.onErrorCallback | +| Empty task list | Component | Early return | Render nothing | + +## Troubleshooting + +### Issue: No tasks displayed + +**Possible Causes:** +1. store.taskList is empty +2. No tasks assigned to agent +3. Task list observable not updating + +**Solution:** +- Check `store.taskList` in console +- Verify tasks exist: `store.taskList.size` +- Check task events are being received + +### Issue: Accept button doesn't work + +**Possible Causes:** +1. Task already accepted +2. Browser mode restrictions (isBrowser flag) +3. SDK error + +**Solution:** +- Check task state +- Check `deviceType` value +- Check console for "Error accepting task" logs + +### Issue: Task selection doesn't highlight + +**Possible Causes:** +1. onTaskSelect not calling store.setCurrentTask +2. currentTask observable not updating +3. CSS styling issue + +**Solution:** +- Check `store.currentTask` after clicking +- Verify `isCurrentTaskSelected` utility returns true +- Check `.selected` class applied to task card + +### Issue: Callbacks not firing + +**Possible Causes:** +1. Callbacks not provided as props +2. Store callback registration failed +3. Event not emitted by backend + +**Solution:** +- Verify callbacks passed to widget +- Check console for callback registration logs +- Monitor network tab for task events + +## Performance Considerations + +- **Observable Updates:** TaskList only re-renders when store.taskList or store.currentTask changes +- **Map to Array Conversion:** Done on every render, but taskList is typically small (<10 tasks) +- **No Polling:** Event-driven updates via MobX observables +- **Task Event Cleanup:** Not needed (callbacks are widget-level, not task-specific) + +## Testing + +### Unit Tests + +**Widget Tests** (`tests/TaskList/index.tsx`): +- Renders without crashing +- Passes props to hook correctly +- Error boundary catches errors + +**Hook Tests** (`tests/helper.ts`): +- acceptTask() calls task.accept() +- declineTask() calls task.decline() +- onTaskSelect() calls store.setCurrentTask() +- Store callbacks registered on mount +- Callbacks fire correctly + +**Component Tests** (`cc-components tests`): +- Displays all tasks in list +- Accept button calls acceptTask handler +- Decline button calls declineTask handler +- Clicking task calls onTaskSelect +- Selected task is highlighted + +### E2E Tests + +- Multi-session → Multiple tasks in list → Accept one → Task removed from pending +- Task list → Select task → Call controls show for selected task +- Task list → Decline task → Task removed +