/ai/stream request opens a persistent HTTP connection; the backend pushes events as they happen rather than waiting for a complete response.
Event format
Each event is a JSON object on a single line, prefixed withdata: and followed by a double newline:
AiService parses each line, strips the data: prefix, and decodes the JSON.
Event types
phase
Indicates which stage the pipeline is in. Flutter updates the loading indicator text.
| Phase | UI label |
|---|---|
thinking | ”Thinking…” (animated dots) |
tool_call | ”Analyzing…” (replaced by tool event label) |
generating | ”Generating…” |
tool
Fires when a tool call starts. Flutter shows a dismissible badge in the chat bubble.
_toolLabels map in chat_provider.dart converts tool names to readable labels:
token
Incremental text from Claude’s streaming output. Flutter appends each token to the current message bubble in real time.
done
Final event. Contains the complete assembled text plus any widgets and follow-up suggestions.
error
Replaces done if the pipeline fails. Flutter shows an error bubble with fallback suggestions.
Widget response schema
When the answer is better expressed as a chart or card, Claude returns a structured widget object in thedone event’s widgets array. Flutter’s widget_renderer.dart renders each widget natively.
Chart widget
Summary card
Alert card
Multi-widget
HTTP headers
The/ai/stream endpoint sets three headers critical for SSE to work through nginx:
X-Accel-Buffering: no is essential — without it, nginx buffers the entire response before forwarding, which defeats the purpose of streaming.
Flutter SSE parsing
chat_provider.dart listens to this stream and updates state for each event type, triggering selective widget rebuilds via Riverpod.
Non-streaming endpoint
POST /ai/ask runs the same pipeline but collects all SSE events internally and returns a single AgentResponse. Use this for programmatic calls where you don’t need real-time streaming: