Skip to content

Frontend: useAgentChat hook — WebSocket connection and message streaming #504

@frankbria

Description

@frankbria

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

  1. On sessionId change: open WebSocket to /ws/sessions/{sessionId}/chat?token=<JWT>
  2. Send { type: "ping" } every 30s to keep alive
  3. On text_delta events: append content to the current in-progress assistant message
  4. On tool_use_start: push a new tool_use message
  5. On tool_result: push a tool_result message
  6. On thinking: push a thinking message
  7. On cost_update: update costUsd, inputTokens, outputTokens in state
  8. On done: finalize the in-progress assistant message, set status to idle
  9. On error: set error field, set status to error
  10. On WS close: set connected = false, status to disconnected, auto-reconnect with exponential backoff (max 5 attempts)
  11. sendMessage(content): send { type: "message", content } over WS, push optimistic user message, set status to thinking
  12. interrupt(): send { type: "interrupt" } over WS
  13. 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

  • Hook connects to WebSocket on mount, disconnects on unmount
  • Streaming text deltas update the in-progress message character-by-character
  • Tool use and tool result messages appear inline between assistant turns
  • Thinking messages render separately from text content
  • sendMessage pushes optimistic user message immediately, then sends over WS
  • interrupt() sends interrupt message
  • Reconnects automatically on disconnect (up to 5 attempts, exponential backoff)
  • status transitions correctly: idlethinkingstreamingidle
  • No memory leaks: cleanup on unmount

Out of Scope

  • The visual chat panel (tracked in the next sub-issue)
  • Terminal (separate sub-issue)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions