Skip to main content

Architecture: Live Status, Input Required & Unviewed Message Indicators on Sidebar Conversations

Decision​

Surface per-conversation streaming, input-required, and unviewed state directly in the sidebar with three distinct visual treatments:

AlternativeProsConsDecision
Green antenna (live) + amber question (input) + blue dot (unviewed)Clear three-phase lifecycle, always visible, leverages existing A2A input-required stateAdds new store state for both unviewed and input-required trackingSelected
Single green dot for all statesSimplerNo distinction between statesRejected
Browser notifications for completed responsesWorks even when tab is backgroundedIntrusive, requires permission, OS-dependentRejected
Badge with unread countPrecise informationOver-engineered for this use case; count not meaningfulRejected

Solution Architecture​

Data Flow​

chat-store.ts Sidebar.tsx
┌───────────────────────┐ subscribe ┌──────────────────────────┐
│ streamingConversations │ ──────────────> │ isConversationStreaming() │
│ Map<id, state> │ │ → isLive │
├───────────────────────┤ ├──────────────────────────┤
│ inputRequiredConvs │ ──────────────> │ isConversationInputReq() │
│ Set<id> │ │ → isInputRequired │
├───────────────────────┤ ├──────────────────────────┤
│ unviewedConversations │ ──────────────> │ hasUnviewedMessages() │
│ Set<id> │ │ → isUnviewed │
└───────────┬───────────┘ └──────────┬───────────────┘
│ │
│ addA2AEvent(UserInputMetaData) │ Render priority:
│ ──► add to inputRequired │ 1. isLive → green antenna
│ │ 2. isInputRequired → amber ?
│ setConversationStreaming(id, state) │ 3. isUnviewed → blue dot
│ ──► clear inputRequired │ 4. default → MessageSquare
│ │
│ setConversationStreaming(id, null) │
│ ──► if not active → add to unviewed │
│ │
│ setActiveConversation(id) │
│ ──► remove from unviewed + inputReq │
└────────────────────────────────────────┘

Visual States​

ElementDefaultLive (streaming)Input needed (HITL)Unviewed (new response)
IconMessageSquare (gray)Radio (emerald, pulse)MessageCircleQuestion (amber, pulse)MessageSquare (blue)
DotNoneGreen ping dotAmber ping dotSolid blue dot
Backgroundbg-mutedbg-emerald-500/10bg-amber-500/10bg-blue-500/5
Bordertransparentborder-emerald-500/30border-amber-500/30border-blue-500/25
Date textformatDate(updatedAt)"Live" (emerald, bold)"Input needed" (amber, bold)"New response" (blue, bold)

State Lifecycle​

  1. User sends a message → conversation starts streaming → Live indicator appears
  2. Agent requests user input (HITL) → Input needed indicator appears (amber)
  3. User submits input → streaming resumes → back to Live, input-required flag cleared
  4. Streaming completes while user is on a different conversation → Unviewed indicator appears
  5. User clicks the conversation → unviewed and input-required flags are cleared → Default appearance

If streaming completes while the user is already viewing that conversation, no unviewed indicator is shown (they saw the response arrive in real time).

Refresh Guard​

Two-layer warning system for users who try to refresh or close while chats are streaming:

  1. In-app banner (LiveStreamBanner): A thin emerald bar appears at the top of the app whenever any conversation is actively streaming, showing "N live chat(s) receiving response(s) — refreshing will interrupt". Proactive and always visible — users see it before they hit Cmd-R.

  2. Native browser dialog (beforeunload): If the user does try to refresh/close, the browser's confirmation dialog appears. A descriptive returnValue message is set (e.g. "You have 1 live chat receiving a response. Refreshing will interrupt it."), though modern browsers replace it with generic text. Data is still saved regardless of the user's choice.

Collapsed Sidebar​

The icon container is rendered outside the !collapsed guard, so both the green antenna (live) and blue dot (unviewed) are visible in icon-only mode.

Chat Tab Notification Badges​

The "Chat" tab in the AppHeader nav pills shows a count badge to surface live/input/unviewed status across all pages (Skills, Knowledge Bases, Admin):

  • Green pulsing badge (with ping animation): Count of conversations actively streaming
  • Amber pulsing badge: Count of conversations waiting for user input (HITL)
  • Blue solid badge: Count of unviewed responses
  • No badge: Idle — nothing requires attention

Priority: green > amber > blue (only the highest-priority badge is shown).

Components Changed​

  • ui/src/components/layout/AppHeader.tsx

    • Added streamingConversations, inputRequiredConversations, and unviewedConversations from the chat store
    • Chat tab link now has relative positioning and conditionally renders green (streaming), amber (input-required), or blue (unviewed) count badges
  • ui/src/store/chat-store.ts

    • Added unviewedConversations: Set<string> and inputRequiredConversations: Set<string> to store state
    • Added markConversationUnviewed, clearConversationUnviewed, hasUnviewedMessages actions
    • Added markConversationInputRequired, clearConversationInputRequired, isConversationInputRequired actions
    • Updated addA2AEvent to mark conversation as input-required when UserInputMetaData artifact arrives
    • Updated setConversationStreaming to clear input-required when streaming starts (user submitted input) and mark unviewed when streaming ends on non-active conversation
    • Updated setActiveConversation to clear both unviewed and input-required flags on navigation
    • Updated beforeunload handler to trigger native browser confirmation when streaming is active
  • ui/src/components/layout/Sidebar.tsx

    • Added Radio and MessageCircleQuestion imports from lucide-react
    • Added isConversationStreaming, isConversationInputRequired, and hasUnviewedMessages from the chat store
    • Added isLive, isInputRequired, and isUnviewed checks per conversation item
    • Four-state conditional rendering for icon, background, border, and date text
    • Amber ping dot and "Input needed" text for input-required conversations
    • Blue dot badge and "New response" text for unviewed conversations
  • ui/src/components/layout/LiveStreamBanner.tsx (new)

    • Thin emerald banner at top of app when streaming conversations exist
    • Shows count and descriptive message ("N live chat(s) — refreshing will interrupt")
    • Auto-hides when no streams are active; accessible with role="status" and aria-live="polite"
  • ui/src/app/(app)/layout.tsx

    • Added LiveStreamBanner between AppHeader and page content