What every widget in agentty's UI accepts (Config schema) and what agentty
fills in. Read this alongside RENDERING.md, which
walks the visual hierarchy and data flow.
The architectural rule:
agentty builds widget Configs from
Modelstate. maya widgets own all rendering. agentty constructs no Elements.
Concrete: every .cpp under src/runtime/view/ (except the legacy
overlay files in §24) contains zero Element{...}, zero dsl::v(...),
zero dsl::text(...). Each file is a function Model → SomeWidget::Config
and one widget = one adapter file: filenames mirror the widget
they adapt, and the directory layout mirrors the widget hierarchy
(see §25). The single view(m) call materializes everything via one
maya::AppLayout{...}.build().
// src/runtime/view/view.cpp
maya::Element view(const Model& m) {
return maya::AppLayout{{
.thread = thread_config(m),
.changes_strip = changes_strip_config(m),
.composer = composer_config(m),
.status_bar = status_bar_config(m),
.overlay = pick_overlay(m),
}}.build();
}That's the entire host-side view layer. The body is one declarative
struct expression — no imperative composition, no if branches around
layout primitives. Each field is filled by exactly one adapter
function from a sibling file.
maya/include/maya/widget/app_layout.hpp
struct AppLayout::Config {
Thread::Config thread;
ChangesStrip::Config changes_strip;
Composer::Config composer;
StatusBar::Config status_bar;
std::optional<Element> overlay; // nullopt = no overlay
};Composes four nested widget Configs into a vstack with the Thread growing to fill, then z-stacks an Overlay on top when present. Caller provides one nested Config tree per frame; AppLayout invokes the sub-widgets internally.
Adapter: view.cpp::view(m) (this is also the AppLayout adapter —
view.cpp builds the AppLayout::Config inline since it's the entry
point).
maya/include/maya/widget/thread.hpp
struct Thread::Config {
bool is_empty = false;
WelcomeScreen::Config welcome; // when is_empty
Conversation::Config conversation; // when !is_empty
};Owns the empty-vs-populated branch:
is_empty == true→ rendersmaya::WelcomeScreen{welcome}is_empty == false→ rendersmaya::Conversation{conversation}
Adapter: thread/thread.cpp::thread_config(m).
Thread::Config thread_config(const Model& m) {
Thread::Config cfg;
if (m.d.current.messages.empty()) {
cfg.is_empty = true;
cfg.welcome = welcome_screen_config(m);
return cfg;
}
cfg.conversation = conversation_config(m);
return cfg;
}Three lines: pick the branch, delegate to the sub-adapter for either WelcomeScreen or Conversation. No element work, no rendering logic.
maya/include/maya/widget/welcome_screen.hpp
struct WelcomeScreen::Config {
std::vector<std::string> wordmark; // typically 3 rows
Color wordmark_color = Color::magenta();
std::string tagline;
Element model_badge; // pre-built (e.g. ModelBadge)
std::string profile_label;
Color profile_color = Color::magenta();
std::string starters_title = "Try";
std::vector<std::string> starters;
std::string hint_intro = "type to begin";
std::vector<Hint> hints; // {key, label, key_color}
Color accent_color = Color::magenta();
Color text_color = Color::bright_white();
};Widget owns: wordmark gradient (last row dim), centering, small-caps
title in starters card, bottom hint row layout. agentty owns the brand
content (the m o h a glyphs, tagline copy, starter prompts).
Adapter: thread/welcome_screen.cpp::welcome_screen_config(m).
maya/include/maya/widget/conversation.hpp
struct Conversation::Config {
std::vector<Turn::Config> turns;
std::optional<ActivityIndicator::Config> in_flight;
};A vstack of turns (each one rendered by maya::Turn) separated by
thin dim ─ rules, with an optional maya::ActivityIndicator at
the bottom. The widget builds the Turn from each Turn::Config
internally — the host never sees an Element here.
Adapter: thread/conversation.cpp::conversation_config(m). Walks the
visible message window (m.ui.thread_view_start..end), calls
turn_config() per message, then asks activity_indicator_config()
whether to show the bottom chip.
maya/include/maya/widget/turn.hpp
struct Turn::Config {
std::string glyph; // ✦ for assistant, ❯ for user
std::string label; // "Opus 4.7", "You"
Color rail_color;
std::string meta; // "12:34 · 4.2s · turn 3"
std::vector<BodySlot> body; // typed; Turn auto-spaces between
std::string error; // empty = no error banner
bool checkpoint_above = false;
std::string checkpoint_label = "Restore checkpoint";
Color checkpoint_color = Color::yellow();
};BodySlot is the discriminated body variant:
using BodySlot = std::variant<
PlainText, // user text path: { content, color }
MarkdownText, // markdown path: { content }
AgentTimeline::Config, // tool calls panel
Permission::Config, // inline permission card
Element // escape hatch — only for cached StreamingMarkdown
>;Turn:
- Renders the header row.
- Walks each body slot, dispatching via
std::visitto the matching widget. Inserts a blank line between consecutive non-empty slots — callers don't push spacers. - Optional error banner (
⚠row). - Wraps in the bold left-only border (the speaker rail) at
rail_color. - Optional
CheckpointDividerabove the rail.
Why typed slots: agentty can't construct Element{TextElement{}} for
spacers anymore. Turn handles spacing itself; agentty just lists the
content slots in order.
Adapter: thread/turn/turn.cpp::turn_config(msg, idx, n, m).
maya/include/maya/widget/agent_timeline.hpp
struct AgentTimeline::Config {
std::string title; // " ACTIONS · 3/5 · Bash "
Color border_color = Color::bright_black();
int frame = 0; // for breathing/spinner
std::vector<AgentTimelineStat> stats; // {label, count, color}
std::vector<AgentTimelineEvent> events;
std::optional<AgentTimelineFooter> footer; // {glyph, text, color, summary}
};
struct AgentTimelineEvent {
std::string name; // "Bash", "Read"
std::string detail; // "npm test · exit 0"
float elapsed_seconds = 0.0f;
Color category_color = Color::blue();
AgentEventStatus status = AgentEventStatus::Pending;
ToolBodyPreview::Config body; // typed body — no Element
};Widget owns: round border, title/footer rendering, tree glyph
selection (── / ╭─ / ├─ / ╰─ per position), status icon
(braille spinner / ✓ / ✗ / ⊘), inter-event connector colors,
duration formatting, category-color application.
Adapter: thread/turn/agent_timeline/agent_timeline.cpp::agent_timeline_config(msg, frame, rail_color).
Walks msg.tool_calls, computes done/total/elapsed, picks per-category
colors, builds the events vector. Each event's body is filled via
tool_body_preview_config(tc).
maya/include/maya/widget/tool_body_preview.hpp
Drives the content under each timeline event's │ stripe. Picks one
of five renderers based on kind:
enum class Kind {
None, // empty
CodeBlock, // dim'd head+tail preview
Failure, // CodeBlock in red
EditDiff, // multi-hunk per-side diff with head+tail elision
GitDiff, // unified diff with per-line +/-/@@ coloring
TodoList, // ✓ ◍ ○ checkbox list
};Adapter: thread/turn/agent_timeline/tool_body_preview.cpp::tool_body_preview_config(tc).
Pure data extraction:
| Tool | Resulting Kind |
|---|---|
edit (with hunks) |
EditDiff |
write |
CodeBlock (content arg) |
bash / diagnostics (terminal) |
CodeBlock (stripped output) |
bash (running, with progress text) |
CodeBlock (live stdout) |
git_diff (terminal) |
GitDiff |
read/list_dir/grep/glob/etc. |
CodeBlock (output) |
todo (with todos) |
TodoList |
| any failed tool with output | Failure |
| anything else | None |
Pure helpers shared with AgentTimeline adapter live in
thread/turn/agent_timeline/tool_helpers.cpp (display name, category
color/label, event status, timeline detail) and
thread/turn/agent_timeline/tool_args.cpp (arg extraction).
maya/include/maya/widget/permission.hpp
struct Permission::Config {
std::string tool_name;
std::string description;
bool show_always_allow = false;
};Renders the "tool wants to do X" prompt with [y] allow [n] deny [a] always keys.
Lives as a body slot inside Turn::Config::body when a tool is
awaiting approval.
Adapter: thread/turn/permission.cpp::inline_permission_config(pp, tc).
maya/include/maya/widget/checkpoint_divider.hpp
struct CheckpointDivider::Config {
std::string label = "Restore checkpoint";
Color color = Color::yellow();
};─── [↺ Restore checkpoint] ─── — full-width rule that lives outside
the rail, above a turn. Triggered by Turn::Config::checkpoint_above
(no separate adapter — Turn's adapter sets the flag based on
msg.checkpoint_id).
maya/include/maya/widget/activity_indicator.hpp
struct ActivityIndicator::Config {
Color edge_color = Color::cyan();
std::string spinner_glyph;
std::string label;
};▎ ⠋ streaming… — floats at the bottom of the thread when the model
is mid-stream and the active turn has no Timeline visible (Timeline
already carries the in-flight signal).
Adapter: thread/activity_indicator.cpp::activity_indicator_config(m)
returns optional<ActivityIndicator::Config> so the bottom slot
collapses cleanly when nothing is in flight.
maya/include/maya/widget/changes_strip.hpp
struct ChangesStrip::Config {
std::vector<FileChange> changes;
Color border_color = Color::yellow();
Color text_color = Color::bright_white();
Color accept_color = Color::green();
Color reject_color = Color::red();
};Header row (Changes (N) … Ctrl+R review · A accept · X reject) plus
a maya::FileChanges body with the file list. When changes is
empty, renders to an empty Element so the AppLayout slot collapses
without a host-side if.
Adapter: changes_strip.cpp::changes_strip_config(m).
maya/include/maya/widget/composer.hpp
struct Composer::Config {
std::string text;
int cursor = 0;
enum class State { Idle, AwaitingPermission, Streaming, ExecutingTool };
State state = State::Idle;
Color active_color = Color::cyan(); // when state is Streaming/ExecutingTool
Color text_color = Color::bright_white();
Color accent_color = Color::magenta(); // "primed" border, idle+text
Color warn_color = Color::yellow();
Color highlight_color = Color::cyan(); // queue chip
std::size_t queued = 0;
ProfileChip profile; // { label, color }
bool expanded = false;
};State drives:
- Border + prompt color (idle/streaming/awaiting/has-text → muted/active/warn/accent)
- Placeholder text ("type a message…" / "running tool — type to queue…")
- Prompt boldness (active/has-text → bold; empty-idle → dim)
- Height pin (during activity, height pins to
min_rows=3to prevent vertical bobbing as layout reflows above)
Hint row is width-adaptive (drops expand then newline keys on
narrow widths). Right-side ambient indicators: queue depth, words /
~tokens counters, profile chip.
Adapter: composer.cpp::composer_config(m).
maya/include/maya/widget/status_bar.hpp
Five fixed rows (always 5 — the status row never grows or shrinks, so the composer above never bobs vertically when a toast appears). StatusBar::Config nests typed sub-widget Configs so each sub-widget gets its own agentty adapter:
struct StatusBar::Config {
Color phase_color = Color::cyan(); // top/bottom PhaseAccent + leading rail
// Activity row sub-widget configs.
TitleChip::Config breadcrumb; // empty title = hide
PhaseChip::Config phase;
TokenStreamSparkline::Config token_stream;
Element model_badge;
ContextGauge::Config context; // max=0 = hide
// Status row.
StatusBanner::Config status_banner;
// Shortcut row.
ShortcutRow::Config shortcuts;
// Width thresholds for activity-row pieces.
int breadcrumb_min_width = 130;
int token_stream_min_width = 110;
int ctx_bar_min_width = 55;
int phase_verb_min_width = 50; // < this drops phase verb
int phase_elapsed_min_width = 80; // < this drops phase elapsed
};Adapter: status_bar/status_bar.cpp::status_bar_config(m) is a thin
composer — calls the sub-adapters and assembles:
maya::StatusBar::Config status_bar_config(const Model& m) {
StatusBar::Config cfg;
cfg.phase_color = phase_color(m.s.phase);
cfg.breadcrumb = title_chip_config(m);
cfg.phase = phase_chip_config(m);
cfg.token_stream = token_stream_sparkline_config(m);
cfg.model_badge = model_badge_config(m).build();
cfg.context = context_gauge_config(m);
cfg.status_banner = status_banner_config(m);
cfg.shortcuts = shortcut_row_config(m);
// … width thresholds …
return cfg;
}maya/include/maya/widget/title_chip.hpp
struct TitleChip::Config {
std::string title; // empty = blank
Color edge_color = Color::cyan();
Color text_color = Color::bright_white();
std::size_t max_chars = 28;
};▎ implement /loop dynamic m… — leading colored ▎ + bold title with
middle-truncation. Used in StatusBar's activity row to show the
current thread title. Renders nothing when title is empty.
(Distinct from maya::Breadcrumb, which is a multi-segment path
chip — TitleChip is the simpler single-label cousin.)
Adapter: status_bar/title_chip.cpp::title_chip_config(m).
maya/include/maya/widget/phase_chip.hpp
struct PhaseChip::Config {
std::string glyph;
std::string verb;
Color color = Color::cyan();
bool breathing = false;
int frame = 0;
int verb_width = 10; // 0 = drop verb (very narrow)
float elapsed_secs = -1.0f; // < 0 = omit
};Owns the breathing animation cadence (32-frame cycle, bold half / dim
half — slightly slower than resting heart-rate so the indicator feels
alive without becoming a tick). verb_width truncates-or-pads to
exactly N display columns so the chips to the right stay pinned as
the verb changes.
Adapter: status_bar/phase_chip.cpp::phase_chip_config(m). The
verb_width and elapsed_secs are defaults — StatusBar overrides
them per-frame based on terminal width.
maya/include/maya/widget/token_stream_sparkline.hpp
struct TokenStreamSparkline::Config {
float rate = 0.0f;
int total = 0;
std::vector<float> history;
Color color = Color::cyan();
bool live = false; // false = dim (frozen)
};Stable-width 37-cell slot: ⚡ ▕rate 5▏ t/s ▕spark 16▏ ▕total 5▏.
Every segment is fixed display width so the slot occupies the same
cells whether rate is 0.5 or 1234 — surrounding chips don't shove
leftward as numbers tick.
Adapter: status_bar/token_stream_sparkline.cpp::token_stream_sparkline_config(m).
Resolves the displayed rate from live deltas (after a 250 ms warm-up)
or the most recent ring-buffer sample when paused.
Sparkline persistence: the rate_history ring buffer is not
wiped on StreamStarted. It carries across sub-turns and tool gaps so
the user sees a continuous trace of generation rate over the whole
session. The per-burst rate accumulator (live_delta_bytes,
first_delta_at) does still reset, so the rate number measures
only the current burst.
maya/include/maya/widget/context_gauge.hpp
struct ContextGauge::Config {
int used = 0;
int max = 0;
int cells = 10; // bar width
bool show_bar = true; // false = drop bar + ratio (very narrow)
};Owns: 1/8-gradation block bar with per-cell threshold coloring (cells
0–60% green, 60–80% amber, 80–100% red). When used == 0, renders a
dim placeholder slot the same width as the live version, so the
right-side chips don't shove leftward when the first usage event
fires mid-stream.
Adapter: status_bar/context_gauge.cpp::context_gauge_config(m).
maya/include/maya/widget/status_banner.hpp
struct StatusBanner::Config {
std::string text; // empty = blank slot
bool is_error = false;
Color muted_color = Color::bright_black();
Color error_color = Color::red();
};Single-row banner with a leading edge mark + italic text:
- Empty
text→ 1-cell blank placeholder (keeps the surrounding StatusBar's row count fixed — no jitter when toasts come and go). is_error=true→ red edge + ⚠ glyph.
Adapter: status_bar/status_banner.cpp::status_banner_config(m).
maya/include/maya/widget/shortcut_row.hpp
struct ShortcutRow::Binding {
std::string key;
std::string label;
Color key_color = Color::cyan();
int priority = 0; // higher = kept longer
};
struct ShortcutRow::Config {
std::vector<Binding> bindings;
Color text_color = Color::bright_white();
};Helix / Lazygit / k9s style: bold key in default fg, dim label, no chip background. Greedy-fit width adaptation: starts with every binding labelled, then sheds labels in priority-ascending order until the row fits the available width; if still over budget, sheds whole bindings in the same order. The last remaining binding is never dropped.
Adapter: status_bar/shortcut_row.cpp::shortcut_row_config(m).
maya/include/maya/widget/phase_accent.hpp
struct PhaseAccent::Config {
Color color = Color::cyan();
Position position = Position::Top; // Top → ▔, Bottom → ▁
};Width-aware row of half-block glyphs in the phase color, dim. Reads as a "soft state shelf" rather than a hard line — the color carries app-state information without using extra chrome characters.
No agentty adapter — used internally by StatusBar (top + bottom
strips), driven by StatusBar::Config::phase_color.
maya/include/maya/widget/model_badge.hpp
Compact ● Opus / ● Sonnet / ● Haiku brand chip. Used in two
places: WelcomeScreen's chip row and StatusBar's activity row.
Adapter: status_bar/model_badge.cpp::model_badge_config(m). Returns
the configured ModelBadge widget; callers .build() it where they
need an Element (the widget predates the strict Config-pattern
reshape).
maya/include/maya/widget/overlay.hpp
struct Overlay::Config {
Element base;
Element overlay; // empty Element + present=false collapses
bool present = false;
};Z-stacks overlay over base, centered horizontally, pinned to the
bottom edge, with an opaque background so base content doesn't bleed
through. When present = false collapses to just base.
AppLayout accepts std::optional<Element> and translates internally
— host code never has to construct an empty placeholder Element.
The "adapter" is view.cpp::pick_overlay(m) which returns
optional<Element> based on which modal is currently open (login,
pickers, command palette, diff review, todo). Each modal's Element
still comes from the legacy DSL-based files in §24; once those are
widgetized, pick_overlay becomes a config-builder too.
Every per-widget adapter file under src/runtime/view/ has the same
shape: one function Model → SomeWidget::Config.
| Adapter file | Function | Returns |
|---|---|---|
view.cpp |
view(m) |
Element (the one .build()) |
view.cpp |
pick_overlay(m) |
optional<Element> |
thread/thread.cpp |
thread_config(m) |
Thread::Config |
thread/welcome_screen.cpp |
welcome_screen_config(m) |
WelcomeScreen::Config |
thread/conversation.cpp |
conversation_config(m) |
Conversation::Config |
thread/activity_indicator.cpp |
activity_indicator_config(m) |
optional<ActivityIndicator::Config> |
thread/turn/turn.cpp |
turn_config(msg, idx, n, m) |
Turn::Config |
thread/turn/permission.cpp |
inline_permission_config(pp,tc) |
Permission::Config |
thread/turn/agent_timeline/agent_timeline.cpp |
agent_timeline_config(msg, frame, c) |
AgentTimeline::Config |
thread/turn/agent_timeline/tool_body_preview.cpp |
tool_body_preview_config(tc) |
ToolBodyPreview::Config |
composer.cpp |
composer_config(m) |
Composer::Config |
changes_strip.cpp |
changes_strip_config(m) |
ChangesStrip::Config |
status_bar/status_bar.cpp |
status_bar_config(m) |
StatusBar::Config |
status_bar/title_chip.cpp |
title_chip_config(m) |
TitleChip::Config |
status_bar/phase_chip.cpp |
phase_chip_config(m) |
PhaseChip::Config |
status_bar/token_stream_sparkline.cpp |
token_stream_sparkline_config(m) |
TokenStreamSparkline::Config |
status_bar/context_gauge.cpp |
context_gauge_config(m) |
ContextGauge::Config |
status_bar/status_banner.cpp |
status_banner_config(m) |
StatusBanner::Config |
status_bar/shortcut_row.cpp |
shortcut_row_config(m) |
ShortcutRow::Config |
status_bar/model_badge.cpp |
model_badge_config(m) |
maya::ModelBadge |
Pure helpers (no maya types touched): under
thread/turn/agent_timeline/tool_helpers.cpp (display name, category
color/label, event status, timeline detail) and
thread/turn/agent_timeline/tool_args.cpp (safe_arg, pick_arg,
count_lines, strip_bash_output_fence, parse_exit_code).
Top-level shared (no widget binding): cache.cpp, helpers.cpp,
palette.hpp.
The single Element-returning function inside an adapter is
cached_markdown_for (private to thread/turn/turn.cpp) — it exists
because maya::StreamingMarkdown is stateful (per-block parse cache
must persist across frames). agentty holds the widget instance, calls
set_content() per frame, and slots instance.build() into a Turn
body via the typed Element variant.
The adapter tree mirrors the widget hierarchy.
src/runtime/view/
├── view.cpp # AppLayout
├── changes_strip.cpp # ChangesStrip
├── composer.cpp # Composer
├── cache.cpp · helpers.cpp # shared (not adapters)
├── login.cpp · pickers.cpp · diff_review.cpp # legacy modals (§26)
├── thread/
│ ├── thread.cpp # Thread
│ ├── welcome_screen.cpp # WelcomeScreen (empty branch)
│ ├── conversation.cpp # Conversation (non-empty branch)
│ ├── activity_indicator.cpp # ActivityIndicator (bottom of conversation)
│ └── turn/
│ ├── turn.cpp # Turn
│ ├── permission.cpp # Permission (body slot)
│ └── agent_timeline/
│ ├── agent_timeline.cpp # AgentTimeline (body slot)
│ ├── tool_body_preview.cpp # ToolBodyPreview (per-event body)
│ ├── tool_helpers.cpp # per-tool helpers
│ └── tool_args.cpp # arg parsers
└── status_bar/
├── status_bar.cpp # StatusBar
├── title_chip.cpp # TitleChip (activity row)
├── phase_chip.cpp # PhaseChip (activity row)
├── token_stream_sparkline.cpp # TokenStreamSparkline (activity row)
├── context_gauge.cpp # ContextGauge (activity row)
├── model_badge.cpp # ModelBadge (activity row)
├── status_banner.cpp # StatusBanner (status row)
└── shortcut_row.cpp # ShortcutRow (shortcut row)
Headers mirror the same layout under include/agentty/runtime/view/.
// include/agentty/runtime/view/cache.hpp
struct MessageMdCache {
std::shared_ptr<maya::Element> finalized;
std::shared_ptr<maya::StreamingMarkdown> streaming;
};
[[nodiscard]] MessageMdCache& message_md_cache(const ThreadId& tid,
std::size_t msg_idx);One thread-local cache, keyed on (thread_id, msg_idx). Streaming
messages hold a live StreamingMarkdown instance whose internal
block-cache makes each delta O(new_chars); finalized messages cache
the resulting Element once and return the same pointer forever.
Streaming-rate sparkline (separate from this cache) lives in
StreamState::rate_history — a 16-slot ring buffer that survives
across sub-turns and tool gaps (only the per-burst rate accumulator
resets on StreamStarted).
maya/include/maya/dsl.hpp. agentty's main view files don't import it
anymore — they only build Configs. But:
- Widget authors use it inside
maya/include/maya/widget/*.hppwhen implementingbuild(). - Overlay modals in agentty (
login.cpp,pickers.cpp,diff_review.cpp) still construct elements via DSL; they predate the controller-only refactor and will be widgetized next.
Quick reference (full primer in maya/include/maya/dsl.hpp header
comments):
| Form | Returns |
|---|---|
t<"..."> |
Compile-time TextNode |
text(s) / text(s, style) |
Runtime TextNode |
v(c1, c2, …) |
Vertical box (FlexDirection::Column) |
h(c1, c2, …) |
Horizontal box (FlexDirection::Row) |
spacer() |
Flex-grow gap (grow=1.0f) |
blank() |
Empty 1-line text |
when(cond, then, else?) |
Conditional branch |
map(range, proj) |
Project a range into nodes |
dyn([&]{ return E; }) |
Runtime escape hatch |
| Bold / | Dim / | Italic |
Compile-time style pipe |
| fg<0xHEX> / | Fg<R,G,B> |
Compile-time foreground color |
| pad<T,R,B,L> / | border_<Round> / | grow_<1> |
Compile-time layout pipe |
| fgc(c) / | padding(...) / | border(BS) / | bcolor(c) / | btext(s, pos, align) / | grow(f) / | height(h) / | width(w) |
Runtime layout pipes |
Element types (variants of maya::Element):
| Variant | Purpose |
|---|---|
TextElement |
Single line of text + optional vector<StyledRun> |
BoxElement |
Container with layout + border + children |
ElementList |
Heterogeneous list (rare; v(...) produces this) |
ComponentElement |
Lazy (w,h) → Element callback for width-aware UI |
Three host files still build elements directly — overlay modals that predate the strict controller-only rule:
src/runtime/view/login.cpp— login modalsrc/runtime/view/pickers.cpp— model picker, thread list, command palette, todo modalsrc/runtime/view/diff_review.cpp— pending-changes review modal
Future widgets to absorb them:
maya::LoginModalmaya::Picker(orCommandPalette+ThreadList+ModelPicker+TodoModal)maya::DiffReview
Once those land, every host file under src/runtime/view/ is a pure
data adapter and the using namespace maya::dsl line disappears from
agentty entirely.