Files
kandev/docs/specs/quick-chat-idle-dot/spec.md
T
Clément Fiere ba96562f04 feat: add quick chat idle dot indicator (#2750)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-08-18 06:52:26 +00:00

124 lines
6.7 KiB
Markdown

---
status: draft
created: 2026-08-16
owner: kandev
---
# Quick Chat Idle Dot
## Why
Quick chat sessions keep running after the user closes the dialog. A session's
turn can finish while the chat window is out of view, and nothing tells the
user the agent has answered. Replies are missed until the user happens to
reopen the dialog.
## What
- Each Quick Chat entry icon in the active workspace — the sidebar rail Quick
Chat item, the sidebar Quick Chat shortcut, the tablet kanban header button,
the mobile kanban header button, and the mobile task-switcher sheet button —
shows a small red dot in the icon's corner while the Quick Chat dialog is
closed and at least one quick chat session of that workspace has completed a
turn (or settled to idle) since the dialog was last open.
- A turn that completes while the dialog is open does NOT raise a dot; the
user is looking at the chat window.
- Opening the Quick Chat dialog clears every dot. Closing it re-arms tracking:
a later turn completion while closed raises the dot again.
- The dot is per-workspace: only sessions belonging to the active workspace
contribute to that workspace's entry icons.
- A session whose tab is closed or whose backing task is deleted stops
contributing immediately.
- The dot is decorative (`aria-hidden`). Button labels, tooltips, and the
dialog itself are unchanged.
## State machine
The unseen marker is a client-only, ephemeral flag keyed per (workspace,
session id): `unseenIdleByWorkspace[workspaceId][sessionId]`. Workspace
ownership lives in the key, so it survives hydration of a different
workspace's session list.
| State | Transition | Trigger | Actor |
|---|---|---|---|
| unmarked | marked | `session.state_changed` settles an active session (STARTING/RUNNING → IDLE/WAITING_FOR_INPUT/COMPLETED/FAILED/CANCELLED) or `session.turn.completed` arrives, while the dialog is closed, for a session known to `quickChat.sessions` (workspace taken from that tab entry) | WS handler |
| marked | unmarked | dialog opens (`openQuickChat`) — all workspaces | user |
| marked | unmarked | session becomes the active dialog tab | user |
| marked | unmarked | session tab closed or backing task deleted | user / sync |
| marked | unmarked | revision-guarded server resync removes the session from its workspace's list | resync |
## Failure modes
- **WS disconnected**: `session.state_changed` / `session.turn.completed` are
live broadcasts; a turn that completes while the socket is down is never
delivered and no dot is raised. There is no backfill. The dot is a
best-effort cue, not a reliable delivery ledger.
- **Race: event before tab**: a settle event for a session whose tab is not
yet in `quickChat.sessions` (e.g. the `task.created` tab arrives after the
session already settled) does not raise a dot. The user never saw that chat
window, so no dot is the conservative outcome.
- **Re-delivery**: a re-delivered `session.turn.completed` (reconnect replay,
duplicate broadcast) for a turn already recorded as completed does not raise
a dot — not after the tab arrives, and not after the dialog was opened and
cleared the marker. Only a genuinely new turn completion raises the dot.
For `session.state_changed` the same guarantee holds within the settle
ledger's retention: the generation (`updated_at`) is kept per session for a
60-minute window (max 500 sessions); a duplicate delivered after its
generation was evicted (aged out or capped out) MAY raise a dot — accepted
bounded-memory tradeoff, since practical replay windows are seconds; the dot
self-heals on the next dialog open.
- **Abandoned turns**: `AbandonOpenTurns` (session resume/startup cleanup,
rejected-message cleanup) publishes `session.turn.completed` for orphan
turns whose `completed_at` equals `started_at` — the backend already
recognizes this shape (`isAbandonedTurnCompletion` in
`apps/backend/internal/backendapp/gateway.go`), so a discriminator EXISTS in
the payload. The marker treats these uniformly with live completions and
deliberately does NOT filter on it: the session did settle to idle, and the
`session.state_changed` settle transition raises the same signal, so
excluding abandoned closures would not prevent the dot anyway. This is an
intentional product decision, pinned by a test; filtering is out of scope.
- **Receipt-time semantics**: the marker reflects events handled while the
dialog is closed, not the instant the turn completed. `session.state_changed`
and `session.turn.completed` travel over separate event-bus subscriptions
and the gateway broadcasts both to every connected client (the payloads
carry no `workspace_id`, so the workspace-broadcast path falls back to
global delivery); per-workspace dots rely on session-tab membership and the
selector, not transport scoping. Their relative order is not guaranteed; a
completion notification handled while the dialog is open never marks, and
one handled after the dialog closed marks, even if the turn itself finished
earlier. The false-positive window is one notification round-trip and
self-heals on the next dialog open.
## Scenarios
- **GIVEN** no quick chat session in the active workspace, **WHEN** the app
renders, **THEN** no Quick Chat entry icon shows a dot.
- **GIVEN** a quick chat session with a running turn and the dialog closed,
**WHEN** the turn completes (the session settles to an idle state), **THEN**
a red dot appears on every Quick Chat entry icon of that workspace.
- **GIVEN** a quick chat session with a running turn, **WHEN** the turn
completes while the dialog is open, **THEN** no dot appears for that turn.
- **GIVEN** a dot showing, **WHEN** the user opens the Quick Chat dialog,
**THEN** the dot disappears.
- **GIVEN** a dot cleared by opening the dialog, **WHEN** the same session
later completes another turn while the dialog is closed, **THEN** the dot
reappears.
- **GIVEN** an unseen idle session in workspace A and none in workspace B,
**WHEN** the user switches to workspace B, **THEN** workspace B's entry
icons show no dot.
- **GIVEN** a dot contributed by one session, **WHEN** that session's tab is
closed or its backing task is deleted, **THEN** the dot disappears.
- **GIVEN** a quick chat session tab that has never completed a turn,
**WHEN** the page reloads, **THEN** no dot shows (markers are ephemeral and
not persisted).
## Out of scope
- No persistence: markers reset on page reload (scenario above).
- No new copy, tooltips, sounds, or banner notifications.
- No dot on the command-palette Quick Chat item or the config-chat floating
panel.
- No backend changes: the existing global broadcasts of
`session.state_changed` and `session.turn.completed` are the only signals.
- No change to quick chat session creation, tab sync, or dialog behavior.