Architecture
The ACP client is a Cordis plugin: it provides ctx.acpClient, speaks ACP, and does not bundle or import any harness. The TUI is a shell on this client tree (Rust owns the TTY). TUI plugins such as palettes inject client-side services, not the Martty package name and not dsh-acp.
The primary dsh --profile martty path starts two processes. The Host process mounts the complete ACP plugin on its Base Cordis tree, then the Host runner starts a separate TUI Client process. They speak ACP only through the TUI Client's standard stdin/stdout; no Cordis services, plugin ids, inject, or fibers cross the process boundary. The Client process receives the user TTY on fd 3/4 and maps it to the Rust painter's stdin/stdout. Host↔Client ACP never uses fd 3/4.
The standalone martty entry remains a general ACP client: its Client tree may spawn dsh-acp, dsh --profile acp, or another ACP agent, or attach a caller-owned stream.
ACP client: root plugin or attached plugin
One apply, two composition seats:
| Standalone root | Attached to an existing tree | |
|---|---|---|
| Seat | First row of the client composition; process root | One insert in someone else's client tree |
| Provides | ctx.acpClient (and client services it boots) |
The same ctx.acpClient |
| Agent | config.agent.command spawn (default may be dsh-acp) or config.stream on standard pipes |
The profile runner attaches config.stream to cross-process stdio; standalone may spawn |
| Must not | ctx.plugin dsh-base |
Share a process with the ACP Host plugin or replace ACP with an in-process stream |
Attaching to dsh is agent config, e.g. { command: "dsh-acp" } or { command: "dsh", args: ["--profile", "acp"] }. Another ACP agent is a different command.
Negotiated DSH Cordis ACP extensions
The Server tree (Base + ACP plugin in the profile path, or standalone dsh-acp) and the TUI Client tree live in different processes and do not share Cordis. They coordinate with ACP only:
| Direction | Over ACP | Not over the wire |
|---|---|---|
| Agent → UI | session/update, available_commands_update, config_option_update, permission; negotiated _dsh/cordis/inspect/*, run/*, and plugins/* notifications |
The whole plugin graph, inject lists, fibers, ctx.* |
| UI → Agent | session/prompt, cancel, set_session_config_option, authenticate; negotiated _dsh/cordis/inspect/*, run/*, plugin/*, and plugins/* requests/notifications |
A client plugin registering a server service |
A server plugin that any client should see is projected into standard ACP (commands, config, updates). Client plugins (skins, slots) stay on this tree. The TUI is a Cordis Client: it serializes the discoverable Theme.listTokens and Slots.list directories over _dsh/cordis/*, the same kind of projection performed by the web Client. Both sides must negotiate protocol 0 at initialize _meta.dsh.cordis.protocol; without it, the TUI neither sends nor consumes these extensions.
The follow-up FIFO and Agent/session navigation are Client interaction state,
not timeline content. The Rust painter projects local snapshots into the Client
compositor. Builtin tuiQueue/queue-view render Queue state in
conversation.input.dock, with every queued item visible above the input;
empty Enter steers the FIFO head immediately, and a deferred steer restores it
to the front. Otherwise the head advances as the next prompt when the turn idles.
The steer route is negotiated from the initialize response: top-level
_meta.steering.supported selects the shared _session/steering extension
(claude-agent-acp, codex-acp, deepseek-harness-acp; outcome:"injected" or
startedNewTurn means the agent took it, promptRequired is deferred);
_meta["minimax-code/extensions"].methods listing mcode/session/steer selects
that method (text only — image steers are deferred because MiniMax Code rejects
concurrent session/prompt); with neither advertised, a concurrent
session/prompt joins the active turn (the legacy convention).
Builtin tuiAgents/agents-view render session
navigation in conversation.navigation.dock. The navigation row sits inside
the composer between its input and metadata row. It defaults to a collapsed
· Agents · completed/total summary; ↓ expands every item in place. ←/→ moves,
Enter opens, and Escape collapses. No popup or mouse action is created. Native
Queue and Agent views retain
the same semantics as fallbacks when no Client tree is present.
The native tab is the single source of truth for the visible Session. On each
bind or tab switch, Rust projects its Session id (null while unbound) into the
Client tree with the _dsh/cordis/tui/session/active request. acpSessionConfig,
acpSessionPlan, acpSessionStats, and acpSessionStatus publish only that
Session's cached snapshot; ACP updates from background Sessions update their own
caches without replacing the visible tab.
A standard ACP tool_call carrying subagent _meta still enters the parent
transcript as the original tool event; the parser only appends a
SubagentStarted projection. A terminal tool_call_update likewise produces
the original ToolResult before appending SubagentFinished, so subagent
lifecycle never replaces the visible tool request.
Host process: Base Cordis + ACP plugin
│ TUI Client child stdin/stdout (ACP)
Client process: independent Cordis root
client plugins sibling insert + inject ['tuiTheme' / 'tuiPresets' / 'tuiSlots']
Client runner injects Client services and runs dsh-tool-cordis code.client
TUI shell injects acpClient plus Client services; owns only the TTY
plan-view injects acpSessionPlan + tuiSlots + tuiCommands + tuiOverlay
tui-queue receives local FIFO snapshots and provides tuiQueue
queue-view tuiQueue → conversation.input.dock
tui-agents receives local Agent snapshots and provides tuiAgents
agents-view tuiAgents → conversation.navigation.dock + /agents
stats-view injects acpSessionStats + tuiSlots
status-view injects acpSessionStatus + acpSessionStats +
tuiCommands + tuiOverlay; registers the /status command
harness-view injects tuiCommands + tuiOverlay + acpClient + acpSessionStatus;
standalone startup/switch initializes and binds an empty session;
/harness switches it immediately before the first prompt,
otherwise confirms before initializing the next Harness;
internal product forcedHarness is empty by default and, when
set, wins persisted defaultHarness at startup; it is not a CLI
startup argument; /harness only updates defaultHarness;
profile runtime stays Host-owned
tui-presets provides tuiPresets; owns persistent /ui composition selection
martty-preset default UI Plugin → welcome.hero + welcome.info
deepseek-logo injects tuiPresets + tuiSlots; registers the deepseek UI Plugin
acp-session-status provides acpSessionStatus (connection/server/auth/
session/model/effort/permission/plan/agent run-state facts)
tui-slots provides tuiSlots; declares welcome.hero / welcome.info / chrome.right /
conversation.input.dock / conversation.navigation.dock /
conversation.composer.dock
tui-theme provides tuiTheme
acp-client attaches Host stdio, provides acpClient
│ Client fd 3/4 carries only the inherited TTY
Rust painter stdin/stdout owns the TTY; its own fd 3/4 is compositor mux
standalone: the Client process may instead spawn any ACP agent on standard stdio
A profile only needs @openma/deepseek-harness-tui. dsh supplies Base through
the normal profile composition; the TUI bundle mounts the ACP plugin and Creator
Host overlay from its runtime dependencies, plus the Host runner. The runner
does not spawn dsh-acp: it starts the independent TUI Client process and binds
standard ACP stdio. That process constructs the Client root; Host bundle rows
are not Client fibers.
Creator skill overlay
Creator skills belong to the agent Host, not the TUI Client tree. The TUI
package's internal creator-overlay entry is layered onto the Host Base tree.
It injects agentPresets and
skills, obtains the
upstream Creator standing scope key through standingKeyFor('cordis'), then
mounts its own fiber with that key and registers tui-plugin-development.
Only the cordis scope layer receives the skill; other presets and the global
catalog do not see it, and unloading removes only this contribution.
This is not a Creator fork and does not edit its agent.cordis.yml. Restarting
the profile after an upstream upgrade mounts the overlay on the new standing
composition. Skill discovery and registration do not use ACP; TUI Client
capability queries and dynamic code.client execution still use the negotiated
Client inspect/run channel.
Layers
Third-party plugins sibling insert + inject ['tuiTheme'] → palette register
sibling insert + inject ['tuiPresets'] → UI composition register
sibling insert + inject ['tuiSlots'] → slot register
tui-theme palette → Theme registry
tui-presets several UI contributions → persistent /ui composition
tui-slots TuiNode trees → welcome.hero / welcome.info / chrome.right / input / navigation / telemetry dock registry
plan-view standard ACP Plan projection → input dock + /plan-view overlay
tui-queue local Client FIFO snapshots → tuiQueue service
queue-view tuiQueue → input dock summary and in-composer picker
tui-agents local Agent/session snapshots → tuiAgents service
agents-view tuiAgents → navigation dock + inline session selection
stats-view standard ACP usage/timing projection → composer dock stats line
status-view acpSessionStatus + acpSessionStats → /status markdown overlay
harness-view Harness registry + started state → /harness confirmation + new process
martty-preset default UI Plugin → Martty Hero + native dynamic information
deepseek-logo deepseek UI Plugin → DeepSeek Hero + native dynamic information
acp-session-status standard ACP run-state projection: connection/server/auth/
session/model/effort/permission/plan/agent — no token or
timing accumulation (acpSessionStats owns that)
Client runner Client inspect + code.client mount/stop
TUI shell consumes Theme, slot, and generic overlay snapshot trees
acp-client session/new · session/fork · authenticate · prompt · cancel · config · commands
provides acpSessionConfig / acpSessionPlan / acpSessionStats
plus a generic effect-scoped ACP observer registry
`_dsh/cordis/*` is negotiated at initialize
`_dsh/cordis/tui/*` is the mux-isolated painter child domain
Rust painter input, keymap, existing widgets reading Theme, kitty, clipboard
| Layer | Owns | Does not own |
|---|---|---|
| Palette plugin | dark/light values for closed token names | Layout, token names, the TTY, a particular harness |
| acp-client | ACP session control; root or insert | Composing dsh, the UI model |
| tui-theme / tui-slots | The tuiTheme / tuiSlots registries |
The TTY, any agent dependency |
| Client runner | Publishes TUI Client capabilities to dsh-tool-cordis and mounts code.client |
Owning the TTY, running Web React plugins |
| TUI shell | TTY, input, and compositor; consumes structured slot/overlay snapshots | Owning Plan/stats domain projections or interpreting harness SessionEvent |
| Rust | Painting Theme through existing widgets | Interpreting colors in JS |
The tui-theme service starts with builtin default. A dynamic Theme Plugin
declares its palette with tuiTheme.register; /theme is a special single-select
Plugin switch that starts the target and stops the current Theme Plugin. Its
palette, commands, overlays, slots, and RPC therefore share one Fiber lifetime.
martty --demo stays on default; --demo-skin remains the static gallery path.
Slash commands
Builtin / commands are resolved on the client. /fork runs only when initialize returns agentCapabilities.sessionCapabilities.fork as an object (the standard form is {}); otherwise the row stays disabled and states why. It sends session/fork for the current session with the same parameters as session/load (cwd, mcpServers, and additionalDirectories when that capability was advertised), with no message id and no _meta. The new sessionId opens through the existing tab strip and becomes current. The original session stays. The gate is not the harness name or version.
Builtins keep the bare name. A same-named command from available_commands_update remains in the menu as agent-name /command (for example pi-acp /model). Choosing it sends the original /name … line as a prompt. /model and /session follow the same rule.
Control plane vs paint
Control uses existing ACP methods (including authenticate, matching Backchat's probe / sign-in panel: forms go through _meta, Terminal Auth occupies the TTY). Client initialize advertises fs.readTextFile / fs.writeTextFile and terminal (createTerminal, distinct from auth.terminal); mid-session auth_required opens client /auth — do not tell the user to type /login. The transcript is painted from session/update. Palettes are tuiTheme.register on the client tree; _dsh/cordis/tui/theme/update reaches the Rust painter as a notification and is not a third-party plugin API. _meta.dsh.cordis negotiates capability only; chrome data belongs in extension notifications.
Theme
Token names are closed; see plugins.md. Built-in default keeps the cold bluish values. A palette pack may (and must) supply #RRGGBB for every token. When conversation nodes open later, nodes still name tokens; they do not carry RGB.
/theme toggle or ctrl+t toggles dark/light inside the current theme. /theme <id> switches the whole
Theme Plugin. Kitty pet sprites are RGBA and do not recolor. In the startup
lockup, MAR reads the ocean gradient while TTY uses terminal foreground
(white in dark mode, black in light mode). A UI Plugin can compose several UI
contributions and is not a Theme alias. Both builtin default (Martty) and deepseek
fill welcome.hero and welcome.info: the first is the centered logo + hint
brand region; the second is the lower-left version/model/workspace/session/
credential/access/help region. Both currently reuse the native dynamic info
renderer, while the slot remains independently replaceable. The Rust painter
reuses the original responsive whale primitive and owns whole-welcome vertical
centering. /ui deepseek and /ui default switch the saved composition.
Out of scope
- Running Web
dsh-client-ui-*React components in the terminal. - An open ACP method table for third parties.
- Putting acp-client and acp-bridge in one process that fights over stdout.
- The TUI npm package depending directly on
@deepseek-ai/dsh-*.@deepseek-ai/cordisand ACP are the explicit runtime dependencies; ACP and the internal Creator overlay stay out of Client bundle rows. - Plugins emitting kitty, entering raw mode, or reading full-screen size for absolute layout.
- Treating an extra timeline message as proof the TUI is pluggable.