Client Manifest
The the chat client / web client reads the deployed app's YAML
(through the daemon's app-detail surface) and uses the
ui: block plus a handful of app: and runtime:
fields to tailor its UI: which greeting to show, which
panels to hide, which accent colour to paint, which
/slash palette to render.
This page documents what the client actually consumes - the
daemon never reads ui: itself, it just passes the values
through. Every field on this page maps to a real Pydantic field;
entries are cited with file + line.
What the client reads
| Source | Used for |
|---|---|
app.app_id, app.name, app.icon, app.color, app.category, app.tags, app.description | App card in the catalog. |
app.quick_prompts | One-click prompt suggestions on the empty conversation screen. |
runtime.mode | Conversation vs one_shot vs background; client switches input UX (chat box vs single submit form). |
runtime.workdir_mode | When none, the client hides the workspace path picker. |
ui.theme | Accent + background colour overrides. |
ui.features | 12 boolean toggles for individual UI panels / behaviours. |
ui.greeting | Empty-state greeting under the input box. |
ui.slash_commands | The /-palette entries. |
ui.quick_prompts | Same shape as app.quick_prompts; client merges both lists. |
ui.workspace | Renderer hint + layout (render_mode, entry_file, title, position, width_pct, auto_open_on_first_tool, default_open, default_view, hidden_views, preview_chrome). |
ui.templates | One-click bootstrap gallery shown in the empty state. |
ui.widgets | Declarative widget tree rendered in chat, sidebar, modals. |
ui.layout | High-level chat preset (default, code, builder, research, minimal, lovable). |
ui.density | Bubble spacing (compact / comfortable). |
ui.thinking | Thinking-block visibility and initial collapsed state. |
ui.tool_calls | Tool-chip collapse default and silent-tools visibility. |
ui.composer | Composer toolbar (file upload, voice, slash, quick prompts). Wins over the matching ui.features.X keys. |
ui.visual | Bubble accent / style / user alignment. |
The first three groups are covered in
App Configuration → app and
App Configuration → runtime;
this page focuses on the ui: block.
ui.features - 12 toggles
AppMeta.features and UIBlock.features
() are mirror dicts that share the same key set:
| Key | Default (when unspecified) | Effect when false |
|---|---|---|
voice | true | Hides the voice-input button (microphone). |
attachments | true | Hides the file/image attachment paperclip. |
tools_panel | true | Hides the right-side panel showing tool calls in real time. |
snippets | true | Hides the @-mention snippet picker. |
tasks_panel | true | Hides the todos / tasks side panel (driven by memory.task_create). |
memory_panel | true | Hides the memory snapshot panel (goal + facts). |
context_ring | true | Hides the token-pressure ring around the input. |
markdown | true | Renders assistant messages as plain text (no markdown parsing). |
slash_commands | true | Hides the /-palette popup. |
message_actions | true | Hides the per-message Edit / Retry / Copy hover actions. |
status_pills | true | Hides the inline running / done status pills next to assistant messages. |
token_badges | true | Hides the per-message token counts. |
Source of truth: the docstring lists the exact keys the chat client recognises today. Unknown keys are ignored silently (the spec is forward-compatible).
ui:
features:
voice: false
attachments: false
tasks_panel: false
memory_panel: false
context_ring: false
token_badges: false
# tools_panel, snippets, markdown, slash_commands, message_actions,
# status_pills default to true → kept visible
Mirror.
app.featuresis a deprecated nesting that the compiler still accepts - it lifts toui.featuresvia the alias pass. Setui.featuresdirectly in v2 YAML; the compiler emits a warning when you use the nested form.
ui.theme - accent + background
UIBlock.theme. Two recognised keys:
ui:
theme:
accent: "#6EE7B7" # hex; overrides app.color for fine control
background: "#0F172A" # hex; client may apply this to the chat surface
Other keys are passed through but unused by the current the chat client /
web clients. Treat theme as a forward-compat dict - only accent
and background are guaranteed.
app.color is the catalog accent (visible on
the app card). ui.theme.accent overrides it inside the app once
the user is in the conversation. Set them independently if you
want different colours in the catalog vs in the chat.
ui.greeting - empty-state message
UIBlock.greeting. The text shown above the
input field when a conversation has no messages yet.
ui:
greeting: |
Hello! I'm your code-review assistant.
Drop a file, paste a diff, or describe what you want reviewed.
Plain text by default; markdown when ui.features.markdown: true
(the default). Templated values ({{app.name}}, {{sys.date}},
...) are resolved at compile time, not at render time.
ui.slash_commands - / palette
UIBlock.slash_commands. List of SlashCommand
(, extra: allow).
ui:
slash_commands:
- command: /commit
description: "Commit the current diff with a conventional message"
template: "Run /commit using {{branch ?? 'the current branch'}}"
- command: /review
description: "Review the active file"
template: "Review {{file}} for security issues"
| Field | Type | Required | Description |
|---|---|---|---|
command | string (min 1) | yes | The /foo id. |
description | string | no | One-line description in the palette. |
template | string | no | Message template the client sends to the agent when the user picks this command. Supports {{var}} placeholders the client fills from a popup form. |
Distinct from dev.skills (server-side reusable workflow
markdown the agent loads via use_skill). Slash commands are
pure UI sugar - the agent never knows the slash palette existed,
it just sees the rendered template as a normal user message.
See Skills System for the difference + the skills that DO live server-side.
ui.quick_prompts - empty-state buttons
UIBlock.quick_prompts - list of QuickPrompt
(, extra: allow).
ui:
quick_prompts:
- label: "New PR"
message: "Open a PR with the latest changes"
icon: "rocket"
- label: "Daily standup"
message: "Summarize what I did yesterday"
icon: "clipboard"
| Field | Type | Required | Description |
|---|---|---|---|
label | string (min 1) | yes | Short button label. |
message | string (min 1) | yes | Full prompt sent when the user clicks. |
icon | string | no (default "") | Emoji or icon name. |
Mirror: app.quick_prompts holds the same shape.
The client merges both lists, deduping by label. Either is
fine; pick one place per app for clarity.
ui.workspace - renderer hint + layout
UIBlock.workspace is WorkspaceBlock (,
extra: forbid). Tells the client this app uses the in-memory
virtual filesystem and how to position the viewer relative to the
chat.
Renderer fields (documented in Workspace & Preview):
render_mode: str(default"auto") -react,html,markdown,slides,code,latex,builder, orauto. Auto detects from the first file the agent writes.entry_file: str | null- default file the renderer opens.title: str | null- workspace toolbar label.
Layout fields (added 2026-05-04, drive how the chat ↔ workspace split looks):
position: str(default"right") -right,bottom,hidden, oroverlay.hiddenkeeps the workspace off-screen even when files are written;overlayfloats it over the chat.width_pct: int(default50, range10..90) - pane width as a percentage of the chat-vs-workspace split. Ignored whenpositionishidden/overlay.auto_open_on_first_tool: bool(defaulttrue) - whentrue(default), the client opens the workspace pane the first time the agent writes a file. Setfalsefor chat-only apps that should not surface a renderer just because a tool wrote one log.default_open: bool(defaultfalse, added 2026-05-14) - whentrue, the workspace pane opens IMMEDIATELY on session mount, before any agent action. Right for Lovable-style apps where the workspace IS the product surface (templates gallery, live preview iframe).
View routing fields (added 2026-05-14, control which workspace tab the user sees and which ones are reachable at all):
default_view: str(default"auto") -code,preview,changes,activity, orauto.autopickspreviewwhenrender_modeis anything other thancode, elsecode.hidden_views: list[str](default[]) - subset of["code", "preview", "changes", "activity"]to remove from the workspace mode menu. Right for hiding Monaco on apps where the user should never see the editor, or hiding Changes on auto-approve sandboxes. If the current view becomes hidden, the panel bounces to the first non-hidden one (preview → code → changes → activity).
Preview chrome (added 2026-05-14, per-feature flags for the toolbar above the live preview iframe):
preview_chrome.enabled: bool(defaulttrue) - master switch.falsehides the entire chrome (bare iframe).preview_chrome.refresh: bool(defaulttrue) - refresh button.preview_chrome.open_in_new_tab: bool(defaulttrue) - external-link button. Auto-suppressed for daemon-bundled URLs.preview_chrome.viewport_toggle: bool(defaultfalse) - Mobile (375px) / Tablet (768px) / Desktop preset toggle.preview_chrome.url_bar: str(default"auto") -auto,always, ornever.autoreveals the URL pill once the iframe app has reporteddigi:route-changefor at least two distinct routes (signals an SPA with routing).
ui:
workspace:
render_mode: react
entry_file: src/App.tsx
title: My App
position: right
width_pct: 65
auto_open_on_first_tool: true
default_open: true
default_view: preview
hidden_views: []
preview_chrome:
enabled: true
refresh: true
open_in_new_tab: true
viewport_toggle: true
url_bar: auto
ui.widgets - declarative widget tree
UIBlock.widgets is WidgetsConfig
(). Four sub-trees:
| Field | Type | Description |
|---|---|---|
version | int | Spec version. Daemon refuses unknown versions; only 1 today. |
chat_side | ChatSideWidget | null | Right-side panel rendered alongside the chat. |
workspace_tabs | list[WorkspaceTabWidget] | Tabs in the workspace panel. |
modals | dict[name, ModalWidget] | Named modals the agent can open via widget.open action. |
inline | dict[name, InlineWidget] | Inline widgets the agent renders inside chat via widget.render with a ref:. |
Full surface - 43 widget primitives, 15 client-side action-types
(distinct from the 7 server-side widget module actions),
server-side template substitution, live widget:* Socket.IO
events - is in Widgets. External widget files under
./widgets/*.yaml in the bundle dir are auto-loaded into
inline by the compiler (keyed by file stem, same pattern as
skills).
ui.layout - high-level chat preset (2026-05-04)
UIBlock.layout is a str with default "default". Allowed
values:
default- historical conversational chat.code- code-editor-friendly chat (Cursor-style).builder- YAML-editor + smoke-test focus.research- long-form, citations and agent-group prominent.minimal- chat only, no workspace, terse chrome.lovable- workspace-dominant split with auto-open on first tool call.
The preset is a sugar layer: when the YAML omits a fine-grained
sub-block (thinking, tool_calls, composer, visual,
workspace), the client uses the preset's defaults. Any
sub-block the YAML DOES define ALWAYS wins over the preset, so
deriving from lovable and tweaking just workspace.width_pct
is supported.
ui.density - bubble spacing (2026-05-04)
UIBlock.density: str, default "comfortable". Allowed:
compact, comfortable. Applies to message bubbles and the
gap between consecutive messages.
ui.thinking - thinking-block defaults (2026-05-04)
UIBlock.thinking is ChatThinkingBlock (,
extra: forbid). Two flags:
visible: bool(defaulttrue) - whenfalse, thinking blocks are hidden entirely. The agent can still emit them, the client just drops them at render time.collapsed_default: bool(defaulttrue) - initial collapsed state whenvisibleistrue. The user can still toggle.
ui:
thinking:
visible: false # production conversational app
ui.tool_calls - tool-chip defaults (2026-05-04)
UIBlock.tool_calls is ChatToolCallsBlock (,
extra: forbid):
collapsed_default: bool(defaulttrue) - initial collapsed state of every tool-call chip. The user can expand individual chips with the chevron.show_silent: bool(defaultfalse) - whentrue, plumbing tools (memory.remember,agent_spawninternals, discovery meta-tools likesearch_tools/list_categories) are rendered. Defaultfalsekeeps them hidden so the chat reads as a clean conversation rather than an internals trace.inject_intent: bool(defaultfalse) - whentrue, the context builder prepends a requiredintentfield to every tool schema; the model fills it with a short '-ing' phrase and the frontend renders a single shimmering line in place of the tool chip. Trade-off: ~10-20 extra tokens per tool call.hide_details: bool(defaultfalse) - only meaningful withinject_intent: true. Whentrue, the intent line has no chevron and no drilldown - the user can never inspect raw tool plumbing. For consumer / demo surfaces.strict_mode: bool(defaultfalse) - Lovable-style full shimmer: extends the progressive line to thinking and intermediate text blocks too, not just tool calls. The final answer streams in clear; text before anask_useris also revealed so the user sees the question's context. Strict opt-in: off = zero per-turn overhead, sub-agents are also bypassed (theirAgentContextis built without the gate stash).intent_phrases: IntentPhrasesConfig- source for the shimmer phrases whenstrict_mode: true(ignored otherwise).source: auto | llm | static. Thellmpath goes through the gateway (single egress, never direct provider) and falls back tostaticon timeout / error whensource=auto. Full schema (gateway model, prompt template, static phase matrix, timeouts) in02-app-config.md#uitool_callsintent_phrases.
# Standard - clean chip view
ui:
tool_calls:
collapsed_default: true
show_silent: false
# Lovable-style full strict mode
ui:
tool_calls:
inject_intent: true
hide_details: true
strict_mode: true
intent_phrases:
source: auto
llm:
gateway_model: gpt-4o-mini
ui.composer - composer toolbar (2026-05-04)
UIBlock.composer is ChatComposerBlock (,
extra: forbid). Mirrors the legacy ui.features flags for the
same concepts; when both are present the typed composer.X wins.
file_upload: bool(defaulttrue) - paperclip / drag-drop attachment. Equivalent tofeatures.attachments.voice: bool(defaultfalse) - microphone button. Defaultfalsehere (opt-in for production privacy) vsfeatures.voicewhich historically defaulted totrue.slash_commands: bool(defaulttrue) -/-palette popup. Equivalent tofeatures.slash_commands.quick_prompts_visible: bool(defaulttrue) - suggested prompt chips above the composer when the conversation is empty.
ui:
composer:
file_upload: true
voice: false
slash_commands: true
quick_prompts_visible: true
ui.visual - bubble accent / style (2026-05-04)
UIBlock.visual is ChatVisualBlock (,
extra: forbid). Three knobs:
accent: str(hex, default"") - accent colour for the send button, cursor, and any per-app highlights. Fallback chain:visual.accent→theme.accent→app.color. Empty here means "use the next level".bubble_style: str(default"card") -card(rounded box with shadow),flat(filled background no shadow), orminimal(no background, just text + thin separator).user_bubble_alignment: str(default"right") -right(default chat-room layout) orleft(RTL or stacked layout variants).
ui:
visual:
accent: "#10b981"
bubble_style: flat
user_bubble_alignment: right
ui.activity - opt-in sub-agent observability pane (2026-05-06)
UIBlock.activity is ActivityPanelBlock (,
extra: forbid). Surfaces the live sub-agent fan-out, background
tasks, and recent terminal events as a dedicated workspace mode
(web Activity ▾ entry / the chat client Activity mode in the
workspace toolbar).
Opt-in contract. When the YAML omits this block, both clients
hide the Activity entry entirely - a simple chat app that never
spawns sub-agents stays clean. Apps that orchestrate fan-out
(coordinator, multi-agent research, dev assistants) opt in The pane is driven by the daemon-resource protocol: snapshot on
mount + Socket.IO live updates + heartbeat-driven reconcile, so it
survives daemon restarts, socket drops, and tab-focus cycles
without zombie state. The wire contract (snapshot +
heartbeat + turn_terminal consolidated event) is the
daemon-resource protocol used by every workspace pane.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Master switch. Set false to disable the pane while keeping config (staged rollouts). |
position | str | "right" | Where the pane attaches: right, bottom, overlay. |
title | str | null | null | Panel header label. Defaults to the localised "Activity" string. |
show_running | bool | true | Render the live sub-agent strip at the top. |
show_recent | bool | true | Render the recent-terminal-events scrollable list. |
show_stats | bool | true | Render the aggregate stats footer (success rate, avg duration). Pulls from digitorn_agent_* Prometheus counters. |
show_bg_tasks | bool | true | Interleave background shell tasks alongside sub-agents. |
max_recent | int | 50 | Cap on the number of terminal events kept (range 5..500). FIFO eviction past the cap. |
auto_open_on_spawn | bool | false | Auto-switch to the Activity pane on first sub-agent spawn. Off by default - surface only when the user opens it explicitly. |
ui:
activity:
enabled: true
title: "Activity"
position: right
show_running: true
show_recent: true
show_stats: true
show_bg_tasks: true
max_recent: 50
auto_open_on_spawn: false
What the pane shows (mirrored across all clients):
- Header strip - pulse dot + status label + live counters
(
3 running · 8 done · 1 failed). - Live agents (sticky top) - one row per running sub-agent
with name, current task, tool count, durée live (1 Hz local
tick - keeps moving even when the daemon throttles
agent_progress), and a Cancel button. Click →POST /sessions/{sid}/agents/{id}/cancel(cooperative cancel viacancel_eventthen hardTask.cancel). - Recent (scrollable) - collapsible rows for the last
max_recentterminal events with one-line preview / error. Click to expand the full body. - Stats footer - success ratio, average duration, totals.
- Stale overlay - soft pulsing badge ("Synchronisation…")
when
isStale(no heartbeat for 15 s, daemon restarted, etc.).
Late-event safety. The pane consumes the lastTerminalSeq
guard from the protocol primitive: any agent_event arriving with
seq <= lastTerminalSeq[turn_id] is dropped, so a turn that
already emitted turn_terminal can't have its agents re-armed by a
straggling event.
ui.tool_renderers - custom tool-call rendering (2026-05-06)
UIBlock.tool_renderers is ToolRenderersBlock (lives in its own
module so it can be deleted without touching
beyond one forward-string field - see the Rollback
note further down). Maps tool names (and regex patterns) to inline
widget refs the client mounts in place of the legacy tool chip.
Opt-in contract. When the YAML omits the block (or sets
enabled: false), every tool call uses the client's legacy chip -
zero behaviour change. The block being present + enabled: true is what flips the dispatcher to consult the maps.
Bindings exposed to the widget tree (template engine, same syntax as the v1 widgets host):
| Token | Type |
|---|---|
{{tool.name}} | str |
{{tool.params.X}} | any |
{{tool.result.X}} | any |
{{tool.result}} | json |
{{tool.error}} | str |
{{tool.duration_ms}} | int |
{{tool.status}} | str |
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Master toggle. False keeps every tool on the legacy chip even if the maps are populated. |
by_name | map | {} | Exact-match tool_name → { ref: widget_id }. Checked first; an exact hit short-circuits pattern lookup. |
by_pattern | map | {} | Regex map. Each key is a re.search pattern tested against the tool name in iteration order. First match wins. |
fallback_on_error | bool | true | When the renderer throws, fall back to the legacy chip. Set false during local renderer dev to surface failures inline. |
ui:
tool_renderers:
enabled: true
by_name:
WsRead: file_card # → ui.widgets.inline.file_card
WsWrite: file_card
by_pattern:
"Bash.*": terminal_card # any tool starting with "Bash"
"memory.+": memory_chip # memory.recall, memory.remember, …
fallback_on_error: true
widgets:
inline:
file_card:
tree:
type: card
children:
- type: row
children:
- { type: icon, name: "file-text" }
- { type: text, text: "{{tool.params.path}}", variant: title }
- { type: text, text: "{{tool.status}} · {{tool.duration_ms}}ms", variant: caption }
terminal_card:
tree:
type: card
children:
- { type: text, text: "$ {{tool.params.command}}", variant: title }
- { type: markdown, text: "```\n{{tool.result.stdout}}\n```" }
memory_chip:
tree:
type: badge
icon: "brain"
label: "{{tool.name}}"
Match priority (single tool call, single matching renderer):
by_name[tool.name]- exact match wins.by_pattern- first regex that matches (iteration order).- No match → legacy chip.
A regex that fails to compile ((unbalanced, etc.) is silently
skipped at the dispatch site so a typo doesn't crash the whole
chat. The compiler does not validate regex compilability - guard
client-side.
Rollback. The block lives in Removing the file
cleanly drops the field from UIBlock (the import is wrapped in
try / except ImportError) - every tool falls back to its legacy
chip. Client-side: web has a single <ToolRendererOrLegacy> adapter
in tool-call-block.tsx; the chat client has a single _renderToolEntry
method in chat_bubbles.dart. Replacing those
two adapters back to the original <ToolCallRow> / _buildToolContent
calls is the full client-side rollback.
Common gotchas.
- Long renderers compete with the message column. The widget
card renders inline in the chat timeline at message-column width
(
maxWidth: 720on web,800on the chat client desktop). Trees taller than ~160 px push the rest of the conversation down - keep them card-sized. {{tool.result}}is JSON-dumped raw. No prettification, no syntax highlighting. Usemarkdownnodes with explicit triple backticks if you want code formatting ("```json\n{{tool.result}}\n```").- Streaming tools fire with
status: runningfirst. The widget receives an emptyresultanderroruntil the daemon reports completion. Render a placeholder whentool.statusis"running"to avoid showingnullin the body.
Recipes
Lovable-clone
ui:
layout: lovable
density: compact
thinking: { visible: false }
tool_calls: { collapsed_default: true, show_silent: false }
composer: { file_upload: true, voice: false, quick_prompts_visible: false }
workspace:
render_mode: react
position: right
width_pct: 65
auto_open_on_first_tool: true
visual:
accent: "#10b981"
bubble_style: flat
Minimal conversational
ui:
layout: minimal
thinking: { visible: false }
tool_calls: { collapsed_default: true }
composer: { voice: false }
workspace: { position: hidden }
visual: { bubble_style: minimal }
Research / long-form
ui:
layout: research
density: comfortable
thinking: { visible: true, collapsed_default: false }
tool_calls: { collapsed_default: true, show_silent: true }
workspace: { position: bottom, width_pct: 40 }
Coordinator / multi-agent (with Activity pane)
ui:
layout: research
density: comfortable
thinking: { collapsed_default: true }
activity:
enabled: true
title: "Sub-agents"
auto_open_on_spawn: true
show_bg_tasks: true
max_recent: 100
workspace: { position: right, width_pct: 55 }
Surfaces the Activity pane the moment the coordinator spawns its first sub-agent. The pane stays in sync across daemon restarts / socket drops thanks to the resource protocol - no zombie pulsing dots, no stale "running" rows.
What the daemon doesn't read
The ui: block is purely passed through - the daemon's tool
dispatcher, security gates, and behavior engine all ignore it. No
canvas-side check uses ui.features.tools_panel to gate anything
server-side; the gating is the client's job.
That separation matters for trust: a malicious client can ignore
ui.features.tools_panel: false and show the panel anyway. The
real security boundary is tools.capabilities
(Security) - ui.features is purely cosmetic.
Cross-references
- App-config block reference for the
ui:block: App Configuration → ui - Workspace renderer + preview proxy: Workspace & Preview
- Declarative widget primitives + actions: Widgets
- Skills (server-side, distinct from
ui.slash_commands): Skills System - Bundle namespaces (where
{{prompt.X}}/{{include:}}come from): Bundle namespaces - Building a custom React UI inside the preview iframe:
Preview SDK -
<DigiPreview>provider,useWorkspaceFiles,useSessionMeta,useSessionLifecycle, hidden__sdk__/namespace, host ↔ iframe protocol.