The Desktop SurfaceOne run card with two moments, a transcript that pairs itself, and the palette that makes the log symmetric.

The desktop app is where the Harness becomes visible. Its surfaces are not skins over the API — several of them exist to express specific invariants, and they are worth reading as design arguments.

The pages

frontend/apps/desktop/src/pages/agents/ holds the list, the detail page (tabs: sessions, triggers, memory, tools, prompt, settings), and the session page. The assistant sidebar (components/assistant-panel.tsx) renders the same transcript from the same rows — the row model's own comment puts it best: "the sidebar is a session view like any other, just narrower."

One run card, two moments

run-card.tsx has a single body rendered in two places:

    SessionRunCard is the pinned card above the composer while a run is live — the only work surface, carrying the merged checklist, live activity, and cancel controls.

    RunRecordCard is the same card frozen into the chat log at the moment the run completed, as a run-record transcript row interleaved by finishedAt against event timestamps.

The pinned card suppresses itself when the log already holds that run's record (frozenRunIds), so the two moments never double up. The record card only mounts while its bubble is expanded — which, as its comment notes, "keeps its socket and queries off until someone actually looks."

Inside: the Activity drawer renders the script journal as narrated lines, a source drawer shows the exact module a workflow run executed, and RunErrorChip turns a failure into an inspector — code, message, the failing tool call rendered as the chat's own tool row, the offending workflow.js:LINE excerpted from the stored source, structured detail, the stack, and a door into the child session.

The transcript row model

models/agent-session-rows.ts turns durable events into rows: message, error, raw, and run-record. It pairs each tool_call with its tool_result by toolCallId into a single row, so a call and its result are one thing in the UI even though they are two events on the Log.

isOptimisticUserEcho decides when a durable event replaces the optimistic row you typed. It matches on clientMessageId first and falls back to rawMarkdown — deliberately not content, because the server re-serializes message blocks through its own markdown writer and the texts disagree on multi-paragraph spacing. Matching on content left the pending row beside its own echo, and the message appeared twice.

A delegate row finds its child while the child is still working by looking for the run whose parentToolCallId equals that call's id — no result event needs to exist yet. That is why a delegation is a door during the work, not only after it.

The wrench palette: the symmetric half

The composer's palette (user-tool-palette.tsx) is where the user holds the same verbs the agent does. It offers read, write, and call, generates input forms from each tool's JSON Schema contract, and falls back to a raw JSON textarea when a schema has a shape the form can't render. Invocations go through InvokeSessionTool and land on the log as actor: 'user' events with a "You" chip — which the agent then reads as ground truth (Sessions).

It is a button beside Send rather than a / command, because the composer is a ProseMirror editor that owns its keystrokes — an argument recorded in the M3 review rather than left implicit.

Parked-run affordances

When a run parks, ParkedRunActions reads the wait and offers exactly what fits it: Resume for a budget pause, Answer for an event wait — labeled with the signal the park advertises — and Answer with data, which is a toggle revealing a JSON payload field rather than a separate button. All three send SignalRun.

The Tools tab

Lists the agent's tool documents via ListAgentTools. For an authored lambda it opens the full document: description, source, input and output schemas, and the content address — with the note that the CID "changes every time the agent rewrites the tool." This is the owner's window into what an agent has written for itself.

The local server

The desktop runs the agents service as a subprocess, with a rule stated in its own source: dev attaches, packaged spawns. Resolution order is SEED_NO_AGENTS_SPAWN (never spawn) → SEED_AGENTS_SERVER_URL (attach, loudly if unhealthy) → a healthy server already on the default port (almost always the ./dev up pane) → spawn the bundled binary. A spawned server is pointed at the app's own daemon for both hypermedia and IPFS.

Related

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime