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:
| Alternative | Pros | Cons | Decision |
|---|---|---|---|
| Green antenna (live) + amber question (input) + blue dot (unviewed) | Clear three-phase lifecycle, always visible, leverages existing A2A input-required state | Adds new store state for both unviewed and input-required tracking | Selected |
| Single green dot for all states | Simpler | No distinction between states | Rejected |
| Browser notifications for completed responses | Works even when tab is backgrounded | Intrusive, requires permission, OS-dependent | Rejected |
| Badge with unread count | Precise information | Over-engineered for this use case; count not meaningful | Rejected |
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
| Element | Default | Live (streaming) | Input needed (HITL) | Unviewed (new response) |
|---|---|---|---|---|
| Icon | MessageSquare (gray) | Radio (emerald, pulse) | MessageCircleQuestion (amber, pulse) | MessageSquare (blue) |
| Dot | None | Green ping dot | Amber ping dot | Solid blue dot |
| Background | bg-muted | bg-emerald-500/10 | bg-amber-500/10 | bg-blue-500/5 |
| Border | transparent | border-emerald-500/30 | border-amber-500/30 | border-blue-500/25 |
| Date text | formatDate(updatedAt) | "Live" (emerald, bold) | "Input needed" (amber, bold) | "New response" (blue, bold) |
State Lifecycle
- User sends a message → conversation starts streaming → Live indicator appears
- Agent requests user input (HITL) → Input needed indicator appears (amber)
- User submits input → streaming resumes → back to Live, input-required flag cleared
- Streaming completes while user is on a different conversation → Unviewed indicator appears
- 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:
-
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. -
Native browser dialog (
beforeunload): If the user does try to refresh/close, the browser's confirmation dialog appears. A descriptivereturnValuemessage 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, andunviewedConversationsfrom the chat store - Chat tab link now has
relativepositioning and conditionally renders green (streaming), amber (input-required), or blue (unviewed) count badges
- Added
-
ui/src/store/chat-store.ts- Added
unviewedConversations: Set<string>andinputRequiredConversations: Set<string>to store state - Added
markConversationUnviewed,clearConversationUnviewed,hasUnviewedMessagesactions - Added
markConversationInputRequired,clearConversationInputRequired,isConversationInputRequiredactions - Updated
addA2AEventto mark conversation as input-required whenUserInputMetaDataartifact arrives - Updated
setConversationStreamingto clear input-required when streaming starts (user submitted input) and mark unviewed when streaming ends on non-active conversation - Updated
setActiveConversationto clear both unviewed and input-required flags on navigation - Updated
beforeunloadhandler to trigger native browser confirmation when streaming is active
- Added
-
ui/src/components/layout/Sidebar.tsx- Added
RadioandMessageCircleQuestionimports from lucide-react - Added
isConversationStreaming,isConversationInputRequired, andhasUnviewedMessagesfrom the chat store - Added
isLive,isInputRequired, andisUnviewedchecks 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
- Added
-
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"andaria-live="polite"
-
ui/src/app/(app)/layout.tsx- Added
LiveStreamBannerbetweenAppHeaderand page content
- Added
Related
- Spec: spec.md