Skip to main content

Client Manifest (ui:)

Everything under ui: shapes what the chat client (digitorn_web, CLI, embed widget) renders for this app. Unknown keys are rejected at compile time, so a typo fails loud instead of silently.

Almost all of ui: is client-only. The daemon compiles it and serves it back through the manifest endpoint (GET /api/apps/{app_id}/manifest), but does not itself branch on most of these values. Two fields are read server-side too: ui.activity (copied into app meta) and ui.tool_calls.inject_intent (used to prepend an intent property to tool schemas). Everything else is display-layer only.

The one rule: nothing is on by default​

Every boolean surface under ui:, every entry in ui.features, and every block's own enabled field (ui.context_ring.enabled, ui.agent_rendering.enabled, ui.tool_renderers.enabled, ui.message_actions.enabled, ui.activity.enabled) defaults to false. A block or flag that isn't in the YAML stays invisible in the client. A chat surface shows exactly what an app author asked for. The code and changes workspace views follow the same rule through ui.workspace.shown_views (see workspace below).

Sub-options inside an already-enabled block can default to something other than false when that is the neutral, no-op value for that control. ui.context_ring.detail defaults true, because once the ring itself is on, letting it open a detail view is the unsurprising baseline. ui.visual.font_scale defaults 1.0, meaning no scaling. The rule is about whole capabilities staying hidden until asked for, not about every leaf value being false.

theme​

map[string]string. Colour token overrides. No fixed key list, whatever the active theme accepts.

features​

yaml
ui:
features:
voice: true
attachments: true
tools_panel: true
tasks_panel: true
snippets: true
skills: true
slash_commands: true
KeyGates
voiceMic button in the composer
attachmentsFile-upload button in the composer
tools_panelThe /tools slash command (opens the Tools panel)
tasks_panelThe /tasks slash command (opens the Background tasks panel)
snippets@-snippet picker in the composer
skills"Use skill" / /use_skill in the composer
slash_commands/-command palette in the composer

All default false.

workspace​

The side/bottom pane next to the chat that shows live code, a preview, or documents (Craft, Code, LaTeX-style apps). Omit for a plain chat app.

yaml
ui:
workspace:
render_mode: auto # react | html | markdown | slides | code | latex | builder | auto
entry_file: web/index.html
title: Preview
position: right # right | hidden
width_pct: 50 # 10-90, split vs. the chat
auto_open_on_first_tool: true
default_open: false # true opens it immediately on mount
default_view: auto # code | preview | changes | activity | documents | auto
shown_views: [code, changes, preview] # code, changes, preview, and project are hidden unless listed here
hidden_views: [changes] # views to hide from the mode menu, even a shown one
source_control: github # "" (hidden) or "github" - shows Open-a-repo / Sync
deploy_target: vercel # "" (hidden) or "vercel" - shows Publish, independent of source_control
diff_stats_bar: false # the "+ins -del" pending-changes pill above the composer
preview_chrome:
enabled: true
refresh: true
open_in_new_tab: true
viewport_toggle: false
url_bar: auto # auto | always | never

render_mode (react/html/markdown/…) chooses how the workspace panel renders. It is not a template preview (that is chosen by a preview_path file extension) and not usePreviewAttach().kind (static/devserver/web) — the word html means something different in each. See Which "preview" is which?.

code, changes, preview, and project are hidden by default like every other opt-in chat surface - list them in shown_views to turn them on. Once preview is shown, the daemon fills it automatically - no SDK code, no preview-module call. It watches the session workdir and, when it finds a built entry (dist/index.html, build/index.html, out/index.html, public/index.html, a root index.html, or one of those one directory deep), serves it in the pane; until then the tab shows a clean empty state. It also attaches an agent-started dev server it detects on a loopback port - except on a cloud install (apps.channel: server), where dev-server auto-attach is off, so ship a static build there. (The preview module - inspect/snapshot - only sees and drives this pane; it does not attach the build.) This is the zero-code live preview; a custom usePreviewAttach() view is only for a hand-built panel. project (Deploy / Database / SEO, for Craft-style web-project apps) would otherwise trigger from files the agent wrote for unrelated reasons (a package.json on a general coding app), so it needs the same explicit opt-in. activity and documents don't need this: activity needs its own ui.activity.enabled, and documents shows once the session has a real attachment. hidden_views still wins over shown_views for any view listed in both.

diff_stats_bar is hidden by default like the rest of this block - it shows a small "+ins -del" pill above the composer summarizing pending workspace changes, with a jump-to-Code affordance. It only ever renders once code is also reachable (shown_views).

One-click recompile is a runtime + SDK primitive, not a chrome flag. Declare the build command under runtime.preview_compile (author-defined, same trust as the agent's bash - the iframe only triggers it, never supplies it), and let the app add its own button through the SDK:

yaml
runtime:
preview_compile: "tectonic main.tex > compile.log 2>&1"
tsx
// inside the app's preview (an @digitornai/sdk view)
import { useCompile, useTopBarActions } from "@digitornai/sdk";
const { compile, compiling } = useCompile();
useTopBarActions([
{ id: "recompile", label: compiling ? "Compiling…" : "Recompile",
icon: "hammer", onClick: () => void compile() },
]);

useCompile() POSTs to the session's compile endpoint, which runs runtime.preview_compile in the workdir with the app's provisioned requirements on PATH (so tectonic resolves), without an agent turn (no LLM cost), 120s timeout. The app owns the button (any icon/label/placement via useTopBarActions); the daemon owns the command. Generic: a LaTeX viewer declares tectonic main.tex, a Vite app npm run build.

ui.workspace.custom_views adds app-defined tabs of your own to this same mode menu, backed by a static file the app ships under its own web/dist/ bundle. See Custom workspace views.

slash_commands​

Custom /-commands offered in the composer.

yaml
ui:
slash_commands:
- command: /review
description: Review the current diff
template: "Review this change for correctness and style."
action: {} # optional structured action payload

Needs ui.features.slash_commands: true to actually surface in the composer.

quick_prompts​

One-click suggestions on the empty conversation screen.

yaml
ui:
quick_prompts:
- label: Summarize
message: "Summarize the current context…"
icon: sparkles

app.quick_prompts (top-level, under app:) is the same concept, compiled to the same shape.

greeting​

string. Empty-state message shown above the composer before the first message.

composer​

yaml
ui:
composer:
file_upload: true
voice: true
slash_commands: true
quick_prompts_visible: true

Each of these narrows the matching ui.features.* flag. Both must allow an affordance for it to show: attachments need composer.file_upload: true and features.attachments: true.

density​

yaml
ui:
density: comfortable # compact | comfortable

Chat information density.

thinking​

yaml
ui:
thinking:
visible: true
collapsed_default: true

Whether the model's thinking block renders at all, and whether it starts collapsed.

tool_calls​

yaml
ui:
tool_calls:
show_silent: false # show plumbing tools (memory ops, agent_spawn internals, discovery meta-tools)
inject_intent: true # daemon prepends an intent property to every tool schema
hide_details: false # only relevant when inject_intent is true; hides params/results/diffs
strict_mode: false # hide every intermediate turn, show only the final answer
intent_phrases:
source: auto # llm | static | auto
static:
phases:
analyzing: ["Analyzing your request..."]
thinking: ["Thinking"]
tool_streaming: ["Working on it..."]
between_tools: ["Reviewing results..."]
finalizing: ["Wrapping up..."]

strict_mode has a per-user override. See "App-level defaults vs. per-user overrides" below.

visual​

yaml
ui:
visual:
accent: "#6EE7B7" # hex; empty falls back to theme.accent / app.color
font_scale: 1.0 # 0.85-1.4, multiplies chat text size for this app's chat

font_scale has a per-user override. The thinking block's own text size is fixed and never scales with this.

widgets​

Named panes the chat can render inside a conversation, referenced by ui.slots.*.ref and by ui.tool_renderers/ui.message_actions rules.

yaml
ui:
widgets:
version: 1
inline:
my_pane:
root: { type: text, value: "Hello" }
chat_side: {}
workspace_tabs: []
modals: {}

button/link/icon_button node kinds render as placeholders; use message_actions for a real clickable action.

slots​

Extension points for inline widgets: header, sidebar_left, sidebar_right, footer_left, footer_right. Each is {kind: inline, ref: <widgets.inline key>}.

yaml
ui:
slots:
footer_left:
kind: inline
ref: my_pane

activity​

yaml
ui:
activity:
enabled: true
title: Activity
show_running: true
show_recent: true
show_stats: true
show_bg_tasks: true
max_recent: 60 # 5-500
auto_open_on_spawn: true

tool_renderers​

Swaps a tool-call's default chip for a custom widget (ui.widgets.inline), by exact tool name or a regex pattern.

yaml
ui:
tool_renderers:
enabled: false
by_name:
web.search:
ref: my_pane
by_pattern:
"^filesystem\\.":
ref: my_pane
fallback_on_error: true

message_actions​

Renders a custom ui.widgets.inline pane under a message matching a rule.

yaml
ui:
message_actions:
enabled: false
rules:
- match:
role: assistant
tool_used: web.search
tool_pattern: ""
content_regex: ""
has_tool_calls: true
ref: my_pane
fallback_on_error: true

This is separate from the standard copy/feedback/share/regenerate icon row under a reply, which each person turns on or off for themselves from their own App settings and does not have a YAML field.

context_ring​

The composer's context-usage gauge.

yaml
ui:
context_ring:
enabled: true # shows the gauge at all
detail: true # click opens the detail panel; false leaves a passive gauge only
allow_model_change: false # a control inside the detail panel to switch the model

enabled: true, detail: false gives a percentage-only gauge with no click and no way into the detail panel; the /context slash command is suppressed too. allow_model_change: true turns the model name in the detail panel into a control that opens the model picker.

agent_rendering​

App-level default for the chat layout where the response stays pinned at the top of the turn while thinking, tool calls, and approvals stream in a live activity zone below it, instead of the classic top-to-bottom timeline.

yaml
ui:
agent_rendering:
enabled: false # classic top-to-bottom rendering unless on
collapse_after_turn: true # fold the activity zone into a summary once the turn ends

Has a per-user override for both fields.

App-level defaults vs. per-user overrides​

A few ui: fields are defaults a fresh session starts with, not hard settings. Each person can override them from the client's App settings → General → Chat dialog, stored per-app in their own browser and not written back to the YAML.

YAML defaultApp settings control
ui.agent_rendering.enabledAgent rendering
ui.agent_rendering.collapse_after_turnAgent rendering's collapse sub-option
ui.tool_calls.strict_modeTool-call shimmer
ui.visual.font_scaleFont size
runtime.mid_turn_messagesInject mid-turn messages

The reset control next to each of these resets to the app's own configured default, not a fixed global value.

Workspace preview​

Live app preview uses the preview / previewshot modules and HTTP /preview/serve/... plus ui.workspace chrome.