# Streaming and Prompt Queue

Anteroom streams AI responses in real-time via Server-Sent Events (SSE) and supports prompt queuing so you never have to wait.

## Token-by-Token Streaming

Responses arrive token-by-token and render live:

- Markdown and math render as tokens arrive
- **Raw mode toggle** (eye icon in top bar) --- view unprocessed text during streaming, persists across sessions
- Stop generation mid-response with `Escape` or the stop button
- Context-aware phase indicator: connecting, thinking, writing, running tools
- Error messages show inline with a **Retry** button

## Prompt Queue

Type and submit new messages while the AI is responding. They queue (up to 10) and process in FIFO order when the current response finishes.

When a stream is active for a conversation, new messages return `{"status": "queued"}` instead of opening a new SSE stream. The queued messages process automatically in order.

!!! tip
    This means you can rapid-fire multiple follow-up prompts without waiting for each response to complete.

### Queue Position Feedback

When a message is queued, the UI shows a pill-shaped badge next to the user message with `queued (N)` where N is the position in the queue.

- The badge appears inline next to the "YOU" role label
- When the queued message begins processing, the badge highlights briefly and then removes itself
- If the queue is full (10 messages), a toast notification reads "Queue full --- wait for current work to finish"

The `queued_message` SSE event includes enriched metadata:

| Field | Type | Description |
|---|---|---|
| `content` | string | The queued message text |
| `position` | integer | 1-based position of this message in the current processing batch |
| `queue_depth` | integer | Number of messages remaining in the queue after this one |

## Parallel Tool Execution

When the AI calls multiple tools in one response, they run concurrently via `asyncio.as_completed` instead of sequentially. For example, if the AI calls `read_file` on three different files, all three reads happen simultaneously.

Tool calls render as expandable detail panels:

- Input parameters shown during execution
- Output and status shown when complete
- Spinner animation while tools execute

## Sub-Agent Loading Indicator

When the AI calls `run_agent` to spawn a sub-agent, the tool call panel renders with a distinctive loading state instead of the standard collapsed detail element:

- **Expanded by default** with a pulsing left-border accent animation
- **Loading prompt** shows the first 200 characters of the sub-agent's task
- **Spinner** indicates the sub-agent is actively running

As sub-agent events arrive, the loading prompt is replaced by per-agent progress cards:

| SSE Event | `kind` field | Payload fields |
|---|---|---|
| `subagent_event` | `subagent_start` | `agent_id`, `model`, `prompt` |
| `subagent_event` | `tool_call_start` | `agent_id`, `tool_name` |
| `subagent_event` | `subagent_end` | `agent_id`, `elapsed_seconds`, `tool_calls`, `error` |

On completion, the panel summary updates to "Sub-agent complete" (success) or "Sub-agent failed" (error), and the pulsing animation stops. Non-sub-agent tool calls are unaffected by these styles.

## Thinking Indicator

Between tool execution and the next API call, Anteroom emits a `"thinking"` event that triggers a pulsing dots animation in the UI. Phase-aware labels provide context about what the AI is doing:

- **connecting** --- waiting for API connection
- **thinking** --- waiting for the first token
- **writing** --- tokens are streaming, with character count
- **running tools** --- during tool execution, showing tool count and summaries for single tools
- **retry N/M** --- retrying after a transient error

An elapsed timer appears after 1.5 seconds. Stall detection highlights when streaming pauses unexpectedly.

## Canvas Streaming

When the AI calls `create_canvas`, `update_canvas`, or `patch_canvas`, a panel appears alongside the chat and updates in real-time via the following SSE events:

| Event | When emitted | Payload fields |
|---|---|---|
| `canvas_stream_start` | AI begins generating canvas content | `canvas_id`, `title`, `content_type` |
| `canvas_streaming` | Each streamed content chunk | `canvas_id`, `delta` |
| `canvas_created` | `create_canvas` completes | `canvas_id`, `title`, `content` |
| `canvas_updated` | `update_canvas` completes | `canvas_id`, `content` |
| `canvas_patched` | `patch_canvas` completes | `canvas_id`, `edits_applied` |

Canvas content is streamed token-by-token using `tool_call_args_delta` events from `ai_service.py`, giving the same live-render experience as normal text responses.

## Safety Approval Events

When a destructive tool call is intercepted by the safety gate, the stream emits an `approval_required` event before pausing:

| Event | Payload fields |
|---|---|
| `approval_required` | `approval_id`, `tool_name`, `command` (or `path`) |
| `approval_resolved` | `approval_id`, `approved`, `reason` |

The browser renders an inline Approve / Deny prompt inside the tool call panel. The user's response is submitted to `POST /api/approvals/{approval_id}/respond`. See [Tool Safety](../security/tool-safety.md#web-ui-approval-flow) for the full flow.

**Deduplication**: The browser tracks shown approval IDs in a `_shownApprovalIds` set. Duplicate `approval_required` events for the same `approval_id` (e.g., on SSE reconnect) are ignored. On SSE reconnect, all stale approval prompts are cleared from the DOM and the set is reset.

**Resolution display**: When `approval_resolved` fires, the approval card updates to show "Allowed" (green) or "Denied" (red) status. Timed-out approvals show "Timed out".

## Detached Subagents

When the AI launches a subagent with `detach: true`, the agent runs in the background and the foreground turn ends immediately. The browser receives lifecycle events via SSE:

| Event | Payload fields |
|---|---|
| `agent_run_started` | `run_id`, `prompt` (truncated to 80 chars) |
| `agent_run_completed` | `run_id`, `status`, `summary`, `duration_seconds`, `tool_calls_count` |

**Rendering**: A detached agent chip appears in the conversation timeline — a spinner while running, a checkmark (completed) or X (failed) when done. Unlike inline sub-agents, detached agents do not show a blocking spinner or progress cards in the foreground stream.

**Lifecycle controls**: The cancel and retry buttons on the chip call `DELETE /api/agent-runs/{run_id}` and `POST /api/agent-runs/{run_id}/retry` respectively. Retry creates a new run with `parent_run_id` linkage to the original — the full prompt is preserved for re-execution.

**Status inspection**: `GET /api/agent-runs/{run_id}` returns a `TaskResult`-based payload for completed runs: `status` (`completed`, `failed`, or `cancelled`), `summary` (first 200 chars of output), `error` (human-readable message on failure), `tool_calls` (ordered list of tool names invoked, capped at 20), `truncated` (whether summary was cut), and `duration_seconds`. The response also includes `run_id`, `title`, `started_at`, `completed_at`, `duration_ms`, and `parent_run_id`. Runs still in progress fall back to a legacy dict with `output` (first 2000 chars) and `tool_calls_made`. The `agent_run_status` READ-tier tool also exposes this to the AI for autonomous follow-up.

## Completion Notifications

Both background tasks (`task_completed`) and detached subagents (`agent_run_completed`) emit enriched completion payloads:

- `task_completed` includes `duration_seconds` (elapsed wall-clock time from start to completion, rounded to 2 decimal places).
- `agent_run_completed` includes `duration_seconds`, `tool_calls_count`, and `summary` (first 200 chars of agent output).

**Deduplication**: The browser tracks delivered notification IDs in a `_deliveredNotificationIds` set. Duplicate events for the same `task_id` or `run_id` (e.g., on SSE reconnect) are silently dropped. The set is cleared on each SSE reconnect (`onopen`).

**Off-screen toast**: When a completion arrives and the chip is not visible in the viewport (the chat area is scrolled up more than 80px from the bottom), `Chat.showCompletionToast(message)` renders a fixed-position dismissible banner at the top of the chat area. The banner auto-dismisses after 6 seconds and has `role="alert"` for screen reader accessibility.
