@aiwayds/dsh-tui-pi 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [English](README.md) | [简体中文](README.zh-CN.md)
2
+
1
3
  # dsh-tui-pi
2
4
 
3
5
  pi-style terminal UI for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) — a plugin suite that turns dsh into a pi-like coding agent experience.
@@ -201,6 +203,10 @@ There's no slash command to toggle the feature — it's always on, controlled by
201
203
 
202
204
  While the model is mid-turn it can pause and ask you structured questions via the `ask_user_question` tool (`@deepseek-ai/dsh-tool-ask-user`, mounted by this profile's bundle patch). The TUI hosts the answering side: a bordered panel pins itself directly above the chat input (the Todos-panel slot — no floating popup), takes the keyboard while open, the tool call stays pending until you answer, and your answers flow back to the model as a normal tool result.
203
205
 
206
+ Watch the ask-user-question flow in action:
207
+
208
+ https://github.com/user-attachments/assets/aa36be36-a508-4f53-ba85-efe0394dab11
209
+
204
210
  - **One question at a time, tabs for the rest** — with several questions the panel shows exactly ONE question block (header row + supporting `detail` text, option rows, plus a `Type something.` sentinel row for free text); a tab strip under the title (`[1] · 2✓ · 3` — brackets mark the focused tab, ✓ an answered one) folds the other questions away. `←`/`→` (and Tab/Shift-Tab) switch tabs; answering a single-select tab auto-advances to the next unanswered one (or onto the Confirm row once everything is answered). Single-select replaces on Enter; multi-select toggles (`●`/`○` marks) and never auto-advances.
205
211
  - **Ctrl+T folds the panel to a 3-line strip** — the questions panel can block the transcript it stacks on while you think; Ctrl+T collapses it to borders + one summary line (phase, tab position, answered count, how to expand) and the same key unfolds it. While folded only the toggle and the Esc chain act; folding mid-edit commits the buffer like the ↑↓ arrow-exit does.
206
212
  - **Single-question fast path** — a lone single-select question submits immediately on Enter: picking an option or committing typed free text both submit right away (a question without options is answered by typing alone). A lone multiSelect question instead gets a `⏎ Confirm answers` row so you can pick several options before submitting.
@@ -441,6 +447,37 @@ dsh --profile tui # or: dsh-tui-pi (bin shim)
441
447
 
442
448
  ---
443
449
 
450
+ ## Companion plugins (optional)
451
+
452
+ - **[@aiwayds/dsh-ask-router](https://www.npmjs.com/package/@aiwayds/dsh-ask-router)**
453
+ (ships as a default dependency). Owns the single `ctx.userQuestions`
454
+ provider slot and fans every `ask_user_question` out to the interaction
455
+ surfaces bound to the asking session — first answer wins, losing surfaces
456
+ dismiss automatically. Activate it by listing `@aiwayds/dsh-ask-router`
457
+ in the profile's `bundles` **before any UI bundle**; without it the TUI
458
+ panel simply owns questions by itself.
459
+ - **[@aiwayds/dsh-feishu](https://github.com/fan56/dsh-feishu)** (optional).
460
+ Drives an existing dsh session from Feishu/Lark on the phone: round cards,
461
+ interactive `/resume` picker, and an ask-user **card surface** that joins
462
+ the router's fan-out — ask on the desktop, answer on the phone, or both
463
+ render and the first answer wins. Install into the same profile when you
464
+ want phone-side participation; skip it for desktop-only setups. Never
465
+ install the router into a **web** profile (the upstream web apiproxy
466
+ registers its own provider and does not tolerate duplicates).
467
+
468
+ ### Feishu integration demo
469
+
470
+ The dsh-feishu companion in action — dsh-tui-pi on the desktop and
471
+ Feishu/Lark on the phone driving (and answering for) the same dsh session:
472
+
473
+ https://github.com/user-attachments/assets/177e8839-523b-487e-b3d1-6d725cd8aba5
474
+
475
+ https://github.com/user-attachments/assets/c0d7092f-deda-4443-b75a-2bc93bd30d86
476
+
477
+ Demos courtesy of the [dsh-feishu Demos issue](https://github.com/fan56/dsh-feishu/issues/1).
478
+
479
+ ---
480
+
444
481
  ## Dev
445
482
 
446
483
  ```sh
@@ -0,0 +1,527 @@
1
+ English | [简体中文](README.zh-CN.md)
2
+
3
+ # dsh-tui-pi
4
+
5
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)的 pi 风格终端 UI —— 一套把 dsh 变成 pi 风格编码代理体验的 cordis 插件套件。
6
+
7
+ **兼容性**:针对 dsh `0.1.1-rc.2` 测试。斜杠命令的执行经过一个
8
+ `executeCommand()` 兼容 shim(`src/commands.ts`),在运行时探测
9
+ `dsh-commands` 的 `execute()` 参数个数——同时支持 rc.8 之前的 3 参形式
10
+ `(agent, line, signal)` 与当前的 4 参形式 `(agent, line, images, signal)`
11
+ (自 `0.1.0-rc.8` 起未再变化)。经单元测试与 tmux 真机 e2e 冒烟验证。
12
+
13
+ > 中文说明:本文件为英文 [README.md](README.md) 的简体中文翻译。
14
+
15
+ ## 截图
16
+
17
+ https://github.com/user-attachments/assets/6a7e00bb-1fd0-4bc5-9070-457f1e9fa54d
18
+
19
+ 一段真实 session 的终端录制(MP4,1.5× 速度)——todos、运行中的 subagent、think/tool 面板和 powerline footer 的实际效果。
20
+ ([asciinema 交互播放](https://asciinema.org/a/BE212ZO8x1zEZyZn))
21
+
22
+ ### 布局总览
23
+
24
+ ```
25
+ ┌─────────────────────────────────────────────────────────────────────┐
26
+ │ Transcript(可滚动对话区) │
27
+ │ ┌─────────────────────────────────────────────────────────────┐ │
28
+ │ │ 💭 thinking — reasoning in progress │ │
29
+ │ └─────────────────────────────────────────────────────────────┘ │
30
+ │ ⚙ bash python scripts/demo.py … ✔ bash │
31
+ │ ↳ 生成 2 个 todo, 每个 todo 起一个 10s 的 subagent │
32
+ │ ↳ ⠼ Workhorse 10s 任务 · 1.2k token · 19.0s │
33
+ └─────────────────────────────────────────────────────────────────────┘
34
+ ┌─ ● Todos (0/8) ────────────────────────────────────────────────────┐
35
+ │ ├─ ☑ 调研 dsh-tui-pi 斜杠命令/补全机制 │
36
+ │ ├─ ◐ 调研 harness ctx.skills API │
37
+ │ └─ ☐ 实现 /skill:<name> 补全并触发 skill │
38
+ └─────────────────────────────────────────────────────────────────────┘
39
+ ∴ working… │
40
+ ~/github (Full access) │ ⎇ main │
41
+ [ 请输入指令… ] │
42
+ ↳ 第一 打slash 命令的时候 显示 /skill:<skill name> 选择后使用 │
43
+ ↳ ⠼ 牛马狗 · 1.5m/1m · 635.7s │
44
+ dsh ▸ volc-ark-plan ▸ deepseek-v4-flash ▸ high ▸ 48.7k/1.0M(4.6%) │
45
+ ▸ ⚡ CH85.4% ▸ 15 msgs ▸ 11 tools 00:02:13 │
46
+ Esc ×2: stop · Ctrl+C ×2: quit · Ctrl+G: subagents · ↑↓: history │
47
+ └─────────────────────────────────────────────────────────────────────┘
48
+ │ │ │
49
+ │ │ └─ Footer(powerline 状态栏)
50
+ │ └─ 运行中的 subagent(last-request 区域)
51
+ └─ Todos 面板(有边框,固定在编辑器上方)
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 功能特性
57
+
58
+ ### Footer 状态栏
59
+
60
+ 固定在屏幕底部的 powerline 风格状态栏,一眼看清会话的实时状态:
61
+
62
+ ```
63
+ dsh ▸ volc-ark-plan ▸ deepseek-v4-flash ▸ high ▸ 48.7k/1.0M(4.6%) ▸ ⚡ CH85.4% ▸ 15 msgs ▸ 11 tools 00:02:13
64
+ ```
65
+
66
+ 七个分段全部从 O(1) 维护的计数器读取(从不重新扫描 session 日志):
67
+
68
+ | 分段 | 内容 |
69
+ |---|---|
70
+ | **Provider** | 当前 `provider/model` 路由 |
71
+ | **Model** | 模型简称 |
72
+ | **Thinking** | 推理强度等级(`off` / `high` / `max`) |
73
+ | **Context** | `已用 / 上限 (百分比%)` |
74
+ | **Cache-hit** | `CHxx%` —— prompt 缓存命中率 |
75
+ | **Messages** | user + assistant 消息总数 |
76
+ | **Tools** | 工具调用总数 |
77
+ | **Clock** | 右对齐实时 HH:MM:SS(每秒刷新) |
78
+
79
+ 分段用 [U+E0B0](https://www.nerdfonts.com/cheat-sheet) powerline 箭头渲染;配色随当前主题热切换。
80
+
81
+ 编辑器顶部边框显示工作目录和 git 分支:
82
+
83
+ ```
84
+ ~/github (Full access) │ ⎇ main
85
+ ```
86
+
87
+ ---
88
+
89
+ ### Think 与 Tool 面板
90
+
91
+ 进行中的思考和工具调用渲染为**固定面板,钉在聊天输入框上方**(永远不会出现在可滚动的 transcript 里):
92
+
93
+ ```
94
+ ┌─ 💭 thinking ──────────────────────────────────────────────┐
95
+ │ Actually, I can check list_agents or wait… │
96
+ └────────────────────────────────────────────────────────────┘
97
+ ⚙ bash python scripts/demo.py … ✔ bash
98
+ ```
99
+
100
+ 行为要点:
101
+
102
+ - **每种类型只有一个面板** —— 整个运行期间只有一个 `ThinkPanel` 和一个 `ToolPanel`;每个事件原地刷新面板,不会刷屏 transcript。
103
+ - **空 = 隐藏** —— 无活动时面板渲染 0 行并消失。
104
+ - **`dsh-tui.panelHeight`**(默认 `1`):一行无边框(块 id + 耗时 + 最后一行内容,右截断);`5`/`7`/`10` 渲染带边框面板;`all` 输出完整内容。
105
+ - **委派类工具**(`use_agent`、`subagent`、`workflow`、`ralph`)不打开工具面板——它们的子任务以运行中 agent 行的形式显示(见 Subagents)。
106
+
107
+ ---
108
+
109
+ ### Subagents 子代理
110
+
111
+ 运行中的 subagent 活动显示在**编辑器下方的 last-request 区域**,每个子代理一行的紧凑状态:
112
+
113
+ ```
114
+ ↳ 创建 2 个 todo, 每个 todo 起一个 10s 的 subagent
115
+ ↳ ⠼ Subagent A 10s 任务 · 1.2k token · 19.0s
116
+ ↳ ⠼ Subagent B 10s 任务 · 562 token · 6.0s
117
+ ```
118
+
119
+ 每行显示:spinner + agent **名称**、重试次数(`↻N≤M`)、紧凑的**当前上下文占用**(`X/Y` —— 该子代理最近一次请求的 billed input+output 加上其后消息的 CJK 估算,除以它的上下文窗口;**不是**只增不减的累计 token 消耗)、rounds(`round N/M` —— assistant 消息数对上限,仅当 `maxRounds > 0` 时显示 `/M`)、耗时,以及策略注入(maxRounds 收尾、steer)到达子代理后的 `⚡` 标记。不显示 provider,无边框,无标题——每个运行中的子代理就一行。
120
+
121
+ **spawn 派生**与 **fork 派生**两类子代理都会被追踪——dsh 通过 `childSessionMeta` 同时写入 `origin: 'subagent'` 和 `delegationDepth` 预算,头部识别对两种标记都认得(只有预算没有 origin 的头部作为防御性兜底也会被接纳,标记为 `fork <id8>`;当前的 dsh 不会产生这种形态)。非子代理会话按**值**而非字段有无被挡在板外:jsonl 持久化后端在每条恢复出来的头部上都会物化 `delegationDepth: 0`,所以闸门要求预算严格 `> 0`。面向用户的 session fork(fork 出来的*对话*:`Session.fork` 只设 `parentSession` + `seedLength`,不带预算)刻意不进子代理面板,仍可通过 `/resume` 恢复——`/resume` 的过滤器(`isResumableSessionHeader`)恰好排除被委派的子代理(`origin: 'subagent'` 或预算 > 0)。
122
+
123
+ #### Todos 待办面板
124
+
125
+ `● Todos (done/total)` 树是一个有边框的面板,**固定在聊天输入框上方**(不随 transcript 滚动):
126
+
127
+ ```
128
+ ┌─ ● Todos (0/8) ──────────────────────────────────────────┐
129
+ │ ├─ ☑ Todo 1: research subagent spawn API │
130
+ │ ├─ ◐ Todo 2: implement /skill:<name> autocomplete │
131
+ │ └─ ☐ Todo 3: add settings panel skills branch │
132
+ └───────────────────────────────────────────────────────────┘
133
+ ```
134
+
135
+ 图标:`☑` 已完成,`◐` 进行中,`☐` 待处理。结束的子代理从列表消失;都为空时,todo 面板与 agent 行一并折叠为 0 行。
136
+
137
+ #### 查看器与限制
138
+
139
+ `Ctrl+G`(或 `/subagents`)打开一个 80% 宽度的选择器,列出被追踪的子代理——运行中的排前面,然后是最近结束的 5 个。Enter 打开实时 transcript 查看器(约 3×/s 刷新,tail-follow 自动跟随)。
140
+
141
+ **Steering(中途转向)**:在 transcript 查看器里按 `Enter` 打开多行 steer 输入框(`Enter` 发送 · `Shift+Enter` 换行 · `Esc` 取消)。消息作为插件来源的 user 消息投递,并按子代理的实时状态路由:运行中的子代理在它的下一个 step 边界收到注入(`steer`);空闲但尚未结束的子代理把它排队为自己的下一个 follow-up turn;已经结束的子代理不再弹出输入框——查看器改为显示 "This subagent has ended — steering unavailable"。发送失败保留草稿并给出内联错误以便重试;发送成功回到 transcript 并显示一条短提示。这些查看器按键是硬编码的,不能通过 keybindings.json 重映射。
142
+
143
+ 两个上限项(通过 `/agents` → `l` 配置):
144
+
145
+ - **`maxAgents`**(默认 4,`0` = 无限制)—— 达到上限时拒绝新的 spawn。
146
+ - **`maxRounds`**(默认 75,`0` = 无限制)—— 子代理的 assistant 消息数(每次 LLM 往返计一条,即"rounds")达到上限后,TUI 注入一条收尾指令且从不强制终止:运行中的子代理在其下一个 step 边界收到(`steer` —— 即下一次 LLM 往返),空闲的子代理作为自己的下一个 turn 收到。注入是可见的:紧凑行、Ctrl+G 选择器行和查看器头部都会显示 `⚡` 标记,transcript 把注入的消息渲染为 `⚡ <文本>`——这样就能区分子代理 LLM 无视了收尾指令与注入从未发生这两种情况。
147
+
148
+ ---
149
+
150
+ ### DCP(Dynamic Context Pruning 动态上下文裁剪)
151
+
152
+ [DCP](https://github.com/fan56/dsh-dcp) 是 dsh 的独立零 LLM 压缩(compaction)插件——自动修剪上下文以保持在限制内,无需调用 LLM 做摘要。
153
+
154
+ `dsh-tui-pi` 把 `@aiwayds/dsh-dcp` 列为依赖,但**并不挂载它**——dsh-dcp 自带 `cordis.patch.yml`(自 `@aiwayds/dsh-dcp@0.2.0` 起)。启用方式:
155
+
156
+ ```sh
157
+ dsh plugin --profile tui add @aiwayds/dsh-dcp
158
+ ```
159
+
160
+ 挂载后 DCP 在后台透明运行。footer 的 **Context** 分段计算的是当前占用——最近一次请求的 billed context 加上其后消息的 CJK 估算——所以压缩之后下一次请求会变小,显示随之回落(百分比封顶 100,窗口是硬上限)。**Cache-hit** 分段反映当前 provider/model 路由的缓存复用率——命中率按路由分段分别计算,provider 或 model 变化时归零(在下一条 billed 消息到来前隐藏)。
161
+
162
+ 在 subagent 内部,已提交的压缩同样可见:DCP 在子代理自己的日志里为每次压缩追加一行 `user/message` **notice**,Ctrl+G 的 transcript 用 `🧹` 标记渲染它(区别于通用的 `ⓘ`),选择器行的描述里带有该子代理的压缩次数(描述中的 `🧹 N×`)。DCP 的 `roundInterval` 与 TUI 的 `maxRounds` 数的是**同一样东西**——`assistant/message` 事件,每次 LLM 往返计一条——但行为不同:子代理计数达到 `maxRounds` 时 TUI 排队发一个收尾请求;会话计数达到 `roundInterval` 后 DCP 在下一个空闲边界执行压缩(修剪上下文)。一个触发工作,一个释放上下文。
163
+
164
+ ---
165
+
166
+ ### APPEND_SYSTEM.md
167
+
168
+ 一份用户可编辑的 markdown 文件,其内容会追加到**本 TUI 创建的主 agent 的 system prompt 末尾**——借鉴 pi 的 `~/.pi/agent/APPEND_SYSTEM.md` 约定,dsh 侧对应 `$DSH_HOME/APPEND_SYSTEM.md`(默认 `~/.dsh/APPEND_SYSTEM.md`,沿用 dsh 其余部分共用的 `$DSH_HOME` 覆盖机制)。
169
+
170
+ - **热应用** —— section 提供者在每次组装 prompt 时读盘,改完文件**下一次请求**即生效:无需重启、无需 watcher、无需 `/reload`。
171
+ - **首次运行自动播种** —— 文件不存在时,TUI 启动时一次性从随包模板 `templates/APPEND_SYSTEM.md` 创建(英文版 orchestrator 身份模板:身份、核心规则、执行工作流——含「subagent 仅指已注册 subagents」的用语规则)。已有文件归用户所有——TUI 永远不会覆盖用户内容;只在文件尚未包含带标记的 todo-lifecycle section 时追加该段,以及(按短语匹配、幂等地)在尚未出现 subagents 规则措辞时追加之。
172
+ - **TUI 自有 section** —— 一个带标记的 block(`<!-- dsh-tui-pi:todo-lifecycle -->`)只追加一次,之后幂等维护,确保模型在所有条目完成时清空自己的 `todo/write` 列表。已带标记的文件后续启动保持逐字节不变。
173
+ - **旧版迁移** —— 同一段 todo block 过去是通过 `~/.dsh/AGENTS.md` 下发的。启动时 TUI 一次性把它剥掉(不存在时 no-op),避免重复下发。
174
+ - **空 / 读不到 = 无 section** —— 文件缺失或读不了时该 section 被静默丢弃。无报错,不影响 TUI 启动。
175
+
176
+ #### 作用范围:仅主 agent
177
+
178
+ 该 section 注册在主 agent **带作用域**的 agent context 上(`src/session.ts` 的 `installAppendSystem`)——落在该 agent 自己的 prompt-scope 层,subagent 的 scope 不会合并它。orchestrator 身份(「调度子代理、不要自己执行」)若下发到子代理身上会自废武功,所以子代理完全看不到这个文件。机制与 `dsh-subagent-registry` 给每个子代理设置 persona 时相同。
179
+
180
+ #### 示例
181
+
182
+ ```sh
183
+ # 首次启动从 templates/APPEND_SYSTEM.md 自动播种 —— 打开直接编辑即可。
184
+ $EDITOR ~/.dsh/APPEND_SYSTEM.md
185
+
186
+ # 或者完全替换为你自己的版本(TUI 仍会保留它的带标记
187
+ # todo-lifecycle section —— 缺失时会重新追加)。
188
+ cat > ~/.dsh/APPEND_SYSTEM.md <<'EOF'
189
+ # Project ground rules
190
+
191
+ - Always run `pnpm test` before claiming a task is done.
192
+ - Prefer dispatching `workhorse` for multi-step investigations.
193
+ EOF
194
+ ```
195
+
196
+ 没有开关此功能的斜杠命令——它始终开启,完全由文件内容控制。
197
+
198
+ ---
199
+
200
+ ### Ask User Question(向用户提问)
201
+
202
+ 模型在一轮回答中途可以暂停下来,通过 `ask_user_question` 工具向你提出结构化问题(`@deepseek-ai/dsh-tool-ask-user`,由本 profile 的 bundle patch 挂载)。TUI 承载应答侧:一个有边框的面板钉在聊天输入框正上方(Todos 面板的槽位——不是浮动的 popup),打开期间接管键盘,工具调用保持 pending 直到你作答,你的答案作为普通的 tool result 流回模型。
203
+
204
+ 看看 ask-user-question 流程的实际效果:
205
+
206
+ https://github.com/user-attachments/assets/aa36be36-a508-4f53-ba85-efe0394dab11
207
+
208
+ - **一次一个问题,其余收进标签页** —— 有多个问题时,面板恰好只显示一个问题的 block(标题行 + 辅助 `detail` 文本、选项行,外加自由输入的 `Type something.` sentinel 行);标题下方的标签条(`[1] · 2✓ · 3` —— 方括号标出焦点 tab,✓ 表示已答)把其他问题折叠起来。`←`/`→`(以及 Tab/Shift-Tab)切换标签;单选 tab 作答后自动跳到下一个未答问题(全部答完后跳到 Confirm 行)。单选在 Enter 时替换选中项;多选为切换(`●`/`○` 标记)且从不自动跳转。
209
+ - **Ctrl+T 把面板折叠成 3 行小条** —— 你思考问题时,questions 面板可能挡住叠在其下的 transcript;Ctrl+T 把它折叠成边框 + 一行摘要(阶段、tab 位置、已答数量、如何展开),同一按键再次展开。折叠期间只有切换键和 Esc 链生效;编辑中途折叠会像 ↑↓ 离开一样提交缓冲区。
210
+ - **单问题快速通道** —— 孤立的单选问题在 Enter 时立即提交:选选项或输入自由文本回车都会立刻提交(无选项的问题靠打字即可作答)。孤立的多选问题则会得到一个 `⏎ Confirm answers` 行,让你先勾选多个选项再提交。
211
+ - **多问题确认页** —— ≥ 2 个问题时出现 `⏎ Confirm answers` 行,跳转到列出全部答案的 review 页,每一行都可原位编辑(跳回去会把焦点切回那个问题的 tab);`Submit answers` 提交(有答案缺失时在其上按 Enter 会闪烁提示而不是静默失败)。
212
+ - **双击 Esc 表示拒绝作答** —— 200ms 内两次 Esc 返回 declined envelope(模型读到的是一条正常的回复,表示未给出答案);长按不会误触发(低于最小间隔的按键自动重复被忽略);工具调用被中止时也按 declined 结算。面板打开期间像打开的 overlay 一样独占键盘:Esc 永远不会进入运行任务的停止链,app 快捷键(Ctrl+L/G/O、Tab)也让位于面板。
213
+ - **节制使用引导** —— 一段 system-prompt 引导模型只在真正需要你时才提问(1–3 个问题,每个 2–4 个选项),避免 TUI 变成问卷调查。
214
+ - **键盘操作** —— `←→` 切换问题标签 · `↑↓` 导航 · `Enter` 选择/勾选/确认 · 在 sentinel 行打字输入自由文本 · `Ctrl+T` 折叠/展开面板 · 连按两次 `Esc` 拒绝作答。
215
+
216
+ 灵感来自 [juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rpiv-ask-user-question)。
217
+
218
+ ---
219
+
220
+ ## 斜杠命令
221
+
222
+ | 命令 | 功能 |
223
+ |---|---|
224
+ | `/model` | 两阶段 provider/model 选择器(然后选 thinking level)。实时切换并持久化;面板内按键:`f` 收藏 · `h` 隐藏 · `/` 过滤(收藏/隐藏经 settings 持久化)。 |
225
+ | `/think` | 当前模型的推理强度选择器(`Off`/`High`/`Max`)。 |
226
+ | `/session` | 只读信息面板:id、cwd、model、token 用量、事件计数。 |
227
+ | `/resume` | 选择一个持久化的 session,校验其日志后恢复。按最后更新时间排序(日志文件 mtime),新的在上;`Updated` 列显示生效时间。 |
228
+ | `/new` | 分离当前 session;下一次输入开启新 session。 |
229
+ | `/settings` | 文本式设置浏览器(命名空间、schema 遍历、内联编辑器、密钥脱敏)。 |
230
+ | `/export` | 将当前 session 日志导出为 JSONL(`~/Downloads/dsh-session-<id>.jsonl`)。 |
231
+ | `/permission` | 权限预设选择器(read-only / workspace-write / danger-full-access)。 |
232
+ | `/theme` | 配色方案选择器(`auto` / `light` / `dark`),即时生效。 |
233
+ | `/preset` | agent preset 选择器;`<name>` 直接切换,`next` 向前循环(同 `Tab`)。 |
234
+ | `/agents` | 管理 agent markdown 文件 + subagent 上限(`maxAgents`、`maxRounds`)。 |
235
+ | `/subagents` | 选择运行中/最近的 subagent 并观看其实时 transcript;在查看器内按 `Enter` 可对子代理 steer(见 Subagents)。 |
236
+ | `/reload` | 从源码热重载插件(`pnpm build` 后执行),无需重启 dsh。 |
237
+ | `/login` | 登录 provider:从目录选择(或 `/login openai` 直达),输入一个 API key。**Custom provider…** 条目(`/login custom`)打开六字段表单,接入任何 pi-ai 未内置的 OpenAI/Anthropic 兼容网关——route id、显示名、协议、base URL、模型列表、API key——写出与 Web Models 页组合出的同一种 hand-declared 路由。 |
238
+ | `/logout` | 选择一个已登录的 provider,同时删除存储的 key 和它的 provider profile。 |
239
+ | `/hotkeys` | 快捷键浏览器和实时编辑器。 |
240
+
241
+ 凡不能解析为已注册命令的内容,都作为普通 prompt 落给模型。
242
+
243
+ ---
244
+
245
+ ## 键盘快捷键
246
+
247
+ | 按键 | 功能 |
248
+ |---|---|
249
+ | `Enter` | 发送 prompt |
250
+ | `Esc` | **双击停止** —— 单击进入待发状态(500ms 窗口);popup 打开时改为关闭 popup;空闲(无运行中任务)时不做任何操作 |
251
+ | `Ctrl+C` | 对话中:第一次取消当前轮次,第二次退出。空闲时:清空编辑器 / 退出。**长按自动重复绝不会触发退出。** |
252
+ | `Ctrl+D` | 退出(仅在编辑器为空时) |
253
+ | `Ctrl+L` | 打开 model/think 选择器 |
254
+ | `Ctrl+G` | 打开 subagent 选择器(有子代理运行时);在 transcript 查看器内按 `Enter` 打开 steer 输入框(查看器按键为硬编码) |
255
+ | `Tab` | 循环切换 agent preset(footer 品牌段显示当前预设 `dsh(<name>)`) |
256
+ | `↑` / `↓` | 浏览已发送消息历史(shell 风格,500 条) |
257
+
258
+ ### 自定义快捷键
259
+
260
+ 通过 `~/.dsh/keybindings.json` 重映射任意 app 按键——一个部分 JSON 映射表,键为 app 按键、值为按键 id(`ctrl+letter`、`alt+letter`、命名键)。可手动编辑,或用 `/hotkeys` 交互式修改(实时生效,无需重启)。
261
+
262
+ ---
263
+
264
+ ## Agent presets
265
+
266
+ 部署提供了 `standard` agent preset 时,TUI 启动即选中它;否则选中扫描到的第一项。这只是本地选择:在你操作 `/preset` 或按 `Tab` 之前,创建 session 时不会发送任何 `meta.agentPreset`,因此仍由服务端默认值(`agent-presets.default`)决定。footer 品牌段反映本地选择(`dsh(<name>)`);一次切换在下一个空白 session 生效。
267
+
268
+ ---
269
+
270
+ ## 主题
271
+
272
+ GitHub light / GitHub dark 配色,运行时热切换:
273
+
274
+ - `/theme` —— 实时选择器;整个屏幕重绘(含背景)。
275
+ - `DSH_TUI_THEME=light|dark` —— 环境变量钉选,优先于偏好设置。
276
+ - `DSH_TUI_TRANSPARENT=1` —— 透明画布(终端背景透出)。
277
+ - `DSH_TUI_MOUSE=buttons|all|off` —— 终端鼠标追踪模式(默认 `buttons`:点击/滚轮/拖选继续可用,空闲指针移动不上报;`all` = pi-tui 的全动作追踪,cmux 下其事件突发可能漏进编辑器;`off` = 关闭鼠标)。
278
+ - `auto` 模式检测终端并跟随实时的明暗切换。
279
+
280
+ 全屏画布背景随包内置——写流装饰器(`src/canvas-terminal.ts`)用主题色经 BCE 给每条擦除序列上色,无需补丁依赖。
281
+
282
+ ---
283
+
284
+ ## Session 管理
285
+
286
+ 两组配置项共同管理 session 存储,都位于 `dsh-tui` settings 命名空间下
287
+ (`~/.dsh/settings.yaml`),各配一个环境变量逃生口:
288
+
289
+ ```yaml
290
+ dsh-tui:
291
+ # ~/.dsh/sessions 的启动清理器 —— 会删除窗口之外的整目录 session 日志。
292
+ # 每进程启动时执行一次。
293
+ retention:
294
+ maxCount: 100 # 最多保留这么多 session;<= 0 关闭清理器
295
+ maxAgeDays: 7 # 删除超过这么多天未动的日志(> 0)
296
+ minIdleHours: 24 # 仅作用于条数规则的空闲保护(小时,>= 0)
297
+
298
+ # /resume 显示过滤器 —— 只隐藏选择器行,从不删除。
299
+ # 每次打开选择器时重新解析(改设置对下一次 /resume 生效,
300
+ # 无需重启)。
301
+ resume:
302
+ maxAgeDays: 7 # 只显示日志活动在此窗口内的 session(> 0)
303
+ minBytes: 20480 # 一行的最小压缩后日志大小(>= 0)
304
+ ```
305
+
306
+ 每个字段的优先级:settings.yaml 里的显式值 > `DSH_TUI_RETENTION_MAX_COUNT`
307
+ / `DSH_TUI_RETENTION_MAX_AGE_DAYS` / `DSH_TUI_RETENTION_MIN_IDLE_HOURS`
308
+ 与 `DSH_TUI_RESUME_MAX_AGE_DAYS` / `DSH_TUI_RESUME_MIN_BYTES`
309
+ 环境变量 > 上述默认值。非法的 settings 值经由共享 notice bridge 弹出一条瞬态提示
310
+ (没有注册 TUI sink 时被静默丢弃——headless 运行永不打印)并回落到下一层;
311
+ 非法的环境变量值则静默回落——一个 typo 既不会扩大也不会架空策略。`maxCount`
312
+ 与 `minBytes` 在每一层都必须是整数(小数的上限或字节门槛是垃圾数据,
313
+ 不是窗口)。
314
+
315
+ **完全关闭 retention** —— 对于要 read-attach 旧 session 的常驻进程
316
+ (远程 bridge、headless cron 运行),默认窗口会把它们裁掉:
317
+
318
+ ```yaml
319
+ dsh-tui:
320
+ retention:
321
+ maxCount: 0 # 或:DSH_TUI_RETENTION_MAX_COUNT=0
322
+ ```
323
+
324
+ 时机:**retention 只在启动时跑一次**(从不在会话中途;`/reload`
325
+ 不会重跑它——下一次冷启动才会),而 **resume 过滤器在每次 `/resume`
326
+ 打开时生效**。两处 `7` 默认出自同一个「一周即工作集」决策,
327
+ 但服务对象不同——retention 删除日志,resume 过滤器只隐藏行。
328
+
329
+ ---
330
+
331
+ ## 字体
332
+
333
+ TUI 唯一的私有区(PUA)字形是 footer 的 powerline 分隔符(U+E0B0)——
334
+ 没有哪个默认终端字体自带它,没装 Nerd/Powerline 字体的终端会显示豆腐块。
335
+ `dsh-tui.iconSet` 设置(`auto` | `nerdfont` | `plain`,默认 `auto`)让危险字形
336
+ (U+E0B0、⏹、⭘)自适应终端:
337
+
338
+ - `auto` —— 启动时探测到 Nerd/Powerline 字体就用 powerline 字形,
339
+ 否则用安全的 Unicode 替代(`▸ ■ ●`)。
340
+ - `nerdfont` —— 始终用 powerline 字形(你已经设好字体了)。
341
+ - `plain` —— 始终用安全替代,无需任何字体。
342
+
343
+ **一键安装内置字体**(安装 + 把终端指过去,保留你的字号):
344
+
345
+ ```sh
346
+ node scripts/install-font.mjs
347
+ ```
348
+
349
+ 脚本把 `assets/fonts/dsh-tui-pi-nerd.ttf`(约 170KB 的子集:ASCII +
350
+ U+E0B0 + TUI 渲染的每一个符号)拷进用户字体目录,并尽力翻转终端设置:
351
+ macOS iTerm2(PlistBuddy,默认 bookmark)、Linux GNOME Terminal
352
+ (gsettings)以及 kitty/alacritty/wezterm(改配置文件,先备份)。
353
+ Terminal.app 被刻意跳过(它的字体是二进制 blob)——请手动设置。
354
+ 每一步都有防护:失败只记录警告并继续,绝不破坏性地改动你的配置。
355
+
356
+ **或者手动设置终端字体** —— 任意 Nerd Font 家族设为终端主字体即可
357
+ (如 JetBrainsMono Nerd Font、Hack Nerd Font,或安装后的内置
358
+ `DSH TUI Nerd`):iTerm2 → Settings → Profiles → Text → Font;
359
+ Terminal.app → Settings → Profiles → Text;kitty → `font_family`;
360
+ alacritty → `[font] family`;wezterm → `wezterm.font("…")`。
361
+ 下次启动时 `auto` 就会解析成 powerline 字形。
362
+
363
+ ---
364
+
365
+ ## 安装(本地)
366
+
367
+ `tui` profile 从 npm registry 安装本插件——profile 的 `package.json`
368
+ 钉住 `"@aiwayds/dsh-tui-pi": "<version>"`,由 pnpm 像普通依赖一样解析。
369
+ 发版后升级 profile:
370
+
371
+ ```sh
372
+ node scripts/dev-upgrade.mjs # 最新版
373
+ node scripts/dev-upgrade.mjs 0.15.1 --dry-run # 先预览执行计划
374
+ ```
375
+
376
+ 脚本先在 registry 校验版本存在,然后只更新
377
+ `~/.dsh/profiles/tui/package.json` 里的 `"@aiwayds/dsh-tui-pi"` 一个键
378
+ (保格式的 read-modify-write),在该目录执行 `pnpm install`,最后校验
379
+ 安装副本报告的版本与目标一致。绝不碰 `~/.dsh/settings.yaml` 或
380
+ `.credentials.yaml`。重启 dsh(或在 TUI 内 `/reload`)加载新副本。
381
+
382
+ ## 安装(npm)
383
+
384
+ 在全新 profile 里安装完整的 dsh 插件套件:
385
+
386
+ ```sh
387
+ dsh plugin --profile tui add @aiwayds/dsh-tui-pi
388
+ dsh plugin --profile tui add @aiwayds/dsh-subagent-registry
389
+ dsh plugin --profile tui add @aiwayds/dsh-dcp
390
+ ```
391
+
392
+ 然后启动:
393
+
394
+ ```sh
395
+ dsh --profile tui
396
+ ```
397
+
398
+ **自动发生的事:**
399
+
400
+ - dsh 通过 `reconcilePlugins` 把三个插件注册进 `dsh.profile.bundles`。
401
+ - dsh 在 profile 的 `pnpm-workspace.yaml` 里设置 `autoInstallPeers: false`。
402
+ - 首次启动时 dsh 调用 `healProfilesModuleFallback`,在
403
+ `~/.dsh/profiles/node_modules/@deepseek-ai/*` 下创建软链指向全局 dsh
404
+ 闭包(`$(which dsh)/../../node_modules/@deepseek-ai`)。这让所有插件共享
405
+ 同一个 `@deepseek-ai/cordis` 实例——无需手动搭建闭包。
406
+ - `compaction-basic` 被 `@aiwayds/dsh-dcp` 的补丁禁用;dsh-dcp 接管成为
407
+ compaction 后端。
408
+
409
+ **不会自动发生的事:**
410
+
411
+ - 不再有补丁相关的事:自 0.8.0 起,仓库和 npm 包运行同一个原版
412
+ `@earendil-works/pi-tui`——画布背景由我们自己的写流装饰器(BCE)
413
+ 绘制,随包内置,消费方 profile 不需要任何 `pnpm-workspace.yaml` 条目。
414
+
415
+ ### 故障排查
416
+
417
+ | 症状 | 原因 | 修复 |
418
+ |---|---|---|
419
+ | `Cannot find package '<name>' imported from ~/.dsh/profiles/...` | 某 bundle 的 `cordis.patch.yml` 的 `name` 字段与 scoped 包名不匹配。 | 更新插件;所有 `@aiwayds/*` 插件的补丁现在都用 `name: '@aiwayds/<pkg>'`。 |
420
+ | npm 安装的 dsh 报 `Cannot find package '@deepseek-ai/dsh-client-schema-form'` | npm 分发的 dsh 闭包缺这个包(上游打包缺口——[deepseek-harness discussion #3471](https://github.com/deepseek-ai/deepseek-harness/discussions/3471))。 | 本插件自 0.8.1 起已修(辅助函数 vendored,不再 import 缺失的包)。需要它的其他插件:`cd ~/.dsh/profiles/<profile> && pnpm add @deepseek-ai/dsh-client-schema-form@next`。 |
421
+ | `Cannot read properties of undefined (reading 'prepare')` | 出现重复的 `@deepseek-ai/cordis` 模块实例(profile 树里有两份物理副本)。 | 见 AGENTS.md 铁律 8。删除物理副本 `~/.dsh/profiles/tui/node_modules/@deepseek-ai` 并让 dsh heal 兜底:`rm -rf ~/.dsh/profiles/tui/node_modules/@deepseek-ai && dsh --profile tui`(heal 会重建为软链)。 |
422
+ | pnpm 提示 `Peer dependencies that should be installed: @deepseek-ai/...` | 某插件把 `@deepseek-ai/*` 放进了普通 `dependencies` 而非 `peerDependencies`。 | 更新插件(所有 `@aiwayds/*` dsh 插件都用 optional peerDeps)。警告无害——pnpm 不会自动安装 optional peers。 |
423
+ | pnpm 提示 `Ignored build scripts: @aiwayds/dsh-tui-pi@...` | pnpm 10 默认阻止 build 脚本,tui-pi 的 postinstall(`link-dsh-closure.mjs`)被跳过。 | 这是预期且**无害**的——postinstall 只影响仓库开发流,不影响 npm 消费者。闭包链接由 dsh 的 `healProfilesModuleFallback` 处理。 |
424
+
425
+ ---
426
+
427
+ ## 使用
428
+
429
+ ```sh
430
+ dsh --profile tui # 或:dsh-tui-pi(bin shim)
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Companion plugins(可选)
436
+
437
+ - **[@aiwayds/dsh-ask-router](https://www.npmjs.com/package/@aiwayds/dsh-ask-router)**
438
+ (作为默认依赖附带)。独占唯一的 `ctx.userQuestions` provider 槽位,
439
+ 把每个 `ask_user_question` 扇出到绑定到提问 session 的各个交互面——
440
+ 第一个答案获胜,落败的交互面自动关闭。激活方式是在 profile 的 `bundles`
441
+ 里把 `@aiwayds/dsh-ask-router` 列在**任何 UI bundle 之前**;没有它时
442
+ TUI 面板独自接管问题。
443
+ - **[@aiwayds/dsh-feishu](https://github.com/fan56/dsh-feishu)**(可选)。
444
+ 用手机上的飞书/Lark 驱动已有的 dsh session:轮次卡片、交互式 `/resume`
445
+ 选择器,以及加入 router 扇出的 ask-user **卡片面**——桌面上提问,
446
+ 手机上作答,或两边同时呈现而第一个答案获胜。想要手机侧参与时装进同一
447
+ profile;纯桌面环境可跳过。绝不把 router 装进 **web** profile
448
+ (上游 web apiproxy 注册自己的 provider,不容忍重复)。
449
+
450
+ ### 飞书集成演示
451
+
452
+ dsh-feishu companion 实战——桌面上的 dsh-tui-pi 与手机上的飞书/Lark
453
+ 驱动(并代答)同一个 dsh session:
454
+
455
+ https://github.com/user-attachments/assets/177e8839-523b-487e-b3d1-6d725cd8aba5
456
+
457
+ https://github.com/user-attachments/assets/c0d7092f-deda-4443-b75a-2bc93bd30d86
458
+
459
+ 演示来自 [dsh-feishu Demos issue](https://github.com/fan56/dsh-feishu/issues/1)。
460
+
461
+ ---
462
+
463
+ ## 开发
464
+
465
+ ```sh
466
+ pnpm check # tsc --noEmit
467
+ pnpm build # 输出 lib/
468
+ pnpm test # 单元测试,node --test 对 lib/ 执行(757 个测试,pretest 构建)
469
+ ```
470
+
471
+ 本地类型检查通过 symlink 把 `node_modules/@deepseek-ai/*` 指向已安装的
472
+ dsh 闭包(`/opt/homebrew/lib/node_modules/@deepseek-ai/dsh/node_modules`);
473
+ 这些 symlink 不会打入任何 tarball。`scripts/link-dsh-closure.mjs`
474
+ (包的 `postinstall`)在每次 `pnpm install` 后重建所有链接。
475
+
476
+ **pi-tui**:npm 上的原版 `@earendil-works/pi-tui` 0.84.2——无补丁、
477
+ 无 fork。全屏画布背景由我们自己的写流装饰器实现
478
+ (`src/canvas-terminal.ts`,BCE)。
479
+
480
+ ---
481
+
482
+ ## 目录结构
483
+
484
+ ```
485
+ bin/dsh-tui-pi launcher shim(exec dsh --profile tui)
486
+ cordis.patch.yml bundle patch:将插件挂载为 `tui-pi`
487
+ src/
488
+ index.ts cordis plugin 入口:命令注册、footer、
489
+ git watcher、时钟、bridge、主题热切换、shutdown
490
+ tui.ts alt-screen 树、transcript ScrollView、dock、canvas 背景
491
+ session.ts DshSessionBridge:agent 创建、followup、resume、
492
+ O(1) 增量统计、subagent tracker
493
+ live-widgets.ts Todos 面板 + 运行中 agent 活动行
494
+ messages.ts TranscriptRenderer:session 事件 → pi-tui 组件、
495
+ 流式 setText、高度可配置面板
496
+ footer.ts PowerlineFooter(7 分段 + 时钟)
497
+ editor.ts CwdBorderEditor(顶部边框:cwd + git 分支)
498
+ subagent-policy.ts maxAgents 守卫 + maxRounds 收尾注入
499
+ (运行中走 steer;⚡ 标记,查看器可见)
500
+ subagent-viewer.ts Ctrl+G 选择器 + 实时 transcript 面板 + Enter steer 注入
501
+ ask-user.ts Ask User Question 停靠面板:纯状态 reducer +
502
+ 带边框 overlay UI + ctx.userQuestions provider
503
+ steer-flow.ts Steer / follow-up 决策层:带竞态兜底的路由投递、
504
+ 队列操作(remove / promote)、通知
505
+ route-dialog.ts 提交路由对话框(排队为 follow-up 还是立即 steer):
506
+ 纯 key reducer + 带边框 overlay
507
+ queue-panel.ts Ctrl+O 待发消息队列:d remove · s steer now,
508
+ 实时刷新 overlay
509
+ theme/ GitHub light/dark 配色 + 终端检测
510
+ test/*.test.mjs 单元测试(757 个,覆盖 44 个文件)
511
+ ```
512
+
513
+ ---
514
+
515
+ ## 更新日志
516
+
517
+ 发布历史见 [CHANGELOG.md](CHANGELOG.md)。
518
+
519
+ ---
520
+
521
+ ## 致谢
522
+
523
+ - [Ask User Question(向用户提问)](#ask-user-question向用户提问) 的灵感来自
524
+ [juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rpiv-ask-user-question) ——
525
+ 其交互设计(编号选项列表 + 自由文本 sentinel、多问题 review 页、拒绝手势;
526
+ 后来又重构为一次一个问题的 tab 视图加可折叠小条)被适配到了本 TUI 的
527
+ 停靠面板架构与 dsh `userQuestions` provider 架构上。这里的全部代码均为原创。