Martty文档
DSH-NATIVE AGENT TUIEnglish

架构

Plugin Package 与视图

Cordis Plugin Package 可以带可选的 code.host、code.client,因此形成 Host-only、 Client-only 与双向三种运行形态。双向不是两棵树合并:两个 half 分别运行在 Host 与 Martty Client 的 Cordis tree,由同一个 run 管理,并通过 ACP 上的 Package RPC 交换 JSON 数据。

TUI 中的四个入口是不同的数据视图:/ui、/theme 展示 UI 与 Theme contribution; 它们的 owner 可以是 Client-only 或双向 Package。/plugins 只读展示当前已加载的 静态 Plugin。/cordis-plugins 展示 Cordis 模式创建的临时 Plugin run,它们可随当前 运行生命周期被停止、替换或回收。

ACP client 是一个 Cordis 插件:提供 ctx.acpClient,讲 ACP,不包、不 import 任何 harness。TUI 是挂在这棵 client 树 上的壳(Rust 占 TTY)。配色等 TUI 插件 inject 的是 client 侧服务,不是 Martty 包名,也不是 dsh-acp。

主路径 dsh --profile martty 启动两个进程:Host 进程的 Base Cordis 树直接挂完整 ACP plugin;Host runner 再启动独立的 TUI Client 进程。两者只通过 TUI Client 的标准 stdin/stdout 讲 ACP,不共享 Cordis service、plugin id、inject 或 fiber。Client 进程的 fd 3/4 只继承用户 TTY,再映射为 Rust painter 的 stdin/stdout;Host↔Client ACP 不走 fd 3/4。

独立入口 martty 保持通用 ACP client 语义:它的 Client 树可以 spawn dsh-acp、dsh --profile acp 或其它 ACP agent,也可以接调用方提供的 stream。

ACP client:根插件或附加插件

同一份 apply,两种组合位置:

独立根插件 附加在已有树上
位置 client 组合的第一行,进程根 别人的 client 树里一行 insert
提供 ctx.acpClient(以及它 boot 的 client 服务) 同样 ctx.acpClient
Agent config.agent.command spawn(默认可 dsh-acp)或 config.stream 接已有标准管道 profile 主路径由 Host runner 把 config.stream 接到跨进程 stdio;独立入口可 spawn
不做什么 不 ctx.plugin 进 dsh-base 不与 ACP Host 插件同进程、也不把 ACP 改成进程内 stream

接到 dsh 上只是 agent 配置,例如 { command: "dsh-acp" } 或 { command: "dsh", args: ["--profile", "acp"] }。换成别的 ACP agent 只改这条命令。

协商后的 DSH Cordis ACP 扩展

Server 树(profile 主路径里的 Base + ACP plugin,或 standalone dsh-acp)和 Client 树(TUI/皮)位于不同进程且不共享 Cordis。协调只用 ACP:

方向 走 ACP 不走
Agent → UI session/update、available_commands_update、config_option_update、permission;协商后可发 _dsh/cordis/inspect/*、run/*、plugins/* notification 整棵插件图、inject 列表、fiber、ctx.*
UI → Agent session/prompt、cancel、set_session_config_option、authenticate;协商后可发 _dsh/cordis/inspect/*、run/*、plugin/*、plugins/* request/notification client 插件去 register 对端服务

Server 插件要让任意 client 看见:投影成标准 ACP(命令、config、update)。Client 插件(换皮、槽)只改本树。TUI 作为 Cordis Client 把可发现的能力目录(Theme.listTokens、Slots.list)序列化到 _dsh/cordis/* 扩展,和网页同步 inspect manifest 是同一类工作。双方必须先在 initialize capability 的 _meta.dsh.cordis.protocol 上达成版本 0;普通 ACP agent 没有该 capability 时,TUI 不发送也不消费这些扩展。

运行中由 Enter 产生的 follow-up FIFO 属于 Martty Client:composer 为空时按 Enter 会把队首立即 steer 给当前轮次,deferred 时放回队首;否则在 session 变为 空闲后作为下一次 session/prompt 发出。Rust 把 FIFO 的只读投影经 本地 _dsh/cordis/tui/queue/update 交给 Client tree;内置 tuiQueue service 折叠快照, queue-view Client Plugin 再向现有 conversation.input.dock 注册 composer 展示。 Queue 标题位于 composer 顶沿,全部等待条目始终在同一个 composer 边框内、输入区上方 展开;Alt+↑ 进入选择态后可用 ↑/↓ 任选一条编辑。等待内容不进入 timeline,出队时才 创建普通 user timeline cell;编辑、删除和预览更新也都不进入 ACP。没有 Client tree 的 独立 painter 保留原生 shelf 作为降级显示。 Send Now 的投递方式按 initialize 响应协商:顶层 _meta.steering.supported 走通用的 _session/steering 扩展(claude-agent-acp、codex-acp、deepseek-harness-acp 共用;outcome:"injected" / startedNewTurn 均视为已被 agent 接收,promptRequired 视为 deferred); _meta["minimax-code/extensions"].methods 含 mcode/session/steer 走该方法(仅文本, 带图片的 Send Now 直接 deferred,因为 MiniMax Code 的 session/prompt 拒绝并发); 都未宣告时发并发 session/prompt(旧版 agent 的默认约定)。任一路线被 agent 拒绝时,ACP transport 只向 Client 回报 deferred,由同一 Client FIFO 接管,不在 transport 内保留第二份重试队列。

ACP 的 prompt 调度与慢控制请求分开执行:src/acp.rs 持有每个 session 的轮次和发送队列,src/acp/control.rs 执行控制请求。创建/恢复共用一个串行队列, 使绑定结果保持与 Rust tab 的等待 FIFO 同序;配置操作按 session 分别串行, 该 session 的下一轮 prompt 等待先前配置完成,其他 session 的 prompt、完成事件、 取消和关闭继续处理。短控制请求保留 120 秒 deadline。扩展路线的 steer (_session/steering、mcode/session/steer)同样使用这个 deadline;并发 session/prompt 路线上的整轮 prompt 与 steer 不使用这个总时长限制。Queue 的选择和编辑状态随 session 保存,后台会话同样遵守暂停规则。

原生 tab 是当前可见会话的唯一选择源。每次 bind 或 tab 切换,Rust 通过 _dsh/cordis/tui/session/active request 把 session id(未绑定时为 null)投影到 Client tree; acpSessionConfig、acpSessionPlan、acpSessionStats 和 acpSessionStatus 只发布该会话的 缓存快照,后台会话的 ACP 更新只更新各自缓存,不能覆盖可见 tab。

Agent/subagent 导航也走同一条 Client compositor 边界:Rust painter 把主会话与子会话的 只读快照送到内置 tuiAgents service,agents-view Client Plugin 再把紧凑导航注册到 conversation.navigation.dock。这个 slot 位于 composer 内部,输入区与 Standard 模式行之间;默认只画 · Agents · 完成/总数 摘要,↓ 后才展开全部条目; ←/→ 移动、Enter 打开、Esc 折叠,不创建弹窗或鼠标 action。无 Client tree 时 Rust 保留同语义的原生导航作为降级路径。

标准 ACP tool_call 即使携带 subagent _meta 也始终先按原事件进入父会话 transcript; 解析器只在其后追加 SubagentStarted 投影。终态 tool_call_update 同样先生成原始 ToolResult 闭合请求块,再追加 SubagentFinished,subagent 生命周期不会替换工具事件。

Host 进程:Base Cordis + ACP plugin
  tui-client-plugin-registry 收集已安装 package 的绝对 Client entry
        │ TUI Client 子进程的标准 stdin/stdout(ACP)
        │ runner 另以 JSON 环境快照传递 Client entry 目录(不传 Cordis 对象)
Client 进程:独立 Cordis root
  第三方插件   sibling insert + inject ['tuiTheme' / 'tuiPresets' / 'tuiSlots']
  Client runner inject ['tuiTheme', 'tuiPresets', 'tuiSlots', ...],执行 dsh-tool-cordis 的 code.client
  TUI 壳       注入 acpClient 与 Client services,只占 TTY
  plan-view    注入 acpSessionPlan + tuiSlots + tuiCommands + tuiOverlay
  tui-queue    接收 Rust painter 的本地 Queue 快照,提供 tuiQueue
  queue-view   注入 tuiQueue + tuiSlots,向 conversation.input.dock 注册全部条目与选择状态
  tui-agents   接收 Rust painter 的本地 Agent 快照,提供 tuiAgents
  agents-view  注入 tuiAgents + tuiSlots,向 conversation.navigation.dock 注册导航
  stats-view   注入 acpSessionStats + tuiSlots
  status-view  注入 acpSessionStatus + acpSessionStats + tuiCommands +
               tuiOverlay,注册 /status 命令
  harness-view 注入 tuiCommands + tuiOverlay + acpClient + acpSessionStatus;
               /harness 选择只保存 defaultHarness;下一次 /new 使用它;
               完成面板 Enter 或 /harness <id> --new 复用 /new 的空 tab 复用逻辑;
               每个 tab 保留所属 ACP 连接,切换默认值不修改已有会话;
               产品内部 forcedHarness 初始化配置默认为空;有值时启动优先于
               持久化 defaultHarness;它不是 CLI 启动参数;
               /harness 只更新 defaultHarness,不修改产品强制值;
               profile 主路径仍由 Host 拥有 runtime
  tui-presets  提供 tuiPresets,注册持久化 /ui 选择与组合生命周期
  martty-preset default UI Plugin,组合 welcome.hero + welcome.info
  deepseek-logo 注入 tuiPresets + tuiSlots,注册 deepseek UI Plugin;
               组合 DeepSeek welcome.hero + 动态 welcome.info
  acp-session-status 提供 acpSessionStatus(连接/服务端/认证/会话/
               模型/effort/权限/plan/agent 等运行状态事实)
  tui-slots    提供 tuiSlots,声明 welcome.hero / welcome.info / chrome.right /
               conversation.input.dock / conversation.navigation.dock /
               conversation.composer.dock
  tui-theme    提供 tuiTheme
  tui-local-plugins 合并已安装 package entries 与 $MARTTY_HOME/plugins,
               恢复 UI Plugin,并按 /theme owner 启停 Theme Plugin
  acp-client   attach Host stdio,提供 acpClient
        │ Client 进程 fd 3/4 仅传 TTY
  Rust painter stdin/stdout 占 TTY;自己的 fd 3/4 是 compositor mux

standalone:Client 进程也可改为 spawn 任意 ACP agent 的标准 stdio

profile 只需安装 @openma/deepseek-harness-tui。dsh 仍按正常 profile 规则提供 Base;TUI bundle 从自己的运行时依赖挂 ACP plugin 与 Creator Host overlay,并挂 Host runner。runner 不再 spawn dsh-acp,只启动独立 TUI Client 进程并连接标准 ACP stdio。Client root 由该进程自行创建,Host bundle rows 不等于 Client fibers。

Harness catalog startup

Add / Install / Connect 只准备和保存 Harness recipe,不启动 ACP、不认证。 安装成功后自动写入 settings.json 并刷新已配置列表;后台安装不抢焦点。 /harness 选择保存 defaultHarness。空会话直接走 /new,复用当前 tab,不弹保存提醒; 已有聊天时显示保存提醒,Esc 返回当前会话,Enter 走 /new。 下载完成面板 Enter 和 /harness <id> --new 同样保存默认值,再走 /new。 每次 /new 都按 defaultHarness 创建新会话;当前 tab 只有提示、没有聊天内容时原位复用, 否则保留原 tab 并新建 tab。复用时保留未发送草稿,已替换会话的晚到绑定结果丢弃。 已有 tab 的 transcript、草稿、模型、认证和能力信息与其连接一起保留。

standalone 的 acpClient 用稳定的 stdio 入口管理多个 ACP 连接。连接按命令、 参数和环境区分;setDefaultAgent 只保存下一次 session/new 的 recipe, 到创建会话时才按需 spawn、initialize。请求和回复按 session 与连接路由, 不同 Agent 的 session id、认证 method id 和双向 RPC id 冲突由本地连接层隔离。 单个连接的失败或等待认证不会销毁、重定向或阻塞其他连接的会话。 退出 Client 时统一关闭所有子进程。profile 的 config.stream 仍由 Host 拥有。 本地连接层附加的 marttyConnection 仅携带协商结果与连接身份供 Client 路由, 其中包括该连接 initialize 的 _meta(Send Now 据此为每个会话选择 steer 路线); 它不是 Agent 协议扩展,也不携带 TUI chrome。

已配置的 npx/uvx recipe 直接启动,由 runner 复用缓存。选择器把持久化 defaultHarness 标为默认并置顶,允许再次选择;删除保护按所有实际运行的连接判断。 添加和安装本身不改变默认值,同 id 的显式配置替换会更新 recipe,旧后台下载 不能覆盖更新的配置意图。

harness-view 先从 settings.json 同级的 cache/acp-registry.json 读取上次 验证成功的官方目录;没有缓存时使用随 npm lib 发布的 acp-registry.snapshot.json。快照来自官方 https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json,当前随包 快照获取于 2026-09-29,不是手写 Harness 名单。发布时应更新该官方原始快照。

打开 Add Harness 立即返回可搜索的目录,不等待网络或 PATH 探测。目录刷新与 本地探测在后台执行,同 id 的 select 原位更新且保留搜索/选择;离线时保留已显示 目录。尚未探测的条目标为 Catalog,不能提前声称已安装或未下载;选择它时只 探测对应 recipe,再进入本地配置或下载确认。成功刷新以临时文件加 rename 更新 磁盘缓存,失败不覆盖旧缓存;关闭面板后的异步结果不抢回焦点。

Harness icon

内置 harness-badge 订阅当前 Session 的连接身份,匹配 Registry 图标并经 conversation.harness slot 投影到模型名前。图标下载和 PNG 磁盘缓存属于 Client, 图片位置与 Kitty 生命周期属于 Rust。默认 Harness 变更不影响当前 tab 的标识。 缺失、加载失败或终端不支持图片时显示 Harness 名称。 DeepSeek 尚无 Registry 条目时,从依赖包 @lobehub/icons-static-svg 读取 DeepSeek SVG(MIT),无需网络;按库版本生成缓存键,仍走同一 PNG 缓存。

Creator skill overlay

Creator 的 skill 属于 agent Host,不属于 TUI Client 树。TUI 主包内部的 creator-overlay 入口叠到 Host Base tree:它注入 agentPresets 与 skills,通过 standingKeyFor('cordis') 取得上游 Creator 的 standing scope key,再用相同 key 挂一个自己的 fiber 并注册 tui-plugin-development。因此 skill 只进入 cordis 的 scope layer;其它 preset 和全局目录看不到,卸载时也只撤销这一层贡献。

overlay 通过 profile Loader 取得 Host 正在使用的 dsh-scope 模块实例。这样 link: 热部署包即使有自己的开发依赖,也不会产生第二套私有 scope symbol,把 本应属于 Creator 的 skill 误注册到全局层。

这不是 Creator fork,也不修改其 agent.cordis.yml。上游 preset 升级后重启 profile 即会得到新组合,overlay 重新叠在新 standing scope 上。skill 的发现与 注册不走 ACP;TUI Client 能力查询和动态 code.client 执行仍走已经协商的 Client inspect/run 通道。

分层

第三方插件    sibling insert + inject ['tuiTheme'] → 配色 register
              sibling insert + inject ['tuiPresets'] → UI 组合 register
              sibling insert + inject ['tuiSlots'] → slot register
tui-theme     配色表 → Theme registry
tui-presets   多个 UI contribution → 持久化 /ui 单选组合
tui-slots     TuiNode 树 → welcome.hero / welcome.info / chrome.right / input / navigation / telemetry dock registry
plan-view     标准 ACP Plan 投影 → input dock 摘要 + /plan-view overlay
tui-queue     Rust Client FIFO 快照 → Client tree 的 tuiQueue service
queue-view    tuiQueue → input dock 顶沿标题 + composer 内全部等待条目
tui-agents    Rust Agent/session 快照 → Client tree 的 tuiAgents service
agents-view   tuiAgents → composer 内部 navigation dock + inline 会话选择
stats-view    标准 ACP usage/timing 投影 → composer dock 统计行
status-view   acpSessionStatus + acpSessionStats → /status markdown overlay
harness-view  Harness registry → defaultHarness 配置 + /new action
martty-preset default UI Plugin → Martty Hero + 原生动态信息区
deepseek-logo deepseek UI Plugin → DeepSeek Hero + 原生动态信息区
acp-session-status 标准 ACP 运行状态投影:连接/服务端/认证/会话/模型/
              effort/权限/plan/agent —— 不累计 token 或耗时(那是
              acpSessionStats 的职责)
Client runner Client inspect + code.client 挂载/停止
TUI 壳        消费 Theme、slot 与通用 overlay 快照树;把本地 Queue/Agent 快照路由给对应 service
acp-client    session/new · session/fork · authenticate · prompt · cancel · config · commands
              standalone 使用稳定代理流,可替换 ACP 子进程而不断开 TUI compositor;
              启动/替换时清空旧 Agent capability,initialize 后立即 session/new
              提供 acpSessionConfig(观察标准快照;set 仍由 Rust 发标准 ACP)
              提供 acpSessionPlan(观察标准 plan update;内置 Plan 插件只是消费者)
              提供 acpSessionStats 与通用、随 effect 回收的 ACP observer registry
              `_dsh/cordis/*` 是 initialize 协商后的 Cordis 扩展
              `_dsh/cordis/tui/*` 是 mux 隔离的 painter 子域
Rust 画布     输入、keymap、Client FIFO、现有 widget 读 Theme/slot、kitty、剪贴板
层 职责 不负责
配色插件 为封闭 token 名提供 dark/light 色值 改布局、改 token 名、占 TTY、依赖某个 harness
acp-client ACP 会话控制;作根或作 insert 组合 dsh、当 UI 内模
tui-theme / tui-slots 提供 tuiTheme / tuiSlots registry 占 TTY、依赖 agent
Client runner 向 dsh-tool-cordis 发布 TUI Client 能力并挂载 code.client 占 TTY、运行 Web React 插件
TUI 壳 TTY、输入、compositor;消费插件提供的结构化 slot/overlay snapshot 拥有 Plan/统计等业务投影,或解释 harness SessionEvent
Rust 把 Theme 画进现有 widget 在 JS 里解释颜色

本体只有内置 default。动态 Theme Plugin 用 tuiTheme.register 声明 palette; /theme 是特殊的单选 Plugin 开关,启动目标 Plugin 并停止当前 Theme Plugin,因而 palette、command、overlay、slot 与 RPC 随同一个 Fiber 一起上下线。martty --demo 保持 default;--demo-skin 仍是静态 gallery 演示路径。常驻 gallery 包 ayu(dark=Ayu、light=Ayu Light)、catppuccin(dark=Catppuccin Mocha、light=Catppuccin Latte)、kanagawa(dark=Kanagawa Wave、light=Kanagawa Lotus)、one(dark=One Dark、light=One Light)、tomorrow(dark=Tomorrow Night Bright、light=Tomorrow)、everforest / iceberg / solarized(均为 dark+light 双变体),色值取自 terminalcolors.com,随 Client boot 以 sibling insert 行注册,/theme 直接可切。

斜杠命令

/ 菜单里的自带命令由客户端解析。/fork 只在 initialize 的 agentCapabilities.sessionCapabilities.fork 为对象(标准写法 {})时执行;否则该行禁用并写明原因。执行时对当前会话发 session/fork,参数与 session/load 相同(cwd、mcpServers,以及已声明时的 additionalDirectories),不带消息 id,不带 _meta。新 sessionId 按现有多标签打开并切过去,原会话保留。判断不看 Harness 名称或版本。

自带命令占用原名。available_commands_update 里的同名命令保留在菜单中,显示为「Agent 名 + 原命令」(例如 pi-acp /model)。选中后把原来的 /name … 作为 prompt 发出。/model 与 /session 跟这条规则一样。

控制面与绘制面

标准 auth-required 错误和结构化 data.errorKind: authentication_failed 都打开所属会话的认证面板, 保留 Agent 的具体错误原因;原请求暂存,认证成功后只重试该连接的暂存请求。 Agent 登录以 ACP authenticate response 为准:请求进行中显示 SigningIn, 成功响应后更新认证状态并继续缺失的 session/new;失败(包括返回 auth_required 的账号资格拒绝)显示 Failed 与 Agent 的具体原因。浏览器 OAuth 完成页不是 ACP 登录成功的证据。失败面板在 landing 页也可见, /auth 可再次选择认证方式;等待期间不重复发送登录请求。 initialize.authMethods 只声明可选认证方式,不证明当前凭据来源;只有实际 提交的 authenticate 才记录所用 method。已有凭据使 session/new 成功时, 只显示就绪、来源未上报。每次新的 initialize 清空旧连接的 config、status、 plan、stats 及待处理请求;初始化返回的 Agent 名称先于会话创建完成显示, 切换期间不回退到启动时的 DSH 信息。 配置 set 与预览事务绑定发起时的会话代次,旧响应与旧插件回滚不能修改新会话。 真实运行的 model/effort 来自 ACP 配置或事件;状态服务优先按 model / thought_level category 识别,只有 demo 使用静态模型默认值。

控制面走 ACP 已有方法(含 authenticate,对标 Backchat 的 probe / 登录面板:表单走 _meta,Terminal Auth 占 TTY)。Client initialize 声明 fs.readTextFile / fs.writeTextFile 与 terminal(createTerminal,有别于 auth.terminal);会话中途 auth_required 打开 client /auth,不要让用户去敲 /login。Transcript 来自 session/update。配色是 client 树 上的 tuiTheme.register;送到 Rust 画布的是 _dsh/cordis/tui/theme/update notification,不是第三方插件 API。_meta.dsh.cordis 只协商能力,chrome 数据必须走扩展 notification。

主题

Token 名封闭,见 plugins.md。内置 default 的色值仍是冷蓝灰。配色包可以(也必须)为全部 token 提供 #RRGGBB。对话节点以后若开放,节点上仍然只写 token 名,不写 RGB。

/theme toggle 或 ctrl+t 切当前主题的 dark/light。/theme <id> 切整个 Theme Plugin。kitty 宠物是 RGBA 精灵,不随 token 重上色;启动锁屏的 MAR 读海洋渐变,TTY 读终端前景黑/白。 UI Plugin 可以组合多个结构性 UI contribution,但不等同于 Theme。default(Martty)与 deepseek 都同时装配 welcome.hero 和 welcome.info:前者是居中的 logo + hint 品牌区,后者是左下的版本、模型、workspace、session、凭据、访问说明 与帮助区。两套 preset 当前复用同一个原生动态 info renderer,但该区域可以独立被 插件替换。旧 DeepSeek Harness 鲸鱼仍复用原版响应式 primitive。Rust 负责内部几何、 水平居中与整个欢迎块的垂直居中;/ui deepseek / /ui default 切换并持久化到 $MARTTY_HOME/settings.json。

Creator 的 cordis_define/run Package 属于 Session 进程内预览。显式保存后,Client 源码以 $MARTTY_HOME/plugins/<artifact-id>/plugin.json 为磁盘真源;Client 启动时 重新发现。第三方 package 则继续由 dsh profile 安装,其 Host registrar 只向 tuiClientPlugins 登记 { id, kind, entry },runner 把这个可序列化快照交给独立 Client import。两类来源进入同一个 lifecycle manager;同 id 时安装 package 优先。 新磁盘 artifact 使用 kind: "ui";旧 ui-preset 与内部 uiPreset key 仅为兼容。 UI Plugin 只组合结构性 UI contribution,不拥有 Theme;保存的 uiPreset、theme 和 dark/light 相互独立。

MARTTY_HOME 依次取显式环境变量、$DSH_HOME/.martty、~/.martty。默认 Session 根是 $MARTTY_HOME/sessions。旧 settings、Creator artifacts 与 Session 根只作为 非破坏迁移/发现来源保留。

明确不做

  • 把 Web 的 dsh-client-ui-* React 组件跑在终端里。
  • 开放 ACP 方法表给第三方。
  • 把 acp-client 和 acp-bridge 放进同一进程抢 stdout。
  • TUI npm 只允许在 @deepseek-ai/* 里依赖 @deepseek-ai/cordis,不直接依赖 dsh-*;ACP 与 Creator 是明确的运行时传递依赖,但不进入 Client bundle rows。
  • 插件发 kitty、设 raw mode、读整屏 size 做绝对定位。
  • 用往时间线加一条消息来冒充「换了 TUI」。