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
ui:
features:
voice: true
attachments: true
tools_panel: true
tasks_panel: true
snippets: true
skills: true
slash_commands: true
| Key | Gates |
|---|---|
voice | Mic button in the composer |
attachments | File-upload button in the composer |
tools_panel | The /tools slash command (opens the Tools panel) |
tasks_panel | The /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.
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:
runtime:
preview_compile: "tectonic main.tex > compile.log 2>&1"
// 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.
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.
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
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
ui:
density: comfortable # compact | comfortable
Chat information density.
thinking
ui:
thinking:
visible: true
collapsed_default: true
Whether the model's thinking block renders at all, and whether it starts collapsed.
tool_calls
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
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.
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>}.
ui:
slots:
footer_left:
kind: inline
ref: my_pane
activity
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.
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.
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.
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.
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 default | App settings control |
|---|---|
ui.agent_rendering.enabled | Agent rendering |
ui.agent_rendering.collapse_after_turn | Agent rendering's collapse sub-option |
ui.tool_calls.strict_mode | Tool-call shimmer |
ui.visual.font_scale | Font size |
runtime.mid_turn_messages | Inject 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.