ba96562f04
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
6.7 KiB
6.7 KiB
status, created, owner
| status | created | owner |
|---|---|---|
| draft | 2026-08-16 | 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.completedare 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. thetask.createdtab 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. Forsession.state_changedthe 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) publishessession.turn.completedfor orphan turns whosecompleted_atequalsstarted_at— the backend already recognizes this shape (isAbandonedTurnCompletioninapps/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 thesession.state_changedsettle 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_changedandsession.turn.completedtravel over separate event-bus subscriptions and the gateway broadcasts both to every connected client (the payloads carry noworkspace_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_changedandsession.turn.completedare the only signals. - No change to quick chat session creation, tab sync, or dialog behavior.