streamingMachine. The machine is the source of truth for the activity stream rendered in the chat, and it drives canvas updates via a downstream interceptor hook. This page covers the lifecycle, the spec-stream parser, and the React 19 throttling pattern.
States and transitions
The machine lives atsrc/machines/streaming/machine.ts. Initial state idle. Six states total: idle, sending, streaming, complete, cancelled, error.
Transitions and actions:
SUBMIT→ entersending. Actions:resetInlineWidgetStream,initSpecStreamParser.STREAM_START→ enterstreaming. (Phase 4 spec-stream parser already initialized.)CHUNK→ actionprocessSpecStream(text)— push text through the mixed-stream parser to extract JSONL patch lines, emit remaining text to the activity stream.THINKING→ push toactivityStreamas a thinking-block entry.TOOL_START/TOOL_COMPLETE→pushToolToStream/updateToolInStream(inactions/activity-stream.ts).USAGE_UPDATE→ bumps token counters; tracked per-API-turn.streamingstateexit→flushSpecStream,finalizeInlineWidgetStream(runs on ALL exit paths — complete, cancel, error).COMPLETE→persistInlineWidgetbeforefinalizeMessage. ✓ VERIFIED at machine.ts:498-509.
The activity stream
activityStream is an array on the machine’s context. Each entry is one of:
| Type | Contents | Created by |
|---|---|---|
text | Streaming text accumulated from CHUNK events | processSpecStream (text-only portion) |
thinking | Thinking-block content (extended reasoning) | THINKING handler |
tool | Tool invocation: {name, input, status, output, isError, parentToolUseId} | pushToolToStream + updateToolInStream |
subagent | Subagent run reference (with parentToolUseId) | subagentStart/subagentComplete |
useCanvasToolInterceptor) consumes this stream. By the time the interceptor sees an item, it’s already been normalized and pushed by XState actions — the interceptor does NOT receive raw IPC. ✓ VERIFIED at useCanvasToolInterceptor.ts:108.
Spec streaming — the mixedStreamParser
When an agent emits amcp__canvas__canvas(action: 'render_widget') tool call with a streaming spec, the spec doesn’t arrive as one blob — it streams in alongside the assistant text. Two mechanisms cooperate:
initSpecStreamParser
Action defined inactions/spec-stream.ts:31-50. Creates a createMixedStreamParser() closure with an onPatch callback. On every patch:
processSpecStream
Action invoked on everyCHUNK event. Pushes the chunk text through the parser. The parser:
- Detects JSONL patch lines mixed in (each on its own line, prefixed with
{"). - Strips them from the chat text before rendering.
- Applies them to the accumulating spec.
spec-stream.ts:88-130.
flushSpecStream + finalizeInlineWidgetStream
Run onexit of streaming state (every path). Ensures any in-flight spec gets finalized before the message is persisted.
persistInlineWidget
Only runs onCOMPLETE. Stores the final inline widget so it survives session reload.
React 19 throttling pattern
React 19’s automatic batching can collapse multiple state updates within a single render cycle. For a streaming machine emitting tens of updates per second, this would lose intermediate spec frames.useStreamingSession (src/hooks/useStreamingSession.ts) subscribes to the XState actor’s state changes and uses requestAnimationFrame to throttle:
useStreamingSession.ts:303-321. Result: at most one React render per animation frame, intermediate spec frames preserved as they arrive.
SidecarEvent → XState mapping
The listener inStreamingSession (the JS wrapper around the actor) maps Tauri IPC events to XState events:
| SidecarEvent | XState event | Action |
|---|---|---|
text | CHUNK | processSpecStream |
thinking | THINKING | pushThinkingToStream |
toolStart | TOOL_START | pushToolToStream (status=‘running’) |
toolStarting | TOOL_STARTING | (permission about-to-run precursor) |
toolComplete | TOOL_COMPLETE | updateToolInStream (status=‘complete’) |
subagentStart | SUBAGENT_START | pushSubagentToStream |
subagentComplete | SUBAGENT_COMPLETE | updateSubagentInStream |
usageUpdate | USAGE_UPDATE | bumpTokens |
permissionRequest | (handled outside XState — modal IPC) | — |
complete | COMPLETE | finalizeMessage |
error | ERROR | recordError |
cancelled | CANCEL | recordCancellation |
Inline widget streaming
A SEPARATE store atsrc/stores/inlineWidgetStreamStore.ts holds inline-widget state per message. Spec streaming pushes patches to both canvasStore.updateStreamingSpec (for the canvas) AND inlineWidgetStreamStore.pushSpec (for the in-chat inline widget). They’re independent — a single agent message may render both an inline preview in the chat AND auto-open the full widget in the canvas, depending on the element count threshold.
finalizeInlineWidgetStream (on streaming exit) commits the final spec to the inline widget for that message.
persistInlineWidget (on COMPLETE) writes to backend storage so the widget survives session reload — but the exact persistence path was flagged as an open question in the audit (see Trust → GAP-C / open questions).
Session lifecycle integration
The XState machine doesn’t own session lifecycle.useStreamingSession does:
- Singleton
SessionManagerinstance (getSessionManager()atuseStreamingSession.ts:50). getOrCreateStreamingSession(activeSessionId, conversationId)creates/retrieves aStreamingSession(which owns the XState actor).- TIGER-5 fix: persists
sdkSessionIdto the DB when it first appears in the machine context (lines 323-345). - Liveness watchdog: tracks
lastEventAtRefon every IPC event; setsshowInactivityWarningafter 5 min silence.
Key files
src/machines/streaming/machine.ts
src/machines/streaming/machine.ts
The XState v5 machine definition. State graph, transitions, actions wired in. Spec-stream init at L379. Persist hooks at L498-509.
src/machines/streaming/actions/spec-stream.ts
src/machines/streaming/actions/spec-stream.ts
initSpecStreamParser (L31-50), processSpecStream (L88-130), flushSpecStream. Mixed-stream parsing logic.src/machines/streaming/actions/activity-stream.ts
src/machines/streaming/actions/activity-stream.ts
pushToolToStream, updateToolInStream, pushSubagentToStream, pushThinkingToStream.src/machines/streaming/events.ts
src/machines/streaming/events.ts
The
StreamingEvent union type — XState’s input alphabet.src/hooks/useStreamingSession.ts
src/hooks/useStreamingSession.ts
Singleton session manager + actor subscription with rAF throttling. ~760 LOC. Throttle at L303-321.
src/hooks/useStreamingMachineWrapper.ts
src/hooks/useStreamingMachineWrapper.ts
Composed hook returning the public API (activityStream, send, cancel, retry).
src/stores/inlineWidgetStreamStore.ts
src/stores/inlineWidgetStreamStore.ts
Separate Zustand store for inline-widget streaming per message.
Open questions
- Spec stream backpressure — if patches arrive faster than React can render, do they queue or drop? rAF throttle suggests they coalesce but exact behavior under heavy load was not verified.
- Inline widget persistence target —
persistInlineWidgetwrites somewhere onCOMPLETE. Is it message-scoped DB? Vault file? Need to readpersist*action wiring.
Next
TipTap Editor
How the document-mode canvas surface handles AI-mediated content updates.