Parent Issue
Part of #500 — depends on #501 and #502 (backend must be running)
Context
The web-ui currently has useEventSource (SSE, one-way) and useExecutionMonitor (aggregates SSE events into UI state). For interactive sessions, we need a bidirectional WebSocket hook that connects to /ws/sessions/{id}/chat, sends user messages, and accumulates streamed token events into structured chat messages.
Existing Code to Build On
web-ui/src/hooks/useEventSource.ts — reconnect pattern to adapt for WS
web-ui/src/hooks/useExecutionMonitor.ts — state accumulation pattern to follow
- Reference: Optio
apps/web/src/hooks/use-logs.ts + use-websocket.ts
What to Build
New file: web-ui/src/hooks/useAgentChat.ts
type MessageRole =
| "user"
| "assistant"
| "tool_use"
| "tool_result"
| "thinking"
| "system"
| "error";
interface ChatMessage {
id: string; // client-generated UUID per turn
role: MessageRole;
content: string;
toolName?: string; // for tool_use / tool_result
toolInput?: unknown; // for tool_use
createdAt: string;
}
interface AgentChatState {
messages: ChatMessage[];
status: "idle" | "connecting" | "thinking" | "streaming" | "error" | "disconnected";
costUsd: number;
inputTokens: number;
outputTokens: number;
error: string | null;
connected: boolean;
}
interface UseAgentChat {
state: AgentChatState;
sendMessage: (content: string) => void;
interrupt: () => void;
clearMessages: () => void;
}
export function useAgentChat(sessionId: string | null): UseAgentChat
Behavior
- On
sessionId change: open WebSocket to /ws/sessions/{sessionId}/chat?token=<JWT>
- Send
{ type: "ping" } every 30s to keep alive
- On
text_delta events: append content to the current in-progress assistant message
- On
tool_use_start: push a new tool_use message
- On
tool_result: push a tool_result message
- On
thinking: push a thinking message
- On
cost_update: update costUsd, inputTokens, outputTokens in state
- On
done: finalize the in-progress assistant message, set status to idle
- On
error: set error field, set status to error
- On WS close: set
connected = false, status to disconnected, auto-reconnect with exponential backoff (max 5 attempts)
sendMessage(content): send { type: "message", content } over WS, push optimistic user message, set status to thinking
interrupt(): send { type: "interrupt" } over WS
- Use
requestAnimationFrame batching for text_delta updates to avoid render thrash
Auth
Get the JWT from the existing auth context/local storage — look at how other hooks pass credentials.
Acceptance Criteria
Out of Scope
- The visual chat panel (tracked in the next sub-issue)
- Terminal (separate sub-issue)
Parent Issue
Part of #500 — depends on #501 and #502 (backend must be running)
Context
The web-ui currently has
useEventSource(SSE, one-way) anduseExecutionMonitor(aggregates SSE events into UI state). For interactive sessions, we need a bidirectional WebSocket hook that connects to/ws/sessions/{id}/chat, sends user messages, and accumulates streamed token events into structured chat messages.Existing Code to Build On
web-ui/src/hooks/useEventSource.ts— reconnect pattern to adapt for WSweb-ui/src/hooks/useExecutionMonitor.ts— state accumulation pattern to followapps/web/src/hooks/use-logs.ts+use-websocket.tsWhat to Build
New file:
web-ui/src/hooks/useAgentChat.tsBehavior
sessionIdchange: openWebSocketto/ws/sessions/{sessionId}/chat?token=<JWT>{ type: "ping" }every 30s to keep alivetext_deltaevents: append content to the current in-progressassistantmessagetool_use_start: push a newtool_usemessagetool_result: push atool_resultmessagethinking: push athinkingmessagecost_update: updatecostUsd,inputTokens,outputTokensin statedone: finalize the in-progress assistant message, set status toidleerror: seterrorfield, set status toerrorconnected = false, status todisconnected, auto-reconnect with exponential backoff (max 5 attempts)sendMessage(content): send{ type: "message", content }over WS, push optimistic user message, set status tothinkinginterrupt(): send{ type: "interrupt" }over WSrequestAnimationFramebatching fortext_deltaupdates to avoid render thrashAuth
Get the JWT from the existing auth context/local storage — look at how other hooks pass credentials.
Acceptance Criteria
sendMessagepushes optimistic user message immediately, then sends over WSinterrupt()sends interrupt messagestatustransitions correctly:idle→thinking→streaming→idleOut of Scope