Files
agent-desk-ng/docs/superpowers/plans/2026-10-14-operator-workspace.md

293 lines
13 KiB
Markdown

# Operator Workspace: Slash Commands, Collision Detection & Alerts Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Enhance the human operator workspace with inline slash command (`/`) quick replies, real-time operator collision detection (preventing two operators from answering the same customer at once), and audio-visual alerts for incoming messages.
**Architecture:** Extend the existing WebSocket service with a lightweight `operator.presence` topic broadcast (`viewing`, `typing`, `left`). Integrate slash command trigger into `SharedMessageEditor` for instant macro insertion. Add client-side presence state tracking with automatic TTL decay, rendering an operator collision banner in the chat panel. Provide synthesized Web Audio chime alerts for incoming customer messages.
**Tech Stack:** Go 1.26, Gorilla WebSocket, Next.js 16 App Router, TypeScript, Tiptap, Base UI / Tailwind CSS, Web Audio API.
**Spec:** Agreed enhancements roadmap (Option 2 - Operator Workspace improvements).
## Global Constraints
- Follow AGENTS.md layer ownership (no direct SQL in handlers, clean WebSocket envelope structures).
- Symmetrical 3-locale i18n parity (`zh-CN.json`, `en-US.json`, `ru-RU.json`).
- Zero external binary/audio assets — synthesize audio chime using native Web Audio API.
- TypeScript compilation must pass with 0 errors (`tsc --noEmit`).
- Go formatting (`gofmt`) and all Go tests must pass cleanly.
## Review Focus
1. Operator presence event storms: typing events must be throttled (max once per 3s).
2. Presence cleanup: presence records must auto-expire after TTL (15s) so disconnected or crashed tabs don't show ghost operators.
3. Slash command replacement: selecting a quick reply via `/` must replace the `/` text cleanly without leaving orphan slashes.
4. Audio context autoplay policies: Web Audio API must initialize lazily upon user interaction and safely no-op when blocked.
5. Multi-operator collision UI: banner must be informative, non-disruptive, and properly localized across all 3 languages.
---
### Task 1: Backend Operator Presence Event & WebSocket Protocol
**Files:**
- Modify: `internal/pkg/enums/im.go`
- Modify: `internal/services/ws_realtime_types.go`
- Modify: `internal/services/ws_service.go`
- Test: `internal/services/ws_service_test.go`
**Interfaces:**
- Produces:
- `enums.IMRealtimeEventOperatorPresence = "operator.presence"`
- `RealtimeOperatorPresencePayload`:
```go
type RealtimeOperatorPresencePayload struct {
ConversationID int64 `json:"conversationId"`
UserID int64 `json:"userId"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Action string `json:"action"` // "viewing", "typing", "left"
At string `json:"at"`
}
```
- `RealtimeOperatorPresenceEvent` implementing `RealtimeDomainEvent`.
- Client message parsing in `ws_service.go` for `type: "presence"`.
- `WsService.PublishOperatorPresence(conversationID int64, principal *dto.AuthPrincipal, action string, excludeConnID string)`.
- [ ] **Step 1: Write test for Operator Presence publishing**
In `internal/services/ws_service_test.go`:
Add `TestPublishOperatorPresence` verifying that publishing presence sends the event to subscribers on `conversation:{id}` while skipping `excludeConnID`.
- [ ] **Step 2: Run test to verify it fails**
Run: `& 'C:\Program Files\Go\bin\go.exe' test -run TestPublishOperatorPresence ./internal/services/...`
Expected: FAIL
- [ ] **Step 3: Implement enum, payload, event, and WsService methods**
1. In `internal/pkg/enums/im.go`: define `IMRealtimeEventOperatorPresence = "operator.presence"`.
2. In `internal/services/ws_realtime_types.go`: define `RealtimeOperatorPresencePayload` and `RealtimeOperatorPresenceEvent`.
3. In `internal/services/ws_service.go`:
- Handle client message `type == "presence"`: parse `conversationId` and `action`, call `PublishOperatorPresence`.
- Implement `PublishOperatorPresence(conversationID int64, principal *dto.AuthPrincipal, action string, excludeConnID string)`.
- [ ] **Step 4: Run test to verify it passes**
Run: `& 'C:\Program Files\Go\bin\go.exe' test -run TestPublishOperatorPresence ./internal/services/...`
Expected: PASS
- [ ] **Step 5: Run gofmt and commit**
```bash
git add internal/pkg/enums/im.go internal/services/ws_realtime_types.go internal/services/ws_service.go internal/services/ws_service_test.go
git commit -m "feat(ws): support operator.presence realtime event and client presence messages"
```
---
### Task 2: Symmetrical i18n Translations for Collision Alerts and Audio Notifications
**Files:**
- Modify: `web/messages/en-US.json`
- Modify: `web/messages/ru-RU.json`
- Modify: `web/messages/zh-CN.json`
- Test: `web/i18n/message-integrity.test.mjs`
**Interfaces:**
- Produces: Symmetrical keys across all 3 locales:
- `operatorPresence.viewing`: "{name} is viewing this conversation" / "{name} просматривает этот диалог" / "{name} 正在查看此会话"
- `operatorPresence.typing`: "{name} is typing a reply..." / "{name} печатает ответ..." / "{name} 正在输入回复..."
- `operatorPresence.multipleViewing`: "{count} operators are viewing this conversation" / "{count} оператора просматривают этот диалог" / "{count} 位客服正在查看此会话"
- `operatorPresence.collisionWarning`: "Warning: Another operator is active here to avoid duplicate replies." / "Внимание: другой оператор активен в этом диалоге во избежание дублирования ответов." / "注意:其他客服正在处理此会话,请避免重复回复。"
- `conversation.quickReplySlashHint`: "Type / for quick replies" / "Введите / для быстрого ответа" / "输入 / 快捷回复"
- [ ] **Step 1: Check baseline integrity**
Run: `node --test web/i18n/message-integrity.test.mjs`
- [ ] **Step 2: Add keys symmetrically to all 3 JSON files**
Add `operatorPresence.*` and `conversation.quickReplySlashHint` with exact placeholder parity `{name}` and `{count}`.
- [ ] **Step 3: Run integrity tests**
Run: `node --test web/i18n/message-integrity.test.mjs`
Expected: PASS (0 missing keys, 0 placeholder mismatches).
- [ ] **Step 4: Commit**
```bash
git add web/messages/en-US.json web/messages/ru-RU.json web/messages/zh-CN.json
git commit -m "feat(i18n): add localized messages for operator collision and slash commands"
```
---
### Task 3: Slash Commands (`/`) Quick Reply Autocomplete in SharedMessageEditor
**Files:**
- Modify: `web/components/chat/shared-message-editor.tsx`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/agent-message-editor.tsx`
- Test: `web/node_modules/.bin/tsc.cmd --noEmit` and manual verification
**Interfaces:**
- Consumes: `quickReplies` prop in `SharedMessageEditor`
- Produces:
- When typing `/` as first character or when editor text matches `/query`, opens the Quick Replies popover and filters items by `query`.
- Selecting a quick reply removes the `/query` prefix and inserts the chosen quick reply content.
- Shows subtle hint `conversation.quickReplySlashHint` in editor toolbar for operator variant.
- [ ] **Step 1: Implement slash trigger and query tracking**
In `web/components/chat/shared-message-editor.tsx`:
1. In `editor.on("update")`:
- Inspect plain text of current block.
- If text matches `^/([^\s]*)$`:
- Open quick replies popover.
- Set quick reply search query to match the typed characters.
2. In `handleInsertQuickReply(item)`:
- If current text starts with `/`, delete the slash prefix before inserting `item.content`.
3. In toolbar: show subtle hint for operators (`conversation.quickReplySlashHint`).
- [ ] **Step 2: Run typecheck**
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
Expected: 0 errors.
- [ ] **Step 3: Commit**
```bash
git add web/components/chat/shared-message-editor.tsx web/app/\(dashboard\)/dashboard/conversations/_components/agent-message-editor.tsx
git commit -m "feat(editor): support slash command / for quick reply autocomplete"
```
---
### Task 4: Operator Presence Tracking & Collision Warning UI
**Files:**
- Create: `web/lib/operator-presence-state.ts`
- Create: `web/lib/operator-presence-state.test.mjs`
- Modify: `web/hooks/use-agent-conversation-realtime.ts`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/chat-panel.tsx`
- Test: `node --test web/lib/operator-presence-state.test.mjs` and `web/node_modules/.bin/tsc.cmd --noEmit`
**Interfaces:**
- Produces:
- `resolveOperatorPresence(currentList, incomingEvent, currentUserId)` -> pure function returning active peer presences, purging expired entries (>15s TTL).
- Outgoing presence messages:
- `"viewing"` sent when selecting conversation.
- `"typing"` sent when operator types (throttled to 3s).
- `"left"` sent when unselecting conversation.
- In `chat-panel.tsx`:
- Renders an alert banner above the message editor or below conversation header if peer operators are active.
- Displays avatar badge and text e.g. "Jane is viewing this conversation" or "Jane is typing a reply...".
- [ ] **Step 1: Write unit tests for pure presence state transitions**
Create `web/lib/operator-presence-state.test.mjs` verifying:
- Adds new peer operator with timestamp.
- Ignores self (currentUserId).
- Updates action from "viewing" to "typing".
- Removes operator on action "left".
- Filters out records older than 15 seconds.
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test web/lib/operator-presence-state.test.mjs`
Expected: FAIL
- [ ] **Step 3: Implement `web/lib/operator-presence-state.ts`**
Export:
- `OperatorPresenceRecord`
- `updateOperatorPresenceList`
- `purgeExpiredOperatorPresences`
- [ ] **Step 4: Run test to verify it passes**
Run: `node --test web/lib/operator-presence-state.test.mjs`
Expected: PASS
- [ ] **Step 5: Integrate into `use-agent-conversation-realtime.ts` and `chat-panel.tsx`**
1. In `use-agent-conversation-realtime.ts`:
- Send `{"type": "presence", "conversationId": id, "action": "viewing"}` on subscribe.
- Send `{"type": "presence", "conversationId": id, "action": "left"}` on unsubscribe.
- Listen for `operator.presence` events and update store presence state.
2. In `chat-panel.tsx`:
- When active peer operators exist on `currentConversation.id`:
- Render collision warning banner with `UsersIcon` and localized description.
- [ ] **Step 6: Run typecheck and tests**
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
Run: `node --test web/lib/operator-presence-state.test.mjs`
Expected: ALL PASS with 0 errors.
- [ ] **Step 7: Commit**
```bash
git add web/lib/operator-presence-state.ts web/lib/operator-presence-state.test.mjs web/hooks/use-agent-conversation-realtime.ts web/app/\(dashboard\)/dashboard/conversations/_components/chat-panel.tsx
git commit -m "feat(presence): detect operator collision and render warning banner"
```
---
### Task 5: Incoming Message Audio & Expanded Browser Alerting
**Files:**
- Create: `web/lib/sound-alert.ts`
- Create: `web/lib/sound-alert.test.mjs`
- Modify: `web/hooks/use-agent-conversation-realtime.ts`
- Test: `node --test web/lib/sound-alert.test.mjs` and `web/node_modules/.bin/tsc.cmd --noEmit`
**Interfaces:**
- Produces:
- `playIncomingMessageSound()`: synthesizes a clean 587Hz -> 880Hz (D5 -> A5) chime via Web Audio API. Safe when audio is suspended or unsupported.
- In `use-agent-conversation-realtime.ts`:
- When `message.created` arrives from a customer:
- If message is from customer and either:
- assigned to current operator, OR
- conversation is unassigned / pending
- Play sound alert via `playIncomingMessageSound()`.
- Show browser notification if tab is in background.
- [ ] **Step 1: Write unit test for sound alert helper**
In `web/lib/sound-alert.test.mjs`:
Test safe fallback when `AudioContext` is undefined (Node.js/SSR environment).
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test web/lib/sound-alert.test.mjs`
Expected: FAIL
- [ ] **Step 3: Implement `web/lib/sound-alert.ts`**
Synthesizes gentle dual-tone sine oscillator with exponential decay.
- [ ] **Step 4: Run test to verify it passes**
Run: `node --test web/lib/sound-alert.test.mjs`
Expected: PASS
- [ ] **Step 5: Wire into `use-agent-conversation-realtime.ts`**
Trigger `playIncomingMessageSound()` on new customer messages for assigned/pending conversations.
- [ ] **Step 6: Run typecheck and all tests**
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
Run: `node --test web/lib/*.test.mjs web/i18n/*.test.mjs`
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/...`
Expected: ALL PASS with 0 errors.
- [ ] **Step 7: Commit**
```bash
git add web/lib/sound-alert.ts web/lib/sound-alert.test.mjs web/hooks/use-agent-conversation-realtime.ts
git commit -m "feat(notification): add audio chime and expanded alerts for incoming customer messages"
```