message-processor.mjs translates them into a unified camelCase SidecarEvent stream serialized by Rust serde to the frontend XState machine.
The flow
Canonical SidecarEvent variants
The full list, all camelCase always:text, thinking
text, thinking
Token-level streaming text.
thinking is reasoning content (Claude extended thinking, Pi thinking_delta).toolStart, toolStarting, toolProgress, toolComplete
toolStart, toolStarting, toolProgress, toolComplete
Tool lifecycle.
toolStarting is a precursor for some Claude SDK tools (permission about-to-run). toolComplete carries isError: boolean extracted from the user message’s tool_result block.complete, error, cancelled
complete, error, cancelled
Terminal states.
complete is normal completion. error carries failure details. cancelled is emitted when a turn is aborted mid-stream.usageUpdate, contextTokens
usageUpdate, contextTokens
Token accounting.
usageUpdate flows per-turn from both engines; contextTokens is initial context size at session start.pong
pong
Health-check response for sidecar liveness.
permissionRequest
permissionRequest
Emitted when
canUseTool needs user input. Frontend renders an approval prompt and replies via IPC.subagentStart, subagentComplete
subagentStart, subagentComplete
Subagent lifecycle.
parentToolUseId is set so the frontend can nest under the spawning tool call. See Subagents.userMessageTracked, rewindResult, projectContextChange
userMessageTracked, rewindResult, projectContextChange
Bookkeeping events.
userMessageTracked records that a user turn was committed. rewindResult is the response to a file-checkpoint rewind. projectContextChange fires when PARA context auto-switches.Mapping tables
Claude SDK → SidecarEvent
| SDK Message | SidecarEvent |
|---|---|
stream_event text delta | text |
assistant thinking block | thinking |
assistant tool_use block | toolStart (with toolStarting precursor) |
user tool_result | toolComplete (with isError) |
system init | usageUpdate (initial context tokens) |
result final | complete + usageUpdate |
permission_request IPC | permissionRequest |
Pi → SidecarEvent
| Pi Event | SidecarEvent |
|---|---|
text_delta | text |
thinking_delta | thinking |
| Pi tool lifecycle start | toolStart |
| Pi tool lifecycle complete | toolComplete |
usage (per-turn) | usageUpdate |
done | complete |
error | error |
| Pi subagent start | subagentStart |
| Pi subagent done | subagentComplete |
The Rust enum
The whole IPC contract is defined by the RustSidecarEvent enum at src-tauri/src/sidecar/events.rs. serde’s tag = "type", rename_all = "camelCase" is what makes the JSON wire format camelCase.
How the frontend consumes
The frontend’s XState v5streamingMachine (src/machines/streaming/machine.ts) dispatches events:
| SidecarEvent | XState event |
|---|---|
text | CHUNK |
thinking | THINKING |
toolStart | TOOL_START |
toolComplete | TOOL_COMPLETE |
complete | COMPLETE |
error | ERROR |
usageUpdate | USAGE_UPDATE |
pushToolToStream, updateToolInStream in actions/activity-stream.ts), not from raw IPC. By the time useCanvasToolInterceptor sees a tool — it’s already an activityStream[i] entry with status: 'complete'. See Frontend Surfaces for the downstream story.
Key files
src-tauri/sidecar/query/message-processor.mjs
src-tauri/sidecar/query/message-processor.mjs
Per-API-turn span construction. Both engines’ events flow through here. Session id extraction at L733. ✓ VERIFIED
src-tauri/src/sidecar/events.rs
src-tauri/src/sidecar/events.rs
Rust
SidecarEvent enum. The serde contract.src-tauri/src/sidecar/registry/routing.rs
src-tauri/src/sidecar/registry/routing.rs
Exhaustive-match arms routing each variant: broadcast (window-scoped) vs id-keyed (response-routed).
src/lib/ipc/conversation.ts
src/lib/ipc/conversation.ts
TypeScript
SidecarEvent union type. Must match Rust serde output exactly.src/machines/streaming/machine.ts
src/machines/streaming/machine.ts
Frontend XState v5 machine. SidecarEvent → XState event mapping happens in the Tauri event listeners on
StreamingSession.Next
Subagents
subagentStart/subagentComplete carry parentToolUseId. How does nesting actually work in each engine?Frontend Canvas
Once SidecarEvents reach XState, how do they drive the canvas?