Skip to Content
ContributeFrontendArchitect Chat UI

Architect Chat UI

This page documents the frontend architecture for the Architect chat experience in apps/frontend/src/app/(protected)/architect/.

The implementation pairs:

  • REST session management (ArchitectClient)
  • WebSocket event streaming (useArchitectChat)
  • chat UI components (ArchitectChat, ArchitectMessageBubble, StreamingIndicator)

Entry points

LayerFileResponsibility
Routeapp/(protected)/architect/page.tsxMounts the Architect client page
API clientutils/api-client/architect-client.tsCRUD for architect sessions and messages
Hookhooks/useArchitectChat.tsWebSocket subscriptions, message state, streaming state
Main UIcomponents/ArchitectChat.tsxLayout, action buttons, loading flow, plan panel
Inputcomponents/ArchitectChatInput.tsxtext input, file attachments, send behavior

Session and message flow

The UI uses ArchitectClient REST calls for durable session state:

  • GET /architect/sessions
  • POST /architect/sessions
  • GET /architect/sessions/\{id\}
  • GET /architect/sessions/\{id\}/messages

useArchitectChat then subscribes to architect:\{session_id\} over WebSocket and merges streamed updates into local state.

WebSocket events consumed by the UI

The frontend event enum in utils/websocket/types.ts includes Architect-specific events:

  • architect.message
  • architect.response
  • architect.thinking
  • architect.tool_start
  • architect.tool_end
  • architect.plan_update
  • architect.mode_change
  • architect.stream_start
  • architect.text_chunk
  • architect.stream_end
  • architect.task_progress
  • architect.error

Payloads tracked in UI state

useArchitectChat stores:

  • chat messages (ArchitectChatMessage[])
  • streaming state (isThinking, activeTools, completedTools)
  • current mode (discovery, planning, creating, executing)
  • plan markdown snapshot
  • isAwaitingTask flag
  • autoApproveAll toggle state

Charts arrive as charts on architect.response and are stored on the message; history reloads read them from the message’s metadata_.charts. ArchitectMessageBubble renders each one below the markdown with ArchitectChart, which maps the spec’s chart_type to an @mui/x-charts component (or a color-scaled table for heat_table) and formats axes from each measure’s kind. The spec type and the row-to-series helpers live in utils/architect/charts.ts.

Colors come from theme.chartPalettes.architect, defined per mode: neutral and neutralLine, pale teal for context, and series, four teal shades that step evenly in lightness from the accent, so they read apart without color vision. They stay lighter or darker and less saturated than the solid brand teal, which in the chat marks clickable things. The pale neutral sits below the 3:1 non-text contrast WCAG asks of chart graphics. That is deliberate: the numbers the bars show are also available as text, through the value labels on ranked bar charts and the Show data table on every chart. seriesColors and categoryColors apply the spec’s focus (the values the reply is about, in the accent) or the palette in order. The Export menu (utils/architect/chart-export.ts) writes the rows as CSV, or serializes the rendered SVG with its computed styles inlined plus a drawn title and legend, then rasterizes it to PNG; MUI X’s own export is Pro-only.

architect.task_progress payloads are routed into the same StreamingState shape used for tool calls. The hook ignores progress for other sessions, requires both task_id and label, and only attaches updates when a waiting bubble has been registered from an architect.response payload with awaiting_task: true.

Confirmation and auto-approve behavior

The backend can return needs_confirmation in architect.response. When true, the UI attaches Accept and Change actions to the latest assistant message.

  • Accept sends a confirmation message ("Yes, go ahead."); the agent decides what that approves (see Write guard)
  • Change focuses the input for user edits

The UI also includes an Auto-approve switch. When enabled:

  • outgoing architect.message payloads include auto_approve: true
  • confirmation action buttons are suppressed in the frontend as a safety check

The frontend checks autoApproveAll before rendering confirmation buttons, even if needs_confirmation is present in a response payload.

Streaming indicators

StreamingIndicator and ToolCallList present in-flight agent activity:

  • thinking dots during architect.thinking
  • active tool rows during architect.tool_start
  • completed tool rows with success/failure and duration on architect.tool_end; a failed row shows the payload’s error on hover
  • awaited background task rows during architect.task_progress
  • optional per-tool reasoning sections

ToolCallList prioritizes active tools at the top and collapses completed tools when activity is ongoing. Before rendering it drops failures that a later call of the same tool succeeded (unrecoveredTools). The collapsed summary counts the rows it hides that succeeded and that failed apart (“2 completed, 1 failed” is three calls; with no successes it reads “1 failed”) and shows an error icon instead of the check when any failed.

Task progress uses the Celery task ID as the row key. started and progress statuses render as active rows; completed and failed statuses move the row into completed activity. If step and total are present, the UI appends them to the label as progress counts.

File attachment handling

ArchitectChatInput supports multi-file attachments and sends each file as:

  • filename
  • content_type
  • base64 data
  • size

Input constraints currently include:

  • accepted file extensions configured in ArchitectChatInput.tsx
  • per-file size limit of 5 MB in the client

Minimal event wiring example

architect-event-subscription.ts
subscribe(EventType.ARCHITECT_TOOL_START, (msg) => {
const payload = msg.payload as ArchitectToolPayload
setStreamingState(prev => ({
    ...prev,
    activeTools: [
      ...prev.activeTools,
      {
        tool: payload.tool,
        description: payload.description,
        reasoning: payload.reasoning,
        startedAt: Date.now(),
      },
    ],
}))
})

subscribe(EventType.ARCHITECT_TOOL_END, (msg) => {
const payload = msg.payload as ArchitectToolPayload
setStreamingState(prev => ({
    ...prev,
    activeTools: prev.activeTools.filter(t => t.tool !== payload.tool),
    completedTools: [
      ...prev.completedTools,
      {
        tool: payload.tool,
        success: payload.success ?? true,
        durationMs: payload.duration_ms,
        startedAt: Date.now(),
      },
    ],
}))
})