13 KiB
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
- Operator presence event storms: typing events must be throttled (max once per 3s).
- Presence cleanup: presence records must auto-expire after TTL (15s) so disconnected or crashed tabs don't show ghost operators.
- Slash command replacement: selecting a quick reply via
/must replace the/text cleanly without leaving orphan slashes. - Audio context autoplay policies: Web Audio API must initialize lazily upon user interaction and safely no-op when blocked.
- 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: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"` }RealtimeOperatorPresenceEventimplementingRealtimeDomainEvent.- Client message parsing in
ws_service.gofortype: "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
- In
internal/pkg/enums/im.go: defineIMRealtimeEventOperatorPresence = "operator.presence". - In
internal/services/ws_realtime_types.go: defineRealtimeOperatorPresencePayloadandRealtimeOperatorPresenceEvent. - In
internal/services/ws_service.go:- Handle client message
type == "presence": parseconversationIdandaction, callPublishOperatorPresence. - Implement
PublishOperatorPresence(conversationID int64, principal *dto.AuthPrincipal, action string, excludeConnID string).
- Handle client message
- 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
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
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 --noEmitand manual verification
Interfaces:
-
Consumes:
quickRepliesprop inSharedMessageEditor -
Produces:
- When typing
/as first character or when editor text matches/query, opens the Quick Replies popover and filters items byquery. - Selecting a quick reply removes the
/queryprefix and inserts the chosen quick reply content. - Shows subtle hint
conversation.quickReplySlashHintin editor toolbar for operator variant.
- When typing
-
Step 1: Implement slash trigger and query tracking
In web/components/chat/shared-message-editor.tsx:
- 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.
- In
handleInsertQuickReply(item):- If current text starts with
/, delete the slash prefix before insertingitem.content.
- If current text starts with
- 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
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.mjsandweb/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.tsandchat-panel.tsx
- 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.presenceevents and update store presence state.
- Send
- In
chat-panel.tsx:- When active peer operators exist on
currentConversation.id:- Render collision warning banner with
UsersIconand localized description.
- Render collision warning banner with
- When active peer operators exist on
- 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
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.mjsandweb/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.createdarrives 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.
- If message is from customer and either:
- When
-
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
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"