Client interface architecture
Zuno's TUI, headless CLI, HTTP server, ACP adapter, and future GUI clients are views over the same durable agent runtime. No client owns a private turn loop.
Shared model
Every client uses four runtime surfaces:
- Commands admit user intent through the same command registry and real handlers.
- Session events are the durable record of prompts, model output, tools, retries, questions, approvals, subagent reports, and lifecycle changes.
- Projections derive current conversation and status state from those events.
- The durable inbox accepts prompts, live steering, and
reportDelivery: nextStepreports before work is scheduled.
The shared projection vocabulary includes:
ActivityProjectionfor one model step's commands, reads, searches, images, delegations, and other tool activity;WorkStateProjectionfor the active goal, todos, durable jobs, memory candidates, and resident entries;SessionUsagefor cumulative provider-accounted tokens and context-window state;BackgroundExecutionProjectionfor process-owned terminal state and bounded output.
The server exposes cursor-based replay followed by live delivery. A reconnect sends the last committed cursor, receives every later event in order, then joins the bounded live stream. A client that misses live events must replay; it must not infer the missing state.
The only HTTP event operations are GET /api/event for live process-wide notifications and GET /api/session/{sessionID}/event for durable session replay plus live delivery. The session operation emits sessionID:sequence as the SSE id and accepts that value through Last-Event-ID on reconnect. Zuno does not mount an unscoped /event adapter or a second event envelope.
Routes and OpenAPI operations exist only when a real handler exists. Optional provider, credential, or client capabilities register their operations with their implementation rather than exposing a permanent placeholder that can only fail.
Client capabilities
A client handshake should advertise only presentation and transport capabilities:
- supported event and projection versions;
- inline image and attachment rendering;
- terminal, diff, location, and generic tool renderers;
- interactive question, approval, and permission controls;
- maximum accepted snapshot and event batch sizes.
Capability negotiation must not alter agent semantics. An unsupported renderer falls back to a generic event view; it does not hide the durable event or change the tool call.
Input admission
Every submitted input receives an admission identifier before execution. A client may optimistically render a pending row keyed by that identifier, then replace it when the committed event arrives. Reconnecting with the same identifier must not create a duplicate input.
Input also names its session target. A root composer uses the mounted root target; an attached child composer sends the durable child session id and never tunnels the message through the parent transcript. The runtime validates that target, writes the message to that child's inbox, and supervises delivery independently of the render loop. A completed child acquires an idle run lease and reopens its TurnHost; a running child receives the same admitted input as a soft steer. The wake coordinator closes the active-to-idle race, so a message that misses the running turn remains pending and becomes the next child turn instead of being lost.
During an active turn, ordinary text and rich content target the nearest safe step as steering. Commands and explicit next-turn work remain queued. A steer interrupts a provider wait or provider-retry delay, checkpoints partial assistant output without ending the turn, and starts the next model step with the promoted input. It does not cancel a side-effecting tool already in flight. A steer that misses the final safe point stays durably pending and becomes the next FIFO turn; client channel capacity or reconnect timing never decides its fate.
Human input has priority over an automatic goal retry. The client may show the persisted retry deadline and reason, but cancellation, pause, and resume are explicit commands rather than local timer changes.
Backpressure and disconnects
- Durable writes complete before a success response is returned.
- Bounded live channels may drop a subscriber, never a committed event.
- Slow clients resume from their last cursor.
- Large projections use a snapshot envelope plus later events.
- Background reports settle durably before they wake a parent.
- A client disconnect never cancels an active goal unless it issued an explicit interrupt.
ACP child-session projection
ACP always exposes delegation through the stable task tool-call projection. That card is sufficient for clients which know nothing about child sessions and is retained even when a richer projection is active.
The draft native-subagent extension is opt-in presentation, not a different execution path. After direct clientCapabilities.subagents negotiation, a foreground child's durable session becomes an additional ACP update route: spawn and terminal state are sent to its direct parent, while replay and live turn events are sent to the child id. The adapter drains the bounded child projection queue before returning the parent prompt response. If high-frequency updates must be omitted under backpressure, it emits a typed omission notice; spawn and terminal transitions are protected.
Historical child trees are reconstructed from durable parent edges and replayed parent-before-child. Their terminal state is disconnected, because durable history proves identity and content but not process liveness. Background children remain jobs: projecting them as foreground sessions would erase their independent cancellation, report-delivery, and uncertain-outcome semantics.
Human requests follow the same routing contract. A negotiated native child uses its own session id. A compatibility client receives the ask on the known root session with _meta.zuno.childSessionId, so it never has to accept an unknown route. Reusable permission grants are still owned by the root ACP session and are cleared when it closes.
TUI
The TUI favors dense, keyboard-first operation:
- stable transcript and status-strip dimensions;
- a composer that uses the available left pane with only a one-column gutter;
- multiline question input with bounded growth that replaces the composer area while the tool waits;
Esccancels it as a refusal rather than returning a synthetic answer; - visible permission, retry, diagnostics, and background-job states. TUI presentation settings live in
tui.json; authorization remains a runtime concern in the mainzuno.jsonpermissionblock.mode: allow_allskips every Zuno tool-approval ask, while explicit denies and catastrophic Shell denials remain authoritative. Root and child asks share one foreground queue, but every request retains its trusted session, assistant-message, and tool-call origin. Closing the attached TUI refuses outstanding asks, and anAlwaysgrant is scoped to the originating session; - explicit
working,awaiting approval, andawaiting answerstates. During a running turn the firstEscarms interruption and shows the confirmation immediately above the composer; the second within the confirmation window cancels it. If a modal or autocomplete surface owns the first press, that same press closes the surface and arms the turn, so two physical presses remain sufficient.Stopping the active turn…remains visible until the terminal turn event clears it;TurnInterruptedthen appends one session-owned interruption marker, and replay reconstructs that marker from a persisted assistant abort rather than nesting it inside partial assistant output; - liveness animation advances from a bounded UI clock, not from provider event arrival. A slow provider, shell command, or MCP call therefore keeps moving while producing no output; the clock stops while waiting for human input and at the completed or interrupted turn boundary;
- generic rendering for unknown future events;
- a
systemtheme that reads non-invasive terminal color hints when available and otherwise preserves the terminal's foreground and background defaults. Its unordered-list marker uses muted text without bold emphasis, while ordered enumerations retain their own emphasized token. Ordinary content, reasoning, service names, and sidebar metadata use neutral foreground hierarchy; semantic green is reserved for compact success or health glyphs rather than complete sentences or rows; - a full-height ambient sidebar outside the transcript, prompt, status, and info column, so no left-hand band renders underneath it;
- a visible transcript scrollbar with wheel and thumb dragging, plus application-owned text selection that is clipped to the transcript. Releasing a drag copies automatically, right-click copies the retained selection, and success or failure is reported without clearing the highlight. Root and attached-child composers use the same captured drag selection and clipboard path, and render an inverse theme-derived caret on both empty and populated input buffers; pasting one supported local image path, or image bytes supplied by the clipboard backend, inserts a visible
[Image #N]draft handle while retaining filename, MIME, and bytes as separate typed content.@project/pathresolves bounded text or images below the project root, and the same rich submission shape survives queue, steer, child continuation, durable replay, and provider encoding. See images and file references;mouse: falseopts back into terminal-native selection and alternate-scroll translation; - ordinary modal overlays are centred in both axes; composer-owned questions remain anchored to the composer region. Leader continuation help is a compact, centred, titled, bordered overlay: it preserves readable cell widths and reports
+N moreinstead of filling the frame with clipped descriptions. Numeric quick-session bindings remain active but are omitted from this help surface so nine repeatedSwitch to session in quick slotrows do not obscure higher-value commands. The default leader timeout is five seconds, so the continuation overlay does not vanish before it can be read. An open modal captures pointer input so clicks cannot activate covered transcript or sidebar content. Picker rows and confirmation buttons accept left-click selection as well as keyboard actions. Slash autocomplete, structured questions, permissions, sessions, models, agents, themes, MCP servers, subagents, background jobs, and memory entries are mouse-selectable; list and reference panels accept the wheel as well as keyboard navigation; - clicking a user prompt opens its message actions.
Copy messagewrites the complete prompt through the shared clipboard path and reports the result.Revert this turnis offered only when the live newest prompt has a restorable boundary; choosing it opens the same explicit Restore/Keep confirmation as/undo, and only Restore admits the typed undo command; - step-level activity summaries for completed routine commands, reads, searches, images, and delegations. Running work, approvals, failures, and important results remain visible. A folded group retains one bounded identifier line per call: shell command text, read path and requested window, search query, or the tool-specific summary. Full arguments and results remain behind disclosure rather than flooding the main answer.
Ctrl+Topens the complete scrollable durable transcript and preserves manual scroll position;Alt+Tchanges reasoning effort. Each thinking block remains folded by default, uses muted styling in both states, and owns an independent disclosure target. Its header is explicitly labelled◇ Thought; tool rows are explicitly labelledTool, so the two secondary content types do not share an ambiguous visual shape. Clicking a thought header opens the complete persisted body without changing sibling blocks./thinkingremains the keyboard-wide fallback. If replay contains both the visible reasoning event and an identical provider-reasoning capsule, the client projects one block while retaining both durable records; - user and assistant prose use the same CommonMark renderer, including GFM tables, lists, headings, code, quotes, and links. Rendering never changes the durable source text. The transcript adds hierarchy without adding saturated panels: a speaker title is followed by a weak neutral rule, Markdown headings use the neutral title role, and list/quote/rule chrome uses the muted role while prose remains primary. Structure therefore remains visible without relying on purple, green, or colour alone;
- per-call tool disclosure in the complete transcript, with subagent rendering selected by persisted
ToolUiIntent::Subagentrather than hard-coded tool names. The collapsed row stays summary-only; expanding a call renders a pretty-printedArgumentssection and a distinctResultsection, with explicit bounded-overflow notices for pathological inputs. A call refused before execution is projected asblockedwith warning styling and a durable block kind; only a call that actually ran and failed is projected as an error. Tool headers are composed from separate theme roles: disclosure and separators are muted,Tooland the tool identity are neutral titles, the argument summary is secondary, and warning/error/running emphasis is confined to the status glyph. Overflow hints are muted rather than rendered as primary actions. These span-level distinctions retain the existing per-call disclosure target and remain legible in monochrome terminals; - one subagent overview for native child sessions and configured Codex or Claude Code product agents. It shows product/target, objective, status, elapsed time, session/run, job, report delivery, result, and safety diagnostics without exposing product-internal reasoning or tool streams. Enter toggles details; each compact row separates focus, status glyph, product title, target, and muted objective, with the selection background applied across the complete row. Expanded details use a weak
Detailsdivider, muted field labels, readable values, and per-child status glyphs instead of tinting whole rows; pressingxtwice requests cancellation of a running job and keeps the list mounted for consecutive cancellations. Native child hosts additionally publish a full main-pane session projection:Ctrl+X Downenters the first direct child,Ctrl+X Upreturns to its parent, andCtrl+X Left/Rightremain available for sibling navigation. While the attached child's composer is empty, plain Left/Right cycle siblings directly; once a draft exists, those same keys return to ordinary cursor movement rather than stealing text editing. Mouse-wheel events scroll the attached child's own transcript instead of being dropped at the child composer. The fixed two-row child footer shows the resolved Agent/model, current context occupancy, direct-child position such as3/8, sibling keys, the parent key, and whether Enter will steer or continue. The parent host remains mounted and running while the child transcript receives live events, so child progress is visible before the foregroundtaskcall completes. Every attached child owns its ownInputEditordraft. Enter steers a running child and continues a completed child; the text is admitted to the child inbox before execution. Durable child rows and retained history are projected again when a TUI resumes the parent after a process restart. The child continuation identity stores its resolved Agent, model, effort, parent Attempt, and workflow lineage in session metadata, so a restored child is not view-only. Child input reopens the same fullTurnHostused by a root session, including tools, permissions, cancellation, lifecycle reporting, usage, and automatic context compaction. Child input is literal text, so/helpand other slash-looking strings are not dispatched as root or host commands. Switching siblings or returning to the parent preserves each child draft. A child may delegate only when its selected Agent exposestask, names the target indelegates, passes permission, and remains belowsubagent_depth; using the same host does not bypass those limits. Historical navigation follows durable direct-child session edges rather than inferring a child from ordinary tool activity. When a retained session has no delegated child,Ctrl+X Downleaves the transcript in place and directs the user to the transcript details instead of implying that history restoration failed; - a skill census that separates discovery from use: the heading reports
loaded/discovered, and only a successfully completedskilltool call marks a row✓ skill-name · loaded. Expanded skills are grouped with loaded skills first and not-loaded skills second, with each group sorted by name and source. Same-named skills display their source locator, and completion marks only the exact source selected by the tool call; - an independently scrollable and selectable sidebar whose current-session title is a fixed header and participates in the same application-owned drag-selection and clipboard path as the body; its location/version footer stays fixed. Only the projection body scrolls. Foreground transcript-backed delegations appear immediately under
Agents; once a call acquires a matching durable job it yields to the richerJobsprojection rather than appearing twice. The sidebar also projects goal, todos, pending memory, token usage, LSP, MCP, and skills from shared state rather than polling; /psfor process-owned background terminals and/memoryfor auditable candidate review. Both keep their list mounted after an action so several entries can be handled consecutively;- the welcome screen owns only a prepared process identity. It creates no durable session until the first model-bound submission commits the session and user message together.
session.materializedupdates the in-place session catalog, so/sessionsees the new row without remounting; - usage comes from the durable
SessionUsageprojection on resume. A history whose provider accounting cannot be recovered displays an unavailable marker rather than a fabricated zero. Cumulative input/output/cache buckets remain available for accounting and history, but the live sidebar shows only the latest whole prompt, model-window limit, and decimal occupancy percentage; - no empty LSP status or setup prompt; the sidebar adds LSP only for configured services or real diagnostics;
/sessionlists active root sessions from the current durable database and exact working directory. Selecting another session is admitted only between turns and remounts the complete session composition, including transcript replay, permissions, cancellation ownership, LSP/MCP workers, and snapshot history. The physical terminal activation remains mounted throughout, so a switch never leaves and re-enters the alternate screen;/newperforms the same in-process remount to a fresh prepared identity. The command opens a blank conversation shell directly rather than returning to the launch welcome page. It creates no row; the first model-bound prompt materializes exactly one new session through the normal durable-input transaction;/compactinvokes the runtime compaction agent and persists the resulting summary;/goalinvokes the shared durable goal handler. Both are typed native session commands rather than model prompts.compaction.threshold_percentcontrols proactive compaction against the usable model window, whilecompaction.auto: falsedisables that proactive trigger without removing manual compaction or bounded context-limit recovery;/plan,/start-plan, and/start-workare native collaboration-mode controls. ACP publishes standard mode/config updates after a transactional host replacement, while the TUI keeps its explicit confirmation surface. Neither path may return from Plan to Work without a durable plan;- the same session list owns row actions:
Ctrl+Ropens a pre-filled rename prompt, whileCtrl+Dmust be pressed twice on the same row before deletion. Both actions are revalidated by the host and use the transactional session store; deleting the current session remounts the most recent remaining session in that directory, or creates a new one when none remains. A successful delete reopens the refreshed session list on that replacement so users can delete several sessions without invoking/sessionagain; a refused delete leaves the existing list mounted. A current session with background subagents still running is refused rather than deleting state those tasks can still write; - warning and error notices wrap inside the viewport and remain visible long enough to inspect or select. Ephemeral command guidance, such as an unknown slash command, is a short-lived toast and does not become durable transcript content;
- no blocking network, LSP, or provider work in the render loop.
Future GUI
A GUI should reuse the HTTP event replay, projections, command registry, question and approval protocols, and attachment references. GUI-only packages may own layout, theming, caching, and richer tool renderers. They must not import provider implementations or mutate session tables directly.
The first GUI milestone therefore needs no engine fork:
- versioned handshake and session snapshot;
- cursor replay plus SSE or WebSocket live events;
- idempotent input admission;
- command, question, approval, interrupt, goal pause, and goal resume endpoints;
- generic tool rendering with optional specialized renderers.