Skip to content

fix(site/AgentsPage): suppress unread dot while a chat is streaming#25373

Open
blinkagent[bot] wants to merge 1 commit into
mainfrom
chats-no-unread-while-streaming
Open

fix(site/AgentsPage): suppress unread dot while a chat is streaming#25373
blinkagent[bot] wants to merge 1 commit into
mainfrom
chats-no-unread-while-streaming

Conversation

@blinkagent
Copy link
Copy Markdown
Contributor

@blinkagent blinkagent Bot commented May 14, 2026

Summary

The per-chat unread indicator in the agents sidebar was being painted at the same time the row spinner was running. Two related problems with that:

  1. Mixed signals. If the spinner is going, the chat is mid-turn and the user has nothing to act on. An unread dot in that state implies "come look / agent is waiting on you," which is the opposite of what we want.
  2. Flicker. The dot would appear and then vanish a beat later as intermediate status_change events raced the authoritative has_unread value computed by the server on the next list refetch.

Root cause

Two independent paths converged on this:

  • mergeWatchedChatSummary in site/src/api/queries/chats.ts flipped has_unread = true on any status_change event for a non-active chat, including intermediate pendingrunning transitions during a turn.
  • The sidebar row rendered the unread dot purely off chat.has_unread && !isActiveChat, regardless of streaming state, so any cached has_unread = true would paint even while chat.status was running / pending.

The server-side has_unread (coderd/database/queries/chats.sql) is based on the last_read_message_id cursor, which is only advanced by markChatAsRead in coderd/exp_chats.go on stream connect/disconnect. That part is correct and unchanged.

Fix

Two coordinated changes on the client:

  • mergeWatchedChatSummary: only flip has_unread to true on a status_change when the new status is not running / pending. Existing has_unread is preserved (a chat that was already unread before the new turn started keeps its dot until the user opens it). Terminal-ish transitions (waiting, completed, requires_action, error) still mark inactive chats unread.
  • AgentsSidebar row: gate both the visible unread dot and the sr-only "(unread)" label on !isStreaming. This defends against the case where the cache carried has_unread = true from before the current turn began.

Tests

Four new cases in chats.test.ts under mergeWatchedChatSummary:

  • does not mark inactive chats unread when transitioning to running
  • does not mark inactive chats unread when transitioning to pending
  • preserves existing has_unread = true when transitioning to a streaming status
  • still marks inactive chats unread when transitioning to waiting

Existing test "marks other chats unread on fresh status updates" continues to pass (it transitions to completed, which is terminal).

pnpm vitest run src/api/queries/chats.test.ts
  Test Files  1 passed (1)
       Tests  94 passed (94)

pnpm biome check ...     # clean
pnpm tsc -p .            # clean

Requested on behalf of @tracyjohnsonux.

The sidebar was painting a per-chat unread indicator at the same time
the row spinner was running. That combination implies the agent is
waiting on user action, but during running/pending the chat is mid-turn
and the user has nothing to act on. The dot also flickered (appear,
then vanish) as intermediate status_change events raced the
authoritative server value from the next list refetch.

Two coordinated fixes:

- mergeWatchedChatSummary: do not flip has_unread to true on a
  status_change event when the new status is running or pending.
  Existing has_unread is still preserved (so a chat that was already
  unread before the new turn started keeps its dot until the user
  opens it). Terminal-ish transitions (waiting / completed /
  requires_action / error) still mark inactive chats unread.

- AgentsSidebar row: gate the visible unread indicator and sr-only
  unread label on !isStreaming, so any cached has_unread=true is
  hidden while the row is mid-turn. Defends against the case where
  the cache carried has_unread=true from before the current turn.

Tests cover the four new branches in mergeWatchedChatSummary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant