Skip to main content
Every reply streams live from your local agent — text, reasoning, tool cards, and usage all arrive as typed events over 127.0.0.1.

Quick Start

1

Send your first message

Type in the composer and press Enter. The reply streams in as the agent produces it.
2

Watch the events render

Text, a live reasoning panel, tool cards, and a usage line all appear in the same turn as they stream.
3

Act on the turn

Hover a turn to Copy, Regenerate, Fork, or Delete it.

How It Works

The Desktop app opens POST /chat and streams these 11 event types straight into the turn — you write no client code for it.
The engine sends Server-Sent Events on POST /chat. Each event maps to one visible piece of the turn: Every event carries a msg_id that scopes it to a specific assistant message; start additionally carries a run_id that scopes it to a specific run, so a POST /approve/{aid} or POST /cancel sent later addresses the right run and not whichever one happens to be pending. The vocabulary is exactly the 11 events below — a StreamProtocolVocabulary test in the engine fails CI if a new emitter or a new documented event ever drifts from that list, so this table stays a faithful mirror of the wire. Every event the stream emits (from engine/server.py), with the exact payload a client sees on the SSE wire: Every event carries msg_id so the client can address a specific message rather than assuming the last one is live. Clients integrating with the SSE stream should handle all 11 events — a client written against a subset silently ignores the rest, and approval_request in particular will block the run invisibly until the 300 s approval timeout if the client does not render it.
Silence is treated as a failure, and the two shapes are reported differently:
  • empty — nothing at all happened (no text, no tools). Still reported as “No output”.
  • no_answer — the tools ran but the model produced no follow-up answer. Reported as “The tools ran, but no answer came back” and lists how many tool calls succeeded above.
Tool cards remain rendered even when the answer never arrives — the work isn’t hidden by a failure banner.

Capabilities

When the model emits reasoning, a live “Thinking… Xs” panel shows it and collapses to “Thought for Xs” when the turn ends. Toggle it with Show reasoning and Collapse reasoning by default in Settings.
A tool_call opens a card with a running dot; tool_drafting previews the arguments; tool_result fills the result pane. Long output is tail-truncated with Show all and Copy output. Tool cards remain visible even if the model produces no follow-up answer — a no_answer banner then explains what didn’t come back.
Drop a file and the composer branches on its MIME type — images and PDFs reach a vision model as bytes, everything else folds into the prompt as text.Drag-drop or use the + button. Chips show each file’s size. Limits: 2 MB max, up to 5 files.
  • Images (image/*) and PDFs are read as base64 and passed to the agent as attachments=[...], so a vision-capable model (gpt-4o, claude-3-*, gemini-*) sees the image itself — not a text description of it. Point the Desktop app at a text-only model and the image is ignored silently.
  • Text files are clipped to 100 kB of text and folded into the prompt, so long pastes condense into an attachment instead of filling the context.
Only text is stored in the turn’s history; the image bytes are ephemeral to the turn.
Point Desktop at a vision-capable model (gpt-4o, claude-3-7-sonnet-*, gemini-2.0-flash, …) if you want it to actually see dropped images. A text-only model still runs, but the image never reaches it.
Stop cancels a live run — the client is told explicitly with a cancelled event rather than inferring it from silence. Cancelling with Stop no longer double-reports as “No output” — a cancelled event is the only banner the user sees. Per-turn hover actions cover Copy, Regenerate, Fork (POST /fork/{cid}/{idx}), and Delete message (DELETE /messages/{cid}/{idx}).

Message Versions

Regenerating an answer keeps the previous one as a prior version on the same message, so you can compare without copying anything out.
  • Regenerate sends POST /chat with regenerate_of set to the message index, so the new answer is filed as another version instead of a duplicate question.
  • A < 1/2 > picker appears on hover once a message has more than one version, opening at the newest.
  • Stepping through the picker calls POST /version/{cid}/{idx}/{n} and repaints the chosen answer.
  • In the server payload every assistant row exposes versions, active, version_count, and version_active.
A message written before versioning has no versions key and is its own single version — nothing to pick between, so no picker shows.

Message Queue

Typing while a turn is running queues your next message instead of interrupting it.
  • The queued message sends automatically when the current turn finishes.
  • Stop clears the queue rather than sending it — cancelling means “not this either”.

In-App Confirmation Dialogs

Destructive actions — delete conversation, delete-all, delete message — route through an in-app modal, not the browser’s window.confirm.
window.confirm returns false immediately in WKWebView, which silently cancelled every destructive action. The in-app dialog is why Delete conversation now actually works. The confirm_delete setting gates whether the dialog appears at all.

Keyboard Shortcuts


Approvals & Safety

How tool calls are approved before they run

Conversations & Search

Fork, delete, projects, and ⌘K search