From 1d71347a8be00c6ced7309e679929cae0954e048 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:30:52 +0530 Subject: [PATCH 1/2] docs(ai-docs): migrate to SDLC-Templates component-repo standard Reshape the repo's AI documentation to conform to the SDLC-Templates component-repo standard (library version 0.1.0-draft). Standing docs (repo-level, ai-docs/): - Reshape root AGENTS.md into the agent-entry contract - Add SPEC_INDEX.md (router + module registry), ARCHITECTURE.md - Add RULES, GLOSSARY, SECURITY, CONTRACTS, GETTING_STARTED, REVIEW_CHECKLIST, SERVICE_STATE - Add adr/ and rules/ scaffolds with seed entries Machine source of truth: - .sdd/manifest.json (per-module coverage state, commands, contracts) - .sdd/coverage-policy.defaults.yaml (drift thresholds, coverage bar) - Track .sdd/manifest.json via .gitignore exception Per-module specs (source-local /ai-docs/-spec.md): - store, cc-components, cc-widgets, station-login, user-state, task, ui-logging, test-fixtures, and legacy @webex/widgets Pre-standard per-package AGENTS.md/ARCHITECTURE.md pairs are preserved under ai-docs/_archive/pre-sdlc-migration/ rather than deleted. --- .gitignore | 1 + .sdd/coverage-policy.defaults.yaml | 19 + .sdd/manifest.json | 117 +++ AGENTS.md | 727 +++--------------- ai-docs/ARCHITECTURE.md | 133 ++++ ai-docs/CONTRACTS.md | 45 ++ ai-docs/GETTING_STARTED.md | 43 ++ ai-docs/GLOSSARY.md | 58 ++ ai-docs/README.md | 27 +- ai-docs/REVIEW_CHECKLIST.md | 41 + ai-docs/RULES.md | 139 ++-- ai-docs/SECURITY.md | 48 ++ ai-docs/SERVICE_STATE.md | 48 ++ ai-docs/SPEC_INDEX.md | 69 ++ ai-docs/_archive/pre-sdlc-migration/README.md | 20 + .../@webex/widgets/ai-docs/AGENTS.md | 0 .../@webex/widgets/ai-docs/ARCHITECTURE.md | 0 .../cc-components/ai-docs/AGENTS.md | 0 .../cc-components/ai-docs/ARCHITECTURE.md | 0 .../cc-widgets/ai-docs/AGENTS.md | 0 .../cc-widgets/ai-docs/ARCHITECTURE.md | 0 .../station-login/ai-docs/AGENTS.md | 0 .../station-login/ai-docs/ARCHITECTURE.md | 0 .../contact-center/store/ai-docs/AGENTS.md | 0 .../store/ai-docs/ARCHITECTURE.md | 0 .../ai-docs/widgets/CallControl/AGENTS.md | 0 .../widgets/CallControl/ARCHITECTURE.md | 0 .../ai-docs/widgets/IncomingTask/AGENTS.md | 0 .../widgets/IncomingTask/ARCHITECTURE.md | 0 .../ai-docs/widgets/OutdialCall/AGENTS.md | 0 .../widgets/OutdialCall/ARCHITECTURE.md | 0 .../task/ai-docs/widgets/TaskList/AGENTS.md | 0 .../ai-docs/widgets/TaskList/ARCHITECTURE.md | 0 .../test-fixtures/ai-docs/AGENTS.md | 0 .../test-fixtures/ai-docs/ARCHITECTURE.md | 0 .../ui-logging/ai-docs/AGENTS.md | 0 .../ui-logging/ai-docs/ARCHITECTURE.md | 0 .../user-state/ai-docs/AGENTS.md | 0 .../user-state/ai-docs/ARCHITECTURE.md | 0 .../packages/playwright}/ai-docs/AGENTS.md | 0 .../playwright}/ai-docs/ARCHITECTURE.md | 0 .../cc/samples-cc-react-app/ai-docs/AGENTS.md | 0 .../pre-sdlc-migration/root/AGENTS.md | 621 +++++++++++++++ .../0001-one-directional-dependency-flow.md | 53 ++ ai-docs/adr/README.md | 19 + ai-docs/rules/README.md | 17 + ai-docs/rules/sdk-access-via-store.md | 31 + .../@webex/widgets/ai-docs/widgets-spec.md | 416 ++++++++++ .../ai-docs/cc-components-spec.md | 319 ++++++++ .../cc-widgets/ai-docs/cc-widgets-spec.md | 360 +++++++++ .../ai-docs/station-login-spec.md | 363 +++++++++ .../store/ai-docs/store-spec.md | 350 +++++++++ .../contact-center/task/ai-docs/task-spec.md | 510 ++++++++++++ .../ai-docs/test-fixtures-spec.md | 227 ++++++ .../ui-logging/ai-docs/ui-logging-spec.md | 293 +++++++ .../user-state/ai-docs/user-state-spec.md | 324 ++++++++ 56 files changed, 4758 insertions(+), 680 deletions(-) create mode 100644 .sdd/coverage-policy.defaults.yaml create mode 100644 .sdd/manifest.json create mode 100644 ai-docs/ARCHITECTURE.md create mode 100644 ai-docs/CONTRACTS.md create mode 100644 ai-docs/GETTING_STARTED.md create mode 100644 ai-docs/GLOSSARY.md create mode 100644 ai-docs/REVIEW_CHECKLIST.md create mode 100644 ai-docs/SECURITY.md create mode 100644 ai-docs/SERVICE_STATE.md create mode 100644 ai-docs/SPEC_INDEX.md create mode 100644 ai-docs/_archive/pre-sdlc-migration/README.md rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/@webex/widgets/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/@webex/widgets/ai-docs/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/cc-components/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/cc-components/ai-docs/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/cc-widgets/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/cc-widgets/ai-docs/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/station-login/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/station-login/ai-docs/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/store/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/store/ai-docs/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/test-fixtures/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/test-fixtures/ai-docs/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/ui-logging/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/ui-logging/ai-docs/ARCHITECTURE.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/user-state/ai-docs/AGENTS.md (100%) rename {packages => ai-docs/_archive/pre-sdlc-migration/packages}/contact-center/user-state/ai-docs/ARCHITECTURE.md (100%) rename {playwright => ai-docs/_archive/pre-sdlc-migration/packages/playwright}/ai-docs/AGENTS.md (100%) rename {playwright => ai-docs/_archive/pre-sdlc-migration/packages/playwright}/ai-docs/ARCHITECTURE.md (100%) rename {widgets-samples => ai-docs/_archive/pre-sdlc-migration/packages/widgets-samples}/cc/samples-cc-react-app/ai-docs/AGENTS.md (100%) create mode 100644 ai-docs/_archive/pre-sdlc-migration/root/AGENTS.md create mode 100644 ai-docs/adr/0001-one-directional-dependency-flow.md create mode 100644 ai-docs/adr/README.md create mode 100644 ai-docs/rules/README.md create mode 100644 ai-docs/rules/sdk-access-via-store.md create mode 100644 packages/@webex/widgets/ai-docs/widgets-spec.md create mode 100644 packages/contact-center/cc-components/ai-docs/cc-components-spec.md create mode 100644 packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md create mode 100644 packages/contact-center/station-login/ai-docs/station-login-spec.md create mode 100644 packages/contact-center/store/ai-docs/store-spec.md create mode 100644 packages/contact-center/task/ai-docs/task-spec.md create mode 100644 packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md create mode 100644 packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md create mode 100644 packages/contact-center/user-state/ai-docs/user-state-spec.md diff --git a/.gitignore b/.gitignore index 5f35c58cb..0afe52a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ reports/ #Local stuff *.json !docs/changelog/logs/*.json +!.sdd/manifest.json .env* !.env.default !package.json diff --git a/.sdd/coverage-policy.defaults.yaml b/.sdd/coverage-policy.defaults.yaml new file mode 100644 index 000000000..fb472b570 --- /dev/null +++ b/.sdd/coverage-policy.defaults.yaml @@ -0,0 +1,19 @@ +# Coverage / drift policy defaults for the SDD doc set. +# Mirrored by ai-docs/RULES.md (Spec-Currency & Drift Thresholds) and consumed by review checks. +# See .sdd/manifest.json for per-module coverage state. + +# Maximum tolerated drift between a module spec and its code, by coverage state. +# Drift = share of spec claims (signatures, flows, contracts) no longer matching code. +driftThresholds: + AUTHORITATIVE: 0.05 # <= 5% — spec is trusted; larger drift fails the spec-currency gate + PARTIAL: 0.15 # <= 15% + DRAFT: 0.25 # <= 25% — freshly generated; cross-check code before relying on a claim + NONE: null # no spec to drift from + +# Changed-line coverage bar enforced by the test-adequacy review check (C4). +coverageBar: + changedLines: 0.80 + +# Spec-currency: the spec/docs MUST be updated in the same change as the code. +specCurrency: + sameChangeRequired: true diff --git a/.sdd/manifest.json b/.sdd/manifest.json new file mode 100644 index 000000000..dc0761b03 --- /dev/null +++ b/.sdd/manifest.json @@ -0,0 +1,117 @@ +{ + "$comment": "Machine source of truth for SDD coverage state. Human mirror: ai-docs/SPEC_INDEX.md. Generated during migration to SDLC-Templates component-repo standard 0.1.0-draft on 2026-06-29.", + "schemaVersion": "0.1.0-draft", + "templateLibraryVersion": "0.1.0-draft", + "repo": { + "name": "webex-widgets", + "displayName": "Webex Contact Center Widgets", + "type": "monorepo", + "packageManager": "yarn@4.5.1", + "workspaces": ["packages/**/*", "packages/contact-center/*", "widgets-samples/**/**"], + "standingDocsRoot": "ai-docs/", + "agentEntry": "AGENTS.md" + }, + "commands": { + "install": "yarn install", + "build": "yarn build:dev", + "test": "yarn test:cc-widgets", + "testPackage": "yarn workspace @webex/{pkg} test:unit", + "lint": "yarn test:styles", + "e2e": "yarn test:e2e" + }, + "coveragePolicy": ".sdd/coverage-policy.defaults.yaml", + "coverageStates": { + "AUTHORITATIVE": "Spec is validated and trusted; code cross-check not required for routine work.", + "PARTIAL": "Spec exists and is code-grounded but not fully validated; cross-check code for high-risk changes.", + "DRAFT": "Spec freshly generated during migration; cross-check code before relying on any claim.", + "NONE": "No module spec yet." + }, + "modules": [ + { + "id": "store", + "package": "@webex/cc-store", + "path": "packages/contact-center/store", + "spec": "packages/contact-center/store/ai-docs/store-spec.md", + "responsibility": "MobX singleton holding global CC state; proxies SDK events; sole SDK access point.", + "coverageState": "DRAFT", + "tier": 1 + }, + { + "id": "cc-components", + "package": "@webex/cc-components", + "path": "packages/contact-center/cc-components", + "spec": "packages/contact-center/cc-components/ai-docs/cc-components-spec.md", + "responsibility": "Shared presentational React UI primitives consumed by widget packages.", + "coverageState": "DRAFT", + "tier": 1 + }, + { + "id": "cc-widgets", + "package": "@webex/cc-widgets", + "path": "packages/contact-center/cc-widgets", + "spec": "packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md", + "responsibility": "r2wc Web Component wrappers aggregating widget packages for framework-agnostic consumption.", + "coverageState": "DRAFT", + "tier": 1 + }, + { + "id": "station-login", + "package": "@webex/cc-station-login", + "path": "packages/contact-center/station-login", + "spec": "packages/contact-center/station-login/ai-docs/station-login-spec.md", + "responsibility": "Agent login widget: team and device selection.", + "coverageState": "DRAFT", + "tier": 1 + }, + { + "id": "user-state", + "package": "@webex/cc-user-state", + "path": "packages/contact-center/user-state", + "spec": "packages/contact-center/user-state/ai-docs/user-state-spec.md", + "responsibility": "Agent state widget: state selection, idle codes, elapsed timer.", + "coverageState": "DRAFT", + "tier": 1 + }, + { + "id": "task", + "package": "@webex/cc-task", + "path": "packages/contact-center/task", + "spec": "packages/contact-center/task/ai-docs/task-spec.md", + "responsibility": "Task widget bundle: CallControl, CallControlCAD, IncomingTask, OutdialCall, TaskList.", + "coverageState": "DRAFT", + "tier": 1, + "subWidgets": ["CallControl", "CallControlCAD", "IncomingTask", "OutdialCall", "TaskList"] + }, + { + "id": "ui-logging", + "package": "@webex/cc-ui-logging", + "path": "packages/contact-center/ui-logging", + "spec": "packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md", + "responsibility": "Metrics/telemetry: withMetrics HOC and metricsLogger.", + "coverageState": "DRAFT", + "tier": 2 + }, + { + "id": "test-fixtures", + "package": "@webex/test-fixtures", + "path": "packages/contact-center/test-fixtures", + "spec": "packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md", + "responsibility": "Shared test mocks and helpers across CC packages.", + "coverageState": "DRAFT", + "tier": 2 + }, + { + "id": "meetings-widgets", + "package": "@webex/widgets", + "path": "packages/@webex/widgets", + "spec": "packages/@webex/widgets/ai-docs/widgets-spec.md", + "responsibility": "Legacy meetings widgets (non-CC); separate widget family in the monorepo.", + "coverageState": "DRAFT", + "tier": 2 + } + ], + "contracts": { + "index": "ai-docs/CONTRACTS.md", + "sdkApiReference": "contact-centre-sdk-apis/contact-center.json" + } +} diff --git a/AGENTS.md b/AGENTS.md index 193649a3c..4e7601d8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,621 +1,106 @@ -# Contact Center Widgets - AI Agent Guide - -## Purpose - -This is the main orchestrator for AI assistants working on this repository. It routes you to the correct templates and documentation based on the developer's task. - -**For every developer request:** (1) Identify task type (A–F below). (2) If the work is in an existing package, widget, or test framework scope, load that scope's ai-docs (see [Package and widget ai-docs reference](#package-and-widget-ai-docs-reference)) and follow its AGENTS.md. (3) Open the template for that type and complete its mandatory pre-steps (see [Mandatory pre-steps by task type](#mandatory-pre-steps-by-task-type)). (4) Then follow the rest of this guide and the template. - ---- - -## Quick Start - -**When developer provides a task, follow this workflow:** - -1. **Understand the task** - Identify what type of work is needed -2. **Break down large or multi-part tasks** - If the prompt mixes multiple tasks (for example, "create new widget" **and** "fix a bug" or "add a feature"), or the task is very large, split it into smaller, clearly scoped subtasks and handle them one by one -3. **Route to appropriate template** - Use modular templates for guidance -4. **Load package/widget ai-docs when working in that scope** - If fixing, generating, or enhancing code in an existing package or widget, read that scope's [ai-docs/AGENTS.md and ARCHITECTURE.md](#package-and-widget-ai-docs-reference) and follow them (see CRITICAL RULES rule 3). -5. **Complete that template's mandatory pre-step section** - See [Mandatory pre-steps by task type](#mandatory-pre-steps-by-task-type) in CRITICAL RULES. Do not generate code until pre-steps are done or the developer explicitly waives them. -6. **Generate/fix code** - Follow established patterns -7. **Update documentation** - Keep ai-docs in sync with code changes -8. **Ask for review** - Confirm completion with developer - ---- - -## Step 1: Identify Task Type - -**Ask developer:** "What do you need help with?" - -If the developer's message contains multiple distinct task types (for example, "create new widget", "fix a bug", and "add a feature" in one prompt), treat each as a separate internal task. Clarify priorities or ordering with the developer when needed, and then execute the subtasks sequentially rather than trying to complete everything at once. - -### Task Types - -**A. Create New Widget** -- Developer wants to build a completely new widget from scratch -- **Route to:** [templates/new-widget/00-master.md](./ai-docs/templates/new-widget/00-master.md) -- **Follow:** All 7 modules (pre-questions → validation) -- **⚠️ MANDATORY FIRST STEP:** Collect design input (see below) - -**B. Fix Bug in Existing Widget** -- Developer reports a bug or issue in existing code -- **Route to:** [templates/existing-widget/bug-fix.md](./ai-docs/templates/existing-widget/bug-fix.md) -- **Follow:** Bug fix workflow with root cause analysis - -**C. Add Feature to Existing Widget** -- Developer wants to enhance existing widget with new functionality -- **Route to:** [templates/existing-widget/feature-enhancement.md](./ai-docs/templates/existing-widget/feature-enhancement.md) -- **Follow:** Feature addition workflow with backward compatibility - -**D. Generate/Update Documentation Only** -- Developer needs documentation for existing code -- **Route to:** [templates/documentation/create-agent-md.md](./ai-docs/templates/documentation/create-agent-md.md) and [templates/documentation/create-architecture-md.md](./ai-docs/templates/documentation/create-architecture-md.md) -- **Follow:** Documentation templates (reusable for all packages) - -**E. Understanding Architecture** -- Developer needs to understand how something works -- **Read:** That scope's `ai-docs/AGENTS.md` (usage) and `ai-docs/ARCHITECTURE.md` (technical details); use [Package and widget ai-docs reference](#package-and-widget-ai-docs-reference) to find the path. -- **Available for:** station-login, user-state, store, cc-components, cc-widgets, ui-logging, test-fixtures, playwright; for task package use per-widget ai-docs (CallControl, IncomingTask, OutdialCall, TaskList). - -**F. Playwright E2E Test Work** -- Developer wants to add/update/stabilize Playwright tests, suites, sets, or test framework docs -- **Route to:** [templates/playwright/00-master.md](./ai-docs/templates/playwright/00-master.md) -- **Follow:** Playwright template workflow (pre-questions → implementation → validation) -- **⚠️ MANDATORY FIRST STEP:** Complete pre-questions in [templates/playwright/01-pre-questions.md](./ai-docs/templates/playwright/01-pre-questions.md) - ---- - -## ⚠️ CRITICAL RULES - Always Follow - -### 1. Circular Dependency Prevention - -**NEVER create these imports:** - -```typescript -// ❌ WRONG - Circular dependencies -import { Widget } from '@webex/cc-widgets'; // In widget package code -import { Widget } from '@webex/cc-widget-name'; // In cc-components code -``` - -**ALWAYS use this pattern:** - -```typescript -// ✅ CORRECT - Proper dependency flow -// In widget code: -import { Component } from '@webex/cc-components'; -import store from '@webex/cc-store'; -import { withMetrics } from '@webex/cc-ui-logging'; - -// In cc-widgets aggregator (ONLY): -import { Widget } from '@webex/cc-widget-name'; -``` - -**Dependency Flow (One Direction Only):** - -``` -cc-widgets → widget packages → cc-components → store → SDK -``` - -**Validation Before Code Generation:** - -- [ ] Widget does NOT import from `@webex/cc-widgets` -- [ ] cc-components does NOT import from any widget packages -- [ ] All imports follow one-directional flow -- [ ] No circular references between packages - -**If circular dependency is detected → STOP and refactor imports immediately.** - -### 2. Mandatory Design Input (For New Widgets) - -**STOP! Before generating ANY new widget code, collect design input.** - -#### Required Input (ONE of these): - -1. **Figma Link/File** - - Share Figma link or file - - LLM will extract design tokens, components, interactions - -2. **Screenshot/Mockup** - - Upload image of desired widget UI - - LLM will analyze colors, layout, components, spacing - -3. **Design Specification** - - Provide detailed specs: - - Colors (hex/RGB or Momentum tokens) - - Layout structure (flex/grid) - - Components needed (Button, Icon, Avatar, etc.) - - Typography (sizes, weights) - - Interactions (hover, click states) - -#### If Design Input Provided: - -**Analyze and document:** -- **Colors:** Extract hex/RGB values or Momentum tokens -- **Components:** Identify Momentum UI components to use -- **Layout:** Grid, flex, spacing patterns (8px/0.5rem grid) -- **Typography:** Sizes, weights (Momentum typography scale) -- **Interactions:** Buttons, hover states, transitions - -#### If NO Design Input: - -**ASK THE USER:** - -``` -⚠️ Design Input Required - -I cannot generate a widget without visual design reference. This ensures: -- UI matches your design system -- Correct Momentum components are used -- Proper styling and theming - -Please provide ONE of: -1. Figma link/file -2. Screenshot of desired UI -3. Detailed design specification (colors, layout, components) - -Once provided, I'll analyze it and generate the widget accordingly. -``` - -**DO NOT proceed without design input.** - -### 3. Use Package/Widget ai-docs When Working in That Scope - -When fixing, generating, or enhancing code in a package or widget, you MUST read and follow that scope's `ai-docs/AGENTS.md` (and `ai-docs/ARCHITECTURE.md` where available). See [Package and widget ai-docs reference](#package-and-widget-ai-docs-reference) in Step 2. The root AGENTS.md is the orchestrator; each package/widget AGENTS.md is the authoritative reference for that scope—use both. - -### 4. Complete Template Pre-Steps Before Code - -Before generating or changing any code, you MUST complete the **pre-step section** of the template for the task type (see table below). Either fill it from the developer's message and confirm, or ask the developer for missing items. Do not proceed to implementation steps until pre-steps are done or the developer explicitly asks to skip them. - -#### Mandatory pre-steps by task type (Rule 4) - -| Task type | Template | Mandatory pre-step (complete before code) | -| --------- | -------- | ----------------------------------------- | -| **A. Create New Widget** | [new-widget/00-master.md](./ai-docs/templates/new-widget/00-master.md) | Design input + [01-pre-questions.md](./ai-docs/templates/new-widget/01-pre-questions.md) (widget name, 4 technical inputs) | -| **B. Fix Bug** | [existing-widget/bug-fix.md](./ai-docs/templates/existing-widget/bug-fix.md) | [Pre-Fix Questions](./ai-docs/templates/existing-widget/bug-fix.md) (bug info, scope, impact, existing tests) | -| **C. Add Feature** | [existing-widget/feature-enhancement.md](./ai-docs/templates/existing-widget/feature-enhancement.md) | [Pre-Enhancement Questions](./ai-docs/templates/existing-widget/feature-enhancement.md) (feature info, requirements, compatibility, design input) | -| **D. Documentation only** | documentation templates | Optional: confirm scope with developer (no code change) | -| **E. Understanding** | Package ai-docs | None (read-only) | -| **F. Playwright E2E Test Work** | [playwright/00-master.md](./ai-docs/templates/playwright/00-master.md) | [Pre-Questions](./ai-docs/templates/playwright/01-pre-questions.md) (scope, scenarios, setup/utilities, stability expectations) | - ---- - -## Step 2: Load Context - -**Before generating code, load appropriate context:** - -### Always Read (Minimal Context) -1. **Pattern documentation** - [patterns/](./ai-docs/patterns/) folder - - [typescript-patterns.md](./ai-docs/patterns/typescript-patterns.md) - Type safety, naming conventions - - [react-patterns.md](./ai-docs/patterns/react-patterns.md) - Component patterns, hooks - - [mobx-patterns.md](./ai-docs/patterns/mobx-patterns.md) - State management with observer HOC - - [web-component-patterns.md](./ai-docs/patterns/web-component-patterns.md) - r2wc patterns - - [testing-patterns.md](./ai-docs/patterns/testing-patterns.md) - Jest, RTL, Playwright - -2. **Package/widget ai-docs (mandatory when working in that scope)** - When fixing, generating, or enhancing code in a package or widget, you **MUST** read that scope's `ai-docs/AGENTS.md` (and `ARCHITECTURE.md` where listed) and follow its instructions. The root AGENTS.md orchestrates; package/widget AGENTS.md is the authoritative reference for that scope. Use the table below to find the right path. - -#### Package and widget ai-docs reference - -| Scope you're working on | AGENTS.md | ARCHITECTURE.md | -| ----------------------- | --------- | --------------- | -| **station-login** | [packages/contact-center/station-login/ai-docs/AGENTS.md](packages/contact-center/station-login/ai-docs/AGENTS.md) | Same folder | -| **user-state** | [packages/contact-center/user-state/ai-docs/AGENTS.md](packages/contact-center/user-state/ai-docs/AGENTS.md) | Same folder | -| **task – CallControl** | [packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md](packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md) | Same folder | -| **task – IncomingTask** | [packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md](packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md) | Same folder | -| **task – OutdialCall** | [packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md](packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md) | Same folder | -| **task – TaskList** | [packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md](packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md) | Same folder | -| **store** | [packages/contact-center/store/ai-docs/AGENTS.md](packages/contact-center/store/ai-docs/AGENTS.md) | Same folder | -| **cc-components** | [packages/contact-center/cc-components/ai-docs/AGENTS.md](packages/contact-center/cc-components/ai-docs/AGENTS.md) | Same folder | -| **cc-widgets** | [packages/contact-center/cc-widgets/ai-docs/AGENTS.md](packages/contact-center/cc-widgets/ai-docs/AGENTS.md) | Same folder | -| **ui-logging** | [packages/contact-center/ui-logging/ai-docs/AGENTS.md](packages/contact-center/ui-logging/ai-docs/AGENTS.md) | Same folder | -| **samples-cc-react-app** | [widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md](widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md) | Same folder if present | -| **playwright framework** | [playwright/ai-docs/AGENTS.md](playwright/ai-docs/AGENTS.md) | [playwright/ai-docs/ARCHITECTURE.md](playwright/ai-docs/ARCHITECTURE.md) | - -**Task package note:** The task package has multiple widgets (CallControl, IncomingTask, OutdialCall, TaskList). When working on one of them, use that widget's ai-docs path above, not a generic task path. - -### Conditionally Read - -**If using SDK APIs:** -- Scan: [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) -- Find available methods, events, types -- Check method signatures before using - -**If working on Playwright tests/framework:** -- Read: `playwright/ai-docs/AGENTS.md` -- Read: `playwright/ai-docs/ARCHITECTURE.md` -- Use: `ai-docs/templates/playwright/00-master.md` and complete `01-pre-questions.md` before implementation - ---- - -## Step 3: SDK API Consultation (Before Code Generation) - -**Before using ANY SDK method, consult the SDK knowledge base.** - -### Process - -#### 1. Identify Required SDK Functionality - -Based on widget requirements, list needed operations: -- Making calls? → Search: "call", "dial", "telephony", "outdial" -- Fetching agents? → Search: "agent", "buddy", "team" -- Managing tasks? → Search: "task", "interaction", "contact" -- Checking state? → Search: "state", "status", "presence" - -#### 2. Search SDK Knowledge Base - -**File:** [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) - -**Search Strategy:** -- Use keyword search in JSON -- Look for method names, descriptions -- Check similar/related methods - -#### 3. Verify API Signature - -For each method found, confirm: -- ✅ Method name (exact spelling) -- ✅ Parameters (names, types, required vs optional) -- ✅ Return type -- ✅ Error conditions -- ✅ Usage notes - -#### 4. Use Correct Access Pattern - -```typescript -// ✅ CORRECT - Via store -await store.cc.methodName(params); - -// ❌ WRONG - Direct SDK import -import sdk from '@webex/contact-center'; -await sdk.methodName(params); -``` - -#### 5. Add Error Handling - -```typescript -try { - const result = await store.cc.methodName(params); - // Handle success - props.onSuccess?.(result); -} catch (error) { - console.error('SDK error:', error); - props.onError?.(error); -} -``` - -### Example: OutdialCall Widget - -**Requirement:** Make outbound call - -**SDK Search:** "outdial", "call", "dial" - -**Found:** `startOutdial(destination: string, origin: string)` - -**Usage:** - -```typescript -const handleDial = async (phoneNumber: string) => { - try { - await store.cc.startOutdial(phoneNumber, 'WidgetName'); - props.onDial?.(phoneNumber); - } catch (error) { - console.error('Outdial failed:', error); - props.onError?.(error); - } -}; -``` - -### Common SDK Operations - -- **Agent State:** `store.cc.setAgentState(state, reasonCode)` -- **Task Accept:** `task.accept()` -- **Task Hold:** `task.hold()` -- **Task End:** `task.end()` -- **Events:** `store.cc.on('eventName', handler)` - ---- - -## Step 4: Architecture Pattern - -**All code must follow this pattern:** - -``` -Widget (observer HOC) - ↓ -Custom Hook (business logic) - ↓ -Presentational Component (pure UI) - ↓ -Store (MobX singleton) - ↓ -SDK (Contact Center API) -``` - -**Key Rules:** -- Widget consumes SDK methods via the store (through a hook) — it NEVER calls the SDK directly -- Component NEVER accesses store (receives props) -- Always use `observer` HOC for widgets -- Always use `runInAction` for store mutations -- Always wrap with ErrorBoundary -- Always apply withMetrics HOC for exports - ---- - -## Step 5: Generate/Fix Code - -**Follow the template you were routed to in Step 1** - -You must have already completed that template's pre-step section (Pre-Enhancement Questions, Pre-Fix Questions, or 01-pre-questions as applicable); if not, do that first. - -**During code generation:** -1. Follow pattern documentation strictly -2. Reference existing widgets for examples -3. Use proper TypeScript types (no `any`) -4. Include error handling -5. Add loading/error states -6. Write tests alongside code - ---- - -## Step 5.5: Functionality Validation (CRITICAL) - -**After generating code, VERIFY functionality layer by layer.** - -### Validation Checklist - -#### 1. SDK Integration ✓ - -- [ ] SDK methods exist in [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) -- [ ] Parameters match SDK signature exactly -- [ ] Accessed via `store.cc.methodName()` (not direct import) -- [ ] Error handling present (try/catch) -- [ ] Success callbacks fire -- [ ] Error callbacks fire - -**If method not found or signature mismatch → FIX before proceeding** - -#### 2. Store Integration ✓ - -- [ ] Observable data accessed correctly -- [ ] `runInAction` used for mutations -- [ ] No direct store property assignments -- [ ] Store subscriptions cleaned up (useEffect return) - -#### 3. Event Flow ✓ - -**Trace each user interaction:** - -1. User action (click, input) → Event handler called? -2. Handler → State update triggered? -3. State update → Re-render triggered? -4. Re-render → UI updated correctly? - -**If ANY step fails → Debug and fix** - -#### 4. Data Flow ✓ - -**Trace data through ALL layers:** - -``` -User Action (UI) - ↓ -Widget Handler (index.tsx) - ↓ -Hook Method (helper.ts) - ↓ -Store/SDK Call - ↓ -Response/Observable Update - ↓ -Hook State Update - ↓ -Component Props - ↓ -UI Render -``` - -**Verify each transition is correct.** - -#### 5. UI Visual Validation ✓ - -**Compare with design input:** - -- [ ] Colors match (or use Momentum tokens) -- [ ] Spacing matches (8px/0.5rem grid) -- [ ] Components match design -- [ ] Layout matches (flex/grid structure) -- [ ] Typography matches (sizes, weights) -- [ ] Interactions work (hover, click, etc.) - -**If visual doesn't match → Update styling** - -#### 6. Import Validation ✓ - -**Check for circular dependencies:** - -```bash -# In widget code, search for: -grep -r "from '@webex/cc-widgets'" src/ -# Should return: NO MATCHES - -# In cc-components, search for: -grep -r "from '@webex/cc-.*-widget'" packages/contact-center/cc-components/src/ -# Should return: NO MATCHES -``` - -**If any matches found → Refactor imports** - -#### 7. Compiler Test ✓ - -```bash -yarn build -# Expected: NO ERRORS -``` - -**Common errors:** -- Missing types → Add type definitions -- Import errors → Check paths and exports -- Circular dependencies → Refactor imports -- Syntax errors → Fix code - -**Fix ALL compiler errors before completing.** - ---- - -## Step 6: Update Documentation - -**CRITICAL: After any code change, check if documentation needs updates** - -**Ask developer:** "The code changes are complete. Do I need to update any documentation?" - -### Documentation to Consider - -**If new widget created:** -- Generated via templates (AGENTS.md + ARCHITECTURE.md) - -**If widget modified:** -- Update: `packages/contact-center/{widget-name}/ai-docs/AGENTS.md` (if API changed) -- Update: `packages/contact-center/{widget-name}/ai-docs/ARCHITECTURE.md` (if architecture changed) -- Add: New examples to AGENTS.md (if new use cases) -- Update: Troubleshooting in ARCHITECTURE.md (if new issues discovered) - -**If store modified:** -- Update: `packages/contact-center/store/ai-docs/AGENTS.md` -- Update: `packages/contact-center/store/ai-docs/ARCHITECTURE.md` - -**If component library modified:** -- Update: `packages/contact-center/cc-components/ai-docs/AGENTS.md` - -**If new pattern established:** -- Update: Relevant pattern file in [patterns/](./ai-docs/patterns/) - -**If architecture changed:** -- Update: Relevant architecture documentation as needed - -**If Playwright E2E framework/docs changed:** -- Update: `playwright/ai-docs/AGENTS.md` -- Update: `playwright/ai-docs/ARCHITECTURE.md` -- Update relevant modules in: `ai-docs/templates/playwright/` - ---- - -## Step 7: Validation & Review - -**Before marking task complete:** - -1. **Run validation checks** - - Tests pass: `yarn test:unit` - - Linting passes: `yarn test:styles` - - Build succeeds: `yarn build` - -2. **Code quality checks** - - Follows patterns - - No layer violations - - Error handling present - - Types are correct - - Code is precise and concise (no unnecessary complexity or dead code) - -3. **Documentation checks** - - AGENTS.md updated if needed - - ARCHITECTURE.md updated if needed - - Examples work - -4. **Ask developer for review:** - - "Task complete. Would you like to review the changes?" - - "Should I make any adjustments?" - - "Is the documentation clear?" - ---- - -## Repository Structure - -``` -ccWidgets/ -├── packages/contact-center/ -│ ├── ai-docs/migration/ # Task refactor migration docs (old → new) -│ ├── station-login/ # Widget with ai-docs/ -│ ├── user-state/ # Widget with ai-docs/ -│ ├── task/ # Widget package -│ ├── store/ # MobX store with ai-docs/ -│ ├── cc-components/ # React components with ai-docs/ -│ ├── cc-widgets/ # Web Component wrappers with ai-docs/ -│ ├── ui-logging/ # Metrics utilities with ai-docs/ -│ └── test-fixtures/ # Test mocks with ai-docs/ -├── widgets-samples/ -│ └── cc/ -│ ├── samples-cc-react-app/ # React sample -│ └── samples-cc-wc-app/ # Web Component sample -├── playwright/ # E2E tests -└── ai-docs/ - ├── AGENTS.md # This file - ├── RULES.md # Repository rules - ├── patterns/ # Repo-wide patterns - ├── templates/ # Code generation templates - └── contact-centre-sdk-apis/ # SDK API reference -``` - ---- - -## Common Questions to Ask - -**Before starting any work:** -- "What component/widget are you working on?" -- "Is this a new widget, bug fix, or enhancement?" -- "Do you have design specifications (Figma, screenshots)?" - -**During code generation:** -- "Should I add/update tests?" -- "Do you want examples in documentation?" -- "Should I update the sample apps?" - -**After code generation:** -- "The code is complete. Should I update documentation?" -- "Would you like to review before I mark this complete?" -- "Should I check for any other impacted components?" - ---- - -## SDK Knowledge Base - -**Location:** [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) - -**Contents:** -- All exposed SDK APIs (methods, events, types) -- Method signatures and parameters -- Event names and data structures -- Links to SDK source code (next branch) - -**Usage:** -- Scan JSON when using SDK methods -- Search for API by name or functionality -- Check parameter types and return values -- Verify event names before subscribing - -**Note:** This JSON is TypeDoc output from @webex/contact-center SDK - ---- - -## Success Criteria - -**Code generation/fix is successful when:** -- ✅ Follows architecture pattern (Widget → Hook → Component → Store → SDK) -- ✅ Uses patterns correctly (TypeScript, React, MobX, WC) -- ✅ Includes proper error handling -- ✅ Has tests with good coverage -- ✅ Documentation is updated (if code changed) -- ✅ Works in both sample apps (React + WC) -- ✅ No console errors or warnings -- ✅ Passes validation checks -- ✅ Developer approves changes - ---- - -## Related Documentation - -- **Repository Rules:** [RULES.md](./RULES.md) -- **Templates Overview:** [templates/README.md](./ai-docs/templates/README.md) -- **Task Refactor Migration (Contact Center):** [packages/contact-center/ai-docs/migration/migration-overview.md](./packages/contact-center/ai-docs/migration/migration-overview.md) — overview and entry point for CC SDK task-refactor migration docs - ---- - -_Last Updated: 2025-11-26_ +# AGENTS.md — webex-widgets (Contact Center) + +> You are the agent entry point — read first. Next: router [`SPEC_INDEX.md`](ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ai-docs/ARCHITECTURE.md). Load this + `SPEC_INDEX.md` first; pull module/standing docs on demand. +> Context-efficiency: link to canonical docs — don't duplicate them; keep this file under ~200 lines. + +> Cross-tool context file. Auto-loaded by AI coding agents. A module's high-level design lives in its +> manifest-routed module spec, source-local as `/ai-docs/-spec.md` — not in an `AGENTS.md`. + +## Repo Overview +**webex-widgets** is a Yarn (PnP) monorepo of Webex Contact Center UI widgets — TypeScript, React 18, +MobX, and Web Components (r2wc) — that embed agent-desktop capabilities (login, state, call/task control) +into host applications. + +**What it is:** +- A library monorepo publishing CC widget packages + r2wc Web Component wrappers. +- A thin UI layer over the `@webex/contact-center` SDK, mediated by a single MobX store. + +**What it is NOT:** +- ❌ NOT the Contact Center SDK or backend — it does not own telephony, routing, or agent data. +- ❌ NOT a standalone application — widgets are embedded by host apps (sample apps live in `widgets-samples/`). +- ❌ NOT the owner of any persistent datastore — all domain data comes from the SDK at runtime. + +## Tech Stack +- TypeScript, React 18, MobX, Web Components via `@r2wc/react-to-web-component`. +- Yarn 4.5.1 (PnP, workspaces); Webpack + Babel build. +- Jest + React Testing Library (unit); Playwright (E2E). Momentum UI design system. + +## Architecture +``` +Host app / Web Component + → Widget (observer HOC) → Custom Hook (helper.ts) → Presentational Component (cc-components) + → Store (MobX singleton, Store.getInstance()) → @webex/contact-center SDK +``` +Dependency flow is one direction only: `cc-widgets → widget packages → cc-components → store → SDK`. +→ Full repo architecture & component responsibilities: **[ARCHITECTURE.md](./ai-docs/ARCHITECTURE.md)** + +## Module / Package Structure +``` +packages/contact-center/ +├── store/ # @webex/cc-store — MobX singleton; sole SDK access point +├── cc-components/ # @webex/cc-components — shared presentational React primitives +├── cc-widgets/ # @webex/cc-widgets — r2wc Web Component wrappers (aggregator) +├── station-login/ # @webex/cc-station-login — agent login widget +├── user-state/ # @webex/cc-user-state — agent state widget +├── task/ # @webex/cc-task — CallControl, IncomingTask, OutdialCall, TaskList, CallControlCAD +├── ui-logging/ # @webex/cc-ui-logging — withMetrics, metricsLogger +└── test-fixtures/ # @webex/test-fixtures — shared test mocks/helpers +packages/@webex/widgets/ # @webex/widgets — legacy meetings widgets (separate family) +widgets-samples/ # React + Web Component sample apps +playwright/ # E2E suites +``` +→ Per-module docs and the spec router: **[ai-docs/SPEC_INDEX.md](./ai-docs/SPEC_INDEX.md)** + +## Critical Rules +1. **Code is the source of truth.** Never invent an SDK method, event, path, flag, or constant — read the + real file (SDK surface: `contact-centre-sdk-apis/contact-center.json`). +2. **Ask before coding.** Present a plan / Spec Summary; wait for confirmation before non-trivial changes. +3. **One-directional dependency flow.** `cc-widgets → widgets → cc-components → store → SDK`. Never import + upstream (cc-components must not import widget packages; widgets must not import cc-widgets). +4. **SDK only through the store.** Call `store.cc.methodName()` — never import the SDK directly in a widget + or component. +5. **MobX discipline.** Widgets consuming store data use the `observer()` HOC; all store mutations run in + `runInAction()`. +6. **No `any` types.** Strongly type props and public surfaces; co-locate types in `*.types.ts`. +7. **Wrap widgets** with `ErrorBoundary` and the `withMetrics` HOC. +8. **No PII or credentials** in logs. +9. **Spec-currency.** Update the module spec / standing doc in the SAME change as the code (see `ai-docs/RULES.md`). + +## Essential Commands +| Task | Command | +|---|---| +| Install | `yarn install` | +| Build (all packages) | `yarn build:dev` | +| Test (a package) | `yarn workspace @webex/{pkg} test:unit` | +| Test (all CC widgets) | `yarn test:cc-widgets` | +| Lint / styles | `yarn test:styles` | +| E2E | `yarn test:e2e` | + +Always use `yarn workspace` commands for tests — never `npx jest` directly. Worktrees need +`yarn install` + `yarn build:dev` before anything works (no node_modules by default). + +## Common Gotchas +1. Cross-package TypeScript imports require `yarn build:dev` first — a fresh clone/worktree fails type-check + until packages are built. +2. Pre-commit hooks run the full test suite, so commits can take a while — don't assume a hang. +3. The store is a singleton (`Store.getInstance()`); tests that mutate it must reset state or they leak + across cases. +4. `@webex/widgets` (meetings) is a separate widget family — CC rules and the store do not apply to it. + +## Pre-Commit Checklist +- [ ] Tests pass (`yarn test:cc-widgets` or the touched package); coverage meets the repo bar. +- [ ] Module spec / standing doc updated in the same change (spec-currency). +- [ ] No upstream imports; SDK accessed only via the store. +- [ ] No `any` types; no hardcoded secrets; no PII/credentials logged. + +## External Source Access +| Provider class | Source / host pattern | Preferred access | If unavailable | +|---|---|---|---| +| ticket-tracker | Jira (`jira-eng-*`) | MCP connector / REST | STOP and ask — never guess | +| source-host | GitHub `webex/widgets` | `gh` CLI | STOP and ask | +| SDK reference | `contact-centre-sdk-apis/contact-center.json` | local file (TypeDoc) | STOP and ask — never invent an API | + +--- +**SDD coverage:** this repo's per-module coverage state lives in `.sdd/manifest.json` (human mirror in +[`ai-docs/SPEC_INDEX.md`](ai-docs/SPEC_INDEX.md)). Use that state to decide whether a spec is authoritative +or code must be cross-checked. All module specs are currently `DRAFT` (freshly generated) — cross-check code. diff --git a/ai-docs/ARCHITECTURE.md b/ai-docs/ARCHITECTURE.md new file mode 100644 index 000000000..6dcdaea3a --- /dev/null +++ b/ai-docs/ARCHITECTURE.md @@ -0,0 +1,133 @@ +# ARCHITECTURE — webex-widgets (Contact Center) + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md). This is the system architecture; per-module detail lives in each manifest-routed module spec, source-local as `/ai-docs/-spec.md`. +> Context-efficiency: link to canonical docs — don't duplicate them; this loads on demand, not upfront. + +## Design Overview +webex-widgets is a library monorepo that packages Webex Contact Center agent-desktop capabilities as +embeddable UI. The guiding design choice is a **strict one-directional layering** that isolates SDK +coupling: every widget renders through a presentational component, derives its data and callbacks from a +single MobX store, and the store is the only layer that touches the `@webex/contact-center` SDK. This keeps +presentational components pure and framework-agnostic, lets the r2wc layer expose the same widgets as Web +Components for non-React hosts, and means SDK changes ripple through exactly one boundary (the store). + +State is centralized in a MobX **singleton** (`Store.getInstance()`) so independently-mounted widgets +(login, state, call control) share one coherent view of the agent session without prop drilling or +cross-widget coupling. Widgets observe the store via the `observer()` HOC; the store proxies SDK events +into observables and exposes convenience methods for SDK calls and list fetches. + +The repo owns no persistent data — all domain data (teams, queues, tasks, agent state) is fetched from the +SDK at runtime — so there is no datastore, schema, or migration discipline to document. + +## Component Inventory & Responsibilities +| Component | Responsibility (one line) | Docs | +|---|---|---| +| `store/` | MobX singleton: global CC state, SDK event wiring, SDK access surface | `packages/contact-center/store/ai-docs/store-spec.md` | +| `cc-components/` | Pure presentational React primitives (props-only) | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` | +| `cc-widgets/` | r2wc Web Component wrappers; aggregates and exports all widgets | `packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md` | +| `station-login/` | Agent login widget (team + device selection) | `packages/contact-center/station-login/ai-docs/station-login-spec.md` | +| `user-state/` | Agent state widget (state, idle codes, timer) | `packages/contact-center/user-state/ai-docs/user-state-spec.md` | +| `task/` | Task widgets: CallControl, CallControlCAD, IncomingTask, OutdialCall, TaskList | `packages/contact-center/task/ai-docs/task-spec.md` | +| `ui-logging/` | Metrics/telemetry (`withMetrics` HOC, `metricsLogger`) | `packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md` | +| `test-fixtures/` | Shared test mocks/helpers | `packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md` | +| `@webex/widgets/` | Legacy meetings widgets (separate family) | `packages/@webex/widgets/ai-docs/widgets-spec.md` | + +## Component Interaction +```mermaid +graph TD + Host[Host app / Web Component] --> Widget + subgraph WidgetPackages[Widget packages: station-login, user-state, task] + Widget[Widget = observer HOC] --> Hook[Custom hook helper.ts] + end + Hook --> Comp[Presentational component cc-components] + Hook --> Store[(MobX store singleton)] + Comp -->|metrics| UILog[ui-logging] + Store --> SDK[@webex/contact-center SDK] + CCW[cc-widgets r2wc] -.wraps.-> Widget +``` +A host mounts a widget (directly in React, or as a custom element via `cc-widgets`). The widget is an +`observer` that calls its custom hook (`helper.ts`); the hook reads store observables and invokes +`store.cc.*` methods. The presentational component (`cc-components`) receives everything via props and emits +metrics through `ui-logging`. The store proxies SDK events back into observables, which re-render observers. + +## Execution & Flow +**Init & Call Flow (library):** Host calls `store.init(...)` / `Store.getInstance()` → store registers with +the `@webex/contact-center` SDK and subscribes to events → host mounts a widget → widget `observer` +subscribes to store observables → user action (e.g. set state, accept task) → hook calls `store.cc.()` +→ SDK responds and/or emits an event → store updates observables in `runInAction()` → observing widgets +re-render. Grounded in `packages/contact-center/store/src/store.ts` and `storeEventsWrapper.ts`. + +## Dependencies +| Dependency | Type (internal / external / peer) | How used | Failure / version handling | +|---|---|---|---| +| `@webex/contact-center` (SDK) | external | All telephony/agent/task operations; accessed only via the store | Errors surfaced through store callbacks; version per package.json | +| `@webex/cc-store` | internal | Shared singleton state; imported by all widget + component packages | Workspace-pinned | +| `@webex/cc-components` | internal | Presentational components used by widget packages | Workspace-pinned | +| `@webex/cc-ui-logging` | internal | Metrics/telemetry HOC + logger | Workspace-pinned | +| `react` / `react-dom` | peer | UI runtime | Peer `^18` | +| `mobx` / `mobx-react-lite` | external | Reactive state + `observer` | Per package.json | +| `@momentum-ui/*` / Momentum design | external | UI primitives, CSS | Per package.json | +| `@r2wc/react-to-web-component` | external | React→Web Component wrapping (cc-widgets only) | Per package.json | + +## State Model + +The store holds the client-side session model: agent profile/state, login options (teams, device type), +task map and per-task lifecycle, and fetched lists (queues, entry points, buddy agents, address book). +Transitions are driven by user-invoked `store.cc.*` methods and by SDK events the store proxies; all +mutations occur in `runInAction()`. Detailed slices live in `store-spec.md`. + +## Cross-Cutting Concerns +- **Security:** No secrets in the repo; the SDK holds the authenticated Webex session and tokens. Widgets + never handle raw credentials. Never log PII or credentials. See `SECURITY.md`. +- **Observability:** Metrics/telemetry go through `ui-logging` (`withMetrics`, `metricsLogger`); widgets are + wrapped with `withMetrics` and an `ErrorBoundary`. + +## Non-Functional Posture +**Footprint & Compatibility:** Published as consumable packages + Web Components. React `^18` peer; widgets +must mount in both React hosts and framework-agnostic hosts (via r2wc). Prefer memoization and MobX batching +to avoid unnecessary re-renders. Backward compatibility of exported surfaces and custom-element names is a +release concern (see `CONTRACTS.md`). + + +## Package Map & Inter-Package Dependencies +- **Workspace tooling:** Yarn 4.5.1 (PnP). Workspace globs: `packages/**/*`, `packages/contact-center/*`, + `widgets-samples/**/**`. +- **Inter-package dependency graph** (from each package's `package.json`): +``` +cc-widgets ── wraps ──> station-login, user-state, task (+ cc-digital-channels) +station-login ─┐ +user-state ─┼──> cc-components, cc-store +task ─┘ +cc-components ──> cc-store, cc-ui-logging +ui-logging ──> cc-store +store ──> (no internal CC deps) ──> @webex/contact-center SDK +``` +- **Visibility:** `cc-widgets` is the public aggregator (consumers import widgets/store from it); + `store`, `cc-components`, `ui-logging` are shared internals. +- **Version-sync rule:** workspace-internal deps are pinned across the monorepo; releases via + semantic-release (`release:widgets`). +- **Different-kind package:** `packages/@webex/widgets` is the legacy **meetings** widget family — it does + not participate in the CC dependency flow or share the CC store. + + +## Release & Versioning +- Published as `@webex/*` packages; release driven by `semantic-release` (`yarn release:widgets`). +- Public surfaces (exports, custom-element tag names, events) follow semver; breaking changes need a major + bump and a consumer transition note. See `CONTRACTS.md` for the compatibility policy. + + +## Host Integration & Theming +- Widgets mount in two ways: React components (import from the widget package or `cc-widgets`) and custom + elements (r2wc, registered by `cc-widgets`). Hosts must load Momentum UI CSS + (`@momentum-ui/core/css/momentum-ui.min.css`, imported by `cc-widgets`). Peer React `^18`. + +--- +→ Per-module orientation and detailed design live in each manifest-routed module spec, source-local as +`/ai-docs/-spec.md`. Routing: `SPEC_INDEX.md`. + +## Architecture Reference Links +| Reference | Location | When to read | +|---|---|---| +| Architecture decisions | `adr/` | To understand why major design choices were made and what alternatives were rejected | +| Repo patterns | `patterns/` | To follow established implementation conventions (TypeScript, React, MobX, testing) | +| Enforceable rules | `RULES.md` + `rules/` | To understand constraints every architecture-affecting change must obey | diff --git a/ai-docs/CONTRACTS.md b/ai-docs/CONTRACTS.md new file mode 100644 index 000000000..6d2510676 --- /dev/null +++ b/ai-docs/CONTRACTS.md @@ -0,0 +1,45 @@ +# Contracts Catalog — webex-widgets (Contact Center) + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this root contract index; detailed contracts live with owning modules, feature designs, or canonical schema files. Machine source `.sdd/manifest.json`. +> Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. + +> Read before adding any public-facing surface — check here first. Machine source of truth: `.sdd/manifest.json`. + +This repo is consumed as libraries: its public surface is the package **exports**, the **r2wc custom elements** registered by `cc-widgets`, and the **widget event/callback props** — not HTTP routes. There are no HTTP endpoints, no message broker, and no CLI; those template sections are dropped. + +### Exported API & Types + +The aggregator package `@webex/cc-widgets` re-exports every widget plus the `store` singleton (`packages/contact-center/cc-widgets/src/index.ts:8-19`). When imported via the bundled `wc.ts` entry, each widget is also registered as a custom element (tag names below). + +| Contract ID | Owner module/package | Symbol | Signature | Stability / deprecation | Schema / detail link | Defined at | +|---|---|---|---|---|---|---| +| cc-widgets.StationLogin | `@webex/cc-station-login` | `StationLogin` | React component; custom element `widget-cc-station-login`; props `onLogin`, `onLogout` (functions) | stable semver; tag name is breaking surface | `packages/contact-center/station-login/ai-docs/station-login-spec.md` | `packages/contact-center/station-login/src/index.ts` | +| cc-widgets.UserState | `@webex/cc-user-state` | `UserState` | React component; custom element `widget-cc-user-state`; prop `onStateChange` (function) | stable semver | `packages/contact-center/user-state/ai-docs/user-state-spec.md` | `packages/contact-center/user-state/src/index.ts` | +| cc-widgets.IncomingTask | `@webex/cc-task` | `IncomingTask` | React component; custom element `widget-cc-incoming-task`; props `incomingTask` (json), `onAccepted`, `onRejected` (functions) | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | +| cc-widgets.CallControl | `@webex/cc-task` | `CallControl` | React component; custom element `widget-cc-call-control`; props `onHoldResume`, `onEnd`, `onWrapUp`, `onRecordingToggle` (functions) | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | +| cc-widgets.CallControlCAD | `@webex/cc-task` | `CallControlCAD` | React component; custom element `widget-cc-call-control-cad`; props `onHoldResume`, `onEnd`, `onWrapUp`, `onRecordingToggle` (functions) | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | +| cc-widgets.TaskList | `@webex/cc-task` | `TaskList` | React component; custom element `widget-cc-task-list`; props `onTaskAccepted`, `onTaskDeclined`, `onTaskSelected` (functions), `hasCampaignPreviewEnabled` (boolean) | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | +| cc-widgets.OutdialCall | `@webex/cc-task` | `OutdialCall` | React component; custom element `widget-cc-outdial-call`; no declared props | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | +| cc-widgets.RealTimeTranscript | `@webex/cc-task` | `RealTimeTranscript` | React component; custom element `widget-cc-realtime-transcript`; props `liveTranscriptEntries` (json), `className` (string) | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | +| cc-widgets.DigitalChannels | `@webex/cc-digital-channels` | `DigitalChannels` | React component; custom element `widget-cc-digital-channels`; no declared props | stable semver | `packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md` | `packages/contact-center/cc-digital-channels/src/index.tsx` | +| cc-widgets.store | `@webex/cc-store` | `store` | MobX singleton (`Store.getInstance()`); `init(options: InitParams, setupEventListeners): Promise`; sole SDK access point via `store.cc.*` | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/index.ts:1,4` | +| store.types | `@webex/cc-store` | Type re-exports (`IContactCenter`, `ITask`, `Profile`, `Team`, `AgentLogin`, `IStore`, `ILogger`, `InitParams`, `IWebex`, `RealTimeTranscriptionData`, plus ~20 more) | TypeScript `type`/`interface` exports describing the SDK-backed domain surface | stable semver; SDK-shaped types track SDK | `contact-centre-sdk-apis/contact-center.json` (SDK source); `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/store.types.ts:334-366` | +| store.constants | `@webex/cc-store` | Value/enum re-exports (`CC_EVENTS`, `TASK_EVENTS`, `LoginOptions`, `ConsultStatus`, `CAMPAIGN_PREVIEW_OUTBOUND_TYPES`, `DESKTOP`, `EXTENSION`, etc.) | Exported consts/enums for event names and login/consult/campaign domain values | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/store.types.ts:368-403` | +| store.task-utils | `@webex/cc-store` | Pure task helpers (`isIncomingTask`, `getTaskStatus`, `getConsultStatus`, `getConferenceParticipants`, `isInteractionOnHold`, `findHoldStatus`, etc.) | `(task: ITask, agentId?: string) => boolean \| string \| number \| Participant[]` selectors over SDK task objects | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/task-utils.ts` | + +## Requires — what this repo depends on +| Dependency (service / package / datastore) | What is consumed | Schema / detail link | Availability assumption | Fallback on failure | Version floor | +|---|---|---|---|---|---| +| `@webex/contact-center` SDK | The entire CC runtime: `Webex.init()`, `webex.cc.*` methods, CC/task event stream, agent `Profile`, `webex.credentials.getUserToken()` | `contact-centre-sdk-apis/contact-center.json` (TypeDoc); consumed only via the store (`packages/contact-center/store/src/storeEventsWrapper.ts`) | Host establishes the authenticated Webex session; SDK assumed reachable | `Store.init()` rejects after a 6s init timeout; widgets stay inert and surface error UI (`packages/contact-center/store/src/store.ts:140-142`) | Pinned by the SDK dependency in each package's `package.json` | +| `react` / `react-dom` (18) | Component runtime; consumer peer dependency | React docs | Provided by host or bundled | N/A (build-time/runtime peer) | React 18 | +| `mobx` / `mobx-react-lite` | Store reactivity (`runInAction`, `observer`) | MobX docs | Bundled with store package | N/A | per `package.json` | +| `@r2wc/react-to-web-component` | Wraps React widgets as custom elements (`packages/contact-center/cc-widgets/src/wc.ts:1`) | r2wc docs | Bundled with `cc-widgets` | N/A | per `package.json` | + +## Compatibility & Deprecation Policy +- **Breaking-change rule:** Semver. A breaking change requires a major version bump. The breaking surface is: the named **package exports** above, the **custom-element tag names** (`widget-cc-*` in `packages/contact-center/cc-widgets/src/wc.ts:68-78`), and the **declared widget event/callback prop names**. Renaming/removing any of these, or changing a callback signature, is breaking. Adding a new widget, export, or optional prop is additive (minor). +- **Deprecation:** Releases are cut by `semantic-release` (`package.json`, `.releaserc`); commit type drives the version bump (`fix` → patch, `feat` → minor, breaking footer → major). Deprecated surfaces are kept for at least one minor release with a JSDoc `@deprecated` note before removal in a major. + +## Maintenance +- When a public surface is added/changed/removed, update this catalog, the owning module spec summary, the SDK detail source where relevant, and `.sdd/manifest.json` in the same change. +- For incompatible changes, include the consumer transition/deprecation plan in the owning module spec and summarize it in the Compatibility / deprecation column. +- Cross-reference: security posture → [`SECURITY.md`](SECURITY.md); current dependencies/flags → [`SERVICE_STATE.md`](SERVICE_STATE.md). diff --git a/ai-docs/GETTING_STARTED.md b/ai-docs/GETTING_STARTED.md new file mode 100644 index 000000000..bfdae5abb --- /dev/null +++ b/ai-docs/GETTING_STARTED.md @@ -0,0 +1,43 @@ +# Getting Started — Webex Contact Center Widgets + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc to get a build/test loop running. +> Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. + +## Prerequisites +- Node `lts/krypton` (Node 23.x — see `.nvmrc`; `nvm use` to select it). +- Yarn 4.5.1 via Corepack — this is a Yarn PnP monorepo, `packageManager` is pinned in `package.json`. Run `corepack enable` if `yarn` is unavailable. +- Required access: none beyond the public registry to build. The Webex Contact Center SDK (`@webex/contact-center`) is a published npm dependency, not a sibling clone. + +## Clone & Install +```bash +git clone git@github.com:webex/widgets.git +cd widgets +corepack enable # if yarn is missing +yarn install # installs all workspace deps (Yarn PnP) +yarn build:dev # build all packages — required before cross-package tsc imports resolve +``` +A fresh clone (or a new git worktree) has no `node_modules`; `yarn install` followed by `yarn build:dev` must complete before any per-package test or cross-package import will resolve. + +## Build / Run / Test +| Task | Command | +|---|---| +| Build | `yarn build:dev` | +| Run (local) | `yarn samples:serve` (builds with `yarn samples:build` first; or `yarn samples:serve-react` / `yarn samples:serve-wc`) | +| Test (all CC widgets) | `yarn test:cc-widgets` | +| Test (one package) | `yarn workspace @webex/{pkg} test:unit` | +| Style tests | `yarn test:styles` | +| E2E | `yarn test:e2e` (Playwright) | +| Lint / format | ESLint (`.eslintrc`, airbnb + prettier) — runs via editor/CI; pre-commit husky hooks run the full test suite | + +Never run `npx jest` directly — always go through `yarn workspace @webex/{pkg} test:unit` so the PnP resolver and workspace config apply. + +## First-Run Verification +- After `yarn install && yarn build:dev`, run a single package's tests to confirm the toolchain is wired: + ```bash + yarn workspace @webex/cc-store test:unit + ``` + A passing suite confirms install, build, and the Jest + RTL harness all work end to end. + +## Where to Go Next +- Agent entry: `../AGENTS.md` · System shape: `ARCHITECTURE.md` · Routing: `SPEC_INDEX.md` +- Conventions: `patterns/` + `rules/` (and `RULES.md`). diff --git a/ai-docs/GLOSSARY.md b/ai-docs/GLOSSARY.md new file mode 100644 index 000000000..4727a5ca3 --- /dev/null +++ b/ai-docs/GLOSSARY.md @@ -0,0 +1,58 @@ +# Glossary — webex-widgets + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc; related module specs are indexed in `SPEC_INDEX.md`. +> Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. + +> Read this before naming anything. Use the canonical name exactly; never introduce a synonym. Find a term +> in code that isn't here? Add it rather than guessing its meaning. + +## Domain Terms + +| Term | Definition (one or two sentences) | Authoritative location (file/type) | Notes / synonyms to avoid | +|---|---|---|---| +| Store | The single MobX store class holding global Contact Center state (`teams`, `idleCodes`, `agentId`, `currentState`, `cc`, etc.) and proxying SDK events into observables. It is the sole SDK access point. | `packages/contact-center/store/src/store.ts` (`class Store implements IStore`) | Not "state container" / "Redux store". | +| `Store.getInstance()` | Static accessor returning the lazily-created singleton `Store` instance; the default export of `@webex/cc-store` is this instance. | `packages/contact-center/store/src/store.ts` (`public static getInstance()`) | Never `new Store()`. Import as `import store from '@webex/cc-store'`. | +| Widget | A self-contained CC feature unit following the Widget → Hook → Component flow; the exported widget is an `observer()` component wrapped in an `ErrorBoundary`. | `packages/contact-center/{station-login,user-state,task}/src/.../index.tsx` | Not "component" (that means the presentational layer here). | +| observer HOC | `observer()` from `mobx-react-lite` wrapping a widget so it re-renders when the store observables it reads change. | `packages/contact-center/user-state/src/user-state/index.tsx`; pattern in `ai-docs/patterns/mobx-patterns.md` | Not "connect" / "subscribe". | +| helper hook | The custom hook (e.g. `useUserState`, `useCallControl`) in a package's `helper.ts` that holds business logic, SDK calls (via the store), and event wiring; widgets call it, components do not. | `packages/contact-center/{pkg}/src/helper.ts` | Not "service" / "controller". | +| Presentational component | Pure-UI React component receiving props only, living in `cc-components`; never accesses the store or SDK. | `packages/contact-center/cc-components/src/` (e.g. `CallControlCADComponent`) | Not "widget". Suffix `...Component`. | +| Task | An active contact/interaction (call, etc.) the agent is handling, carrying queue/entry-point metadata; modeled as `ITask`. | `packages/contact-center/task/src/task.types.ts` (`ITask`); store utils `packages/contact-center/store/src/task-utils.ts` | Not "session" / "interaction" in code identifiers. | +| IncomingTask | The widget that presents a ringing/offered task to the agent (accept/decline). | `packages/contact-center/task/src/IncomingTask/index.tsx` | Not "RingingTask". | +| CallControl | The widget providing in-call controls (hold/resume, transfer, consult, mute, end). | `packages/contact-center/task/src/CallControl/index.tsx` | Distinct from CallControlCAD. | +| CallControlCAD | CallControl variant surfacing Call-Associated Data and wrapup codes alongside controls. | `packages/contact-center/task/src/CallControlCAD/index.tsx` | "CAD" = Call-Associated Data; do not expand as "computer-aided". | +| OutdialCall | The widget that initiates an outbound (outdial) call. | `packages/contact-center/task/src/OutdialCall/index.tsx` | Not "Dialer". | +| TaskList | The widget listing the agent's active tasks. | `packages/contact-center/task/src/TaskList/index.tsx` | Not "queue list". | +| RealTimeTranscript | Task sub-widget rendering a live transcript of the interaction. | registered in `packages/contact-center/cc-widgets/src/wc.ts`; exported from `@webex/cc-task` | — | +| idle code | A configured non-available agent state/reason (e.g. Break, Lunch) selectable in the user-state widget; modeled as `IdleCode`. | `packages/contact-center/store/src/store.types.ts` (`IdleCode`, `idleCodes`); `packages/contact-center/user-state/src/helper.ts` | Not "aux code" / "reason code" in identifiers (though it maps to `lastStateAuxCodeId`). | +| agent state | The agent's current presence/availability, held as `currentState` and changed via the user-state widget through the store. | `packages/contact-center/store/src/store.ts` (`currentState`); `packages/contact-center/user-state/src/helper.ts` | Not "status". | +| station login | The agent login flow selecting team and device (dial number / extension / browser); the station-login widget. | `packages/contact-center/station-login/src/station-login/index.tsx` (+ `station-login.types.ts`) | Not "sign-in" in identifiers. | +| buddy agents | Other agents available as consult/transfer targets, loaded via `store.getBuddyAgents()` into the task hook as `BuddyDetails[]`. | `packages/contact-center/task/src/helper.ts` (`loadBuddyAgents`, `buddyAgents`) | Not "peers" / "colleagues". | +| queue | A routing destination for tasks; a transfer/consult target type and a task metadata field. | `packages/contact-center/task/src/task.types.ts` (`QUEUE: 'queue'`); store `currentConsultQueueId`, `allowConsultToQueue` in `store/src/store.ts` | Not "skill group". | +| entry point | A routing entry destination; a transfer/consult target type for tasks. | `packages/contact-center/task/src/task.types.ts` (`ENTRY_POINT: 'entryPoint'`) | One concept; write `entryPoint` in code. | +| wrapup code | A configured post-interaction disposition code applied at task end; modeled as `IWrapupCode`. | `packages/contact-center/store/src/store.ts` (`wrapupCodes: IWrapupCode[]`); surfaced in `CallControlCAD` | Not "disposition" in identifiers. | +| r2wc / Web Component | The `@r2wc/react-to-web-component` wrapper that turns each React widget into a framework-agnostic custom element registered via `customElements.define`. | `packages/contact-center/cc-widgets/src/wc.ts` | "r2wc" is the library; the output is a custom element / Web Component. | +| withMetrics | HOC from `@webex/cc-ui-logging` that wraps a widget export to emit usage/telemetry metrics. | `packages/contact-center/ui-logging/src/withMetrics.tsx` (exported from `ui-logging/src/index.ts`) | Not a generic "logger wrapper". | +| metricsLogger | The telemetry logger utility (and `WidgetMetrics` type) behind `withMetrics`. | `packages/contact-center/ui-logging/src/metricsLogger.ts` | — | +| ErrorBoundary | `react-error-boundary` boundary wrapping every widget export; its `onError` routes to `store.onErrorCallback('WidgetName', error)` with a non-throwing fallback. | pattern in `ai-docs/patterns/react-patterns.md`; realized in `task/src/CallControlCAD/index.tsx` | — | +| `store.cc` | The held SDK Contact Center instance (`observable.ref`); the only path to SDK methods/events (e.g. `store.cc.on(...)`). | `packages/contact-center/store/src/store.ts` (`cc: IContactCenter`) | Never import the SDK directly. | + +## Abbreviations & Acronyms + +| Abbreviation | Expansion | Meaning in this repo | +|---|---|---| +| CC | Contact Center | The Webex Contact Center product family; prefix of the CC widget packages (`@webex/cc-*`). | +| SDK | Software Development Kit | The `@webex/contact-center` SDK, reached only via `store.cc`. | +| r2wc | react-to-web-component | `@r2wc/react-to-web-component`, used in `cc-widgets/src/wc.ts` to emit custom elements. | +| HOC | Higher-Order Component | React wrapping pattern; used for `observer()` and `withMetrics`. | +| MobX | (product name) | The reactive state library backing the store; mutations go through `runInAction`. | +| RTL | React Testing Library | Component/unit test library paired with Jest. | +| PnP | Plug'n'Play | Yarn 4.5.1 module resolution mode used by this monorepo (no `node_modules` by default). | +| CAD | Call-Associated Data | Interaction metadata surfaced by the CallControlCAD widget. | +| E2E | End-to-End | Playwright browser tests under `playwright/`. | +| PII | Personally Identifiable Information | Must never be logged (see `RULES.md` Logging/Security). | +| RONA | Redirect On No Answer | An agent state value referenced in state handling. | +| DN | Dial Number | The agent's telephony dial number (`store.dialNumber`); a station-login device option. | + +## Maintenance +- When a new domain concept is introduced (new entity, event, state), add it here in the same change. +- Cross-reference: module specs → `SPEC_INDEX.md`; SDK surface → `contact-centre-sdk-apis/contact-center.json`. diff --git a/ai-docs/README.md b/ai-docs/README.md index 67739e788..12194f6d4 100644 --- a/ai-docs/README.md +++ b/ai-docs/README.md @@ -174,20 +174,27 @@ Widget (Observer) → Custom Hook (Business Logic) → Component (UI) → Store ## Documentation Structure -**Repository Patterns:** -- `ai-docs/patterns/` - TypeScript, MobX, React, Web Components, Testing patterns -- `ai-docs/templates/` - Task templates (new widget, bug fix, feature enhancement, documentation, Playwright) +This repository follows the SDLC-Templates `component-repo` standard (library version `0.1.0-draft`). +The doc spine is: -**Component Documentation:** -- `packages/*/ai-docs/README.md` - API and usage -- `packages/*/ai-docs/OVERVIEW.md` - Architecture and design -- `packages/*/ai-docs/EXAMPLES.md` - Code examples -- `packages/*/ai-docs/RULES.md` - Component conventions -- `packages/*/ai-docs/diagrams/` - Visual flows +**Standing docs (repo-level, under `ai-docs/`):** +- [`../AGENTS.md`](../AGENTS.md) — agent entry contract (read first) +- [`SPEC_INDEX.md`](./SPEC_INDEX.md) — router: which doc to load per task + module registry +- [`ARCHITECTURE.md`](./ARCHITECTURE.md) — system components, interactions, package map +- [`RULES.md`](./RULES.md), [`GLOSSARY.md`](./GLOSSARY.md), [`SECURITY.md`](./SECURITY.md), [`CONTRACTS.md`](./CONTRACTS.md), [`GETTING_STARTED.md`](./GETTING_STARTED.md), [`REVIEW_CHECKLIST.md`](./REVIEW_CHECKLIST.md), [`SERVICE_STATE.md`](./SERVICE_STATE.md) +- `patterns/`, `rules/`, `adr/` — reference conventions, rules, and decisions +- `templates/` — code-generation task templates (new widget, bug fix, feature, Playwright) + +**Per-module specs (source-local):** +- `packages///ai-docs/-spec.md` — canonical module spec (orientation, requirements, design, flows, tests). See `SPEC_INDEX.md` for the registry. + +**Machine source of truth:** `.sdd/manifest.json` (coverage state per module) and `.sdd/coverage-policy.defaults.yaml`. + +Docs predating this standard are preserved under `_archive/pre-sdlc-migration/`. ## For AI Assistants -See [AGENTS.md](./AGENTS.md) for AI navigation guidance, task-based workflows, and best practices. +See [`../AGENTS.md`](../AGENTS.md) for the agent entry contract, then [`SPEC_INDEX.md`](./SPEC_INDEX.md) for routing. --- diff --git a/ai-docs/REVIEW_CHECKLIST.md b/ai-docs/REVIEW_CHECKLIST.md new file mode 100644 index 000000000..463b1f0da --- /dev/null +++ b/ai-docs/REVIEW_CHECKLIST.md @@ -0,0 +1,41 @@ +# Review-Check Catalog — Webex Contact Center Widgets + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc at Review & Merge. +> Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. + +> Each finding records: severity (Blocking / Important / Medium / Minor), check id, file path, what's wrong, +> why it matters, a concrete fix. Any Blocking finding fails the gate. + +## Core checks (always run) +| # | Check | What it verifies | Severity if it fails | +|---|---|---|---| +| C1 | Spec-currency + WHAT/WHY | Module spec/docs changed in the same change as code (`specCurrency.sameChangeRequired`); the touched module's `ai-docs/` spec and any AI Docs Impact entries are updated; every requirement (incl. ADDED) states WHAT and WHY | Blocking | +| C2 | Contract correctness | Public-surface delta is real and complete — exported components/hooks/store members and r2wc custom-element tag names (e.g. `widget-cc-station-login`) match `CONTRACTS.md`; no undocumented breaking change to an exported API or element tag/attribute | Blocking | +| C3 | Code-vs-spec match | Signatures, data-flow (Widget → hook → component → store → SDK), and architecture claims in the spec match the actual code (file path) | Blocking | +| C4 | Test adequacy | Each acceptance criterion has a Jest/RTL test with a positive AND a negative case; changed-line coverage meets the 80% bar (`coverageBar.changedLines` in `.sdd/coverage-policy.defaults.yaml`) | Important | +| C5 | Error handling + input validation | SDK/event boundaries and untrusted input validated; failure/edge paths handled, not swallowed; widgets wrapped in ErrorBoundary | Important | +| C6 | Security baseline | No hardcoded secrets/credentials; no PII or credentials logged; SDK accessed only via the store (`store.cc.*`), never imported directly in a widget/component (per `SECURITY.md` and `RULES.md`) | Blocking | + +## Coverage-conditional checks (run by the touched module's manifest coverage state) +| # | Check | When it applies | What it verifies | Severity | +|---|---|---|---|---| +| K1 | Regression guard | Modifying a weakly covered module (DRAFT/PARTIAL), or any MODIFIED/REMOVED requirement | A characterization baseline exists; invariants the change claims NOT to alter still hold (positive + negative) | Blocking | +| K2 | Grounding | Weakly covered (DRAFT) module — all modules are DRAFT during migration | Claims cite real code (file path), not memory; uncovered public surfaces flagged `[NEEDS HUMAN INPUT]` | Important | +| K3 | Drift threshold | Any tracked module | Module drift is within its status threshold (DRAFT 25% / PARTIAL 15% / AUTHORITATIVE 5%; see `RULES.md` / `.sdd/coverage-policy.defaults.yaml`) | Important | +| K4 | Coverage-state accuracy | Coverage-state change proposed | The recorded `.sdd/manifest.json` coverage state matches the evidence; promotion/demotion rules honored | Medium | + +## Cross-cutting checks (apply at higher risk / autonomy) +| # | Check | What it verifies | Severity | +|---|---|---|---| +| X1 | Cross-model review | The artifact was validated by a different runtime than the one that generated it (generator ≠ validator) | Blocking when required | +| X2 | Observability | Metrics via `withMetrics` / `metricsLogger` adequate for the change; nothing sensitive (PII/credentials) logged | Medium | +| X3 | Rollout safety | Behavior-changing defaults are safe; rollback path exists; PR targets `next` and follows the FedRAMP-mandated `.github/PULL_REQUEST_TEMPLATE.md` | Important | + +## How the set is selected +1. Always run the 6 core checks. +2. Add the coverage-conditional checks whose "when it applies" matches the touched modules' manifest coverage state. +3. Add the cross-cutting checks when the change is high-risk or runs at higher autonomy. + +## Output +- A compliance matrix + severity-sorted findings + a verdict (Pass / Pass-with-warnings / Blocked). + Draft only; a human posts. diff --git a/ai-docs/RULES.md b/ai-docs/RULES.md index eccaadba0..666f3310e 100644 --- a/ai-docs/RULES.md +++ b/ai-docs/RULES.md @@ -1,51 +1,92 @@ -# Repository Rules & Design Patterns - -## Architectural principles - -- Clear layering: Widgets → UI components (`cc-components`) → Store (MobX) → SDK/backends (where applicable). - -## Naming & structure - -- Mirror code structure in docs under `ai-docs/`. -- Keep widget folders self-contained with `agent.md`, `architecture.md`, `README.md`. - -## Components & APIs - -- Strongly typed props and public surfaces (`index.ts` per package/widget). -- Co-locate types with components (e.g., `*.types.ts`). - -## Error handling - -- Surface user-friendly errors in UI; avoid swallowing exceptions. - -## Accessibility & i18n - -- Ensure keyboard and screen-reader support in components. - -## Styling & theming - -- Keep component styles modular (`.scss` or `.css` in component folders). - -## Performance budgets - -- Prefer memoization for derived values; avoid unnecessary re-renders; batch updates with MobX where needed. +# Rules — webex-widgets + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry, carries the critical rules) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc; per-language detail in `patterns/`. +> Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. + +> These rules are checkable. Every MUST rule records its source requirement/risk, verification path, +> severity, and owner. Name the tool where one enforces a rule; say "review only" plus why otherwise. + +## Coverage Map (which docs/specs to trust) + +Coverage state is mirrored from `.sdd/manifest.json`. Every module is currently `DRAFT`: specs were generated fresh during the SDLC migration (2026-06-29) and have not been validated. **Cross-check code before relying on any spec claim** (drift tolerance ≤ 25%, see below). + +| Module | Manifest coverage state | What it means here | +|---|---|---| +| `store` (`@webex/cc-store`, `packages/contact-center/store`) | DRAFT | Tier-1, sole SDK access point (`store.ts` `getInstance`). Spec `store/ai-docs/store-spec.md` is unvalidated — verify observables/SDK proxying against `store/src/store.ts` and `store/src/storeEventsWrapper.ts`. | +| `cc-components` (`@webex/cc-components`, `packages/contact-center/cc-components`) | DRAFT | Tier-1 presentational primitives. Verify prop contracts against `cc-components/src/` before trusting the spec. | +| `cc-widgets` (`@webex/cc-widgets`, `packages/contact-center/cc-widgets`) | DRAFT | Tier-1 r2wc aggregator. The custom-element registry lives in `cc-widgets/src/wc.ts` — cross-check element names/attrs there. | +| `station-login` (`@webex/cc-station-login`, `packages/contact-center/station-login`) | DRAFT | Tier-1 widget. Verify against `station-login/src/`. | +| `user-state` (`@webex/cc-user-state`, `packages/contact-center/user-state`) | DRAFT | Tier-1 widget. Verify state/idle-code logic against `user-state/src/helper.ts`. | +| `task` (`@webex/cc-task`, `packages/contact-center/task`) | DRAFT | Tier-1 bundle of sub-widgets CallControl, CallControlCAD, IncomingTask, OutdialCall, TaskList. Verify per-widget behavior against `task/src/{Widget}/index.tsx` and `task/src/helper.ts`. | +| `ui-logging` (`@webex/cc-ui-logging`, `packages/contact-center/ui-logging`) | DRAFT | Tier-2 telemetry. `withMetrics` HOC + `metricsLogger` in `ui-logging/src/`. | +| `test-fixtures` (`@webex/test-fixtures`, `packages/contact-center/test-fixtures`) | DRAFT | Tier-2 shared mocks/helpers in `test-fixtures/src/`. | +| `meetings-widgets` (`@webex/widgets`, `packages/@webex/widgets`) | DRAFT | Tier-2 legacy meetings family, separate from the CC widget family. Out of scope for CC rules below unless explicitly named. | + +## Autonomy & Ask-First +- **May proceed:** read-only research; bug fixes and feature work scoped to a single CC widget package that follow the established Widget → Hook → Component → Store flow; adding unit/E2E tests; doc edits under `ai-docs/`; copy/string and styling tweaks. +- **Ask first / plan + confirm:** changes to the dependency direction or to `cc-widgets/src/wc.ts` custom-element names/attributes (a public contract); changes to the store's observable shape or SDK access surface in `store/src/`; new third-party dependencies; touching the `@webex/widgets` legacy meetings family. +- **Never without explicit human approval:** `git push`, opening/merging PRs, or any deploy. PRs target the `next` branch and are draft by default. + +## Naming +Grounded in `patterns/typescript-patterns.md` and the real code: +- Interfaces are prefixed with `I` and PascalCase: `IUserState`, `IStationLoginProps`, `IContactCenter` (`store/src/store.types.ts`, `task/src/task.types.ts`). Never an un-prefixed `UserState` interface. +- Components are PascalCase in `.tsx` files: `UserState.tsx`, `CallControl/index.tsx`. Hooks are camelCase with a `use` prefix in `.ts` files: `useUserState`, `useCallControl` (`*/src/helper.ts`). +- Types are co-located in `{name}.types.ts` (e.g. `user-state/src/user-state.types.ts`); derive subsets with `Pick`/`Partial` rather than re-declaring (e.g. `IUserStateProps = Pick`). +- Event/state names are enums in SCREAMING_SNAKE_CASE values, e.g. `CC_EVENTS.AGENT_STATE_CHANGED`, `TASK_EVENTS.TASK_INCOMING`. Constants are SCREAMING_SNAKE_CASE. + +## Logging +- Use the `ui-logging` helpers — `withMetrics` HOC and `metricsLogger` (`ui-logging/src/index.ts`, `ui-logging/src/metricsLogger.ts`) — and the store `logger` passed into hooks. Calls carry a structured context object `{module, method}` (see `task/src/helper.ts` `loadBuddyAgents`: `logger.info('Loaded N buddy agents', {module: 'helper.ts', method: 'loadBuddyAgents'})`). +- **NEVER log PII or credentials** (agent identifiers in sensitive contexts, dial numbers, tokens, session secrets). Severity: high. Verification: review + grep for new `console.*`/`logger.*` calls in a diff. See Security below. +- Prefer the injected `logger` over bare `console.*`; `console.error` is tolerated only inside hook catch blocks where no logger is available. + +## Error Handling +- Every widget MUST be wrapped in an `ErrorBoundary` (`react-error-boundary`) at its exported boundary, with `onError` routing to `store.onErrorCallback?.('WidgetName', error)` and a non-throwing fallback (`patterns/react-patterns.md`; pattern realized across `*/src/{Widget}/index.tsx`). Severity: high. Verification: review only. +- SDK/async calls in hooks (`helper.ts`) MUST be wrapped in `try/catch`; on failure, log via the injected `logger` and invoke the optional `onError`-style callback rather than letting the rejection escape. Never swallow an error silently. +- Surface user-friendly errors in the presentational component (loading/error/empty states); never leak raw SDK errors or stack traces to the UI. + +## Imports / Dependencies +**Dependency flow is one direction only** (`.sdd/manifest.json`; enforced by review, source: legacy task-router rule "Circular Dependency Prevention"): +``` +cc-widgets → widget packages (station-login, user-state, task) → cc-components → store → @webex/contact-center SDK +``` +- A widget package MUST NOT import from `@webex/cc-widgets`. `cc-components` MUST NOT import from any widget package. No package imports upstream. Severity: high — if a circular import is detected, STOP and refactor. +- Access the SDK ONLY through the store: `store.cc.methodName()` / `store.getBuddyAgents()` (`task/src/helper.ts:519`). NEVER `import ... from '@webex/contact-center'` in widget or component code. Severity: high. +- Import the store as the singleton default export: `import store from '@webex/cc-store'`. Never `new Store()`; the instance comes from `Store.getInstance()` (`store/src/store.ts:64`). +- New third-party dependencies require maintainer approval (ask-first). + +## Testing +Grounded in `patterns/testing-patterns.md`: +- Unit/component tests use Jest + React Testing Library and live in each package's `tests/` folder; E2E tests use Playwright under `playwright/` at repo root. +- Each behavior gets both a positive test and the relevant negative/guard test (e.g. error path fires `onError`, callback NOT called on failure). Test behavior via `data-testid`, not CSS selectors or implementation details. +- Mock the store with `@webex/test-fixtures` (`test-fixtures/src/`); never hit the real SDK. +- Write a failing test first (TDD), then implement. Update the spec/docs in the same change (see Spec-Currency below). +- Changed-line coverage bar: **≥ 80%** (`.sdd/coverage-policy.defaults.yaml` `coverageBar.changedLines`). +- Run tests with `yarn workspace @webex/{pkg} test:unit` (single package) or `yarn test:cc-widgets` (all CC); styles via `yarn test:styles`; E2E via `yarn test:e2e`. **Never** `npx jest` directly. ## Security - -- Do not log PII or credentials. Sanitize user-provided inputs. - -## Observability - -- Use `ui-logging` helpers (`withMetrics`, `metricsLogger`) for metrics and logs. - -## Testing standards - -- Unit/component tests per package under `tests/`. -- E2E tests in `playwright/` with suites and helpers. - -## Review & contribution - -- Follow patterns in `ai-docs/patterns/*.md`. -- Keep docs in sync when APIs change. - - +- No PII or credentials in logs (see Logging) — agent dial numbers, tokens, session/auth material must never reach `logger`/`console` or telemetry payloads. +- Reach the SDK only through the store; never import the SDK directly (prevents bypassing the store's auth/state boundary). +- No hardcoded secrets/tokens/keys anywhere (see Secrets Policy). Sanitize user-provided input rendered in the UI. +- This repo owns NO persistent datastore — all domain data comes from the SDK at runtime, so there is no at-rest data-handling surface here (N/A by construction). A dedicated `SECURITY.md` is not yet present; these rules are the current security posture. + +## Spec-Currency & Drift Thresholds +- Update the spec/docs in the SAME change as the code (spec-currency: `.sdd/coverage-policy.defaults.yaml` `specCurrency.sameChangeRequired: true`). +- Drift thresholds mirror `.sdd/coverage-policy.defaults.yaml` (drift = share of spec claims no longer matching code): + - AUTHORITATIVE ≤ 5% + - PARTIAL ≤ 15% + - DRAFT ≤ 25% (current state for ALL modules — cross-check code before relying on a claim) + - NONE — no spec to drift from + +## Secrets Policy +- No hardcoded secrets/tokens/keys/connection strings — ever. The widgets receive auth context from the host application via the SDK/store at runtime; nothing is sourced from a committed file. Never log secrets, never commit them. + +## Concurrency & Async +The repo is reactive (MobX) and event-driven (SDK events), so these apply: +- All store mutations MUST go through `runInAction(() => { ... })` — never mutate observable state directly (`patterns/mobx-patterns.md`; realized in `store/src/storeEventsWrapper.ts`). Severity: high. Verification: review only. +- Widgets that read store state MUST be wrapped in `observer()` from `mobx-react-lite` so re-renders track observable reads. Severity: high. +- SDK event subscriptions registered in `useEffect` MUST be torn down in the cleanup return (`cc.on(...)` paired with `cc.off(...)`) to avoid duplicate handlers/leaks (`patterns/react-patterns.md`). +- `cc` is held as `observable.ref` (`store/src/store.ts`) — replace the reference, don't deep-mutate the SDK instance. + +## Maintenance +- Add a rule when a review correction recurs; remove it when a lint rule starts enforcing it. +- Cross-reference: patterns → `patterns/` (`typescript-patterns.md`, `react-patterns.md`, `mobx-patterns.md`, `testing-patterns.md`); module specs → `SPEC_INDEX.md`. diff --git a/ai-docs/SECURITY.md b/ai-docs/SECURITY.md new file mode 100644 index 000000000..cdc935f64 --- /dev/null +++ b/ai-docs/SECURITY.md @@ -0,0 +1,48 @@ +# Security Baseline — webex-widgets (Contact Center) + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc; per-feature security design lives in each feature's design doc. +> Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. + +> Read before changing anything that touches input, identity, data, or external calls. Don't weaken a +> documented control without an explicit, approved decision (record it as an ADR). + +This repo is a client-side React/Web-Component widget library. It hosts no network service: there is no HTTP server, no endpoints, no sessions or cookies issued by this code. The authenticated Webex session and its tokens are owned by the host application and the `@webex/contact-center` SDK; widgets reach the SDK only through the store (`store.cc.*`). + +## Trust Boundaries +| Boundary | Untrusted side | Trusted side | What is enforced at the crossing | +|---|---|---|---| +| Widget initialization | Host application (passes `webexConfig` + `access_token`, or a pre-initialized `webex` object) | Store | `Store.init()` accepts `InitParams` and hands credentials straight to `Webex.init()`; widgets never parse or store the raw token (`packages/contact-center/store/src/store.ts:132-151`) | +| Widget props / events | Host application (event callbacks, JSON props passed to custom elements) | Widget React tree | r2wc coerces declared prop types (`function`, `json`, `boolean`, `string`); only declared props are wired (`packages/contact-center/cc-widgets/src/wc.ts:8-78`) | +| SDK data into UI | `@webex/contact-center` SDK (task/agent/profile payloads via events) | Store + widgets | All SDK access funnels through the store wrapper; widgets consume MobX observables, never the SDK directly (`packages/contact-center/store/src/storeEventsWrapper.ts`) | +| Browser DOM rendering | SDK-supplied strings (names, phone numbers, transcript text) | React DOM | React escapes interpolated text by default; no `dangerouslySetInnerHTML` in widget render paths | + +## Authentication & Authorization Model +- **Authentication:** Owned by the host app and the Webex SDK, not this repo. The host supplies either a live `webex` instance or `{webexConfig, access_token}`; the store passes the token to `Webex.init({credentials: {access_token}})` and otherwise treats identity as opaque (`packages/contact-center/store/src/store.ts:144-151`). Token retrieval for downstream SDK features delegates to the SDK: `getAccessToken()` calls `webex.credentials.getUserToken()` (`packages/contact-center/store/src/storeEventsWrapper.ts:988-998`). +- **Authorization:** Owned by the SDK / back end. The store surfaces the agent's capabilities as read-only feature flags derived from the SDK-provided `Profile` (`packages/contact-center/store/src/util.ts:3-36`); widgets use these only to show/hide UI. There is no access-decision logic enforced in this repo. +- **Default posture:** Widgets are inert until the host completes `Store.init()`; with no valid host-supplied session the SDK never initializes (`Webex.init` rejects after a 6s timeout — `packages/contact-center/store/src/store.ts:140-142`), so no agent data flows. + +## Secret & Credential Handling +- Secrets source: None stored in this repo. The `access_token` is provided at runtime by the host application; no vault, env-baked secret, or connection string exists in source. A grep of `packages/contact-center/*/src` finds token references only in the store init/retrieval paths above and as a transient in-memory `jwtToken` in Digital Channels (fetched via `getAccessToken`, held in component state, never persisted — `packages/contact-center/cc-digital-channels/src/helper.ts:76-83`). +- Injection: Passed in by the host at `Store.init()` and forwarded directly to the SDK; the access token is never copied into the MobX store, logs, or persistent storage. +- Rotation: Owned by the host/SDK (`webex.credentials.getUserToken()` is re-queried on demand); this repo does not cache tokens, so it has no rotation responsibility. +- **Hard rule:** never commit secrets, tokens, keys, or connection strings; never log them. + +## Data Classification & Handling +| Data class | Examples | Storage rule | Logging rule | In transit | +|---|---|---|---|---| +| Auth credential | `access_token` (host-supplied), SDK user token | Never persisted; passed to SDK only, held transiently in memory | Never log — confirmed: only error/status strings are logged in token paths, never the token value (`packages/contact-center/store/src/storeEventsWrapper.ts:994-998`) | Carried by the SDK over its own HTTPS transport; not handled by this repo | +| Agent/customer PII | Caller name, phone number (DNIS/ANI), task/interaction data, transcript text, address-book entries | Held in memory as MobX observables only; no datastore in this repo | Never log raw PII; widget log lines carry `{module, method}` context, not payloads | SDK-owned HTTPS | +| Telemetry props | Widget metrics props passed to `metricsLogger` | Not persisted by this repo | **Risk: props are NOT sanitized today** — `metricsLogger` documents this explicitly (`packages/contact-center/ui-logging/src/metricsLogger.ts:73-76`). Do not pass PII-bearing objects as metrics props; see Known Sensitive Areas | Telemetry sink owned by host/SDK | + +## Input Validation & Output Encoding Posture +- Untrusted input enters only via host-supplied custom-element props/events (type-coerced by r2wc — `packages/contact-center/cc-widgets/src/wc.ts`) and via SDK event payloads (typed through `store.types`). Rendered output goes through React, which escapes interpolated text by sink; widget render paths use no `dangerouslySetInnerHTML`. There are no SQL/shell/query sinks in this client library, so parameterization is N/A. + +## Known Sensitive Areas & Accepted Risks +| Area | Risk | Mitigation / why accepted | Owner | +|---|---|---|---| +| `ui-logging` metrics props | Widget props are logged without sanitization (`metricsLogger.ts:73-76`) | Callers must not pass PII-bearing objects as metrics props; sanitization is a documented future enhancement | cc-ui-logging maintainers | +| `getAccessToken()` SDK gap | `webex.credentials.getUserToken()` is `@ts-expect-error`-typed (SDK API not yet typed) (`storeEventsWrapper.ts:990-992`) | Token value is returned to the caller and never logged; failures log only an error message | cc-store maintainers | + +## Reporting & Review +- Security-relevant changes (anything touching the store init/credential path, the `@webex/contact-center` SDK boundary, logging, or the public export/custom-element surface) require review by the package CODEOWNERS on the `next`-targeted PR, following `.github/PULL_REQUEST_TEMPLATE.md` (FedRAMP/GAI sections). Suspected vulnerabilities: report through the Webex internal security channel, not a public issue. +- Cross-reference: stable public surface → [`CONTRACTS.md`](CONTRACTS.md); current dependencies → [`SERVICE_STATE.md`](SERVICE_STATE.md). diff --git a/ai-docs/SERVICE_STATE.md b/ai-docs/SERVICE_STATE.md new file mode 100644 index 000000000..59da4a2fe --- /dev/null +++ b/ai-docs/SERVICE_STATE.md @@ -0,0 +1,48 @@ +# Service State (living) — webex-widgets (Contact Center) + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Read this FIRST before adding a surface; stable contracts in [`CONTRACTS.md`](CONTRACTS.md). +> Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. + +> Source of truth for "does X already exist?" Keep current in the same change that adds/removes a surface. + +This is a client-side widget library, not a network service. It exposes no HTTP endpoints, publishes/consumes no broker events, and owns no datastore — so the Current Endpoints, Current Events, Data Stores, and Rate Limits sections are dropped (no applicable content). All domain data is obtained at runtime from the `@webex/contact-center` SDK via the store. The as-built public surface (exports + custom elements) lives in [`CONTRACTS.md`](CONTRACTS.md). + +## External Dependencies +| Dependency | Used for | Timeout / retry | Circuit breaker / fallback | +|---|---|---|---| +| `@webex/contact-center` SDK | Sole source of CC domain data and actions: agent login/state, tasks/call control, profile + feature flags, transcripts, access token (`webex.credentials.getUserToken()`). Accessed only through the store (`store.cc.*`, `packages/contact-center/store/src/storeEventsWrapper.ts`). | This repo owns no retry policy; the SDK owns its own timeouts/retries. The one timeout this repo holds is the 6s SDK-init guard in `Store.init()` (`packages/contact-center/store/src/store.ts:140-142`). | No breaker. On init failure `Store.init()` rejects; widgets remain inert and surface error UI. Token failures log an error and return without crashing (`packages/contact-center/store/src/storeEventsWrapper.ts:994-998`). | +| `react` / `react-dom` 18, `mobx` / `mobx-react-lite`, `@r2wc/react-to-web-component` | Runtime peers: component rendering, store reactivity, custom-element wrapping (`packages/contact-center/cc-widgets/src/wc.ts`). | N/A (in-process libraries) | N/A | + +## Feature Flags (current) +Feature flags are not owned or defaulted by this repo — they are read from the SDK-provided agent `Profile` and surfaced read-only via `getFeatureFlags()` to drive UI visibility (`packages/contact-center/store/src/util.ts:3-36`). Defaults and ownership live with the back end / SDK profile; "Current default" below is therefore **SDK-provided (per-agent)**. + +| Flag | Gates | Current default | Owner | Safe to remove when | +|---|---|---|---|---| +| `isOutboundEnabledForTenant` | Outdial UI at tenant level | SDK-provided (per-agent) | Webex CC back end | SDK stops emitting it on `Profile` | +| `isOutboundEnabledForAgent` | Outdial UI at agent level | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isAdhocDialingEnabled` | Ad-hoc dial entry in OutdialCall | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isCampaignManagementEnabled` | Campaign management UI | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isEndCallEnabled` | End-call action in CallControl | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isEndConsultEnabled` | End-consult action | SDK-provided | Webex CC back end | SDK stops emitting it | +| `agentPersonalStatsEnabled` | Agent personal stats UI | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isCallMonitoringEnabled` | Call monitoring controls | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isMidCallMonitoringEnabled` | Mid-call monitoring controls | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isBargeInEnabled` | Barge-in control | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isManagedTeamsEnabled` | Managed-teams selection | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isManagedQueuesEnabled` | Managed-queues selection | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isSendMessageEnabled` | Send-message action | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isAgentStateChangeEnabled` | Agent state change UI | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isSignOutAgentsEnabled` | Sign-out-agents action | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isTimeoutDesktopInactivityEnabled` | Desktop inactivity timeout behavior | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isAnalyzerEnabled` | Analyzer-backed features | SDK-provided | Webex CC back end | SDK stops emitting it | +| `webRtcEnabled` | WebRTC (browser) device option | SDK-provided | Webex CC back end | SDK stops emitting it | +| `isRecordingManagementEnabled` | Recording toggle in CallControl | SDK-provided | Webex CC back end | SDK stops emitting it | +| `allowConsultToQueue` | Consult-to-queue option | SDK-provided | Webex CC back end | SDK stops emitting it | + +## Compliance / Certifications +- FedRAMP: PR template (`.github/PULL_REQUEST_TEMPLATE.md`) compliance is mandatory and must not be regressed (COMPLETES, Change Type, test scenarios, GAI Policy, Checklist sections). +- PII: agent/customer PII (names, phone numbers, task/transcript data) passes through widgets at runtime and must never be logged or persisted — see [`SECURITY.md`](SECURITY.md). Metrics props are not yet sanitized (`packages/contact-center/ui-logging/src/metricsLogger.ts:73-76`); do not pass PII-bearing objects to `metricsLogger`. + +## Maintenance +- Update the relevant row in the same change that adds/changes/removes a surface, dependency, limit, or flag (e.g. add a flag here when `getFeatureFlags()` in `util.ts` gains a key). +- Cross-reference: stable contracts → [`CONTRACTS.md`](CONTRACTS.md); security posture → [`SECURITY.md`](SECURITY.md). diff --git a/ai-docs/SPEC_INDEX.md b/ai-docs/SPEC_INDEX.md new file mode 100644 index 000000000..2a926540b --- /dev/null +++ b/ai-docs/SPEC_INDEX.md @@ -0,0 +1,69 @@ +# Spec Index — webex-widgets (Contact Center) + +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry). This file is the router (`ai-docs/SPEC_INDEX.md`); system overview in [`ARCHITECTURE.md`](ARCHITECTURE.md). Load `AGENTS.md` + this file first; pull every other doc on demand. +> Context-efficiency: link to canonical docs — don't duplicate them; route to the minimum needed per task. + +> AI agent entry point after `AGENTS.md`. Load this once at session start; pull other docs on demand. +> **Source of truth:** `.sdd/manifest.json` (this file mirrors it for humans). + +## Module Registry +| Module | Responsibility | Manifest coverage state | Start here | +|---|---|---|---| +| `store/` | MobX singleton; global CC state; proxies SDK events; sole SDK access point | DRAFT | `packages/contact-center/store/ai-docs/store-spec.md` | +| `cc-components/` | Shared presentational React UI primitives | DRAFT | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` | +| `cc-widgets/` | r2wc Web Component wrappers (aggregator) | DRAFT | `packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md` | +| `station-login/` | Agent login: team + device selection | DRAFT | `packages/contact-center/station-login/ai-docs/station-login-spec.md` | +| `user-state/` | Agent state: state, idle codes, timer | DRAFT | `packages/contact-center/user-state/ai-docs/user-state-spec.md` | +| `task/` | Task widget bundle: CallControl, IncomingTask, OutdialCall, TaskList, CallControlCAD | DRAFT | `packages/contact-center/task/ai-docs/task-spec.md` | +| `ui-logging/` | Metrics/telemetry: `withMetrics`, `metricsLogger` | DRAFT | `packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md` | +| `test-fixtures/` | Shared test mocks/helpers | DRAFT | `packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md` | +| `@webex/widgets/` | Legacy meetings widgets (separate family) | DRAFT | `packages/@webex/widgets/ai-docs/widgets-spec.md` | + +## Task Routing +| If the task is… | Load | +|---|---| +| Understanding the system | `ARCHITECTURE.md` | +| Working in a module | that module's `-spec.md` (see registry); load only the relevant section | +| Changing the store / SDK access | `store-spec.md` + `ARCHITECTURE.md` Component Interaction; check `contact-centre-sdk-apis/contact-center.json` | +| Adding/changing a public surface (export, custom element, event) | `CONTRACTS.md` first, then the owning module spec | +| A UI/component change | `cc-components-spec.md` + `patterns/react-patterns.md` | +| A new widget | new-widget templates (`ai-docs/templates/new-widget/`); create the module spec as part of the change | +| A bug fix | the touched module spec + `patterns/` + `RULES.md`; bug-fix template `ai-docs/templates/existing-widget/bug-fix.md` | +| Playwright E2E work | `playwright/` suites + `ai-docs/templates/playwright/` | +| Naming a concept | `GLOSSARY.md` | +| Anything touching input/identity/logging | `SECURITY.md` | +| Review & merge | `REVIEW_CHECKLIST.md` | + +## Intake Routing +``` +What kind of change? +├─ New feature / widget -> create/update an intake record under .generated/sdd/features//run-records/ +├─ Bug / defect -> create/update an intake record; load the touched module spec +├─ New module / component -> create the module spec (module-spec template) + register in .sdd/manifest.json +└─ Doc/spec backfill only -> no feature intake record; reconcile the target spec, regenerate, run conformance +``` +The intake record confirms scope/modules **against the code** and sets the change class +(routine / security / contract / perf-critical / ui) that gates conditional spec sections. + +## Incident History +| INC id | Date | Module | One-line | Link | +|---|---|---|---|---| +| _none recorded_ | — | — | Populate from the tracker as RCAs are written | — | + +## Spec Registry +| Doc | Location | Purpose | +|---|---|---| +| Agent entry | `AGENTS.md` (root) | First file read: commands, critical rules, boundaries, routing | +| Architecture | `ai-docs/ARCHITECTURE.md` | System components, interactions, monorepo package map | +| Patterns | `ai-docs/patterns/` | Repo conventions (TypeScript, React, MobX, testing), correct vs incorrect | +| Rules | `ai-docs/RULES.md` + `ai-docs/rules/` | Enforceable do/don't beyond AGENTS critical rules | +| Glossary | `ai-docs/GLOSSARY.md` | Ubiquitous language: term → definition → code location | +| Security | `ai-docs/SECURITY.md` | Trust boundaries, secret/token handling, data classification | +| Contracts | `ai-docs/CONTRACTS.md` | Root index of public surfaces (exports, custom elements, events) | +| Service state | `ai-docs/SERVICE_STATE.md` | Living as-built registry — read first to avoid duplicating a surface | +| Getting started | `ai-docs/GETTING_STARTED.md` | Clone/build/test loop, workspace layout | +| Decision records | `ai-docs/adr/` | Standing ADRs — why the architecture is the way it is | +| Review catalog | `ai-docs/REVIEW_CHECKLIST.md` | 6-core + 4-coverage + 3-cross-cutting review checks | +| SDK reference | `contact-centre-sdk-apis/contact-center.json` | TypeDoc of `@webex/contact-center` — verify every SDK call | + +_No `DATA_MODEL.md`: this repo owns no persistent datastore (all domain data comes from the SDK at runtime)._ diff --git a/ai-docs/_archive/pre-sdlc-migration/README.md b/ai-docs/_archive/pre-sdlc-migration/README.md new file mode 100644 index 000000000..8ca3a9c51 --- /dev/null +++ b/ai-docs/_archive/pre-sdlc-migration/README.md @@ -0,0 +1,20 @@ +# Pre-SDLC-Migration Archive + +Snapshot of the repository's AI documentation as it existed **before** migration to the +SDLC-Templates `component-repo` standard (`0.1.0-draft`). + +These files are kept for provenance and content recovery only. They are **not** maintained and +**not** part of the live doc set. Do not route agents here. + +## What was here + +| Original file | Superseded by | +|---|---| +| `root/AGENTS.md` (task-router orchestrator) | root `AGENTS.md` (agent-entry contract) + `ai-docs/SPEC_INDEX.md` (router) | +| `packages/**/ai-docs/AGENTS.md` + `ARCHITECTURE.md` (per-package pairs) | `/ai-docs/-spec.md` (canonical module spec) | + +The task-router workflow content (task types A–F, SDK consultation, pre-step gates) was folded into +the new root `AGENTS.md`, `ai-docs/SPEC_INDEX.md` (Task Routing / Intake Routing), and +`ai-docs/RULES.md`. + +Migration date: 2026-06-29. diff --git a/packages/@webex/widgets/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/@webex/widgets/ai-docs/AGENTS.md similarity index 100% rename from packages/@webex/widgets/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/@webex/widgets/ai-docs/AGENTS.md diff --git a/packages/@webex/widgets/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/@webex/widgets/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/@webex/widgets/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/@webex/widgets/ai-docs/ARCHITECTURE.md diff --git a/packages/contact-center/cc-components/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/AGENTS.md similarity index 100% rename from packages/contact-center/cc-components/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/AGENTS.md diff --git a/packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md diff --git a/packages/contact-center/cc-widgets/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-widgets/ai-docs/AGENTS.md similarity index 100% rename from packages/contact-center/cc-widgets/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-widgets/ai-docs/AGENTS.md diff --git a/packages/contact-center/cc-widgets/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-widgets/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/cc-widgets/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-widgets/ai-docs/ARCHITECTURE.md diff --git a/packages/contact-center/station-login/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/AGENTS.md similarity index 100% rename from packages/contact-center/station-login/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/AGENTS.md diff --git a/packages/contact-center/station-login/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/station-login/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/ARCHITECTURE.md diff --git a/packages/contact-center/store/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/AGENTS.md similarity index 100% rename from packages/contact-center/store/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/AGENTS.md diff --git a/packages/contact-center/store/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/store/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/ARCHITECTURE.md diff --git a/packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md diff --git a/packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/CallControl/ARCHITECTURE.md diff --git a/packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md diff --git a/packages/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/IncomingTask/ARCHITECTURE.md diff --git a/packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md diff --git a/packages/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/OutdialCall/ARCHITECTURE.md diff --git a/packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md diff --git a/packages/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/task/ai-docs/widgets/TaskList/ARCHITECTURE.md diff --git a/packages/contact-center/test-fixtures/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/test-fixtures/ai-docs/AGENTS.md similarity index 100% rename from packages/contact-center/test-fixtures/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/test-fixtures/ai-docs/AGENTS.md diff --git a/packages/contact-center/test-fixtures/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/test-fixtures/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/test-fixtures/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/test-fixtures/ai-docs/ARCHITECTURE.md diff --git a/packages/contact-center/ui-logging/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/ui-logging/ai-docs/AGENTS.md similarity index 100% rename from packages/contact-center/ui-logging/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/ui-logging/ai-docs/AGENTS.md diff --git a/packages/contact-center/ui-logging/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/ui-logging/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/ui-logging/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/ui-logging/ai-docs/ARCHITECTURE.md diff --git a/packages/contact-center/user-state/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/user-state/ai-docs/AGENTS.md similarity index 100% rename from packages/contact-center/user-state/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/user-state/ai-docs/AGENTS.md diff --git a/packages/contact-center/user-state/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/contact-center/user-state/ai-docs/ARCHITECTURE.md similarity index 100% rename from packages/contact-center/user-state/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/contact-center/user-state/ai-docs/ARCHITECTURE.md diff --git a/playwright/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/playwright/ai-docs/AGENTS.md similarity index 100% rename from playwright/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/playwright/ai-docs/AGENTS.md diff --git a/playwright/ai-docs/ARCHITECTURE.md b/ai-docs/_archive/pre-sdlc-migration/packages/playwright/ai-docs/ARCHITECTURE.md similarity index 100% rename from playwright/ai-docs/ARCHITECTURE.md rename to ai-docs/_archive/pre-sdlc-migration/packages/playwright/ai-docs/ARCHITECTURE.md diff --git a/widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/packages/widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md similarity index 100% rename from widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md rename to ai-docs/_archive/pre-sdlc-migration/packages/widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md diff --git a/ai-docs/_archive/pre-sdlc-migration/root/AGENTS.md b/ai-docs/_archive/pre-sdlc-migration/root/AGENTS.md new file mode 100644 index 000000000..193649a3c --- /dev/null +++ b/ai-docs/_archive/pre-sdlc-migration/root/AGENTS.md @@ -0,0 +1,621 @@ +# Contact Center Widgets - AI Agent Guide + +## Purpose + +This is the main orchestrator for AI assistants working on this repository. It routes you to the correct templates and documentation based on the developer's task. + +**For every developer request:** (1) Identify task type (A–F below). (2) If the work is in an existing package, widget, or test framework scope, load that scope's ai-docs (see [Package and widget ai-docs reference](#package-and-widget-ai-docs-reference)) and follow its AGENTS.md. (3) Open the template for that type and complete its mandatory pre-steps (see [Mandatory pre-steps by task type](#mandatory-pre-steps-by-task-type)). (4) Then follow the rest of this guide and the template. + +--- + +## Quick Start + +**When developer provides a task, follow this workflow:** + +1. **Understand the task** - Identify what type of work is needed +2. **Break down large or multi-part tasks** - If the prompt mixes multiple tasks (for example, "create new widget" **and** "fix a bug" or "add a feature"), or the task is very large, split it into smaller, clearly scoped subtasks and handle them one by one +3. **Route to appropriate template** - Use modular templates for guidance +4. **Load package/widget ai-docs when working in that scope** - If fixing, generating, or enhancing code in an existing package or widget, read that scope's [ai-docs/AGENTS.md and ARCHITECTURE.md](#package-and-widget-ai-docs-reference) and follow them (see CRITICAL RULES rule 3). +5. **Complete that template's mandatory pre-step section** - See [Mandatory pre-steps by task type](#mandatory-pre-steps-by-task-type) in CRITICAL RULES. Do not generate code until pre-steps are done or the developer explicitly waives them. +6. **Generate/fix code** - Follow established patterns +7. **Update documentation** - Keep ai-docs in sync with code changes +8. **Ask for review** - Confirm completion with developer + +--- + +## Step 1: Identify Task Type + +**Ask developer:** "What do you need help with?" + +If the developer's message contains multiple distinct task types (for example, "create new widget", "fix a bug", and "add a feature" in one prompt), treat each as a separate internal task. Clarify priorities or ordering with the developer when needed, and then execute the subtasks sequentially rather than trying to complete everything at once. + +### Task Types + +**A. Create New Widget** +- Developer wants to build a completely new widget from scratch +- **Route to:** [templates/new-widget/00-master.md](./ai-docs/templates/new-widget/00-master.md) +- **Follow:** All 7 modules (pre-questions → validation) +- **⚠️ MANDATORY FIRST STEP:** Collect design input (see below) + +**B. Fix Bug in Existing Widget** +- Developer reports a bug or issue in existing code +- **Route to:** [templates/existing-widget/bug-fix.md](./ai-docs/templates/existing-widget/bug-fix.md) +- **Follow:** Bug fix workflow with root cause analysis + +**C. Add Feature to Existing Widget** +- Developer wants to enhance existing widget with new functionality +- **Route to:** [templates/existing-widget/feature-enhancement.md](./ai-docs/templates/existing-widget/feature-enhancement.md) +- **Follow:** Feature addition workflow with backward compatibility + +**D. Generate/Update Documentation Only** +- Developer needs documentation for existing code +- **Route to:** [templates/documentation/create-agent-md.md](./ai-docs/templates/documentation/create-agent-md.md) and [templates/documentation/create-architecture-md.md](./ai-docs/templates/documentation/create-architecture-md.md) +- **Follow:** Documentation templates (reusable for all packages) + +**E. Understanding Architecture** +- Developer needs to understand how something works +- **Read:** That scope's `ai-docs/AGENTS.md` (usage) and `ai-docs/ARCHITECTURE.md` (technical details); use [Package and widget ai-docs reference](#package-and-widget-ai-docs-reference) to find the path. +- **Available for:** station-login, user-state, store, cc-components, cc-widgets, ui-logging, test-fixtures, playwright; for task package use per-widget ai-docs (CallControl, IncomingTask, OutdialCall, TaskList). + +**F. Playwright E2E Test Work** +- Developer wants to add/update/stabilize Playwright tests, suites, sets, or test framework docs +- **Route to:** [templates/playwright/00-master.md](./ai-docs/templates/playwright/00-master.md) +- **Follow:** Playwright template workflow (pre-questions → implementation → validation) +- **⚠️ MANDATORY FIRST STEP:** Complete pre-questions in [templates/playwright/01-pre-questions.md](./ai-docs/templates/playwright/01-pre-questions.md) + +--- + +## ⚠️ CRITICAL RULES - Always Follow + +### 1. Circular Dependency Prevention + +**NEVER create these imports:** + +```typescript +// ❌ WRONG - Circular dependencies +import { Widget } from '@webex/cc-widgets'; // In widget package code +import { Widget } from '@webex/cc-widget-name'; // In cc-components code +``` + +**ALWAYS use this pattern:** + +```typescript +// ✅ CORRECT - Proper dependency flow +// In widget code: +import { Component } from '@webex/cc-components'; +import store from '@webex/cc-store'; +import { withMetrics } from '@webex/cc-ui-logging'; + +// In cc-widgets aggregator (ONLY): +import { Widget } from '@webex/cc-widget-name'; +``` + +**Dependency Flow (One Direction Only):** + +``` +cc-widgets → widget packages → cc-components → store → SDK +``` + +**Validation Before Code Generation:** + +- [ ] Widget does NOT import from `@webex/cc-widgets` +- [ ] cc-components does NOT import from any widget packages +- [ ] All imports follow one-directional flow +- [ ] No circular references between packages + +**If circular dependency is detected → STOP and refactor imports immediately.** + +### 2. Mandatory Design Input (For New Widgets) + +**STOP! Before generating ANY new widget code, collect design input.** + +#### Required Input (ONE of these): + +1. **Figma Link/File** + - Share Figma link or file + - LLM will extract design tokens, components, interactions + +2. **Screenshot/Mockup** + - Upload image of desired widget UI + - LLM will analyze colors, layout, components, spacing + +3. **Design Specification** + - Provide detailed specs: + - Colors (hex/RGB or Momentum tokens) + - Layout structure (flex/grid) + - Components needed (Button, Icon, Avatar, etc.) + - Typography (sizes, weights) + - Interactions (hover, click states) + +#### If Design Input Provided: + +**Analyze and document:** +- **Colors:** Extract hex/RGB values or Momentum tokens +- **Components:** Identify Momentum UI components to use +- **Layout:** Grid, flex, spacing patterns (8px/0.5rem grid) +- **Typography:** Sizes, weights (Momentum typography scale) +- **Interactions:** Buttons, hover states, transitions + +#### If NO Design Input: + +**ASK THE USER:** + +``` +⚠️ Design Input Required + +I cannot generate a widget without visual design reference. This ensures: +- UI matches your design system +- Correct Momentum components are used +- Proper styling and theming + +Please provide ONE of: +1. Figma link/file +2. Screenshot of desired UI +3. Detailed design specification (colors, layout, components) + +Once provided, I'll analyze it and generate the widget accordingly. +``` + +**DO NOT proceed without design input.** + +### 3. Use Package/Widget ai-docs When Working in That Scope + +When fixing, generating, or enhancing code in a package or widget, you MUST read and follow that scope's `ai-docs/AGENTS.md` (and `ai-docs/ARCHITECTURE.md` where available). See [Package and widget ai-docs reference](#package-and-widget-ai-docs-reference) in Step 2. The root AGENTS.md is the orchestrator; each package/widget AGENTS.md is the authoritative reference for that scope—use both. + +### 4. Complete Template Pre-Steps Before Code + +Before generating or changing any code, you MUST complete the **pre-step section** of the template for the task type (see table below). Either fill it from the developer's message and confirm, or ask the developer for missing items. Do not proceed to implementation steps until pre-steps are done or the developer explicitly asks to skip them. + +#### Mandatory pre-steps by task type (Rule 4) + +| Task type | Template | Mandatory pre-step (complete before code) | +| --------- | -------- | ----------------------------------------- | +| **A. Create New Widget** | [new-widget/00-master.md](./ai-docs/templates/new-widget/00-master.md) | Design input + [01-pre-questions.md](./ai-docs/templates/new-widget/01-pre-questions.md) (widget name, 4 technical inputs) | +| **B. Fix Bug** | [existing-widget/bug-fix.md](./ai-docs/templates/existing-widget/bug-fix.md) | [Pre-Fix Questions](./ai-docs/templates/existing-widget/bug-fix.md) (bug info, scope, impact, existing tests) | +| **C. Add Feature** | [existing-widget/feature-enhancement.md](./ai-docs/templates/existing-widget/feature-enhancement.md) | [Pre-Enhancement Questions](./ai-docs/templates/existing-widget/feature-enhancement.md) (feature info, requirements, compatibility, design input) | +| **D. Documentation only** | documentation templates | Optional: confirm scope with developer (no code change) | +| **E. Understanding** | Package ai-docs | None (read-only) | +| **F. Playwright E2E Test Work** | [playwright/00-master.md](./ai-docs/templates/playwright/00-master.md) | [Pre-Questions](./ai-docs/templates/playwright/01-pre-questions.md) (scope, scenarios, setup/utilities, stability expectations) | + +--- + +## Step 2: Load Context + +**Before generating code, load appropriate context:** + +### Always Read (Minimal Context) +1. **Pattern documentation** - [patterns/](./ai-docs/patterns/) folder + - [typescript-patterns.md](./ai-docs/patterns/typescript-patterns.md) - Type safety, naming conventions + - [react-patterns.md](./ai-docs/patterns/react-patterns.md) - Component patterns, hooks + - [mobx-patterns.md](./ai-docs/patterns/mobx-patterns.md) - State management with observer HOC + - [web-component-patterns.md](./ai-docs/patterns/web-component-patterns.md) - r2wc patterns + - [testing-patterns.md](./ai-docs/patterns/testing-patterns.md) - Jest, RTL, Playwright + +2. **Package/widget ai-docs (mandatory when working in that scope)** + When fixing, generating, or enhancing code in a package or widget, you **MUST** read that scope's `ai-docs/AGENTS.md` (and `ARCHITECTURE.md` where listed) and follow its instructions. The root AGENTS.md orchestrates; package/widget AGENTS.md is the authoritative reference for that scope. Use the table below to find the right path. + +#### Package and widget ai-docs reference + +| Scope you're working on | AGENTS.md | ARCHITECTURE.md | +| ----------------------- | --------- | --------------- | +| **station-login** | [packages/contact-center/station-login/ai-docs/AGENTS.md](packages/contact-center/station-login/ai-docs/AGENTS.md) | Same folder | +| **user-state** | [packages/contact-center/user-state/ai-docs/AGENTS.md](packages/contact-center/user-state/ai-docs/AGENTS.md) | Same folder | +| **task – CallControl** | [packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md](packages/contact-center/task/ai-docs/widgets/CallControl/AGENTS.md) | Same folder | +| **task – IncomingTask** | [packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md](packages/contact-center/task/ai-docs/widgets/IncomingTask/AGENTS.md) | Same folder | +| **task – OutdialCall** | [packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md](packages/contact-center/task/ai-docs/widgets/OutdialCall/AGENTS.md) | Same folder | +| **task – TaskList** | [packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md](packages/contact-center/task/ai-docs/widgets/TaskList/AGENTS.md) | Same folder | +| **store** | [packages/contact-center/store/ai-docs/AGENTS.md](packages/contact-center/store/ai-docs/AGENTS.md) | Same folder | +| **cc-components** | [packages/contact-center/cc-components/ai-docs/AGENTS.md](packages/contact-center/cc-components/ai-docs/AGENTS.md) | Same folder | +| **cc-widgets** | [packages/contact-center/cc-widgets/ai-docs/AGENTS.md](packages/contact-center/cc-widgets/ai-docs/AGENTS.md) | Same folder | +| **ui-logging** | [packages/contact-center/ui-logging/ai-docs/AGENTS.md](packages/contact-center/ui-logging/ai-docs/AGENTS.md) | Same folder | +| **samples-cc-react-app** | [widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md](widgets-samples/cc/samples-cc-react-app/ai-docs/AGENTS.md) | Same folder if present | +| **playwright framework** | [playwright/ai-docs/AGENTS.md](playwright/ai-docs/AGENTS.md) | [playwright/ai-docs/ARCHITECTURE.md](playwright/ai-docs/ARCHITECTURE.md) | + +**Task package note:** The task package has multiple widgets (CallControl, IncomingTask, OutdialCall, TaskList). When working on one of them, use that widget's ai-docs path above, not a generic task path. + +### Conditionally Read + +**If using SDK APIs:** +- Scan: [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) +- Find available methods, events, types +- Check method signatures before using + +**If working on Playwright tests/framework:** +- Read: `playwright/ai-docs/AGENTS.md` +- Read: `playwright/ai-docs/ARCHITECTURE.md` +- Use: `ai-docs/templates/playwright/00-master.md` and complete `01-pre-questions.md` before implementation + +--- + +## Step 3: SDK API Consultation (Before Code Generation) + +**Before using ANY SDK method, consult the SDK knowledge base.** + +### Process + +#### 1. Identify Required SDK Functionality + +Based on widget requirements, list needed operations: +- Making calls? → Search: "call", "dial", "telephony", "outdial" +- Fetching agents? → Search: "agent", "buddy", "team" +- Managing tasks? → Search: "task", "interaction", "contact" +- Checking state? → Search: "state", "status", "presence" + +#### 2. Search SDK Knowledge Base + +**File:** [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) + +**Search Strategy:** +- Use keyword search in JSON +- Look for method names, descriptions +- Check similar/related methods + +#### 3. Verify API Signature + +For each method found, confirm: +- ✅ Method name (exact spelling) +- ✅ Parameters (names, types, required vs optional) +- ✅ Return type +- ✅ Error conditions +- ✅ Usage notes + +#### 4. Use Correct Access Pattern + +```typescript +// ✅ CORRECT - Via store +await store.cc.methodName(params); + +// ❌ WRONG - Direct SDK import +import sdk from '@webex/contact-center'; +await sdk.methodName(params); +``` + +#### 5. Add Error Handling + +```typescript +try { + const result = await store.cc.methodName(params); + // Handle success + props.onSuccess?.(result); +} catch (error) { + console.error('SDK error:', error); + props.onError?.(error); +} +``` + +### Example: OutdialCall Widget + +**Requirement:** Make outbound call + +**SDK Search:** "outdial", "call", "dial" + +**Found:** `startOutdial(destination: string, origin: string)` + +**Usage:** + +```typescript +const handleDial = async (phoneNumber: string) => { + try { + await store.cc.startOutdial(phoneNumber, 'WidgetName'); + props.onDial?.(phoneNumber); + } catch (error) { + console.error('Outdial failed:', error); + props.onError?.(error); + } +}; +``` + +### Common SDK Operations + +- **Agent State:** `store.cc.setAgentState(state, reasonCode)` +- **Task Accept:** `task.accept()` +- **Task Hold:** `task.hold()` +- **Task End:** `task.end()` +- **Events:** `store.cc.on('eventName', handler)` + +--- + +## Step 4: Architecture Pattern + +**All code must follow this pattern:** + +``` +Widget (observer HOC) + ↓ +Custom Hook (business logic) + ↓ +Presentational Component (pure UI) + ↓ +Store (MobX singleton) + ↓ +SDK (Contact Center API) +``` + +**Key Rules:** +- Widget consumes SDK methods via the store (through a hook) — it NEVER calls the SDK directly +- Component NEVER accesses store (receives props) +- Always use `observer` HOC for widgets +- Always use `runInAction` for store mutations +- Always wrap with ErrorBoundary +- Always apply withMetrics HOC for exports + +--- + +## Step 5: Generate/Fix Code + +**Follow the template you were routed to in Step 1** + +You must have already completed that template's pre-step section (Pre-Enhancement Questions, Pre-Fix Questions, or 01-pre-questions as applicable); if not, do that first. + +**During code generation:** +1. Follow pattern documentation strictly +2. Reference existing widgets for examples +3. Use proper TypeScript types (no `any`) +4. Include error handling +5. Add loading/error states +6. Write tests alongside code + +--- + +## Step 5.5: Functionality Validation (CRITICAL) + +**After generating code, VERIFY functionality layer by layer.** + +### Validation Checklist + +#### 1. SDK Integration ✓ + +- [ ] SDK methods exist in [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) +- [ ] Parameters match SDK signature exactly +- [ ] Accessed via `store.cc.methodName()` (not direct import) +- [ ] Error handling present (try/catch) +- [ ] Success callbacks fire +- [ ] Error callbacks fire + +**If method not found or signature mismatch → FIX before proceeding** + +#### 2. Store Integration ✓ + +- [ ] Observable data accessed correctly +- [ ] `runInAction` used for mutations +- [ ] No direct store property assignments +- [ ] Store subscriptions cleaned up (useEffect return) + +#### 3. Event Flow ✓ + +**Trace each user interaction:** + +1. User action (click, input) → Event handler called? +2. Handler → State update triggered? +3. State update → Re-render triggered? +4. Re-render → UI updated correctly? + +**If ANY step fails → Debug and fix** + +#### 4. Data Flow ✓ + +**Trace data through ALL layers:** + +``` +User Action (UI) + ↓ +Widget Handler (index.tsx) + ↓ +Hook Method (helper.ts) + ↓ +Store/SDK Call + ↓ +Response/Observable Update + ↓ +Hook State Update + ↓ +Component Props + ↓ +UI Render +``` + +**Verify each transition is correct.** + +#### 5. UI Visual Validation ✓ + +**Compare with design input:** + +- [ ] Colors match (or use Momentum tokens) +- [ ] Spacing matches (8px/0.5rem grid) +- [ ] Components match design +- [ ] Layout matches (flex/grid structure) +- [ ] Typography matches (sizes, weights) +- [ ] Interactions work (hover, click, etc.) + +**If visual doesn't match → Update styling** + +#### 6. Import Validation ✓ + +**Check for circular dependencies:** + +```bash +# In widget code, search for: +grep -r "from '@webex/cc-widgets'" src/ +# Should return: NO MATCHES + +# In cc-components, search for: +grep -r "from '@webex/cc-.*-widget'" packages/contact-center/cc-components/src/ +# Should return: NO MATCHES +``` + +**If any matches found → Refactor imports** + +#### 7. Compiler Test ✓ + +```bash +yarn build +# Expected: NO ERRORS +``` + +**Common errors:** +- Missing types → Add type definitions +- Import errors → Check paths and exports +- Circular dependencies → Refactor imports +- Syntax errors → Fix code + +**Fix ALL compiler errors before completing.** + +--- + +## Step 6: Update Documentation + +**CRITICAL: After any code change, check if documentation needs updates** + +**Ask developer:** "The code changes are complete. Do I need to update any documentation?" + +### Documentation to Consider + +**If new widget created:** +- Generated via templates (AGENTS.md + ARCHITECTURE.md) + +**If widget modified:** +- Update: `packages/contact-center/{widget-name}/ai-docs/AGENTS.md` (if API changed) +- Update: `packages/contact-center/{widget-name}/ai-docs/ARCHITECTURE.md` (if architecture changed) +- Add: New examples to AGENTS.md (if new use cases) +- Update: Troubleshooting in ARCHITECTURE.md (if new issues discovered) + +**If store modified:** +- Update: `packages/contact-center/store/ai-docs/AGENTS.md` +- Update: `packages/contact-center/store/ai-docs/ARCHITECTURE.md` + +**If component library modified:** +- Update: `packages/contact-center/cc-components/ai-docs/AGENTS.md` + +**If new pattern established:** +- Update: Relevant pattern file in [patterns/](./ai-docs/patterns/) + +**If architecture changed:** +- Update: Relevant architecture documentation as needed + +**If Playwright E2E framework/docs changed:** +- Update: `playwright/ai-docs/AGENTS.md` +- Update: `playwright/ai-docs/ARCHITECTURE.md` +- Update relevant modules in: `ai-docs/templates/playwright/` + +--- + +## Step 7: Validation & Review + +**Before marking task complete:** + +1. **Run validation checks** + - Tests pass: `yarn test:unit` + - Linting passes: `yarn test:styles` + - Build succeeds: `yarn build` + +2. **Code quality checks** + - Follows patterns + - No layer violations + - Error handling present + - Types are correct + - Code is precise and concise (no unnecessary complexity or dead code) + +3. **Documentation checks** + - AGENTS.md updated if needed + - ARCHITECTURE.md updated if needed + - Examples work + +4. **Ask developer for review:** + - "Task complete. Would you like to review the changes?" + - "Should I make any adjustments?" + - "Is the documentation clear?" + +--- + +## Repository Structure + +``` +ccWidgets/ +├── packages/contact-center/ +│ ├── ai-docs/migration/ # Task refactor migration docs (old → new) +│ ├── station-login/ # Widget with ai-docs/ +│ ├── user-state/ # Widget with ai-docs/ +│ ├── task/ # Widget package +│ ├── store/ # MobX store with ai-docs/ +│ ├── cc-components/ # React components with ai-docs/ +│ ├── cc-widgets/ # Web Component wrappers with ai-docs/ +│ ├── ui-logging/ # Metrics utilities with ai-docs/ +│ └── test-fixtures/ # Test mocks with ai-docs/ +├── widgets-samples/ +│ └── cc/ +│ ├── samples-cc-react-app/ # React sample +│ └── samples-cc-wc-app/ # Web Component sample +├── playwright/ # E2E tests +└── ai-docs/ + ├── AGENTS.md # This file + ├── RULES.md # Repository rules + ├── patterns/ # Repo-wide patterns + ├── templates/ # Code generation templates + └── contact-centre-sdk-apis/ # SDK API reference +``` + +--- + +## Common Questions to Ask + +**Before starting any work:** +- "What component/widget are you working on?" +- "Is this a new widget, bug fix, or enhancement?" +- "Do you have design specifications (Figma, screenshots)?" + +**During code generation:** +- "Should I add/update tests?" +- "Do you want examples in documentation?" +- "Should I update the sample apps?" + +**After code generation:** +- "The code is complete. Should I update documentation?" +- "Would you like to review before I mark this complete?" +- "Should I check for any other impacted components?" + +--- + +## SDK Knowledge Base + +**Location:** [contact-centre-sdk-apis/contact-center.json](./contact-centre-sdk-apis/contact-center.json) + +**Contents:** +- All exposed SDK APIs (methods, events, types) +- Method signatures and parameters +- Event names and data structures +- Links to SDK source code (next branch) + +**Usage:** +- Scan JSON when using SDK methods +- Search for API by name or functionality +- Check parameter types and return values +- Verify event names before subscribing + +**Note:** This JSON is TypeDoc output from @webex/contact-center SDK + +--- + +## Success Criteria + +**Code generation/fix is successful when:** +- ✅ Follows architecture pattern (Widget → Hook → Component → Store → SDK) +- ✅ Uses patterns correctly (TypeScript, React, MobX, WC) +- ✅ Includes proper error handling +- ✅ Has tests with good coverage +- ✅ Documentation is updated (if code changed) +- ✅ Works in both sample apps (React + WC) +- ✅ No console errors or warnings +- ✅ Passes validation checks +- ✅ Developer approves changes + +--- + +## Related Documentation + +- **Repository Rules:** [RULES.md](./RULES.md) +- **Templates Overview:** [templates/README.md](./ai-docs/templates/README.md) +- **Task Refactor Migration (Contact Center):** [packages/contact-center/ai-docs/migration/migration-overview.md](./packages/contact-center/ai-docs/migration/migration-overview.md) — overview and entry point for CC SDK task-refactor migration docs + +--- + +_Last Updated: 2025-11-26_ diff --git a/ai-docs/adr/0001-one-directional-dependency-flow.md b/ai-docs/adr/0001-one-directional-dependency-flow.md new file mode 100644 index 000000000..d63866e44 --- /dev/null +++ b/ai-docs/adr/0001-one-directional-dependency-flow.md @@ -0,0 +1,53 @@ +# ADR-0001 — One-directional dependency flow with a single SDK boundary + +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../ARCHITECTURE.md). This is a standing `ai-docs/adr/` decision record; the folder README explains numbering/supersession. +> Context-efficiency: link to canonical docs — don't duplicate them; one decision per file. + +| Field | Value | +|---|---| +| Status | Accepted | +| Date | 2026-06-29 | +| Deciders | CC widgets architect + package maintainers | +| Supersedes / Superseded by | none | +| Generated from | `adr` @ SDLC template library `0.1.0-draft` | + +## Context +The monorepo ships several independently-publishable packages that compose into framework-agnostic Web +Components. Without a stated direction of dependency, packages would import each other freely, producing +import cycles between presentational primitives, feature widgets, and the aggregating wrapper layer — and +every package would couple directly to the Webex Contact Center SDK (`@webex/contact-center`), so an SDK +change would ripple through the whole tree and SDK calls would be impossible to mock at one place in tests. + +The actual `package.json` dependency graph already encodes a single direction: +- `@webex/cc-station-login`, `@webex/cc-user-state`, `@webex/cc-task` → depend on `@webex/cc-components` + `@webex/cc-store`. +- `@webex/cc-components` → depends on `@webex/cc-store` + `@webex/cc-ui-logging`. +- `@webex/cc-widgets` → depends on the feature widgets (`cc-station-login`, `cc-user-state`, `cc-task`, `cc-digital-channels`) + `@webex/cc-store`. +- `@webex/cc-store` → is the only package that depends on `@webex/contact-center` (the SDK). + +This ADR records that arrangement as a deliberate constraint, not an accident. + +## Decision +Dependencies flow in exactly one direction: + +``` +cc-widgets → widget packages (station-login, user-state, task, digital-channels) → cc-components → store → SDK +``` + +No package may import "upstream" against this arrow (cc-components must not import a widget package; a +widget must not import cc-widgets). The Webex Contact Center SDK is accessed **only** through the +`@webex/cc-store` MobX singleton (`Store.getInstance()`, `store.cc.*`); no other package imports +`@webex/contact-center` directly. The store is the single SDK boundary for the entire repo. + +## Alternatives Considered +| Alternative | Pros | Cons | Why rejected | +|---|---|---|---| +| Widgets / components call the SDK directly | Fewer hops for a single call | SDK coupling scattered across every package; an SDK change ripples everywhere; SDK calls hard to mock in tests | Rejected — defeats the single-boundary goal and makes the dependency graph cyclic | +| Multiple stores (one per widget) | Local state ownership per feature | Cross-widget state (agent state, active task, login) becomes incoherent; duplicated SDK wiring; sync bugs | Rejected — a single MobX singleton keeps global CC state coherent across widgets | + +## Consequences +- **Positive:** No import cycles; SDK coupling isolated to one package, so SDK upgrades touch one boundary; widgets/components are trivially testable by mocking the store; global CC state stays coherent. +- **Negative / cost:** All SDK surface a widget needs must be exposed through the store first; adding a new SDK call means extending the store rather than calling inline. +- **Agents must:** Never import `@webex/contact-center` outside `packages/contact-center/store/`; never add an upstream import that reverses the dependency arrow; route all SDK access through `store.cc.*`. + +## Revisit When +- A second legitimate SDK boundary emerges (e.g. a non-CC SDK with no shared state) that the single store cannot reasonably own. diff --git a/ai-docs/adr/README.md b/ai-docs/adr/README.md new file mode 100644 index 000000000..bad2b3ab7 --- /dev/null +++ b/ai-docs/adr/README.md @@ -0,0 +1,19 @@ +# ai-docs/adr/ — Architecture Decision Records + +Standing, append-only record of **why** the architecture is the way it is — including the options that +were rejected. Unlike temporary generation records, ADRs live with the repo forever, so an agent sees +the reasoning behind a constraint instead of "fixing" it by accident. + +## Use ADRs For + +Use ADRs for durable architecture decisions that constrain future work. Do not use ADRs for run +notes, temporary investigation findings, or per-feature task history. + +- **Fill-in shape:** see `0001-one-directional-dependency-flow.md` (Context · Decision · Alternatives Considered · Consequences · Revisit When). +- **Numbering:** one file per decision, `NNNN-short-title.md` (zero-padded, monotonic). +- **Immutability:** ADRs are immutable once `Accepted`. To change a decision, write a new ADR that + supersedes the old one (and set the old one's status to `Superseded by ADR-NNNN`). +- Reference ADRs from `../ARCHITECTURE.md` and module specs where a decision constrains the design. + +Each ADR carries a navigation pointer back to the root `../../AGENTS.md`, router `../SPEC_INDEX.md`, and +system `../ARCHITECTURE.md`. diff --git a/ai-docs/rules/README.md b/ai-docs/rules/README.md new file mode 100644 index 000000000..c207aca84 --- /dev/null +++ b/ai-docs/rules/README.md @@ -0,0 +1,17 @@ +# ai-docs/rules/ — deeper repo rules + +`../../AGENTS.md` carries the 5–10 **critical** rules; the repo-wide `../RULES.md` is the digest; this +folder holds the **fuller, per-rule detail** an agent loads on demand. + +## Use Rules For + +Use rule files when a future change must consistently follow a repo-specific constraint. Keep the +short rule in `../../AGENTS.md` or `../RULES.md`; put examples, rationale, and enforcement details here. + +- **Fill-in shape:** see `sdk-access-via-store.md` (Rule · Why · How to follow · Enforced by). +- **Routing:** generic rules live directly in `ai-docs/rules/`; language-specific ones in + `ai-docs/rules//`. +- **Defer to tooling:** if a linter/CI already enforces a rule, the rule file points to that rather than + restating it. + +Each rule file carries a navigation pointer back to the root `../../AGENTS.md` and router `../SPEC_INDEX.md`. diff --git a/ai-docs/rules/sdk-access-via-store.md b/ai-docs/rules/sdk-access-via-store.md new file mode 100644 index 000000000..414341298 --- /dev/null +++ b/ai-docs/rules/sdk-access-via-store.md @@ -0,0 +1,31 @@ +# Rule: Access the SDK only through the store + +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry, carries the critical rules) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is an `ai-docs/rules/` fill-in; the folder README explains generic-vs-per-language routing; the repo-wide rules digest is `../RULES.md`. +> Context-efficiency: link to canonical docs — don't duplicate them; one rule per file; defer to the linter where it enforces. + +## Rule +Never import the Webex Contact Center SDK (`@webex/contact-center`) directly in a widget or component; +always reach the SDK through the store singleton via `store.cc.*`. + +## Why +The store (`@webex/cc-store`) is the single SDK boundary for the repo (see ADR-0001). Importing the SDK +elsewhere scatters SDK coupling across packages — an SDK upgrade then ripples through every importer, +SDK calls become impossible to mock at one place so unit tests get brittle, and error handling around +SDK calls fragments instead of being centralized. Keeping access in the store keeps coupling, mocking, +and consistent error/event handling at one point. + +## How to follow +- The SDK is a dependency of `@webex/cc-store` only. Its access layer lives in + `packages/contact-center/store/src/` (the store wraps the SDK and exposes `store.cc`). +- In a widget or component, get the singleton and call through it: + ```ts + import Store from '@webex/cc-store'; + const store = Store.getInstance(); + await store.cc.someMethod(); // never: import {...} from '@webex/contact-center' + ``` +- In tests, mock the store (or `store.cc`) rather than the SDK. + +## Enforced by +Review only — no automated lint rule yet. The repo's ESLint config (`.eslintrc`) does not include a +`no-restricted-imports` ban on `@webex/contact-center`, so reviewers enforce this (review check C6). +Consider adding a `no-restricted-imports` rule scoped to non-store packages to make it automatic. diff --git a/packages/@webex/widgets/ai-docs/widgets-spec.md b/packages/@webex/widgets/ai-docs/widgets-spec.md new file mode 100644 index 000000000..711bd7891 --- /dev/null +++ b/packages/@webex/widgets/ai-docs/widgets-spec.md @@ -0,0 +1,416 @@ +# Meetings Widgets — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, state, UI, host integration, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `meetings-widgets` | +| Source path(s) | `packages/@webex/widgets/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| Validation status | not-run | + +Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with +`<0-100%>` plus the report path/evidence. + +## Evidence Rules +Every requirement below cites concrete source evidence using `file path`. Source evidence, test evidence, +examples, assumptions, and gaps are kept separate so validators and future agents can distinguish truth +from context. Test evidence is preferred for WHY. This module's only repo-resident source is the widget +wrapper and its tests; the meeting UI, adapter, and SDK live in external packages (`@webex/components`, +`@webex/sdk-component-adapter`, `webex`) and are cited as dependency boundaries, not as in-repo evidence. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/@webex/widgets/ai-docs/AGENTS.md` | overview / API / examples | reconciled | Orientation → Overview/Purpose; props → Public Surface; capabilities → Use Cases. External-package API tables (adapter/control methods) retained as reference under Class/Component Relationships and Sequence Diagrams; they describe `@webex/sdk-component-adapter`, not this repo's code. | +| `ai-docs/_archive/pre-sdlc-migration/packages/@webex/widgets/ai-docs/ARCHITECTURE.md` | architecture / flows / state | reconciled | Three-repo layering → Design Overview/Data Flow; event flows → Sequence Diagram(s); state machine → State Machine; troubleshooting → Pitfalls. Component table is external (`@webex/components`) and kept as reference, not as owned design. | +| `packages/@webex/widgets/src/widgets/WebexMeetings/README.md` | overview / usage | reference-only | Layout values, custom-controls contract, and browser limitations folded into Public Surface, UI Flow, and Pitfalls. | +| `ai-docs/CONTRACTS.md` | API/contract index | conflicting / not applicable | The root contracts catalog documents the Contact Center widget family only; it does NOT list `@webex/widgets`. This spec routes Public Surface to the package entry point (`packages/@webex/widgets/src/index.js`) instead of a CONTRACTS anchor. Flagged as a gap for human follow-up. | + +## Overview +`@webex/widgets` is the **legacy Webex Meetings widget family** and is a separate concern from the Contact +Center (CC) widgets that dominate this monorepo. It does NOT use the CC store (`@webex/cc-store`), the CC +`Widget → Hook → Component → Store → SDK` dependency flow, MobX, TypeScript, or the r2wc custom-element +wrappers. It is a standalone React (JSX + PropTypes) package whose single public export is +`WebexMeetingsWidget` (`packages/@webex/widgets/src/index.js:1`). + +The package is a thin **composition/integration layer**. The repo-resident code is one widget component +(`packages/@webex/widgets/src/widgets/WebexMeetings/WebexMeetings.jsx`) plus an SVG logo. The actual meeting +experience comes from three external Webex repositories that the widget wires together: `webex` (the +JS SDK), `@webex/sdk-component-adapter` (an RxJS-observable adapter over the SDK), and `@webex/components` +(the React meeting UI and its `withAdapter`/`withMeeting` HOCs and hooks). The widget's own responsibility +is small: construct the SDK + adapter from an `accessToken`, hand the adapter to `@webex/components` via the +`withAdapter` HOC, choose between the media-permission prompt and the full meeting view based on the +meeting's permission state, and layer in a custom keyboard-accessibility shim over the rendered DOM. + +A maintainer should start at `WebexMeetings.jsx`: the default export (bottom of file) is +`withAdapter(withMeeting(WebexMeetingsWidget), adapterFactory)`, so the class you read is wrapped twice +before it ships. SDK construction lives only in the `adapterFactory` argument; the class body handles render +branching and accessibility. Behavior beyond that (joining, muting, sharing, device switching, state +transitions) is owned by the external adapter and components, not by this repo. + +## Purpose / Responsibility +Owns the embeddable `WebexMeetingsWidget` React component: it bootstraps a Webex SDK instance + adapter from +an access token, renders either the browser media-permission prompt or the `@webex/components` +`WebexMeeting` view, and applies a custom keyboard/focus accessibility shim. It does NOT own meeting +lifecycle logic, media negotiation, controls, or UI internals — those belong to `webex`, +`@webex/sdk-component-adapter`, and `@webex/components`. + +## Stack +JavaScript (ES + JSX, no TypeScript), React 18.3.1 with class components and `prop-types` for prop +validation. Build: webpack 5 producing an ESM bundle (`main`/`module` → `dist/webexWidgets.mjs`), Babel +(`@babel/preset-env`, `@babel/preset-react`). Tests: Jest + React Testing Library (`tests/`, +`jest.config.js`) for unit; WebdriverIO + Jasmine (`wdio.conf.js`, `tests/WebexMeeting.e2e.js`) for E2E. +Released via `semantic-release`. Source: `packages/@webex/widgets/package.json`. + +## Folder / Package Structure +``` +packages/@webex/widgets/ +├── src/ +│ ├── index.js # Package barrel — exports WebexMeetingsWidget +│ └── widgets/WebexMeetings/ +│ ├── WebexMeetings.jsx # The widget (class + withAdapter/withMeeting wrap + adapter factory) +│ ├── WebexMeetings.css # Widget root + content styles +│ ├── WebexLogo.jsx # Inline SVG logo (passed to WebexMeeting) +│ ├── webex-logo.svg # Logo asset +│ └── README.md # Usage, layouts, custom controls, browser limitations +├── tests/ +│ ├── WebexMeetings/WebexMeetings.test.jsx # Unit tests (Jest + RTL) +│ ├── WebexMeeting.e2e.js # E2E suite (WebdriverIO) +│ ├── pages/ # E2E page objects (MeetingWidget, Samples) +│ └── util.js # E2E test-user/SDK helpers +├── demo/ # Standalone demo app (webpack dev server) +├── docs/ # Built demo bundle (generated; not source) +├── jest.config.js # Jest config (sets __appVersion__, ignores WebexLogo from coverage) +├── webpack.config.js # Build + demo wiring (defines __appVersion__ from version) +├── wdio.conf.js # E2E runner config +└── package.json # Manifest; deps pin webex/components/adapter versions +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `packages/@webex/widgets/src/index.js` | The public export barrel — the package's entire public surface (`WebexMeetingsWidget`). | +| `packages/@webex/widgets/src/widgets/WebexMeetings/WebexMeetings.jsx` | Widget class, `propTypes`/`defaultProps` (authoritative prop contract), render branching, the `withAdapter`/`withMeeting` wrap, and the SDK `adapterFactory` (SDK config: `appName`, `appVersion`, `fedramp`, experimental meetings flags). | +| `packages/@webex/widgets/package.json` | Pinned versions of `webex`, `@webex/sdk-component-adapter`, `@webex/components`, peer deps. Never infer these versions elsewhere. | +| `packages/@webex/widgets/jest.config.js` / `webpack.config.js` | Define the `__appVersion__` global the widget references at build/test time. | + +## Public Surface +This package is consumed as an npm library; its public surface is the single named export and the props it +accepts. There is no HTTP/event/CLI surface, and (unlike the CC widgets) no r2wc custom element — it is a +plain React component. The authoritative prop contract is `WebexMeetingsWidget.propTypes` / +`defaultProps` in `WebexMeetings.jsx`. + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `meetings-widgets.WebexMeetingsWidget` | SDK (React component export) | `import {WebexMeetingsWidget} from '@webex/widgets'` | Embed a full Webex meeting experience in a React host | Public; export name + required props are the breaking surface (semver) | `packages/@webex/widgets/src/index.js:1`; props in `packages/@webex/widgets/src/widgets/WebexMeetings/WebexMeetings.jsx:231-255` | Not catalogued in `../../../../ai-docs/CONTRACTS.md` (CC-only) — see Source Material Register `[NEEDS HUMAN INPUT]` | + +Props (from `WebexMeetings.jsx:231-255`): + +| Prop | Type | Required | Default | Purpose | +|---|---|---|---|---| +| `accessToken` | string | Yes | — | Webex access token; feeds the SDK `adapterFactory` | +| `meetingDestination` | string | Yes | — | URL / SIP / email / PMR; consumed by `withMeeting` HOC | +| `meetingPasswordOrPin` | string | No | `''` | Password or host pin, forwarded to `WebexMeeting` | +| `participantName` | string | No | `''` | Guest display name, forwarded to `WebexMeeting` | +| `fedramp` | bool | No | `false` | Sets `config.fedramp` in the SDK factory | +| `layout` | string | No | `'Grid'` | Remote video layout, forwarded to `WebexMeeting` | +| `controls` | func | No | `undefined` | Returns control IDs to render; forwarded to `WebexMeeting` | +| `controlsCollapseRangeStart` | number | No | `undefined` | First collapsible control index; forwarded | +| `controlsCollapseRangeEnd` | number | No | `undefined` | Index before last collapsible control; forwarded | +| `className` | string | No | `''` | Appended to the root `webex-meetings-widget` class | +| `style` | object | No | `{}` | Inline style on the root element | + +Compatibility notes: +- Adding an optional prop is additive (minor). Renaming/removing the `WebexMeetingsWidget` export, or + changing the type/requiredness of `accessToken` or `meetingDestination`, is breaking (major). +- The `controls` function contract (receives `inMeeting: boolean`, returns string[] of control IDs) is + defined by `@webex/components`; see `src/widgets/WebexMeetings/README.md`. + +## Requires (dependencies) +- `webex` `2.60.4` (pinned, also peer) — the JS SDK; constructed in the `adapterFactory` + (`WebexMeetings.jsx:260`). No fallback: an invalid/expired token leaves the widget on a loading state. +- `@webex/sdk-component-adapter` `1.112.12` (pinned) — RxJS adapter wrapping the SDK; constructed via + `new WebexSDKAdapter(webex)` (`WebexMeetings.jsx:277`). +- `@webex/components` `1.275.2` (pinned) — supplies `WebexMeeting`, `WebexMediaAccess`, and the + `withAdapter`/`withMeeting` HOCs (`WebexMeetings.jsx:4`) plus the `webex-components.css`. +- `@webex/component-adapter-interfaces` `^1.30.5` — meeting-state enum (`MeetingState`) and adapter + interfaces used by the adapter/components layers. +- Peer: `react` / `react-dom` `18.3.1`, `prop-types` `^15.7.2` — host must provide these. +- Build globals: `__appVersion__` (defined by webpack/jest) is read in the SDK config. +- Versions are authoritative in `packages/@webex/widgets/package.json` — do not hardcode them elsewhere. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `MEETINGS-WIDGETS-R-001` | Default export is `WebexMeetingsWidget` wrapped by `withMeeting` then `withAdapter`, so consumers get SDK + adapter + meeting bootstrapped automatically. | Consumers should not build meeting/adapter logic themselves; the HOC chain is the integration contract. | `WebexMeetings.jsx:259`, `index.js:1` | `tests/WebexMeetings/WebexMeetings.test.jsx:28-44` (mocks `withAdapter`/`withMeeting`, captures factory) | none | PRESENT | +| `MEETINGS-WIDGETS-R-002` | The `adapterFactory` constructs `new Webex({credentials:{access_token}})` and returns `new WebexSDKAdapter(webex)`. | The token-to-adapter wiring is the widget's core job; everything downstream depends on it. | `WebexMeetings.jsx:259-278` | `tests/WebexMeetings/WebexMeetings.test.jsx:516-570` (token, fedramp, experimental config, adapter creation) | none | PRESENT | +| `MEETINGS-WIDGETS-R-003` | SDK config sets `fedramp` from the prop, `appVersion` from `__appVersion__`, and meetings experimental flags `enableUnifiedMeetings`/`enableAdhocMeetings = true`. | FedRAMP environments and unified/adhoc meetings must be enabled at SDK construction; flipping these later is not possible. | `WebexMeetings.jsx:263-274` | `tests/WebexMeetings/WebexMeetings.test.jsx:527-562` | none | PRESENT | +| `MEETINGS-WIDGETS-R-004` | `appName` is `webex-widgets-meetings` in production and `webex-widgets-meetings-dev` otherwise. | Telemetry/identification must distinguish prod from dev builds. | `WebexMeetings.jsx:257` | `tests/WebexMeetings/WebexMeetings.test.jsx:572-580` (dev appName) | No test asserts the production branch (NODE_ENV=production) | PRESENT | +| `MEETINGS-WIDGETS-R-005` | When `meeting.localAudio.permission === 'ASKING'` the widget renders `WebexMediaAccess` with `media="microphone"`; audio ASKING takes priority over video ASKING. | The browser permission prompt must show before the meeting UI, and a single prompt is shown at a time. | `WebexMeetings.jsx:181-191` | `tests/WebexMeetings/WebexMeetings.test.jsx:104-135` | none | PRESENT | +| `MEETINGS-WIDGETS-R-006` | When `meeting.localVideo.permission === 'ASKING'` (and audio is not) the widget renders `WebexMediaAccess` with `media="camera"`. | Camera permission prompt path. | `WebexMeetings.jsx:190-191` | `tests/WebexMeetings/WebexMeetings.test.jsx:115-124,144-149` | none | PRESENT | +| `MEETINGS-WIDGETS-R-007` | When neither permission is ASKING, the widget renders `WebexMeeting`, forwarding `meetingID`, `meetingPasswordOrPin`, `participantName`, `layout`, `controls`, collapse range, and a fixed `webex-meetings-widget__content` className + `WebexLogo`. | The full meeting view is the default; prop forwarding is the integration contract with `@webex/components`. | `WebexMeetings.jsx:192-206` | `tests/WebexMeetings/WebexMeetings.test.jsx:151-178` | none | PRESENT | +| `MEETINGS-WIDGETS-R-008` | The root element has class `webex-meetings-widget` (plus optional `className`), applies `style`, and is focusable (`tabIndex=0`). | Host styling/sizing and the accessibility shim depend on a stable, focusable root. | `WebexMeetings.jsx:208-218` | `tests/WebexMeetings/WebexMeetings.test.jsx:96-102,180-192,202-206` | none | PRESENT | +| `MEETINGS-WIDGETS-R-009` | Defaults: `layout='Grid'`, `className=''`, `meetingPasswordOrPin=''`, `participantName=''`, `fedramp=false`, `style={}`, controls/range `undefined`. | Optional props must behave predictably when omitted. | `WebexMeetings.jsx:245-255` | `tests/WebexMeetings/WebexMeetings.test.jsx:195-219` | none | PRESENT | +| `MEETINGS-WIDGETS-R-010` | On mount the widget installs focus/keyboard handlers (focus redirect to media container or Join button, arrow-key cycling across control-bar buttons, Tab into inner-meeting interactive elements) and a `MutationObserver` to re-attach them; it disconnects the observer on unmount. | Custom a11y shim because base `@webex/components` `WebexMeeting` lacks these; observer must be cleaned up to avoid leaks. | `WebexMeetings.jsx:41-177,222-228` | `tests/WebexMeetings/WebexMeetings.test.jsx:235-513` (focus, arrow nav, Tab, observer re-attach, unmount disconnect) | This is an explicitly temporary workaround to remove once upstream supports it (`WebexMeetings.jsx:36-39`) | PRESENT | + +## Design Overview +The widget is a composition seam between three external Webex repos. Structurally it is a single React +class component exported through two HOCs supplied by `@webex/components`: + +1. `withAdapter(Component, adapterFactory)` calls `adapterFactory(props)` once to build the SDK + adapter, + calls `adapter.connect()` (device register → mercury WebSocket → meetings register/sync), and provides + the adapter through `AdapterContext`. The factory here builds `new Webex(...)` from `accessToken` and + wraps it in `new WebexSDKAdapter(...)` (`WebexMeetings.jsx:259-278`). +2. `withMeeting(Component)` creates a meeting from `meetingDestination` and injects the live meeting object + as the `meeting` prop. +3. `WebexMeetingsWidget` (the class) renders. Its `render()` reads `meeting.localAudio.permission` / + `localVideo.permission` and branches: ASKING → `WebexMediaAccess` prompt; otherwise → `WebexMeeting`. + +This split matters for debugging: SDK/adapter construction and meeting creation happen in the HOC layer +(external), not in the class's `componentDidMount`/`componentWillUnmount`. The class lifecycle only manages +the accessibility shim (focus routing, arrow-key navigation, a `MutationObserver`). The archived +troubleshooting notes call this out explicitly — duplicate-initialization bugs must be chased in the HOCs, +not here (`Pitfalls`). + +The widget deliberately stays declarative and stateless: it holds no React state and no MobX store. All +meeting state lives in the adapter's RxJS observables, surfaced to UI components via `@webex/components` +hooks (`useMeeting`, `useMeetingControl`). The widget only reads `meeting.ID` and the two `permission` +fields off the injected `meeting` prop. + +## Data Flow +Data is in-process React props/context outbound, and RxJS observable subscriptions inbound; the SDK itself +talks to the backend over REST + a Mercury WebSocket. The widget participates only at the edges (build +adapter; read permission fields; forward props). + +```mermaid +flowchart TD + Host[Host React App] -->|accessToken, meetingDestination, props| WAdapter[withAdapter HOC @webex/components] + WAdapter -->|adapterFactory props| Factory["new Webex() -> new WebexSDKAdapter()"] + Factory --> Adapter[WebexSDKAdapter / MeetingsSDKAdapter] + Adapter -->|connect: device.register, mercury.connect, meetings.register| SDK[webex JS SDK] + SDK <-->|REST + Mercury WebSocket| Backend[(Webex Backend)] + WAdapter -->|AdapterContext + meeting prop via withMeeting| Widget[WebexMeetingsWidget] + Widget -->|permission==ASKING| MediaAccess[WebexMediaAccess] + Widget -->|else| Meeting[WebexMeeting @webex/components] + Meeting -->|useMeeting / useMeetingControl| Adapter + Adapter -->|RxJS BehaviorSubject emits meeting state| Meeting +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Bootstrap + render branch (this repo's code) | "Mount → adapter factory → permission branch" | Loading state when adapter not ready; ASKING permission shows prompt instead of meeting | +| Join / authenticate (external adapter + components) | "Join meeting with optional password" | alt branch for password required / invalid password | +| State transition to JOINED / LOBBY (external) | "Waiting for host (LOBBY)" | Catch-all `else` renders WebexWaitingForHost for non-terminal states | + +```mermaid +sequenceDiagram + participant Host + participant withAdapter as withAdapter HOC (@webex/components) + participant Factory as adapterFactory (WebexMeetings.jsx) + participant Widget as WebexMeetingsWidget + participant Meeting as meeting prop (adapter observable) + + Host->>withAdapter: render + withAdapter->>Factory: adapterFactory(props) + Factory->>Factory: new Webex({credentials, config:{appName,appVersion,fedramp,meetings.experimental}}) + Factory-->>withAdapter: new WebexSDKAdapter(webex) + withAdapter->>withAdapter: adapter.connect() (device register, mercury, meetings sync) + withAdapter->>Widget: provide AdapterContext + meeting prop (via withMeeting) + alt localAudio.permission === ASKING + Widget->>Widget: render WebexMediaAccess media="microphone" + else localVideo.permission === ASKING + Widget->>Widget: render WebexMediaAccess media="camera" + else permissions resolved + Widget->>Meeting: render WebexMeeting (forward props + logo) + end + Widget->>Widget: componentDidMount installs focus/arrow-key shim + MutationObserver +``` + +```mermaid +sequenceDiagram + participant User + participant Meeting as WebexMeeting (@webex/components) + participant Adapter as MeetingsSDKAdapter + participant SDK as webex JS SDK + participant Backend + + User->>Meeting: Click "Join meeting" + Meeting->>Adapter: JoinControl.action({meetingID}) + Adapter->>Adapter: joinMeeting(ID, {password, name}) + alt password required + Adapter->>SDK: verifyPassword(password, captcha) + SDK->>Backend: verify + alt invalid + Backend-->>SDK: rejected + Adapter-->>Meeting: emit {invalidPassword:true} + Meeting->>User: show error in auth modal + end + end + Adapter->>SDK: join({pin, moderator, alias}) + SDK->>Backend: join session + negotiate media + Backend-->>SDK: media established + Adapter-->>Meeting: emit {state: JOINED} + Meeting->>Meeting: transition interstitial -> in-meeting view +``` + +```mermaid +sequenceDiagram + participant Meeting as WebexMeeting + participant Adapter as MeetingsSDKAdapter + participant SDK as webex JS SDK + participant Backend + + Note over Meeting: state is LOBBY (host not yet present) + Meeting->>Meeting: else catch-all -> render WebexWaitingForHost + Backend-->>SDK: WebSocket members:update, self.state -> JOINED + SDK-->>Adapter: state change (propagated unfiltered) + Adapter-->>Meeting: emit {state: JOINED} + Meeting->>Meeting: transition WaitingForHost -> in-meeting +``` + +## Class / Component Relationships +The only repo-owned types are `WebexMeetingsWidget` (class) and `WebexLogo` (function component). Everything +else is imported. The diagram shows the owned class, the HOC wrap, and the external collaborators it renders +or constructs. + +```mermaid +classDiagram + class WebexMeetingsWidget { + +props.meeting + +props.accessToken + +render() + +componentDidMount() a11y shim + MutationObserver + +componentWillUnmount() disconnect observer + -widgetDiv + -_arrowNavObserver + } + class adapterFactory { + +creates Webex(config) + +creates WebexSDKAdapter(webex) + } + class WebexLogo + class withAdapter~HOC~ + class withMeeting~HOC~ + class WebexMeeting~external~ + class WebexMediaAccess~external~ + class WebexSDKAdapter~external~ + class Webex~external~ + + withAdapter --> withMeeting : wraps + withMeeting --> WebexMeetingsWidget : wraps, injects meeting prop + withAdapter --> adapterFactory : invokes + adapterFactory --> Webex : new + adapterFactory --> WebexSDKAdapter : new(webex) + WebexMeetingsWidget --> WebexMeeting : renders (default branch) + WebexMeetingsWidget --> WebexMediaAccess : renders (ASKING branch) + WebexMeetingsWidget --> WebexLogo : renders logo prop +``` + +Reference (external, not repo-owned): `WebexMeeting` further orchestrates `WebexInterstitialMeeting`, +`WebexInMeeting`, `WebexWaitingForHost`, `WebexMeetingControlBar`, roster/settings/auth components, and the +`MeetingsSDKAdapter` control classes (`AudioControl`, `VideoControl`, `ShareControl`, `JoinControl`, +`ExitControl`, `RosterControl`, `SettingsControl`, switch-device controls). These live in +`@webex/components` and `@webex/sdk-component-adapter`; see those repos for their internals. + +## Use Cases +- **UC-1 Embed and bootstrap a meeting:** Host renders `` → `withAdapter` builds SDK+adapter and connects → `withMeeting` creates the meeting → widget renders `WebexMeeting`. Outcome: a live meeting UI. Evidence: `WebexMeetings.jsx:259-278`, `tests/WebexMeetings/WebexMeetings.test.jsx:151-178,516-570`. + - UI flow: host sizes the widget via `style`/`className` (fluid by default — `README.md`); the meeting view fills the root. + - Cross-service: SDK `connect()` registers device + opens Mercury WebSocket before the meeting renders (external HOC). +- **UC-2 Grant browser media permissions:** Adapter sets `localAudio`/`localVideo` `permission='ASKING'` → widget renders `WebexMediaAccess` (microphone first, then camera) → user allows/denies → permission resolves → meeting view renders. Outcome: permission prompt precedes the meeting. Evidence: `WebexMeetings.jsx:181-191`, `tests/WebexMeetings/WebexMeetings.test.jsx:104-149`. + - UI flow: only one prompt shows at a time; audio prompt takes priority over video. +- **UC-3 Keyboard-navigate the meeting controls:** User tabs into the widget → focus moves to the media container or Join button → arrow keys cycle control-bar buttons → Tab enters inner-meeting interactive elements. Outcome: keyboard-operable meeting controls. Evidence: `WebexMeetings.jsx:41-177`, `tests/WebexMeetings/WebexMeetings.test.jsx:235-487`. +- **UC-4 Customize controls / layout:** Host passes `controls` (function returning control IDs) and `layout` → forwarded to `WebexMeeting`. Outcome: tailored control set and remote-video layout. Evidence: `WebexMeetings.jsx:192-206`, `src/widgets/WebexMeetings/README.md`, `tests/WebexMeetings/WebexMeetings.test.jsx:158-178`. + +## State Machine +The meeting state is owned and emitted by the external adapter; the widget only reads `permission` fields +off it. The four `MeetingState` values come from `@webex/component-adapter-interfaces`; `@webex/components` +`WebexMeeting` renders `WebexWaitingForHost` for any state that is not `NOT_JOINED`/`JOINED`/`LEFT` +(catch-all), which is where `LOBBY` lands. This is documented here for orientation; it is not enforced in +this repo's code. + +```mermaid +stateDiagram-v2 + [*] --> NOT_JOINED: SDK + adapter ready, meeting created + NOT_JOINED --> JOINED: user joins (JoinControl) + NOT_JOINED --> LOBBY: joins but waiting for host admission + LOBBY --> JOINED: host admits / starts meeting + JOINED --> LEFT: user leaves (ExitControl) + LEFT --> [*]: widget unmounts +``` + +## UI Flow +- **Permission prompt (ASKING):** `WebexMediaAccess` modal for microphone, then camera. Only one shows at a time; audio precedes video (`WebexMeetings.jsx:188-191`). +- **Meeting view (resolved):** `WebexMeeting` renders the interstitial (pre-join, controls `mute-audio`, `mute-video`, `settings`, `join-meeting`), in-meeting (controls add `share-screen`, `member-roster`, `leave-meeting`), waiting-for-host, settings modal, and guest/host auth modals — all from `@webex/components`. +- **Layouts:** `Grid` (default), `Overlay`, `Stack`, `Prominent`, `Focus` via the `layout` prop (`src/widgets/WebexMeetings/README.md:47-93`). +- **Loading state:** while the adapter/meeting are not ready (falsy meeting state), `WebexMeeting` shows a loading view (external). +- **Accessibility:** root is focusable (`tabIndex=0`); custom shim routes focus to the media container or Join button and supports left/right arrow navigation across control buttons (`WebexMeetings.jsx:41-177`). +- **Sizing:** the widget is fluid with no default size; hosts must set `width`/`height` via `style`/`className` (`src/widgets/WebexMeetings/README.md:26-32`). + +## Pitfalls +- **Duplicate SDK/meeting initialization is NOT fixable in the widget class.** `new Webex()`, `new WebexSDKAdapter()`, `adapter.connect()`, and `createMeeting(destination)` all happen in the `withAdapter`/`withMeeting` HOCs from `@webex/components`, not in this class's lifecycle (`WebexMeetings.jsx:259-278`). Strict-mode double-mount or a changing `accessToken`/`meetingDestination`/`key` prop re-runs the factory. Chase these in the HOC layer, not here. +- **The accessibility shim reaches into `@webex/components` DOM via hard-coded class selectors** (`.wxc-meeting-control-bar__controls`, `.wxc-in-meeting__media-container`, etc.) and a `button[aria-label="Join meeting"]` lookup (`WebexMeetings.jsx:51,104,146`). An upstream class/label rename silently breaks focus navigation. It is explicitly marked temporary, to be removed once upstream `WebexMeeting` supports these features (`WebexMeetings.jsx:36-39`). +- **`render()` dereferences `meeting.localAudio?.permission` but assumes `meeting` is non-null.** A null `meeting` prop throws (caught only by a host ErrorBoundary) — see the error-handling test (`tests/WebexMeetings/WebexMeetings.test.jsx:221-233`). +- **`SettingsControl` display state never toggles** — a known `@webex/sdk-component-adapter` inconsistency: `display()` reads `showSettings` but `toggleSettings()` writes `settings.visible`. Cosmetic only; the modal still opens/closes. (External; archived ARCHITECTURE.md.) +- **Browser limitations (external SDK):** microphone switching is disabled in Firefox (single active mic limit); screen share requires `getDisplayMedia` (HTTPS, no mobile/IE); iOS 15.1 refreshes the page on join with camera permission granted due to missing video codecs (`src/widgets/WebexMeetings/README.md:139-156`). +- **Pinned dependency versions:** `webex`, `@webex/components`, and `@webex/sdk-component-adapter` are pinned exactly (no caret) in `package.json`. Bumping one without the others can break the adapter/SDK contract. + +## Module Do's / Don'ts +- DO: keep SDK construction inside the `adapterFactory` argument of `withAdapter` (`WebexMeetings.jsx:259`); the class must stay free of SDK lifecycle. +- DO: read meeting data only off the injected `meeting` prop and through `@webex/components` hooks — never reach into the SDK directly. +- DON'T: assume this widget shares anything with the Contact Center widgets — no `@webex/cc-store`, no MobX, no TypeScript, no r2wc custom element, no CC dependency flow. +- DON'T: add behavior to the accessibility shim without expecting to delete it; it is a temporary workaround over upstream DOM. +- DON'T: hardcode the `webex`/`components`/`adapter` versions anywhere but `package.json`. + +## Export Stability +The package ships an ESM bundle (`main`/`module` → `dist/webexWidgets.mjs`) whose sole export is +`WebexMeetingsWidget` (`src/index.js:1`). The breaking surface is: the export name, the two required props +(`accessToken`, `meetingDestination`), and the type/semantics of all declared props +(`WebexMeetings.jsx:231-255`). Adding an optional prop or a new export is additive (minor); renaming or +removing the export, or changing a required prop's type/requiredness, is breaking (major). There is no +emitted TypeScript declaration surface (JS + PropTypes only) — consumers rely on PropTypes runtime checks +and the README. Releases are cut by `semantic-release` from conventional commits. + +## Host Integration & Theming +- Mounts as a plain React component (``); unlike the CC widgets it is NOT registered as an r2wc custom element. +- Peer requirements: `react`/`react-dom` `18.3.1`, `prop-types` `^15.7.2`, and `webex` `2.60.4` must be provided by the host at the pinned versions (`package.json` peerDependencies). +- Styling: the widget imports `@webex/components/dist/css/webex-components.css` and its own `WebexMeetings.css` (`WebexMeetings.jsx:8-9`). Hosts size it via `style`/`className` on the root (`webex-meetings-widget`); it is fluid with no intrinsic size. +- The root element is given `tabIndex=0` and a `WebexLogo` SVG is injected into `WebexMeeting`. +- FedRAMP: pass `fedramp={true}` to route the SDK to a FedRAMP-compliant environment (`WebexMeetings.jsx:267`). + +## Test-Case Strategy (module) +Unit tests (`tests/WebexMeetings/WebexMeetings.test.jsx`) mock `@webex/components`, `webex`, and +`@webex/sdk-component-adapter`, then capture the adapter factory passed to `withAdapter`. They assert both +positive and negative cases: render branching (media-access vs meeting, audio-over-video priority, the +negative "meeting not rendered when ASKING"), prop forwarding, defaults, a null-`meeting` error path (via a +test ErrorBoundary), the accessibility shim (focus routing, arrow navigation, Tab handling, MutationObserver +re-attach), unmount cleanup, and the SDK factory config (token, fedramp, experimental flags, dev appName, +adapter construction). E2E (`tests/WebexMeeting.e2e.js`, WebdriverIO) drives the demo against a real +test user/room. Edge cases owned downstream (join/password/leave/device-switch, LOBBY transitions) are +exercised by `@webex/components`/`@webex/sdk-component-adapter`, not here. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `MEETINGS-WIDGETS-R-001` | `WebexMeetings.test.jsx:28-44` | none | +| `MEETINGS-WIDGETS-R-002` | `WebexMeetings.test.jsx:516-570` | none | +| `MEETINGS-WIDGETS-R-003` | `WebexMeetings.test.jsx:527-562` | none | +| `MEETINGS-WIDGETS-R-004` | `WebexMeetings.test.jsx:572-580` | No test for the production (`webex-widgets-meetings`) appName branch | +| `MEETINGS-WIDGETS-R-005` | `WebexMeetings.test.jsx:104-135` | none | +| `MEETINGS-WIDGETS-R-006` | `WebexMeetings.test.jsx:115-124,144-149` | none | +| `MEETINGS-WIDGETS-R-007` | `WebexMeetings.test.jsx:151-178` | none | +| `MEETINGS-WIDGETS-R-008` | `WebexMeetings.test.jsx:96-102,180-206` | none | +| `MEETINGS-WIDGETS-R-009` | `WebexMeetings.test.jsx:195-219` | none | +| `MEETINGS-WIDGETS-R-010` | `WebexMeetings.test.jsx:235-513` | none | + +## Traceability +- Repo architecture: [`../../../../ai-docs/ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`../../../../ai-docs/SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) +- Contracts catalog (CC family; this module not yet listed — see Source Material Register): [`../../../../ai-docs/CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/cc-components/ai-docs/cc-components-spec.md b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md new file mode 100644 index 000000000..41da6e734 --- /dev/null +++ b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md @@ -0,0 +1,319 @@ +# cc-components — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, UI, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `cc-components` | +| Source path(s) | `packages/contact-center/cc-components/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `[NEEDS HUMAN INPUT]` / updated_at `2026-06-29` | +| Validation status | not-run | + +## Evidence Rules +Every generated requirement below cites concrete source evidence using `file path`. Source evidence, test evidence, examples, assumptions, and gaps are kept separate so validators and future agents can distinguish truth from context. Test evidence is preferred for WHY. This repository's tests are the authoritative behavior record; commit history is not cited here. Where evidence is missing it is recorded as a gap rather than asserted. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/AGENTS.md` | overview / API / examples | migrated / reconciled | Orientation → Overview/Purpose/Stack; component table → Public Surface; examples → Use Cases. Reconciled: archived table listed 7 components; code exports 11 (added Campaign* and RealTimeTranscript). | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md` | architecture / component table | migrated / reconciled | Component table, file structure, patterns, diagrams → Design Overview, Data Flow, Class/Component Relationships, Folder structure, Pitfalls. Conflict: archived doc imports `@momentum-design/components` and `@momentum-ui/react-collaboration` interchangeably; code uses both (see Stack). | +| `packages/contact-center/cc-components/src/` and `tests/` | source / tests | reference-only (ground truth) | All requirements, props, and component inventory grounded against live source and `tests/components/`. | + +## Overview +`cc-components` is the presentation layer for Webex Contact Center widgets. It is a library of pure, presentational React function components — each receives all data and callbacks via props and renders Momentum UI primitives. Components do not import or read the MobX store, do not call the SDK, and hold only transient local UI state (open/closed menus, selected dropdown values, input text). Business logic and store/SDK access live one layer up, in the widget packages (`station-login`, `user-state`, `task`) that compose these components. + +The package exports two surfaces from `src/index.ts` (React components + their prop types) and `src/wc.ts` (the same components wrapped as custom elements via `@r2wc/react-to-web-component`). A maintainer should start at `src/index.ts` to see the public component set, then open the component directory under `src/components/` (each has `*.tsx`, a `*.types.ts` or shared `task.types.ts`, a `*.utils.ts(x)` for extracted logic, and a `*.scss`). Shared, cross-component logic lives in `src/utils/index.ts` (`formatTime`, `getMediaTypeInfo`) and `src/hooks/` (`useIntersectionObserver`). + +The component set spans the contact-center agent surface: station login, agent state, the task lifecycle (incoming task, call control with consult/transfer, CAD-enabled call control, task list, outdial), live transcript, and campaign-preview dialing (countdown, error dialog, campaign task card/popover/list-item). + +## Purpose / Responsibility +Owns the presentational React UI primitives for contact-center widgets: render agent/task UI from props and emit user intent back through callback props. Does NOT own state management, SDK access, business logic, or web-component registration into the host (the actual custom-element registration into the host app is `cc-widgets`' responsibility; `wc.ts` here only defines `component-cc-*` elements for the library build). + +## Stack +TypeScript 5.6, React 18 (peer `react`/`react-dom` `>=18.3.1`), function components with hooks. UI primitives from `@momentum-ui/react-collaboration` (peer `>=26.197.0`) and `@momentum-design/components/dist/react`. Web-component wrapping via `@r2wc/react-to-web-component` `2.0.3`. Metrics via `@webex/cc-ui-logging` (`withMetrics` HOC). Types consumed from `@webex/cc-store`. Test stack: Jest 29 + React Testing Library 16 + `@testing-library/jest-dom`, jsdom environment, snapshot tests alongside behavioral tests. Build: `tsc` for the type build, Webpack 5 for the bundle. Test command: `yarn workspace @webex/cc-components test:unit`. + +## Folder / Package Structure +``` +packages/contact-center/cc-components/src/ +├── index.ts # React component + type barrel (public surface) +├── wc.ts # Custom-element (r2wc) wrappers — defines component-cc-* elements +├── hooks/ +│ ├── index.ts # Hook barrel +│ └── useIntersectionObserver.ts # Infinite-scroll / lazy-load observer hook +├── utils/ +│ └── index.ts # Shared utils: formatTime, getMediaTypeInfo +└── components/ + ├── StationLogin/ # Agent login / device + team selection UI + ├── UserState/ # Agent state dropdown, idle codes, state timer + └── task/ + ├── constants.ts # Shared task UI label/string constants + ├── task.types.ts # Shared task prop/types + component prop Picks + ├── Task/ # Generic task row (shared by IncomingTask & TaskList) + ├── IncomingTask/ # Incoming task notification (Answer/Decline) + ├── TaskList/ # Active/incoming task list + ├── CallControl/ # Call control buttons (hold/mute/record/end/wrapup) + │ └── CallControlCustom/ # Consult/Transfer popover, list items, dial-number, consult bar + ├── CallControlCAD/ # CAD-enabled call control header (customer/queue/CAD vars) + ├── OutdialCall/ # Outbound dialpad + ANI/address-book selection + ├── RealTimeTranscript/ # Live transcript renderer + ├── AutoWrapupTimer/ # Auto-wrapup countdown bar + ├── TaskTimer/ # Generic elapsed/urgency timer + ├── GlobalVariablesPanel/ # Agent-viewable CAD global variables panel + ├── CampaignCountdown/ # Campaign preview offer countdown + ├── CampaignErrorDialog/ # Campaign action-failure dialog + └── CampaignTask/ # Campaign preview card + ├── CampaignTaskListItem/ # Shared campaign row (avatar/title/countdown/actions) + └── CampaignTaskPopover/ # Hover popover with campaign global variables +tests/components/ # Mirrors src; behavioral + *.snapshot tests + __snapshots__ +tests/hooks/ # Hook tests +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `src/index.ts` | The public React component set and re-exported type barrels — authoritative export list. | +| `src/wc.ts` | Custom-element tag names (`component-cc-*`) and the r2wc prop type maps per component. | +| `src/components/task/task.types.ts` | Shared task prop interfaces and the `Pick`-derived component prop types (`CallControlComponentProps`, `IncomingTaskComponentProps`, `TaskListComponentProps`, `OutdialCallComponentProps`, `RealTimeTranscriptComponentProps`), plus `MEDIA_CHANNEL`, `TaskState`, `ControlVisibility`, campaign types. | +| `src/components/StationLogin/station-login.types.ts` | `IStationLoginProps` and the `StationLoginComponentProps` Pick. | +| `src/components/UserState/user-state.types.ts` | `IUserState`, `UserStateComponentsProps` Pick, `AgentUserState` enum. | +| `src/components/StationLogin/constants.ts` | Login labels/error strings — re-exported from the barrel; never hardcode these elsewhere. | +| `src/components/task/constants.ts` | Task UI label/string constants (e.g. `CAMPAIGN_CALL`, `WRAP_UP`). | +| `src/utils/index.ts` | `formatTime` (timer formatting) and `getMediaTypeInfo` (media icon/label mapping). | + +## Public Surface +Consumed as an imported SDK/code API. The React barrel (`src/index.ts`) is the primary surface; `src/wc.ts` exposes the same components as custom elements for the library build. Exact prop schemas live in the `*.types.ts` files (linked below); the root contract index (`CONTRACTS.md`) documents how the consuming `cc-widgets`/widget layer re-exposes these as custom elements. + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `cc-components.StationLoginComponent` | SDK | `StationLoginComponent` (`StationLoginComponentProps`) | Agent login: device/team selection, login/logout, multiple-login alert, profile mode | semver; props are `Pick`ed — adding optional props = minor, removing/renaming a picked prop = major | `src/components/StationLogin/station-login.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.UserStateComponent` | SDK | `UserStateComponent` (`UserStateComponentsProps`) | Agent state dropdown + idle codes + state timer | semver as above | `src/components/UserState/user-state.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CallControlComponent` | SDK | `CallControlComponent` (`CallControlComponentProps`) | Call control buttons: hold/resume, mute, record, end, wrapup, consult/transfer/conference | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CallControlCADComponent` | SDK | `CallControlCADComponent` (`CallControlComponentProps`) | Call control with customer/queue header and agent-viewable CAD global variables | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.IncomingTaskComponent` | SDK | `IncomingTaskComponent` (`IncomingTaskComponentProps`) | Incoming task notification with Answer/Decline | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.TaskListComponent` | SDK | `TaskListComponent` (`TaskListComponentProps`) | Active + incoming task list; renders campaign preview when enabled | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.OutdialCallComponent` | SDK | `OutdialCallComponent` (`OutdialCallComponentProps`) | Outbound dialpad, ANI selection, address-book search | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.RealTimeTranscriptComponent` | SDK | `RealTimeTranscriptComponent` (`RealTimeTranscriptComponentProps`) | Renders sorted live transcript entries; empty state when none | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignCountdownComponent` | SDK | `CampaignCountdownComponent` (`CampaignCountdownProps`) | Campaign preview offer countdown; fires `onTimeout` at zero | semver as above | `src/components/task/CampaignCountdown/campaign-countdown.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignErrorDialogComponent` | SDK | `CampaignErrorDialogComponent` (`CampaignErrorDialogProps`) | Modal shown when a campaign action (accept/skip/remove/cancel) fails | semver as above | `src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignTaskComponent` | SDK | `CampaignTaskComponent` (`CampaignTaskProps`) | Campaign preview card: accept/skip/remove, countdown, error dialog | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.wc` | SDK | `@webex/cc-components/wc` → custom elements `component-cc-user-state`, `component-cc-station-login`, `component-cc-call-control`, `component-cc-call-control-cad`, `component-cc-incoming-task`, `component-cc-task-list`, `component-cc-out-dial-call`, `component-cc-realtime-transcript` | Custom-element build of the components | tag names are breaking surface; r2wc prop type map is part of the contract | `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | + +Compatibility notes: +- Component props are derived with `Pick<...>` over a larger interface (e.g. `IStationLoginProps`, `ControlProps`); only the picked keys are public. Adding an optional picked prop is additive (minor); removing/renaming a picked prop, or narrowing a prop's type, is breaking (major). +- Custom-element tag names in `wc.ts` (`component-cc-*`) and their r2wc prop type maps are a breaking surface — renaming a tag or changing a prop's r2wc type (`json`/`string`/`function`/...) breaks host consumers. +- `wc.ts` aliases `CallControlCADComponent` to `../CallControl/call-control` (imports `CallControlComponent` under the `CallControlCADComponent` name); the distinct CAD component is `src/components/task/CallControlCAD/call-control-cad.tsx`. See Pitfalls. + +## Requires (dependencies) +- `@webex/cc-store` (workspace:\*) — type-only import surface here (`ITask`, `ILogger`, `IContactCenter`, `IdleCode`, `IWrapupCode`, `BuddyDetails`, etc.) and constants such as `ERROR_TRIGGERING_IDLE_CODES`, `LoginOptions`, `DESKTOP`. Components consume types/constants, not the store singleton. +- `@webex/cc-ui-logging` (workspace:\*) — `withMetrics` HOC wrapping each top-level component for mount/metrics tracking. +- `@momentum-ui/react-collaboration` (peer `>=26.197.0`) and `@momentum-design/components/dist/react` — UI primitives. +- `@r2wc/react-to-web-component` `2.0.3` — custom-element wrapping in `wc.ts`. +- `@momentum-ui/illustrations` `^1.24.0` — illustration assets. +- `react` / `react-dom` (peer `>=18.3.1`) — provided by the host. +- `@webex/test-fixtures` (workspace:\*, dev) — shared test mocks. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `CC-COMPONENTS-R-001` | Components are pure presentational: they receive data + callbacks via props and never read the MobX store or call the SDK directly. | Keeps presentation decoupled from state/SDK so components are testable in isolation and reusable across widgets. | `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx` (props-only; no `@webex/cc-store` singleton import) | `tests/components/StationLogin/station-login.tsx` (renders from props, mocks callbacks) | None | PRESENT | +| `CC-COMPONENTS-R-002` | The public React surface is exactly the 11 components exported from `index.ts` plus the re-exported type barrels. | Defines the supported import surface; consumers must not import internal subcomponents. | `src/index.ts` | `tests/components/StationLogin/station-login.tsx`, `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` (import the exported components) | None | PRESENT | +| `CC-COMPONENTS-R-003` | `StationLoginComponent` invokes the supplied callbacks (`login`, `setDeviceType`, `setDialNumber`, `handleContinue`, `saveLoginOptions`) on the matching user action and surfaces `loginFailure`/`saveError` as error UI. | Login intent and errors must propagate to the widget layer without the component owning login logic. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`calls login function...`, `renders login failure when passed`, `renders save error when passed`) | None | PRESENT | +| `CC-COMPONENTS-R-004` | `StationLoginComponent` hides the Desktop login option when `hideDesktopLogin` is true (in both login and profile mode) and shows it when false/undefined. | Deployments can disable Desktop login; must be honored consistently across modes. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`hides Desktop login option when hideDesktopLogin is true`, `... when false`, `... when undefined`, `... in profile mode`) | None | PRESENT | +| `CC-COMPONENTS-R-005` | `UserStateComponent` renders idle codes sorted/built into the dropdown, reflects `currentState`/`elapsedTime`, and calls `setAgentStatus(auxCodeId)` on selection; error-triggering idle codes are styled distinctly. | Agent must change state and see correct current state + timing; error idle codes need visual emphasis. | `src/components/UserState/user-state.tsx`, `src/components/UserState/user-state.utils.ts` (`buildDropdownItems`, `sortDropdownItems`, `handleSelectionChange`, `getDropdownClass`) | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-006` | `CallControlComponent` builds its button set from `controlVisibility` and current task, and routes button presses to the matching callback (`toggleHold`, `toggleMute`, `toggleRecording`, `endCall`, `wrapupCall`, consult/transfer/conference handlers); wrapup requires selecting a reason. | Call control must reflect the allowed actions for the current interaction state and emit the right intent. | `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/call-control.utils.ts` (`buildCallControlButtons`, `filterButtonsForConsultation`, `handleWrapupCall`) | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-007` | `CallControlCADComponent` renders the customer/queue/caller header and an agent-viewable CAD global variables panel, and renders the campaign call icon + "Campaign call" label when `isCampaignCall` is true. | CAD/header info and campaign branding must be visible to the agent during a call. | `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/task/Task/task.utils.ts` (`getAgentViewableGlobalVariables`) | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-008` | `IncomingTaskComponent` renders the standard `Task` with Answer/Decline when an `incomingTask` is present and renders nothing (hidden) when it is absent; Accept/Decline invoke `accept(task)`/`reject(task)`. | Avoids a stray empty notification when no task; routes accept/decline intent up. | `src/components/task/IncomingTask/incoming-task.tsx`, `src/components/task/IncomingTask/incoming-task.utils.tsx` (`extractIncomingTaskData`) | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-009` | `TaskListComponent` renders nothing when the task list is empty, otherwise renders one row per task; campaign preview tasks render `CampaignTask` (instead of `Task`) only when `hasCampaignPreviewEnabled` (default true) and the task is a campaign preview. | List must collapse when empty and switch row UI for campaign previews per the feature flag. | `src/components/task/TaskList/task-list.tsx`, `src/components/task/TaskList/task-list.utils.ts` (`isTaskListEmpty`, `getTasksArray`, `isCampaignPreviewTask`, `getActiveCampaignPreviewId`) | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-010` | `OutdialCallComponent` validates the entered destination, supports dialpad / ANI / address-book tabs, disables the outdial action while a telephony task is active, and calls `startOutdial(destination, origin?)`. | Outbound dialing must validate input and not start a second call over an active one. | `src/components/task/OutdialCall/outdial-call.tsx`, `src/components/task/OutdialCall/constants.ts` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-011` | `RealTimeTranscriptComponent` sorts `liveTranscriptEntries` by ascending `timestamp` before rendering and shows an empty-state message when there are no entries. | Transcript must read chronologically and degrade gracefully when empty. | `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-012` | Campaign preview UI: `CampaignTaskComponent` renders accept/skip/remove + countdown and triggers the configured auto-action on timeout; failed actions open `CampaignErrorDialogComponent` with the mapped `CampaignErrorType`; `CampaignCountdownComponent` fires `onTimeout` at zero. | Campaign preview offers are time-boxed; failures and timeouts must be surfaced and auto-handled. | `src/components/task/CampaignTask/campaign-task.tsx`, `src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx` (+ `.types.ts` `CAMPAIGN_ACTION_ERROR_MAP`, `ERROR_TITLES`), `src/components/task/CampaignCountdown/campaign-countdown.tsx` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-013` | `formatTime` renders `HH:MM:SS` for durations ≥ 1 hour and `MM:SS` otherwise, with zero-padding; `getMediaTypeInfo` maps media type/channel to icon/label/className/brand-visual, falling back to telephony/chat defaults. | Timers and media badges must format consistently across all task components. | `src/utils/index.ts` | `tests/components/task/CallControl/call-control.utils.tsx`, snapshot tests under `tests/components/task/**/__snapshots__/` exercise formatted output | No dedicated `tests/utils/` file found for `formatTime`/`getMediaTypeInfo` (exercised indirectly via component/utils tests) | WEAK | +| `CC-COMPONENTS-R-014` | `useIntersectionObserver` reports element visibility for infinite-scroll/lazy paths (e.g. outdial address-book paging). | Paged lists must load more on scroll without per-component observer wiring. | `src/hooks/useIntersectionObserver.ts` | `tests/hooks/useIntersectionObserver.test.ts` | None | PRESENT | +| `CC-COMPONENTS-R-015` | Each top-level exported component is wrapped with the `withMetrics` HOC so mount/usage metrics are tracked uniformly. | Consistent telemetry across all widgets without per-component instrumentation. | `withMetrics` import + wrap in `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx`, `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | Covered indirectly by each component's render test | No test asserts the HOC wrapping itself | WEAK | + +## Design Overview +Every component follows the same shape: a typed function component destructures props, derives display data through pure helpers in a co-located `*.utils.ts(x)`, renders Momentum primitives, and calls back through callback props on user interaction. Local `useState` holds only transient UI (open menus, selected-but-not-yet-submitted values, input text) — never domain state. Top-level components are wrapped in `withMetrics`. This keeps each component unit-testable with plain props and jest mocks and is the reason the archived "presentational pattern" guidance still holds. + +Logic that is non-trivial or shared is pulled out of the JSX: per-component utils (`station-login.utils.tsx`, `call-control.utils.ts`, `task-list.utils.ts`, etc.) and library-wide utils (`src/utils/index.ts`: `formatTime`, `getMediaTypeInfo`). `task.types.ts` is the shared type hub for the task family — the larger `ControlProps`/`TaskProps` interfaces describe the full data set, and each component's public prop type is a `Pick` of the keys it actually uses, which is why the public surface is intentionally narrower than the interfaces. + +Composition is deliberate: `IncomingTaskComponent` and `TaskListComponent` both render the generic `Task` row; `CallControlCADComponent` wraps `CallControlComponent` and adds a CAD header + `GlobalVariablesPanel`; `CallControlComponent` embeds the consult/transfer popover (`CallControlCustom/`) and `AutoWrapupTimer`; `CampaignTask` composes `CampaignTaskListItem`, `CampaignTaskPopover`, `CampaignCountdown`, and `CampaignErrorDialog`. The `wc.ts` module is a thin adapter that re-exposes the same components as `component-cc-*` custom elements with explicit r2wc prop type maps; actual registration into a host app happens in `cc-widgets`. + +## Data Flow +In-process React props/callbacks only — there is no network, queue, or socket transport in this module. Data flows down as props (sourced by the widget layer from the store/SDK), is transformed by pure utils into render data, rendered via Momentum primitives, and user interaction flows back up by invoking callback props. + +```mermaid +flowchart LR + Widget["Widget layer
(store/SDK-backed props + callbacks)"] + subgraph cc-components + direction LR + Comp["Presentational component
(*.tsx)"] + Utils["Pure helpers
(*.utils.ts / src/utils)"] + MUI["Momentum UI primitives"] + end + Widget -->|props: data| Comp + Comp -->|raw props| Utils + Utils -->|render data| Comp + Comp -->|renders| MUI + MUI -->|DOM events| Comp + Comp -->|callback props: user intent| Widget +``` + +## Sequence Diagram(s) +These components share one interaction pattern (props in → local UI state → callback out); they differ only in which callbacks fire. One representative sequence plus its failure branch covers the module; the campaign timeout/error path is the one non-trivial async branch and is included. + +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| User-action components (login, state change, call control, accept/decline, outdial) | "Props-in / callback-out interaction" | Error props (`loginFailure`, `saveError`) rendered as error UI; no internal retry — recovery owned by widget layer | +| Campaign preview offer (countdown + action) | "Campaign preview timeout & error" | Countdown timeout auto-action; failed action opens error dialog (alt branch) | + +```mermaid +sequenceDiagram + participant User + participant Comp as Presentational Component + participant Utils as Pure Utils + participant Widget as Widget layer (callbacks) + Widget->>Comp: render(props: data + callbacks) + Comp->>Utils: derive render data (e.g. buildDropdownItems / buildCallControlButtons) + Utils-->>Comp: render data + User->>Comp: interaction (click / select / type) + Comp->>Comp: update local UI state (useState) + Comp->>Widget: invoke callback prop (e.g. setAgentStatus / toggleHold / accept) + alt error prop supplied (loginFailure / saveError) + Widget-->>Comp: re-render with error prop + Comp->>User: render error UI + end +``` + +```mermaid +sequenceDiagram + participant User + participant CT as CampaignTaskComponent + participant CD as CampaignCountdown + participant Dlg as CampaignErrorDialog + participant Widget as Widget callbacks + CT->>CD: render with timeoutTimestamp + onTimeout + alt agent acts before timeout + User->>CT: click Accept / Skip / Remove + CT->>Widget: acceptPreviewContact() / skipPreviewContact() / removePreviewContact() + alt action rejects + Widget-->>CT: promise rejects + CT->>Dlg: open with mapped CampaignErrorType (CAMPAIGN_ACTION_ERROR_MAP) + Dlg->>User: show error title + message + end + else countdown reaches zero + CD->>CT: onTimeout() + CT->>Widget: trigger configured auto-action (ACCEPT/SKIP/REMOVE) + end +``` + +## Class / Component Relationships +```mermaid +graph TD + Index["index.ts (public barrel)"] + Index --> StationLogin[StationLoginComponent] + Index --> UserState[UserStateComponent] + Index --> CallControl[CallControlComponent] + Index --> CallControlCAD[CallControlCADComponent] + Index --> IncomingTask[IncomingTaskComponent] + Index --> TaskList[TaskListComponent] + Index --> Outdial[OutdialCallComponent] + Index --> RTT[RealTimeTranscriptComponent] + Index --> CampCountdown[CampaignCountdownComponent] + Index --> CampError[CampaignErrorDialogComponent] + Index --> CampTask[CampaignTaskComponent] + + IncomingTask --> Task[Task row] + TaskList --> Task + TaskList --> CampTask + CallControlCAD --> CallControl + CallControlCAD --> GVP[GlobalVariablesPanel] + CallControlCAD --> TaskTimer + CallControl --> ConsultPopover[ConsultTransferPopover + CallControlCustom] + CallControl --> AutoWrap[AutoWrapupTimer] + CampTask --> CampListItem[CampaignTaskListItem] + CampTask --> CampPopover[CampaignTaskPopover] + CampTask --> CampCountdown + CampTask --> CampError + CampListItem --> CampCountdown +``` +The exported components are leaves of `index.ts`. Composition is one-directional: `Task` is the shared row reused by `IncomingTask` and `TaskList`; `CallControlCAD` decorates `CallControl` with a CAD header, `GlobalVariablesPanel`, and `TaskTimer`; `CallControl` owns the consult/transfer subtree under `CallControlCustom/` plus `AutoWrapupTimer`; the campaign family composes `CampaignTaskListItem`, `CampaignTaskPopover`, `CampaignCountdown`, and `CampaignErrorDialog`. Prop types are unified in `task.types.ts` via `Pick` over `ControlProps`/`TaskProps`. + +## Use Cases +- **UC-1 Agent logs in:** Widget passes `teams`, `loginOptions`, `deviceType`, and handlers → `StationLoginComponent` renders selectors → agent selects device/team, optionally enters DN → clicks Continue/Save → component calls `login`/`saveLoginOptions`; `loginFailure`/`saveError` props render error UI. Evidence: `src/components/StationLogin/station-login.tsx`, `tests/components/StationLogin/station-login.tsx`. +- **UC-2 Agent changes state:** `UserStateComponent` shows idle-code dropdown with current state + elapsed time → agent selects a code → `setAgentStatus(auxCodeId)` fires. Evidence: `src/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.tsx`. +- **UC-3 Agent controls an active call:** `CallControlComponent` builds buttons from `controlVisibility` → agent presses hold/mute/record/end/wrapup or opens consult/transfer popover → matching callback fires; wrapup requires a selected reason. Evidence: `src/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.tsx`. +- **UC-4 Agent answers/declines incoming task:** `IncomingTaskComponent` renders the `Task` row with Answer/Decline → `accept(task)`/`reject(task)` fire; renders nothing when no incoming task. Evidence: `src/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.tsx`. +- **UC-5 Agent views task list:** `TaskListComponent` renders a row per task (campaign preview rows use `CampaignTask` when enabled), collapsing to nothing when empty. Evidence: `src/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.tsx`. +- **UC-6 Agent places an outbound call:** `OutdialCallComponent` validates the destination, selects an ANI/address-book entry, and calls `startOutdial`; outdial disabled when a telephony task is active. Evidence: `src/components/task/OutdialCall/outdial-call.tsx`, `tests/components/task/OutdialCall/out-dial-call.tsx`. +- **UC-7 Agent handles a campaign preview offer:** `CampaignTaskComponent` shows accept/skip/remove + countdown → agent acts or countdown auto-acts; failures open `CampaignErrorDialog`. Evidence: `src/components/task/CampaignTask/campaign-task.tsx`, `tests/components/task/CampaignTask/campaign-task.test.tsx`. + +UI flow per use case is detailed in the UI Flow section below. + +## State Model +These components hold only transient, local UI state via React `useState` (e.g. open consult/transfer menu, selected-but-unsubmitted wrapup reason and id, mute-button disabled flag in `call-control.tsx`; selected tab, destination text, validation flag, selected ANI in `outdial-call.tsx`). They hold no domain/application state and never read or mutate the MobX store — all persistent state lives in `@webex/cc-store`, owned by the widget layer. Transitions are driven directly by user events and reset on prop changes/remount. No store slices, reducers, or actions are defined in this module (evidence: no MobX or store-singleton import in `src/components/**`). + +## UI Flow +These are UI components; the non-happy-path states are part of the contract. +- **StationLogin:** login screen vs. profile mode; device-type select drives dial-number input visibility; Desktop option hidden when `hideDesktopLogin`; multiple-login alert when `showMultipleLoginAlert`; error states from `loginFailure`/`saveError`; Save/Continue disabled until valid. (`src/components/StationLogin/station-login.tsx`) +- **UserState:** dropdown of idle/custom states with current state highlighted; `isSettingAgentStatus` shows a busy/disabled state; error-triggering idle codes styled distinctly; state timer renders via `formatTime`. (`src/components/UserState/user-state.tsx`) +- **CallControl / CallControlCAD:** button visibility/enablement driven by `controlVisibility`; consult/transfer popover with Agents/Queues/Dial Number/Entry Point tabs (empty-state when no results, loading spinner while fetching); auto-wrapup countdown bar; wrapup requires reason selection before submit. (`src/components/task/CallControl/`, `src/components/task/CallControlCAD/`) +- **IncomingTask / TaskList:** hidden (renders `<>`) when no incoming task / empty list; per-media-type icon and label; campaign preview rows swap UI. (`src/components/task/IncomingTask/`, `src/components/task/TaskList/`) +- **OutdialCall:** dialpad / contacts tabs; invalid-number state disables dial; address-book loading spinner and infinite scroll; disabled while a telephony task is active. (`src/components/task/OutdialCall/outdial-call.tsx`) +- **RealTimeTranscript:** empty-state message ("No live transcript available.") when no entries; entries sorted chronologically; event vs. message rendering. (`src/components/task/RealTimeTranscript/real-time-transcript.tsx`) +- **Campaign preview:** countdown bar (urgent styling near zero); accept "Connecting..." state; disabled accept/skip/remove flags; error dialog modal on failure. (`src/components/task/CampaignTask/`, `src/components/task/CampaignCountdown/`, `src/components/task/CampaignErrorDialog/`) + +## Host Integration & Theming +- Components render Momentum UI primitives (`@momentum-ui/react-collaboration`, `@momentum-design/components/dist/react`) and inherit Momentum theming tokens (e.g. CSS custom properties such as `--mds-color-theme-background-glass-normal` referenced in `task.types.ts` defaults). They assume the host provides Momentum theming/CSS; no `ThemeProvider` is mounted inside this package. +- React peer requirement: `react`/`react-dom` `>=18.3.1`, provided by the host. +- The `wc.ts` build registers custom elements `component-cc-user-state`, `component-cc-station-login`, `component-cc-call-control`, `component-cc-call-control-cad`, `component-cc-incoming-task`, `component-cc-task-list`, `component-cc-out-dial-call`, `component-cc-realtime-transcript`, each guarded by a `customElements.get(...)` check before `define`. The host-facing custom elements (`widget-cc-*`) and their registration are owned by `cc-widgets`; see [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md). + +## Pitfalls +- `wc.ts` imports `CallControlCADComponent` from `../CallControl/call-control` (i.e. it wraps the plain `CallControlComponent`, not the CAD component at `CallControlCAD/call-control-cad.tsx`). The `component-cc-call-control-cad` custom element therefore does NOT render the CAD header from `call-control-cad.tsx`. Confirm intended before relying on the custom-element CAD variant. Evidence: `src/wc.ts` import block. +- Public prop types are `Pick`s over larger interfaces (`IStationLoginProps`, `ControlProps`, `TaskProps`). A field can exist on the interface yet not be public — only keys in the `Pick` are part of the contract. Don't infer a prop is supported just because it's on the interface. +- Components do not own state: passing a new object/array/callback identity each render (instead of memoized) causes avoidable re-renders, especially for list components (`TaskList`, `OutdialCall`). Memoize props in the widget layer. +- `IncomingTaskComponent` and `TaskListComponent` return `<>` (render nothing) for the empty case rather than a placeholder — a "blank" component usually means the guarding prop (`incomingTask`/non-empty `taskList`) is missing, not a render bug. +- `getMediaTypeInfo` falls back to a telephony or chat default for unknown media types rather than throwing; a wrong icon/label usually means an unmapped `MEDIA_CHANNEL` value, not a render failure. Evidence: `src/utils/index.ts`. +- Campaign types (`CampaignCallProcessingDetails`) are bridge types for SDK fields not yet in the installed SDK typings; they can drift from the runtime payload until the SDK is updated. Evidence: `src/components/task/task.types.ts`. + +## Module Do's / Don'ts +- DO: keep components props-only — pass all data and callbacks in; never import the `@webex/cc-store` singleton or call the SDK here. +- DO: derive display data in co-located `*.utils.ts(x)` (and shared logic in `src/utils`) so components stay thin and unit-testable. +- DO: add new public components/types through `src/index.ts` and, for the custom-element build, register them in `src/wc.ts` with an explicit r2wc prop type map guarded by `customElements.get`. +- DON'T: hold domain state in component `useState` — only transient UI state. +- DON'T: hardcode label/error strings — use `StationLogin/constants.ts` and `task/constants.ts`. +- DON'T: widen a component's public surface by exporting internal subcomponents (e.g. `Task`, `CallControlCustom/*`) from the barrel. + +## Export Stability +This package is published (`@webex/cc-components`, `main` → `dist/index.js`, `types` → `dist/types/index.d.ts`, and a `./wc` subpath export). The `.d.ts` of the `index.ts` barrel and the `task.types.ts`/`*.types.ts` exports are the type surface. Semver sensitivity: adding an optional prop to a `Pick`ed component type or adding a new exported component is a minor; removing/renaming a picked prop, narrowing a prop type, removing an exported component, or renaming/removing a `component-cc-*` custom element (or changing its r2wc prop type) is a major. Evidence: `package.json` (`exports`, `version`), `src/index.ts`, `src/wc.ts`. + +## Test-Case Strategy (module) +Each component is tested in isolation with React Testing Library: render from a minimal props object, assert rendered UI (positive) and assert callbacks fire on interaction / error props render error UI (negative), with snapshot tests guarding stable markup. Utils have dedicated `*.utils.tsx` tests asserting pure transformations (e.g. button building, sorting, data extraction). Edge cases covered include empty task list/incoming task (hidden render), `hideDesktopLogin` across modes, campaign timeout/error, and transcript empty + sort. Gaps: no dedicated test file for `src/utils/index.ts` (`formatTime`/`getMediaTypeInfo`) and no explicit assertion that components are `withMetrics`-wrapped. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `CC-COMPONENTS-R-001` | `tests/components/StationLogin/station-login.tsx`, `tests/components/UserState/user-state.tsx`, `tests/components/task/CallControl/call-control.tsx` | None | +| `CC-COMPONENTS-R-002` | Import sites across `tests/components/**` | No single test asserting the full export list | +| `CC-COMPONENTS-R-003` | `tests/components/StationLogin/station-login.tsx` (actions + failure + save error) | None | +| `CC-COMPONENTS-R-004` | `tests/components/StationLogin/station-login.tsx` (hideDesktopLogin cases) | None | +| `CC-COMPONENTS-R-005` | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | +| `CC-COMPONENTS-R-006` | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | +| `CC-COMPONENTS-R-007` | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | +| `CC-COMPONENTS-R-008` | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | +| `CC-COMPONENTS-R-009` | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | +| `CC-COMPONENTS-R-010` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | +| `CC-COMPONENTS-R-011` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | +| `CC-COMPONENTS-R-012` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | +| `CC-COMPONENTS-R-013` | `tests/components/task/CallControl/call-control.utils.tsx`, component snapshots | No dedicated `formatTime`/`getMediaTypeInfo` unit test | +| `CC-COMPONENTS-R-014` | `tests/hooks/useIntersectionObserver.test.ts` | None | +| `CC-COMPONENTS-R-015` | None found (covered indirectly via render tests) | No explicit `withMetrics`-wrapping assertion | + +## Traceability +- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md b/packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md new file mode 100644 index 000000000..d62fd4057 --- /dev/null +++ b/packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md @@ -0,0 +1,360 @@ +# cc-widgets — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, state, protocol, UI, data, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `cc-widgets` | +| Source path(s) | `packages/contact-center/cc-widgets/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `[NEEDS HUMAN INPUT]` / updated_at `2026-06-29` | +| Validation status | not-run | + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source +evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish +truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the +repository policy says history is reliable, and must include the commit hash. If evidence is missing or +conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers +as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-widgets/ai-docs/AGENTS.md` | overview / API | migrated | Orientation → Overview/Purpose; React + Web Component export inventory → Public Surface; usage patterns → Use Cases; dual-bundle note → Export Stability / Host Integration. | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-widgets/ai-docs/ARCHITECTURE.md` | architecture | reconciled | r2wc registration flow → Data Flow / Sequence Diagram(s); widget→tag mapping → Public Surface (corrected against `src/wc.ts`); troubleshooting → Pitfalls. Archived `OutdialCall` was missing `RealTimeTranscript`, `DigitalChannels`, `CallControlCAD` tag and `hasCampaignPreviewEnabled` prop — current code is source of truth. | + +## Overview +`cc-widgets` is the aggregator and distribution surface for the Webex Contact Center widget suite. It +owns no widget UI or business logic of its own; instead it re-exports the React components produced by +the individual widget packages (`@webex/cc-station-login`, `@webex/cc-user-state`, `@webex/cc-task`, +`@webex/cc-digital-channels`) plus the shared MobX `store`, and it converts those same React components +into framework-agnostic custom elements via `@r2wc/react-to-web-component` (r2wc). + +The package has exactly two source files. `src/index.ts` is the React entry point (`main` / +`dist/index.js`): a re-export barrel that also imports Momentum UI base CSS so React consumers get +widget styling. `src/wc.ts` is the Web Component entry point (the `./wc` export / `dist/wc.js`): it wraps +each React widget with r2wc, declares the prop-type map per widget, and registers every wrapper as a +custom element (`widget-cc-*`) at module-load time. + +A maintainer should start at `src/index.ts` to change the React/store public surface and at `src/wc.ts` +to add a widget, change a custom-element tag, or change how a prop crosses the Web Component boundary. +Because this package is pure aggregation, behavior changes almost always belong upstream in the widget +packages; changes here are about what is exported, under what name, and with what prop typing. + +## Purpose / Responsibility +Owns the public distribution surface for the contact center widget suite: re-exports React widgets + the +shared `store`, and registers r2wc-wrapped custom elements. Does NOT own widget UI, business logic, store +state, or SDK access — those live upstream in the widget packages and `@webex/cc-store`. + +## Stack +TypeScript 5.6.3, React 18 (peer `>=18.3.1`), `@r2wc/react-to-web-component` 2.0.3. Test stack: Jest +29.7.0 + jsdom + React Testing Library (configured in `package.json`, but no tests currently exist). +Build: `tsc` for type declarations and webpack 5 for the two bundles (`dist/index.js`, `dist/wc.js`). +No datastore or messaging. + +## Folder / Package Structure +``` +packages/contact-center/cc-widgets/src/ +├── index.ts # React entry — re-export barrel for widgets + store; imports Momentum UI CSS +└── wc.ts # Web Component entry — r2wc wrappers + per-widget prop map + customElements registration +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `packages/contact-center/cc-widgets/src/index.ts` | Authoritative list of React exports and the Momentum CSS import. | +| `packages/contact-center/cc-widgets/src/wc.ts` | Authoritative custom-element tag names, the r2wc prop-type map per widget, and the registration loop. | +| `packages/contact-center/cc-widgets/package.json` | Export entry points (`.` → `dist/index.js`, `./wc` → `dist/wc.js`), version, dependency floors, and peer-dependency versions. | + +## Public Surface +Two consumption modes share one package: React named imports from `@webex/cc-widgets`, and custom +elements registered by importing `@webex/cc-widgets/wc`. Widget prop/event contracts are owned by the +upstream widget packages; this module only re-exports them and maps a subset across the Web Component +boundary (see `src/wc.ts`). + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `cc-widgets.StationLogin` | SDK | React export `StationLogin`; tag `widget-cc-station-login` (props `onLogin`, `onLogout`) | Agent station login UI | stable; removing/renaming export or tag = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.UserState` | SDK | React export `UserState`; tag `widget-cc-user-state` (prop `onStateChange`) | Agent state management UI | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.IncomingTask` | SDK | React export `IncomingTask`; tag `widget-cc-incoming-task` (props `incomingTask:json`, `onAccepted`, `onRejected`) | Incoming task notification UI | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.CallControl` | SDK | React export `CallControl`; tag `widget-cc-call-control` (props `onHoldResume`, `onEnd`, `onWrapUp`, `onRecordingToggle`) | Active-call control buttons | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.CallControlCAD` | SDK | React export `CallControlCAD`; tag `widget-cc-call-control-cad` (props `onHoldResume`, `onEnd`, `onWrapUp`, `onRecordingToggle`) | CAD-enabled call control | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.TaskList` | SDK | React export `TaskList`; tag `widget-cc-task-list` (props `onTaskAccepted`, `onTaskDeclined`, `onTaskSelected`, `hasCampaignPreviewEnabled:boolean`) | Active tasks list UI | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.OutdialCall` | SDK | React export `OutdialCall`; tag `widget-cc-outdial-call` (no mapped props; store-driven) | Outbound dialing UI | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.RealTimeTranscript` | SDK | React export `RealTimeTranscript`; tag `widget-cc-realtime-transcript` (props `liveTranscriptEntries:json`, `className:string`) | Live transcript UI | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.DigitalChannels` | SDK | React export `DigitalChannels`; tag `widget-cc-digital-channels` (no mapped props; store-driven) | Digital channels UI | stable; export/tag change = major | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.store` | SDK | React export `store`; also re-exported from `src/wc.ts` | Shared MobX singleton (`@webex/cc-store`) callers init before mounting widgets | stable; the single shared store instance | `src/index.ts`, `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | + +Compatibility notes: +- Adding a new React export or a new `widget-cc-*` tag is additive (minor). Renaming/removing an export or + tag, or renaming a mapped prop, is breaking (major). +- Web Component tags are registered guarded by `customElements.get(name)` — importing `./wc` twice is + safe and does not throw a redefinition error (`src/wc.ts`). + +## Requires (dependencies) +- Internal widget packages (workspace `*`): `@webex/cc-station-login`, `@webex/cc-user-state`, + `@webex/cc-task` (provides `IncomingTask`, `TaskList`, `CallControl`, `CallControlCAD`, `OutdialCall`, + `RealTimeTranscript`), `@webex/cc-digital-channels`. Source: `package.json` dependencies, `src/index.ts`, + `src/wc.ts`. +- `@webex/cc-store` (workspace `*`) — the shared MobX singleton re-exported to consumers. +- `@r2wc/react-to-web-component` `2.0.3` — React→custom-element conversion (`src/wc.ts`). +- Peer dependencies (host-provided): `react >=18.3.1`, `react-dom >=18.3.1`, + `@momentum-ui/react-collaboration >=26.197.0`, `@momentum-ui/web-components ^2.26.20` + (`package.json`). React/ReactDOM are peers so the host provides a single React instance. +- `@momentum-ui/core/css/momentum-ui.min.css` — base CSS imported in `src/index.ts` for React consumers. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `cc-widgets-R-001` | Re-export the React widgets `StationLogin`, `UserState`, `IncomingTask`, `CallControl`, `CallControlCAD`, `TaskList`, `OutdialCall`, `RealTimeTranscript`, `DigitalChannels`, and `store` from the package root. | Single-package install: consumers import the whole suite + store from `@webex/cc-widgets` without tracking each widget package. | `packages/contact-center/cc-widgets/src/index.ts` | None found | No unit test exists (`tests/` absent; `passWithNoTests` set). | WEAK | +| `cc-widgets-R-002` | Register each widget as a custom element under a `widget-cc-*` tag (`widget-cc-user-state`, `widget-cc-station-login`, `widget-cc-incoming-task`, `widget-cc-task-list`, `widget-cc-call-control`, `widget-cc-outdial-call`, `widget-cc-call-control-cad`, `widget-cc-realtime-transcript`, `widget-cc-digital-channels`) when `./wc` is loaded. | Framework-agnostic embedding: HTML/Angular/Vue/vanilla hosts use the suite without React. | `packages/contact-center/cc-widgets/src/wc.ts` | None found | No test verifies registration. | WEAK | +| `cc-widgets-R-003` | Guard each `customElements.define` with `customElements.get(name)` so re-importing the WC bundle does not throw a duplicate-definition error. | Importing the bundle more than once (multiple micro-frontends/scripts) must be idempotent. | `packages/contact-center/cc-widgets/src/wc.ts` | None found | No regression test for double-import. | WEAK | +| `cc-widgets-R-004` | Map complex/callback props across the WC boundary with explicit r2wc prop types: `function` callbacks, `json` for object props (`incomingTask`, `liveTranscriptEntries`), `boolean` (`hasCampaignPreviewEnabled`), and `string` (`className`). | HTML attributes are strings only; functions and objects must be set as element properties with the right r2wc coercion or the widget won't receive them. | `packages/contact-center/cc-widgets/src/wc.ts` | None found | Prop maps are the WC public contract; no test asserts the type map. | WEAK | +| `cc-widgets-R-005` | Import Momentum UI base CSS in the React entry so React consumers get widget styling without a separate import. | Avoids unstyled widgets in React hosts (a documented prior support issue). | `packages/contact-center/cc-widgets/src/index.ts` | None found | CSS side-effect import is untested. | WEAK | +| `cc-widgets-R-006` | Treat React/ReactDOM as peer dependencies (`>=18.3.1`) rather than bundled runtime deps for the React export. | A single host React instance prevents "Invalid hook call" / duplicate-React failures. | `packages/contact-center/cc-widgets/package.json` | None found | Peer-dep enforcement is by package manager, not tested here. | WEAK | + +## Design Overview +The module is a pure composition/distribution layer with two entry points and no internal state. The React +path (`index.ts`) is a tree-shakeable re-export barrel: the host bundler pulls actual widget code from the +workspace packages, so the React bundle is small and uses the host's React instance. The only side effect +is the Momentum CSS import. + +The Web Component path (`wc.ts`) is self-contained. Each React widget is passed to `r2wc(Component, {props})` +to produce a custom-element class. The `props` map tells r2wc how to bridge each prop: `function` props are +assigned as element properties (callbacks/events), `json` props are parsed from attribute/property values +into objects, and `string`/`boolean` props coerce primitives. Widgets that read everything from the shared +store (`OutdialCall`, `DigitalChannels`) are wrapped with an empty prop map. A single `components` array +pairs each tag name with its wrapper, and a `forEach` registers them all, guarding with +`customElements.get` so registration is idempotent across repeated imports. Registration runs as an +import-time side effect — loading the module is what registers the elements. + +This structure keeps one direction of dependency (widgets → cc-widgets) and concentrates the entire +public naming surface (export names, tag names, prop typing) in two small files, so changes are easy to +review and the rest of the suite stays decoupled from distribution concerns. + +## Data Flow +Transport is in-process JavaScript module evaluation plus the browser CustomElements registry. There is no +network or messaging in this module — all SDK/state access happens downstream in the widgets via the shared +store. + +```mermaid +graph LR + subgraph WidgetPkgs["Widget packages (React components)"] + SL["@webex/cc-station-login"] + US["@webex/cc-user-state"] + TASK["@webex/cc-task"] + DC["@webex/cc-digital-channels"] + STORE["@webex/cc-store"] + end + + subgraph CCW["cc-widgets"] + IDX["index.ts
re-export barrel + CSS"] + WC["wc.ts
r2wc wrap + register"] + end + + subgraph Consumers["Consumers"] + REACT["React host
named imports"] + DOM["Browser DOM
widget-cc-* elements"] + end + + SL --> IDX + US --> IDX + TASK --> IDX + DC --> IDX + STORE --> IDX + + SL --> WC + US --> WC + TASK --> WC + DC --> WC + STORE --> WC + + IDX -->|named exports| REACT + WC -->|customElements.define| DOM +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Web Component registration + mount (`./wc` import → r2wc → custom element → React widget → store) | `Host registers custom element and r2wc mounts the React widget` | `alt` branch: tag already registered → registration is skipped (idempotent); store-not-initialized branch noted. | +| React re-export consumption | covered by Data Flow (trivial pass-through; no distinct sequence) | N/A — direct import, no runtime steps owned by this module. | + +```mermaid +sequenceDiagram + participant Host as Host page / app + participant WC as wc.ts (module eval) + participant R2WC as r2wc + participant Reg as CustomElements registry + participant Widget as React widget + participant Store as @webex/cc-store + + Host->>WC: import '@webex/cc-widgets/wc' + activate WC + loop for each widget + WC->>R2WC: r2wc(Component, {props}) + R2WC-->>WC: custom-element class + WC->>Reg: customElements.get(name) + alt not registered + WC->>Reg: customElements.define(name, class) + else already registered + WC-->>WC: skip (idempotent) + end + end + deactivate WC + + Host->>Reg: in DOM / createElement + Reg->>R2WC: instantiate custom element + R2WC->>Widget: mount React component (props from attrs/props) + Widget->>Store: observer reads store state (store.cc, agent state) + Note over Widget,Store: If store.init not called first,
widget renders but shows no data + Store-->>Widget: state -> observer re-render + Widget-->>Host: rendered widget + CustomEvents/callbacks +``` + +## Class / Component Relationships +```mermaid +graph TD + subgraph react["React entry (index.ts)"] + direction LR + Barrel["re-export barrel"] + end + + subgraph wc["WC entry (wc.ts)"] + direction TB + r2wc["r2wc()"] + WebStationLogin["WebStationLogin"] + WebUserState["WebUserState"] + WebIncomingTask["WebIncomingTask"] + WebTaskList["WebTaskList"] + WebCallControl["WebCallControl"] + WebCallControlCAD["WebCallControlCAD"] + WebOutdialCall["WebOutdialCall"] + WebRealTimeTranscript["WebRealTimeTranscript"] + WebDigitalChannels["WebDigitalChannels"] + components["components[] {name, component}"] + end + + StationLogin -->|r2wc| WebStationLogin + UserState -->|r2wc| WebUserState + IncomingTask -->|r2wc| WebIncomingTask + TaskList -->|r2wc| WebTaskList + CallControl -->|r2wc| WebCallControl + CallControlCAD -->|r2wc| WebCallControlCAD + OutdialCall -->|r2wc| WebOutdialCall + RealTimeTranscript -->|r2wc| WebRealTimeTranscript + DigitalChannels -->|r2wc| WebDigitalChannels + + WebStationLogin --> components + WebUserState --> components + WebIncomingTask --> components + WebTaskList --> components + WebCallControl --> components + WebCallControlCAD --> components + WebOutdialCall --> components + WebRealTimeTranscript --> components + WebDigitalChannels --> components + + components -->|customElements.define| Registry["CustomElements registry"] + + StationLogin --> Barrel + UserState --> Barrel +``` + +The React widget components (imported from the widget packages) are the only "classes" of substance; +`cc-widgets` adds no class of its own beyond the r2wc-generated wrapper classes (`Web*`). Each wrapper is +a `HTMLElement` subclass produced by `r2wc`. The `components` array is the registry manifest. `index.ts` +relates to the same widget components purely by re-export. + +## Use Cases +- **UC-1 React host consumes the suite:** A React app imports `{ StationLogin, UserState, TaskList, store }` + from `@webex/cc-widgets`, calls `store.init({...})`, then renders the widgets as JSX. Outcome: widgets + render against the shared store with the host's React instance. Evidence: `src/index.ts`; archived + example in `.../cc-widgets/ai-docs/AGENTS.md`. No test. +- **UC-2 Framework-agnostic host embeds Web Components:** An HTML/Angular/Vue/vanilla page loads the `./wc` + bundle, initializes `store`, and places `` etc. in the DOM, assigning callbacks + as element properties (e.g. `el.onLogin = fn`). Outcome: r2wc mounts the React widget inside the custom + element. Evidence: `src/wc.ts`; archived example in `.../cc-widgets/ai-docs/AGENTS.md`. No test. +- **UC-3 Add a new widget to the suite:** A maintainer adds the React export in `index.ts`, wraps the + component with `r2wc` and a prop map in `wc.ts`, and appends `{name, component}` to the `components` + array. Outcome: the widget is available in both consumption modes. Evidence: `src/index.ts`, `src/wc.ts`. + No test. + +## Pitfalls +- **WC props are properties, not attributes.** `function` and `json` props (callbacks, `incomingTask`, + `liveTranscriptEntries`) must be assigned as element properties (`el.onLogin = fn`, + `el.incomingTask = obj`), not via `setAttribute`/string attributes — otherwise the widget never receives + them. Source: prop maps in `src/wc.ts`. +- **Don't load both bundles.** Loading the React export and the `wc.js` bundle together can produce two + React instances → "Invalid hook call" and broken context. Choose one mode per host. +- **Store must be initialized before widgets show data.** Widgets render but stay empty if `store.init` + was not called first; the store is a shared singleton, not initialized by this package. +- **Registration is an import-time side effect.** Elements only exist after `./wc` is evaluated. Elements + added to the DOM before the bundle loads stay inert until definition; use + `customElements.whenDefined(tag)` when racing. +- **Tag names are public API.** They live only in the `components` array in `src/wc.ts`; renaming a tag is + a breaking change with no compile-time signal in consumer HTML. +- **Archived docs drift.** The pre-migration ARCHITECTURE/AGENTS docs omitted `CallControlCAD`, + `RealTimeTranscript`, and `DigitalChannels` tags and the `hasCampaignPreviewEnabled` prop. Trust + `src/wc.ts` over those docs. + +## Module Do's / Don'ts +- DO: keep `cc-widgets` aggregation-only — re-export and register; put behavior changes in the upstream + widget packages. +- DO: when adding a widget, update both `src/index.ts` (React export) and `src/wc.ts` (r2wc wrapper + + prop map + `components` entry), keeping the `widget-cc-{name}` tag convention. +- DON'T: import the SDK or mutate store state here — access flows through the re-exported `store`. +- DON'T: register a custom element without the `customElements.get` guard (breaks idempotent re-import). +- DON'T: bundle React into the React (`index.js`) path — React/ReactDOM are peers. + +## Export Stability +Two semver-sensitive surfaces: the React named exports in `src/index.ts` and the `widget-cc-*` tag names +plus their r2wc prop maps in `src/wc.ts`. Adding an export, a tag, or an optional mapped prop is a minor +(additive) change. Renaming or removing any export or tag, or renaming a mapped prop, is a major +(breaking) change. The type-declaration surface is published at `dist/types/index.d.ts` (`.`) and +`dist/types/wc.d.ts` (`./wc`), wired via the `exports`/`types` fields in `package.json`. Peer-dependency +floors (`react >=18.3.1`, Momentum versions) are part of the compatibility contract; raising a floor is a +potentially breaking change for hosts. + +## Host Integration & Theming +This module is the host-mount surface for the widget suite. +- **Custom-element tags:** `widget-cc-user-state`, `widget-cc-station-login`, `widget-cc-incoming-task`, + `widget-cc-task-list`, `widget-cc-call-control`, `widget-cc-call-control-cad`, `widget-cc-outdial-call`, + `widget-cc-realtime-transcript`, `widget-cc-digital-channels` (`src/wc.ts`). +- **Mount contract (WC mode):** load `@webex/cc-widgets/wc` (registers the elements at import time), + initialize the shared `store`, then place the custom elements in the DOM and assign callbacks/objects as + element properties. +- **Mount contract (React mode):** import widgets + `store` from `@webex/cc-widgets`, init `store`, render + as JSX. The host provides React/ReactDOM (peers). +- **Theming:** React consumers get Momentum base styling via the `@momentum-ui/core` CSS imported in + `src/index.ts`; hosts also need the Momentum peer packages (`@momentum-ui/react-collaboration`, + `@momentum-ui/web-components`). Do not assume the host's framework version beyond the declared peer + floors. + +## Test-Case Strategy (module) +No tests exist for this package today (`tests/` is absent and `package.json` sets `passWithNoTests: true`). +Because the module is pure aggregation, the highest-value tests would assert the distribution contract +rather than widget behavior: (positive) importing `./wc` defines every expected `widget-cc-*` tag via +`customElements.get`; (negative) importing `./wc` twice does not throw and does not redefine an element +(idempotent guard). Secondary coverage: assert `index.ts` re-exports each expected symbol and `store`, and +that each r2wc wrapper is created with the documented prop-type map. Widget rendering/behavior is owned and +tested by the upstream widget packages, not here. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `cc-widgets-R-001` (React re-exports present) | None found | No test asserts the export barrel surface. | +| `cc-widgets-R-002` (custom elements registered) | None found | No test asserts each `widget-cc-*` tag is defined after `./wc` import. | +| `cc-widgets-R-003` (idempotent registration) | None found | No test for double-import / `customElements.get` guard. | +| `cc-widgets-R-004` (r2wc prop-type map) | None found | No test asserts function/json/boolean/string prop mapping. | +| `cc-widgets-R-005` (Momentum CSS imported) | None found | No test for the CSS side-effect import. | +| `cc-widgets-R-006` (React/ReactDOM peers) | None found | Enforced by package manager only; no automated check here. | + +## Traceability +- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/station-login/ai-docs/station-login-spec.md b/packages/contact-center/station-login/ai-docs/station-login-spec.md new file mode 100644 index 000000000..af8f7f862 --- /dev/null +++ b/packages/contact-center/station-login/ai-docs/station-login-spec.md @@ -0,0 +1,363 @@ +# station-login — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, UI, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `station-login` | +| Source path(s) | `packages/contact-center/station-login/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| Validation status | not-run | + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source +evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish +truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the +repository policy says history is reliable, and must include the commit hash. If evidence is missing or +conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers +as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/AGENTS.md` | overview / API | migrated | Orientation → Overview/Purpose; props → Public Surface; usage examples → Use Cases; error callback → Error Handling | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/station-login/ai-docs/ARCHITECTURE.md` | architecture | reconciled | Layer table → Class/Component Relationships; data flow + sequences → Data Flow / Sequence Diagram(s); troubleshooting → Pitfalls. Old "renders blank screen" / silent-fail notes mapped to Error Handling; over-generalized `store.login()` arrow in the archived diagram corrected — the hook calls `cc.stationLogin()` directly, not `store.login()` | + +## Overview +`station-login` is the agent station-login widget for Webex Contact Center. It lets an agent pick a team +and a device/login type (Desktop/`BROWSER` WebRTC, `EXTENSION`, or `AGENT_DN` dial number), log in to and +out of their station, sign out of Contact Center, and — in profile mode — update those login options +without a full re-login. It also surfaces the "already logged in elsewhere" multiple-login alert and a +Continue flow. + +The package follows the repo's one-directional widget architecture: the exported `StationLogin` widget +(`src/station-login/index.tsx`) is an `observer()` component wrapped in an `ErrorBoundary`. It reads +reactive state from the MobX store singleton, delegates all behavior to the `useStationLogin` hook +(`src/helper.ts`), and renders the presentational `StationLoginComponent` imported from `@webex/cc-components`. +The hook owns the SDK calls (`cc.stationLogin`, `cc.stationLogout`, `cc.updateAgentProfile`, `cc.deregister`) +and local form/result state; the store owns the SDK instance, observable agent state, and the CC event +callback registry. + +A maintainer should start at `src/station-login/index.tsx` (the public widget and its prop wiring), then +read `src/helper.ts` (all business logic and SDK integration). Prop and state shapes live in +`src/station-login/station-login.types.ts` and, ultimately, `IStationLoginProps`/`LoginOptionsState` in +`@webex/cc-components`. + +## Purpose / Responsibility +Owns the agent station-login UI flow: team + device-type selection, login, logout, CC sign-out, profile +(login-option) updates, and the multiple-login alert/Continue flow. Does NOT own the SDK instance, the +observable agent state (`teams`, `deviceType`, `dialNumber`, `teamId`, `isAgentLoggedIn`, +`showMultipleLoginAlert`), or the presentational rendering — those belong to `@webex/cc-store` and +`@webex/cc-components` respectively. + +## Stack +TypeScript 5.6.3, React `>=18.3.1` (functional component + hooks), MobX via `mobx-react-lite` `^4.1.0` +(`observer`), `react-error-boundary` `^6.0.0`. Tests: Jest 29 + React Testing Library 16 (jsdom). Build: +`tsc` (types) and Webpack 5 (`build:src`). Published as ESM/CJS package `@webex/cc-station-login` +(`main: dist/index.js`, `types: dist/types/index.d.ts`). No datastore or messaging of its own. + +## Folder / Package Structure +``` +station-login/ +├── src/ +│ ├── index.ts # Package barrel — re-exports StationLogin +│ ├── helper.ts # useStationLogin hook — all business logic + SDK calls +│ └── station-login/ +│ ├── index.tsx # StationLogin widget (observer + ErrorBoundary) + StationLoginInternal +│ └── station-login.types.ts # StationLoginProps / UseStationLoginProps (Pick from IStationLoginProps) +└── tests/ + ├── helper.ts # useStationLogin hook tests + └── station-login/ + └── index.tsx # StationLogin widget + ErrorBoundary tests +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `src/index.ts` | Package export barrel; the public surface is whatever this re-exports (`StationLogin`) | +| `src/station-login/index.tsx` | Public widget, prop-to-hook wiring, store reads, ErrorBoundary → `store.onErrorCallback('StationLogin', error)` | +| `src/station-login/station-login.types.ts` | Authoritative public prop type `StationLoginProps` and hook input `UseStationLoginProps` (both `Pick` from `IStationLoginProps`) | +| `src/helper.ts` | `useStationLogin` — login/logout/saveLoginOptions/handleContinue/handleCCSignOut logic, `isLoginOptionsChanged` comparison, SDK event subscriptions | +| `@webex/cc-components` `components/StationLogin/station-login.types.ts` | Canonical `IStationLoginProps` / `LoginOptionsState`; do not redefine prop shapes here | + +## Public Surface +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `cc-widgets.StationLogin` | SDK / Web Component | React component `StationLogin`; mounted in `@webex/cc-widgets` as custom element `widget-cc-station-login` | Agent station login/logout, CC sign-out, profile-option update, multi-login alert | Stable semver; the custom-element tag name and the `profileMode`/`onLogin`/`onLogout`/`onCCSignOut` prop surface are breaking changes | `src/station-login/station-login.types.ts` (`StationLoginProps`), `IStationLoginProps` in `@webex/cc-components` | `../../../../ai-docs/CONTRACTS.md` | + +Public props (`StationLoginProps`, from `src/station-login/station-login.types.ts`): + +| Prop | Type | Required | Notes | +|---|---|---|---| +| `profileMode` | `boolean` | Yes | `true` = profile/save mode; `false` = login/logout mode | +| `onLogin` | `() => void` | No | Invoked on login success (and on mount if already logged in — `helper.ts`) | +| `onLogout` | `() => void` | No | Invoked on `AGENT_LOGOUT_SUCCESS` | +| `onCCSignOut` | `() => void` | No | Invoked after CC sign-out; presence enables the sign-out handler | +| `onSaveStart` | `() => void` | No | Invoked when a profile save begins | +| `onSaveEnd` | `(isComplete: boolean) => void` | No | Invoked when a profile save resolves (`true`) or fails / no-change (`false`) | +| `teamId` | `string` | No | Default/seed team id | +| `doStationLogout` | `boolean` | No | Defaults to `true` when omitted/null; if `false`, CC sign-out skips station logout | +| `hideDesktopLogin` | `boolean` | No | Hides the Desktop (`BROWSER`) option in dropdown | +| `allowInternationalDn` | `boolean` | No | Use international dial-number regex instead of agentConfig/US fallback | + +Compatibility notes: +- Adding a new optional prop is a minor change; removing a prop or changing the custom-element tag name is a major (breaking) change. + +## Requires (dependencies) +- `@webex/cc-store` (`workspace:*`) — MobX singleton; provides `cc` (SDK), `teams`, `loginOptions`, `deviceType`, `dialNumber`, `teamId`, `isAgentLoggedIn`, `showMultipleLoginAlert`, `logger`, `CC_EVENTS`, `setCCCallback`/`removeCCCallback`, `setShowMultipleLoginAlert`, `registerCC`, `onErrorCallback`. +- `@webex/cc-components` (`workspace:*`) — `StationLoginComponent` (presentational), `IStationLoginProps`/`StationLoginComponentProps`/`LoginOptionsState` types. +- `@webex/contact-center` (the SDK, via the store's `cc`) — `stationLogin()`, `stationLogout()`, `updateAgentProfile()`, `deregister()`; types `StationLoginSuccessResponse`, `LogoutSuccess`, `AgentProfileUpdate`, `LoginOption`. +- `mobx-react-lite` `^4.1.0` (`observer`); `react-error-boundary` `^6.0.0`. +- Peer: `react`/`react-dom` `>=18.3.1`, `@momentum-ui/react-collaboration` `>=26.201.9`. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `STATION-LOGIN-R-001` | `login()` calls `cc.stationLogin({teamId, loginOption, dialNumber})`; on success sets `loginSuccess` and clears `loginFailure`, on failure sets `loginFailure` and clears `loginSuccess` | Login result must drive UI success/error display | `src/helper.ts` (`login`) | `tests/helper.ts` "should set loginSuccess on successful login and set loginFailure to undefined", "should set loginFailure on failed login" | none | PRESENT | +| `STATION-LOGIN-R-002` | `logout()` calls `cc.stationLogout({logoutReason})` and sets `logoutSuccess` on success; on failure it logs and does not throw | Logout must update state and never crash the widget | `src/helper.ts` (`logout`) | `tests/helper.ts` "should set logoutSuccess on successful logout", "should log error on logout failure" | none | PRESENT | +| `STATION-LOGIN-R-003` | The `onLogin` callback fires when the agent becomes logged in (on `AGENT_STATION_LOGIN_SUCCESS` and on mount when already logged in); `onLogout` fires on `AGENT_LOGOUT_SUCCESS`. Both are guarded so absent callbacks are a no-op | Host app needs login/logout lifecycle hooks without crashing when omitted | `src/helper.ts` (`handleLogin`/`handleLogout`, `setCCCallback` effect, mount effect) | `tests/helper.ts` "should set loginSuccess on successful login without onLogin callback", "should not call logout callback if not present" | none | PRESENT | +| `STATION-LOGIN-R-004` | `saveLoginOptions()` short-circuits when `isLoginOptionsChanged` is false: sets `saveError` to "No changes detected…" and calls `onSaveEnd(false)` without calling the SDK | Avoids no-op profile writes and gives the host a deterministic failure signal | `src/helper.ts` (`saveLoginOptions`, `isLoginOptionsChanged`) | `tests/helper.ts` "should not save if isLoginOptionsChanged is false" | none | PRESENT | +| `STATION-LOGIN-R-005` | On a real change, `saveLoginOptions()` calls `onSaveStart()`, calls `cc.updateAgentProfile()` with `{loginOption, teamId}` (plus `dialNumber` only when deviceType ≠ `BROWSER`), and on success copies `currentLoginOptions`→`originalLoginOptions` and calls `onSaveEnd(true)` | Profile update must persist only changed options and resync the baseline so the Save button disables | `src/helper.ts` (`saveLoginOptions`) | `tests/helper.ts` "should call updateAgentProfile and update originalLoginOptions on save when changed", "should call updateAgentProfile with no dialNumber when deviceType is BROWSER" | none | PRESENT | +| `STATION-LOGIN-R-006` | When `cc.updateAgentProfile()` rejects, `saveError` is set to the error message and `onSaveEnd(false)` is called | Caller must be able to surface profile-save failures | `src/helper.ts` (`saveLoginOptions` `.catch`) | `tests/helper.ts` "should handle updateAgentProfile errors", "should handle errors in saveLoginOptions main logic" | none | PRESENT | +| `STATION-LOGIN-R-007` | `handleContinue()` clears the multiple-login alert (`store.setShowMultipleLoginAlert(false)`) then calls `store.registerCC()` to force re-registration | Agents already logged in elsewhere must be able to continue/take over the session | `src/helper.ts` (`handleContinue`) | `tests/helper.ts` "should call handleContinue and set device type", "should call handleContinue with agent not logged in", "should call handleContinue and handle error" | none | PRESENT | +| `STATION-LOGIN-R-008` | `handleCCSignOut()` calls `cc.stationLogout()` then `cc.deregister()` only when `doStationLogout` AND `isAgentLoggedIn`; otherwise it skips straight to invoking `onCCSignOut()`. `doStationLogout` defaults to `true` when omitted | Lets profile-mode hosts sign out without dropping the station, while default behavior fully logs out | `src/helper.ts` (`handleCCSignOut`, `doStationLogout` default) | `tests/helper.ts` "should call stationLogout when doStationLogout is not passed", "should not call stationLogout if doStationLogout is false", "should handle error if stationLogout fails in onCCSignOut", "should handle error if deregister fails in onCCSignOut" | none | PRESENT | +| `STATION-LOGIN-R-009` | The widget is wrapped in an `ErrorBoundary` that renders an empty fragment and routes the error to `store.onErrorCallback('StationLogin', error)` | A render/hook error must not blank-crash the host and must be reported with the component name | `src/station-login/index.tsx` (`ErrorBoundary`) | `tests/station-login/index.tsx` "should render empty fragment when ErrorBoundary catches an error" | none | PRESENT | +| `STATION-LOGIN-R-010` | `StationLoginInternal` is an `observer()` that reads store state and forwards it plus hook results into `StationLoginComponent` (including `dialNumberRegex = cc?.agentConfig?.regexUS`, `hideDesktopLogin`, `allowInternationalDn`) | Re-render must be driven by observable store changes and props must reach the presentational component intact | `src/station-login/index.tsx` (`StationLoginInternal`) | `tests/station-login/index.tsx` "renders StationLoginPresentational with correct props" | DN-regex selection (`allowInternationalDn`) is enforced inside `@webex/cc-components`, not this package | PRESENT | + +## Design Overview +The widget is intentionally thin. `StationLogin` exists only to provide the `ErrorBoundary`; the real work +is in `StationLoginInternal`, an `observer()` that pulls observable state off the store singleton and +constructs the `StationLoginComponentProps` object. All mutating behavior is delegated to `useStationLogin`, +keeping the component declarative and re-render-driven-by-MobX. + +`useStationLogin` is the business-logic layer. It holds local React state for the in-progress login form +(`team`, `selectedTeamId`, `selectedDeviceType`, `dialNumberValue`) and for operation results +(`loginSuccess`, `loginFailure`, `logoutSuccess`, `saveError`). It tracks two `LoginOptionsState` snapshots — +`originalLoginOptions` (the saved baseline) and `currentLoginOptions` (the edited values) — and derives +`isLoginOptionsChanged` by comparing them (with a special case that ignores `dialNumber` when the device +type is `BROWSER`, since WebRTC has no dial number). This derivation drives the Save button's enabled state +and the "no changes" short-circuit. Every public method is wrapped in try/catch and routes failures through +the SDK logger; the hook never throws into render. + +SDK event wiring uses the store's callback registry rather than direct SDK listeners: a `useEffect` keyed on +`store.isAgentLoggedIn` registers `handleLogin`/`handleLogout` against `CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS` +and `AGENT_LOGOUT_SUCCESS`. The cleanup/`removeCCCallback` is intentionally commented out (see Pitfalls) to +avoid tearing down the shared store-level event wrapper. + +## Data Flow +In-process React/MobX data flow (no network/queue transport owned by this module — the SDK owns the wire). +Inputs are host props and observable store state; outputs are SDK calls and host callbacks. + +```mermaid +graph LR + Host[Host app props] --> Widget[StationLogin / StationLoginInternal observer] + Store[(cc-store MobX singleton)] -->|observable state| Widget + Widget -->|props + callbacks| Hook[useStationLogin helper.ts] + Hook -->|StationLoginComponentProps| Component[StationLoginComponent cc-components] + Component -->|user actions| Hook + Hook -->|stationLogin / stationLogout / updateAgentProfile / deregister| SDK[Contact Center SDK] + Hook -->|setShowMultipleLoginAlert / registerCC / setCCCallback| Store + SDK -->|AGENT_STATION_LOGIN_SUCCESS / AGENT_LOGOUT_SUCCESS| Store + Store -->|state change → re-render| Widget + Hook -->|onLogin / onLogout / onCCSignOut / onSaveEnd| Host +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Station login | "Login flow" | `alt` success vs. failure branch (`loginFailure` set) | +| Station logout | folded into login diagram's `AGENT_LOGOUT_SUCCESS` path | logout `.catch` logs and no-ops | +| Profile option save | "Profile save flow" | no-change short-circuit + `updateAgentProfile` reject branch | +| CC sign-out | "CC sign-out flow" | conditional station logout/deregister + their failure handling | +| Multiple-login Continue | "Multiple-login Continue flow" | re-register failure logged | + +```mermaid +sequenceDiagram + actor User + participant Widget as StationLogin (observer) + participant Hook as useStationLogin + participant Store as cc-store + participant SDK as Contact Center SDK + + User->>Widget: Submit login (team, deviceType, dialNumber) + Widget->>Hook: login() + Hook->>SDK: cc.stationLogin({teamId, loginOption, dialNumber}) + alt success + SDK-->>Hook: StationLoginSuccessResponse + Hook->>Hook: setLoginSuccess(res); setLoginFailure(undefined) + SDK-->>Store: AGENT_STATION_LOGIN_SUCCESS + Store->>Hook: handleLogin() (registered callback) + Hook->>User: onLogin() + Store-->>Widget: isAgentLoggedIn=true → re-render + else failure + SDK-->>Hook: Error + Hook->>Hook: setLoginFailure(error); setLoginSuccess(undefined) + Widget->>User: render error state + end +``` + +```mermaid +sequenceDiagram + actor User + participant Hook as useStationLogin + participant SDK as Contact Center SDK + participant Host as Host app + + User->>Hook: saveLoginOptions() + alt no changes (isLoginOptionsChanged=false) + Hook->>Hook: setSaveError("No changes detected…") + Hook->>Host: onSaveEnd(false) + else changed + Hook->>Host: onSaveStart() + Hook->>SDK: cc.updateAgentProfile({loginOption, teamId[, dialNumber]}) + alt success + SDK-->>Hook: ok + Hook->>Hook: originalLoginOptions = currentLoginOptions; clear saveError + Hook->>Host: onSaveEnd(true) + else reject + SDK-->>Hook: Error + Hook->>Hook: setSaveError(error.message) + Hook->>Host: onSaveEnd(false) + end + end +``` + +```mermaid +sequenceDiagram + actor User + participant Hook as useStationLogin + participant SDK as Contact Center SDK + participant Host as Host app + + User->>Hook: handleCCSignOut() + alt doStationLogout && isAgentLoggedIn + Hook->>SDK: cc.stationLogout({logoutReason}) + SDK-->>Hook: ok / error (logged, swallowed) + Hook->>SDK: cc.deregister() + SDK-->>Hook: ok / error (logged, swallowed) + end + Hook->>Host: onCCSignOut() +``` + +```mermaid +sequenceDiagram + actor User + participant Hook as useStationLogin + participant Store as cc-store + participant SDK as Contact Center SDK + + Note over Store: showMultipleLoginAlert=true (login elsewhere detected) + User->>Hook: handleContinue() + Hook->>Store: setShowMultipleLoginAlert(false) + Hook->>Store: registerCC() + Store->>SDK: register (force) + alt isAgentLoggedIn after register + SDK-->>Store: success → isAgentLoggedIn=true + Hook->>Hook: log "Agent Relogin Success" + else still not logged in / throws + Hook->>Hook: log "Agent Relogin Failed" / catch+log error + end +``` + +## Class / Component Relationships +```mermaid +graph TD + StationLogin -->|wraps| StationLoginInternal + StationLoginInternal -->|calls| useStationLogin + StationLoginInternal -->|renders| StationLoginComponent + StationLoginInternal -->|reads| Store[(cc-store singleton)] + useStationLogin -->|cc.* methods| SDK[Contact Center SDK] + useStationLogin -->|setCCCallback / registerCC| Store + StationLoginProps -.Pick.-> IStationLoginProps + UseStationLoginProps -.Pick.-> IStationLoginProps + StationLoginComponentProps -.Pick.-> IStationLoginProps +``` +`StationLogin` (exported) is a plain FC that mounts an `ErrorBoundary` around `StationLoginInternal`, an +`observer()` FC. `StationLoginInternal` composes the `useStationLogin` hook's return with store-derived +props into `StationLoginComponentProps` and renders the presentational `StationLoginComponent` from +`@webex/cc-components`. The three prop types in this package (`StationLoginProps`, `UseStationLoginProps`) +and the component prop type are all `Pick`s of the single canonical `IStationLoginProps` declared in +cc-components, so prop shapes never diverge. + +## Use Cases +- **UC-1 Agent login:** Agent selects team + device type (+ dial number for `EXTENSION`/`AGENT_DN`), clicks Login → `login()` → `cc.stationLogin()` → success sets `loginSuccess`, `AGENT_STATION_LOGIN_SUCCESS` fires `onLogin`, widget re-renders logged-in. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: login form → loading → logged-in view or inline error. +- **UC-2 Agent logout:** Agent clicks Logout → `logout()` → `cc.stationLogout()` → `logoutSuccess` set, `onLogout` fires. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: logged-in view → login form. +- **UC-3 Update profile options:** In `profileMode`, agent edits device type/team/dial number → `isLoginOptionsChanged` enables Save → `saveLoginOptions()` → `cc.updateAgentProfile()` → `onSaveEnd(true)` and Save disables. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: profile form → Save (disabled until changed) → success/error message. +- **UC-4 CC sign-out:** Agent clicks Sign Out → `handleCCSignOut()` conditionally logs out + deregisters, then `onCCSignOut()`. Evidence: `src/helper.ts`, `tests/helper.ts`. +- **UC-5 Multiple-login Continue:** Agent already logged in elsewhere → store sets `showMultipleLoginAlert` → alert shown → agent clicks Continue → `handleContinue()` clears alert + `registerCC()` takes over the session. Evidence: `src/helper.ts`, `tests/helper.ts`. UI flow: alert dialog → Continue → logged-in view. + +## UI Flow +- **Login screen (logged out):** Team dropdown, login-option/device-type selector (`EXTENSION`, `AGENT_DN`, Desktop/`BROWSER`), dial-number field (shown for non-`BROWSER` types), Login button. Desktop option hidden when `hideDesktopLogin` is set. +- **Logged-in view:** Logout and/or Sign Out actions. +- **Profile mode (`profileMode=true`):** Same selectors plus a Save button that is disabled until `isLoginOptionsChanged` is true. +- **Error / non-happy states:** Inline login error from `loginFailure`; profile `saveError` message; "no changes detected" message on a no-op save; the multiple-login alert dialog with a Continue action; a top-level render error collapses the widget to an empty fragment (ErrorBoundary) and reports via `store.onErrorCallback`. +- **Validation:** Dial-number validation uses `dialNumberRegex` (`cc.agentConfig.regexUS`) or international regex when `allowInternationalDn` is set; the regex selection/validation itself is enforced in `@webex/cc-components`. + +## Error Handling & Failure Modes +| Condition | Signal (error/code/result) | Caller recovery | +|---|---|---| +| `cc.stationLogin()` rejects | `loginFailure` set (Error), `loginSuccess` cleared; logged | Surface `loginFailure` in UI; agent retries | +| `cc.stationLogout()` rejects | Logged via `logger.error`; no state change, no throw | None required; agent may retry logout | +| `cc.updateAgentProfile()` rejects | `saveError` = error message; `onSaveEnd(false)` | Host shows error; agent edits and re-saves | +| No changed login options | `saveError` = "No changes detected…"; `onSaveEnd(false)` | None; expected no-op | +| `stationLogout`/`deregister` fail during CC sign-out | Logged; `onCCSignOut()` still invoked | Host proceeds with app sign-out | +| `registerCC()` fails on Continue | Logged ("Agent Relogin Failed" / caught error) | Agent retries Continue | +| Render/hook throws | ErrorBoundary → empty fragment + `store.onErrorCallback('StationLogin', error)` | Host's error callback surfaces a notification | + +## Pitfalls +- **CC callback cleanup is intentionally disabled.** The `useEffect` that registers `setCCCallback` does NOT call `removeCCCallback` on unmount (commented out in `helper.ts`) because doing so tore down the shared store-level event wrapper for all consumers. Re-adding naive cleanup will break login/logout events repo-wide. +- **`login()` uses hook-prop values, not the local form state.** `cc.stationLogin` is called with `{teamId: team, loginOption: deviceType, dialNumber}` where `deviceType`/`dialNumber` come from store-derived props and `team` from local `setTeam` state — not from `selectedDeviceType`/`dialNumberValue`. When changing the login payload, trace which value actually feeds `cc.stationLogin`. +- **`isLoginOptionsChanged` ignores `dialNumber` for `BROWSER`.** Desktop/WebRTC has no dial number, so the comparison skips it; a stale `dialNumber` will not (and must not) enable Save in `BROWSER` mode. +- **`doStationLogout` defaults to `true`.** It is treated as `true` when `undefined` or `null`; only an explicit `false` skips station logout on CC sign-out. Profile-mode hosts that don't want a logout must pass `doStationLogout={false}` explicitly. +- **Archived-doc drift.** The pre-migration ARCHITECTURE diagram showed a `store.login()` call; the real code calls `cc.stationLogin()` directly from the hook. Trust `src/helper.ts`. + +## Module Do's / Don'ts +- DO: route all SDK access through `props.cc` (from `store.cc`) inside the hook; never import the SDK directly. +- DO: wrap every hook method body in try/catch and log via the SDK `logger` with `{module, method}` metadata. +- DON'T: reintroduce `removeCCCallback` cleanup without verifying the shared store event wrapper survives. +- DON'T: redefine prop shapes locally — `Pick` from `IStationLoginProps` in `@webex/cc-components`. + +## Export Stability +`src/index.ts` re-exports only `StationLogin`. Adding an optional prop to `StationLoginProps` is a minor +change; removing/renaming a prop, changing a callback signature, or renaming the `widget-cc-station-login` +custom element is a major (breaking) change. Type surface ships from `dist/types/index.d.ts`. + +## Host Integration & Theming +Consumed via `@webex/cc-widgets`, which wraps `StationLogin` as the custom element `widget-cc-station-login` +(r2wc). The store must be initialized (`store.init(...)`) before the widget renders. Hosts typically wrap it +in Momentum `ThemeProvider`/`IconProvider`; peer deps require `react`/`react-dom` `>=18.3.1` and +`@momentum-ui/react-collaboration` `>=26.201.9`. Error reporting is wired through `store.onErrorCallback`. + +## Test-Case Strategy (module) +Hook tests (`tests/helper.ts`) exercise login success/failure, logout success/failure, callback-present and +callback-absent paths, profile save (no-change short-circuit, changed save, `BROWSER` no-dial-number, +update-profile error), `handleContinue` (logged-in, not-logged-in, error), and CC sign-out +(`doStationLogout` true/false, stationLogout/deregister failure), plus a dedicated "Error Handling" block +asserting that try/catch swallows errors in each method. Widget tests (`tests/station-login/index.tsx`) +assert prop wiring into `useStationLogin` and the ErrorBoundary empty-fragment behavior. Each major method +has both a positive and a negative case. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `STATION-LOGIN-R-001` | `tests/helper.ts` login success/failure cases | none | +| `STATION-LOGIN-R-002` | `tests/helper.ts` logout success + failure | none | +| `STATION-LOGIN-R-003` | `tests/helper.ts` callback-present / callback-absent | mount-already-logged-in `onLogin` path is implicit, not a dedicated assertion | +| `STATION-LOGIN-R-004` | `tests/helper.ts` "should not save if isLoginOptionsChanged is false" | none | +| `STATION-LOGIN-R-005` | `tests/helper.ts` save-when-changed + `BROWSER` no-dial-number | none | +| `STATION-LOGIN-R-006` | `tests/helper.ts` updateAgentProfile error cases | none | +| `STATION-LOGIN-R-007` | `tests/helper.ts` handleContinue (3 cases) | none | +| `STATION-LOGIN-R-008` | `tests/helper.ts` `#onCCSignOut` block (4 cases) | none | +| `STATION-LOGIN-R-009` | `tests/station-login/index.tsx` ErrorBoundary test | none | +| `STATION-LOGIN-R-010` | `tests/station-login/index.tsx` "renders … with correct props" | `hideDesktopLogin`/`allowInternationalDn` forwarding not asserted directly | + +## Traceability +- Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md` +- Contracts: `../../../../ai-docs/CONTRACTS.md` (`cc-widgets.StationLogin`) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/store/ai-docs/store-spec.md b/packages/contact-center/store/ai-docs/store-spec.md new file mode 100644 index 000000000..b29a6f890 --- /dev/null +++ b/packages/contact-center/store/ai-docs/store-spec.md @@ -0,0 +1,350 @@ +# store — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, state, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `store` | +| Source path(s) | `packages/contact-center/store/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | generated_by: migration agent / approved_by: [NEEDS HUMAN INPUT] / updated_at: 2026-06-29 | +| Validation status | not-run | + +Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with +`<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc +metadata. + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source +evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish +truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the +repository policy says history is reliable, and must include the commit hash. If evidence is missing or +conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers +as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/AGENTS.md` | overview / API / usage | migrated | Overview, Purpose, Public Surface, Use Cases; usage snippets condensed to behavior. | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/ARCHITECTURE.md` | architecture / sequence diagrams | reconciled | Design Overview, Data Flow, Sequence Diagram(s), Pitfalls. Diagrams re-derived from current `store.ts` / `storeEventsWrapper.ts`; see Conflicts note below for drift corrected. | +| `contact-centre-sdk-apis/contact-center.json` | SDK API reference (TypeDoc) | reference-only | Linked as the authoritative source for SDK-shaped types/methods consumed via `store.cc.*`. | + +## Overview +`@webex/cc-store` is the single shared MobX store for every Webex Contact Center widget. It is the sole boundary between widgets and the `@webex/contact-center` SDK: widgets never import the SDK directly — they read observables and call methods on the store, which proxies to `store.cc.*`. The package is structured in two layers. `Store` (`src/store.ts`) is a `makeAutoObservable` singleton (`Store.getInstance()`) that holds raw observable state and owns initialization/registration with the SDK. `StoreWrapper` (`src/storeEventsWrapper.ts`) is the default export — it wraps the singleton, getter-proxies every observable, owns all SDK event wiring (CC + task events), exposes mutators (all writes funnel through `runInAction`), list-fetch helpers, callback registration, and task-lifecycle handling. + +`src/index.ts` re-exports the `StoreWrapper` instance as the default export plus everything from `store.types.ts` (types, the `CC_EVENTS` / `TASK_EVENTS` enums, login/consult/campaign constants) and `task-utils.ts` (pure selectors over SDK `ITask` objects). `util.ts` extracts a fixed allow-list of feature flags from the agent `Profile` at registration time. + +A maintainer should start at `src/store.ts` to understand the observable shape and init/register flow, then `src/storeEventsWrapper.ts` for how SDK events drive observable updates, then `src/task-utils.ts` for the read-only task/consult/conference selectors widgets consume. + +## Purpose / Responsibility +Owns Contact Center client-side state and the SDK boundary: initialize/register with `@webex/contact-center`, subscribe to CC and task events, expose reactive observables and mutators, fetch domain lists (buddy agents, queues, entry points, address book), and centralize the error callback. It does NOT own UI rendering, business validation, or any direct network protocol beyond delegating to the SDK. + +## Stack +TypeScript 5.6.3, MobX 6.13.5 (`makeAutoObservable`, `observable.ref`, `runInAction`). Consumed in React 18 via `mobx-react-lite` `observer()` in downstream packages (not a dependency of this package itself). SDK peer `@webex/contact-center` 3.12.0-next.42. Tests: Jest 29 + ts compile (`tsc --project tsconfig.test.json && jest --coverage`). Build target: `dist/index.js` (Webpack). Evidence: `packages/contact-center/store/package.json`. + +## Folder / Package Structure +``` +packages/contact-center/store/src/ +├── index.ts # Barrel: default StoreWrapper instance + re-export of types & task-utils +├── store.ts # Store singleton: MobX observables, init() + registerCC() +├── storeEventsWrapper.ts # StoreWrapper (default export): observable proxies, event wiring, mutators, list fetchers, task lifecycle +├── store.types.ts # Types/interfaces, CC_EVENTS & TASK_EVENTS enums, ConsultStatus, login/campaign constants +├── task-utils.ts # Pure selectors over ITask (incoming, consult status, conference participants, hold status) +├── util.ts # getFeatureFlags(): allow-list extraction from agent Profile +└── constants.ts # Task/interaction/consult state + participant-type string constants +``` +Tests mirror src under `packages/contact-center/store/tests/` (`store.ts`, `storeEventsWrapper.ts`, `task-utils.ts`, `util.ts`). + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `packages/contact-center/store/src/store.ts` | The observable state shape, the 6000ms init timeout, and the `registerCC` profile→observable mapping. Never re-declare these defaults elsewhere. | +| `packages/contact-center/store/src/store.types.ts` | `CC_EVENTS` / `TASK_EVENTS` event-name enums, `ConsultStatus`, `LoginOptions` order, `ERROR_TRIGGERING_IDLE_CODES`, `CAMPAIGN_PREVIEW_*` type lists, and the public export barrel. | +| `packages/contact-center/store/src/util.ts` | The exact feature-flag allow-list parsed from the agent profile. | +| `packages/contact-center/store/src/constants.ts` | Canonical task/interaction/consult state strings and `EXCLUDED_PARTICIPANT_TYPES`. | +| `packages/contact-center/store/src/index.ts` | The public export surface (default store + types + task-utils). | + +## Public Surface +This module is consumed as an imported SDK/code API (the `@webex/cc-store` package), not a network surface. Root index: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md). + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `cc-widgets.store` | SDK | default export `store` (StoreWrapper singleton); `init(options, setupEventListeners)`, `registerCC(webex?)`, observable getters, mutators, `getBuddyAgents/getQueues/getEntryPoints/getAddressBookEntries`, `setOnError`, `setCCCallback/removeCCCallback`, `setTaskCallback/removeTaskCallback` | Sole SDK access point and shared reactive state for all CC widgets | stable semver; observable getter set is additive | `packages/contact-center/store/src/storeEventsWrapper.ts`, `src/store.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `store.types` | SDK | type re-exports (`IContactCenter`, `ITask`, `Profile`, `Team`, `IStore`, `IStoreWrapper`, `InitParams`, `RealTimeTranscriptionData`, ~20 more) | Typed domain surface for widget code | stable semver; SDK-shaped types track the SDK | `packages/contact-center/store/src/store.types.ts:334-366`; SDK: `contact-centre-sdk-apis/contact-center.json` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `store.constants` | SDK | value/enum exports (`CC_EVENTS`, `TASK_EVENTS`, `ConsultStatus`, `LoginOptions`, `CAMPAIGN_PREVIEW_*`, `DESKTOP`/`EXTENSION`/`DIAL_NUMBER`) | Event names + domain enums for widgets | stable semver | `packages/contact-center/store/src/store.types.ts:368-403` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `store.task-utils` | SDK | pure selectors (`isIncomingTask`, `getTaskStatus`, `getConsultStatus`, `getConferenceParticipants`, `getConferenceParticipantsCount`, `isInteractionOnHold`, `findHoldStatus`, `findHoldTimestamp`, etc.) | Read-only derivations over `ITask` | stable semver | `packages/contact-center/store/src/task-utils.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | + +Compatibility notes: +- Adding a new observable getter or mutator is additive (minor). Removing/renaming an observable, mutator, or changing the `CC_EVENTS`/`TASK_EVENTS` enum values is breaking (major) — widgets and the SDK event stream depend on the exact string values. +- The `CC_EVENTS` / `TASK_EVENTS` enums are locally declared until the SDK exports them (see `// TODO: remove this once cc sdk exports this enum`, `store.types.ts:247`). They must stay byte-identical to the SDK's emitted event strings. + +## Requires (dependencies) +- `@webex/contact-center` SDK (peer, floor pinned in `package.json` at `3.12.0-next.42`) — the entire CC runtime: `Webex.init()`, `webex.cc.*` methods, the CC/task event stream, agent `Profile`, `webex.credentials.getUserToken()`. Consumed ONLY through the store. Fallback on unavailability: `Store.init()` rejects after a 6000ms timeout (`src/store.ts:140-142`); the wrapper wraps the rejection and invokes `onErrorCallback('Store', err)` (`src/storeEventsWrapper.ts:442-452`). +- `mobx` ^6.13.5 — observable state and `runInAction` for all mutations. +- Internal: none upstream. The store is the lowest widget-layer dependency (`cc-components → widget packages → store → SDK`); it imports no widget package. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `STORE-R-001` | `Store.getInstance()` returns one shared singleton instance; the default export is a single `StoreWrapper` over it | All widgets must share one source of truth for agent/session/task state | `packages/contact-center/store/src/store.ts:64-72`, `src/storeEventsWrapper.ts:51-53,1112-1114` | `tests/store.ts` ("should initialize with default values") | none | PRESENT | +| `STORE-R-002` | `init({webex})` registers immediately; `init({webexConfig, access_token})` calls `Webex.init()`, waits for the `ready` event, then registers | Supports both host-provided Webex and store-bootstrapped Webex | `src/store.ts:132-188` | `tests/store.ts` (init: "should call registerCC if webex is in options", "should initialize webex and call registerCC on ready event") | none | PRESENT | +| `STORE-R-003` | When bootstrapping Webex, init rejects with `Webex SDK failed to initialize` if the `ready` event has not fired within 6000ms | Prevents widgets hanging forever on an unreachable SDK | `src/store.ts:139-142` | `tests/store.ts` ("should reject the promise if Webex SDK fails to initialize") | none | PRESENT | +| `STORE-R-004` | `registerCC()` throws `Webex SDK not initialized` when neither a `webex` arg nor a prior `this.cc` exists | Fail fast on misuse instead of a later null deref | `src/store.ts:74-81` | `tests/store.ts` ("should throw error if webex and cc object are not present") | none | PRESENT | +| `STORE-R-005` | On successful `register()`, the profile is mapped into observables (teams, idleCodes, agentId, wrapupCodes, deviceType, dialNumber, teamId, timestamps, feature flags); registration failures reject and are logged | Populates initial state so widgets render correctly; surfaces failures | `src/store.ts:89-129` | `tests/store.ts` ("should initialise store values on successful register", "should log an error on failed register") | none | PRESENT | +| `STORE-R-006` | `loginOptions` excludes `BROWSER` unless `webRtcEnabled`, and is sorted by the `LoginOptions` key order | WebRTC/browser calling is gated by org capability; UI ordering must be stable | `src/store.ts:100-103`, `src/store.types.ts:319-323` | `tests/store.ts` ("should initialise store values on successful register") | none | PRESENT | +| `STORE-R-007` | `featureFlags` is restricted to a fixed allow-list of profile keys, omitting `undefined` values | Avoid leaking arbitrary profile fields and keep a known flag surface | `src/util.ts:3-36` | `tests/util.ts` ("should return an object with feature flags from agent profile...") | none | PRESENT | +| `STORE-R-008` | All observable mutations go through `runInAction` (directly or via mutators) | MobX strict-mode correctness; batched, atomic reactive updates | `src/storeEventsWrapper.ts` (e.g. 189-237, 269-282, 303-323, 906-921, 1008-1023) | `tests/storeEventsWrapper.ts` ("storeEventsWrapper Proxies", "setState") | none | PRESENT | +| `STORE-R-009` | `setCurrentTask` ignores incoming tasks and pending (state `new`, not yet accepted) campaign-preview tasks (clears `currentTask`); deep-clones the task; fires `onTaskSelected` only when the task actually changes | CallControl must not render for previews still showing Accept/Skip; avoid stale callbacks | `src/storeEventsWrapper.ts:243-283` | `tests/storeEventsWrapper.ts` ("setCurrentTask", "campaign preview task lifecycle") | none | PRESENT | +| `STORE-R-010` | `refreshTaskList()` re-reads `cc.taskManager.getAllTasks()` and reconciles `currentTask`: clears + resets state when empty, keeps current if still present, else promotes the first task | Keep the store's task view consistent with the SDK after any task event | `src/storeEventsWrapper.ts:303-323` | `tests/storeEventsWrapper.ts` ("refreshTaskList") | none | PRESENT | +| `STORE-R-011` | Incoming tasks register the full task-event listener set once; the `onIncomingTask` callback fires only for genuinely new tasks (not already in `taskList`) | Avoid duplicate listeners and duplicate incoming-task UI for consult/re-entry | `src/storeEventsWrapper.ts:690-762` | `tests/storeEventsWrapper.ts` ("storeEventsWrapper events reactions") | none | PRESENT | +| `STORE-R-012` | `handleTaskRemove` detaches every task listener, clears `realtimeTranscriptionData` for the removed current task, drops accepted-campaign tracking, resets custom state, and refreshes the list | Prevent listener/audio/state leaks across task lifecycles | `src/storeEventsWrapper.ts:458-521` | `tests/storeEventsWrapper.ts` ("handleTaskRemove — campaign ID cleanup") | Per-listener detach is asserted only partially; full leak audit is a gap | PRESENT | +| `STORE-R-013` | `agent:logoutSuccess` triggers `cleanUpStore()` which resets session observables and removes CC SDK listeners; `agent:multiLogin` sets `showMultipleLoginAlert` | Clean session teardown and multi-login warning | `src/storeEventsWrapper.ts:811-819,1003-1024,1029-1066` | `tests/storeEventsWrapper.ts` ("storeEventsWrapper events reactions") | none | PRESENT | +| `STORE-R-014` | `agent:stateChange` (type `AgentStateChangeSuccess`) updates `currentState` (defaulting `auxCodeId` `''`→`'0'`) and both state-change timestamps | Drives the agent-state widget and timers | `src/storeEventsWrapper.ts:797-809` | `tests/storeEventsWrapper.ts` ("storeEventsWrapper events reactions") | none | PRESENT | +| `STORE-R-015` | List fetchers proxy the SDK and propagate errors after logging; `getQueues` filters by upper-cased channel type; `getAddressBookEntries` returns empty when `isAddressBookEnabled` is false | Centralize SDK fetch + transform so widgets stay SDK-agnostic | `src/storeEventsWrapper.ts:924-1001` | `tests/storeEventsWrapper.ts` ("storeEventsWrapper", "getAccessToken") | `getBuddyAgents`/`getQueues` happy-path filtering covered; address-book disabled branch coverage is a gap | PRESENT | +| `STORE-R-016` | `setOnError` wraps the caller callback to also submit a behavioral metrics event before invoking it | Consistent telemetry on widget errors | `src/storeEventsWrapper.ts:285-301` | None found | Negative/telemetry-path test missing | WEAK | +| `STORE-R-017` | `isIncomingTask` returns true only when the task is not wrap-up-required, the agent has not joined, and the interaction state is `new`/`consult`/`connected`/`conference` | Gates whether a task is treated as an unanswered incoming offer | `src/task-utils.ts:26-37` | `tests/task-utils.ts` ("isIncomingTask" — incoming / not incoming / edge cases) | none | PRESENT | +| `STORE-R-018` | `getConsultStatus`/`getTaskStatus` map participant `consultState` + interaction state to a `ConsultStatus`, with special handling for secondary EP-DN agents | Consult/conference UI relies on a single derived status | `src/task-utils.ts:39-146` | None found (direct `getConsultStatus` test) | Only `isIncomingTask`, conference, and hold helpers are directly tested; consult-status helper is a gap | WEAK | +| `STORE-R-019` | Conference helpers (`getIsConferenceInProgress`, `getConferenceParticipants`, `getConferenceParticipantsCount`) count only active agent participants, excluding `Customer`/`Supervisor`/`VVA` and those who left | Accurate conference participant display | `src/task-utils.ts:148-247`, `src/constants.ts:33` | `tests/task-utils.ts` ("getIsConferenceInProgress", "getConferenceParticipants", "getConferenceParticipantsCount") | none | PRESENT | +| `STORE-R-020` | `findHoldTimestamp`/`findHoldStatus` resolve hold state per media type, remapping to `mainCall` for secondary EP-DN agents | Hold timers align with Agent Desktop across consult/conference | `src/task-utils.ts:285-362` | `tests/task-utils.ts` ("findHoldTimestamp") | `findHoldStatus` direct coverage is a gap | PRESENT | +| `STORE-R-021` | `handleRealtimeTranscription` upserts transcript lines keyed by `messageId`, normalizing role/timestamp and dropping empty content | Live transcription panel needs deduped, ordered lines | `src/storeEventsWrapper.ts:891-922` | None found | No dedicated transcription test located | WEAK | + +## Design Overview +The store is deliberately split into a thin observable core and a thick wrapper. `Store` (`store.ts`) holds only field declarations + `makeAutoObservable` (with `cc` as `observable.ref` so the SDK object itself is not deeply observed) and the two lifecycle methods `init`/`registerCC`. Everything reactive and event-driven lives in `StoreWrapper` (`storeEventsWrapper.ts`), which composes the singleton via `Store.getInstance()` and re-exposes each field through a getter. This keeps the observable schema in one place while concentrating SDK coupling, event wiring, and mutation discipline in the wrapper. + +Initialization has two entry shapes (`InitParams = WithWebex | WithWebexConfig`). With a host-supplied `webex`, the wrapper wires event listeners and registers synchronously. Without one, the store calls `Webex.init()`, arms a 6000ms timeout, and waits for the `ready` event before wiring listeners and registering; the timeout guards against an SDK that never becomes ready. Registration maps the agent `Profile` into observables once. + +Event handling is the heart of the wrapper. `setupIncomingTaskHandler` is passed into `init` and attaches CC-level listeners (`stationLoginSuccess`, `dnRegistered`/`reloginSuccess`, `multiLogin`, `stateChange`, `logoutSuccess`, task incoming/hydrate/merged/campaign-preview). Per-task listeners are attached in `registerTaskEventListeners` when a task arrives and symmetrically detached in `handleTaskRemove`. Most task events simply call `refreshTaskList()`, which re-reads the SDK's authoritative task map and reconciles `currentTask`. Campaign-preview tasks carry extra state logic (RESERVED vs ENGAGED, an `acceptedCampaignIds` set) so a pending preview never promotes to `currentTask`. + +Mutations are funneled through small mutator methods that wrap `runInAction`, satisfying MobX strict mode and keeping reactive updates atomic. `task-utils.ts` is pure (no store state) — selectors that downstream widgets call to derive consult/conference/hold status from an `ITask`. + +## Data Flow +In-process MobX reactivity; the only external transport is the SDK event stream and method calls (`@webex/contact-center`), which is itself WebSocket/HTTP under the hood but opaque to this module. +```mermaid +graph TB + subgraph Host + App[Host app / cc-widgets] + end + subgraph Widgets + Hook[Custom hook] + UI[observer component] + end + subgraph State + Wrapper[StoreWrapper
storeEventsWrapper.ts] + Store[Store singleton
store.ts] + end + SDK["@webex/contact-center SDK"] + + App -->|init / registerCC| Wrapper + Hook -->|reads observables / calls methods| Wrapper + Wrapper -->|getter proxy| Store + Wrapper -->|store.cc.* methods| SDK + SDK -->|CC + task events| Wrapper + Wrapper -->|runInAction mutations| Store + Store -->|observable reactions| Hook + Hook --> UI +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Init + register | "Store init / register" | 6000ms init timeout reject; register reject; wrapper error callback | +| SDK event → observable update | "Agent state change & multi-login" | non-`AgentStateChangeSuccess` payloads ignored | +| Incoming task lifecycle | "Incoming task → assigned → end/remove" | duplicate-task guard; campaign-preview RESERVED branch; listener detach on remove | +| Representative `store.cc.*` call | "getQueues list fetch" | SDK error logged + rethrown | + +```mermaid +sequenceDiagram + participant App + participant W as StoreWrapper + participant S as Store + participant SDK as "@webex/contact-center" + + App->>W: init(options) + W->>S: init(options, setupIncomingTaskHandler) + alt options has webex + S->>W: setupEventListeners(webex.cc) + S->>S: registerCC(webex) + else webexConfig + access_token + S->>S: setTimeout(6000ms) + S->>SDK: Webex.init({config, credentials}) + alt ready within 6s + SDK-->>S: ready + S->>S: clearTimeout; setupEventListeners(webex.cc) + S->>S: registerCC(webex) + else timeout elapses + S-->>W: reject("Webex SDK failed to initialize") + end + end + S->>SDK: cc.register() + alt register resolves + SDK-->>S: Profile + S->>S: map profile → observables, getFeatureFlags() + S-->>W: resolve + W-->>App: resolve + else register rejects + SDK-->>S: error + S-->>W: reject(error) + W->>W: onErrorCallback("Store", err) + W-->>App: throw err + end +``` + +```mermaid +sequenceDiagram + participant SDK as "@webex/contact-center" + participant W as StoreWrapper + participant S as Store + + SDK-->>W: agent:stateChange (data) + alt data.type == "AgentStateChangeSuccess" + W->>S: setCurrentState(auxCodeId || "0") + W->>S: setLastStateChangeTimestamp(ts) + W->>S: setLastIdleCodeChangeTimestamp(ts) + else other payload + W->>W: ignore + end + SDK-->>W: agent:multiLogin (AgentMultiLoginCloseSession) + W->>S: setShowMultipleLoginAlert(true) + SDK-->>W: agent:logoutSuccess + W->>W: setAgentProfile({}); cleanUpStore(); removeEventListeners() +``` + +```mermaid +sequenceDiagram + participant SDK as "@webex/contact-center" + participant W as StoreWrapper + participant S as Store + + SDK-->>W: task:incoming (ITask) + W->>W: registerTaskEventListeners(task) + alt task not already in taskList + W->>W: onIncomingTask({task}); handleTaskMuteState(task) + end + W->>S: refreshTaskList() + SDK-->>W: task:assigned + alt campaign preview & state == "new" + W->>S: setState(RESERVED) + else + W->>S: setCurrentTask(task); setState(ENGAGED) + end + SDK-->>W: task:end + W->>S: setIsDeclineButtonEnabled(false); refreshTaskList() + Note over W,S: handleTaskRemove detaches all task listeners,
clears transcription, drops accepted-campaign id,
resets state, refreshTaskList() +``` + +```mermaid +sequenceDiagram + participant Widget + participant W as StoreWrapper + participant SDK as "@webex/contact-center" + + Widget->>W: getQueues(mediaType, params) + W->>SDK: cc.getQueues(params) + alt resolves + SDK-->>W: queues + W->>W: filter by channelType == mediaType.toUpperCase() + W-->>Widget: {data, meta} + else rejects + SDK-->>W: error + W->>W: logger.error(...) + W-->>Widget: throw error + end +``` + +## Class / Component Relationships +```mermaid +classDiagram + class IStore { <> } + class IStoreWrapper { <> } + class Store { -instance: Store; +getInstance(); +init(); +registerCC() } + class StoreWrapper { +store: IStore; +init(); +registerCC(); +refreshTaskList(); +get* observables } + IStoreWrapper --|> IStore + Store ..|> IStore + StoreWrapper ..|> IStoreWrapper + StoreWrapper o-- Store : composes (getInstance) + StoreWrapper ..> task_utils : uses isIncomingTask + StoreWrapper ..> SDK : store.cc.* + Store ..> SDK : Webex.init / cc.register + class task_utils { <> isIncomingTask getConsultStatus getConferenceParticipants findHoldStatus } +``` +`StoreWrapper` extends the `IStore` contract (via `IStoreWrapper`) and composes a single `Store` singleton, proxying every observable through getters. `Store` implements `IStore` and is the only class that touches `Webex.init()`/`cc.register()`. `task-utils` is a stateless module of selectors that the wrapper and downstream widgets call against `ITask`. + +## Use Cases +- **UC-1 Bootstrap with host Webex:** Host calls `store.init({webex})` after the SDK `ready` event → wrapper wires listeners and `registerCC` maps the profile into observables → widgets render. Evidence: `src/store.ts:132-138`, `tests/store.ts` (init). +- **UC-2 Bootstrap Webex from store:** Host calls `store.init({webexConfig, access_token})` → store runs `Webex.init()`, waits for `ready` (or rejects at 6s), then registers. Evidence: `src/store.ts:139-188`, `tests/store.ts` (init). +- **UC-3 Observe agent/session state in React:** Widget wraps in `observer()` and reads `store.agentId`, `store.isAgentLoggedIn`, `store.deviceType`, `store.currentState` → re-renders on mutation. Evidence: `src/storeEventsWrapper.ts:56-187`, `_archive/.../AGENTS.md` usage. +- **UC-4 Handle an incoming task through to wrap-up:** SDK `task:incoming` → listeners registered + `onIncomingTask` fired → `task:assigned` sets ENGAGED/current → `task:end` + `handleTaskRemove` cleans up. Evidence: `src/storeEventsWrapper.ts:585-762`, `tests/storeEventsWrapper.ts` ("events reactions"). +- **UC-5 Campaign-preview accept flow:** `task:campaignPreviewReservation` puts a preview in RESERVED; preview stays out of `currentTask` until accepted (`acceptedCampaignIds`), then transitions to ENGAGED. Evidence: `src/storeEventsWrapper.ts:243-283,537-583,772-795`, `tests/storeEventsWrapper.ts` ("campaign preview task lifecycle"). +- **UC-6 Fetch a domain list for a widget dropdown:** Transfer/Consult widget calls `getBuddyAgents()`/`getQueues()`; Outdial calls `getEntryPoints()`/`getAddressBookEntries()` → store proxies the SDK, transforms/filters, returns. Evidence: `src/storeEventsWrapper.ts:924-1001`, `tests/storeEventsWrapper.ts`. + +## State Model +The store is a single MobX `makeAutoObservable` instance. Observable slices (all in `src/store.ts:23-56`): +- **Session / profile:** `agentId`, `agentProfile`, `isAgentLoggedIn`, `deviceType`, `dialNumber`, `teamId`, `teams`, `loginOptions`, `idleCodes`, `wrapupCodes`, `featureFlags`, `dataCenter`. +- **Agent state:** `currentState`, `customState`, `lastStateChangeTimestamp`, `lastIdleCodeChangeTimestamp`, `showMultipleLoginAlert`. +- **Tasks:** `taskList` (`Record`), `currentTask`, `acceptedCampaignIds` (`Set`), `realtimeTranscriptionData`. +- **Call/consult control:** `isMuted`, `callControlAudio`, `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`, `isDeclineButtonEnabled`, `isEndConsultEnabled`, `allowConsultToQueue`, `isDigitalChannelsInitialized`. +- **Misc:** `currentTheme`, `cc` (`observable.ref` — not deeply observed), `isAddressBookEnabled`. + +Transition triggers: SDK CC/task events drive the session/agent/task slices via the wrapper's handlers (`handleStateChange`, `handleTaskAssigned`, `refreshTaskList`, `cleanUpStore`, campaign-preview handlers). Widget-initiated mutators (`setDeviceType`, `setDialNumber`, `setTeamId`, `setState`, `setCurrentTheme`, etc.) drive UI-local slices. All writes pass through `runInAction`. + +## Concurrency & Reactive Flow +- Single-threaded JS, but inherently asynchronous and event-driven: SDK events arrive at arbitrary times and mutate shared observable state. There is no ordering guarantee between unrelated SDK events. +- All state writes are wrapped in `runInAction` (MobX strict mode) so each handler's mutations are applied atomically and observers see a consistent snapshot. +- Idempotency: per-task listeners are registered once (guarded by `!this.taskList[id]` for the incoming callback and by the `realtimeTranscriptionListeners[taskId]` map for transcription) and detached symmetrically in `handleTaskRemove`. `acceptedCampaignIds` is replaced as a new `Set` on each change to keep MobX reactions firing. +- `cc` is `observable.ref` — the SDK object is treated as an opaque reference, never deeply observed, to avoid MobX proxying the SDK's internals. +- Do NOT block inside event handlers; list fetchers are async and return promises rather than blocking the reactive update path. + +## Pitfalls +- **6-second init timeout (`src/store.ts:140`):** only applies to the `webexConfig` bootstrap path. With `init({webex})` there is no timeout — a never-ready host Webex hangs init silently. Ensure the host awaits the SDK `ready` event before calling `init({webex})`. +- **Event enums are local copies (`store.types.ts:204-259`):** `CC_EVENTS`/`TASK_EVENTS` string values must match the SDK exactly; an SDK rename will silently stop a handler from firing. +- **Pending campaign previews must not become `currentTask`:** `setCurrentTask` clears `currentTask` for a preview in state `new` that is not in `acceptedCampaignIds` (`storeEventsWrapper.ts:255-267`). Bypassing this (e.g. calling SDK methods directly) re-introduces the bug where CallControl renders for an unaccepted preview. +- **Listener leaks:** every `task.on(...)` in `registerTaskEventListeners` has a matching `task.off(...)` in `handleTaskRemove`. Adding a listener in one without the other leaks handlers and can double-fire `refreshTaskList`. +- **`getBuddyAgents`/`getQueues` default args dereference `this.currentTask.data.interaction.mediaType` (`storeEventsWrapper.ts:925,941`):** calling them with no `currentTask` set throws. Callers should pass an explicit `mediaType` when no task is active. +- **`@ts-expect-error` markers tie to SDK gaps:** several casts (e.g. `response.teams`, credentials API) are pinned to `CAI-6762`; removing the workaround before the SDK fix breaks the build. + +## Module Do's / Don'ts +- DO: route every SDK access through `store.cc.*`; widgets must never import `@webex/contact-center` directly. +- DO: wrap every observable mutation in `runInAction` (use the existing mutators). +- DO: add a matching `task.off(...)` in `handleTaskRemove` for any new `task.on(...)` in `registerTaskEventListeners`. +- DON'T: mutate observables outside the store, or read `currentTask.data...` in a default arg without a guard. +- DON'T: change a `CC_EVENTS`/`TASK_EVENTS` enum value without confirming the SDK emits that exact string. + +## Export Stability +`@webex/cc-store` is published and consumed by every widget package plus `@webex/cc-widgets`, which re-exports the `store` singleton. Adding an observable getter, mutator, type, or constant is a minor (additive) change. Removing/renaming any export, changing an event-enum value, or changing the `init`/`registerCC` signatures is a major (breaking) change. The TypeScript declaration surface is the `export type`/`export` lists in `store.types.ts:334-403` plus `index.ts`. Evidence: `packages/contact-center/store/src/index.ts`, `ai-docs/CONTRACTS.md`. + +## Test-Case Strategy (module) +Unit tests are split by source file. `tests/store.ts` covers the singleton defaults, `registerCC` profile mapping (positive) and register failure logging (negative), and all `init` branches including the 6s timeout reject and synchronous `Webex.init` throw. `tests/storeEventsWrapper.ts` is the largest suite: observable proxies, `setState`, callback register/remove, list fetchers + `getAccessToken`, event reactions, hydration custom-states, `refreshTaskList`, `setCurrentTask`, and the full campaign-preview lifecycle (accepted/unaccepted, ID cleanup, type branching). `tests/task-utils.ts` covers `isIncomingTask` (incoming / not-incoming / edge), the conference helpers, and `findHoldTimestamp`. `tests/util.ts` covers `getFeatureFlags`. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `STORE-R-001` | `tests/store.ts` | none | +| `STORE-R-002` | `tests/store.ts` (init) | none | +| `STORE-R-003` | `tests/store.ts` ("...fails to initialize") | none | +| `STORE-R-004` | `tests/store.ts` ("...not present") | none | +| `STORE-R-005` | `tests/store.ts` (register positive + negative) | none | +| `STORE-R-006` | `tests/store.ts` | explicit BROWSER-filter assertion could be strengthened | +| `STORE-R-007` | `tests/util.ts` | no negative (unknown-key omission) case | +| `STORE-R-008` | `tests/storeEventsWrapper.ts` (proxies, setState) | none | +| `STORE-R-009` | `tests/storeEventsWrapper.ts` (setCurrentTask, campaign preview) | none | +| `STORE-R-010` | `tests/storeEventsWrapper.ts` (refreshTaskList) | none | +| `STORE-R-011` | `tests/storeEventsWrapper.ts` (events reactions) | none | +| `STORE-R-012` | `tests/storeEventsWrapper.ts` (handleTaskRemove cleanup) | full per-listener detach not exhaustively asserted | +| `STORE-R-013` | `tests/storeEventsWrapper.ts` (events reactions) | none | +| `STORE-R-014` | `tests/storeEventsWrapper.ts` (events reactions) | none | +| `STORE-R-015` | `tests/storeEventsWrapper.ts` (list fetchers, getAccessToken) | address-book-disabled branch not directly asserted | +| `STORE-R-016` | None found | missing telemetry-path test | +| `STORE-R-017` | `tests/task-utils.ts` (isIncomingTask) | none | +| `STORE-R-018` | None found | `getConsultStatus`/`getTaskStatus` untested | +| `STORE-R-019` | `tests/task-utils.ts` (conference helpers) | none | +| `STORE-R-020` | `tests/task-utils.ts` (findHoldTimestamp) | `findHoldStatus` untested | +| `STORE-R-021` | None found | `handleRealtimeTranscription` untested | + +## Traceability +- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/task/ai-docs/task-spec.md b/packages/contact-center/task/ai-docs/task-spec.md new file mode 100644 index 000000000..f4bd3d65d --- /dev/null +++ b/packages/contact-center/task/ai-docs/task-spec.md @@ -0,0 +1,510 @@ +# task — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, state, UI, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `task` | +| Source path(s) | `packages/contact-center/task/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | generated_by: migration agent / approved_by: [NEEDS HUMAN INPUT] / updated_at: 2026-06-29 | +| Validation status | not-run | + +Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with `<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc metadata. + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the repository policy says history is reliable, and must include the commit hash. If evidence is missing or conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/.../task/ai-docs/widgets/CallControl/AGENTS.md` + `ARCHITECTURE.md` | architecture / overview / API | reconciled | Flows landed in Sequence Diagram(s); props in Public Surface. Migration-future claims (`task.uiControls`, renamed events) NOT applied — current code still uses `getControlsVisibility`; see Pitfalls + conflict notes. | +| `ai-docs/_archive/.../task/ai-docs/widgets/IncomingTask/AGENTS.md` + `ARCHITECTURE.md` | architecture / overview / API | reconciled | Accept/decline + RONA flow → Sequence Diagram(s); callbacks → Public Surface. | +| `ai-docs/_archive/.../task/ai-docs/widgets/OutdialCall/AGENTS.md` + `ARCHITECTURE.md` | architecture / overview / API | reconciled | Outdial + ANI flow → Sequence Diagram(s); login-mode behavior → Use Cases / Pitfalls. | +| `ai-docs/_archive/.../task/ai-docs/widgets/TaskList/AGENTS.md` + `ARCHITECTURE.md` | architecture / overview / API | reconciled | Task selection / accept / decline flow → Sequence Diagram(s). | +| `packages/contact-center/ai-docs/migration/*.md` (7 files) | architecture (planned refactor) | reference-only | Describes a planned SDK `task.uiControls` migration that is NOT in current code. Used only to mark conflicts; current behavior documented as-is. | +| `packages/contact-center/task/src/` | source of truth | migrated | All requirements, flows, state, and error tables derive from real code here. | + +## Overview +`task` is the largest CC widget bundle: it exports six React/Web-Component widgets that together cover the full agent interaction lifecycle — being offered a task, accepting/declining it, controlling an active call (hold, mute, record, consult, transfer, conference, wrap-up), placing outbound calls, listing concurrent tasks, and rendering a live transcript. Each widget follows the repo-standard layering: a thin `observer()` widget wraps an `ErrorBoundary`, reads MobX state from `@webex/cc-store`, delegates business logic to a custom hook in `helper.ts`, and renders a presentational component from `@webex/cc-components`. The hook is the only place that touches the SDK (`task.*` / `store.cc.*`) and registers/unregisters store task-event callbacks. + +A maintainer should start at `src/index.ts` (the export barrel), then `src/helper.ts` (all five hooks: `useIncomingTask`, `useTaskList`, `useCallControl`, `useOutdialCall`, `useRealTimeTranscript`), then `src/Utils/task-util.ts` (the `getControlsVisibility` aggregator that decides which call-control buttons are visible/enabled). The widget shells (`src/CallControl/index.tsx` etc.) are intentionally tiny — they only select store fields and forward props. + +State is not owned here: the live task objects (`currentTask`, `incomingTask`, `taskList`), wrap-up codes, device type, feature flags, agent id, and accepted-campaign ids all live in `@webex/cc-store`. The hooks read those, call SDK methods on the `ITask` object, and register callbacks via `store.setTaskCallback(EVENT, fn, interactionId)` so SDK-emitted events flow back into widget-local `useState` and into the consumer's `on*` callbacks. + +Note on migration docs: the archived per-widget docs and `ai-docs/migration/*.md` describe a *planned* refactor to an SDK-computed `task.uiControls` surface and renamed events (e.g. `AGENT_WRAPPEDUP` → `TASK_WRAPPEDUP`). That refactor is **not** present in the current code — control visibility is still computed locally by `getControlsVisibility`, and the store still emits `AGENT_WRAPPEDUP` / `CONTACT_RECORDING_*`. This spec documents the code as it exists today and flags the divergence in Pitfalls. + +## Purpose / Responsibility +Owns the agent-facing UI and SDK orchestration for the contact lifecycle of a single task and the agent's task list: offer→accept/decline, active-call controls (hold/resume/mute/record/consult/transfer/conference/wrap-up), outbound dialing, multi-task listing/selection, and live transcript rendering. It does NOT own task state, SDK connection, agent state/presence, or wrap-up-code configuration — those belong to `store`/SDK. + +## Stack +TypeScript 5, React 18 (function components + hooks), MobX via `mobx-react-lite` `observer()`, `react-error-boundary` for fault isolation. Presentational components are imported from `@webex/cc-components`; all task/agent state and SDK access come from `@webex/cc-store` (`@webex/contact-center` SDK underneath). A `Web Worker` (created from an inline blob) drives the hold timer (`src/Utils/useHoldTimer.ts`). Tests: Jest + React Testing Library under `tests/`. Build target: distributed as part of `@webex/cc-widgets` (r2wc Web Components). + +## Folder / Package Structure +``` +packages/contact-center/task/src/ +├── index.ts # Export barrel: IncomingTask, TaskList, CallControl, OutdialCall, CallControlCAD, RealTimeTranscript +├── task.types.ts # Public prop/callback types per widget + TARGET_TYPE, DeviceTypeFlags +├── helper.ts # All hooks: useIncomingTask, useTaskList, useCallControl, useOutdialCall, useRealTimeTranscript +├── CallControl/index.tsx # Active-call control widget shell (observer + ErrorBoundary) +├── CallControlCAD/index.tsx # CallControl variant with customer-data CSS hooks (callControlClassName / consult class) +├── IncomingTask/index.tsx # Offered-task accept/decline widget shell +├── OutdialCall/index.tsx # Outbound dialpad widget shell +├── TaskList/index.tsx # Concurrent-task list widget shell +├── RealTimeTranscript/index.tsx # Live transcript widget shell +└── Utils/ + ├── task-util.ts # getControlsVisibility aggregator + per-button visibility helpers + campaign-preview checks + findHoldTimestamp + ├── constants.ts # Media-type, max-conference-participant, timer-label, DestinationAgentType constants + ├── timer-utils.ts # calculateStateTimerData / calculateConsultTimerData (wrap-up / post-call / consult labels) + ├── useHoldTimer.ts # Web-Worker-backed hold-elapsed-seconds hook (consult hold prioritized over main hold) + └── sample-task.json # Test/sample fixture +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `src/index.ts` | Authoritative list of exported widgets — do not assume exports from elsewhere. | +| `src/task.types.ts` | Public prop/callback shapes per widget; `TARGET_TYPE`/`TargetType`; `DeviceTypeFlags`; re-exports `CAMPAIGN_PREVIEW_*` from store. | +| `src/helper.ts` | All hook logic and the exact SDK methods + store callbacks each operation uses. | +| `src/Utils/task-util.ts` | `getControlsVisibility` — the single source of truth for which call-control buttons are visible/enabled per device/feature-flag/task-state. | +| `src/Utils/constants.ts` | Media types, `MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE = 7`, timer labels, `DestinationAgentType` enum. | + +## Public Surface +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `task.IncomingTask` | SDK (React component / Web Component) | `IncomingTask` — props: `incomingTask`; callbacks: `onAccepted({task})`, `onRejected({task})` | Render an offered task with accept/decline; notify consumer on accept/reject/RONA | Stable; adding optional props/callbacks = minor | `src/task.types.ts` (`IncomingTaskProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `task.TaskList` | SDK (React component / Web Component) | `TaskList` — props: `hasCampaignPreviewEnabled?`; callbacks: `onTaskAccepted(task)`, `onTaskDeclined(task, reason)`, `onTaskSelected({task, isClicked})` | List concurrent tasks; accept/decline/select | Stable; `hasCampaignPreviewEnabled` defaults true | `src/task.types.ts` (`TaskListProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `task.CallControl` | SDK (React component / Web Component) | `CallControl` — callbacks: `onHoldResume({isHeld,task})`, `onEnd({task})`, `onWrapUp({task,wrapUpReason})`, `onRecordingToggle({isRecording,task})`, `onToggleMute({isMuted,task})`; props: `conferenceEnabled?`, `consultTransferOptions?`, `callControlClassName?`, `callControlConsultClassName?` | Active-call controls for `store.currentTask` | Stable; `conferenceEnabled` defaults `true` | `src/task.types.ts` (`CallControlProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `task.CallControlCAD` | SDK (React component / Web Component) | `CallControlCAD` — same callbacks/props as `CallControl`; emphasizes `callControlClassName` / `callControlConsultClassName` | CallControl variant styled for a customer-data layout | Stable; same surface as CallControl | `src/task.types.ts` (`CallControlProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `task.OutdialCall` | SDK (React component / Web Component) | `OutdialCall` — props: `isAddressBookEnabled?` (default `true`); no consumer callbacks | Outbound dialpad + ANI selection; disabled when a telephony task is active | Stable | `src/task.types.ts` (`OutdialProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `task.RealTimeTranscript` | SDK (React component / Web Component) | `RealTimeTranscript` — props: `liveTranscriptEntries?`, `className?` | Render live transcript for `store.currentTask` | Stable | `src/task.types.ts` (`RealTimeTranscriptProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | + +Compatibility notes: +- Adding an optional prop/callback is additive (minor); removing or renaming one, or changing a callback payload shape, is breaking (major) — these widgets are consumed via r2wc Web Components in `@webex/cc-widgets`. +- `conferenceEnabled` is normalized to `true` when undefined inside the `CallControl`/`CallControlCAD` wrappers; consumers relying on `undefined` getting `false` would break. + +## Requires (dependencies) +- `@webex/cc-store` (peer, internal): MobX singleton supplying `currentTask`, `incomingTask`, `taskList`, `wrapupCodes`, `deviceType`, `featureFlags`, `agentId`, `isMuted`, `acceptedCampaignIds`, `realtimeTranscriptionData`, `logger`, `cc` (SDK), plus `setTaskCallback`/`removeTaskCallback`, `setTaskAssigned`/`setTaskRejected`/`setTaskSelected`, `setCurrentTask`, `setIsMuted`, `getBuddyAgents`, `getAddressBookEntries`, `getEntryPoints`, `getQueues`, and helpers `getConferenceParticipants`, `findMediaResourceId`, `findHoldStatus`, `getConsultStatus`, `getIsConsultInProgress`, `getIsCustomerInCall`, `getConferenceParticipantsCount`, `ConsultStatus`, `TASK_EVENTS`. Source of truth for event names: `packages/contact-center/store/src/store.types.ts`. +- `@webex/cc-components` (internal): presentational components (`IncomingTaskComponent`, `TaskListComponent`, `CallControlComponent`, `CallControlCADComponent`, `OutdialCallComponent`, `RealTimeTranscriptComponent`) and types (`ControlProps`, `TaskProps`, `OutdialCallProps`, `Visibility`, `ControlVisibility`, `RealTimeTranscriptComponentProps`, `CampaignCallProcessingDetails`). +- `@webex/contact-center` (SDK, transitive via store): the `ITask` interface and methods invoked here (`accept`, `decline`, `hold`, `resume`, `end`, `wrapup`, `cancelAutoWrapupTimer`, `pauseRecording`, `resumeRecording`, `toggleMute`, `transfer`, `consult`, `endConsult`, `consultTransfer`, `consultConference`, `transferConference`, `exitConference`), `cc.startOutdial`, `cc.getOutdialAniEntries`, `cc.addressBook.getEntries`, `cc.agentConfig`. +- `react` ^18, `mobx-react-lite`, `react-error-boundary`. +- Browser `Web Worker` + `Blob`/`URL.createObjectURL` for the hold timer (graceful fallback to `holdTime = 0` when no hold timestamp). + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `TASK-R-001` | `IncomingTask.accept()` calls `incomingTask.accept()` only when `incomingTask.data.interactionId` exists; SDK rejection is caught and logged, never thrown to the consumer. | Prevents calling SDK with no task and avoids crashing the widget on backend failure. | `src/helper.ts` (`useIncomingTask.accept`) | `tests/helper.ts` ("should return if there is no taskId for incoming task", "should handle errors when accepting a task", "should handle errors in accept method") | none | PRESENT | +| `TASK-R-002` | `IncomingTask.reject()` calls `incomingTask.decline()` (guarded by interactionId); RONA timeout reaches the same decline path via the timer in the presentational component. | Decline and RONA must converge on `decline()` so the backend reassigns the task. | `src/helper.ts` (`useIncomingTask.reject`) | `tests/helper.ts` ("should handle errors when declining a task", "should call onRejected if it is provided") | RONA countdown UI lives in `@webex/cc-components`, not this module | PRESENT | +| `TASK-R-003` | `useIncomingTask` registers callbacks for `TASK_ASSIGNED`/`TASK_CONSULT_ACCEPTED` (→ `onAccepted`) and `TASK_END`/`TASK_REJECT`/`TASK_CONSULT_END` (→ `onRejected`), keyed by interactionId, and removes them on unmount/task change. | Consumer notifications must fire on real SDK events and listeners must not leak across tasks. | `src/helper.ts` (`useIncomingTask` `useEffect`) | `tests/helper.ts` ("should setup event listeners for the incoming call", "shouldnt setup event listeners is not incoming call", "should call onAccepted if it is provided") | Cleanup uses different fn references than registration for some events (see Pitfalls) | PRESENT | +| `TASK-R-004` | `TaskList.acceptTask`/`declineTask` call `task.accept()`/`task.decline()` per task; `onTaskSelect` calls `store.setCurrentTask(task, true)`. | List actions operate per-task and selection switches the active `currentTask` for CallControl. | `src/helper.ts` (`useTaskList`) | `tests/helper.ts` ("should call onTaskAccepted callback when provided", "should call onTaskDeclined callback when provided", "should call onTaskSelected callback when provided", "should handle errors in onTaskSelect") | none | PRESENT | +| `TASK-R-005` | `useTaskList` wires `store.setTaskAssigned`/`setTaskRejected`/`setTaskSelected` only when the matching consumer callback (`onTaskAccepted`/`onTaskDeclined`/`onTaskSelected`) is provided; each wrapped callback is try/caught. | Avoid registering no-op store callbacks and isolate consumer-thrown errors. | `src/helper.ts` (`useTaskList` `useEffect`) | `tests/helper.ts` ("should not call onTaskAccepted if it is not provided", "should handle errors in taskAssigned callback", "should handle errors in taskSelected callback") | none | PRESENT | +| `TASK-R-006` | `CallControl.toggleHold(true/false)` calls `currentTask.hold()`/`currentTask.resume()`; `TASK_HOLD`/`TASK_RESUME` events fire `onHoldResume({isHeld, task})`. | Hold/resume must reflect real SDK state to the consumer. | `src/helper.ts` (`useCallControl.toggleHold`, `holdCallback`, `resumeCallback`) | `tests/helper.ts` ("should call onHoldResume with hold=true and handle success", "...hold=false...", "should log an error if hold fails", "should log an error if resume fails") | none | PRESENT | +| `TASK-R-007` | `toggleRecording` calls `pauseRecording()` when `isRecording` else `resumeRecording({autoResumed:false})`; `TASK_RECORDING_PAUSED`/`TASK_RECORDING_RESUMED` callbacks set `isRecording` and fire `onRecordingToggle`. | Recording UI state must track SDK events, not just the click. | `src/helper.ts` (`useCallControl.toggleRecording`, `pauseRecordingCallback`, `resumeRecordingCallback`) | `tests/helper.ts` ("should pause the recording when pauseResume is called with true", "should fail and log error if pause failed", "should resume the recording when pauseResume is called with false") | Subscription uses `TASK_RECORDING_PAUSED/RESUMED`; cleanup removes `CONTACT_RECORDING_PAUSED/RESUMED` (mismatch — see Pitfalls) | PRESENT | +| `TASK-R-008` | `toggleMute` no-ops with a warning when `controlVisibility.muteUnmute` is false; otherwise `await currentTask.toggleMute()`, then `store.setIsMuted(intended)` and `onToggleMute` only after success; on failure it reports the prior `isMuted`. | Mute state must reflect SDK truth even under rapid toggles or failure. | `src/helper.ts` (`useCallControl.toggleMute`) | `tests/helper.ts` ("should successfully toggle mute from unmuted to muted", "should handle multiple rapid toggleMute calls correctly", "should not call onToggleMute callback on error if not provided") | none | PRESENT | +| `TASK-R-009` | `wrapupCall(reason, auxCodeId)` calls `currentTask.wrapup(...)`; on resolve it promotes the first remaining task in `store.taskList` to `currentTask` and sets agent state to ENGAGED. | After wrap-up the agent should auto-focus the next task and return to an engaged state. | `src/helper.ts` (`useCallControl.wrapupCall`) | `tests/helper.ts` ("should call wrapupCall", "should log an error if wrapup fails") | ENGAGED label/username are local constants (`ENGAGED_LABEL`, `ENGAGED_USERNAME`) | PRESENT | +| `TASK-R-010` | Auto-wrap-up: when `currentTask.autoWrapup` and `controlVisibility.wrapup` are present, a 1s interval counts `secondsUntilAutoWrapup` down from `getTimeLeftSeconds()`; `cancelAutoWrapup` calls `currentTask.cancelAutoWrapupTimer()`. | Show and allow cancellation of the auto-wrap-up countdown. | `src/helper.ts` (`useCallControl` auto-wrapup `useEffect`, `cancelAutoWrapup`) | `tests/helper.ts` ("should initialize secondsUntilAutoWrapup to null when auto wrap-up is not active", "should call cancelAutoWrapup successfully", "should handle cancelAutoWrapup when currentTask is missing") | none | PRESENT | +| `TASK-R-011` | `consultCall(dest, type, allowParticipantsToInteract)` sends `holdParticipants: !allowParticipantsToInteract`; for `type==='queue'` it sets/clears `store.isQueueConsultInProgress` + `currentConsultQueueId` around the call, including on error. | Queue consult requires tracking the in-flight queue id so `endConsult` can pass it. | `src/helper.ts` (`useCallControl.consultCall`, `endConsultCall`) | `tests/helper.ts` ("should call consultCall successfully", "should call consultCall with allowParticipantsToInteract set to true", "should call endConsultCall with queue parameters when queue consult is in progress") | none | PRESENT | +| `TASK-R-012` | `consultTransfer` calls `currentTask.transferConference()` when `currentTask.data.isConferenceInProgress`, else `currentTask.consultTransfer()`; missing `currentTask.data` early-returns. | Conference and 1:1 consult complete via different SDK calls. | `src/helper.ts` (`useCallControl.consultTransfer`) | `tests/helper.ts` ("should call consultTransfer successfully", "should handle consultTransfer when currentTask data is missing") | none | PRESENT | +| `TASK-R-013` | `transferCall(to, type)` awaits `currentTask.transfer({to, destinationType})` and re-throws on error (unlike most handlers which swallow). | Blind transfer failures must surface to the calling modal so the UI can react. | `src/helper.ts` (`useCallControl.transferCall`) | `tests/helper.ts` ("should call transferCall successfully", "should handle rejection when loading buddy agents") | Re-throw is intentional and differs from hold/end/wrapup which only log | PRESENT | +| `TASK-R-014` | `switchToConsult`/`switchToMainCall` hold/resume the correct media leg via `findMediaResourceId(currentTask, 'mainCall'|'consult')`; `exitConference`/`consultConference` proxy the SDK directly. | Switching between consult and main legs targets the right media resource. | `src/helper.ts` (`useCallControl.switchToConsult/switchToMainCall/exitConference/consultConference`) | `tests/helper.ts` (useCallControl consult/conference cases) | none | WEAK | +| `TASK-R-015` | `getControlsVisibility(deviceType, featureFlags, task, agentId, conferenceEnabled, logger)` returns `{isVisible,isEnabled}` for every control plus consult/conference state flags, and returns safe all-hidden defaults inside a try/catch on any error. | Button visibility must degrade safely and never throw into render. | `src/Utils/task-util.ts` (`getControlsVisibility` + `get*ButtonVisibility`) | `tests/utils/task-util.ts` ("should handle errors when accessing featureFlags and return safe defaults", BROWSER/AGENT_DN/EXTENSION + telephony/chat/email cases) | none | PRESENT | +| `TASK-R-016` | End button is enabled during an EP-DN consult only when on the main call (`consultCallHeld`) or during conference when main is not held & consult not completed; disabled for regular agent-to-agent consult. | Matches Agent Desktop end-call rules for EP-DN vs agent consults. | `src/Utils/task-util.ts` (`getEndButtonVisibility`, `isConsultingWithEpDnAgent`) | `tests/utils/task-util.ts` ("should enable end button during EP_DN consult when switched back to main call...", "should disable end button for regular agent-to-agent consult (non-EP_DN)", EP/EPDN/EntryPoint variant detection) | none | PRESENT | +| `TASK-R-017` | `useHoldTimer` prioritizes the `consult` hold timestamp over `mainCall`, converts second-precision timestamps to ms (`< 1e10`), drives elapsed seconds via a Web Worker, and resets to 0 when no hold timestamp / on resume. | Hold timer must show the leg currently on hold and clean up its worker. | `src/Utils/useHoldTimer.ts` | `tests/utils/useHoldTimer.test.ts` ("should prioritize consult hold over main call hold", "should handle timestamp in seconds and convert to milliseconds", "should reset to 0 when call is resumed", "should return 0 when currentTask is null") | none | PRESENT | +| `TASK-R-018` | State timer prioritizes Wrap Up over Post Call; consult timer returns `Consult Requested` (initiated), `Consult on Hold` (held), else `Consulting`, falling back to participant `lastUpdated` when no consult timestamp. | Drives the correct timer label/timestamp in CallControl. | `src/Utils/timer-utils.ts` (`calculateStateTimerData`, `calculateConsultTimerData`) | `tests/utils/timer-utils.test.ts` ("should prioritize Wrap Up over Post Call", "should return Consult on Hold when consult is held", "should return Consult Requested label when consult is initiated") | none | PRESENT | +| `TASK-R-019` | `OutdialCall.startOutdial(destination, origin?)` alerts and aborts on empty/whitespace destination; passes `origin` (ANI) only when provided; SDK rejection is logged, not thrown. | Prevent empty outdials and honor optional caller-ID selection. | `src/helper.ts` (`useOutdialCall.startOutdial`) | `tests/OutdialCall/index.tsx` (render + `isAddressBookEnabled` cases) | No direct unit test asserts the empty-destination alert (gap) | WEAK | +| `TASK-R-020` | `getOutdialANIEntries` throws if `cc.agentConfig.outdialANIId` is missing, else returns `cc.getOutdialAniEntries({outdialANI})`; `isTelephonyTaskActive` is true iff any task in `store.taskList` has `mediaType === telephony`. | ANI selection requires a configured ANI id; outdial is gated on no active telephony task. | `src/helper.ts` (`useOutdialCall.getOutdialANIEntries`, `isTelephonyTaskActive`) | `tests/OutdialCall/index.tsx` (component render); helper outdial paths in `tests/helper.ts` | No explicit unit test for the "no outdialANIId throws" branch (gap) | WEAK | +| `TASK-R-021` | `useRealTimeTranscript` maps `realtimeTranscriptionData` to `RealTimeTranscriptEntry[]` only when `currentTaskId` is set and data is non-empty; otherwise returns `liveTranscriptEntries` unchanged. Speaker is normalized (AGENT→"You", CUSTOMER/CALLER→"Customer"). | Live transcript must key off the active task and normalize speaker labels. | `src/helper.ts` (`useRealTimeTranscript`, `mapTranscriptLineToEntry`, `getTranscriptSpeaker`) | `tests/RealtimeTranscript/index.tsx` ("passes props to useRealtimeTranscript hook", "renders fallback when an error is thrown") | none | PRESENT | +| `TASK-R-022` | Each widget shell renders inside an `ErrorBoundary` whose `fallbackRender` returns empty and `onError` calls `store.onErrorCallback(widgetName, error)` when set; absence of the callback must not throw. | A crashing widget must isolate and report, never break the host. | `src/{CallControl,CallControlCAD,IncomingTask,TaskList,OutdialCall,RealTimeTranscript}/index.tsx` | `tests/CallControl/index.tsx`, `tests/CallControlCAD/index.tsx`, `tests/IncomingTask/index.tsx`, `tests/TaskList/index.tsx`, `tests/OutdialCall/index.tsx`, `tests/RealtimeTranscript/index.tsx` (each has an ErrorBoundary + "onErrorCallback not set" case) | none | PRESENT | +| `TASK-R-023` | `CallControl`/`CallControlCAD` render nothing when there is no `currentTask` or when the task is an unaccepted campaign preview (`isUnacceptedCampaignPreview(task, acceptedCampaignIds)`). | Controls must only appear for an accepted, active task — matches Agent Desktop campaign-preview behavior. | `src/CallControl/index.tsx`, `src/CallControlCAD/index.tsx`, `src/Utils/task-util.ts` (`isCampaignPreviewTask`, `isUnacceptedCampaignPreview`) | None found for the unaccepted-campaign-preview early return (gap) | Campaign-preview gating relies on `store.acceptedCampaignIds`, not `participants.hasJoined` | WEAK | + +## Design Overview +Every widget is the same four-layer pipeline. The shell (`*/index.tsx`) is an `observer()` that destructures the store fields it needs, builds a hook-input object, calls the hook, merges hook output with extra store fields, and renders the matching `cc-components` component — all wrapped in an `ErrorBoundary` that funnels crashes to `store.onErrorCallback`. The shells contain almost no logic; the only branching there is CallControl's "no task / unaccepted campaign preview → render empty" guard and the `conferenceEnabled ?? true` default. + +`helper.ts` holds all behavior. Each hook (a) registers SDK-event callbacks through `store.setTaskCallback(EVENT, fn, interactionId)` in a `useEffect` and removes them in cleanup, (b) exposes imperative actions (`accept`, `toggleHold`, `consultCall`, `startOutdial`, …) that call `ITask`/`cc` SDK methods, and (c) derives view state. The most complex hook, `useCallControl`, additionally maintains a dozen `useState` values (recording, buddy agents, consult agent name, target type, timers, conference participants) and recomputes `controlVisibility` via `useMemo(getControlsVisibility, …)`. + +`Utils/` is pure logic split out for testability: `task-util.ts` decides control visibility/enablement from device type, feature flags, and a large set of derived task-state booleans (consult status, hold status, conference progress, customer-in-call, participant counts); `timer-utils.ts` and `useHoldTimer.ts` compute timer labels/elapsed times. Keeping these pure means the visibility matrix and timer math are unit-tested without rendering. + +Why this shape: the one-directional layering (`widget → hook → component → store → SDK`) keeps the SDK surface in exactly one file per package and lets MobX `observer()` re-render widgets reactively when the store's task observables change, while consumer callbacks (`on*`) are the only outward coupling. + +## Data Flow +Transport is in-process MobX reactivity inward and SDK promise calls + SDK event callbacks outward. SDK events arrive over the SDK's transport (WebSocket/HTTP underneath, owned by the SDK, not this module) and are surfaced as `store.setTaskCallback` invocations. + +```mermaid +flowchart LR + SDK[("@webex/contact-center SDK")] -->|emits task events| Store[("@webex/cc-store (MobX)")] + Store -->|observable: currentTask / incomingTask / taskList| Widget[Widget shell (observer + ErrorBoundary)] + Widget -->|hook input props| Hook[helper.ts hook] + Hook -->|view state + actions| Component[cc-components presentational] + Component -->|user action| Hook + Hook -->|task.* / cc.* SDK calls| SDK + Hook -->|setTaskCallback EVENT, fn, interactionId| Store + Store -->|invokes registered callback| Hook + Hook -->|on* callbacks| Consumer[Host app] + Hook -->|getControlsVisibility / timer utils| Utils[Utils/*] +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Offer → accept / decline (IncomingTask) + RONA | Incoming task accept/decline | RONA timeout path; SDK reject caught+logged; missing interactionId early-return | +| Hold / resume / record / mute / end (CallControl) | Active-call controls | Hold/resume/record/mute SDK rejection logged; mute reverts on failure; recording event subscription/cleanup mismatch noted | +| Consult / transfer / conference (CallControl) | Consult & transfer | Queue-consult flag rollback on error; `transferCall` re-throws; conference vs 1:1 branch | +| Wrap-up (manual + auto) | Wrap-up | Auto-wrap-up countdown + cancel; wrapup SDK rejection logged; next-task promotion | +| Outbound dial (OutdialCall) | Outdial | Empty-destination alert+abort; missing ANI id throws; SDK reject logged | +| Task list select / accept / decline (TaskList) | Task list actions | Per-task accept/decline reject logged; selection updates currentTask | + +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant C as IncomingTaskComponent + participant H as useIncomingTask + participant S as cc-store + participant SDK as ITask SDK + Note over H,S: useEffect registers TASK_ASSIGNED/CONSULT_ACCEPTED→onAccepted,
TASK_END/REJECT/CONSULT_END→onRejected (keyed by interactionId) + A->>C: click Accept + C->>H: accept() + alt no interactionId + H-->>C: early return (no SDK call) + else + H->>SDK: incomingTask.accept() + SDK-->>S: emits TASK_ASSIGNED + S->>H: registered callback → onAccepted({task}) + SDK--xH: .catch → logError (no throw) + end + Note over A,C: RONA timeout (timer in component) → reject() + A->>C: click Decline / timer expires + C->>H: reject() + H->>SDK: incomingTask.decline() + SDK-->>S: emits TASK_REJECT / TASK_END + S->>H: callback → onRejected({task}) +``` + +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant C as CallControlComponent + participant H as useCallControl + participant S as cc-store + participant SDK as ITask SDK + Note over H,S: useEffect subscribes TASK_HOLD/RESUME/END/AGENT_WRAPPEDUP/
TASK_RECORDING_PAUSED/RESUMED + A->>C: Hold + C->>H: toggleHold(true) + H->>SDK: currentTask.hold() + SDK-->>S: TASK_HOLD + S->>H: holdCallback → onHoldResume({isHeld:true,task}) + SDK--xH: hold rejection → logger.error (swallowed) + A->>C: Mute + C->>H: toggleMute() + alt controlVisibility.muteUnmute == false + H-->>C: warn + no-op + else + H->>SDK: await currentTask.toggleMute() + SDK-->>H: success → store.setIsMuted(intended); onToggleMute(intended) + SDK--xH: failure → onToggleMute(previous isMuted) + end + A->>C: Record toggle + C->>H: toggleRecording() + alt isRecording + H->>SDK: currentTask.pauseRecording() + SDK-->>S: TASK_RECORDING_PAUSED → isRecording=false; onRecordingToggle + else + H->>SDK: currentTask.resumeRecording({autoResumed:false}) + SDK-->>S: TASK_RECORDING_RESUMED → isRecording=true + end + A->>C: End + C->>H: endCall() + H->>SDK: currentTask.end() + SDK-->>S: TASK_END → endCallCallback → onEnd({task}) +``` + +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant C as CallControlComponent + participant H as useCallControl + participant S as cc-store + participant SDK as ITask SDK + A->>C: Consult (select dest) + C->>H: consultCall(dest, type, allowParticipantsToInteract) + alt type == queue + H->>S: setIsQueueConsultInProgress(true); setCurrentConsultQueueId(dest) + end + H->>SDK: currentTask.consult({to, destinationType, holdParticipants:!allow}) + alt success + SDK-->>H: resolved + H->>S: clear queue-consult flags + else error + SDK--xH: rejected + H->>S: clear queue-consult flags (rollback) + H-->>C: throw (caller handles) + end + A->>C: Complete transfer + C->>H: consultTransfer() + alt currentTask.data.isConferenceInProgress + H->>SDK: currentTask.transferConference() + else + H->>SDK: currentTask.consultTransfer() + end + Note over H,SDK: Blind transfer: transferCall(to,type) → currentTask.transfer(...) ; on error RE-THROWS + A->>C: End consult + C->>H: endConsultCall() + H->>SDK: currentTask.endConsult({isConsult:true, taskId, queueId?}) + SDK--xH: error → logError + throw +``` + +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant C as CallControlComponent + participant H as useCallControl + participant S as cc-store + participant SDK as ITask SDK + Note over H: auto-wrapup useEffect: if currentTask.autoWrapup && controlVisibility.wrapup
→ setInterval 1s counting secondsUntilAutoWrapup down from getTimeLeftSeconds() + alt manual wrap-up + A->>C: select wrap-up code + C->>H: wrapupCall(reason, auxCodeId) + H->>SDK: currentTask.wrapup({wrapUpReason, auxCodeId}) + SDK-->>H: resolved + H->>S: setCurrentTask(firstRemaining); setState(ENGAGED) + SDK--xH: rejected → logError (swallowed) + else auto wrap-up cancel + A->>C: Cancel + C->>H: cancelAutoWrapup() + H->>SDK: currentTask.cancelAutoWrapupTimer() + end +``` + +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant C as OutdialCallComponent + participant H as useOutdialCall + participant S as cc-store + participant SDK as cc SDK + C->>H: getOutdialANIEntries() + alt no agentConfig.outdialANIId + H-->>C: throw Error("No OutdialANI Id received.") + else + H->>SDK: cc.getOutdialAniEntries({outdialANI}) + SDK-->>C: ANI entries + end + A->>C: enter number + dial + C->>H: startOutdial(destination, origin?) + alt empty/whitespace destination + H-->>A: alert("Destination number is required..."); abort + else + H->>SDK: cc.startOutdial(...[destination, origin?]) + SDK-->>H: resolved → logger.info + SDK--xH: rejected → logger.error (swallowed) + end + Note over H: outdial gated: isTelephonyTaskActive true if any taskList item mediaType==telephony +``` + +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant C as TaskListComponent + participant H as useTaskList + participant S as cc-store + participant SDK as ITask SDK + Note over H,S: useEffect wires setTaskAssigned/Rejected/Selected only if matching on* prop given + A->>C: Accept task + C->>H: acceptTask(task) + H->>SDK: task.accept() + SDK--xH: reject → logError (swallowed) + A->>C: Decline task + C->>H: declineTask(task) + H->>SDK: task.decline() + A->>C: Click task (select) + C->>H: onTaskSelect(task) + H->>S: setCurrentTask(task, true) + S-->>C: currentTask updated → CallControl re-renders for new task + S->>H: setTaskSelected callback → onTaskSelected({task, isClicked}) +``` + +## Class / Component Relationships +```mermaid +classDiagram + class WidgetShell { + <> + reads store fields + calls hook + renders cc-components + } + class useIncomingTask + class useTaskList + class useCallControl + class useOutdialCall + class useRealTimeTranscript + class TaskUtils { + getControlsVisibility() + isCampaignPreviewTask() + isUnacceptedCampaignPreview() + findHoldTimestamp() + } + class TimerUtils { + calculateStateTimerData() + calculateConsultTimerData() + } + class useHoldTimer { + Web Worker + } + class Store { + <> + currentTask / incomingTask / taskList + setTaskCallback / removeTaskCallback + } + class CcComponents { + <> + } + WidgetShell --> useIncomingTask + WidgetShell --> useTaskList + WidgetShell --> useCallControl + WidgetShell --> useOutdialCall + WidgetShell --> useRealTimeTranscript + WidgetShell --> CcComponents + useCallControl --> TaskUtils + useCallControl --> TimerUtils + useCallControl --> useHoldTimer + useIncomingTask --> Store + useTaskList --> Store + useCallControl --> Store + useOutdialCall --> Store + useRealTimeTranscript --> Store +``` +The six widget shells are siblings that each bind to exactly one hook and one presentational component. Only `useCallControl` composes the `Utils/*` helpers (`getControlsVisibility`, the timer utils, and `useHoldTimer`). All hooks depend on the shared `store` singleton for state and event wiring; none import the SDK directly. + +## Use Cases +- **UC-1 Accept an offered task (IncomingTask):** Agent → store sets `incomingTask` → widget renders card → Agent clicks Accept → `accept()` → `incomingTask.accept()` → `TASK_ASSIGNED` → `onAccepted`. Evidence: `src/helper.ts` (`useIncomingTask`), `tests/helper.ts` ("should call onAccepted if it is provided"). +- **UC-2 Decline / RONA timeout (IncomingTask):** Agent clicks Decline or RONA timer expires → `reject()` → `incomingTask.decline()` → `TASK_REJECT`/`TASK_END` → `onRejected`. Evidence: `src/helper.ts` (`useIncomingTask.reject`), `tests/helper.ts` ("should call onRejected if it is provided"). UI flow: countdown badge on the card; on timeout the card auto-dismisses. +- **UC-3 Hold / resume active call (CallControl):** Agent clicks Hold → `toggleHold(true)` → `currentTask.hold()` → `TASK_HOLD` → hold timer starts via `useHoldTimer`, `onHoldResume({isHeld:true})`. Evidence: `src/helper.ts`, `src/Utils/useHoldTimer.ts`, `tests/helper.ts` (hold/resume cases). UI flow: Hold button toggles to Resume; "Hold" elapsed timer shown. +- **UC-4 Toggle recording (CallControl):** Agent clicks record toggle → `toggleRecording()` → `pauseRecording()`/`resumeRecording()` → `TASK_RECORDING_PAUSED/RESUMED` → `isRecording` flips, `onRecordingToggle`. Evidence: `src/helper.ts`, `tests/helper.ts` (recording cases). +- **UC-5 Mute / unmute (CallControl):** Agent clicks mute → `toggleMute()` (gated by `controlVisibility.muteUnmute`) → `currentTask.toggleMute()` → `store.setIsMuted`, `onToggleMute`. Evidence: `src/helper.ts`, `tests/helper.ts` ("should successfully toggle mute…", "rapid toggleMute"). UI flow: shows error-safe revert on failure. +- **UC-6 Consult an agent/queue/EP-DN then transfer or conference (CallControl):** Agent opens consult modal → `consultCall(dest,type,allow)` → on completion `consultTransfer()` (or `transferConference` in conference) or `endConsultCall()`. Evidence: `src/helper.ts`, `tests/helper.ts` (consult/transfer/conference cases), `tests/utils/task-util.ts` (EP-DN end-button cases). UI flow: consult controls (switch/merge/end) appear; consult timer label `Consult Requested`/`Consulting`/`Consult on Hold`. +- **UC-7 Blind transfer (CallControl):** Agent picks destination → `transferCall(to,type)` → `currentTask.transfer(...)`; failure re-thrown to the modal. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call transferCall successfully"). +- **UC-8 Wrap up a call (CallControl):** Call ends → wrap-up codes shown → Agent selects code → `wrapupCall(reason, auxCodeId)` → `currentTask.wrapup` → next task promoted to `currentTask`, agent → ENGAGED. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call wrapupCall"). Auto-wrap-up: countdown shown with Cancel. UI flow: wrap-up dropdown + optional auto-wrap-up countdown. +- **UC-9 Place an outbound call (OutdialCall):** Agent enters number, selects ANI → dial → `startOutdial(destination, origin)`; empty number alerts and aborts; disabled while a telephony task is active. Evidence: `src/helper.ts` (`useOutdialCall`), `tests/OutdialCall/index.tsx`. UI flow: dialpad with validation, ANI dropdown, address book toggled by `isAddressBookEnabled`. +- **UC-10 Manage concurrent tasks (TaskList):** Agent sees all tasks → accept/decline per task or click to select → selection sets `currentTask` so CallControl follows. Evidence: `src/helper.ts` (`useTaskList`), `tests/helper.ts` (task-list cases), `tests/TaskList/index.tsx`. +- **UC-11 View live transcript (RealTimeTranscript):** As `store.realtimeTranscriptionData` updates for `currentTask`, lines are mapped to entries with normalized speaker/time. Evidence: `src/helper.ts` (`useRealTimeTranscript`), `tests/RealtimeTranscript/index.tsx`. + +## State Model +Widget-local state (held in `useCallControl` via `useState`, server/task data is NOT owned here): `isRecording`, `buddyAgents`, `loadingBuddyAgents`, `consultAgentName`, `startTimestamp`, `secondsUntilAutoWrapup`, `stateTimerLabel`/`stateTimerTimestamp`, `consultTimerLabel`/`consultTimerTimestamp`, `lastTargetType` (`TARGET_TYPE` agent/queue/entryPoint/dialNumber), `conferenceParticipants`. `useHoldTimer` holds `holdTime` and a `Worker` ref. The authoritative task lifecycle state lives on the `ITask` object in `store` (`currentTask`, `incomingTask`, `taskList`); widgets derive booleans from it via `getControlsVisibility` and the timer utils. Transitions are triggered by SDK events delivered through `store.setTaskCallback`. + +## Business Rules & Invariants +- A task with no `data.interactionId` must not have SDK accept/decline called on it — enforced in `useIncomingTask.accept/reject` (`src/helper.ts`). +- CallControl renders nothing unless there is a `currentTask` that is not an unaccepted campaign preview — enforced in `src/CallControl/index.tsx` and `src/CallControlCAD/index.tsx` via `isUnacceptedCampaignPreview` (`src/Utils/task-util.ts`). Acceptance is tracked by `store.acceptedCampaignIds`, not `participants.hasJoined`. +- Queue-consult bookkeeping (`isQueueConsultInProgress`, `currentConsultQueueId`) must be cleared on both success and error of `consultCall` so `endConsultCall` never sends a stale `queueId` — enforced in `useCallControl.consultCall/endConsultCall`. +- Multiparty conference is capped at `MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE = 7` — enforced in `getConsultButtonVisibility` (`src/Utils/task-util.ts`). +- Mute state is only committed (`store.setIsMuted`, `onToggleMute`) after the SDK `toggleMute()` resolves — enforced in `useCallControl.toggleMute`. +- `getControlsVisibility` must always return a complete control set (safe all-hidden defaults on error) and never throw into render — enforced by its try/catch (`src/Utils/task-util.ts`). + +## State Machine +States are derived from the live `ITask` (`data.interaction.state`, participant flags, consult/conference/hold status); this module observes and acts on transitions rather than owning them. +```mermaid +stateDiagram-v2 + [*] --> Offered: store sets incomingTask + Offered --> Active: accept() → TASK_ASSIGNED + Offered --> Ended: decline()/RONA timeout → TASK_REJECT/TASK_END + Active --> Held: toggleHold(true) → TASK_HOLD + Held --> Active: toggleHold(false) → TASK_RESUME + Active --> Consulting: consultCall() → consult media added + Consulting --> ConsultHeld: switchToConsult/hold consult leg + ConsultHeld --> Consulting: switchToMainCall/resume + Consulting --> Conference: consultConference() + Conference --> Active: exitConference() + Consulting --> Ended: consultTransfer()/transferConference() → TASK_END + Active --> Ended: endCall() → TASK_END + Active --> Transferred: transferCall() → TASK_END + Ended --> WrapUp: wrapUpRequired (controlVisibility.wrapup) + WrapUp --> WrapUp: auto-wrap-up countdown / cancelAutoWrapup + WrapUp --> [*]: wrapupCall() → next task promoted / agent ENGAGED + Transferred --> [*] +``` + +## UI Flow +- **IncomingTask:** task card with caller/queue/media info, RONA countdown badge, Accept/Decline buttons. Empty state = no card when `incomingTask` is null. Error state = empty fragment via ErrorBoundary. +- **TaskList:** list of task cards; selected task highlighted (mirrors `currentTask`); per-task Accept/Decline; empty list renders nothing. Campaign-preview tasks render a `CampaignTask` when `hasCampaignPreviewEnabled` (default true). +- **CallControl / CallControlCAD:** rows of controls (hold/resume, mute, record, transfer, consult, conference, end, wrap-up), consult sub-controls (switch/merge/end consult), wrap-up dropdown, auto-wrap-up countdown, hold/consult/state timers. Hidden entirely when no `currentTask` or unaccepted campaign preview. CAD variant adds `callControlClassName` / `callControlConsultClassName` styling hooks. Disabled/enabled state of every button comes from `getControlsVisibility`. +- **OutdialCall:** numeric dialpad with E.164/special-char validation, ANI selector dropdown, optional address book (`isAddressBookEnabled`), dial button disabled on invalid/empty input or while a telephony task is active. +- **RealTimeTranscript:** scrolling transcript with normalized speaker ("You"/"Customer") and `HH:MM` display time; renders supplied `liveTranscriptEntries` when no live data for the current task. + +## Error Handling & Failure Modes +| Condition | Signal (error/code/result) | Caller recovery | +|---|---|---| +| `accept()`/`reject()` with no `interactionId` | Silent early return (no SDK call) | None needed; no-op | +| SDK rejection on accept/decline/hold/resume/end/wrapup/recording | `logger.error(...)`; promise rejection swallowed | None surfaced; consumer relies on subsequent SDK state events | +| `toggleMute` SDK failure | `onToggleMute` fires with the *previous* `isMuted`; store not updated | UI stays consistent with actual mute state | +| `toggleMute` when control hidden | `logger.warn` + no-op | None | +| `consultCall`/`endConsultCall`/`consultTransfer`/`transferCall`/`consultConference`/`switch*`/`exitConference` failure | `logError` then **re-throws** | Calling modal/component must catch and surface to the agent | +| Queue `consultCall` failure | Queue-consult flags rolled back, then re-throw | Caller handles; no stale `queueId` | +| `startOutdial` empty destination | `alert(...)` + abort (no SDK call) | Agent re-enters a valid number | +| `startOutdial` SDK failure | `logger.error` (swallowed) | Agent retries | +| `getOutdialANIEntries` missing `outdialANIId` | `throw Error('No OutdialANI Id received.')` | Caller catches; ANI dropdown empty | +| `getAddressBookEntries`/`getEntryPoints`/`getQueuesFetcher` failure (useCallControl) | `logger.error` + returns `{data:[], meta:{page:0,totalPages:0}}` | Empty paginated result rendered | +| `getControlsVisibility` internal error | try/catch returns all-hidden safe defaults | All controls hidden, no crash | +| Any widget render crash | ErrorBoundary renders empty fragment + `store.onErrorCallback(name, error)` if set | Host notified; widget removed from view | + +## Pitfalls +- **Recording event subscription/cleanup mismatch:** `useCallControl` subscribes to `TASK_RECORDING_PAUSED`/`TASK_RECORDING_RESUMED` but the cleanup removes `CONTACT_RECORDING_PAUSED`/`CONTACT_RECORDING_RESUMED` (`src/helper.ts` recording `useEffect`). Both names exist in `store.types.ts`, so the subscribed callbacks are not removed by name on teardown — a latent listener-leak/duplicate-callback edge. Verify against `packages/contact-center/store/src/store.types.ts` before changing. +- **Callback identity in cleanup (IncomingTask):** registration uses inline closures for `TASK_ASSIGNED` but `removeTaskCallback` is called with `taskAssignCallback`; the references differ, so removal may not match registration. Confirm `store.removeTaskCallback` matching semantics before relying on cleanup. +- **Migration docs are aspirational, not current:** archived docs / `ai-docs/migration/*.md` describe `task.uiControls`, renamed events (`TASK_WRAPPEDUP`, `TASK_CONSULT_CREATED`), and deletion of `getControlsVisibility`. None of this is in the code today — current code computes visibility locally and the store still emits `AGENT_WRAPPEDUP`/`CONTACT_RECORDING_*`. Do not implement against the migration docs as if they were live. +- **Second-vs-millisecond timestamps:** `useHoldTimer` treats values `< 1e10` as seconds and multiplies by 1000; passing an already-ms small value would mis-scale. `findHoldTimestamp` returns `0` as a valid hold timestamp (not null) — guard with explicit null checks. +- **`transferCall`/consult ops re-throw while hold/end/wrapup swallow:** inconsistent error contract within the same hook. Callers of consult/transfer must wrap in try/catch; callers of hold/end/wrapup must not expect a throw. +- **Campaign-preview gating ignores `participants.hasJoined`:** use `store.acceptedCampaignIds` (`isUnacceptedCampaignPreview`) — `hasJoined` can be set by `CampaignContactUpdated` even when the agent only skipped the preview. +- **`conferenceEnabled` defaulting happens in the shell**, not the hook (`?? true`). Reading the prop directly in the hook without the default would see `undefined`. + +## Module Do's / Don'ts +- DO put every SDK call and `store.setTaskCallback` registration in `helper.ts`; keep widget shells to store-selection + render only. +- DO read button visibility/enablement from `getControlsVisibility` output (`controlVisibility`), not from ad-hoc device/feature checks in components. +- DO clear queue-consult flags on both success and failure paths of `consultCall`. +- DON'T import the SDK (`@webex/contact-center`) directly in a widget shell — go through `store`. +- DON'T derive hold/consult state from button `isEnabled` flags; use the task object + `getConsultStatus`/`findHoldStatus`. +- DON'T add new task-event subscriptions without matching the exact event name in both `setTaskCallback` and the cleanup `removeTaskCallback`. + +## Host Integration & Theming +These widgets are published through `@webex/cc-widgets` as r2wc custom elements (e.g. ``); peer `react ^18`. They require an initialized `@webex/cc-store` singleton (SDK connected, agent logged in) before mount — `currentTask`/`incomingTask`/`taskList`/`cc`/`logger` must be populated by the store. Presentational styling comes from `@webex/cc-components`; `CallControlCAD` exposes `callControlClassName`/`callControlConsultClassName` for host CSS overrides. The host supplies `store.onErrorCallback` to receive widget-crash notifications. + +## Test-Case Strategy (module) +Tests are split between widget-shell render tests (each `tests//index.tsx` asserts the hook is called with the right props, the presentational component receives merged output, and the ErrorBoundary renders empty + invokes/handles-missing `onErrorCallback`) and exhaustive hook/util logic tests. `tests/helper.ts` is the large behavioral suite covering accept/decline, hold/resume, end, recording pause/resume (positive + SDK-failure negative cases), mute (including rapid toggles and failure revert), wrap-up + auto-wrap-up cancel, consult/transfer/conference, queue-consult flags, buddy-agent loading, and consulting-agent extraction. `tests/utils/task-util.ts` matrices `getControlsVisibility` across device types (BROWSER/AGENT_DN/EXTENSION) and media types (telephony/chat/email) plus EP-DN end-button rules and the error→safe-defaults path. `tests/utils/timer-utils.test.ts` and `tests/utils/useHoldTimer.test.ts` cover label priority, consult-on-hold, null-task defaults, and consult-vs-main hold prioritization. Edge cases asserted: missing interaction/participants, missing currentTask, error logging in every callback. Gaps: no unit test for the OutdialCall empty-destination alert, the `getOutdialANIEntries` missing-ANI-id throw, or the CallControl unaccepted-campaign-preview early return. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `TASK-R-001` accept guarded + error-safe | `tests/helper.ts` ("should return if there is no taskId for incoming task", "should handle errors when accepting a task") | none | +| `TASK-R-002` reject / RONA | `tests/helper.ts` ("should call onRejected if it is provided", "should handle errors when declining a task") | RONA timer UI tested in cc-components, not here | +| `TASK-R-003` incoming event wiring | `tests/helper.ts` ("should setup event listeners for the incoming call") | none | +| `TASK-R-004` task-list accept/decline/select | `tests/helper.ts` (task-list accept/decline/select cases) | none | +| `TASK-R-005` conditional store-callback wiring | `tests/helper.ts` ("should not call onTaskAccepted if it is not provided") | none | +| `TASK-R-006` hold/resume | `tests/helper.ts` ("should call onHoldResume with hold=true/false…", "should log an error if hold/resume fails") | none | +| `TASK-R-007` recording toggle | `tests/helper.ts` (pause/resume + failure cases) | No test asserts the PAUSED/RESUMED vs CONTACT_* cleanup mismatch | +| `TASK-R-008` mute | `tests/helper.ts` ("toggle mute…", "rapid toggleMute", "onToggleMute on error") | none | +| `TASK-R-009` wrap-up + next-task promotion | `tests/helper.ts` ("should call wrapupCall", "…if wrapup fails") | none | +| `TASK-R-010` auto-wrap-up + cancel | `tests/helper.ts` ("initialize secondsUntilAutoWrapup…", "cancelAutoWrapup…") | none | +| `TASK-R-011` consult + queue flags | `tests/helper.ts` ("consultCall…", "endConsultCall with queue parameters…") | none | +| `TASK-R-012` consult vs conference transfer | `tests/helper.ts` ("consultTransfer successfully", "…when currentTask data is missing") | none | +| `TASK-R-013` blind transfer re-throw | `tests/helper.ts` ("transferCall successfully") | No explicit re-throw assertion | +| `TASK-R-014` switch/exit conference legs | `tests/helper.ts` (consult/conference cases) | Thin coverage of switch-to-main/consult media targeting | +| `TASK-R-015` control visibility matrix | `tests/utils/task-util.ts` (device/media + safe-defaults cases) | none | +| `TASK-R-016` EP-DN end-button rules | `tests/utils/task-util.ts` (EP-DN + variant detection cases) | none | +| `TASK-R-017` hold timer | `tests/utils/useHoldTimer.test.ts` (consult priority, sec→ms, reset) | none | +| `TASK-R-018` timer labels | `tests/utils/timer-utils.test.ts` (wrap-up priority, consult-on-hold/requested) | none | +| `TASK-R-019` outdial validation | `tests/OutdialCall/index.tsx` (render/address-book) | No empty-destination alert test | +| `TASK-R-020` ANI / telephony gating | `tests/OutdialCall/index.tsx` | No missing-ANI-id throw test | +| `TASK-R-021` transcript mapping | `tests/RealtimeTranscript/index.tsx` | none | +| `TASK-R-022` ErrorBoundary isolation | each `tests//index.tsx` (ErrorBoundary + onErrorCallback-undefined) | none | +| `TASK-R-023` campaign-preview gating | None found | No test for unaccepted-campaign-preview early return | + +## Traceability +- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md b/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md new file mode 100644 index 000000000..19b71d248 --- /dev/null +++ b/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md @@ -0,0 +1,227 @@ +# test-fixtures — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata + +| Field | Value | +|---|---| +| Module id | `test-fixtures` | +| Source path(s) | `packages/contact-center/test-fixtures/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| Validation status | not-run | + +Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with `<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc metadata. + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the repository policy says history is reliable, and must include the commit hash. If evidence is missing or conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register + +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/test-fixtures/ai-docs/AGENTS.md` | overview / API / examples | reconciled | Overview, Public Surface, Use Cases. Export list reconciled against `src/fixtures.ts`: archived doc omitted `makeMockTask`, `mockCampaignCpd`, `mockCampaignTask`, `makeMockCampaignTask`, `mockCallAssociatedData`; archived `mockIncomingTaskData`/`mockTaskData`/`mockOutdialCallProps`/`mockAniEntries`/`mockCCWithAni` confirmed present. | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/test-fixtures/ai-docs/ARCHITECTURE.md` | architecture / tests | reconciled | Design Overview, Folder Structure, Pitfalls. Conflict: archived `mockCC` listed task methods (`accept`, `hold`, …) and proxies (`AgentProxy`, `DiagnosticsProxy`, …) that the real `mockCC` (`src/fixtures.ts`) does not define — those live on `mockTask` and the `LoggerProxy`/`taskManager` only. Archived `mockTask.data` shape (flat `origin`/`destination`/`status`) does not match real nested `interaction` shape — corrected from source. | + +## Overview +`test-fixtures` (`@webex/test-fixtures`) is a test-only utility package. It exports pre-built mock objects and small factory functions that other contact-center packages import inside their Jest unit tests, so widgets can be rendered and exercised without a live Contact Center SDK connection or backend. It owns no runtime behavior, holds no state, and ships nothing into the browser bundle of any consumer; consuming packages list it as a dev dependency only. + +The package is structured as a flat set of fixture modules under `src/`, each one re-exported by the barrel `src/index.ts`. `src/fixtures.ts` holds the core SDK-shaped mocks (`mockCC`, `mockProfile`, `mockTask`, queues, agents, address book, campaign-preview tasks). `src/incomingTaskFixtures.ts` and `src/taskListFixtures.ts` hold plain UI-data records keyed by scenario for the task widgets. `src/components/task/outdialCallFixtures.ts` composes `mockCC` into outdial-specific mocks. + +The load-bearing contract of this module is structural: each exported mock is typed against the real SDK / store / cc-components type (e.g. `mockCC: IContactCenter`, `mockProfile: Profile`, `mockTask: ITask`) so that when a consuming test passes a fixture into production code, the shape matches what production code expects at compile time. A maintainer changing a fixture should start at `src/fixtures.ts` and keep the declared types intact. + +## Purpose / Responsibility +Provides shared, type-checked mock SDK instances, mock profile/task/queue/agent data, and component-prop fixtures consumed by other packages' Jest tests; it does NOT provide runtime behavior, test runners, or shared test-render helpers. + +## Stack +TypeScript 5.6.3. No framework runtime — fixtures are plain objects whose methods are Jest mock functions (`jest.fn()`), so the package assumes a Jest global is present in the consumer's test environment. Built with `tsc` (type output) and Webpack 5 + Babel (`build:src`). No datastore, no messaging. `deploy:npm` is intentionally a no-op (`package.json`). + +## Folder / Package Structure +``` +test-fixtures/src/ +├── index.ts # Barrel: re-exports all four fixture modules +├── fixtures.ts # Core SDK-shaped mocks: mockCC, mockProfile, mockTask, queues, agents, address book, campaign tasks +├── incomingTaskFixtures.ts # mockIncomingTaskData — incoming-task UI data by channel scenario +├── taskListFixtures.ts # mockTaskData — task-list UI data by scenario (active/incoming/action/selection) +└── components/task/ + └── outdialCallFixtures.ts # Outdial mocks composed from mockCC: mockOutdialCallProps, mockAniEntries, mockCCWithAni +``` + +## Key Files (source of truth) + +| File | Holds | +|---|---| +| `packages/contact-center/test-fixtures/src/index.ts` | The public export barrel — the authoritative list of what consumers may import. | +| `packages/contact-center/test-fixtures/src/fixtures.ts` | Core fixture values and their type annotations (`IContactCenter`, `Profile`, `ITask`, etc.). Never re-infer these shapes elsewhere. | +| `packages/contact-center/test-fixtures/src/incomingTaskFixtures.ts` | `mockIncomingTaskData` and its `MEDIA_CHANNEL` source import. | +| `packages/contact-center/test-fixtures/src/taskListFixtures.ts` | `mockTaskData` and its `MEDIA_CHANNEL` source import. | +| `packages/contact-center/test-fixtures/src/components/task/outdialCallFixtures.ts` | Outdial fixtures derived from `mockCC`. | +| `packages/contact-center/test-fixtures/package.json` | Dependency list and the `deploy:npm` no-op. | + +## Public Surface +Internal Surface — consumed only by other packages' Jest tests in this monorepo. There is no network/event/CLI contract; the contract is the set of TypeScript exports below, all re-exported through `src/index.ts`. Each is summarized here — read the source file for the exact object shape. + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `test-fixtures.mockCC` | SDK export | `mockCC: IContactCenter` | Mock SDK instance; methods are `jest.fn()` so tests can spy/override | Shape must track `IContactCenter`; removing a mocked method may break consumer tests | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockProfile` | SDK export | `mockProfile: Profile` | Full agent profile (teams, idle/wrapup codes, dial plan, flags) | Track `Profile`; additive fields safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockTask` | SDK export | `mockTask: ITask` | Connected telephony task with nested `interaction`; methods are `jest.fn()` | Track `ITask` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.makeMockTask` | SDK export | `makeMockTask(overrides?): ITask` | Factory producing a fresh task with deep `data`/`interaction` overrides and fresh `jest.fn()`s | Override shape `MakeMockTaskOverrides` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockCampaignTask` | SDK export | `mockCampaignTask: ITask` | Campaign-preview-shaped task (CPD + outbound details) | Track `ITask` + campaign CPD keys | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.makeMockCampaignTask` | SDK export | `makeMockCampaignTask(overrides?): ITask` | Factory for campaign-preview task with `cpd`/`interaction`/`data` overrides | Override shape `IMakeMockCampaignTaskOverrides` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockCampaignCpd` | data export | `mockCampaignCpd: Record` | Default campaign-preview call-processing-detail values | Additive keys safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockQueueDetails` | data export | `mockQueueDetails` | Two fully-populated queue config objects for transfer/queue tests | Additive fields safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockAgents` | data export | `mockAgents` | Buddy-agent list for transfer/consult tests | Additive fields safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockEntryPointsResponse` | data export | `mockEntryPointsResponse: EntryPointListResponse` | Outdial entry-points response | Track `EntryPointListResponse` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockAddressBookEntriesResponse` | data export | `mockAddressBookEntriesResponse: AddressBookEntriesResponse` | Address-book entries response | Track `AddressBookEntriesResponse` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.makeMockAddressBook` | SDK export | `makeMockAddressBook(getEntriesMock?): AddressBook` | Factory for an `AddressBook` mock; default `getEntries` resolves the entries response | Track `AddressBook` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockCallAssociatedData` | data export | `mockCallAssociatedData` | Call-associated-data variants (global, viewable/hidden, secure) | Additive keys safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockIncomingTaskData` | data export | `mockIncomingTaskData` | Incoming-task UI data keyed `webRTC`/`extension`/`social`/`chat` | Additive scenario keys safe | `src/incomingTaskFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockTaskData` | data export | `mockTaskData` | Task-list UI data keyed `active`/`incoming`/`action`/`selection` | Additive scenario keys safe | `src/taskListFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockOutdialCallProps` | data export | `mockOutdialCallProps` | `mockCC` spread + `startOutdial`/`getOutdialANIEntries` jest mocks | Spread of `mockCC` | `src/components/task/outdialCallFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockAniEntries` | data export | `mockAniEntries` | Outdial ANI entry list | Additive fields safe | `src/components/task/outdialCallFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockCCWithAni` | data export | `mockCCWithAni` | `mockCC` + `agentConfig.outdialANIId` + ANI-resolving `getOutdialAniEntries` | Spread of `mockCC` | `src/components/task/outdialCallFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | + +Compatibility notes: +- Adding a new fixture export or an additive field on existing data fixtures is non-breaking. Removing or renaming an export, or removing a method on `mockCC`/`mockTask`, can break consumer test files that reference it — grep consumers before changing. +- `mockAddressBook` and `mockQueuesResponse` are defined in `src/fixtures.ts` but NOT exported; they are internal wiring for `mockCC` only. + +## Requires (dependencies) +- `@webex/cc-store` (`workspace:*`) — imports the `IContactCenter` type used to type `mockCC` (`src/fixtures.ts`). +- `@webex/contact-center` (SDK) — imports types `ITask`, `Interaction`, `Profile`, `TaskData`, `TaskResponse`, `AddressBook`, `EntryPointListResponse`, `AddressBookEntriesResponse`, `ContactServiceQueuesResponse` (`src/fixtures.ts`). Type-only; resolved transitively via `@webex/cc-store` (not a direct dependency in `package.json`). +- `@webex/cc-components` — `incomingTaskFixtures.ts` and `taskListFixtures.ts` import the `MEDIA_CHANNEL` enum via relative path `../../cc-components/src/components/task/task.types` (a cross-package source-relative import, not a `package.json` dependency). +- Jest (peer/ambient) — fixtures call `jest.fn()`; a Jest global must exist in the consumer's test runtime. Not declared in `package.json`. +- `typescript` 5.6.3 (`package.json`). + +## Requirements + +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `test-fixtures-R-001` | Each exported mock is annotated with its real SDK/store type (`mockCC: IContactCenter`, `mockProfile: Profile`, `mockTask: ITask`, `mockEntryPointsResponse: EntryPointListResponse`, `mockAddressBookEntriesResponse: AddressBookEntriesResponse`, `mockQueuesResponse: ContactServiceQueuesResponse`) so consumer code type-checks against production shapes. | Fixtures exist to let tests substitute real SDK shapes; a drifted shape would let tests pass while production code breaks. | `src/fixtures.ts` (type annotations on each declaration) | No package-local tests; consumed by sibling-package tests. | Gap: no in-package compile assertion that fixtures stay in sync beyond `tsc`; relies on cross-package build. | PRESENT | +| `test-fixtures-R-002` | All SDK methods on `mockCC` and `mockTask` are `jest.fn()`s so consumers can spy, assert calls, and override return values. | Tests need to observe and control SDK interactions without a backend. | `src/fixtures.ts` (`mockCC`, `mockTask` method definitions) | Archived usage examples (spy/override) in `_archive/.../AGENTS.md` | none | PRESENT | +| `test-fixtures-R-003` | `makeMockTask` and `makeMockCampaignTask` return a fresh object with new `jest.fn()` method instances per call, applying deep `data`/`interaction` (and `cpd`) overrides. | Reusing a shared mutated fixture causes cross-test bleed; factories give isolation and scenario shaping. | `src/fixtures.ts` (`makeMockTask`, `makeMockCampaignTask`) | none found | Gap: no test verifying fresh-instance isolation. | PRESENT | +| `test-fixtures-R-004` | `makeMockAddressBook` returns an `AddressBook` whose `getEntries` defaults to a `jest.fn()` resolving `mockAddressBookEntriesResponse`, overridable via parameter. | Address-book tests need a controllable async data source. | `src/fixtures.ts` (`makeMockAddressBook`) | Archived example in `_archive/.../AGENTS.md` (search address book) | none | PRESENT | +| `test-fixtures-R-005` | `mockIncomingTaskData` and `mockTaskData` expose UI-data variants keyed by scenario (incoming: `webRTC`/`extension`/`social`/`chat`; list: `active`/`incoming`/`action`/`selection`) using the shared `MEDIA_CHANNEL` enum. | Task widget tests render against named, channel-correct scenarios rather than ad-hoc literals. | `src/incomingTaskFixtures.ts`, `src/taskListFixtures.ts` | none found | none | PRESENT | +| `test-fixtures-R-006` | Outdial fixtures (`mockOutdialCallProps`, `mockCCWithAni`) are composed by spreading `mockCC` and adding outdial-specific jest mocks/config, keeping a single source of SDK shape. | Avoids a divergent second SDK mock; outdial tests inherit the canonical `mockCC`. | `src/components/task/outdialCallFixtures.ts` | none found | none | PRESENT | +| `test-fixtures-R-007` | Every fixture and factory is re-exported through the barrel `src/index.ts`; non-barrelled internals (`mockAddressBook`, `mockQueuesResponse`) are not part of the public surface. | Consumers import from the package root; the barrel is the stability boundary. | `src/index.ts`, `src/fixtures.ts` (export list) | none found | none | PRESENT | + +Do not record raw data/schema inventory as requirements. The per-field contents of each mock are descriptive data in `src/`, not behavioral requirements. + +## Design Overview +The module is a pass-through fixture library: no control flow, no async orchestration, no state. Design choices are all about isolation and shape-fidelity. + +Shape fidelity is achieved by importing the real SDK/store types and annotating each fixture (`const mockCC: IContactCenter = {…}`). `tsc` then fails the build if a fixture drifts from the production type, which is the package's only automated guard. Where the SDK type is broader than what a fixture can fully populate, the code uses a targeted cast (`as unknown as TaskData`, `as {} as AddressBook`) — a deliberate, localized escape hatch documented in Pitfalls. + +Isolation is offered two ways. Static fixtures (`mockTask`, `mockCC`) are shared singletons — cheap but mutable, so tests that mutate must clone. Factory functions (`makeMockTask`, `makeMockCampaignTask`, `makeMockAddressBook`) return fresh objects with brand-new `jest.fn()`s each call, which is the safe path for tests that mutate or assert call counts. + +Composition keeps the SDK mock single-sourced: outdial fixtures spread `mockCC` rather than redeclaring it, so a change to `mockCC` propagates. The same principle is why `mockQueuesResponse` is derived by mapping `mockQueueDetails` instead of being hand-written twice. + +## Data Flow +In-process, compile-time only. A consumer test file imports a fixture from `@webex/test-fixtures`; the fixture (a plain object, often with `jest.fn()` methods) is either passed as a prop/argument into production code under test or used to build a `jest.mock('@webex/cc-store', …)` factory. No network, queue, or wire transport is involved. + +```mermaid +flowchart LR + Types["@webex/cc-store types
@webex/contact-center SDK types
cc-components MEDIA_CHANNEL"] -->|type-check| Fixtures["test-fixtures/src/*
(mockCC, mockTask, makeMock*, mockTaskData…)"] + Fixtures -->|re-export| Barrel["src/index.ts"] + Barrel -->|import in test| ConsumerTest["consuming package test
(station-login / task / user-state …)"] + ConsumerTest -->|prop / arg| Component["component under test"] + ConsumerTest -->|jest.mock factory| StoreMock["mocked @webex/cc-store"] + StoreMock --> Component +``` + +## Sequence Diagram(s) +Sequence coverage: this is a single-operation pass-through utility (import a fixture, use it in a test). The quality bar permits one diagram for a trivial pass-through module, so the import-and-use flow below covers the package; there are no async jobs, retries, or failure branches to diagram. + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Import fixture → use in consumer test | `Fixture use in a consumer test` | N/A — no runtime failure modes (compile-time `tsc` is the only check); `jest.fn()` rejection behavior is configured by the consumer, not this module. | + +```mermaid +sequenceDiagram + participant Test as Consumer test + participant Pkg as test-fixtures (index.ts) + participant Comp as Component under test + Test->>Pkg: import { mockTask, mockCC } + Pkg-->>Test: fixture objects (jest.fn() methods) + opt mutate / isolate + Test->>Pkg: makeMockTask({ data:{...} }) + Pkg-->>Test: fresh ITask + new jest.fn()s + end + Test->>Comp: render() + Test->>Test: assert(mockTask.hold) / spy +``` + +## Class / Component Relationships +```mermaid +classDiagram + class mockCC { + <> + LoggerProxy + taskManager + getBuddyAgents() jest.fn + getQueues() jest.fn + getEntryPoints() jest.fn + } + class mockTask { <> data: TaskData; hold/resume/end/wrapup: jest.fn } + class makeMockTask { <> (overrides) ITask } + class mockCampaignTask { <> } + class makeMockCampaignTask { <> (overrides) ITask } + class mockProfile { <> } + class makeMockAddressBook { <> (getEntriesMock) AddressBook } + class mockOutdialCallProps + class mockCCWithAni + + makeMockTask ..> mockTask : spreads + fresh jest.fn + makeMockCampaignTask ..> mockCampaignTask : spreads + overrides + mockCampaignTask ..> mockTask : spreads base + mockOutdialCallProps ..> mockCC : spreads + mockCCWithAni ..> mockCC : spreads + mockCC ..> makeMockAddressBook : addressBook field +``` +Static base fixtures (`mockCC`, `mockTask`, `mockProfile`) are the roots. Factories and derived fixtures all compose from those bases by object spread, so the type annotations on the bases govern the whole graph. There is no inheritance — composition only. + +## Use Cases +- **UC-1 Render a widget with a mock task:** test imports `mockTask` → passes it as a prop to a task component → asserts UI / spies on `mockTask.hold`. Outcome: widget rendered with no SDK connection. Evidence: `src/fixtures.ts` (`mockTask`), archived examples in `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/test-fixtures/ai-docs/AGENTS.md`. +- **UC-2 Mock the store with profile/SDK fixtures:** test builds a `jest.mock('@webex/cc-store', () => ({ cc: mockCC, teams: mockProfile.teams, … }))` factory → renders a widget that reads the store. Outcome: store-driven widget testable in isolation. Evidence: `src/fixtures.ts` (`mockCC`, `mockProfile`), archived `AGENTS.md`. +- **UC-3 Isolate a mutated task via factory:** test calls `makeMockTask({ data: { interaction: { state: 'hold' } } })` → gets a fresh task with new `jest.fn()`s → asserts without cross-test bleed. Outcome: isolated, scenario-shaped task. Evidence: `src/fixtures.ts` (`makeMockTask`). +- **UC-4 Test outdial flows:** test imports `mockCCWithAni` / `mockOutdialCallProps` / `mockAniEntries` → drives outdial component → asserts ANI handling. Outcome: outdial UI tested with configured ANI. Evidence: `src/components/task/outdialCallFixtures.ts`. +- **UC-5 Drive task widgets with scenario data:** test reads `mockTaskData.incoming.webrtcTelephony` or `mockIncomingTaskData.social` → feeds it to a task-list / incoming-task component. Outcome: channel-correct UI scenario. Evidence: `src/taskListFixtures.ts`, `src/incomingTaskFixtures.ts`. + +## Pitfalls +- **Shared static fixtures are mutable.** `mockTask`, `mockCC`, `mockProfile` are module singletons. A test that mutates `mockTask.data` or `mockCC.stationLogin.mockResolvedValue(…)` without `jest.clearAllMocks()` / cloning leaks into later tests (flaky-together, pass-alone). Use the `makeMock*` factories or spread-clone, and reset mocks in `beforeEach`. +- **`MEDIA_CHANNEL` is a source-relative cross-package import.** `incomingTaskFixtures.ts` / `taskListFixtures.ts` import from `../../cc-components/src/components/task/task.types`, not from a package entry point. Moving that file or the relative depth silently breaks the build; it also makes test-fixtures depend on cc-components source (not reflected in `package.json`). +- **Targeted type casts mask shape drift.** `mockTask.data` uses `as unknown as TaskData` and `makeMockAddressBook` uses `as {} as AddressBook`. These bypass `tsc` for those values, so a real SDK shape change to `TaskData`/`AddressBook` will NOT fail the build here — verify those fixtures manually when the SDK types change. +- **`mockCC` is not a full `IContactCenter` of task methods.** Task lifecycle methods (`hold`, `resume`, `wrapup`, etc.) live on `mockTask`, not `mockCC`. The archived ARCHITECTURE doc incorrectly listed them and several proxies on `mockCC`; do not rely on that. `mockCC` exposes `LoggerProxy`, `taskManager`, and the listed `getX`/state/preview methods only. +- **Jest global is assumed, not declared.** Fixtures call `jest.fn()` at module load. Importing this package outside a Jest environment throws `jest is not defined`. It is test-only by design (`deploy:npm` is a no-op). + +## Module Do's / Don'ts +- DO: keep every fixture annotated with its real SDK/store type so `tsc` catches drift (`src/fixtures.ts`). +- DO: add new mocks to the `src/index.ts` barrel and to the export list in `src/fixtures.ts`. +- DO: prefer `makeMock*` factories when a test mutates state or asserts call counts. +- DON'T: import this package from non-test (runtime) code — it calls `jest.fn()` at load. +- DON'T: redeclare a second SDK mock; spread `mockCC` like the outdial fixtures do. +- DON'T: remove or rename an export without grepping sibling-package tests first. + +## Export Stability +Published/consumed as `@webex/test-fixtures` (`workspace:*`), but `deploy:npm` is a deliberate no-op (`package.json`) — it is an internal monorepo dev dependency, not an npm artifact. Stability rules: adding an export or an additive field on a data fixture is a minor/non-breaking change; removing/renaming an export, or removing a mocked method on `mockCC`/`mockTask`, is breaking for consumer test files and must be done with a repo-wide grep of test imports. Type-declaration surface is emitted to `dist/types/` via `tsc`. + +## Test-Case Strategy (module) +The package ships no tests of its own (no `tests/` directory; confirmed by tree). Its correctness is enforced two ways: (1) `tsc` type-checking the typed fixtures against real SDK/store types during `yarn build:dev`, and (2) the sibling-package test suites that consume the fixtures — a fixture that breaks shape surfaces as a compile error in `task`/`station-login`/`user-state`/etc. tests. The cast-escaped fixtures (`TaskData`, `AddressBook`) and the factory isolation guarantee are the gaps a dedicated test would close. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `test-fixtures-R-001` (typed fixtures) | None in-package; enforced by `tsc` + consumer builds | No explicit type-assertion test; cast-escaped `TaskData`/`AddressBook` not covered | +| `test-fixtures-R-002` (jest.fn methods) | None in-package; exercised by consumer tests | No in-package assertion that methods are mocks | +| `test-fixtures-R-003` (factory freshness) | None found | Missing test that two `makeMockTask()` calls return independent `jest.fn()`s | +| `test-fixtures-R-004` (address book factory) | None found | Missing test for default-resolve + override | +| `test-fixtures-R-005` (scenario UI data) | None found | Consumed indirectly by task widget tests only | +| `test-fixtures-R-006` (outdial composition) | None found | Consumed by outdial widget tests only | +| `test-fixtures-R-007` (barrel surface) | None found | No test guarding the public export list | + +## Traceability +- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md b/packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md new file mode 100644 index 000000000..f66bc59af --- /dev/null +++ b/packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md @@ -0,0 +1,293 @@ +# ui-logging — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `ui-logging` | +| Source path(s) | `packages/contact-center/ui-logging/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `[NEEDS HUMAN INPUT]` / updated_at `2026-06-29` | +| Validation status | not-run | + +Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with +`<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc +metadata. + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source +evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish +truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the +repository policy says history is reliable, and must include the commit hash. If evidence is missing or +conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers +as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/ui-logging/ai-docs/AGENTS.md` | overview / API | migrated | Overview, Purpose, Public Surface, Use Cases. `logMetrics`/`havePropsChanged` documented as public API in the archive but are NOT exported from `src/index.ts` — reconciled to internal in Public Surface. | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/ui-logging/ai-docs/ARCHITECTURE.md` | architecture / tests | reconciled | Design Overview, Data Flow, Sequence Diagram(s), Pitfalls. Archive's `WidgetMetrics` example omits `event` union, `props`, `additionalContext` — corrected from `src/metricsLogger.ts`. `PROPS_UPDATED` is documented as a future event; code confirms it is unused (TODO CAI-6890 in `src/withMetrics.tsx`). | + +## Overview +`ui-logging` is the metrics/telemetry utility for Webex Contact Center widgets, published as +`@webex/cc-ui-logging`. It owns two concerns: a React Higher-Order Component (`withMetrics`) that +auto-emits widget lifecycle metrics, and an internal logging helper (`logMetrics`) that forwards a +typed metric record to the store's logger. It is the lowest-level shared package consumed by every +widget that needs observability, and it depends only on `@webex/cc-store` plus React (peer). + +The module is a thin, dependency-light wrapper. It does not own a sink, transport, queue, or any +persisted state — it formats a metric as a JSON string and hands it to `store.logger.log()`, which is +the SDK `LoggerProxy` wired in `@webex/cc-store`. The store's logger is the actual telemetry destination; +`ui-logging` is a pure pass-through formatter plus a render-lifecycle observer. + +A maintainer should start at `src/index.ts` (the export barrel — only `withMetrics` and the +`WidgetMetrics` type are public), then read `src/withMetrics.tsx` (the HOC + memo comparator) and +`src/metricsLogger.ts` (the logging helper and the shallow `havePropsChanged` comparator). + +## Purpose / Responsibility +Owns emission of widget lifecycle telemetry: wrap a React component so it logs `WIDGET_MOUNTED` on +mount and `WIDGET_UNMOUNTED` on unmount via the store logger. Does NOT own the logging sink/transport, +log persistence, or prop sanitization. + +## Stack +TypeScript ^5.6.3, React (peer `>=18.3.1`, `react-dom >=18.3.1`) HOC + hooks. Tests: Jest 29.7.0 + +React Testing Library 16.0.1 + `@testing-library/jest-dom`, jsdom environment. Build: Webpack 5 +(`webpack --mode=development`), output to `dist/`. Source of truth: `package.json`. + +## Folder / Package Structure +``` +packages/contact-center/ui-logging/ +├── src/ +│ ├── index.ts # Public export barrel: withMetrics + WidgetMetrics type +│ ├── withMetrics.tsx # HOC: memoized lifecycle-tracking wrapper +│ └── metricsLogger.ts # logMetrics() + WidgetMetrics type + havePropsChanged() (internal) +├── tests/ +│ ├── withMetrics.test.tsx # HOC mount/unmount/passthrough/memo tests +│ └── metricsLogger.test.ts # logMetrics + havePropsChanged tests +├── package.json # name, version, deps, scripts (source of truth for stack) +├── tsconfig.json +└── tsconfig.test.json +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `packages/contact-center/ui-logging/src/index.ts` | The public export barrel — authoritative list of what this package exposes (`withMetrics`, `WidgetMetrics`). | +| `packages/contact-center/ui-logging/src/metricsLogger.ts` | The `WidgetMetrics` type (canonical event union), the `logMetrics` formatter, and the `havePropsChanged` comparator. | +| `packages/contact-center/ui-logging/src/withMetrics.tsx` | The HOC behavior: which lifecycle events fire and the `React.memo` comparator wiring. | +| `packages/contact-center/ui-logging/package.json` | Package name, version, dependency/peer-dependency floors, build/test scripts. | + +## Public Surface +This package is consumed as an imported SDK/code API (npm package `@webex/cc-ui-logging`); it has no +network/event/CLI contract. Only the symbols re-exported from `src/index.ts` are public. `logMetrics` +and `havePropsChanged` live in `src/metricsLogger.ts` and are exercised by tests but are NOT in the +export barrel — they are internal. + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `ui-logging.withMetrics` | SDK | `withMetrics

(Component, widgetName: string): React.MemoExoticComponent` | Wrap a widget so it auto-emits mount/unmount metrics; provided to every widget needing telemetry. | stable; default export re-exported as named `withMetrics`. Signature change = major. | `packages/contact-center/ui-logging/src/withMetrics.tsx` | `../../../../ai-docs/CONTRACTS.md` | +| `ui-logging.WidgetMetrics` | SDK | `type WidgetMetrics = { widgetName: string; event: 'WIDGET_MOUNTED' \| 'ERROR' \| 'WIDGET_UNMOUNTED' \| 'PROPS_UPDATED'; props?; timestamp: number; additionalContext? }` | Type for the metric record callers construct. | stable; adding an optional field = minor, narrowing the `event` union or removing a field = major. | `packages/contact-center/ui-logging/src/metricsLogger.ts` | `../../../../ai-docs/CONTRACTS.md` | + +Compatibility notes: +- Adding a new value to the `event` union or a new optional field on `WidgetMetrics` is additive (minor). +- Removing/renaming `withMetrics`, narrowing the `event` union, or changing the `withMetrics` parameter order is breaking (major) — every widget package imports `withMetrics`. + +## Requires (dependencies) +- `@webex/cc-store` (`workspace:*`) — for `store.logger` (`ILogger`, set from `cc.LoggerProxy` in `packages/contact-center/store/src/store.ts:83`). `logMetrics` reads `store.logger` at call time and degrades gracefully (warns, returns) if it is absent. No fallback sink. +- React + react-dom — peer `>=18.3.1` (`package.json`); needed for the HOC, `React.memo`, and `useEffect`. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `ui-logging-R-001` | `withMetrics(Component, widgetName)` emits a `WIDGET_MOUNTED` metric (with `widgetName` and `Date.now()` timestamp) when the wrapped component mounts. | Lifecycle observability — track widget initialization/usage. | `packages/contact-center/ui-logging/src/withMetrics.tsx` | `packages/contact-center/ui-logging/tests/withMetrics.test.tsx` ("should log metrics on mount") | none | PRESENT | +| `ui-logging-R-002` | The HOC emits a `WIDGET_UNMOUNTED` metric on unmount via the `useEffect` cleanup. | Track session duration / cleanup; complements mount. | `packages/contact-center/ui-logging/src/withMetrics.tsx` | `packages/contact-center/ui-logging/tests/withMetrics.test.tsx` ("should log metrics on unmount") | none | PRESENT | +| `ui-logging-R-003` | The wrapped component receives the original props unchanged (transparent pass-through). | The HOC must be a non-invasive wrapper. | `packages/contact-center/ui-logging/src/withMetrics.tsx` (``) | `packages/contact-center/ui-logging/tests/withMetrics.test.tsx` ("should pass through props to wrapped component") | none | PRESENT | +| `ui-logging-R-004` | The HOC re-renders the wrapped component only when props change per shallow comparison; identical props skip the render. | Performance — avoid re-render churn from unstable parent references. | `packages/contact-center/ui-logging/src/withMetrics.tsx` (memo comparator `!havePropsChanged`) + `metricsLogger.ts` (`havePropsChanged`) | `packages/contact-center/ui-logging/tests/withMetrics.test.tsx` ("should not re-render…", "should re-render when props have changed") | none | PRESENT | +| `ui-logging-R-005` | `logMetrics` forwards the metric to `store.logger.log()` as `"CC-Widgets: UI Metrics: "` with context `{module: 'metricsLogger.tsx', method: 'logMetrics'}`. | Centralized, identifiable telemetry routed through the store logger. | `packages/contact-center/ui-logging/src/metricsLogger.ts` | `packages/contact-center/ui-logging/tests/metricsLogger.test.ts` ("should log metrics when logger is available") | none | PRESENT | +| `ui-logging-R-006` | When `store.logger` is absent, `logMetrics` emits a single `console.warn('CC-Widgets: UI Metrics: No logger found')` and returns without throwing. | Graceful degradation — widgets must render even when no logger is wired. | `packages/contact-center/ui-logging/src/metricsLogger.ts` | `packages/contact-center/ui-logging/tests/metricsLogger.test.ts` ("should handle case when logger is not available") | none | PRESENT | +| `ui-logging-R-007` | `havePropsChanged` performs a shallow comparison: `false` for reference-equal or shallow-equal inputs, `true` on differing type, key set, or any first-level value (objects/arrays compared by reference). | Drives the memo comparator; must not deep-compare while props are unsanitized. | `packages/contact-center/ui-logging/src/metricsLogger.ts` | `packages/contact-center/ui-logging/tests/metricsLogger.test.ts` (7 `havePropsChanged` cases) | none | PRESENT | +| `ui-logging-R-008` | The metric payload emitted by the HOC contains only `widgetName`, `event`, and `timestamp` — never raw widget props or credentials. | Privacy — props are not sanitized, so the HOC must not log them (see `havePropsChanged` remark). | `packages/contact-center/ui-logging/src/withMetrics.tsx` (no `props` field passed); `metricsLogger.ts` (`havePropsChanged` `@remarks`: props are unsanitized) | `packages/contact-center/ui-logging/tests/withMetrics.test.tsx` (asserts exact mount/unmount payload, no `props`) | `WidgetMetrics` *allows* optional `props`/`additionalContext`; callers using them must sanitize first. Gap: no automated test enforces PII-absence for caller-supplied `props`. | PRESENT | + +## Design Overview +The module separates the React concern (lifecycle observation + render gating) from the logging concern +(formatting + sink dispatch). `withMetrics.tsx` is a HOC factory: it wraps a component in `React.memo` +with a custom comparator and registers a single mount-effect whose cleanup fires on unmount. The +comparator is `(_prev, next) => !havePropsChanged(prev, next)` — note React's `memo` comparator returns +`true` to *skip* re-render, so the helper is negated. + +`metricsLogger.ts` holds the pure pieces. `logMetrics` is intentionally side-effect-only and +fail-soft: it reads `store.logger` lazily at call time (so it works regardless of store init order), +warns-and-returns if absent, and otherwise pretty-prints the metric into a tagged log line with a +fixed module/method context for grep-ability. `havePropsChanged` is a deliberately shallow comparator +— the inline `@remarks` document that deep comparison is withheld until props are sanitized, because a +deep diff over unsanitized props risks logging/processing PII. + +`PROPS_UPDATED` exists in the `WidgetMetrics` event union but is not emitted anywhere; it is reserved +for the future feature tracked by TODO CAI-6890 in `withMetrics.tsx`. + +## Data Flow +In-process function calls only (no network/queue). A widget is wrapped at module load; at runtime the +HOC's `useEffect` fires `logMetrics`, which formats and forwards to the store logger (the SDK +`LoggerProxy`), which is the telemetry sink. + +```mermaid +graph LR + subgraph Widget Package + W[Widget component] + HOC["withMetrics(Widget, name)"] + end + subgraph ui-logging + LM["logMetrics(metric)"] + HPC["havePropsChanged()"] + end + subgraph "@webex/cc-store" + SL["store.logger (ILogger / LoggerProxy)"] + end + Sink[(Telemetry sink / log output)] + + W -->|wrapped at module load| HOC + HOC -->|mount / unmount useEffect| LM + HOC -->|memo comparator| HPC + LM -->|"tagged JSON string + context"| SL + LM -.->|"logger absent → console.warn, return"| W + SL --> Sink +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Widget mount/unmount metric emission (the module's single behavior) | "Lifecycle metric emission" | `alt` branch: `store.logger` absent → `console.warn` + return, no throw | + +```mermaid +sequenceDiagram + participant App as Host / Widget tree + participant HOC as withMetrics (memo) + participant LM as logMetrics + participant Store as store.logger (ILogger) + + App->>HOC: mount + HOC->>LM: logMetrics({widgetName, WIDGET_MOUNTED, Date.now()}) + alt store.logger present + LM->>Store: log("CC-Widgets: UI Metrics: ", {module, method}) + Store-->>LM: ok + else store.logger undefined + LM->>LM: console.warn("...No logger found") + Note over LM: returns, does not throw + end + HOC->>App: render + + Note over App,Store: later — component unmounts + App->>HOC: unmount (useEffect cleanup) + HOC->>LM: logMetrics({widgetName, WIDGET_UNMOUNTED, Date.now()}) + alt store.logger present + LM->>Store: log(...) + else store.logger undefined + LM->>LM: console.warn(...) + end +``` + +## Class / Component Relationships +```mermaid +graph TD + index["index.ts (barrel)"] -->|re-exports| withMetrics + index -->|re-exports type| WidgetMetrics + withMetrics["withMetrics.tsx (HOC factory)"] -->|calls| logMetrics + withMetrics -->|memo comparator| havePropsChanged + logMetrics["logMetrics()"] -->|reads| storeLogger["store.logger"] + logMetrics -->|typed by| WidgetMetrics["WidgetMetrics (type)"] + havePropsChanged["havePropsChanged()"] +``` +`withMetrics` is the only public runtime symbol; it composes the two internal helpers from +`metricsLogger.ts`. `WidgetMetrics` is the public type shared by `logMetrics` and the HOC's emitted +payloads. There are no classes — the module is functional/HOC-based. + +## Use Cases +- **UC-1 Track widget lifecycle:** a widget package wraps its presentational component as + `withMetrics(WidgetInternal, 'WidgetName')` → on mount a `WIDGET_MOUNTED` metric is logged, on unmount + a `WIDGET_UNMOUNTED` metric is logged, props pass through untouched. Evidence: + `packages/contact-center/ui-logging/src/withMetrics.tsx`, + `packages/contact-center/ui-logging/tests/withMetrics.test.tsx`. +- **UC-2 Render gating:** parent re-renders with shallow-equal props → the memoized wrapper skips + re-rendering the wrapped component; only changed props trigger a re-render. Evidence: + `packages/contact-center/ui-logging/src/withMetrics.tsx` (comparator), + `packages/contact-center/ui-logging/tests/withMetrics.test.tsx` (re-render cases). + +## Error Handling & Failure Modes +| Condition | Signal (error/code/result) | Caller recovery | +|---|---|---| +| `store.logger` undefined at `logMetrics` call time | `console.warn('CC-Widgets: UI Metrics: No logger found')`, function returns; metric is dropped | No exception — widget keeps rendering. To capture metrics, ensure the store logger is initialized before widgets mount. | + +The module raises no errors callers must catch; its only failure mode is a silently-dropped metric +(`ui-logging-R-006`). Evidence: `packages/contact-center/ui-logging/src/metricsLogger.ts`. + +## Pitfalls +- Metric payload omits `props` intentionally (`ui-logging-R-008`). If a future change adds `props` to + the HOC's emitted metric, it would log unsanitized widget props — a PII risk. The `havePropsChanged` + `@remarks` document this: deep comparison/prop logging is withheld until sanitization exists. +- `React.memo`'s comparator returns `true` to *skip* re-render, so the wiring is `!havePropsChanged(...)`. + Inverting this (forgetting the `!`) silently disables memoization or freezes updates. +- `havePropsChanged` is shallow: nested object/array changes are detected only if the reference changes. + A widget mutating a nested object in place will NOT re-render. Pass new references for changed data. +- Unstable inline props (`onChange={() => {}}`, `config={{...}}`) defeat memoization — every parent + render produces new references, so the wrapped component always re-renders. Memoize callbacks/objects + in the parent. +- `logMetrics` reads `store.logger` lazily per call; if widgets mount before the store logger is wired, + early mount metrics are dropped with only a console warning (no retry/buffer). +- `PROPS_UPDATED` is in the `event` union but never emitted (TODO CAI-6890). Do not assume it fires. + +## Module Do's / Don'ts +- DO: wrap widget components with `withMetrics(Component, 'Name')` from `@webex/cc-ui-logging` for lifecycle telemetry. +- DO: keep the HOC's emitted metric to non-PII fields (`widgetName`, `event`, `timestamp`). +- DON'T: log raw widget props or credentials through `logMetrics` — props are not sanitized. +- DON'T: import `logMetrics`/`havePropsChanged` from internal paths as if public; only `withMetrics` and `WidgetMetrics` are exported by `index.ts`. +- DON'T: rely on `PROPS_UPDATED` being emitted. + +## Export Stability +Public exports are `withMetrics` (named, re-exported from default) and the `WidgetMetrics` type, both +from `src/index.ts`; the package ships `dist/index.d.ts` declarations. Semver: adding an optional field +to `WidgetMetrics` or a new `event` union value is a minor (additive) change; removing/renaming +`withMetrics`, changing its parameter order, narrowing the `event` union, or removing a `WidgetMetrics` +field is a major (breaking) change because every widget package imports `withMetrics`. `logMetrics` and +`havePropsChanged` are NOT exported and may change without a major bump. + +## Host Integration & Theming +N/A — the HOC is consumed by other widget packages within the monorepo, not mounted directly into a +host application, and renders no UI of its own (it returns the wrapped component verbatim: +`` in `packages/contact-center/ui-logging/src/withMetrics.tsx`). It has no +theming, custom-element, or provider requirements. + +## Key Design Trade-off +- Shallow comparison over deep comparison in `havePropsChanged`: favors privacy + simplicity over + precise change detection. It preserves the invariant that unsanitized props are never deep-traversed + or logged; the cost is that in-place nested mutations don't trigger re-renders, so callers must pass + fresh references. Evidence: `@remarks` in `packages/contact-center/ui-logging/src/metricsLogger.ts`. + +## Test-Case Strategy (module) +Unit tests cover both modules. `withMetrics.test.tsx` uses RTL `render`/`unmount` with fake timers to +assert exact mount/unmount metric payloads (positive), prop pass-through, and memo behavior on both +unchanged (skip) and changed (re-render) props. `metricsLogger.test.ts` asserts `logMetrics` forwards +to `store.logger.log` with the exact tagged string + context (positive) and warns when the logger is +missing (negative), plus seven `havePropsChanged` cases spanning primitives, type mismatch, key-set +diff, reference identity, and null/undefined. Edge cases covered: missing logger, reference-shared +nested objects, null vs undefined. Eventual consistency: N/A (synchronous in-process calls). + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `ui-logging-R-001` (mount metric) | `tests/withMetrics.test.tsx` ("should log metrics on mount") | none | +| `ui-logging-R-002` (unmount metric) | `tests/withMetrics.test.tsx` ("should log metrics on unmount") | none | +| `ui-logging-R-003` (prop pass-through) | `tests/withMetrics.test.tsx` ("should pass through props…") | none | +| `ui-logging-R-004` (memo re-render gating) | `tests/withMetrics.test.tsx` (re-render cases) | none | +| `ui-logging-R-005` (forward to store logger) | `tests/metricsLogger.test.ts` ("should log metrics when logger is available") | none | +| `ui-logging-R-006` (graceful no-logger) | `tests/metricsLogger.test.ts` ("should handle case when logger is not available") | none | +| `ui-logging-R-007` (shallow comparison) | `tests/metricsLogger.test.ts` (7 `havePropsChanged` cases) | none | +| `ui-logging-R-008` (no PII in emitted metric) | `tests/withMetrics.test.tsx` (asserts exact payload, no `props`) | No test enforces sanitization of caller-supplied `props`/`additionalContext`. | + +## Traceability +- Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md` +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/user-state/ai-docs/user-state-spec.md b/packages/contact-center/user-state/ai-docs/user-state-spec.md new file mode 100644 index 000000000..0bc9d655a --- /dev/null +++ b/packages/contact-center/user-state/ai-docs/user-state-spec.md @@ -0,0 +1,324 @@ +# user-state — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, state, UI, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `user-state` | +| Source path(s) | `packages/contact-center/user-state/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| Validation status | not-run | + +Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with `<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc metadata. + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the repository policy says history is reliable, and must include the commit hash. If evidence is missing or conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/user-state/ai-docs/AGENTS.md` | overview / API | migrated | Orientation → Overview/Purpose/Stack; props → Public Surface; examples → Use Cases. Routing preamble dropped (root `AGENTS.md` now owns it). | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/user-state/ai-docs/ARCHITECTURE.md` | architecture / tests | reconciled | Layer/data flow → Design Overview + Data Flow + Sequence Diagrams; worker timer detail → State Model + State Machine; troubleshooting → Pitfalls. The dual-timer "shows -1/0 on Available" claim reconciled against code: idle timer emits `-1` on stop (`src/helper.ts`). | + +## Overview +`user-state` is the agent-availability widget for the Webex Contact Center desktop. It lets an agent change their presence (Available, or Idle with an aux/idle code) from a dropdown, shows how long the agent has been in the current state via a live elapsed timer, and tracks a second timer for time since the last idle-code change. State selection round-trips through the SDK so the backend stays authoritative. + +The package follows the repo's one-directional layering: the exported `UserState` widget (`src/user-state/index.tsx`) is an `observer` that reads the MobX store, wires it through the `useUserState` hook (`src/helper.ts`), and renders the presentational `UserStateComponent` from `@webex/cc-components`. The widget owns no SDK access of its own — it routes state changes through the `useUserState` hook, which mutates the store via `store.setCurrentState(...)` and calls the SDK via `cc.setAgentState(...)` (the `cc` handle is read from the store), then reacts to store observables (`currentState`, `lastStateChangeTimestamp`, `lastIdleCodeChangeTimestamp`, `customState`) to drive callbacks and timers. + +The only non-trivial machinery is local: a Web Worker created inline from a Blob runs the two `setInterval` timers off the main thread and posts elapsed seconds back to the hook. The hook holds the resulting UI state (`elapsedTime`, `lastIdleStateChangeElapsedTime`, `isSettingAgentStatus`) in React `useState`; everything else is derived from the store. + +A maintainer should start at `src/helper.ts` (all behavior lives there) and treat `src/user-state/index.tsx` as a thin store→hook→component adapter. + +## Purpose / Responsibility +Owns the agent-state UI surface: selecting Available/Idle states with idle codes, persisting the change through the SDK, and displaying state-duration and idle-code-duration timers. It does NOT own the store's state shape, idle-code loading, or SDK lifecycle — those belong to `store/`. + +## Stack +TypeScript 5.6.3, React 18 (`react`/`react-dom` peer `>=18.3.1`), MobX via `mobx-react-lite` `^4.1.0`, `react-error-boundary` `^6.0.0`. Browser Web Worker API for timers (created from an inline Blob, no separate worker file). Build: `tsc` for types + Webpack for the bundle. Tests: Jest 29 + React Testing Library (`renderHook`/`render`), config in the package. Evidence: `packages/contact-center/user-state/package.json`. + +## Folder / Package Structure +``` +user-state/src/ +├── index.ts # Package barrel — re-exports UserState +├── user-state.types.ts # IUserStateProps + UseUserStateProps (Pick from cc-components IUserState) +└── user-state/ + └── index.tsx # UserState widget (ErrorBoundary + observer UserStateInternal) +helper.ts is at src/helper.ts # useUserState hook + inline Web Worker timer script +tests/ +├── helper.ts # Hook behavior + error-path tests +└── user-state/index.tsx # Widget render + ErrorBoundary tests +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `packages/contact-center/user-state/src/helper.ts` | The `useUserState` hook: Web Worker script, timer lifecycle, state-change effects, SDK call, error handling. All behavior. | +| `packages/contact-center/user-state/src/user-state/index.tsx` | The exported `UserState` widget; ErrorBoundary wrapping; which store fields are read and passed to the hook. | +| `packages/contact-center/user-state/src/user-state.types.ts` | The module's prop contracts (`IUserStateProps`, `UseUserStateProps`) as `Pick`s of the canonical `IUserState`. | +| `packages/contact-center/cc-components/src/components/UserState/user-state.types.ts` | Canonical `IUserState` / `UserStateComponentsProps` — the authoritative prop surface; do not redefine locally. | + +## Public Surface +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `cc-widgets.UserState` | SDK (React export) | `UserState` React component, prop `onStateChange?: (state: IdleCode \| ICustomState) => void` | Render the agent-state widget in a React app and observe state changes | stable semver; adding optional props = minor, removing/renaming `onStateChange` = major | `packages/contact-center/user-state/src/index.ts`, props in `src/user-state.types.ts` | `../../../../ai-docs/CONTRACTS.md` | +| `cc-widgets.UserState` (custom element) | event | custom element `widget-cc-user-state` (registered in `cc-widgets`); `onStateChange` exposed as an attribute/event by r2wc | Mount the widget as a Web Component in a non-React host | stable semver; tag name is part of the contract | `packages/contact-center/cc-widgets/src/wc.ts` (`{name: 'widget-cc-user-state', component: WebUserState}`) | `../../../../ai-docs/CONTRACTS.md` | + +Compatibility notes: +- `onStateChange` is invoked with an `IdleCode` (the matching idle code for `currentState`) or an `ICustomState` (when the store holds a `customState` with a `developerName`). Changing which object is passed is a breaking change for consumers. +- The custom-element tag `widget-cc-user-state` is registered by `cc-widgets`, not this package; renaming it is a breaking change owned there. + +## Requires (dependencies) +- `@webex/cc-store` (`workspace:*`) — singleton store; sole SDK access point. The hook reads `idleCodes`, `agentId`, `cc`, `currentState`, `customState`, `lastStateChangeTimestamp`, `lastIdleCodeChangeTimestamp`, `logger` and calls `store.setCurrentState`, `store.setLastStateChangeTimestamp`, `store.setLastIdleCodeChangeTimestamp`, `store.onErrorCallback`. SDK call: `cc.setAgentState(...)`. +- `@webex/cc-components` (`workspace:*`) — provides `UserStateComponent` (presentation) and the canonical `IUserState` types. +- `mobx-react-lite` `^4.1.0` — `observer` HOC for store reactivity. +- `react-error-boundary` `^6.0.0` — wraps the widget so render errors route to `store.onErrorCallback`. +- Browser **Web Worker** API — timers run in a worker created from an inline Blob; no fallback path exists (see Pitfalls). +- Peer: `react` / `react-dom` `>=18.3.1`. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `USER-STATE-R-001` | Selecting a state calls `store.setCurrentState(selectedCode)`; the resulting `currentState` change drives the SDK update — the UI does not call the SDK directly on selection. | Keeps the store authoritative; SDK persistence is a reaction to store change, not a side effect of the click. | `src/helper.ts` (`setAgentStatus`, `useEffect([currentState])`) | `tests/helper.ts` "should handle setAgentStatus correctly and update state" | none | PRESENT | +| `USER-STATE-R-002` | On `currentState` change, `cc.setAgentState({state, auxCodeId, agentId, lastStateChangeReason})` is called with `state` mapped to `'Available'` when the selected code name is `Available`, else `'Idle'`. | Backend distinguishes Available vs Idle; idle codes carry the human reason. | `src/helper.ts` (`updateAgentState`) | `tests/helper.ts` "should update last state change timestamp from setAgentState" (Available) + "should set idle status if name does not match: Available" (Idle) | none | PRESENT | +| `USER-STATE-R-003` | On a successful `setAgentState` response containing `data`, the store timestamps `lastStateChangeTimestamp` and `lastIdleCodeChangeTimestamp` are updated from the response. | Timer resets are driven off server-confirmed timestamps, not local click time. | `src/helper.ts` (`updateAgentState` `.then`) | `tests/helper.ts` "should update last state change timestamp from setAgentState" | none | PRESENT | +| `USER-STATE-R-004` | If `setAgentState` rejects, `currentState` is reverted to the previous value via `store.setCurrentState(prevStateRef.current)` and the error is logged. | The UI must not show a state the backend rejected. | `src/helper.ts` (`updateAgentState` `.catch`) | `tests/helper.ts` "should handle errors from setAgentState and revert state" | none | PRESENT | +| `USER-STATE-R-005` | A Web Worker runs two 1-second timers; the hook surfaces `elapsedTime` and `lastIdleStateChangeElapsedTime`, clamping negative values to `0` on update. | Off-main-thread timing keeps the UI responsive; clamp avoids showing negative durations. | `src/helper.ts` (workerScript, `onmessage`) | `tests/helper.ts` "should increment elapsedTime every second" / "should increment lastIdleStateChangeElapsedTime every second" | none | PRESENT | +| `USER-STATE-R-006` | When `lastStateChangeTimestamp`/`lastIdleCodeChangeTimestamp` change: post `reset` to the worker with the state timestamp; post `resetIdleCode` when the idle timestamp differs from the state timestamp, else post `stopIdleCode`. | Idle-code timer runs only while distinct from the state change (i.e. agent is idle), and stops on Available. | `src/helper.ts` (`useEffect([lastStateChangeTimestamp, lastIdleCodeChangeTimestamp])`) | `tests/helper.ts` "should post resetIdleCode message if lastIdleCodeChangeTimestamp is different from lastStateChangeTimestamp" | No test asserts the `stopIdleCode` branch when timestamps are equal | PRESENT | +| `USER-STATE-R-007` | On `stopIdleCodeTimer` worker message the hook sets `lastIdleStateChangeElapsedTime` to `-1` (sentinel for "no idle timer"). | Lets the presentational component hide the idle timer on Available. | `src/helper.ts` (`onmessage` `stopIdleCodeTimer` branch) | `tests/helper.ts` "should handle stopIdleCodeTimer event and set lastIdleStateChangeElapsedTime to -1" | none | PRESENT | +| `USER-STATE-R-008` | `onStateChange` is invoked with the store's `customState` when it has a truthy `developerName`; otherwise with the `idleCodes` entry whose `id` equals `currentState`. | Custom (developer-defined) states bypass idle-code matching; standard states map to a known idle code. | `src/helper.ts` (`callOnStateChange`) | `tests/helper.ts` "should call onStateChange with customState if provided" / "should call onStateChange with matching idleCode when currentState changes" | none | PRESENT | +| `USER-STATE-R-009` | The Web Worker is terminated on unmount: post `stop` and `stopIdleCode`, call `terminate()`, null the ref. | Prevents leaked workers/intervals across widget remounts. | `src/helper.ts` (initial `useEffect` cleanup) | `tests/helper.ts` "should clean up on unmount" | none | PRESENT | +| `USER-STATE-R-010` | Render errors are contained by an `ErrorBoundary` that renders an empty fragment and calls `store.onErrorCallback('UserState', error)`. | A widget crash must not take down the host desktop; host gets a single error hook. | `src/user-state/index.tsx` (`ErrorBoundary`) | `tests/user-state/index.tsx` "should render empty fragment when ErrorBoundary catches an error" | none | PRESENT | +| `USER-STATE-R-011` | Every hook side effect (worker init, message handling, both state effects, `setAgentStatus`, `updateAgentState`, cleanup) is wrapped in try/catch and logs a scoped `CC-Widgets: UserState: ...` error via the injected logger without throwing out of the effect. | Defensive logging keeps a single failing path from cascading; aids field diagnosis. | `src/helper.ts` (try/catch in each block) | `tests/helper.ts` "Error Handling" suite (callOnStateChange, worker init, onmessage, currentState/customState/timestamp effects, setAgentStatus, updateAgentState, cleanup) | none | PRESENT | + +## Design Overview +The widget is a near-pure adapter. `UserState` (`src/user-state/index.tsx`) wraps `UserStateInternal` in an `ErrorBoundary`; the internal component is an `observer` that destructures the store and forwards exactly the fields the hook needs, then spreads the hook's return plus `customState`/`logger` into `UserStateComponent`. No business logic lives in the widget file beyond that wiring and the error-boundary callback. + +All logic is in `useUserState` (`src/helper.ts`), structured as four effects plus two action functions: +1. **Worker init effect (`[]`)** — builds the inline worker script into a Blob, starts both timers immediately, registers `onmessage` to push elapsed seconds into React state, and returns a cleanup that stops + terminates the worker. +2. **`currentState` effect** — compares against a `prevStateRef`; on a real change it calls `updateAgentState`, and only after the SDK promise resolves does it advance `prevStateRef` and fire `callOnStateChange`. On reject it logs (the revert itself happens inside `updateAgentState`). +3. **`customState` effect** — fires `callOnStateChange` whenever the store's custom state changes. +4. **Timestamp effect (`[lastStateChangeTimestamp, lastIdleCodeChangeTimestamp]`)** — translates server-confirmed timestamps into worker `reset`/`resetIdleCode`/`stopIdleCode` messages. + +`setAgentStatus` is the UI entry point (writes the store); `updateAgentState` is the SDK-sync function (maps the selected code to an SDK payload, sets the loading flag, writes back timestamps, reverts on failure). This split is deliberate: the click only mutates local store state, and persistence is a reaction — so the same store change made elsewhere (e.g. an SDK-driven event) flows through the same code path. + +## Data Flow +In-process React/MobX with an in-process Web Worker (postMessage) sidecar; the only network/IPC boundary is the SDK call inside the store. No HTTP/WebSocket is owned here. + +```mermaid +graph TB + subgraph Presentation + Widget[UserState widget
index.tsx · observer + ErrorBoundary] + Comp[UserStateComponent
@webex/cc-components] + end + Hook[useUserState hook
helper.ts] + Worker[Web Worker
inline Blob · 2 timers] + Store[Store singleton
@webex/cc-store] + SDK[Contact Center SDK] + + Store -->|observable: idleCodes, currentState,
customState, timestamps, agentId, cc| Widget + Widget -->|props + onStateChange| Hook + Hook -->|setCurrentState| Store + Hook -->|setAgentState| SDK + SDK -->|response data: timestamps| Hook + Hook -->|setLast*Timestamp| Store + Hook <-->|start/reset/stop · elapsed seconds| Worker + Hook -->|state + handlers + timers| Widget + Widget -->|spread props| Comp + Comp -->|user selects state| Hook +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| State selection → persist → timer reset | "State change & timer reset" | `alt` branch: `setAgentState` rejects → revert `currentState`, log error | +| Custom/external state → callback | "Custom state callback" | `alt` branch on `developerName` presence | +| Mount/unmount worker lifecycle | "Worker lifecycle" | cleanup wrapped in try/catch (logged, non-throwing) | + +```mermaid +sequenceDiagram + actor User + participant Comp as UserStateComponent + participant Hook as useUserState + participant Store + participant SDK + participant Worker + + User->>Comp: select state (e.g. "Break") + Comp->>Hook: setAgentStatus(selectedCode) + Hook->>Store: setCurrentState(selectedCode) + Note over Hook: currentState effect fires (prevState != current) + Hook->>Hook: updateAgentState() · setIsSettingAgentStatus(true) + Hook->>SDK: setAgentState({state, auxCodeId, agentId, lastStateChangeReason}) + alt success (response has data) + SDK-->>Hook: data.lastStateChangeTimestamp / lastIdleCodeChangeTimestamp + Hook->>Store: setLastStateChangeTimestamp / setLastIdleCodeChangeTimestamp + Hook->>Hook: prevStateRef = current · callOnStateChange() + Note over Hook: timestamp effect fires + Hook->>Worker: postMessage(reset, startTime) + alt idle ts != state ts + Hook->>Worker: postMessage(resetIdleCode) + else equal (Available) + Hook->>Worker: postMessage(stopIdleCode) + Worker-->>Hook: stopIdleCodeTimer + Hook->>Hook: lastIdleStateChangeElapsedTime = -1 + end + else rejected + SDK-->>Hook: error + Hook->>Store: setCurrentState(prevStateRef.current) + Hook->>Hook: logger.error("Error setting agent state...") + end + Hook->>Hook: setIsSettingAgentStatus(false) (finally) + Worker-->>Hook: elapsedTime tick (every 1s) + Hook-->>Comp: elapsedTime / isSettingAgentStatus +``` + +```mermaid +sequenceDiagram + participant Store + participant Hook as useUserState + Note over Store: customState changes (external/SDK-driven) + Store-->>Hook: customState (observable) + Hook->>Hook: callOnStateChange() + alt customState.developerName truthy + Hook->>Hook: onStateChange(customState) + else + Hook->>Hook: find idleCode where id === currentState + Hook->>Hook: onStateChange(matchingCode) + end +``` + +```mermaid +sequenceDiagram + participant Widget as UserState widget + participant Hook as useUserState + participant Worker + Widget->>Hook: mount + Hook->>Worker: new Worker(Blob) · postMessage(start) · postMessage(startIdleCode) + Worker-->>Hook: elapsedTime / lastIdleStateChangeElapsedTime ticks + Widget->>Hook: unmount + Hook->>Worker: postMessage(stop) · postMessage(stopIdleCode) + Hook->>Worker: terminate() · ref = null + Note over Hook: cleanup in try/catch — terminate error is logged, not thrown +``` + +## Class / Component Relationships +```mermaid +classDiagram + class UserState { + +onStateChange? + ErrorBoundary wrapper + } + class UserStateInternal { + observer + reads store fields + } + class useUserState { + +setAgentStatus() + -updateAgentState() + -callOnStateChange() + elapsedTime, isSettingAgentStatus... + } + class UserStateComponent { + presentational (cc-components) + } + class Store { + currentState, idleCodes, timestamps... + cc (SDK) + } + UserState --> UserStateInternal : renders + UserStateInternal --> useUserState : calls + UserStateInternal --> UserStateComponent : renders + UserStateInternal ..> Store : observes + useUserState ..> Store : reads + mutates + useUserState --> Worker : controls timers +``` +`UserState` is the public export; `UserStateInternal` is the observer that binds the store. `useUserState` is the only stateful unit and the single owner of the worker. `UserStateComponent` and the `IUserState`/`UserStateComponentsProps` types are owned by `cc-components`; this module only `Pick`s from `IUserState` for its prop contracts. + +## Use Cases +- **UC-1 Agent goes idle with a code:** Agent opens the dropdown → selects an idle code → `setAgentStatus` writes the store → `currentState` effect calls `cc.setAgentState({state:'Idle', auxCodeId, ...})` → on success timestamps update, the state timer resets and the idle-code timer starts → `onStateChange(idleCode)` fires. Evidence: `src/helper.ts`, `tests/helper.ts` "should set idle status if name does not match: Available". +- **UC-2 Agent returns to Available:** Agent selects Available → SDK called with `state:'Available'` → timestamps return equal → timestamp effect posts `stopIdleCode`, worker emits `stopIdleCodeTimer`, idle timer reads `-1` while the state timer resets. Evidence: `src/helper.ts`, `tests/helper.ts` "should update last state change timestamp from setAgentState" + "should handle stopIdleCodeTimer event...". +- **UC-3 Rejected state change:** SDK rejects → `currentState` reverts to the previous value, error logged, loading flag cleared. Evidence: `src/helper.ts`, `tests/helper.ts` "should handle errors from setAgentState and revert state". +- **UC-4 External/custom state applied:** Store `customState` set with a `developerName` (e.g. RONA) → `customState` effect fires `onStateChange(customState)` directly. Evidence: `src/helper.ts`, `tests/helper.ts` "should call onStateChange with customState if provided". + +### UI Flow (per use case) +- Primary surface is a single state dropdown rendered by `UserStateComponent`: lists Available plus the store's `idleCodes`. While a change is in flight, `isSettingAgentStatus` is `true` (loading). The state-duration timer shows `elapsedTime` (seconds, clamped ≥ 0); the idle-code timer shows `lastIdleStateChangeElapsedTime` and is hidden when that value is `-1` (Available). On a render error the widget shows nothing (empty fragment). Detailed presentation belongs to `cc-components` (`UserStateComponent`). + +## State Model +The hook holds client-side UI state in React `useState`/`useRef` (all in `src/helper.ts`); it does not own domain data (that is the store's). +- `isSettingAgentStatus: boolean` — true between SDK call start and settle; drives the loading affordance. +- `elapsedTime: number` — seconds in the current state; updated from worker `elapsedTime` messages, clamped to ≥ 0. +- `lastIdleStateChangeElapsedTime: number` — seconds since last idle-code change; `-1` is the sentinel meaning "no idle timer" (Available). +- `workerRef: Worker | null` — the live worker; nulled on cleanup. +- `prevStateRef: string` — the last committed `currentState`, used to detect real changes and to revert on SDK failure. + +Triggers: a state dropdown selection → `setCurrentState` (store) → `currentState` effect. Server-confirmed timestamp changes (`lastStateChangeTimestamp`, `lastIdleCodeChangeTimestamp`) → worker reset/stop messages → timer values. Worker ticks → `elapsedTime` / `lastIdleStateChangeElapsedTime`. + +## State Machine +The Web Worker is a small timer state machine with two independent timers (state timer, idle-code timer), driven by `postMessage` commands. + +```mermaid +stateDiagram-v2 + [*] --> Running : start / startIdleCode (on mount) + Running --> Running : reset (new startTime) — emits elapsedTime each 1s + state "Idle timer" as IT { + [*] --> TickingIdle : startIdleCode + TickingIdle --> TickingIdle : resetIdleCode (idle ts != state ts) + TickingIdle --> StoppedIdle : stopIdleCode + StoppedIdle --> TickingIdle : resetIdleCode + StoppedIdle : emits stopIdleCodeTimer → hook sets value -1 + } + Running --> Stopped : stop (cleanup) + Stopped --> [*] : terminate() +``` +States/transitions: the **state timer** is always Running from mount until `stop`; `reset` re-bases its start time (used after a confirmed state change). The **idle-code timer** toggles between Ticking and Stopped based on whether the idle timestamp differs from the state timestamp. Terminal: `terminate()` after `stop`. Invalid: there is no path that emits idle ticks while Stopped — `stopIdleCode` clears the interval before the next tick. + +## Error Handling & Failure Modes +| Condition | Signal (error/code/result) | Caller recovery | +|---|---|---| +| `setAgentState` rejects (network, invalid code, not logged in) | `currentState` reverted via `store.setCurrentState(prevStateRef.current)`; `logger.error("Error setting agent state: ...")`; promise rejects out of `updateAgentState` | UI returns to prior state automatically; host may surface a retry via `store.onErrorCallback` is not triggered here (only for render errors) — caller observes the unchanged store state | +| Synchronous throw in any hook effect/action | Caught, logged as `CC-Widgets: UserState: Error in - `, effect returns without throwing | None required; widget keeps running; field log records the method | +| Web Worker fails to construct (unsupported/blocked) | Init try/catch logs `Error initializing worker`; `workerRef` stays null | Timers never tick (show 0); no crash. No graceful main-thread fallback exists — see Pitfalls | +| Render error inside `UserStateInternal` | `ErrorBoundary` renders empty fragment + `store.onErrorCallback('UserState', error)` | Host's `onErrorCallback` decides UX (notify/track); widget area is blank | + +## Pitfalls +- **No Web Worker fallback.** Timers depend entirely on `Worker` + `URL.createObjectURL`. If the worker can't be created (CSP blocks `blob:`, worker unsupported), init is caught and logged but timers silently stay at 0 — there is no main-thread `setInterval` fallback. Watch CSP `worker-src`/`script-src` when embedding. +- **Idle-timer sentinel is `-1`, not `0`.** `lastIdleStateChangeElapsedTime === -1` means "hide the idle timer" (Available). Treating `-1` as a real duration will render a wrong/negative value. Code clamps positive ticks to ≥ 0 but deliberately sets `-1` on `stopIdleCodeTimer`. +- **Selection persistence is indirect.** Selecting a state only calls `store.setCurrentState`; the SDK call happens in the `currentState` effect. A test or caller that stubs the store setter without re-rendering with the new `currentState` will not see `setAgentState` called (see how `tests/helper.ts` pairs `store.setCurrentState` with `rerender({currentState})`). +- **Timer reset is timestamp-driven, not click-driven.** Timers reset off `lastStateChangeTimestamp`/`lastIdleCodeChangeTimestamp` from the SDK response, not local time. If the response omits `data`, timestamps don't update and timers won't reset — confirm the SDK contract returns timestamps. +- **`updateAgentState` assumes the selected id resolves to an idle code.** It does `idleCodes.filter(c => c.id === selectedCode)[0]` and reads `.id`/`.name`; an unknown id throws (caught + logged) and no SDK call is made. Keep `idleCodes` and `currentState` in sync. + +## Module Do's / Don'ts +- DO: route every state change through `store.setCurrentState` and let the `currentState` effect own the SDK call — never call `cc.setAgentState` directly from the widget. +- DO: terminate the worker in the cleanup return and null `workerRef`; reuse the existing try/catch logging pattern for any new effect. +- DON'T: treat `lastIdleStateChangeElapsedTime === -1` as elapsed seconds — it's the "idle timer off" sentinel. +- DON'T: redefine prop types locally — `Pick` from `cc-components`' `IUserState` (`src/user-state.types.ts`). + +## Host Integration & Theming +Consumed two ways: as the React `UserState` export, or as the `widget-cc-user-state` custom element registered by `cc-widgets` (`packages/contact-center/cc-widgets/src/wc.ts`). Both require the singleton store to be initialized (the widget reads `store.cc`, `store.agentId`, `store.idleCodes`, etc. at render). Theming/presentation is delegated to `UserStateComponent` in `cc-components`; this package passes `logger`/`customState` through and renders nothing else. Peer React `>=18.3.1`. + +## Test-Case Strategy (module) +Hook tests (`tests/helper.ts`) use `renderHook` with a mocked `Worker` (postMessage/terminate spies) and `mockCC.setAgentState`; they assert positive paths (timer increment, store write, SDK payload shape, timestamp write-back) and negatives (SDK reject → revert, every effect's try/catch logging). Widget tests (`tests/user-state/index.tsx`) mock the store, assert the hook receives the exact store fields, and verify the ErrorBoundary renders an empty fragment + calls `onErrorCallback`. Edge cases covered: `-1` idle sentinel, `resetIdleCode` vs equal-timestamp branch, custom vs matched-idle-code callback, worker construction failure, cleanup failure. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `USER-STATE-R-001` | `tests/helper.ts` "should handle setAgentStatus correctly and update state" | none | +| `USER-STATE-R-002` | `tests/helper.ts` "should update last state change timestamp..." (Available) + "should set idle status if name does not match: Available" (Idle) | none | +| `USER-STATE-R-003` | `tests/helper.ts` "should update last state change timestamp from setAgentState" | No explicit test for the `'data' in response` false branch (timestamps not written) | +| `USER-STATE-R-004` | `tests/helper.ts` "should handle errors from setAgentState and revert state" | Asserts logging; does not assert `setCurrentState(prev)` was called with the prior value | +| `USER-STATE-R-005` | `tests/helper.ts` "should increment elapsedTime..." / "should increment lastIdleStateChangeElapsedTime..." | No test asserts the negative-clamp (>0 ? value : 0) branch directly | +| `USER-STATE-R-006` | `tests/helper.ts` "should post resetIdleCode message..." | Missing negative: equal timestamps → `stopIdleCode` posted | +| `USER-STATE-R-007` | `tests/helper.ts` "should handle stopIdleCodeTimer event..." | none | +| `USER-STATE-R-008` | `tests/helper.ts` "should call onStateChange with customState..." / "...with matching idleCode..." + "should not call onStateChange if not available" | none | +| `USER-STATE-R-009` | `tests/helper.ts` "should clean up on unmount" | none | +| `USER-STATE-R-010` | `tests/user-state/index.tsx` "should render empty fragment when ErrorBoundary catches an error" | none | +| `USER-STATE-R-011` | `tests/helper.ts` "Error Handling" suite (8 cases) | none | + +## Traceability +- Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md` · Contracts: `../../../../ai-docs/CONTRACTS.md` +- Coverage state & contracts baseline: `.sdd/manifest.json` From c2c7c57e0c50f4fb92fbd149b1e68c5e402ef036 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:26:59 +0530 Subject: [PATCH 2/2] docs(ai-docs): fix SDLC-Templates conformance gaps in migrated docs Address review findings against the SDLC-Templates component-repo standard (library 0.1.0-draft): - Repoint the broken SDK reference from the nonexistent contact-centre-sdk-apis/contact-center.json to the installed @webex/contact-center type source (dist/types/index.d.ts) across AGENTS, SPEC_INDEX, CONTRACTS, GLOSSARY, store-spec, manifest, templates, and CLAUDE.md. - Register the previously-undocumented cc-digital-channels module: new module spec + manifest/SPEC_INDEX/RULES/AGENTS entries. - Resolve [NEEDS HUMAN INPUT] placeholders (approved_by: pending) across all module specs. - Reconcile contract IDs: task.* -> cc-widgets.*, store's own surface -> store.instance; add ui-logging.* to the contracts catalog; point test-fixtures internal surface at its entry point; fix cc-digital index path. - Migrate ai-docs/patterns/ to the reference-docs standard (add folder README, per-pattern shape, metadata headers) and correct stale legacy claims against real code. - ARCHITECTURE: strip leftover Include-if scaffolding, add WS6 References (N/A) and Dependency/Interaction Topology sections. - Correct the miscited metricsLogger security note; fix nav pointers, promote user-state UI Flow heading, add ADR/rule metadata headers, and record DATA_MODEL N/A in the manifest. --- .sdd/manifest.json | 24 +- AGENTS.md | 7 +- CLAUDE.md | 2 +- ai-docs/ARCHITECTURE.md | 32 +- ai-docs/CONTRACTS.md | 8 +- ai-docs/GLOSSARY.md | 4 +- ai-docs/RULES.md | 3 +- ai-docs/SECURITY.md | 4 +- ai-docs/SERVICE_STATE.md | 2 +- ai-docs/SPEC_INDEX.md | 5 +- .../0001-one-directional-dependency-flow.md | 9 + ai-docs/patterns/README.md | 47 ++ ai-docs/patterns/mobx-patterns.md | 390 +++++++-------- ai-docs/patterns/react-patterns.md | 396 ++++++++------- ai-docs/patterns/testing-patterns.md | 449 ++++++------------ ai-docs/patterns/typescript-patterns.md | 289 ++++++----- ai-docs/rules/sdk-access-via-store.md | 9 + .../new-widget/02-code-generation.md | 2 +- ai-docs/templates/new-widget/06-validation.md | 2 +- .../@webex/widgets/ai-docs/widgets-spec.md | 4 +- .../ai-docs/cc-components-spec.md | 2 +- .../ai-docs/cc-digital-channels-spec.md | 378 +++++++++++++++ .../cc-widgets/ai-docs/cc-widgets-spec.md | 2 +- .../ai-docs/station-login-spec.md | 2 +- .../store/ai-docs/store-spec.md | 8 +- .../contact-center/task/ai-docs/task-spec.md | 14 +- .../ai-docs/test-fixtures-spec.md | 40 +- .../ui-logging/ai-docs/ui-logging-spec.md | 2 +- .../user-state/ai-docs/user-state-spec.md | 4 +- 29 files changed, 1209 insertions(+), 931 deletions(-) create mode 100644 ai-docs/patterns/README.md create mode 100644 packages/contact-center/cc-digital-channels/ai-docs/cc-digital-channels-spec.md diff --git a/.sdd/manifest.json b/.sdd/manifest.json index dc0761b03..15b61ff0f 100644 --- a/.sdd/manifest.json +++ b/.sdd/manifest.json @@ -54,6 +54,15 @@ "coverageState": "DRAFT", "tier": 1 }, + { + "id": "cc-digital-channels", + "package": "@webex/cc-digital-channels", + "path": "packages/contact-center/cc-digital-channels", + "spec": "packages/contact-center/cc-digital-channels/ai-docs/cc-digital-channels-spec.md", + "responsibility": "Digital channels (chat/email/social) widget; embeds the digital-interactions experience via the store.", + "coverageState": "DRAFT", + "tier": 1 + }, { "id": "station-login", "package": "@webex/cc-station-login", @@ -112,6 +121,19 @@ ], "contracts": { "index": "ai-docs/CONTRACTS.md", - "sdkApiReference": "contact-centre-sdk-apis/contact-center.json" + "sdkApiReference": "node_modules/@webex/contact-center/dist/types/index.d.ts" + }, + "standingDocs": { + "AGENTS.md": "AGENTS.md", + "ARCHITECTURE.md": "ai-docs/ARCHITECTURE.md", + "SPEC_INDEX.md": "ai-docs/SPEC_INDEX.md", + "RULES.md": "ai-docs/RULES.md", + "GLOSSARY.md": "ai-docs/GLOSSARY.md", + "SECURITY.md": "ai-docs/SECURITY.md", + "CONTRACTS.md": "ai-docs/CONTRACTS.md", + "SERVICE_STATE.md": "ai-docs/SERVICE_STATE.md", + "GETTING_STARTED.md": "ai-docs/GETTING_STARTED.md", + "REVIEW_CHECKLIST.md": "ai-docs/REVIEW_CHECKLIST.md", + "DATA_MODEL.md": "N/A — repo owns no persistent datastore; all domain data is fetched from the @webex/contact-center SDK at runtime." } } diff --git a/AGENTS.md b/AGENTS.md index 4e7601d8f..0beaf491c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — webex-widgets (Contact Center) -> You are the agent entry point — read first. Next: router [`SPEC_INDEX.md`](ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ai-docs/ARCHITECTURE.md). Load this + `SPEC_INDEX.md` first; pull module/standing docs on demand. +> You are the agent entry point — read first. Next: router [`SPEC_INDEX.md`](ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ai-docs/ARCHITECTURE.md). Load this + `SPEC_INDEX.md` first; pull module/standing docs on demand. (Multi-repo: a workspace-level `AGENTS.md` may sit above this one.) > Context-efficiency: link to canonical docs — don't duplicate them; keep this file under ~200 lines. > Cross-tool context file. Auto-loaded by AI coding agents. A module's high-level design lives in its @@ -40,6 +40,7 @@ packages/contact-center/ ├── store/ # @webex/cc-store — MobX singleton; sole SDK access point ├── cc-components/ # @webex/cc-components — shared presentational React primitives ├── cc-widgets/ # @webex/cc-widgets — r2wc Web Component wrappers (aggregator) +├── cc-digital-channels/ # @webex/cc-digital-channels — digital channels (chat/email/social) widget ├── station-login/ # @webex/cc-station-login — agent login widget ├── user-state/ # @webex/cc-user-state — agent state widget ├── task/ # @webex/cc-task — CallControl, IncomingTask, OutdialCall, TaskList, CallControlCAD @@ -53,7 +54,7 @@ playwright/ # E2E suites ## Critical Rules 1. **Code is the source of truth.** Never invent an SDK method, event, path, flag, or constant — read the - real file (SDK surface: `contact-centre-sdk-apis/contact-center.json`). + real file (SDK surface: `@webex/contact-center` package types at `node_modules/@webex/contact-center/dist/types/index.d.ts`). 2. **Ask before coding.** Present a plan / Spec Summary; wait for confirmation before non-trivial changes. 3. **One-directional dependency flow.** `cc-widgets → widgets → cc-components → store → SDK`. Never import upstream (cc-components must not import widget packages; widgets must not import cc-widgets). @@ -98,7 +99,7 @@ Always use `yarn workspace` commands for tests — never `npx jest` directly. Wo |---|---|---|---| | ticket-tracker | Jira (`jira-eng-*`) | MCP connector / REST | STOP and ask — never guess | | source-host | GitHub `webex/widgets` | `gh` CLI | STOP and ask | -| SDK reference | `contact-centre-sdk-apis/contact-center.json` | local file (TypeDoc) | STOP and ask — never invent an API | +| SDK reference | `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) | installed package `.d.ts` | STOP and ask — never invent an API | --- **SDD coverage:** this repo's per-module coverage state lives in `.sdd/manifest.json` (human mirror in diff --git a/CLAUDE.md b/CLAUDE.md index 5513353ee..5133c6cdf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -123,7 +123,7 @@ Invoke these skills at the right workflow stage — they enforce discipline that | Bug fix template | `ai-docs/templates/existing-widget/bug-fix.md` | | Feature enhancement template | `ai-docs/templates/existing-widget/feature-enhancement.md` | | Playwright E2E template (4-step) | `ai-docs/templates/playwright/` | -| SDK API reference (TypeDoc JSON) | `contact-centre-sdk-apis/contact-center.json` | +| SDK API reference (installed `.d.ts`) | `node_modules/@webex/contact-center/dist/types/index.d.ts` | | Per-package architecture & agent docs | `packages/contact-center/{pkg}/ai-docs/` | When working in a specific package, always read that package's `ai-docs/AGENTS.md` and `ai-docs/ARCHITECTURE.md` first. diff --git a/ai-docs/ARCHITECTURE.md b/ai-docs/ARCHITECTURE.md index 6dcdaea3a..ed36076d2 100644 --- a/ai-docs/ARCHITECTURE.md +++ b/ai-docs/ARCHITECTURE.md @@ -70,7 +70,6 @@ re-render. Grounded in `packages/contact-center/store/src/store.ts` and `storeEv | `@r2wc/react-to-web-component` | external | React→Web Component wrapping (cc-widgets only) | Per package.json | ## State Model - The store holds the client-side session model: agent profile/state, login options (teams, device type), task map and per-task lifecycle, and fetched lists (queues, entry points, buddy agents, address book). Transitions are driven by user-invoked `store.cc.*` methods and by SDK events the store proxies; all @@ -88,7 +87,24 @@ must mount in both React hosts and framework-agnostic hosts (via r2wc). Prefer m to avoid unnecessary re-renders. Backward compatibility of exported surfaces and custom-element names is a release concern (see `CONTRACTS.md`). - +## Dependency / Interaction Topology +The who-calls-whom call graph and the SDK event topology. Calls are synchronous (React render / hook → +store method); events are asynchronous (SDK → store observables → observing widgets). Grounded in +`packages/contact-center/store/src/storeEventsWrapper.ts` (event wiring) and each package's `helper.ts`. +``` +Host ──call──> Widget(observer) ──call──> Hook(helper.ts) ──call──> Store ──call──> @webex/contact-center SDK +SDK ──event(CC_EVENTS/TASK_EVENTS)──> Store(runInAction) ──observable change──> Widget(observer) re-render +Hook ──call──> cc-components (props) ──call──> ui-logging(withMetrics) +``` +| From | To | Kind | Purpose | +|---|---|---|---| +| Widget (observer) | Custom hook (`helper.ts`) | call | Read derived state, obtain action callbacks | +| Custom hook | Store (`store.cc.*`, mutators) | call | Invoke SDK operations; mutate observables via `runInAction` | +| Store | `@webex/contact-center` SDK | call | All telephony/agent/task operations (sole SDK boundary) | +| `@webex/contact-center` SDK | Store | event | `CC_EVENTS` / `TASK_EVENTS` proxied into observables (`storeEventsWrapper.ts`) | +| Store observables | Widget (observer) | event | MobX reactivity re-renders observing widgets | +| Widget / component | `ui-logging` (`withMetrics`) | call | Emit mount/unmount/error telemetry | + ## Package Map & Inter-Package Dependencies - **Workspace tooling:** Yarn 4.5.1 (PnP). Workspace globs: `packages/**/*`, `packages/contact-center/*`, `widgets-samples/**/**`. @@ -109,13 +125,11 @@ store ──> (no internal CC deps) ──> @webex/contact-center SDK - **Different-kind package:** `packages/@webex/widgets` is the legacy **meetings** widget family — it does not participate in the CC dependency flow or share the CC store. - ## Release & Versioning - Published as `@webex/*` packages; release driven by `semantic-release` (`yarn release:widgets`). - Public surfaces (exports, custom-element tag names, events) follow semver; breaking changes need a major bump and a consumer transition note. See `CONTRACTS.md` for the compatibility policy. - ## Host Integration & Theming - Widgets mount in two ways: React components (import from the widget package or `cc-widgets`) and custom elements (r2wc, registered by `cc-widgets`). Hosts must load Momentum UI CSS @@ -131,3 +145,13 @@ store ──> (no internal CC deps) ──> @webex/contact-center SDK | Architecture decisions | `adr/` | To understand why major design choices were made and what alternatives were rejected | | Repo patterns | `patterns/` | To follow established implementation conventions (TypeScript, React, MobX, testing) | | Enforceable rules | `RULES.md` + `rules/` | To understand constraints every architecture-affecting change must obey | + +## WS6 References +N/A — this repository has no WS6 / platform / enterprise-architecture specs. It is a self-contained UI +library over the `@webex/contact-center` SDK; the SDK's own contract (`node_modules/@webex/contact-center/dist/types/index.d.ts`) +is the only upstream architecture reference, and it is already cited in `CONTRACTS.md` and the store spec. +Add rows here if a WS6 spec is later published for this component. + +| WS6 artifact | Relevance to this repo | Link | +|---|---|---| +| _none_ | — | — | diff --git a/ai-docs/CONTRACTS.md b/ai-docs/CONTRACTS.md index 6d2510676..11862cc93 100644 --- a/ai-docs/CONTRACTS.md +++ b/ai-docs/CONTRACTS.md @@ -21,16 +21,18 @@ The aggregator package `@webex/cc-widgets` re-exports every widget plus the `sto | cc-widgets.TaskList | `@webex/cc-task` | `TaskList` | React component; custom element `widget-cc-task-list`; props `onTaskAccepted`, `onTaskDeclined`, `onTaskSelected` (functions), `hasCampaignPreviewEnabled` (boolean) | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | | cc-widgets.OutdialCall | `@webex/cc-task` | `OutdialCall` | React component; custom element `widget-cc-outdial-call`; no declared props | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | | cc-widgets.RealTimeTranscript | `@webex/cc-task` | `RealTimeTranscript` | React component; custom element `widget-cc-realtime-transcript`; props `liveTranscriptEntries` (json), `className` (string) | stable semver | `packages/contact-center/task/ai-docs/task-spec.md` | `packages/contact-center/task/src/index.ts` | -| cc-widgets.DigitalChannels | `@webex/cc-digital-channels` | `DigitalChannels` | React component; custom element `widget-cc-digital-channels`; no declared props | stable semver | `packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md` | `packages/contact-center/cc-digital-channels/src/index.tsx` | +| cc-widgets.DigitalChannels | `@webex/cc-digital-channels` | `DigitalChannels` | React component; custom element `widget-cc-digital-channels`; no declared props | stable semver | `packages/contact-center/cc-digital-channels/ai-docs/cc-digital-channels-spec.md` | `packages/contact-center/cc-digital-channels/src/index.ts` | | cc-widgets.store | `@webex/cc-store` | `store` | MobX singleton (`Store.getInstance()`); `init(options: InitParams, setupEventListeners): Promise`; sole SDK access point via `store.cc.*` | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/index.ts:1,4` | -| store.types | `@webex/cc-store` | Type re-exports (`IContactCenter`, `ITask`, `Profile`, `Team`, `AgentLogin`, `IStore`, `ILogger`, `InitParams`, `IWebex`, `RealTimeTranscriptionData`, plus ~20 more) | TypeScript `type`/`interface` exports describing the SDK-backed domain surface | stable semver; SDK-shaped types track SDK | `contact-centre-sdk-apis/contact-center.json` (SDK source); `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/store.types.ts:334-366` | +| store.types | `@webex/cc-store` | Type re-exports (`IContactCenter`, `ITask`, `Profile`, `Team`, `AgentLogin`, `IStore`, `ILogger`, `InitParams`, `IWebex`, `RealTimeTranscriptionData`, plus ~20 more) | TypeScript `type`/`interface` exports describing the SDK-backed domain surface | stable semver; SDK-shaped types track SDK | `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) (SDK source); `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/store.types.ts:334-366` | | store.constants | `@webex/cc-store` | Value/enum re-exports (`CC_EVENTS`, `TASK_EVENTS`, `LoginOptions`, `ConsultStatus`, `CAMPAIGN_PREVIEW_OUTBOUND_TYPES`, `DESKTOP`, `EXTENSION`, etc.) | Exported consts/enums for event names and login/consult/campaign domain values | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/store.types.ts:368-403` | | store.task-utils | `@webex/cc-store` | Pure task helpers (`isIncomingTask`, `getTaskStatus`, `getConsultStatus`, `getConferenceParticipants`, `isInteractionOnHold`, `findHoldStatus`, etc.) | `(task: ITask, agentId?: string) => boolean \| string \| number \| Participant[]` selectors over SDK task objects | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/task-utils.ts` | +| ui-logging.withMetrics | `@webex/cc-ui-logging` | `withMetrics` | `withMetrics

(Component, widgetName: string): React.MemoExoticComponent` HOC that auto-emits mount/unmount/error metrics; every widget export is wrapped with it | stable semver; signature change is breaking | `packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md` | `packages/contact-center/ui-logging/src/index.ts` | +| ui-logging.WidgetMetrics | `@webex/cc-ui-logging` | `WidgetMetrics` (type) | `{ widgetName; event: 'WIDGET_MOUNTED' \| 'ERROR' \| 'WIDGET_UNMOUNTED' \| 'PROPS_UPDATED'; props?; timestamp; additionalContext? }` | stable semver; narrowing the `event` union or removing a field is breaking | `packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md` | `packages/contact-center/ui-logging/src/index.ts` | ## Requires — what this repo depends on | Dependency (service / package / datastore) | What is consumed | Schema / detail link | Availability assumption | Fallback on failure | Version floor | |---|---|---|---|---|---| -| `@webex/contact-center` SDK | The entire CC runtime: `Webex.init()`, `webex.cc.*` methods, CC/task event stream, agent `Profile`, `webex.credentials.getUserToken()` | `contact-centre-sdk-apis/contact-center.json` (TypeDoc); consumed only via the store (`packages/contact-center/store/src/storeEventsWrapper.ts`) | Host establishes the authenticated Webex session; SDK assumed reachable | `Store.init()` rejects after a 6s init timeout; widgets stay inert and surface error UI (`packages/contact-center/store/src/store.ts:140-142`) | Pinned by the SDK dependency in each package's `package.json` | +| `@webex/contact-center` SDK | The entire CC runtime: `Webex.init()`, `webex.cc.*` methods, CC/task event stream, agent `Profile`, `webex.credentials.getUserToken()` | `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`); consumed only via the store (`packages/contact-center/store/src/storeEventsWrapper.ts`) | Host establishes the authenticated Webex session; SDK assumed reachable | `Store.init()` rejects after a 6s init timeout; widgets stay inert and surface error UI (`packages/contact-center/store/src/store.ts:140-142`) | Pinned by the SDK dependency in each package's `package.json` | | `react` / `react-dom` (18) | Component runtime; consumer peer dependency | React docs | Provided by host or bundled | N/A (build-time/runtime peer) | React 18 | | `mobx` / `mobx-react-lite` | Store reactivity (`runInAction`, `observer`) | MobX docs | Bundled with store package | N/A | per `package.json` | | `@r2wc/react-to-web-component` | Wraps React widgets as custom elements (`packages/contact-center/cc-widgets/src/wc.ts:1`) | r2wc docs | Bundled with `cc-widgets` | N/A | per `package.json` | diff --git a/ai-docs/GLOSSARY.md b/ai-docs/GLOSSARY.md index 4727a5ca3..500514ba0 100644 --- a/ai-docs/GLOSSARY.md +++ b/ai-docs/GLOSSARY.md @@ -1,6 +1,6 @@ # Glossary — webex-widgets -> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc; related module specs are indexed in `SPEC_INDEX.md`. +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc; related: [`CONTRACTS.md`](CONTRACTS.md); module specs indexed in `SPEC_INDEX.md`. > Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. > Read this before naming anything. Use the canonical name exactly; never introduce a synonym. Find a term @@ -55,4 +55,4 @@ ## Maintenance - When a new domain concept is introduced (new entity, event, state), add it here in the same change. -- Cross-reference: module specs → `SPEC_INDEX.md`; SDK surface → `contact-centre-sdk-apis/contact-center.json`. +- Cross-reference: module specs → `SPEC_INDEX.md`; contracts → [`CONTRACTS.md`](CONTRACTS.md); SDK surface → `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`). diff --git a/ai-docs/RULES.md b/ai-docs/RULES.md index 666f3310e..d82984c4c 100644 --- a/ai-docs/RULES.md +++ b/ai-docs/RULES.md @@ -1,6 +1,6 @@ # Rules — webex-widgets -> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry, carries the critical rules) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc; per-language detail in `patterns/`. +> Start here → root [`AGENTS.md`](../AGENTS.md) (agent entry, carries the critical rules) · router [`SPEC_INDEX.md`](SPEC_INDEX.md) · system [`ARCHITECTURE.md`](ARCHITECTURE.md). Then this doc; per-language detail in `patterns/`, individual enforceable rules in `rules/`. > Context-efficiency: link to canonical docs — don't duplicate them; load on demand, not upfront. > These rules are checkable. Every MUST rule records its source requirement/risk, verification path, @@ -15,6 +15,7 @@ Coverage state is mirrored from `.sdd/manifest.json`. Every module is currently | `store` (`@webex/cc-store`, `packages/contact-center/store`) | DRAFT | Tier-1, sole SDK access point (`store.ts` `getInstance`). Spec `store/ai-docs/store-spec.md` is unvalidated — verify observables/SDK proxying against `store/src/store.ts` and `store/src/storeEventsWrapper.ts`. | | `cc-components` (`@webex/cc-components`, `packages/contact-center/cc-components`) | DRAFT | Tier-1 presentational primitives. Verify prop contracts against `cc-components/src/` before trusting the spec. | | `cc-widgets` (`@webex/cc-widgets`, `packages/contact-center/cc-widgets`) | DRAFT | Tier-1 r2wc aggregator. The custom-element registry lives in `cc-widgets/src/wc.ts` — cross-check element names/attrs there. | +| `cc-digital-channels` (`@webex/cc-digital-channels`, `packages/contact-center/cc-digital-channels`) | DRAFT | Tier-1 digital-channels widget (`widget-cc-digital-channels`). Verify against `cc-digital-channels/src/`. | | `station-login` (`@webex/cc-station-login`, `packages/contact-center/station-login`) | DRAFT | Tier-1 widget. Verify against `station-login/src/`. | | `user-state` (`@webex/cc-user-state`, `packages/contact-center/user-state`) | DRAFT | Tier-1 widget. Verify state/idle-code logic against `user-state/src/helper.ts`. | | `task` (`@webex/cc-task`, `packages/contact-center/task`) | DRAFT | Tier-1 bundle of sub-widgets CallControl, CallControlCAD, IncomingTask, OutdialCall, TaskList. Verify per-widget behavior against `task/src/{Widget}/index.tsx` and `task/src/helper.ts`. | diff --git a/ai-docs/SECURITY.md b/ai-docs/SECURITY.md index cdc935f64..bf164274d 100644 --- a/ai-docs/SECURITY.md +++ b/ai-docs/SECURITY.md @@ -32,7 +32,7 @@ This repo is a client-side React/Web-Component widget library. It hosts no netwo |---|---|---|---|---| | Auth credential | `access_token` (host-supplied), SDK user token | Never persisted; passed to SDK only, held transiently in memory | Never log — confirmed: only error/status strings are logged in token paths, never the token value (`packages/contact-center/store/src/storeEventsWrapper.ts:994-998`) | Carried by the SDK over its own HTTPS transport; not handled by this repo | | Agent/customer PII | Caller name, phone number (DNIS/ANI), task/interaction data, transcript text, address-book entries | Held in memory as MobX observables only; no datastore in this repo | Never log raw PII; widget log lines carry `{module, method}` context, not payloads | SDK-owned HTTPS | -| Telemetry props | Widget metrics props passed to `metricsLogger` | Not persisted by this repo | **Risk: props are NOT sanitized today** — `metricsLogger` documents this explicitly (`packages/contact-center/ui-logging/src/metricsLogger.ts:73-76`). Do not pass PII-bearing objects as metrics props; see Known Sensitive Areas | Telemetry sink owned by host/SDK | +| Telemetry props | Widget metrics props passed to `metricsLogger` | Not persisted by this repo | **Risk: props are NOT sanitized today** — noted in the `havePropsChanged` JSDoc `@remarks` ("we dont sanitize our props right now"), `packages/contact-center/ui-logging/src/metricsLogger.ts:73-76`. Do not pass PII-bearing objects as metrics props; see Known Sensitive Areas | Telemetry sink owned by host/SDK | ## Input Validation & Output Encoding Posture - Untrusted input enters only via host-supplied custom-element props/events (type-coerced by r2wc — `packages/contact-center/cc-widgets/src/wc.ts`) and via SDK event payloads (typed through `store.types`). Rendered output goes through React, which escapes interpolated text by sink; widget render paths use no `dangerouslySetInnerHTML`. There are no SQL/shell/query sinks in this client library, so parameterization is N/A. @@ -40,7 +40,7 @@ This repo is a client-side React/Web-Component widget library. It hosts no netwo ## Known Sensitive Areas & Accepted Risks | Area | Risk | Mitigation / why accepted | Owner | |---|---|---|---| -| `ui-logging` metrics props | Widget props are logged without sanitization (`metricsLogger.ts:73-76`) | Callers must not pass PII-bearing objects as metrics props; sanitization is a documented future enhancement | cc-ui-logging maintainers | +| `ui-logging` metrics props | Widget props are logged without sanitization — acknowledged in the `havePropsChanged` JSDoc `@remarks` (`metricsLogger.ts:73-76`) | Callers must not pass PII-bearing objects as metrics props; sanitization is noted as a future enhancement | cc-ui-logging maintainers | | `getAccessToken()` SDK gap | `webex.credentials.getUserToken()` is `@ts-expect-error`-typed (SDK API not yet typed) (`storeEventsWrapper.ts:990-992`) | Token value is returned to the caller and never logged; failures log only an error message | cc-store maintainers | ## Reporting & Review diff --git a/ai-docs/SERVICE_STATE.md b/ai-docs/SERVICE_STATE.md index 59da4a2fe..d4f1b11d6 100644 --- a/ai-docs/SERVICE_STATE.md +++ b/ai-docs/SERVICE_STATE.md @@ -41,7 +41,7 @@ Feature flags are not owned or defaulted by this repo — they are read from the ## Compliance / Certifications - FedRAMP: PR template (`.github/PULL_REQUEST_TEMPLATE.md`) compliance is mandatory and must not be regressed (COMPLETES, Change Type, test scenarios, GAI Policy, Checklist sections). -- PII: agent/customer PII (names, phone numbers, task/transcript data) passes through widgets at runtime and must never be logged or persisted — see [`SECURITY.md`](SECURITY.md). Metrics props are not yet sanitized (`packages/contact-center/ui-logging/src/metricsLogger.ts:73-76`); do not pass PII-bearing objects to `metricsLogger`. +- PII: agent/customer PII (names, phone numbers, task/transcript data) passes through widgets at runtime and must never be logged or persisted — see [`SECURITY.md`](SECURITY.md). Metrics props are not yet sanitized — noted in the `havePropsChanged` JSDoc `@remarks` (`packages/contact-center/ui-logging/src/metricsLogger.ts:73-76`); do not pass PII-bearing objects to `metricsLogger`. ## Maintenance - Update the relevant row in the same change that adds/changes/removes a surface, dependency, limit, or flag (e.g. add a flag here when `getFeatureFlags()` in `util.ts` gains a key). diff --git a/ai-docs/SPEC_INDEX.md b/ai-docs/SPEC_INDEX.md index 2a926540b..7ace16721 100644 --- a/ai-docs/SPEC_INDEX.md +++ b/ai-docs/SPEC_INDEX.md @@ -12,6 +12,7 @@ | `store/` | MobX singleton; global CC state; proxies SDK events; sole SDK access point | DRAFT | `packages/contact-center/store/ai-docs/store-spec.md` | | `cc-components/` | Shared presentational React UI primitives | DRAFT | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` | | `cc-widgets/` | r2wc Web Component wrappers (aggregator) | DRAFT | `packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md` | +| `cc-digital-channels/` | Digital channels (chat/email/social) widget | DRAFT | `packages/contact-center/cc-digital-channels/ai-docs/cc-digital-channels-spec.md` | | `station-login/` | Agent login: team + device selection | DRAFT | `packages/contact-center/station-login/ai-docs/station-login-spec.md` | | `user-state/` | Agent state: state, idle codes, timer | DRAFT | `packages/contact-center/user-state/ai-docs/user-state-spec.md` | | `task/` | Task widget bundle: CallControl, IncomingTask, OutdialCall, TaskList, CallControlCAD | DRAFT | `packages/contact-center/task/ai-docs/task-spec.md` | @@ -24,7 +25,7 @@ |---|---| | Understanding the system | `ARCHITECTURE.md` | | Working in a module | that module's `-spec.md` (see registry); load only the relevant section | -| Changing the store / SDK access | `store-spec.md` + `ARCHITECTURE.md` Component Interaction; check `contact-centre-sdk-apis/contact-center.json` | +| Changing the store / SDK access | `store-spec.md` + `ARCHITECTURE.md` Component Interaction; check `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) | | Adding/changing a public surface (export, custom element, event) | `CONTRACTS.md` first, then the owning module spec | | A UI/component change | `cc-components-spec.md` + `patterns/react-patterns.md` | | A new widget | new-widget templates (`ai-docs/templates/new-widget/`); create the module spec as part of the change | @@ -64,6 +65,6 @@ The intake record confirms scope/modules **against the code** and sets the chang | Getting started | `ai-docs/GETTING_STARTED.md` | Clone/build/test loop, workspace layout | | Decision records | `ai-docs/adr/` | Standing ADRs — why the architecture is the way it is | | Review catalog | `ai-docs/REVIEW_CHECKLIST.md` | 6-core + 4-coverage + 3-cross-cutting review checks | -| SDK reference | `contact-centre-sdk-apis/contact-center.json` | TypeDoc of `@webex/contact-center` — verify every SDK call | +| SDK reference | `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) | installed SDK `.d.ts` surface — verify every SDK call | _No `DATA_MODEL.md`: this repo owns no persistent datastore (all domain data comes from the SDK at runtime)._ diff --git a/ai-docs/adr/0001-one-directional-dependency-flow.md b/ai-docs/adr/0001-one-directional-dependency-flow.md index d63866e44..947535bd3 100644 --- a/ai-docs/adr/0001-one-directional-dependency-flow.md +++ b/ai-docs/adr/0001-one-directional-dependency-flow.md @@ -1,3 +1,12 @@ + + # ADR-0001 — One-directional dependency flow with a single SDK boundary > Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../ARCHITECTURE.md). This is a standing `ai-docs/adr/` decision record; the folder README explains numbering/supersession. diff --git a/ai-docs/patterns/README.md b/ai-docs/patterns/README.md new file mode 100644 index 000000000..9b28966a1 --- /dev/null +++ b/ai-docs/patterns/README.md @@ -0,0 +1,47 @@ + + +# ai-docs/patterns/ — repo conventions (correct vs incorrect) + +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is the `ai-docs/patterns/` folder README; per-pattern files carry their own navigation pointer. +> Context-efficiency: link to canonical docs — don't duplicate them; one small, code-grounded pattern per section. + +Conventions the linter doesn't catch — the local idioms for MobX state, the Widget → Hook → Component +layering, TypeScript prop-derivation, and how tests mock the store — extracted from **real source** +(a convention seen in 3+ files), never invented. + +## Use Patterns For + +Use patterns when a convention is visible in real code but not enforced by tooling. A pattern shows a +correct form (copied from real source, with a `// from ` anchor) and the common incorrect form, so +a future agent follows the local style instead of guessing. + +- **Fill-in shape:** each pattern uses **When to use · Correct · Incorrect** (+ **Why wrong**) **· Where + it appears · Edge cases / exceptions** (see the SDLC template `_pattern-example.md`). +- **Grounded in real code:** the `Correct` snippet is copied from a real file and the `Where it appears` + list names 3+ real paths under `packages/contact-center/*/src/` (or `playwright/`). A convention seen in + fewer than 3 files is kept as a **Candidate** note, not promoted to an enforceable pattern. +- **Defer to the linter:** if ESLint/Prettier/CI already enforces something, point to the tool rather + than writing a pattern here. + +## Routing + +The SDLC standard puts generic patterns directly in `ai-docs/patterns/` and language-specific ones in +`ai-docs/patterns//`. This repo instead groups patterns **by language/layer in one file per +group** (a pre-standard layout kept for continuity), each holding several patterns in the shape above: + +| File | Covers | +|---|---| +| [`mobx-patterns.md`](./mobx-patterns.md) | Singleton store, `runInAction` mutations, `observer` widgets, store-event wiring | +| [`react-patterns.md`](./react-patterns.md) | Widget → Hook → Component layering, `ErrorBoundary`, `helper.ts` hooks, effect cleanup | +| [`typescript-patterns.md`](./typescript-patterns.md) | `*.types.ts` co-location, `Pick`/`Partial` prop derivation, in-repo event enums, JSDoc | +| [`testing-patterns.md`](./testing-patterns.md) | Jest + RTL store mocking, `renderHook`, `data-testid`, Playwright `TestManager` + Utils functions | + +Enforceable, single-constraint rules live in [`../rules/`](../rules/); standing decisions in +[`../adr/`](../adr/); the repo-wide rules digest is [`../RULES.md`](../RULES.md). diff --git a/ai-docs/patterns/mobx-patterns.md b/ai-docs/patterns/mobx-patterns.md index 9e799f857..fe2ac0ed3 100644 --- a/ai-docs/patterns/mobx-patterns.md +++ b/ai-docs/patterns/mobx-patterns.md @@ -1,315 +1,245 @@ -# MobX Patterns + -> Quick reference for LLMs working with MobX state management in this repository. +# Pattern: MobX state management ---- - -## Rules +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is an `ai-docs/patterns/` file; the folder [README](./README.md) explains the per-pattern shape and routing. +> Context-efficiency: link to canonical docs — don't duplicate them. See also ADR [`0001`](../adr/0001-one-directional-dependency-flow.md) (single SDK boundary) and rule [`sdk-access-via-store`](../rules/sdk-access-via-store.md). -- **MUST** use the singleton store pattern via `Store.getInstance()` -- **MUST** wrap widgets with `observer` HOC from `mobx-react-lite` -- **MUST** use `runInAction` for all state mutations -- **MUST** access store via `import store from '@webex/cc-store'` -- **MUST** mark state properties as `observable` -- **MUST** use `makeObservable` in store constructor -- **NEVER** mutate state outside of `runInAction` -- **NEVER** access store directly in presentational components -- **NEVER** create multiple store instances +Language/layer group: **MobX** (state lives in `@webex/cc-store`; the SDK is reached only through it). +Each section below is one pattern in the standard shape. --- -## Store Singleton Pattern +## Singleton store via `Store.getInstance()` -```typescript -// store.ts -import { makeObservable, observable, action, runInAction } from 'mobx'; +**When to use:** Whenever any package needs contact-center state or the SDK. There is exactly one store +for the whole repo; never construct a second one. -class Store { +**Correct** +```typescript +// from packages/contact-center/store/src/store.ts +class Store implements IStore { private static instance: Store; - // Observable state - agentId: string = ''; - currentState: string = ''; + teams: Team[] = []; idleCodes: IdleCode[] = []; - isLoggedIn: boolean = false; - - private constructor() { - makeObservable(this); + agentId: string = ''; + + constructor() { + makeAutoObservable(this, { + cc: observable.ref, // don't deep-observe the SDK instance + }); } - - static getInstance(): Store { + + public static getInstance(): Store { if (!Store.instance) { Store.instance = new Store(); } return Store.instance; } } - -export default Store.getInstance(); ``` +The store uses **`makeAutoObservable`** in the constructor — plain property declarations, no decorators. +The exported default is the singleton wrapper (`packages/contact-center/store/src/index.ts` re-exports +`storeEventsWrapper`, which holds `Store.getInstance()`). ---- - -## makeAutoObservable Pattern - -**ALWAYS use `makeAutoObservable` for store classes in this repository.** - +**Incorrect** ```typescript -import { makeAutoObservable, observable } from 'mobx'; +import {Store} from '@webex/cc-store'; +const store = new Store(); // second instance +``` +**Why wrong:** A second instance holds its own observable state and its own SDK connection, so widgets +observe stale data and events fire against the wrong store. Global CC state must stay coherent across +widgets (see ADR-0001). -class Store implement IStore{ - // Plain property declarations (no decorators) +**Where it appears** +- `packages/contact-center/store/src/store.ts` (class + `getInstance`) , `packages/contact-center/store/src/storeEventsWrapper.ts` (constructs `Store.getInstance()`) , `packages/contact-center/store/src/index.ts` (exports the singleton as default) - private static instance: Store; - agentId: string = ''; - teams: Team[] = []; - currentState: string = ''; - isLoggedIn: boolean = false; - cc: ContactCenter | null = null; - - constructor() { - // makeAutoObservable automatically makes properties observable - makeAutoObservable(this, { - // Only specify overrides for special cases - cc: observable.ref, // Don't observe nested properties on the SDK instance - }); - } -} -``` +**Edge cases / exceptions** +- Decorator-style MobX (`@observable` / `@action`) is **not** used here; the store is `makeAutoObservable`. Ignore any legacy example showing decorators. +- `cc` is registered as `observable.ref` so MobX tracks the reference swap, not the SDK's internal fields. --- -## runInAction Pattern +## Mutate observable state only inside `runInAction` -**ALWAYS use runInAction for state mutations:** +**When to use:** Every write to a store observable that happens outside a MobX action — i.e. inside an +`async` continuation, a promise `.then`, an SDK event callback, or a setter helper. +**Correct** ```typescript -import { runInAction } from 'mobx'; - -// ✅ CORRECT -const handleLogin = async () => { - const result = await cc.login(); +// from packages/contact-center/store/src/storeEventsWrapper.ts +setDigitalChannelsInitialized = (value: boolean): void => { runInAction(() => { - store.agentId = result.agentId; - store.isLoggedIn = true; - store.teams = result.teams; + this.store.isDigitalChannelsInitialized = value; }); }; +``` -// ❌ WRONG - Direct mutation +**Incorrect** +```typescript const handleLogin = async () => { const result = await cc.login(); - store.agentId = result.agentId; // NOT ALLOWED + store.agentId = result.agentId; // direct mutation, no runInAction }; ``` +**Why wrong:** With `makeAutoObservable`, mutating an observable outside an action after an `await` +triggers MobX strict-mode warnings and batches inconsistently, so observers may re-render mid-update or +not at all. ---- - -## Observer HOC Pattern - -**ALWAYS wrap widgets that access store with observer:** - -```typescript -import { observer } from 'mobx-react-lite'; -import store from '@webex/cc-store'; +**Where it appears** +- `packages/contact-center/store/src/storeEventsWrapper.ts` (dozens of setters and event handlers wrap writes in `runInAction`) , `packages/contact-center/task/src/helper.ts` , `packages/contact-center/user-state/src/helper.ts` -const UserStateInternal: React.FC = observer((props) => { - // Access store - component will re-render when these change - const { currentState, idleCodes, agentId } = store; - - return ( - - ); -}); -``` +**Edge cases / exceptions** +- Reads never need `runInAction`; only writes to observables do. +- The store exposes setter methods (`setAgentProfile`, `setCurrentState`, …) that already wrap + `runInAction`; prefer calling those from widgets/hooks over mutating `store.x` directly. --- -## Store Import Pattern +## Wrap store-consuming widgets with `observer` + +**When to use:** Any widget component that reads store observables and must re-render when they change. +**Correct** ```typescript -// ✅ CORRECT - Import singleton +// from packages/contact-center/user-state/src/user-state/index.tsx import store from '@webex/cc-store'; +import {observer} from 'mobx-react-lite'; -const MyWidget = observer(() => { - const { agentId, teams } = store; +const UserStateInternal: React.FunctionComponent = observer(({onStateChange}) => { + const {cc, idleCodes, agentId, currentState} = store; // ... }); - -// ❌ WRONG - Creating new instance -import { Store } from '@webex/cc-store'; -const store = new Store(); // NOT ALLOWED ``` ---- - -## Action Pattern - +**Incorrect** ```typescript -import { action, makeObservable } from 'mobx'; - -class Store { - @observable currentState: string = ''; - - constructor() { - makeObservable(this); - } - - @action - setCurrentState(state: string) { - this.currentState = state; - } - - @action - reset() { - this.currentState = ''; - this.agentId = ''; - this.isLoggedIn = false; - } -} +const UserStateInternal = ({onStateChange}) => { + const {idleCodes} = store; // reads observables but no observer() + // ... +}; ``` +**Why wrong:** Without `observer`, the component captures observable values once and never re-renders +when the store updates, so the UI silently goes stale. ---- - -## Computed Pattern - -```typescript -import { observable, computed, makeObservable } from 'mobx'; +**Where it appears** +- `packages/contact-center/user-state/src/user-state/index.tsx` , `packages/contact-center/station-login/src/station-login/index.tsx` , `packages/contact-center/task/src/CallControl/index.tsx` (also `IncomingTask`, `TaskList`, `OutdialCall`, `CallControlCAD`, `RealTimeTranscript`, `cc-digital-channels`) -class Store { - tasks: ITask[] = []; - - constructor() { - makeObservable(this); - } - - get activeTasks(): ITask[] { - return this.tasks.filter(task => task.status === 'active'); - } - - get taskCount(): number { - return this.tasks.length; - } -} -``` +**Edge cases / exceptions** +- Purely presentational components in `cc-components` receive data via props and must **not** read the store, so they are not `observer`-wrapped. +- The outer `ErrorBoundary` wrapper (see [react-patterns](./react-patterns.md)) is not an `observer`; only the inner `*Internal` component is. --- -## Event Handling with Store Pattern +## Import the store as a default singleton +**When to use:** Any file (widget, hook, logger) that needs store state or `store.cc`. + +**Correct** ```typescript -import { runInAction } from 'mobx'; +// from packages/contact-center/user-state/src/helper.ts import store from '@webex/cc-store'; +``` +The default export of `@webex/cc-store` is the already-instantiated singleton wrapper — importing it gives +every consumer the same instance. -// In helper.ts or hook -useEffect(() => { - const handleTaskIncoming = (task: ITask) => { - runInAction(() => { - store.incomingTask = task; - }); - }; - - store.cc.on(TASK_EVENTS.TASK_INCOMING, handleTaskIncoming); - - return () => { - store.cc.off(TASK_EVENTS.TASK_INCOMING, handleTaskIncoming); - }; -}, []); +**Incorrect** +```typescript +import {Store} from '@webex/cc-store'; +const store = Store.getInstance(); // works, but never `new Store()` — and prefer the default import ``` +**Why wrong:** Constructing or re-resolving the instance in consumers scatters access styles; the default +import is the one supported entry point and keeps the singleton contract obvious. + +**Where it appears** +- `packages/contact-center/user-state/src/helper.ts` , `packages/contact-center/station-login/src/station-login/index.tsx` , `packages/contact-center/task/src/helper.ts` (and every widget `index.tsx` + `ui-logging/src/metricsLogger.ts`) + +**Edge cases / exceptions** +- Inside the store package itself, code references `Store.getInstance()` / the wrapper directly rather than the published default export. --- -## Store Wrapper Pattern +## Central SDK-event wiring in the store wrapper -```typescript -// storeEventsWrapper.ts -import { runInAction } from 'mobx'; -import store from './store'; - -export const initStoreEventListeners = () => { - store.cc.on(CC_EVENTS.AGENT_STATE_CHANGED, (data) => { - runInAction(() => { - store.currentState = data.state; - store.lastStateChangeTimestamp = Date.now(); - }); - }); +**When to use:** Registering handlers for SDK events (`agent:*`, `task:*`) that update global state. This +lives in the store, not in widgets. - store.cc.on(CC_EVENTS.AGENT_LOGOUT_SUCCESS, () => { - runInAction(() => { - store.reset(); - }); +**Correct** +```typescript +// from packages/contact-center/store/src/storeEventsWrapper.ts +const handleLogin = (payload: Profile) => { + runInAction(() => { + this.setAgentProfile(payload); + this.setIsAgentLoggedIn(true); + this.setCurrentState(payload.auxCodeId?.trim() !== '' ? payload.auxCodeId : '0'); }); }; -``` - ---- -## Store Access in Widgets +ccSDK.on(CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS, handleLogin); +``` +**Incorrect** ```typescript -// Widget file -import { observer } from 'mobx-react-lite'; -import store from '@webex/cc-store'; - -const StationLoginInternal = observer(() => { - // Destructure what you need from store - const { - cc, - teams, - dialNumbers, - isAgentLoggedIn, - loginConfig, - } = store; - - // Use in component - return ( - - ); -}); +// in a widget index.tsx — global agent state wired up per-widget +store.cc.on(CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS, (p) => { store.agentId = p.agentId; }); ``` +**Why wrong:** Global state (login, agent state, current task) wired in a widget duplicates listeners, +leaks them on unmount, and mutates observables outside `runInAction`. Global wiring belongs in the store +wrapper so there is one registration and one source of truth. + +**Where it appears** +- `packages/contact-center/store/src/storeEventsWrapper.ts` (login, logout, state-change, task events) — this is the single place global SDK events are wired. + +**Edge cases / exceptions** +- **Widget-local, task-scoped** listeners are legitimate in a hook, but go through the store's + `setTaskCallback` / `removeTaskCallback` helpers with `useEffect` cleanup (see the effect-cleanup + pattern in [react-patterns](./react-patterns.md)), not raw `cc.on` in the widget. --- -## Async Action Pattern +## Computed-style getters on the store wrapper -```typescript -import { runInAction } from 'mobx'; +**When to use:** Exposing a derived view of raw store state (e.g. filtering system idle codes) so +consumers read a ready-to-use value. -const fetchData = async () => { - // Set loading state - runInAction(() => { - store.isLoading = true; - store.error = null; +**Correct** +```typescript +// from packages/contact-center/store/src/storeEventsWrapper.ts +get idleCodes() { + return this.store.idleCodes.filter((code) => { + return Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(code.name) || !code.isSystem; }); +} +``` +Derivations are plain TypeScript getters on the `StoreWrapper` that proxy/transform the underlying +`Store` observables (there are 30+ such getters on the wrapper). Because the wrapper reads observables +inside the getter, `observer` components stay reactive. - try { - const result = await store.cc.fetchTeams(); - - // Update with result - runInAction(() => { - store.teams = result.teams; - store.isLoading = false; - }); - } catch (error) { - // Handle error - runInAction(() => { - store.error = error; - store.isLoading = false; - }); - } -}; +**Incorrect** +```typescript +// filtering the same derived value inside every widget +const usable = store.idleCodes.filter((c) => !c.isSystem); // duplicated derivation logic ``` +**Why wrong:** Copying the derivation into each consumer drifts over time and re-implements the same rule +in many places; centralizing it on the wrapper keeps one definition. + +**Where it appears** +- `packages/contact-center/store/src/storeEventsWrapper.ts` (`get idleCodes`, `get currentTask`, `get taskList`, `get currentState`, and other proxy getters). + +**Edge cases / exceptions** +- These are TypeScript getters on the wrapper, **not** MobX `@computed` — there is no memoization; keep the derivation cheap. +- The plain `Store` class holds no getters; all derivation lives on the wrapper. --- ## Related -- [React Patterns](./react-patterns.md) -- [TypeScript Patterns](./typescript-patterns.md) -- [Testing Patterns](./testing-patterns.md) +- [React Patterns](./react-patterns.md) · [TypeScript Patterns](./typescript-patterns.md) · [Testing Patterns](./testing-patterns.md) +- Rule: [Access the SDK only through the store](../rules/sdk-access-via-store.md) · ADR: [One-directional dependency flow](../adr/0001-one-directional-dependency-flow.md) diff --git a/ai-docs/patterns/react-patterns.md b/ai-docs/patterns/react-patterns.md index ccd133620..a8bb9b5e9 100644 --- a/ai-docs/patterns/react-patterns.md +++ b/ai-docs/patterns/react-patterns.md @@ -1,288 +1,262 @@ -# React Patterns + -> Quick reference for LLMs working with React in this repository. +# Pattern: React component structure ---- - -## Rules - -- **Component style** - - **MUST** use functional components with hooks - - **MUST NOT** use class components - -- **Three-layer architecture (Widget → Hook → Component)** - - **MUST** follow the pattern: **Widget → Hook → Presentational Component** - - **MUST** encapsulate business logic and SDK calls inside custom hooks (`helper.ts`) - - **MUST** keep presentational components in the `cc-components` package - - **MUST NOT** access store directly in presentational components - - **MUST NOT** call SDK methods directly from widgets or presentational components (only from hooks) +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is an `ai-docs/patterns/` file; the folder [README](./README.md) explains the per-pattern shape and routing. +> Context-efficiency: link to canonical docs — don't duplicate them. The one-directional layering is fixed by ADR [`0001`](../adr/0001-one-directional-dependency-flow.md). -- **MobX + Error handling** - - **MUST** wrap every widget with `ErrorBoundary` from `react-error-boundary` - - **MUST** use `observer` from `mobx-react-lite` for widgets that access the store +Language/layer group: **React**. Functional components with hooks only — no class components. +Each section below is one pattern in the standard shape. --- -## Three-Layer Architecture +## Three-layer architecture: Widget → Hook → Presentational Component -``` -┌─────────────────────────────────────┐ -│ Widget (observer) │ ← MobX observer, ErrorBoundary wrapper -│ packages/*/src/{widget}/index.tsx │ -├─────────────────────────────────────┤ -│ Custom Hook │ ← Business logic, SDK calls, events -│ packages/*/src/helper.ts │ -├─────────────────────────────────────┤ -│ Presentational Component │ ← Pure UI, props only -│ packages/cc-components/src/... │ -└─────────────────────────────────────┘ -``` - ---- -## Widget Pattern +**When to use:** Every user-facing feature. The widget (in a feature package) reads the store and wires +an `ErrorBoundary`; a `helper.ts` hook holds business logic and SDK calls; the presentational component +(in `cc-components`) is pure UI driven by props. +**Correct** ```typescript -// index.tsx -import { observer } from 'mobx-react-lite'; -import { ErrorBoundary } from 'react-error-boundary'; -import store from '@webex/cc-store'; -import { UserStateComponent } from '@webex/cc-components'; -import { useUserState } from '../helper'; -import { IUserStateProps } from './user-state.types'; - -const UserStateInternal: React.FC = observer((props) => { - const { onStateChange } = props; - - // Get data from store - const { cc, idleCodes, currentState, agentId } = store; - - // Use custom hook for logic - const { selectedState, isLoading, handleSetState } = useUserState({ - cc, +// from packages/contact-center/user-state/src/user-state/index.tsx +const UserStateInternal: React.FunctionComponent = observer(({onStateChange}) => { + const {cc, idleCodes, agentId, currentState /* ...from store */} = store; + const props: UserStateComponentsProps = { + ...useUserState({cc, idleCodes, agentId, currentState, onStateChange /* ... */}), idleCodes, currentState, - onStateChange, - }); - - // Render presentational component - return ( - - ); + }; + return ; }); +``` +The three real layers for this feature: +- Widget: `packages/contact-center/user-state/src/user-state/index.tsx` +- Hook: `packages/contact-center/user-state/src/helper.ts` (`useUserState`) +- Component: `packages/contact-center/cc-components/src/components/UserState/user-state.tsx` (`UserStateComponent`) -const UserState: React.FC = (props) => ( - <>} - onError={(error) => store.onErrorCallback?.('UserState', error)} - > - - -); - -export { UserState }; +**Incorrect** +```typescript +// a presentational component in cc-components reaching into the store +import store from '@webex/cc-store'; +export const UserStateComponent = () => { + const {idleCodes} = store; // component must not read the store or call the SDK +}; ``` +**Why wrong:** It reverses the dependency arrow (`cc-components` must not import the store/SDK) and makes +the component untestable in isolation — it can no longer be driven purely by props. See ADR-0001. ---- +**Where it appears** +- `user-state`: `.../user-state/src/user-state/index.tsx` → `.../user-state/src/helper.ts` → `.../cc-components/src/components/UserState/user-state.tsx` +- `station-login`: `.../station-login/src/station-login/index.tsx` → `.../station-login/src/helper.ts` → `.../cc-components/src/components/StationLogin/station-login.tsx` +- `task` (CallControl): `.../task/src/CallControl/index.tsx` → `.../task/src/helper.ts` → `.../cc-components/src/components/task/CallControl/call-control.tsx` -## Error Boundary Pattern +**Edge cases / exceptions** +- The `task` package has several widgets sharing one `helper.ts` (see the hooks pattern below). +- Small presentational sub-components may compose without their own hook, but data still arrives via props. -**ALWAYS wrap widgets with this pattern:** +--- -```typescript -import { ErrorBoundary } from 'react-error-boundary'; -import { observer } from 'mobx-react-lite'; -import store from '@webex/cc-store'; +## Wrap every widget with `ErrorBoundary` -// Internal observer component -const UserStateInternal: React.FC = observer((props) => { - // Widget logic here - return ; -}); +**When to use:** Every exported widget. An inner `observer` component does the work; an outer wrapper +catches render errors and reports them through `store.onErrorCallback`. -// External wrapper with ErrorBoundary -const UserState: React.FC = (props) => { +**Correct** +```typescript +// from packages/contact-center/task/src/CallControl/index.tsx +const CallControl: React.FunctionComponent = (props) => { return ( <>} onError={(error: Error) => { - if (store.onErrorCallback) { - store.onErrorCallback('UserState', error); - } + if (store.onErrorCallback) store.onErrorCallback('CallControl', error); }} > - + ); }; +``` -export { UserState }; +**Incorrect** +```typescript +// exporting the observer component directly, with no boundary +export {CallControlInternal as CallControl}; ``` +**Why wrong:** A render error in one widget would otherwise bubble up and blank out the whole host page. +The boundary contains the failure to that widget and forwards it to the host via `onErrorCallback`. + +**Where it appears** +- `packages/contact-center/user-state/src/user-state/index.tsx` , `packages/contact-center/station-login/src/station-login/index.tsx` , `packages/contact-center/task/src/CallControl/index.tsx` (also `IncomingTask`, `OutdialCall`, `CallControlCAD`) + +**Edge cases / exceptions** +- `fallbackRender={() => <>}` renders nothing on failure by design (widgets are embedded in a host app that owns the surrounding UI). +- The first `onError` argument is the widget name string — keep it matching the widget so host telemetry attributes errors correctly. --- -## Custom Hook Pattern +## Encapsulate logic in a `helper.ts` hook -**ALWAYS encapsulate business logic in hooks:** +**When to use:** Any SDK call, event subscription, timer, or local UI state a widget needs. It goes in a +`use*` hook exported from the feature's `helper.ts`, not inline in the widget. +**Correct** ```typescript -// helper.ts -export const useUserState = (props: UseUserStateProps) => { - const { cc, idleCodes, currentState, onStateChange } = props; - - const [selectedState, setSelectedState] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - // Event listener setup - useEffect(() => { - const handleStateChange = (data: StateChangeEvent) => { - setSelectedState(data.state); - onStateChange?.(data.state); - }; - - cc.on(CC_EVENTS.AGENT_STATE_CHANGED, handleStateChange); - - return () => { - cc.off(CC_EVENTS.AGENT_STATE_CHANGED, handleStateChange); - }; - }, [cc, onStateChange]); - - // Action handler - const handleSetState = useCallback(async (state: IdleCode) => { - setIsLoading(true); - try { - await cc.setAgentState(state); - } catch (error) { - console.error('Failed to set state:', error); - } finally { - setIsLoading(false); - } - }, [cc]); - - return { - selectedState, - isLoading, - handleSetState, - }; -}; +// from packages/contact-center/task/src/helper.ts +const loadBuddyAgents = useCallback(async () => { + try { + setLoadingBuddyAgents(true); + const agents = await store.getBuddyAgents(); + setBuddyAgents(agents); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error loading buddy agents - ${error.message || error}`, { + module: 'useCallControl', + method: 'loadBuddyAgents', + }); + setBuddyAgents([]); + } finally { + setLoadingBuddyAgents(false); + } +}, [logger]); ``` +Real hooks: `useUserState` (`user-state/src/helper.ts`), `useStationLogin` (`station-login/src/helper.ts`), +and `useTaskList` / `useIncomingTask` / `useCallControl` / `useOutdialCall` / `useRealTimeTranscript` +(all in `task/src/helper.ts`). + +**Incorrect** +```typescript +// SDK call inline in the widget instead of a hook +const CallControlInternal = observer((props) => { + const onHold = () => store.cc.hold(); // logic leaks into the widget +}); +``` +**Why wrong:** Inline logic can't be unit-tested with `renderHook`, gets duplicated across widgets, and +mixes rendering with side effects. Hooks keep the widget thin and the logic reusable/testable. + +**Where it appears** +- `packages/contact-center/user-state/src/helper.ts` , `packages/contact-center/station-login/src/helper.ts` , `packages/contact-center/task/src/helper.ts` (also `packages/contact-center/cc-digital-channels/src/helper.ts`) + +**Edge cases / exceptions** +- One `helper.ts` may export several hooks when a package hosts several widgets (the `task` package does). +- A few narrowly-reusable hooks live outside `helper.ts` — e.g. `task/src/Utils/useHoldTimer.ts`, `cc-components/src/hooks/useIntersectionObserver.ts` — when they're shared UI utilities rather than a widget's business logic. --- +## Pure presentational components in `cc-components` -## Presentational Component Pattern +**When to use:** All shared UI. Components take data and callbacks via props, render, and never touch the +store or SDK. +**Correct** ```typescript -// cc-components/src/components/UserState/UserState.tsx -import React from 'react'; -import { IUserStateComponentProps } from './user-state.types'; - -export const UserStateComponent: React.FC = ({ - idleCodes, - currentState, - selectedState, - isLoading, - onStateSelect, -}) => { +// from packages/contact-center/cc-components/src/components/UserState/user-state.tsx +const UserStateComponent: React.FunctionComponent = (props) => { + const {idleCodes, setAgentStatus, isSettingAgentStatus, currentState, customState, logger} = props; + const items = buildDropdownItems(customState, idleCodes, currentState, logger); return ( -

- {idleCodes.map((code) => ( - - ))} +
+ {/* renders from props only */}
); }; ``` +**Incorrect** +```typescript +import store from '@webex/cc-store'; // component pulling state itself +``` +**Why wrong:** Same as the layering rule — importing the store into `cc-components` reverses the +dependency arrow and destroys prop-driven testability. + +**Where it appears** +- `packages/contact-center/cc-components/src/components/UserState/user-state.tsx` , `packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx` , `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` (also `task/IncomingTask`, `task/TaskList`) + +**Edge cases / exceptions** +- Components may hold local view-only state (open/closed, hover) and use UI utility hooks; they just never own domain state or call the SDK. + --- -## useEffect Cleanup Pattern +## Clean up event/callback subscriptions in `useEffect` -**ALWAYS clean up event listeners and subscriptions:** +**When to use:** Any effect that registers a task/SDK callback or subscribes to an event. Always return a +cleanup that unregisters the exact same handler. +**Correct** ```typescript +// from packages/contact-center/task/src/helper.ts useEffect(() => { - const handler = (data: EventData) => { - // Handle event - }; + if (!currentTask?.data?.interactionId) return; + const interactionId = currentTask.data.interactionId; + + store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, interactionId); + store.setTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, interactionId); - cc.on(CC_EVENTS.SOME_EVENT, handler); - - // Cleanup function return () => { - cc.off(CC_EVENTS.SOME_EVENT, handler); + store.removeTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, interactionId); + store.removeTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, interactionId); }; -}, [cc]); +}, [currentTask]); ``` +Note the repo registers task-scoped listeners through the store's `setTaskCallback` / +`removeTaskCallback` helpers (not raw `cc.on` / `cc.off` in the widget). ---- - -## useCallback Pattern - -**ALWAYS use useCallback for handlers passed to child components:** - +**Incorrect** ```typescript -const handleClick = useCallback((id: string) => { - // Handle click -}, [dependency1, dependency2]); +useEffect(() => { + store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, interactionId); + // no return — handler never removed +}, [currentTask]); ``` +**Why wrong:** Without cleanup, handlers accumulate across re-renders/task changes, firing multiple times +and holding references to stale task state (a memory + double-fire leak). + +**Where it appears** +- `packages/contact-center/task/src/helper.ts` (task callbacks) , `packages/contact-center/user-state/src/helper.ts` (worker lifecycle) , `packages/contact-center/cc-digital-channels/src/helper.ts` + +**Edge cases / exceptions** +- The cleanup must reference the **same function identity** passed on registration (define handlers in the hook body or memoize them), or removal is a no-op. --- -## Conditional Rendering Pattern +## Memoize callbacks with `useCallback` +**When to use:** A handler defined in a hook that is (a) a dependency of another effect/hook or (b) +passed to a memoized child. Keeps identity stable across renders. + +**Correct** ```typescript -// Loading state -if (isLoading) { - return ; -} - -// Error state -if (error) { - return ; -} - -// Empty state -if (!data || data.length === 0) { - return ; -} - -// Normal render -return ; +// from packages/contact-center/task/src/helper.ts +const getEntryPoints = useCallback(async () => { + // ...fetch and set state... +}, [logger]); ``` ---- - -## Props Destructuring Pattern - +**Incorrect** ```typescript -const Component: React.FC = ({ - prop1, - prop2, - optionalProp = 'default', - onCallback, -}) => { - // Component logic -}; +const getEntryPoints = async () => { /* ... */ }; // new identity every render +useEffect(() => { getEntryPoints(); }, [getEntryPoints]); // effect re-runs every render ``` +**Why wrong:** A fresh function each render changes the effect's dependency identity, re-running the +effect on every render — an infinite-ish fetch loop. + +**Where it appears** +- `packages/contact-center/task/src/helper.ts` (`loadBuddyAgents`, `getAddressBookEntries`, `getEntryPoints`, `getQueuesFetcher`, `extractConsultingAgent`). + +**Edge cases / exceptions** +- Skip `useCallback` for handlers used only inline in JSX with no memoized child and no effect dependency — the memo overhead buys nothing there. --- ## Related -- [TypeScript Patterns](./typescript-patterns.md) -- [MobX Patterns](./mobx-patterns.md) -- [Web Component Patterns](./web-component-patterns.md) -- [Testing Patterns](./testing-patterns.md) +- [MobX Patterns](./mobx-patterns.md) · [TypeScript Patterns](./typescript-patterns.md) · [Testing Patterns](./testing-patterns.md) +- ADR: [One-directional dependency flow](../adr/0001-one-directional-dependency-flow.md) diff --git a/ai-docs/patterns/testing-patterns.md b/ai-docs/patterns/testing-patterns.md index 92f528192..3633bc811 100644 --- a/ai-docs/patterns/testing-patterns.md +++ b/ai-docs/patterns/testing-patterns.md @@ -1,372 +1,227 @@ -# Testing Patterns + -> Quick reference for LLMs working with tests in this repository. +# Pattern: Testing conventions ---- - -## Rules +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is an `ai-docs/patterns/` file; the folder [README](./README.md) explains the per-pattern shape and routing. +> Context-efficiency: link to canonical docs — don't duplicate them. Unit tests: Jest + React Testing Library, per package under `tests/`. E2E: Playwright under `playwright/`. -- **MUST** use Jest for unit tests -- **MUST** use React Testing Library for component tests -- **MUST** use Playwright for E2E tests -- **MUST** mock the store using `@webex/test-fixtures` -- **MUST** use `data-testid` attributes for test selectors -- **MUST** place unit tests in `tests/` folder within each package -- **MUST** place E2E tests in `playwright/` folder at repo root -- **NEVER** test implementation details - test behavior -- **NEVER** use CSS selectors in tests - use `data-testid` +Language/layer group: **Testing**. Each section below is one pattern in the standard shape. --- -## Test File Structure - -``` -packages/contact-center/{package}/ -├── src/ -│ └── {widget}/ -│ └── index.tsx -└── tests/ - └── {widget}/ - └── index.test.tsx - -playwright/ -├── tests/ -│ ├── station-login-test.spec.ts -│ ├── user-state-test.spec.ts -│ └── tasklist-test.spec.ts -└── Utils/ - ├── stationLoginUtils.ts - └── userStateUtils.ts -``` +## Mock `@webex/cc-store` at the module boundary (widget tests) ---- +**When to use:** A widget test that renders a widget which imports the store default export. Replace the +store with an inline mock object so the component's store reads are deterministic. -## Jest Unit Test Pattern +**Correct** +```typescript +// from packages/contact-center/user-state/tests/user-state/index.tsx +import store from '@webex/cc-store'; + +jest.mock('@webex/cc-store', () => { + return { + cc: {on: jest.fn(), off: jest.fn()}, + idleCodes: [], + agentId: 'testAgentId', + logger: {log: jest.fn(), info: jest.fn(), error: jest.fn(), warn: jest.fn()}, + currentState: '0', + onErrorCallback: jest.fn(), + }; +}); +``` +**Incorrect** ```typescript -// tests/{widget}/index.test.tsx -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { UserState } from '@webex/cc-user-state'; -import { mockStore } from '@webex/test-fixtures'; - -// Mock the store -jest.mock('@webex/cc-store', () => ({ - __esModule: true, - default: mockStore, -})); - -describe('UserState', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); +// mutating the real singleton in a test instead of mocking it +import store from '@webex/cc-store'; +store.idleCodes = [{id: '1', name: 'Available'}]; // leaks state across tests, needs a live SDK +``` +**Why wrong:** The store default export wires up SDK event listeners; using the real singleton makes tests +order-dependent and couples them to SDK internals. Mocking at the module boundary isolates the widget. - it('should render idle codes', () => { - mockStore.idleCodes = [ - { id: '1', name: 'Available' }, - { id: '2', name: 'Break' }, - ]; +**Where it appears** +- `packages/contact-center/user-state/tests/user-state/index.tsx` , `packages/contact-center/station-login/tests/station-login/index.tsx` , `packages/contact-center/task/tests/IncomingTask/index.tsx` (also `task/tests/{TaskList,OutdialCall,RealtimeTranscript}/index.tsx`, `cc-digital-channels/tests/digital-channels/index.tsx`) - render(); +**Edge cases / exceptions** +- The mock is an **inline object literal** in `jest.mock`, not a shared `mockStore` fixture. `@webex/test-fixtures` does **not** export a `mockStore`; its store-adjacent fixture is `mockCC` (a mock `IContactCenter` SDK instance) plus data fixtures (`mockProfile`, `mockTask`, `makeMockTask`, …). Use `mockCC` for the `cc` field and `@testing-library/jest-dom` for DOM matchers. - expect(screen.getByText('Available')).toBeInTheDocument(); - expect(screen.getByText('Break')).toBeInTheDocument(); - }); +--- + +## Test hooks with `renderHook` - it('should call onStateChange when state is selected', async () => { - const onStateChange = jest.fn(); - mockStore.idleCodes = [{ id: '1', name: 'Available' }]; +**When to use:** Unit-testing a `use*` hook's logic (state transitions, SDK calls, event handling) in +isolation, without rendering a widget. - render(); +**Correct** +```typescript +// from packages/contact-center/user-state/tests/helper.ts +import {renderHook, act, waitFor} from '@testing-library/react'; +import {useUserState} from '../src/helper'; +import {mockCC} from '@webex/test-fixtures'; - fireEvent.click(screen.getByText('Available')); +const {result} = renderHook(() => + useUserState({cc: mockCC, idleCodes, agentId, currentState: 'Available', onStateChange, logger}), +); - await waitFor(() => { - expect(onStateChange).toHaveBeenCalled(); - }); - }); +await act(async () => { + await result.current.setAgentStatus('2'); }); ``` ---- - -## Mock Store Pattern - +**Incorrect** ```typescript -// test-fixtures/src/mockStore.ts -export const mockStore = { - cc: { - on: jest.fn(), - off: jest.fn(), - login: jest.fn(), - logout: jest.fn(), - setAgentState: jest.fn(), - }, - agentId: 'test-agent-123', - isAgentLoggedIn: false, - teams: [], - idleCodes: [], - currentState: 'Available', - onErrorCallback: jest.fn(), -}; - -// Usage in test -jest.mock('@webex/cc-store', () => ({ - __esModule: true, - default: mockStore, -})); +// testing hook logic only through the widget DOM, so failures are hard to localize +render(); +fireEvent.click(screen.getByText('Available')); ``` +**Why wrong:** Driving a hook only through the widget mixes render concerns with logic and makes it hard to +exercise edge cases (errors, timers) directly; `renderHook` targets the hook's return surface. + +**Where it appears** +- `packages/contact-center/user-state/tests/helper.ts` , `packages/contact-center/station-login/tests/helper.ts` , `packages/contact-center/task/tests/helper.ts` (also `cc-digital-channels/tests/helper.ts`, `task/tests/utils/useHoldTimer.test.ts`, `cc-components/tests/hooks/useIntersectionObserver.test.ts`) + +**Edge cases / exceptions** +- `renderHook`/`act`/`waitFor` come from `@testing-library/react` (not the deprecated `@testing-library/react-hooks`). +- Hook tests live in the package's `tests/helper.ts` (mirroring `src/helper.ts`), which is why they aren't `*.test.tsx`. --- -## Hook Testing Pattern +## Select by `data-testid`, not CSS + +**When to use:** Locating elements in unit tests and Playwright specs. Components expose stable +`data-testid` attributes; tests query them. +**Correct** ```typescript -import { renderHook, act } from '@testing-library/react'; -import { useUserState } from '../../src/helper'; -import { mockStore } from '@webex/test-fixtures'; - -describe('useUserState', () => { - it('should handle state change', async () => { - const onStateChange = jest.fn(); - - const { result } = renderHook(() => - useUserState({ - cc: mockStore.cc, - idleCodes: mockStore.idleCodes, - currentState: 'Available', - onStateChange, - }) - ); - - await act(async () => { - await result.current.handleSetState({ id: '1', name: 'Break' }); - }); - - expect(mockStore.cc.setAgentState).toHaveBeenCalled(); - }); -}); +// component (packages/contact-center/cc-components/src/components/UserState/user-state.tsx) +
+// test +expect(screen.getByTestId('global-variables-panel')).toBeInTheDocument(); ``` ---- - -## data-testid Pattern - +**Incorrect** ```typescript -// In component - - -
- {/* content */} -
- -// In test -const loginButton = screen.getByTestId('login-button'); -const dropdown = screen.getByTestId('user-state-dropdown'); +container.querySelector('.user-state-container'); // brittle: breaks on a class rename ``` +**Why wrong:** Class names are styling details that change freely; `data-testid` is a deliberate test +contract, so selectors stay stable across restyles. + +**Where it appears** +- Components: `.../cc-components/src/components/UserState/user-state.tsx` , `.../cc-components/src/components/task/GlobalVariablesPanel/global-variables-panel.tsx` , `.../cc-components/src/components/StationLogin/station-login.tsx` +- Tests: `.../cc-components/tests/components/task/CampaignTask/campaign-variables-panel.test.tsx` and many others (`getByTestId` appears 80+ times across tests). + +**Edge cases / exceptions** +- Text assertions (`getByText`, `findByText`) are fine for user-visible copy; reserve `data-testid` for structural/interactive elements. --- -## Playwright E2E Test Pattern +## Snapshot presentational components + +**When to use:** Locking the rendered markup of a pure `cc-components` component so unintended UI changes +show up in review. +**Correct** ```typescript -// playwright/tests/station-login-test.spec.ts -import { test, expect } from '@playwright/test'; -import { StationLoginUtils } from '../Utils/stationLoginUtils'; +// from packages/contact-center/cc-components/tests/components/task/CampaignCountdown/campaign-countdown.snapshot.tsx +it('should match snapshot with 30 seconds timeout', () => { + const {container} = render(); + expect(container).toMatchSnapshot(); +}); +``` -test.describe('Station Login', () => { - let utils: StationLoginUtils; +**Incorrect** +```typescript +// snapshotting a stateful widget wired to the real store — snapshots churn nondeterministically +const {container} = render(); +expect(container).toMatchSnapshot(); +``` +**Why wrong:** Snapshots of components with live/async state produce noisy diffs and false failures; snapshot +the pure, props-driven `cc-components` render instead. - test.beforeEach(async ({ page }) => { - utils = new StationLoginUtils(page); - await utils.navigateToApp(); - }); +**Where it appears** +- `.../cc-components/tests/components/UserState/user-state.snapshot.tsx` , `.../cc-components/tests/components/StationLogin/station-login.snapshot.tsx` , `.../cc-components/tests/components/task/CampaignCountdown/campaign-countdown.snapshot.tsx` (snapshots stored under `__snapshots__/`) - test('should login successfully', async ({ page }) => { - await utils.selectTeam('Team A'); - await utils.selectDialNumber('+1234567890'); - await utils.clickLogin(); +**Edge cases / exceptions** +- Snapshot files use the `*.snapshot.tsx` naming (not `*.test.tsx`); behavior assertions still go in the regular test files. - await expect(page.getByTestId('login-success')).toBeVisible(); - }); +--- - test('should show error on invalid credentials', async ({ page }) => { - await utils.clickLogin(); +## Playwright E2E: `TestManager` + standalone Utils functions - await expect(page.getByTestId('error-message')).toBeVisible(); - }); -}); -``` - -### TestManager Pattern +**When to use:** End-to-end specs under `playwright/tests/`. A per-project `TestManager` owns browser +setup/teardown; feature actions are **standalone async functions** imported from `playwright/Utils/`. +**Correct** ```typescript -// playwright/tests/station-login-test.spec.ts -import {test, expect} from '@playwright/test'; +// from playwright/tests/station-login-test.spec.ts import {TestManager} from '../test-manager'; -import { - telephonyLogin, - verifyLoginMode, - ensureUserStateVisible, -} from '../Utils/stationLoginUtils'; -import {LOGIN_MODE} from '../constants'; +import {telephonyLogin, verifyLoginMode, ensureUserStateVisible} from '../Utils/stationLoginUtils'; test.describe('Station Login Tests - Dial Number Mode', () => { let testManager: TestManager; test.beforeAll(async ({browser}, testInfo) => { - const projectName = testInfo.project.name; - testManager = new TestManager(projectName); + testManager = new TestManager(testInfo.project.name); await testManager.setupForStationLogin(browser); }); - test.afterAll(async () => { - if (testManager) { - await testManager.cleanup(); - } - }); - - test('should login with Dial Number mode and verify login state', async () => { - await ensureUserStateVisible( - testManager.agent1Page, - LOGIN_MODE.DIAL_NUMBER, - process.env[`${testManager.projectName}_ENTRY_POINT`], - ); - - await telephonyLogin( - testManager.agent1Page, - LOGIN_MODE.DIAL_NUMBER, - process.env[`${testManager.projectName}_ENTRY_POINT`], - ); - - await verifyLoginMode(testManager.agent1Page, 'Dial Number'); - }); + test.afterAll(async () => { await testManager?.cleanup(); }); }); ``` - ---- - -## Playwright Utils Pattern - ```typescript -// playwright/Utils/stationLoginUtils.ts -import { Page, Locator } from '@playwright/test'; - -export class StationLoginUtils { - private page: Page; - - constructor(page: Page) { - this.page = page; - } - - async navigateToApp(): Promise { - await this.page.goto('/'); - } - - async selectTeam(teamName: string): Promise { - await this.page.getByTestId('team-dropdown').click(); - await this.page.getByText(teamName).click(); - } - - async selectDialNumber(number: string): Promise { - await this.page.getByTestId('dial-number-input').fill(number); - } - - async clickLogin(): Promise { - await this.page.getByTestId('login-button').click(); - } - - async waitForLoginSuccess(): Promise { - await this.page.waitForSelector('[data-testid="login-success"]'); - } -} +// from playwright/Utils/stationLoginUtils.ts — utilities are exported functions, not a class +export const telephonyLogin = async (page: Page, mode: string, number?: string): Promise => { + /* ... */ +}; ``` ---- - -## Async Testing Pattern - +**Incorrect** ```typescript -// Using waitFor -await waitFor(() => { - expect(screen.getByText('Success')).toBeInTheDocument(); -}); - -// Using findBy (auto-waits) -const successMessage = await screen.findByText('Success'); -expect(successMessage).toBeInTheDocument(); - -// Using act for state updates -await act(async () => { - fireEvent.click(button); -}); +// per-feature Utils class with its own page — the repo moved away from this shape +class StationLoginUtils { constructor(private page: Page) {} async clickLogin() {/*...*/} } ``` +**Why wrong:** The current suite centralizes multi-agent browser/session lifecycle in `TestManager` and +keeps actions as composable functions. A per-feature `Utils` class duplicates page/session handling that +`TestManager` already owns. ---- - -## Mock Event Pattern - -```typescript -// Mock event listener -const mockOn = jest.fn(); -const mockOff = jest.fn(); +**Where it appears** +- Specs: `playwright/tests/station-login-test.spec.ts` , `playwright/tests/user-state-test.spec.ts` , `playwright/tests/incoming-task-and-controls-multi-session.spec.ts` +- Utils (function exports): `playwright/Utils/stationLoginUtils.ts` , `playwright/Utils/userStateUtils.ts` , `playwright/Utils/taskControlUtils.ts` (also `outdialUtils.ts`, `conferenceUtils.ts`, `incomingTaskUtils.ts`, …) +- Manager: `playwright/test-manager.ts` -mockStore.cc = { - on: mockOn, - off: mockOff, -}; - -// Simulate event -const eventHandler = mockOn.mock.calls[0][1]; -act(() => { - eventHandler({ state: 'Break' }); -}); -``` +**Edge cases / exceptions** +- **Legacy shape (do not follow):** an older per-feature `StationLoginUtils` *class* pattern appears in earlier docs/history. The current convention is `TestManager` + standalone functions; write new E2E code that way. --- -## Snapshot Testing Pattern +## Test commands -```typescript -it('should match snapshot', async () => { - const { container } = await render(); - expect(container).toMatchSnapshot(); -}); -``` - ---- - -## Test Commands +Run tests through the workspace scripts (from root `package.json`), never `npx jest` directly: ```bash -# Run all unit tests -yarn test:unit - -# Run all style tests -yarn test:styles - -# Run all E2E tests -yarn test:e2e - - -# Run all tests for tooling -yarn test:tooling - -# Run specific package tests -yarn workspace @webex/cc-station-login test:unit +yarn test:unit # tooling + cc-widgets + meetings-widget unit tests +yarn test:cc-widgets # unit tests for all contact-center widget packages +yarn test:e2e # Playwright E2E (yarn playwright test) +yarn test:styles # style tests across workspaces +yarn workspace @webex/cc-user-state test:unit # a single package +``` -# Run with coverage -yarn run test:unit --coverage +**Where it appears** +- Root `package.json` `scripts` block (`test:unit`, `test:cc-widgets`, `test:e2e`, `test:styles`, `test:tooling`, `test:meetings-widget`). -# Run specific E2E test -npx playwright test tests/station-login-test.spec.ts -``` +**Edge cases / exceptions** +- Pre-commit hooks run the full unit suite, so commits can be slow — expected, not a failure. --- ## Related -- [React Patterns](./react-patterns.md) -- [TypeScript Patterns](./typescript-patterns.md) -- [MobX Patterns](./mobx-patterns.md) +- [React Patterns](./react-patterns.md) · [TypeScript Patterns](./typescript-patterns.md) · [MobX Patterns](./mobx-patterns.md) diff --git a/ai-docs/patterns/typescript-patterns.md b/ai-docs/patterns/typescript-patterns.md index c8dfd1890..43ad3c721 100644 --- a/ai-docs/patterns/typescript-patterns.md +++ b/ai-docs/patterns/typescript-patterns.md @@ -1,216 +1,241 @@ -# TypeScript Patterns + -> Quick reference for LLMs working with TypeScript in this repository. +# Pattern: TypeScript conventions ---- - -## Rules +> Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is an `ai-docs/patterns/` file; the folder [README](./README.md) explains the per-pattern shape and routing. +> Context-efficiency: link to canonical docs — don't duplicate them. `no-any` and formatting are enforced by ESLint/Prettier — those are rules, not patterns. -- **MUST** prefix all interfaces with `I` (e.g., `IUserState`, `IStationLoginProps`) -- **MUST** use PascalCase for components and interfaces -- **MUST** use camelCase for hooks with `use` prefix (e.g., `useUserState`) -- **MUST** use `.tsx` extension for components, `.ts` for hooks and utilities -- **MUST** co-locate types in `{component}.types.ts` files -- **MUST** use `Pick` and `Partial` to derive types from parent interfaces -- **MUST** document every interface property with JSDoc comments -- **MUST** use enums for event names and constants -- **NEVER** use `any` without ESLint disable comment and explanation -- **NEVER** duplicate type definitions - derive from source with `Pick` +Language/layer group: **TypeScript**. Each section below is one pattern in the standard shape. --- -## Naming Conventions +## Co-locate types in `*.types.ts` -### Components -```typescript -// PascalCase, .tsx extension -UserState.tsx -StationLogin.tsx -CallControl.tsx -``` +**When to use:** Every widget and component defines its props/state types in a sibling +`.types.ts`, not inline in the `.tsx`. -### Hooks +**Correct** ```typescript -// camelCase with 'use' prefix, .ts extension -useUserState.ts -useStationLogin.ts -useCallControl.ts +// from packages/contact-center/cc-components/src/components/UserState/user-state.types.ts +export interface IUserState { + idleCodes: IdleCode[]; + agentId: string; + currentState: string; + onStateChange?: (arg: IdleCode | ICustomState) => void; +} ``` -### Interfaces & Types +**Incorrect** ```typescript -// PascalCase with 'I' prefix -interface IUserState { ... } -interface IStationLoginProps { ... } -interface IContactCenter { ... } +// props type declared inline in the component .tsx and duplicated in the widget +const UserStateComponent = (props: {idleCodes: IdleCode[]; currentState: string}) => { /* ... */ }; ``` +**Why wrong:** Inline prop types can't be shared with the widget/hook and drift out of sync; the +`.types.ts` file is the single source the widget derives from (see the `Pick`/`Partial` pattern). -### Constants -```typescript -// SCREAMING_SNAKE_CASE -const MAX_RETRY_COUNT = 3; -const DEFAULT_TIMEOUT = 5000; -``` +**Where it appears** +- `packages/contact-center/store/src/store.types.ts` , `packages/contact-center/cc-components/src/components/UserState/user-state.types.ts` , `packages/contact-center/cc-components/src/components/StationLogin/station-login.types.ts` (also `task/task.types.ts`, `user-state/src/user-state.types.ts`, `station-login/src/station-login/station-login.types.ts`, `cc-digital-channels/.../digital-channels.types.ts`) -### File Structure -``` -packages/*/src/{widget}/index.tsx # Widget entry -packages/*/src/helper.ts # Hooks/helpers -packages/*/src/{widget}/{widget}.types.ts # Types -``` +**Edge cases / exceptions** +- The `I`-prefix on interface names is used for many core types (`IUserState`, `IStationLoginProps`, `IContactCenter`, `IStore`) but is **not** consistently applied — several prop/data interfaces omit it (`TaskProps`, `ControlProps`, `OutdialCallProps`, `LoginOptionsState`). Follow the prefix for new interfaces but don't assume every existing type has it. --- -## Import Patterns +## Derive prop types with `Pick` / `Partial` -### Store Import -```typescript -import store from '@webex/cc-store'; -``` +**When to use:** A widget or hook needs a subset of a larger interface. Derive it with `Pick` (and +`Partial` for optional callbacks) instead of re-declaring fields. -### Components Import +**Correct** ```typescript -import { Component } from '@webex/cc-components'; +// from packages/contact-center/user-state/src/user-state.types.ts +export type IUserStateProps = Pick; ``` - -### Types Import ```typescript -import { IUserState } from './user-state.types'; +// from packages/contact-center/task/src/task.types.ts +export type UseTaskProps = Pick & + Partial>; ``` -### MobX Import +**Incorrect** ```typescript -import { observer } from 'mobx-react-lite'; -import { runInAction } from 'mobx'; +export type IUserStateProps = { + onStateChange?: (arg: IdleCode | ICustomState) => void; // hand-copied, will drift from IUserState +}; ``` +**Why wrong:** A hand-copied subset silently diverges when the source interface changes (a field's type +updates but the copy doesn't), producing type errors far from the edit or, worse, none at all. + +**Where it appears** +- `packages/contact-center/user-state/src/user-state.types.ts` , `packages/contact-center/cc-components/src/components/UserState/user-state.types.ts` (`UserStateComponentsProps` = `Pick`) , `packages/contact-center/task/src/task.types.ts` + +**Edge cases / exceptions** +- When a prop truly does not exist on any parent interface (a widget-only flag), declaring it directly is fine — derive only what genuinely overlaps a source type. --- -## Interface Patterns +## Union types for closed value sets -### Pattern 1: Interface with I Prefix -```typescript -interface IUserState { - idleCodes: IdleCode[]; - agentId: string; - cc: IContactCenter; - currentState: string; - onStateChange?: (arg: IdleCode | ICustomState) => void; -} -``` +**When to use:** A value is one of a small, fixed set of shapes or string literals. -### Pattern 2: Use Pick to Derive Types +**Correct** ```typescript -// Widget picks only what it needs from component interface -export type IUserStateProps = Pick; - -// Hook picks different subset -export type UseUserStateProps = Pick< - IUserState, - 'idleCodes' | 'agentId' | 'cc' | 'currentState' | 'logger' ->; +// from packages/contact-center/store/src/store.types.ts +type ICustomState = ICustomStateSet | ICustomStateReset; ``` - -### Pattern 3: Combine Pick with Partial ```typescript -// Required props + optional callback props -export type StationLoginProps = - Pick & - Partial>; +// from packages/contact-center/cc-components/src/components/task/task.types.ts +export type CallControlMenuType = 'Consult' | 'Transfer' | 'ExitConference'; ``` -### Pattern 4: Union Types +**Incorrect** ```typescript -type ICustomState = ICustomStateSet | ICustomStateReset; +type CustomState = {kind: string; [k: string]: any}; // open-ended, loses exhaustiveness ``` +**Why wrong:** An open shape defeats exhaustiveness checking — `switch` over the value no longer errors on +an unhandled variant, and `any` re-enters the codebase. + +**Where it appears** +- `packages/contact-center/store/src/store.types.ts` (`ICustomState`) , `packages/contact-center/cc-components/src/components/task/task.types.ts` (`CallControlMenuType`, `CategoryType`, `CampaignAutoAction`) + +**Edge cases / exceptions** +- **Candidate (fewer than 3 occurrences of one union shape):** each union above is used in only one or two spots. The *convention* (prefer a closed union over an open string) holds broadly, but no single union is a repo-wide 3+ pattern. --- -## Enum Patterns +## Event/state enums are defined in-repo -### Event Enums +**When to use:** Referencing SDK event names or fixed agent-state values. Use the repo's enums; do not +scatter string literals. + +**Correct** ```typescript -export enum TASK_EVENTS { +// from packages/contact-center/store/src/store.types.ts +enum TASK_EVENTS { TASK_INCOMING = 'task:incoming', TASK_ASSIGNED = 'task:assigned', TASK_HOLD = 'task:hold', -} - -export enum CC_EVENTS { - AGENT_DN_REGISTERED = 'agent:dnRegistered', - AGENT_LOGOUT_SUCCESS = 'agent:logoutSuccess', -} + // ... +} // TODO: remove this once cc sdk exports this enum ``` -### State Enums +**Incorrect** ```typescript -export enum AgentUserState { - Available = 'Available', - RONA = 'RONA', - Engaged = 'ENGAGED', -} +store.cc.on('task:incoming', handler); // raw string literal, easy to typo ``` +**Why wrong:** A mistyped event string fails silently (the handler simply never fires). The enum gives one +authoritative spelling and lets TypeScript catch typos. + +**Where it appears** +- `packages/contact-center/store/src/store.types.ts` (`TASK_EVENTS`, `CC_EVENTS`, `ConsultStatus`, agent-state constants) , `packages/contact-center/cc-components/src/components/UserState/user-state.types.ts` (`AgentUserState`). + +**Edge cases / exceptions** +- These enums are **defined in this repo, not imported from `@webex/contact-center`** — the SDK does not yet export them (see the `// TODO: remove this once cc sdk exports this enum` comment). Data *types* like `Profile`, `ITask`, `Team`, `IdleCode`, `BuddyDetails` do come from the SDK (imported at the top of `store.types.ts`); the event/state enums are the local exception. When the SDK begins exporting these enums, prefer the SDK's. --- -## JSDoc Pattern +## JSDoc on public interface properties +**When to use:** Every property of an exported props/state interface in `cc-components` (and the store's +public surface) carries a `/** ... */` describing intent. + +**Correct** ```typescript -/** - * Interface representing the properties for the Station Login component. - */ +// from packages/contact-center/cc-components/src/components/StationLogin/station-login.types.ts export interface IStationLoginProps { /** - * Webex Contact Center instance. + * Webex instance. */ cc: IContactCenter; /** - * Array of teams the agent belongs to. + * Callback function to be invoked once the agent login is successful */ - teams: Team[]; + onLogin?: () => void; +} +``` - /** - * Handler called when login completes. - */ +**Incorrect** +```typescript +export interface IStationLoginProps { + cc: IContactCenter; // no doc — meaning of the prop is opaque to consumers onLogin?: () => void; } ``` +**Why wrong:** These interfaces are the public API of the widgets; without JSDoc, host-app integrators (and +generated typedoc) have no description of each prop. + +**Where it appears** +- `packages/contact-center/cc-components/src/components/UserState/user-state.types.ts` , `packages/contact-center/cc-components/src/components/StationLogin/station-login.types.ts` , `packages/contact-center/cc-components/src/components/task/task.types.ts` + +**Edge cases / exceptions** +- Internal helper types not part of a public surface don't require full JSDoc. --- -## Type Export Pattern +## Typed optional callback props +**When to use:** A component/widget exposes an event to its host as an optional prop. Type the full +signature; make it optional with `?`. + +**Correct** ```typescript -// Central export from *.types.ts -export type { - IContactCenter, - ITask, - Profile, - Team, -}; +// from packages/contact-center/cc-components/src/components/task/task.types.ts +onAccepted?: ({task}: {task: ITask}) => void; +onRejected?: ({task}: {task: ITask}) => void; +``` +```typescript +// from packages/contact-center/cc-components/src/components/UserState/user-state.types.ts +onStateChange?: (arg: IdleCode | ICustomState) => void; +``` -export { - CC_EVENTS, - TASK_EVENTS, -}; +**Incorrect** +```typescript +onAccepted?: Function; // untyped — arguments and return are unchecked ``` +**Why wrong:** `Function` (or `any` args) removes checking on what the host receives, so a callback shape +change won't surface at the call site. + +**Where it appears** +- `packages/contact-center/cc-components/src/components/task/task.types.ts` (`onAccepted`, `onRejected`) , `packages/contact-center/cc-components/src/components/StationLogin/station-login.types.ts` (`onLogin`, `onLogout`) , `packages/contact-center/cc-components/src/components/UserState/user-state.types.ts` (`onStateChange`) + +**Edge cases / exceptions** +- Required callbacks (no `?`) follow the same typed-signature rule — the pattern is "type the signature," and optionality is orthogonal. --- -## Callback Type Pattern +## Naming: files and identifiers -```typescript -// Optional callback with specific signature -onStateChange?: (arg: IdleCode | ICustomState) => void; -onLogin?: () => void; -onSaveEnd?: (isComplete: boolean) => void; +**When to use:** Naming new files and symbols. + +**Correct** +- Components: PascalCase, `.tsx` (`user-state.tsx` exports `UserStateComponent`; `DigitalChannelsComponent.tsx`). +- Hooks: `use*` prefix in a `.ts` file — the widget's hooks live in `helper.ts`; broadly-shared ones in a `hooks/` or `Utils/` folder (`cc-components/src/hooks/useIntersectionObserver.ts`, `task/src/Utils/useHoldTimer.ts`). +- Constants/enums: SCREAMING_SNAKE_CASE (`TASK_EVENTS`, `CC_EVENTS`, `AGENT_STATE_AVAILABLE`). + +**Incorrect** +``` +UseUserState.ts // hook file should be camelCase and, for a widget's logic, live in helper.ts ``` +**Why wrong:** Inconsistent casing/placement makes hooks hard to locate; the repo convention is a widget's +hooks in `helper.ts` and shared hooks in a dedicated folder. + +**Where it appears** +- Components: `.../cc-components/src/components/UserState/user-state.tsx` , `.../cc-components/src/components/StationLogin/station-login.tsx` , `.../cc-digital-channels/src/digital-channels/DigitalChannelsComponent.tsx` +- Hooks: `.../task/src/helper.ts` , `.../cc-components/src/hooks/useIntersectionObserver.ts` , `.../task/src/Utils/useHoldTimer.ts` + +**Edge cases / exceptions** +- `no-any` is enforced by ESLint (`@typescript-eslint/no-explicit-any`); where `any` is genuinely required it carries an inline `eslint-disable` with a reason (see `IContactCenter` in `store.types.ts`). That's a lint rule, not a documented pattern. --- ## Related -- [React Patterns](./react-patterns.md) -- [MobX Patterns](./mobx-patterns.md) -- [Testing Patterns](./testing-patterns.md) +- [React Patterns](./react-patterns.md) · [MobX Patterns](./mobx-patterns.md) · [Testing Patterns](./testing-patterns.md) diff --git a/ai-docs/rules/sdk-access-via-store.md b/ai-docs/rules/sdk-access-via-store.md index 414341298..d1fec8082 100644 --- a/ai-docs/rules/sdk-access-via-store.md +++ b/ai-docs/rules/sdk-access-via-store.md @@ -1,3 +1,12 @@ + + # Rule: Access the SDK only through the store > Start here → repo root [`AGENTS.md`](../../AGENTS.md) (agent entry, carries the critical rules) · router [`SPEC_INDEX.md`](../SPEC_INDEX.md). This is an `ai-docs/rules/` fill-in; the folder README explains generic-vs-per-language routing; the repo-wide rules digest is `../RULES.md`. diff --git a/ai-docs/templates/new-widget/02-code-generation.md b/ai-docs/templates/new-widget/02-code-generation.md index f019e023d..b9f978a4f 100644 --- a/ai-docs/templates/new-widget/02-code-generation.md +++ b/ai-docs/templates/new-widget/02-code-generation.md @@ -80,7 +80,7 @@ From 01-pre-questions.md, load: 1. **Find exact API path in SDK knowledge base:** ```typescript - // Open: ai-docs/contact-centre-sdk-apis/contact-center.json + // Open: node_modules/@webex/contact-center/dist/types/index.d.ts // Search for method name // Verify path exists and is correct ``` diff --git a/ai-docs/templates/new-widget/06-validation.md b/ai-docs/templates/new-widget/06-validation.md index ffb0ab8d8..7f4918897 100644 --- a/ai-docs/templates/new-widget/06-validation.md +++ b/ai-docs/templates/new-widget/06-validation.md @@ -116,7 +116,7 @@ Before running any tests, verify implementation matches approved diagrams: **For each SDK method in sequence diagrams:** -- [ ] Method exists in [contact-centre-sdk-apis/contact-center.json](../../contact-centre-sdk-apis/contact-center.json) +- [ ] Method exists in `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) - [ ] **Exact path matches sequence diagram** (e.g., `store.cc.someService.someMethod`) - [ ] **Parameters match diagram specification** (type, order, values) - [ ] **Return type matches SDK documentation AND diagram** diff --git a/packages/@webex/widgets/ai-docs/widgets-spec.md b/packages/@webex/widgets/ai-docs/widgets-spec.md index 711bd7891..e9599c27d 100644 --- a/packages/@webex/widgets/ai-docs/widgets-spec.md +++ b/packages/@webex/widgets/ai-docs/widgets-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| generated_by / approved_by / updated_at | migration agent / pending / 2026-06-29 | | Validation status | not-run | Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with @@ -108,7 +108,7 @@ plain React component. The authoritative prop contract is `WebexMeetingsWidget.p | Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | |---|---|---|---|---|---|---| -| `meetings-widgets.WebexMeetingsWidget` | SDK (React component export) | `import {WebexMeetingsWidget} from '@webex/widgets'` | Embed a full Webex meeting experience in a React host | Public; export name + required props are the breaking surface (semver) | `packages/@webex/widgets/src/index.js:1`; props in `packages/@webex/widgets/src/widgets/WebexMeetings/WebexMeetings.jsx:231-255` | Not catalogued in `../../../../ai-docs/CONTRACTS.md` (CC-only) — see Source Material Register `[NEEDS HUMAN INPUT]` | +| `meetings-widgets.WebexMeetingsWidget` | SDK (React component export) | `import {WebexMeetingsWidget} from '@webex/widgets'` | Embed a full Webex meeting experience in a React host | Public; export name + required props are the breaking surface (semver) | `packages/@webex/widgets/src/index.js:1`; props in `packages/@webex/widgets/src/widgets/WebexMeetings/WebexMeetings.jsx:231-255` | Not catalogued in `../../../../ai-docs/CONTRACTS.md` (CC-only; this is the legacy meetings family) | Props (from `WebexMeetings.jsx:231-255`): diff --git a/packages/contact-center/cc-components/ai-docs/cc-components-spec.md b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md index 41da6e734..1439d9e0a 100644 --- a/packages/contact-center/cc-components/ai-docs/cc-components-spec.md +++ b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `[NEEDS HUMAN INPUT]` / updated_at `2026-06-29` | +| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `pending` / updated_at `2026-06-29` | | Validation status | not-run | ## Evidence Rules diff --git a/packages/contact-center/cc-digital-channels/ai-docs/cc-digital-channels-spec.md b/packages/contact-center/cc-digital-channels/ai-docs/cc-digital-channels-spec.md new file mode 100644 index 000000000..3ff06e881 --- /dev/null +++ b/packages/contact-center/cc-digital-channels/ai-docs/cc-digital-channels-spec.md @@ -0,0 +1,378 @@ +# cc-digital-channels — SPEC + +> Start here → root [`AGENTS.md`](../../../../AGENTS.md) (agent entry) · router [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical spec: orientation, requirements, design, flows, UI, and tests. +> Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand per `SPEC_INDEX.md`. + +## Metadata +| Field | Value | +|---|---| +| Module id | `cc-digital-channels` | +| Source path(s) | `packages/contact-center/cc-digital-channels/src/` | +| Doc kind | Module spec | +| Coverage score | Pending coverage assessment | +| Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | +| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `pending` / updated_at `2026-07-01` | +| Validation status | not-run | + +## Evidence Rules +Every generated requirement below must cite concrete source evidence using `file path`. Separate source +evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish +truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the +repository policy says history is reliable, and must include the commit hash. If evidence is missing or +conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers +as approved unknowns only when the human explicitly defers or does not know. + +## Source Material Register +| Source doc | Scope | Decision | Detail location or disposition | +|---|---|---|---| +| N/A (no pre-migration ai-docs) | none | none | This package had no `ai-docs/` directory before this spec; all content is derived directly from `src/` and `tests/` as source of truth. | +| `ai-docs/CONTRACTS.md` | API / contract | reconciled | `cc-widgets.DigitalChannels` contract row (line 24) → Public Surface. Note: CONTRACTS.md lists the root index as `packages/contact-center/cc-digital-channels/src/index.tsx`, but the real barrel is `src/index.ts` (there is no `src/index.tsx`); trust `src/index.ts`. | + +## Overview +`cc-digital-channels` is the agent-facing Digital Channels widget for Webex Contact Center. It embeds the +third-party `cc-digital-interactions` "Engage" experience (email / chat / messaging conversation surface) +inside the agent desktop, scoped to the agent's currently active digital task. It contributes no +conversation UI of its own — it fetches the credentials and identifiers the Engage widget needs, initializes +the Engage app once per session, and mounts the Engage component with the correct theme. + +The package follows the repo's one-directional widget architecture. The exported `DigitalChannels` widget +(`src/digital-channels/index.tsx`) is a plain FC that wraps `DigitalChannelsInternal` — an `observer()` — in +an `ErrorBoundary`. `DigitalChannelsInternal` reads reactive state from the MobX store singleton +(`@webex/cc-store`) and delegates all logic to two hooks in `src/helper.ts`: `useDigitalChannelsData` (fetch +the access token and derive the `conversationId` from the current task) and `useDigitalChannelsInit` +(initialize the Engage app exactly once per session, guarded by a store flag). Once every prerequisite is +present it renders the presentational `DigitalChannelsComponent` (`src/digital-channels/DigitalChannelsComponent.tsx`), +which wraps the `cc-digital-interactions` `Engage` default export in a Momentum `` element. + +A maintainer should start at `src/digital-channels/index.tsx` (the widget, its store reads, and the render +gate), then read `src/helper.ts` (token fetch, conversationId derivation, and one-time initialization), +and finally `src/digital-channels/DigitalChannelsComponent.tsx` (how Engage is mounted and themed). Prop +and hook-input shapes live in `src/digital-channels/digital-channels.types.ts`; the custom JSX intrinsic +element `md-theme` is declared in `src/types/global.d.ts`. + +## Purpose / Responsibility +Owns embedding the Digital Channels ("Engage") experience for the active digital task: fetch the JWT access +token, derive the conversation id from `store.currentTask`, initialize the `cc-digital-interactions` app once +per session, and mount the themed Engage widget. Does NOT own the SDK instance, the observable agent/task +state (`currentTask`, `dataCenter`, `currentTheme`, `isDigitalChannelsInitialized`), token issuance, or the +Engage conversation UI itself — those belong to `@webex/cc-store` and `cc-digital-interactions`. + +## Stack +TypeScript 5.6.3, React `>=18.3.1` (functional components + hooks), MobX via `mobx-react-lite` `^4.1.0` +(`observer`), `react-error-boundary` (via the shared widget pattern). Third-party UI dependency +`cc-digital-interactions` `3.0.8-beta.2` (the Engage widget + `initializeApp`). Tests: Jest 29.7.0 + React +Testing Library 16 + `@testing-library/jest-dom` (jsdom), with Babel transform. Build: `tsc` (type +declarations) + Webpack 5 (`build:src`). Published as `@webex/cc-digital-channels` (`main: dist/index.js`, +`types: dist/types/index.d.ts`). No datastore or messaging of its own. + +## Folder / Package Structure +``` +cc-digital-channels/ +├── src/ +│ ├── index.ts # Package barrel — re-exports DigitalChannels (named + default) +│ ├── helper.ts # useDigitalChannelsData + useDigitalChannelsInit hooks +│ ├── digital-channels/ +│ │ ├── index.tsx # DigitalChannels widget (ErrorBoundary) + DigitalChannelsInternal (observer) +│ │ ├── DigitalChannelsComponent.tsx # Presentational — wraps Engage in +│ │ └── digital-channels.types.ts # Hook-input + component prop interfaces +│ └── types/ +│ └── global.d.ts # JSX intrinsic declaration for +└── tests/ + ├── helper.ts # Hook tests (init + data) + └── digital-channels/ + ├── index.tsx # Widget integration + ErrorBoundary tests + └── DigitalChannelsComponent.test.tsx # Presentational theming tests +``` + +## Key Files (source of truth) +| File | Holds | +|---|---| +| `src/index.ts` | Package export barrel; the public surface is what it re-exports (`DigitalChannels`, plus default export). | +| `src/digital-channels/index.tsx` | Public widget, store reads, the render gate (`if (!currentTask || !jwtToken || !dataCenter || hasError || !initialized || !conversationId) return null`), and the `ErrorBoundary` → `store.onErrorCallback('DigitalChannels', error)` wiring. | +| `src/helper.ts` | `useDigitalChannelsData` (token fetch + `conversationId` derivation) and `useDigitalChannelsInit` (one-time `initializeApp` guarded by `isDigitalChannelsInitialized`). | +| `src/digital-channels/DigitalChannelsComponent.tsx` | The exact props passed to the `cc-digital-interactions` `Engage` widget, the `` wrapper, the dark/light theme mapping, and the remount `componentKey`. | +| `src/digital-channels/digital-channels.types.ts` | Authoritative interfaces: `DigitalChannelsInitHookProps`, `DigitalChannelsDataHookProps`, `DigitalChannelsComponentProps`. | +| `src/types/global.d.ts` | The `md-theme` JSX intrinsic-element declaration (`theme`, `class`, `darktheme`, `lighttheme`). | +| `package.json` | Version, dependency floors (`cc-digital-interactions` `3.0.8-beta.2`), peer deps (`react`/`react-dom` `>=18.3.1`, `@momentum-ui/web-components` `^2.26.20`), export entry points. | + +## Public Surface +`DigitalChannels` is a React component exported by `@webex/cc-digital-channels` and re-exported from +`@webex/cc-widgets`, which registers it as the custom element `widget-cc-digital-channels` via r2wc with an +empty prop map (`r2wc(DigitalChannels, {})` in `packages/contact-center/cc-widgets/src/wc.ts`). The widget +declares **no props** — it is entirely store-driven; all its inputs are read from the shared MobX store +(`currentTask`, `dataCenter`, `currentTheme`, `isDigitalChannelsInitialized`, `getAccessToken`, `logger`, +`onErrorCallback`, `setDigitalChannelsInitialized`). + +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +|---|---|---|---|---|---|---| +| `cc-widgets.DigitalChannels` | SDK / Web Component | React component `DigitalChannels`; mounted in `@webex/cc-widgets` as custom element `widget-cc-digital-channels` (no declared props; store-driven) | Embeds the Engage digital-channels experience for the active digital task | Stable semver; renaming/removing the export or the `widget-cc-digital-channels` tag is a major (breaking) change | `src/index.ts`, `packages/contact-center/cc-widgets/src/wc.ts`; SDK-backed store types in `@webex/contact-center` package types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | + +Compatibility notes: +- The widget has no prop surface, so there is no prop-level compatibility contract; the compatibility surface + is the export name and the custom-element tag. Adding a prop later would be additive (minor); removing the + export or renaming the tag is breaking (major). +- The store fields this widget reads (`currentTask`, `dataCenter`, `currentTheme`, + `isDigitalChannelsInitialized`, `getAccessToken`, `setDigitalChannelsInitialized`, `onErrorCallback`) are an + implicit contract with `@webex/cc-store`; renaming or removing them upstream breaks this widget silently. + +## Requires (dependencies) +- `@webex/cc-store` (`workspace:*`) — the MobX singleton (default export). Provides `currentTask` (`ITask`), + `dataCenter`, `currentTheme`, `isDigitalChannelsInitialized`, `setDigitalChannelsInitialized(value)`, + `getAccessToken(): Promise`, `logger`, and `onErrorCallback`. Source: `src/digital-channels/index.tsx`; + store fields at `packages/contact-center/store/src/store.ts:29,31,53,54` and + `packages/contact-center/store/src/storeEventsWrapper.ts:988` (`getAccessToken`). +- `cc-digital-interactions` `3.0.8-beta.2` — third-party dependency providing `initializeApp(dataCenter, jwtToken)` + (named, used in `src/helper.ts`) and the `Engage` widget (default export, used in `DigitalChannelsComponent.tsx`). + Source: `package.json` dependencies. +- `mobx-react-lite` `^4.1.0` — `observer` HOC (`src/digital-channels/index.tsx`). +- `react-error-boundary` — `ErrorBoundary` used in `src/digital-channels/index.tsx` (provided transitively; the + shared widget pattern). Source: import in `src/digital-channels/index.tsx`. +- Peer dependencies (host-provided): `react >=18.3.1`, `react-dom >=18.3.1`, `@momentum-ui/web-components` + `^2.26.20` (imported for the `` custom element in `DigitalChannelsComponent.tsx`). Source: + `package.json` peerDependencies. + +## Requirements +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +|---|---|---|---|---|---|---| +| `CC-DIGITAL-CHANNELS-R-001` | `useDigitalChannelsData` fetches the JWT via `getAccessToken()` into `jwtToken`; on rejection it logs `[DIGITAL_CHANNELS] ❌ Failed to get access token`, sets `tokenError`/`hasError` true, and does not throw. | The Engage widget cannot mount without a JWT; a token failure must degrade to a non-rendering, non-crashing state. | `src/helper.ts` (`useDigitalChannelsData.fetchToken`) | `tests/helper.ts` "should fetch access token and extract conversationId", "should handle token fetch error and set error flags", "should handle token fetch error gracefully when logger is undefined" | none | PRESENT | +| `CC-DIGITAL-CHANNELS-R-002` | `useDigitalChannelsData` derives `conversationId` from `currentTask.data.interaction.callAssociatedDetails.mediaResourceId`, returning `''` when the task, the details, or the id is absent. | Engage is scoped to a single conversation; a missing id must produce an empty value (which gates rendering) rather than a crash. | `src/helper.ts` (`conversationId` `useMemo`) | `tests/helper.ts` "should fetch access token and extract conversationId", "should return empty conversationId when currentTask is missing", "should return empty conversationId when mediaResourceId is missing" | Deep-optional chain into `interaction.callAssociatedDetails`; typed via an inline cast (see Pitfalls). | PRESENT | +| `CC-DIGITAL-CHANNELS-R-003` | `useDigitalChannelsInit` calls `initializeApp(dataCenter, jwtToken)` at most once per session: it runs only when `isDigitalChannelsInitialized` is false, then calls `setDigitalChannelsInitialized(true)` and sets local `initialized` true. | Re-initializing the Engage app per render/task would be wasteful and can break the embedded editor; init must be idempotent across the session. | `src/helper.ts` (`useDigitalChannelsInit.initialize`) | `tests/helper.ts` "should initialize app when not already initialized", "should skip initialization when already initialized" | Session flag lives in the store (`isDigitalChannelsInitialized`), not local state. | PRESENT | +| `CC-DIGITAL-CHANNELS-R-004` | `useDigitalChannelsInit` skips all initialization work when `skipInit` is true, leaving `initialized` at its initial value and never calling `initializeApp`. | The widget passes `skipInit: !currentTask || !jwtToken || !dataCenter`; init must not fire until every prerequisite exists. | `src/helper.ts` (`useDigitalChannelsInit`, early `if (skipInit) return`); `src/digital-channels/index.tsx` (`skipInit` computation) | `tests/helper.ts` "should skip initialization when skipInit is true" | none | PRESENT | +| `CC-DIGITAL-CHANNELS-R-005` | On `initializeApp` rejection, `useDigitalChannelsInit` logs `[DIGITAL_CHANNELS_INIT] ❌ Failed to initialize…` with the error message (or "Unknown error" for a non-`Error` throw) and does not throw; `initialized` stays false. | An init failure must be observable in logs and must not crash the widget or set the initialized flag. | `src/helper.ts` (`initialize` `try/catch`, `error instanceof Error` branch) | `tests/helper.ts` "should handle initialization error", "should log unknown error message when initialization throws non-Error" | none | PRESENT | +| `CC-DIGITAL-CHANNELS-R-006` | `DigitalChannelsInternal` renders `null` unless ALL of `currentTask`, `jwtToken`, `dataCenter`, `conversationId`, and `initialized` are truthy and `hasError` is false; the early return runs only after all hooks are called. | Mounting Engage with incomplete data or after an error must be prevented, while React's rules-of-hooks (unconditional hook calls) must be preserved. | `src/digital-channels/index.tsx` (render gate + comment "Early return after all hooks are called") | `tests/digital-channels/index.tsx` "should not render" (dataCenter empty), "should not render" (currentTask null), "should re-render when store updates are received by the widget" | none | PRESENT | +| `CC-DIGITAL-CHANNELS-R-007` | When all prerequisites are met, `DigitalChannelsInternal` renders `DigitalChannelsComponent` with `conversationId`, `jwtToken`, `dataCenter`, and `currentTheme` from the store. | The presentational component must receive exactly the store-derived values so Engage mounts against the active conversation and theme. | `src/digital-channels/index.tsx` (``) | `tests/digital-channels/index.tsx` "should successfully load and initialize real Engage component without errors", "should have proper store integration" | none | PRESENT | +| `CC-DIGITAL-CHANNELS-R-008` | `DigitalChannelsComponent` renders the `Engage` widget inside ``, setting `darktheme` when `currentTheme` uppercases to `DARK` (else `lighttheme`), and passes Engage `theme="dark"`/`"light"` plus fixed `interactionId=""`, `readonly={false}`, `isVisualRebrand={true}`. | Engage must be themed to match the desktop; the mapping is case-insensitive on `currentTheme` and defaults to light. | `src/digital-channels/DigitalChannelsComponent.tsx` (`isDarkTheme`, ``, `` props) | `tests/digital-channels/DigitalChannelsComponent.test.tsx` (DARK / LIGHT / default / lowercase / mixed-case cases); `tests/digital-channels/index.tsx` "should render with dark theme when currentTheme is DARK in store" | none | PRESENT | +| `CC-DIGITAL-CHANNELS-R-009` | `DigitalChannelsComponent` computes a `componentKey` = `${conversationId}-${jwtToken.slice(-8)}-${dataCenter}` and passes it as `Engage`'s `key`, forcing a remount when any of those change. | Prevents the embedded Froala editor from improperly reusing/reinitializing when the conversation, token, or data center changes (documented rationale in the source comment). | `src/digital-channels/DigitalChannelsComponent.tsx` (`componentKey` `useMemo`, `key={componentKey}`) | None found (no test asserts the `key`/remount behavior) | Remount-on-key behavior is not directly asserted by a test. | WEAK | +| `CC-DIGITAL-CHANNELS-R-010` | The widget is wrapped in an `ErrorBoundary` whose fallback renders an empty fragment and whose `onError` routes to `store.onErrorCallback('DigitalChannels', error)`, guarded so an absent callback is a no-op. | A render/hook error in Engage or the widget must not blank-crash the host and must be reported under the component name; hosts without a callback must not crash. | `src/digital-channels/index.tsx` (`ErrorBoundary` `fallbackRender`/`onError`) | `tests/digital-channels/index.tsx` "should call onErrorCallback when child throws", "should handle error gracefully when onErrorCallback is undefined" | none | PRESENT | + +## Design Overview +The widget is intentionally thin and store-driven. `DigitalChannels` (the export) exists only to provide the +`ErrorBoundary`; the real work is in `DigitalChannelsInternal`, an `observer()` that destructures the store +singleton and orchestrates two hooks. This keeps the component declarative and its re-render driven by MobX +observability on the store fields it reads. + +`useDigitalChannelsData` owns the "what do we need to render" concern: it asynchronously fetches the JWT via +`getAccessToken()` (setting `tokenError` on failure) and synchronously derives `conversationId` from the +current task's `callAssociatedDetails.mediaResourceId`. It returns `jwtToken`, `conversationId`, `tokenError`, +and a `hasError` alias. `useDigitalChannelsInit` owns the "initialize the third-party app exactly once" +concern: it reads the store's `isDigitalChannelsInitialized` flag and only calls `initializeApp(dataCenter, +jwtToken)` on the first successful pass, flipping the store flag so subsequent mounts (a new task, a re-render) +skip re-initialization. Both hooks wrap their async work in `try/catch` and route failures through the store +logger; neither throws into render. + +The ordering in `DigitalChannelsInternal` is deliberate: both hooks are called unconditionally (so React's +rules of hooks hold), `useDigitalChannelsInit` is fed a `skipInit` flag derived from data readiness, and only +*after* both hooks run does the component apply its render gate and return `null` when any prerequisite is +missing or an error occurred. When everything is ready it renders the presentational +`DigitalChannelsComponent`, which is the only piece that touches `cc-digital-interactions`' `Engage` widget — +wrapping it in a Momentum `` and using a composite `key` to force a clean remount when the +conversation/token/data-center identity changes. + +## Data Flow +In-process React/MobX data flow. The only external transport is (a) the store's `getAccessToken()` async call +(the store owns the wire to the token service) and (b) `cc-digital-interactions`' `initializeApp`/`Engage`, +which own their own network to the Engage backend. This module owns no network of its own. + +```mermaid +graph LR + Store[(cc-store MobX singleton)] -->|currentTask, dataCenter, currentTheme,\nisDigitalChannelsInitialized, getAccessToken, logger| Widget[DigitalChannels / DigitalChannelsInternal observer] + Widget -->|getAccessToken, currentTask, logger| DataHook[useDigitalChannelsData] + DataHook -->|jwtToken, conversationId, hasError| Widget + Widget -->|dataCenter, jwtToken, currentTask, skipInit| InitHook[useDigitalChannelsInit] + InitHook -->|initializeApp dataCenter, jwtToken| Engage3P[cc-digital-interactions app] + InitHook -->|setDigitalChannelsInitialized true| Store + InitHook -->|initialized| Widget + Widget -->|conversationId, jwtToken, dataCenter, currentTheme| Component[DigitalChannelsComponent] + Component -->|Engage props + md-theme| EngageWidget[cc-digital-interactions Engage widget] + Widget -->|onError -> onErrorCallback DigitalChannels| Store +``` + +## Sequence Diagram(s) +Sequence coverage: + +| Operation group | Diagram | Failure / recovery coverage | +|---|---|---| +| Mount → fetch token → init → render Engage | "Digital Channels mount and Engage render" | `alt` branches: token fetch reject (`hasError` → render null), init reject (logged, `initialized` stays false), and the data-not-ready gate returning null | +| Error-boundary capture | folded into the mount diagram's `ErrorBoundary` note + a dedicated `alt` | render/hook throw → empty fragment + `onErrorCallback`; absent callback = no-op | +| Theme mapping / remount | covered by Data Flow + Requirements R-008/R-009 (pure prop derivation, no distinct async sequence) | N/A — synchronous prop computation, no timeout/retry path | + +```mermaid +sequenceDiagram + participant Store as cc-store (observable) + participant Widget as DigitalChannelsInternal (observer) + participant Data as useDigitalChannelsData + participant Init as useDigitalChannelsInit + participant Engage3P as cc-digital-interactions + participant Comp as DigitalChannelsComponent + + Widget->>Store: read currentTask, dataCenter, currentTheme,\nisDigitalChannelsInitialized, getAccessToken, logger + Widget->>Data: useDigitalChannelsData({getAccessToken, currentTask, logger}) + Data->>Store: getAccessToken() + alt token resolves + Store-->>Data: jwtToken + Data-->>Widget: {jwtToken, conversationId, hasError:false} + else token rejects + Store-->>Data: Error + Data->>Store: logger.error("Failed to get access token") + Data-->>Widget: {jwtToken:'', hasError:true} + Note over Widget: render gate → return null + end + + Widget->>Init: useDigitalChannelsInit({..., skipInit: !task||!jwt||!dc}) + alt skipInit or already initialized + Init-->>Widget: initialized (unchanged / true), no initializeApp + else first run, prerequisites present + Init->>Engage3P: initializeApp(dataCenter, jwtToken) + alt init resolves + Engage3P-->>Init: ok + Init->>Store: setDigitalChannelsInitialized(true) + Init-->>Widget: initialized=true + else init rejects + Engage3P-->>Init: Error + Init->>Store: logger.error("Failed to initialize…") + Init-->>Widget: initialized=false + end + end + + alt all of currentTask, jwtToken, dataCenter, conversationId, initialized present && !hasError + Widget->>Comp: render + Comp->>Engage3P: mount in + else any missing / hasError / not initialized + Widget-->>Widget: return null + end + + Note over Widget,Store: If any render/hook throws,
ErrorBoundary → empty fragment +
store.onErrorCallback('DigitalChannels', error) (no-op if undefined) +``` + +## Class / Component Relationships +```mermaid +graph TD + DigitalChannels -->|wraps in ErrorBoundary| DigitalChannelsInternal + DigitalChannelsInternal -->|calls| useDigitalChannelsData + DigitalChannelsInternal -->|calls| useDigitalChannelsInit + DigitalChannelsInternal -->|renders| DigitalChannelsComponent + DigitalChannelsInternal -->|reads observable state| Store[(cc-store singleton)] + useDigitalChannelsData -->|getAccessToken| Store + useDigitalChannelsInit -->|initializeApp| Engage3P[cc-digital-interactions] + useDigitalChannelsInit -->|setDigitalChannelsInitialized| Store + DigitalChannelsComponent -->|mounts Engage in md-theme| EngageWidget[cc-digital-interactions Engage] + DigitalChannelsInitHookProps -.types.-> useDigitalChannelsInit + DigitalChannelsDataHookProps -.types.-> useDigitalChannelsData + DigitalChannelsComponentProps -.types.-> DigitalChannelsComponent +``` +`DigitalChannels` (exported) is a plain FC that mounts an `ErrorBoundary` around `DigitalChannelsInternal`, an +`observer()` FC. `DigitalChannelsInternal` composes the two hooks' returns with store-derived values and +renders the presentational `DigitalChannelsComponent`, which is the sole consumer of the third-party +`cc-digital-interactions` `Engage` widget. The three interfaces in +`src/digital-channels/digital-channels.types.ts` type the hook inputs and the component props; the widget adds +no class of its own. + +## Use Cases +- **UC-1 Agent opens a digital task:** Store sets `currentTask` (a digital interaction) and `dataCenter`; the + widget fetches the JWT (`getAccessToken`), derives `conversationId` from the task, initializes the Engage app + once (`initializeApp`), and renders the themed Engage widget for that conversation. Outcome: the agent sees + the Engage conversation surface. Evidence: `src/digital-channels/index.tsx`, `src/helper.ts`, + `tests/digital-channels/index.tsx` ("successfully load and initialize real Engage component"). + UI flow: nothing rendered → (data ready + init) → `` + Engage widget. +- **UC-2 No active task / incomplete data:** `currentTask` is null, `dataCenter` is empty, the token failed, or + no `conversationId` — the widget renders `null` (empty DOM). Outcome: no Engage surface, no crash. Evidence: + `src/digital-channels/index.tsx` render gate; `tests/digital-channels/index.tsx` ("should not render" for + null task / empty dataCenter). +- **UC-3 Theme switch:** Store `currentTheme` toggles between `LIGHT`/`DARK` (case-insensitive); the widget + re-renders and Engage is mounted with the corresponding `md-theme` attribute and Engage `theme` prop. + Outcome: Engage matches the desktop theme. Evidence: `DigitalChannelsComponent.tsx`; + `tests/digital-channels/DigitalChannelsComponent.test.tsx`, `tests/digital-channels/index.tsx` (dark-theme case). +- **UC-4 Runtime error in Engage/widget:** A render or hook error is caught by the `ErrorBoundary`, which + renders an empty fragment and calls `store.onErrorCallback('DigitalChannels', error)`. Outcome: host stays + alive and is notified. Evidence: `src/digital-channels/index.tsx`; `tests/digital-channels/index.tsx` + (ErrorBoundary block). + +## Error Handling & Failure Modes +| Condition | Signal (error/code/result) | Caller recovery | +|---|---|---| +| `getAccessToken()` rejects | `tokenError`/`hasError` set true; logged `[DIGITAL_CHANNELS] ❌ Failed to get access token`; widget renders `null` | None required by host; retry occurs on the next `getAccessToken`/`logger` change (effect deps). The widget silently shows nothing. | +| `conversationId` cannot be derived (no task / no `mediaResourceId`) | `conversationId = ''`; render gate returns `null` | Provide a task with `callAssociatedDetails.mediaResourceId`; expected empty state otherwise. | +| `initializeApp()` rejects | Logged `[DIGITAL_CHANNELS_INIT] ❌ Failed to initialize…` (message or "Unknown error"); `initialized` stays false; render gate returns `null` | Store flag is not set, so a later mount retries init. | +| Data not yet ready (`skipInit` true) | No `initializeApp`; `initialized` unchanged; render `null` | Wait for `currentTask` + `jwtToken` + `dataCenter`. | +| Render/hook throws | `ErrorBoundary` → empty fragment + `store.onErrorCallback('DigitalChannels', error)` | Host's error callback surfaces a notification; if `onErrorCallback` is undefined, it is a silent no-op. | + +## Pitfalls +- **`conversationId` derivation uses an inline cast, bypassing `ITask` typing.** `currentTask.data.interaction` + is cast to `{callAssociatedDetails?: {mediaResourceId?: string}}` in `src/helper.ts` because the SDK `ITask` + type does not surface `mediaResourceId` directly. If the SDK reshapes `interaction`, TypeScript will NOT + catch the break — verify against the live task shape. +- **Init is gated by a store-level flag, not local state.** `isDigitalChannelsInitialized` lives on the shared + store singleton, so `initializeApp` runs at most once *per session across all mounts*, not once per widget + instance. Do not add a second init path or reset the flag casually — re-init can break the embedded Froala + editor (the reason the remount `key` exists). +- **The render gate must stay AFTER both hook calls.** The early `return null` is placed after + `useDigitalChannelsData` and `useDigitalChannelsInit` on purpose (see the source comment). Moving the guard + above a hook call violates the rules of hooks and will crash. +- **The Engage `key` is a deliberate remount trigger, not decorative.** `componentKey` + (`conversationId`-`jwtToken.slice(-8)`-`dataCenter`) forces a full remount of `Engage` when identity changes, + preventing improper Froala cleanup/reinit. Removing or weakening the key can reintroduce editor teardown bugs. +- **`useDigitalChannelsInit` effect deps omit `dataCenter` and `isDigitalChannelsInitialized`.** The effect is + keyed on `[currentTask, skipInit, jwtToken]`. Because init reads `dataCenter` and the store flag from closure, + changing only `dataCenter` (with the same task/jwt/skipInit) will not re-run the effect. In practice + `dataCenter` changing usually coincides with a token/task change, but this is a latent staleness edge. +- **No mapped WC props.** `widget-cc-digital-channels` is registered with `r2wc(DigitalChannels, {})` — there is + no attribute/property bridge. All inputs must be present on the shared store before the element renders; you + cannot configure this widget via HTML attributes. + +## Module Do's / Don'ts +- DO: read every input from the `@webex/cc-store` singleton inside `DigitalChannelsInternal`; keep the widget + store-driven and prop-less. +- DO: keep all `cc-digital-interactions` usage confined to `DigitalChannelsComponent.tsx` (Engage) and + `helper.ts` (`initializeApp`). +- DO: wrap hook async bodies in `try/catch` and log via the store `logger` with `{module, method}` metadata. +- DON'T: move the `return null` render gate above the hook calls (rules of hooks). +- DON'T: remove/weaken the `Engage` `key`, or reset `isDigitalChannelsInitialized`, without accounting for + Froala re-init behavior. + +## Export Stability +`src/index.ts` re-exports `DigitalChannels` as both a named and the default export. The widget declares no +props, so the semver-sensitive surface is (1) the export name `DigitalChannels`, (2) the custom-element tag +`widget-cc-digital-channels` (owned by `@webex/cc-widgets/src/wc.ts`), and (3) the implicit set of store fields +it consumes. Adding a prop later is additive (minor); renaming/removing the export or the tag is a major +(breaking) change. Type declarations ship from `dist/types/index.d.ts` (`package.json` `types`). + +## Host Integration & Theming +Consumed via `@webex/cc-widgets`, which wraps `DigitalChannels` as the custom element +`widget-cc-digital-channels` (r2wc, empty prop map). The store must be initialized (`store.init(...)`) and must +have an active digital `currentTask`, a `dataCenter`, and a working `getAccessToken` before anything renders. +Theming: the presentational component wraps Engage in a Momentum `` +element (from `@momentum-ui/web-components`, a peer dep imported in `DigitalChannelsComponent.tsx`) and maps +`store.currentTheme` (case-insensitive `DARK`/anything-else) to `darktheme`/`lighttheme` plus the Engage `theme` +prop. Error reporting is wired through `store.onErrorCallback`. Peer deps: `react`/`react-dom` `>=18.3.1`, +`@momentum-ui/web-components` `^2.26.20`. + +## Test-Case Strategy (module) +Hook tests (`tests/helper.ts`) cover `useDigitalChannelsInit` (first-time init, already-initialized skip, +`skipInit` skip, init rejection with `Error` and non-`Error` throws) and `useDigitalChannelsData` (token + +`conversationId` happy path, missing task, missing `mediaResourceId`, token rejection with and without a +logger). Widget integration tests (`tests/digital-channels/index.tsx`) mock the store and +`cc-digital-interactions` and assert: full render with correct Engage attributes, dark-theme render from store, +store integration, re-render on store update (`currentTask` null → set), non-render on empty `dataCenter` / +null `currentTask`, and the `ErrorBoundary` path (callback called with `('DigitalChannels', Error)`, and +graceful handling when `onErrorCallback` is undefined). Presentational tests +(`tests/digital-channels/DigitalChannelsComponent.test.tsx`) cover theme mapping across `DARK`/`LIGHT`/default +and mixed-case values and the props passed to Engage. Each behavior has both positive and negative cases, +except the remount `key` (R-009), which has no dedicated assertion. + +| Behavior / Requirement | Existing test evidence | Gap | +|---|---|---| +| `CC-DIGITAL-CHANNELS-R-001` (token fetch + failure) | `tests/helper.ts` token success + error (+ no-logger) cases | none | +| `CC-DIGITAL-CHANNELS-R-002` (conversationId derivation) | `tests/helper.ts` happy + missing-task + missing-mediaResourceId | none | +| `CC-DIGITAL-CHANNELS-R-003` (init once) | `tests/helper.ts` first-time + already-initialized | none | +| `CC-DIGITAL-CHANNELS-R-004` (skipInit) | `tests/helper.ts` "should skip initialization when skipInit is true" | none | +| `CC-DIGITAL-CHANNELS-R-005` (init error) | `tests/helper.ts` init error + non-Error throw | none | +| `CC-DIGITAL-CHANNELS-R-006` (render gate) | `tests/digital-channels/index.tsx` null-task / empty-dataCenter / re-render | none | +| `CC-DIGITAL-CHANNELS-R-007` (renders component with store values) | `tests/digital-channels/index.tsx` full-render + store-integration | none | +| `CC-DIGITAL-CHANNELS-R-008` (theme mapping) | `DigitalChannelsComponent.test.tsx` (all theme cases); `index.tsx` dark-theme | none | +| `CC-DIGITAL-CHANNELS-R-009` (remount key) | None found | No test asserts `key`/remount on conversation/token/dataCenter change | +| `CC-DIGITAL-CHANNELS-R-010` (ErrorBoundary) | `tests/digital-channels/index.tsx` ErrorBoundary block (2 cases) | none | + +## Traceability +- Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) +- Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) (`cc-widgets.DigitalChannels`) +- Coverage state & contracts baseline: `.sdd/manifest.json` diff --git a/packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md b/packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md index d62fd4057..983dbce96 100644 --- a/packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md +++ b/packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `[NEEDS HUMAN INPUT]` / updated_at `2026-06-29` | +| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `pending` / updated_at `2026-06-29` | | Validation status | not-run | ## Evidence Rules diff --git a/packages/contact-center/station-login/ai-docs/station-login-spec.md b/packages/contact-center/station-login/ai-docs/station-login-spec.md index af8f7f862..68ed0d8a9 100644 --- a/packages/contact-center/station-login/ai-docs/station-login-spec.md +++ b/packages/contact-center/station-login/ai-docs/station-login-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| generated_by / approved_by / updated_at | migration agent / pending / 2026-06-29 | | Validation status | not-run | ## Evidence Rules diff --git a/packages/contact-center/store/ai-docs/store-spec.md b/packages/contact-center/store/ai-docs/store-spec.md index b29a6f890..8e9e76621 100644 --- a/packages/contact-center/store/ai-docs/store-spec.md +++ b/packages/contact-center/store/ai-docs/store-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | generated_by: migration agent / approved_by: [NEEDS HUMAN INPUT] / updated_at: 2026-06-29 | +| generated_by / approved_by / updated_at | generated_by: migration agent / approved_by: pending / updated_at: 2026-06-29 | | Validation status | not-run | Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with @@ -31,7 +31,7 @@ as approved unknowns only when the human explicitly defers or does not know. |---|---|---|---| | `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/AGENTS.md` | overview / API / usage | migrated | Overview, Purpose, Public Surface, Use Cases; usage snippets condensed to behavior. | | `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/ARCHITECTURE.md` | architecture / sequence diagrams | reconciled | Design Overview, Data Flow, Sequence Diagram(s), Pitfalls. Diagrams re-derived from current `store.ts` / `storeEventsWrapper.ts`; see Conflicts note below for drift corrected. | -| `contact-centre-sdk-apis/contact-center.json` | SDK API reference (TypeDoc) | reference-only | Linked as the authoritative source for SDK-shaped types/methods consumed via `store.cc.*`. | +| `@webex/contact-center` package types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) | SDK API reference (installed `.d.ts`) | reference-only | Linked as the authoritative source for SDK-shaped types/methods consumed via `store.cc.*`. | ## Overview `@webex/cc-store` is the single shared MobX store for every Webex Contact Center widget. It is the sole boundary between widgets and the `@webex/contact-center` SDK: widgets never import the SDK directly — they read observables and call methods on the store, which proxies to `store.cc.*`. The package is structured in two layers. `Store` (`src/store.ts`) is a `makeAutoObservable` singleton (`Store.getInstance()`) that holds raw observable state and owns initialization/registration with the SDK. `StoreWrapper` (`src/storeEventsWrapper.ts`) is the default export — it wraps the singleton, getter-proxies every observable, owns all SDK event wiring (CC + task events), exposes mutators (all writes funnel through `runInAction`), list-fetch helpers, callback registration, and task-lifecycle handling. @@ -73,8 +73,8 @@ This module is consumed as an imported SDK/code API (the `@webex/cc-store` packa | Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | |---|---|---|---|---|---|---| -| `cc-widgets.store` | SDK | default export `store` (StoreWrapper singleton); `init(options, setupEventListeners)`, `registerCC(webex?)`, observable getters, mutators, `getBuddyAgents/getQueues/getEntryPoints/getAddressBookEntries`, `setOnError`, `setCCCallback/removeCCCallback`, `setTaskCallback/removeTaskCallback` | Sole SDK access point and shared reactive state for all CC widgets | stable semver; observable getter set is additive | `packages/contact-center/store/src/storeEventsWrapper.ts`, `src/store.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `store.types` | SDK | type re-exports (`IContactCenter`, `ITask`, `Profile`, `Team`, `IStore`, `IStoreWrapper`, `InitParams`, `RealTimeTranscriptionData`, ~20 more) | Typed domain surface for widget code | stable semver; SDK-shaped types track the SDK | `packages/contact-center/store/src/store.types.ts:334-366`; SDK: `contact-centre-sdk-apis/contact-center.json` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `store.instance` | SDK | default export `store` (StoreWrapper singleton); `init(options, setupEventListeners)`, `registerCC(webex?)`, observable getters, mutators, `getBuddyAgents/getQueues/getEntryPoints/getAddressBookEntries`, `setOnError`, `setCCCallback/removeCCCallback`, `setTaskCallback/removeTaskCallback` | Sole SDK access point and shared reactive state for all CC widgets | stable semver; observable getter set is additive | `packages/contact-center/store/src/storeEventsWrapper.ts`, `src/store.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `store.types` | SDK | type re-exports (`IContactCenter`, `ITask`, `Profile`, `Team`, `IStore`, `IStoreWrapper`, `InitParams`, `RealTimeTranscriptionData`, ~20 more) | Typed domain surface for widget code | stable semver; SDK-shaped types track the SDK | `packages/contact-center/store/src/store.types.ts:334-366`; SDK: `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | | `store.constants` | SDK | value/enum exports (`CC_EVENTS`, `TASK_EVENTS`, `ConsultStatus`, `LoginOptions`, `CAMPAIGN_PREVIEW_*`, `DESKTOP`/`EXTENSION`/`DIAL_NUMBER`) | Event names + domain enums for widgets | stable semver | `packages/contact-center/store/src/store.types.ts:368-403` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | | `store.task-utils` | SDK | pure selectors (`isIncomingTask`, `getTaskStatus`, `getConsultStatus`, `getConferenceParticipants`, `getConferenceParticipantsCount`, `isInteractionOnHold`, `findHoldStatus`, `findHoldTimestamp`, etc.) | Read-only derivations over `ITask` | stable semver | `packages/contact-center/store/src/task-utils.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | diff --git a/packages/contact-center/task/ai-docs/task-spec.md b/packages/contact-center/task/ai-docs/task-spec.md index f4bd3d65d..060f6e128 100644 --- a/packages/contact-center/task/ai-docs/task-spec.md +++ b/packages/contact-center/task/ai-docs/task-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | generated_by: migration agent / approved_by: [NEEDS HUMAN INPUT] / updated_at: 2026-06-29 | +| generated_by / approved_by / updated_at | generated_by: migration agent / approved_by: pending / updated_at: 2026-06-29 | | Validation status | not-run | Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with `<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc metadata. @@ -76,12 +76,12 @@ packages/contact-center/task/src/ ## Public Surface | Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | |---|---|---|---|---|---|---| -| `task.IncomingTask` | SDK (React component / Web Component) | `IncomingTask` — props: `incomingTask`; callbacks: `onAccepted({task})`, `onRejected({task})` | Render an offered task with accept/decline; notify consumer on accept/reject/RONA | Stable; adding optional props/callbacks = minor | `src/task.types.ts` (`IncomingTaskProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `task.TaskList` | SDK (React component / Web Component) | `TaskList` — props: `hasCampaignPreviewEnabled?`; callbacks: `onTaskAccepted(task)`, `onTaskDeclined(task, reason)`, `onTaskSelected({task, isClicked})` | List concurrent tasks; accept/decline/select | Stable; `hasCampaignPreviewEnabled` defaults true | `src/task.types.ts` (`TaskListProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `task.CallControl` | SDK (React component / Web Component) | `CallControl` — callbacks: `onHoldResume({isHeld,task})`, `onEnd({task})`, `onWrapUp({task,wrapUpReason})`, `onRecordingToggle({isRecording,task})`, `onToggleMute({isMuted,task})`; props: `conferenceEnabled?`, `consultTransferOptions?`, `callControlClassName?`, `callControlConsultClassName?` | Active-call controls for `store.currentTask` | Stable; `conferenceEnabled` defaults `true` | `src/task.types.ts` (`CallControlProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `task.CallControlCAD` | SDK (React component / Web Component) | `CallControlCAD` — same callbacks/props as `CallControl`; emphasizes `callControlClassName` / `callControlConsultClassName` | CallControl variant styled for a customer-data layout | Stable; same surface as CallControl | `src/task.types.ts` (`CallControlProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `task.OutdialCall` | SDK (React component / Web Component) | `OutdialCall` — props: `isAddressBookEnabled?` (default `true`); no consumer callbacks | Outbound dialpad + ANI selection; disabled when a telephony task is active | Stable | `src/task.types.ts` (`OutdialProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `task.RealTimeTranscript` | SDK (React component / Web Component) | `RealTimeTranscript` — props: `liveTranscriptEntries?`, `className?` | Render live transcript for `store.currentTask` | Stable | `src/task.types.ts` (`RealTimeTranscriptProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.IncomingTask` | SDK (React component / Web Component) | `IncomingTask` — props: `incomingTask`; callbacks: `onAccepted({task})`, `onRejected({task})` | Render an offered task with accept/decline; notify consumer on accept/reject/RONA | Stable; adding optional props/callbacks = minor | `src/task.types.ts` (`IncomingTaskProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.TaskList` | SDK (React component / Web Component) | `TaskList` — props: `hasCampaignPreviewEnabled?`; callbacks: `onTaskAccepted(task)`, `onTaskDeclined(task, reason)`, `onTaskSelected({task, isClicked})` | List concurrent tasks; accept/decline/select | Stable; `hasCampaignPreviewEnabled` defaults true | `src/task.types.ts` (`TaskListProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.CallControl` | SDK (React component / Web Component) | `CallControl` — callbacks: `onHoldResume({isHeld,task})`, `onEnd({task})`, `onWrapUp({task,wrapUpReason})`, `onRecordingToggle({isRecording,task})`, `onToggleMute({isMuted,task})`; props: `conferenceEnabled?`, `consultTransferOptions?`, `callControlClassName?`, `callControlConsultClassName?` | Active-call controls for `store.currentTask` | Stable; `conferenceEnabled` defaults `true` | `src/task.types.ts` (`CallControlProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.CallControlCAD` | SDK (React component / Web Component) | `CallControlCAD` — same callbacks/props as `CallControl`; emphasizes `callControlClassName` / `callControlConsultClassName` | CallControl variant styled for a customer-data layout | Stable; same surface as CallControl | `src/task.types.ts` (`CallControlProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.OutdialCall` | SDK (React component / Web Component) | `OutdialCall` — props: `isAddressBookEnabled?` (default `true`); no consumer callbacks | Outbound dialpad + ANI selection; disabled when a telephony task is active | Stable | `src/task.types.ts` (`OutdialProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-widgets.RealTimeTranscript` | SDK (React component / Web Component) | `RealTimeTranscript` — props: `liveTranscriptEntries?`, `className?` | Render live transcript for `store.currentTask` | Stable | `src/task.types.ts` (`RealTimeTranscriptProps`) | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | Compatibility notes: - Adding an optional prop/callback is additive (minor); removing or renaming one, or changing a callback payload shape, is breaking (major) — these widgets are consumed via r2wc Web Components in `@webex/cc-widgets`. diff --git a/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md b/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md index 19b71d248..4a4efdbb5 100644 --- a/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md +++ b/packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md @@ -12,7 +12,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| generated_by / approved_by / updated_at | migration agent / pending / 2026-06-29 | | Validation status | not-run | Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with `<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc metadata. @@ -65,26 +65,26 @@ test-fixtures/src/ ## Public Surface Internal Surface — consumed only by other packages' Jest tests in this monorepo. There is no network/event/CLI contract; the contract is the set of TypeScript exports below, all re-exported through `src/index.ts`. Each is summarized here — read the source file for the exact object shape. -| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Entry point | |---|---|---|---|---|---|---| -| `test-fixtures.mockCC` | SDK export | `mockCC: IContactCenter` | Mock SDK instance; methods are `jest.fn()` so tests can spy/override | Shape must track `IContactCenter`; removing a mocked method may break consumer tests | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockProfile` | SDK export | `mockProfile: Profile` | Full agent profile (teams, idle/wrapup codes, dial plan, flags) | Track `Profile`; additive fields safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockTask` | SDK export | `mockTask: ITask` | Connected telephony task with nested `interaction`; methods are `jest.fn()` | Track `ITask` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.makeMockTask` | SDK export | `makeMockTask(overrides?): ITask` | Factory producing a fresh task with deep `data`/`interaction` overrides and fresh `jest.fn()`s | Override shape `MakeMockTaskOverrides` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockCampaignTask` | SDK export | `mockCampaignTask: ITask` | Campaign-preview-shaped task (CPD + outbound details) | Track `ITask` + campaign CPD keys | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.makeMockCampaignTask` | SDK export | `makeMockCampaignTask(overrides?): ITask` | Factory for campaign-preview task with `cpd`/`interaction`/`data` overrides | Override shape `IMakeMockCampaignTaskOverrides` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockCampaignCpd` | data export | `mockCampaignCpd: Record` | Default campaign-preview call-processing-detail values | Additive keys safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockQueueDetails` | data export | `mockQueueDetails` | Two fully-populated queue config objects for transfer/queue tests | Additive fields safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockAgents` | data export | `mockAgents` | Buddy-agent list for transfer/consult tests | Additive fields safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockEntryPointsResponse` | data export | `mockEntryPointsResponse: EntryPointListResponse` | Outdial entry-points response | Track `EntryPointListResponse` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockAddressBookEntriesResponse` | data export | `mockAddressBookEntriesResponse: AddressBookEntriesResponse` | Address-book entries response | Track `AddressBookEntriesResponse` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.makeMockAddressBook` | SDK export | `makeMockAddressBook(getEntriesMock?): AddressBook` | Factory for an `AddressBook` mock; default `getEntries` resolves the entries response | Track `AddressBook` | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockCallAssociatedData` | data export | `mockCallAssociatedData` | Call-associated-data variants (global, viewable/hidden, secure) | Additive keys safe | `src/fixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockIncomingTaskData` | data export | `mockIncomingTaskData` | Incoming-task UI data keyed `webRTC`/`extension`/`social`/`chat` | Additive scenario keys safe | `src/incomingTaskFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockTaskData` | data export | `mockTaskData` | Task-list UI data keyed `active`/`incoming`/`action`/`selection` | Additive scenario keys safe | `src/taskListFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockOutdialCallProps` | data export | `mockOutdialCallProps` | `mockCC` spread + `startOutdial`/`getOutdialANIEntries` jest mocks | Spread of `mockCC` | `src/components/task/outdialCallFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockAniEntries` | data export | `mockAniEntries` | Outdial ANI entry list | Additive fields safe | `src/components/task/outdialCallFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `test-fixtures.mockCCWithAni` | data export | `mockCCWithAni` | `mockCC` + `agentConfig.outdialANIId` + ANI-resolving `getOutdialAniEntries` | Spread of `mockCC` | `src/components/task/outdialCallFixtures.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `test-fixtures.mockCC` | SDK export | `mockCC: IContactCenter` | Mock SDK instance; methods are `jest.fn()` so tests can spy/override | Shape must track `IContactCenter`; removing a mocked method may break consumer tests | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockProfile` | SDK export | `mockProfile: Profile` | Full agent profile (teams, idle/wrapup codes, dial plan, flags) | Track `Profile`; additive fields safe | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockTask` | SDK export | `mockTask: ITask` | Connected telephony task with nested `interaction`; methods are `jest.fn()` | Track `ITask` | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.makeMockTask` | SDK export | `makeMockTask(overrides?): ITask` | Factory producing a fresh task with deep `data`/`interaction` overrides and fresh `jest.fn()`s | Override shape `MakeMockTaskOverrides` | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockCampaignTask` | SDK export | `mockCampaignTask: ITask` | Campaign-preview-shaped task (CPD + outbound details) | Track `ITask` + campaign CPD keys | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.makeMockCampaignTask` | SDK export | `makeMockCampaignTask(overrides?): ITask` | Factory for campaign-preview task with `cpd`/`interaction`/`data` overrides | Override shape `IMakeMockCampaignTaskOverrides` | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockCampaignCpd` | data export | `mockCampaignCpd: Record` | Default campaign-preview call-processing-detail values | Additive keys safe | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockQueueDetails` | data export | `mockQueueDetails` | Two fully-populated queue config objects for transfer/queue tests | Additive fields safe | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockAgents` | data export | `mockAgents` | Buddy-agent list for transfer/consult tests | Additive fields safe | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockEntryPointsResponse` | data export | `mockEntryPointsResponse: EntryPointListResponse` | Outdial entry-points response | Track `EntryPointListResponse` | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockAddressBookEntriesResponse` | data export | `mockAddressBookEntriesResponse: AddressBookEntriesResponse` | Address-book entries response | Track `AddressBookEntriesResponse` | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.makeMockAddressBook` | SDK export | `makeMockAddressBook(getEntriesMock?): AddressBook` | Factory for an `AddressBook` mock; default `getEntries` resolves the entries response | Track `AddressBook` | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockCallAssociatedData` | data export | `mockCallAssociatedData` | Call-associated-data variants (global, viewable/hidden, secure) | Additive keys safe | `src/fixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockIncomingTaskData` | data export | `mockIncomingTaskData` | Incoming-task UI data keyed `webRTC`/`extension`/`social`/`chat` | Additive scenario keys safe | `src/incomingTaskFixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockTaskData` | data export | `mockTaskData` | Task-list UI data keyed `active`/`incoming`/`action`/`selection` | Additive scenario keys safe | `src/taskListFixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockOutdialCallProps` | data export | `mockOutdialCallProps` | `mockCC` spread + `startOutdial`/`getOutdialANIEntries` jest mocks | Spread of `mockCC` | `src/components/task/outdialCallFixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockAniEntries` | data export | `mockAniEntries` | Outdial ANI entry list | Additive fields safe | `src/components/task/outdialCallFixtures.ts` | internal (`src/index.ts`) | +| `test-fixtures.mockCCWithAni` | data export | `mockCCWithAni` | `mockCC` + `agentConfig.outdialANIId` + ANI-resolving `getOutdialAniEntries` | Spread of `mockCC` | `src/components/task/outdialCallFixtures.ts` | internal (`src/index.ts`) | Compatibility notes: - Adding a new fixture export or an additive field on existing data fixtures is non-breaking. Removing or renaming an export, or removing a method on `mockCC`/`mockTask`, can break consumer test files that reference it — grep consumers before changing. diff --git a/packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md b/packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md index f66bc59af..8eeba953a 100644 --- a/packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md +++ b/packages/contact-center/ui-logging/ai-docs/ui-logging-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `[NEEDS HUMAN INPUT]` / updated_at `2026-06-29` | +| generated_by / approved_by / updated_at | generated_by `migration agent` / approved_by `pending` / updated_at `2026-06-29` | | Validation status | not-run | Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with diff --git a/packages/contact-center/user-state/ai-docs/user-state-spec.md b/packages/contact-center/user-state/ai-docs/user-state-spec.md index 0bc9d655a..9ccdbb105 100644 --- a/packages/contact-center/user-state/ai-docs/user-state-spec.md +++ b/packages/contact-center/user-state/ai-docs/user-state-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | migration agent / [NEEDS HUMAN INPUT] / 2026-06-29 | +| generated_by / approved_by / updated_at | migration agent / pending / 2026-06-29 | | Validation status | not-run | Coverage score: `Pending coverage assessment` before the first report; after assessment, replace with `<0-100%>` plus the report path/evidence. Keep manifest coverage state outside the rendered module doc metadata. @@ -246,7 +246,7 @@ classDiagram - **UC-3 Rejected state change:** SDK rejects → `currentState` reverts to the previous value, error logged, loading flag cleared. Evidence: `src/helper.ts`, `tests/helper.ts` "should handle errors from setAgentState and revert state". - **UC-4 External/custom state applied:** Store `customState` set with a `developerName` (e.g. RONA) → `customState` effect fires `onStateChange(customState)` directly. Evidence: `src/helper.ts`, `tests/helper.ts` "should call onStateChange with customState if provided". -### UI Flow (per use case) +## UI Flow - Primary surface is a single state dropdown rendered by `UserStateComponent`: lists Available plus the store's `idleCodes`. While a change is in flight, `isSettingAgentStatus` is `true` (loading). The state-duration timer shows `elapsedTime` (seconds, clamped ≥ 0); the idle-code timer shows `lastIdleStateChangeElapsedTime` and is hidden when that value is `-1` (Available). On a render error the widget shows nothing (empty fragment). Detailed presentation belongs to `cc-components` (`UserStateComponent`). ## State Model