架构
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」。