@xmoon76/dsh-pi-tui 0.3.3 → 0.3.5

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,494 +1,624 @@
1
1
  # dsh-pi-tui
2
2
 
3
- English | [简体中文](README.zh-CN.md)
3
+ [English](README.en.md) | 简体中文
4
4
 
5
- Release history: [CHANGELOG.md](CHANGELOG.md) · [简体中文更新日志](CHANGELOG.zh-CN.md)
5
+ [![npm](https://img.shields.io/npm/v/@xmoon76/dsh-pi-tui.svg)](https://www.npmjs.com/package/@xmoon76/dsh-pi-tui)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
7
 
7
- A third-party TUI mode for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`), built on a vendored fork of [pi-tui](https://github.com/MoonshotAI/kimi-code/tree/main/packages/pi-tui).
8
+ 基于 Pi TUI 的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 终端前端。
8
9
 
9
- Run `dsh --profile pi-tui` for a terminal UI instead of the browser GUI (`dsh --profile web`) or one-shot mode (`dsh --profile headless`).
10
+ `dsh-pi-tui` 作为独立的 dsh bundle 安装到 profile 中,提供流式对话、工具调用、会话管理、Subagent、历史搜索、Shell、审批与设置等终端交互。模型、工具、Session、权限、Skills、Plan、Goal、Subagent 等运行时能力仍由 DeepSeek Harness 提供。
10
11
 
11
- > **Status: working.** The TUI covers the main session loop — input → session events,
12
- > approvals, commands, session switching and full-text search — plus presets, skills,
13
- > model/settings menus, and slash commands. Rendering and input routing are verified
14
- > by headless tests (`@xterm/headless`) with no TTY or model connection needed.
12
+ ```sh
13
+ dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui
14
+ dsh --profile pi-tui
15
+ ```
16
+
17
+ ![dsh-pi-tui](docs/dsh-pi-tui.png)
18
+
19
+ ## 功能
20
+
21
+ ### 对话与工具
22
+
23
+ * 流式 Markdown 输出
24
+ * Thinking 折叠与展开
25
+ * Tool Call 卡片及运行状态
26
+ * Tool / System 详情折叠
27
+ * Transcript 全文搜索
28
+ * 长会话历史折叠
29
+ * Context、Token、模型和运行状态显示
30
+ * Approval 与 `ask_user_question` 交互
31
+ * Plan Review
32
+ * Todo / Goal 状态展示
33
+ * 可读的终端窗口标题
34
+ * Compaction / prune 后不会出现重复的幽灵 Tool Card
35
+
36
+ `Ctrl+O` 控制工具和系统详情;在全屏 Focus 下它整体展开最近几个 Thought root,或全部收起。`Alt+T` 单独控制 Thinking。
37
+
38
+ ### Focus Mode
39
+
40
+ `/focus` 可以把运行中的 Thinking、Tool Call 和中间回复聚合为一个实时更新的 Thought 区块。
41
+
42
+ 需要查看过程时可以展开,关闭 Focus 后恢复普通 Transcript 展示。全屏 Focus 中可以按 Thought root 批量展开/收起,也可以单独点击卡片;切换或缩放时会保留 viewport。Focus 只影响界面投影,不修改 Session 中保存的事件。
43
+
44
+ ### Session
45
+
46
+ 支持 DSH 持久化 Session,包括:
47
+
48
+ * 新建和恢复 Session
49
+ * Session 切换
50
+ * 重命名
51
+ * Fork
52
+ * Rewind
53
+ * Session lineage
54
+ * Transcript 导出
55
+
56
+ 使用:
57
+
58
+ ```text
59
+ /sessions
60
+ /fork
61
+ /rewind
62
+ ```
63
+
64
+ 空闲且编辑器为空时也可以快速按两次 `Esc` 打开 Rewind。
65
+
66
+ Rewind 会从选中的历史 User Turn 创建新的 Child Session,并把对应 Prompt 放回编辑器。原 Session 不会被修改。
67
+
68
+ ### 输入历史
69
+
70
+ `Ctrl+R` 打开输入历史搜索。
71
+
72
+ 支持三个范围:
73
+
74
+ * Current session
75
+ * Current directory
76
+ * All directories
77
+
78
+ 历史结果包含 Prompt、工作目录、时间和 Session 信息。选中历史后只恢复到编辑器,不会立即发送。
79
+
80
+ 普通的 `↑` / `↓` 仍用于快速浏览最近输入。
81
+
82
+ ### Subagent 与后台任务
83
+
84
+ `/tasks` 提供当前 Session 的任务浏览器。
85
+
86
+ Subagent 按完整 lineage 显示,包括嵌套创建的 descendant:
87
+
88
+ ```text
89
+ main
90
+ ├─ subagent A
91
+ │ └─ subagent B
92
+ └─ subagent C
93
+ ```
94
+
95
+ 浏览器会区分:
15
96
 
16
- ## Screenshot
97
+ * `continuable`
98
+ * `one-shot`
99
+ * running / inactive
100
+ * nested descendant
101
+ * 后台 Job
17
102
 
18
- ![dsh-pi-tui running in a terminal](https://raw.githubusercontent.com/XMoon/dsh-pi-tui/main/docs/dsh-pi-tui.png)
103
+ 已经结束的 one-shot Subagent 仍可以打开并查看持久化 Transcript。
19
104
 
20
- ## Layout
105
+ 对于当前 Session 的直接 `continuable` Child,可以进入交互式 Viewer,并直接向该 Subagent 发送后续消息。Child 使用自己的 Transcript、Draft 和运行状态,不会修改主 Session 的输入。
21
106
 
22
- The full repository layout lives in [AGENTS.md](AGENTS.md) (the contributor
23
- operating manual). In one line: the **repository root is the published
24
- `@xmoon76/dsh-pi-tui` bundle** (the only published package — its manifest
25
- declares `dsh.bundle.patch` and the `exports` point at the built `dist/`),
26
- and `packages/pi-tui/` is the vendored `@moonshot-ai/pi-tui` fork (rescoped
27
- to `@xmoon76/pi-tui`, private, never published — its divergence ledger lives
28
- in `packages/pi-tui/AGENTS.md`), bundled into the root package's build
29
- output.
107
+ 更深层的 nested Subagent 默认以只读方式查看。
30
108
 
31
- ## Prerequisites
109
+ ### Shell
32
110
 
33
- - A DeepSeek Harness installation with profiles support (`dsh` on your `PATH`),
34
- version `0.1.1-rc.1` or later on the same compat line (the TUI consumes the
35
- split credential events and the `ctx.authorization` seam of that release).
36
- - Node >= 22.19 (`^22.19.0 || >=24`, same range as dsh). Running from source
37
- needs Node with native TypeScript support (>= 23.6) or the tsx ESM hook
38
- (`node --import tsx/esm`, how dsh's own source launch works).
39
- - [pnpm](https://pnpm.io) only when installing from source.
111
+ 编辑器支持两种 Shell 模式:
40
112
 
41
- ## Install
113
+ ```text
114
+ ! git status
115
+ ```
116
+
117
+ 执行本地命令,并把输出提交到当前 Session。
118
+
119
+ ```text
120
+ !! git status
121
+ ```
122
+
123
+ 只在本地执行,输出不会进入模型上下文。
124
+
125
+ `!` / `!!` 是独立的编辑器模式,而不是普通文本前缀。进入 Shell 模式后 Prompt 和补全行为会同步切换。
126
+
127
+ Shell 卡片默认只显示有限的输出预览,`Ctrl+O` 可以展开完整保留内容——全屏 Focus 除外:那里 `Ctrl+O` 负责 Thought root 的整体开关,Shell 卡片保持折叠。
128
+
129
+ ### 文件引用与图片
130
+
131
+ 输入 `@` 可以搜索和补全工作区文件:
132
+
133
+ ```text
134
+ @src/index.ts
135
+ @"path with spaces/file.ts"
136
+ ```
137
+
138
+ 能够解析的相对路径会在提交时转换为明确的文件路径。
139
+
140
+ 支持通过 `Ctrl+V` 添加剪贴板图片,并使用 DSH Attachment 能力保存到 Session。
141
+
142
+ ### 模型与运行设置
143
+
144
+ TUI 使用 DSH 提供的模型和设置服务。
145
+
146
+ 常用入口:
42
147
 
43
- `dsh plugin` runs pnpm inside the target profile's directory, so the usual
44
- pnpm verbs (`add`, `remove`, `update`, `list`) all work.
148
+ ```text
149
+ /model
150
+ /settings
151
+ /login
152
+ /permission
153
+ /plan
154
+ /goal
155
+ /compact
156
+ /footer
157
+ /statusline
158
+ ```
159
+
160
+ 模型切换、Reasoning Effort、权限 Preset、Plan 和 Goal 都沿用 DSH 对应的运行时语义。
161
+
162
+ `/settings` 中的 `Icon style` 可切换 TUI 结构图标的风格:`Emoji`(默认,
163
+ 彩色)、`Symbols`(紧凑的单格终端符号)、`Minimal`(隐藏装饰性图标,只
164
+ 保留状态/交互标记);切换立即生效并持久化。
165
+
166
+ 其他插件注册到 `ctx.commands` 的 Slash Command 也会被自动发现。
167
+
168
+ ### Footer 自定义
169
+
170
+ 状态行是一个**可组合表面**——常见场景无需插件或 shell。
171
+
172
+ `/settings` → Status line(或 `dsh-pi-tui` 设置文档中的 `footer` 键)
173
+ 选择预设:
174
+
175
+ | 值 | 含义 |
176
+ |---|---|
177
+ | `default`(旧名 `full`) | 经典两行 Footer(状态 + 统计) |
178
+ | `compact` | 仅状态行(隐藏统计行) |
179
+ | `custom` | 版本化 `footerLayout`(见下) |
180
+ | `command` | 用户配置的命令渲染状态表面(见下) |
181
+
182
+ 前三个值可在 `/settings` 面板选择;`command` **不在面板中**——它只能
183
+ 通过 USER 层设置文档(`footer: "command"` + `footerCommand`)启用,
184
+ `/settings` 的 Status line 行只有 `default / compact / custom` 三个选项。
185
+
186
+ `/footer` 是层级式交互配置器:先选行(Row Selector),再编辑该行的
187
+ 条目——`↑/↓` 在整行条目间顺序移动(Left/Right 只是视觉分组),
188
+ `←/→` 左右换侧,`Space` 移除,`A` 打开可搜索的 Add Picker(按
189
+ label / id / 描述过滤,选中项下方显示描述),`M` 进入 Move Mode
190
+ 排序,`Enter` 打开 Item Editor(Style 候选以条目的真实渲染作示例;
191
+ Tone 语义色;Advanced 编辑 prefix / suffix / importance 并可一键
192
+ Reset)。预览由真实 Footer 引擎合成,与 contextual help 一起固定在
193
+ 面板顶部,任何终端尺寸下都不会随列表滚动消失。Row Selector 页 `S`
194
+ 保存(持久化),`Esc` 逐页返回、在首页关闭且不影响当前生效布局。
195
+ 无会话时也可使用。
196
+
197
+ Add Picker 的末尾还可以选择 `+ Create Custom Text`,创建用户自定义的静态文本条目。创建后可编辑文本、默认语义色、显示名称,也可以删除;条目定义只从 USER 层读取并持久化。定义 Tone 与布局中的放置 Tone 分开,条目仍可在 `/footer` 中显示/隐藏、移动和排序。
198
+
199
+ `footerLayout` 是嵌套设置对象(schemaVersion 1,1–2 行,左/右区域,
200
+ 分隔符,有限 formatter,语义 tone,prefix/suffix,importance)。
201
+ `/footer` 配置器可交互地构建它;YAML 形状如下:
202
+
203
+ ```yaml
204
+ footer: custom
205
+ footerLayout:
206
+ schemaVersion: 1
207
+ rows:
208
+ - left:
209
+ - id: agent-preset
210
+ format: compact
211
+ - id: model
212
+ - id: project
213
+ - id: context
214
+ format: full
215
+ - id: cache-hit
216
+ - id: token-usage
217
+ format: io
218
+ - id: performance
219
+ format: speed
220
+ - id: version
221
+ format: tui
222
+ right:
223
+ - id: focus-mode
224
+ separator:
225
+ text: " │ "
226
+ tone: textDim
227
+ ```
228
+
229
+ 内置 format 是有限集合,继续使用现有的 `format` 字段(不新增第二套
230
+ style schema):Model 为 `badge` / `plain` / `compact`;Permission preset
231
+ 为 `badge` / `plain` / `compact`;Plan state 为 `badge` / `plain`;Working
232
+ directory 为 `short` / `basename` / `full`;Git branch 为 `plain` / `label`;
233
+ Context 为 `bar` / `percent` / `full`;Token usage 为 `io` / `total` /
234
+ `compact`;Cache hit 为 `full` / `compact`;Performance 为 `full` / `speed` /
235
+ `latency`(平均首 token 时间);Turns/steps 为 `both` / `turns` / `steps`;
236
+ Version 保留 `tui` / `dsh` / `both`。省略 `format` 时仍使用各条目的旧默认值。
237
+
238
+ 内置条目 id:`agent-preset`、`model`、`reasoning`、
239
+ `permission-preset`、`sandbox-mode`、`approval-policy`、`plan-state`、
240
+ `focus-mode`、`focused-seat`、`view-scope`、`cwd`、`project`、
241
+ `git-branch`、`run-state`、`queue`、`tasks`、`agents`、`todo`、
242
+ `context`、`cache-hit`、`token-usage`、`performance`、`turns-steps`、
243
+ `stats-line`、`version`、`ext:*`(旧扩展段)。非法的 `footerLayout`
244
+ 会警告一次并回退到默认布局——TUI 始终能启动。
245
+
246
+ `footer: command` 把状态表面交给用户配置的命令(Claude/Kimi 风格):
247
+ 当前状态快照以 JSON 序列化到命令的 stdin(schemaVersion 1——不含
248
+ secret、凭据、提示词),命令的 stdout(经过净化:仅保留 SGR 颜色与
249
+ OSC 8 超链接)渲染状态表面。Host 的指令表面(如 Ctrl+C 退出提示)
250
+ 始终叠加在最上层。
251
+
252
+ ```yaml
253
+ footer: command
254
+ footerCommand:
255
+ schemaVersion: 1
256
+ command: "~/.config/dsh/statusline.sh"
257
+ timeoutMs: 300 # 默认 300,最大 1000
258
+ refreshIntervalMs: 1000 # 最小 1000
259
+ maxRows: 1 # 1..2
260
+ ```
45
261
 
46
- ### Option A — from the npm registry (recommended)
262
+ **安全:** 只有当命令位于你的设置文档的 USER 层时才会被执行。
263
+ 仓库/项目提供的 `footerCommand` 永远不会被执行——命令模式被禁用并
264
+ 回退到原生布局。命令按 `refreshIntervalMs` 周期刷新,每次最多输出 2 行;失败(空输出、非零退出、超时)自动回退到原生布局。
265
+
266
+ ### 扩展 Footer 条目
267
+
268
+ 插件可以通过 Stable 扩展 API(`@xmoon76/dsh-pi-tui/extensions`)贡献
269
+ **可配置的 Footer 条目**:在 `chrome.footer.item` 槽位注册一个
270
+ `FooterItemContribution`——包含 label 与纯数据 `segment`(带样式的
271
+ span;Host 会剥离任何终端控制序列,插件永远不能直接给终端上样式)。
272
+ 用户可在 `/footer` 中像内置条目一样开关、排序、左右放置。注册前请
273
+ 先 feature-detect `slot.chrome.footer.item` 能力(该能力在任何 surface
274
+ 存在之前就已声明)。条目的配置身份是规范键 `ext:<owner>/<id>`,其中
275
+ owner 是插件的稳定名称——**跨 HMR 稳定**:引用已卸载插件条目的布局
276
+ 保留引用,插件重载后自动恢复。npm scoped 插件名(`@scope/name`)合法:
277
+ 其 `/` 在键中按 `encodeURIComponent` 百分号编码(`ext:%40scope%2Fname/<id>`);
278
+ id 本身不得包含 `/`。旧的 `chrome.footer.status` 槽位不变:
279
+ 其 segment 聚合为单一的 `ext:*` 条目。完整作者指南:
280
+ [docs/extension-api.md](docs/extension-api.md)。
281
+
282
+ ## 常用按键
283
+
284
+ | 按键 | 功能 |
285
+ | ------------- | ---------------------- |
286
+ | `Enter` | 提交输入 |
287
+ | `Ctrl+Enter` | Agent 忙碌时把草稿入队(与 Enter 相反) |
288
+ | `Shift+Enter` | 换行 |
289
+ | `Esc` | 取消当前交互 / 中断运行 |
290
+ | `Esc Esc` | 空闲时打开 Rewind |
291
+ | `Ctrl+C` | 中断 / 清空当前输入 |
292
+ | `Ctrl+D` | 退出 TUI(等同 `/exit`) |
293
+ | `Ctrl+S` | Steer:把队列消息和草稿一起发送到正在运行的回合 |
294
+ | `Ctrl+T` | 切换 Todo 面板 |
295
+ | `Ctrl+R` | 搜索输入历史 |
296
+ | `Ctrl+F` | 搜索 Transcript |
297
+ | `Ctrl+O` | 展开 / 折叠工具和系统详情;全屏 Focus 下整体切换 Thought root |
298
+ | `Alt+T` | 展开 / 折叠 Thinking |
299
+ | `Ctrl+G` | 使用 `$VISUAL`/`$EDITOR` 编辑输入 |
300
+ | `Ctrl+V` | 粘贴图片 |
301
+ | `Tab` | 补全斜杠命令与文件路径 |
302
+ | `@` | 文件补全 |
303
+ | `!` | 进入 Shell 模式 |
304
+ | `!!` | 进入 Local-only Shell 模式 |
305
+
306
+ 完整按键和命令以 TUI 中的 `/help` 为准。表中的快捷键是默认值;用户自定义后,以 `/help` 和 `/keybindings` 显示的生效键位为准。
307
+
308
+ ### 自定义快捷键
309
+
310
+ Host 快捷键是语义 action(`app.*`),通过 context-aware keymap 解析——
311
+ UI(页脚提示、`/help`、`/keybindings`)始终显示**生效**的按键,因此
312
+ 改键后所有提示自动更新。在 `dsh-pi-tui` settings 命名空间中配置,
313
+ 然后用 `/keybindings reload` 应用(显式 reload——改设置后执行 reload
314
+ 即生效,无需重启):
315
+
316
+ ```yaml
317
+ dsh-pi-tui:
318
+ keybindings:
319
+ app.input.steer: ctrl+s # 单个按键
320
+ app.permission.cycle: [shift+tab, ctrl+shift+p] # 多个按键
321
+ app.history.search: ctrl+r
322
+ app.transcript.toggleThinking: false # 禁用该 action 的按键
323
+ leader: ctrl+x # M6:leader 序列
324
+ bindings:
325
+ app.tasks.open: <leader>t
326
+ ```
47
327
 
48
- The published package is self-contained: the vendored pi-tui fork is bundled
49
- into its build output, so `@xmoon76/dsh-pi-tui` is the only package you install
50
- (`@xmoon76/pi-tui` stays private in this repo, like kimi-code keeps
51
- `@moonshot-ai/pi-tui` private):
328
+ - 普通可打印键永远不能绑定到 Host action(会吞掉输入);坏配置只是
329
+ 警告,绝不会导致启动失败(fail-soft)。
330
+ - 任何用户声明都会**替换**该 action 的内置默认键:`app.input.steer:
331
+ ctrl+x` 让 Ctrl+X steer、Ctrl+S 不再 steer;仅 leader 的
332
+ `app.todo.toggle: <leader>t` 让 Leader T 成为唯一切换触发(Ctrl+T
333
+ 失效);`['ctrl+z', '<leader>s']` 同时保留两个用户触发;`false`
334
+ 移除该 action 的全部触发。若 effective editor-owned submit key 会在
335
+ leader machine 之前消费某个 completion(例如 `<leader>enter`),该死序列会被
336
+ 拒绝而不会被展示。
337
+ - `DSH_PI_TUI_SAFE_KEYBINDINGS=1` 忽略所有用户覆盖(仅使用内置默认)。Safe mode
338
+ 开启时整个 `/keybindings` 编辑器只读,避免保存只会在关闭 safe mode 后才发现的
339
+ 冲突配置。
340
+ - 编辑器中未自定义 action 且仍然 effective 的默认按键可选择。`Add shortcut` 会把
341
+ 这些仍生效的默认键与新键一起写入;已被 shadow 的 definition default 仅作参考,替换
342
+ 或删除一个仍生效的默认键会保留其余 sibling。action 已有用户声明后,仍按上文规则
343
+ 替换内置按键集合。
344
+ - `/help` 仍是按键优先的只读帮助;`/keybindings` 是按 action 优先的
345
+ 可编辑 Keyboard Shortcuts Editor:按类别分组,搜索 action ID/描述/当前键和
346
+ 默认键,并标记 customized、conflict、Unbound、Disabled 和 fixed 状态。
347
+ 独立的 Leader key 行还可设置全局 leader key。
348
+ - `/settings` 只有一个 `Keyboard shortcuts` 入口,打开与 `/keybindings` 相同的
349
+ 编辑器和持久化控制器。
350
+ - 录制器读取真实终端按键,通过 `parseKey` 规范化为 `KeyId`;保存前会拒绝
351
+ 无法匹配、吞输入、终端歧义或已知冲突的按键。普通录制器按 `Esc` 立即取消;
352
+ Host interrupt action 的 direct recorder 使用短暂双击窗口:一次 `Esc` 取消,
353
+ 两次 `Esc` press event 才录入物理 Escape。repeat/release 不算第二次,不再有单字母
354
+ 快捷方式;物理 Escape 保留给 Host 生命周期路径。
355
+ - 条件 affordance 会在编辑器中单独标注(例如空编辑器任务浏览器的
356
+ `Down (conditional)`),不会伪装成普通已配置快捷键。
357
+ - `/keybindings conflicts` 列出冲突(同键 + 作用域重叠 + 同优先级——绝不
358
+ 静默 last-write-wins);`/keybindings reload` 重新读取设置(fail-soft:坏配置会
359
+ 被诊断并跳过,读取异常才会给出错误提示——都不会崩溃,keymap 保留
360
+ last-known-good 配置);`/keybindings reset` 通过 settings 服务清除覆盖,并
361
+ 立即重建运行中的 keymap。
362
+ - 子代理查看器按 action id 阻止父级 action,因此改键后的父级快捷键
363
+ 在查看器内依然被阻止。
364
+ - 条件 affordance 是**累加**的:绑定 `app.tasks.open: ctrl+x` 是**增加**
365
+ 一个触发——空编辑器的 `↓` 任务浏览器仍然有效;只有 `false` 才会
366
+ 移除某 action 的全部触发。
367
+
368
+
369
+ ## 安装
370
+
371
+ ### 环境要求
372
+
373
+ * DeepSeek Harness
374
+ * Node.js `^22.19.0 || >=24`
375
+
376
+ 项目当前跟随 DeepSeek Harness `0.1.1-rc.x` 版本线开发。
377
+
378
+ ### npm
379
+
380
+ 推荐使用单独的 `pi-tui` profile:
52
381
 
53
382
  ```sh
54
- # install the bundle into the pi-tui profile (creates the profile if needed)
55
383
  dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui
56
-
57
- # run it
58
384
  dsh --profile pi-tui
59
385
  ```
60
386
 
61
- Any dependency whose manifest declares `dsh.bundle` joins the profile's layer
62
- stack automatically — no manual `cordis.patch.yml` wiring.
387
+ 恢复已有 Session:
388
+
389
+ ```sh
390
+ dsh --profile pi-tui --session <session-id>
391
+ ```
392
+
393
+ 安装包已经包含运行所需的 Pi TUI fork,不需要额外安装内部的 TUI package。
394
+
395
+ ### 更新
396
+
397
+ ```sh
398
+ dsh plugin --profile pi-tui -- update @xmoon76/dsh-pi-tui
399
+ ```
400
+
401
+ 查看已安装插件:
402
+
403
+ ```sh
404
+ dsh plugin --profile pi-tui -- list
405
+ ```
406
+
407
+ 卸载:
63
408
 
64
- ### Option B — from source
409
+ ```sh
410
+ dsh plugin --profile pi-tui -- remove @xmoon76/dsh-pi-tui
411
+ ```
65
412
 
66
- Build artifacts are not committed (`dist/` for both packages is gitignored and
67
- the package `exports` point at the built files), so build before installing
68
- from a clone:
413
+ ## 从源码运行
69
414
 
70
415
  ```sh
71
416
  git clone https://github.com/XMoon/dsh-pi-tui
72
417
  cd dsh-pi-tui
418
+
73
419
  pnpm install
74
- pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + root tsdown (dist/, bundles pi-tui)
420
+ pnpm build
421
+ ```
75
422
 
76
- # file: — the bundle is copied into the profile at add time; rebuild + re-add
77
- # to refresh (see "Update / uninstall" below)
78
- dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
423
+ 使用 `file:` 安装:
79
424
 
80
- # link: — a live symlink instead; `pnpm build` output is picked up directly
81
- dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@link:$PWD
425
+ ```sh
426
+ dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
82
427
  ```
83
428
 
84
- ### Verify the install
429
+ `file:` 会在安装时复制当前构建结果。修改源码后需要重新 build 并重新 add。
430
+
431
+ 持续开发可以使用 `link:`:
85
432
 
86
433
  ```sh
87
- dsh plugin --profile pi-tui -- list # @xmoon76/dsh-pi-tui present
88
- dsh --profile pi-tui # TUI starts instead of the web GUI
434
+ dsh plugin --profile pi-tui-dev -- add @xmoon76/dsh-pi-tui@link:$PWD
435
+ dsh --profile pi-tui-dev
89
436
  ```
90
437
 
91
- ### Update / uninstall
438
+ 之后重新执行:
92
439
 
93
440
  ```sh
94
- # registry installs:
95
- dsh plugin --profile pi-tui -- update @xmoon76/dsh-pi-tui
96
- # file: source installs copy at add time — rebuild + re-add to refresh
97
- # (link: installs track the repo live and need only `pnpm build`):
98
- pnpm build && dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
99
-
100
- dsh plugin --profile pi-tui -- remove @xmoon76/dsh-pi-tui
441
+ pnpm build
101
442
  ```
102
443
 
103
- ## Development
444
+ 即可让开发 Profile 使用新的构建结果。
445
+
446
+ ## DeepSeek Harness 集成
447
+
448
+ `dsh-pi-tui` 只实现终端交互层。
449
+
450
+ 以下能力由 DeepSeek Harness 提供:
451
+
452
+ * Agent Loop
453
+ * LLM / Provider
454
+ * Session Persistence
455
+ * Tools
456
+ * Skills
457
+ * Approval
458
+ * Permission Presets
459
+ * Plan Mode
460
+ * Goal
461
+ * Jobs
462
+ * Subagents
463
+ * Credentials
464
+ * Settings
465
+
466
+ 因此 TUI 不需要维护独立的模型配置、Session 格式或 Agent Runtime。
467
+
468
+ 它可以和其他 DSH Surface 使用同一套运行时数据:
104
469
 
105
470
  ```sh
106
- pnpm install
107
- pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + root tsdown (dist/, bundles pi-tui)
108
- pnpm test # pi-tui's own suite (node --test) + dsh-pi-tui headless tests
109
- pnpm typecheck
110
- node --expose-gc scripts/bench.mts # performance baseline (optional)
471
+ dsh --profile web
472
+ dsh --profile headless
473
+ dsh --profile pi-tui
111
474
  ```
112
475
 
113
- Tests drive the UI through `@xterm/headless` (see `test/virtual-terminal.ts`),
114
- so rendering and input routing are verified without a TTY or a model connection.
115
-
116
- ### Development history (dogfooding)
117
-
118
- This project started development on the browser surface (`dsh --profile web`) and
119
- switched to building itself with itself: since August 15 2026, all fixes and
120
- features are developed inside this TUI, the same way this README and the
121
- codebase are maintained. The dev loop runs on a dedicated `pi-tui-dev` profile
122
- installed with Option B's `link:` specifier
123
- (`dsh plugin --profile pi-tui-dev -- add @xmoon76/dsh-pi-tui@link:$PWD`)
124
- — a live symlink, so `pnpm build` is picked up without re-adding — while the
125
- `pi-tui` profile stays on the published registry package for real use.
126
-
127
- ## Extensions (early, stabilizing)
128
-
129
- Since `0.2.0` the bundle ships a small, versioned extension surface so a
130
- third-party Cordis plugin can contribute chrome without touching the TUI
131
- internals. It is **early and stabilizing**: the capabilities below are the
132
- current set; the API version (`1`) is bumped only on breaking changes, and
133
- plugins must **feature-detect** capabilities instead of parsing the package
134
- version.
135
-
136
- All extension plugins remain standard DeepSeek Harness / Cordis plugins using
137
- `name`, `inject`, and `apply(ctx)` against the single `piTuiExtensions`
138
- service; the tiers are capability facades over that one service, not separate
139
- plugin systems or runtimes.
140
-
141
- The extension surface ships three tiers: a plugin imports ONLY the
142
- public entry — never the stable entry's internals (`TuiApp`,
143
- `TuiMainScreen`, `TuiAltScreen`) nor repository-relative paths.
144
-
145
- | Tier | Entry | Contract |
146
- |---|---|---|
147
- | Stable | `@xmoon76/dsh-pi-tui/extensions` | compatibility-oriented; additive-first; existing semantics never silently change; removal requires a planned breaking change |
148
- | Advanced | `@xmoon76/dsh-pi-tui/extensions/advanced` | experimental; minor releases may break; a migration note is required; no long-term shims |
149
- | Unstable | `@xmoon76/dsh-pi-tui/extensions/unstable` | NO compatibility guarantee; implementation may change anytime |
150
-
151
- All tiers reuse the SAME shared extension runtime: caller-fiber ownership,
152
- surface lifecycle, invalidation, capability discovery. Do not fork a second
153
- ownership/lifecycle model per tier. The tiers have grown since Phase 1:
154
-
155
- - **Advanced** (`ADVANCED_API_LEVEL = 1`, Phase 2 + Phase 4): normalized
156
- input capture, focused interactive surfaces (interactive managed
157
- overlays), advanced editor control, the imperative UI broker
158
- (select/confirm/input/notify), custom interactive UI and the host-state
159
- facade (theme/title/working/tools-expanded) — still Host-mediated,
160
- never raw terminal bytes. Author guide: `docs/extension-advanced.md`;
161
- Pi capability reference: `docs/extension-capability-matrix.md`.
162
- - **Unstable** (`UNSTABLE_API_LEVEL = 1`, Phase 3): raw input
163
- interception (observe/consume/rewrite, exclusive raw ownership), the
164
- Host emergency fail-safe (triple-Esc), and a selected low-level surface
165
- seam — NO compatibility guarantee; a broken plugin can disrupt Host
166
- behavior. Author guide: `docs/extension-unstable.md`.
167
- - **Real-plugin validation (Phase 5):** the tier selection is proven by
168
- real consumers in `examples/plugins/` — a
169
- production-class vim modal editor (Advanced editor SDK), a
170
- questionnaire form (Advanced imperative UI broker) and an interactive
171
- shell (Unstable raw seam). The "which tier should I use?" decision
172
- tree: `docs/plugin-authoring.md`.
173
-
174
- A plugin imports only the public entry:
476
+ ## Extension API
477
+
478
+ 除作为 TUI 使用外,`dsh-pi-tui` 还提供版本化的 Extension API,供其他 Cordis / DSH 插件扩展终端界面。
479
+
480
+ 目前分为三个入口:
481
+
482
+ | Entry | 用途 | 稳定性 |
483
+ | ----------------------------------------- | ------------ | -------- |
484
+ | `@xmoon76/dsh-pi-tui/extensions` | 常规扩展 | Stable |
485
+ | `@xmoon76/dsh-pi-tui/extensions/advanced` | 高级交互能力 | Advanced |
486
+ | `@xmoon76/dsh-pi-tui/extensions/unstable` | Low-level 能力 | Unstable |
487
+
488
+ 可扩展的内容包括:
489
+
490
+ * Header / Footer
491
+ * Input Widget
492
+ * Slash Command
493
+ * Theme
494
+ * Setting
495
+ * Autocomplete
496
+ * Keybinding
497
+ * Message Renderer
498
+ * Tool Renderer
499
+ * Overlay
500
+ * Interactive UI
501
+ * Editor Control
502
+ * Replacement Editor
503
+
504
+ 插件只需要依赖公开入口,不需要 import `TuiApp`、`TuiMainScreen` 等内部实现。
505
+
506
+ 简单示例:
175
507
 
176
508
  ```ts
177
- import { PI_TUI_EXTENSIONS_SERVICE, type PiTuiExtensionService } from '@xmoon76/dsh-pi-tui/extensions'
509
+ import {
510
+ PI_TUI_EXTENSIONS_SERVICE,
511
+ type PiTuiExtensionService,
512
+ } from '@xmoon76/dsh-pi-tui/extensions'
178
513
 
179
514
  export const name = 'my-plugin'
180
515
  export const inject = ['tuiStartup', PI_TUI_EXTENSIONS_SERVICE]
181
516
 
182
517
  export function apply(ctx: Context): void {
183
- const service = ctx.get(PI_TUI_EXTENSIONS_SERVICE) as PiTuiExtensionService
184
- if (!service.api().capabilities.has('slot.chrome.header.badge')) return
185
- service.register<{ text: string; tone?: 'info' | 'warning' | 'error' | 'success' }>(
518
+ const service = ctx.get(
519
+ PI_TUI_EXTENSIONS_SERVICE,
520
+ ) as PiTuiExtensionService
521
+
522
+ if (!service.api().capabilities.has('slot.chrome.header.badge')) {
523
+ return
524
+ }
525
+
526
+ service.register(
186
527
  'chrome.header.badge',
187
- { id: 'my-badge', order: 100, description: 'A header badge from my plugin.' },
188
- { text: 'my-badge', tone: 'info' },
528
+ {
529
+ id: 'my-badge',
530
+ order: 100,
531
+ },
532
+ {
533
+ text: 'my-plugin',
534
+ tone: 'info',
535
+ },
189
536
  )
190
537
  }
191
538
  ```
192
539
 
193
- Current extension points (v1):
194
-
195
- | Slot | Semantics | Contribution |
196
- |---|---|---|
197
- | `chrome.header.badge` | list | a short `[badge]` after the host title |
198
- | `input.dock.item` | list | a dock line above the todo panel |
199
- | `chrome.footer.status` | list | a footer segment (host owns width/truncation) |
200
- | `input.widget.above` / `input.widget.below` | list | a bounded widget around the editor (M4 component kit) |
201
-
202
- Contributions are **plain data**, not render functions: a plugin supplies
203
- `HeaderBadge` / `DockItem` / `FooterSegment` / `InputWidget` values (text +
204
- semantic tone spans, or a structured `ExtensionView` tree for widgets) and
205
- the host owns rendering, ANSI compilation, width budgets and truncation.
206
- There is deliberately no `render(context)` callback in v1 — plugins never
207
- hold a rendering context, so a contribution can never capture or mutate
208
- host internals.
209
-
210
- Since `0.2.0` the widget slots (`input.widget.above` / `input.widget.below`)
211
- accept a bounded component kit: `ExtensionView` is a structured view tree
212
- (`text` / `markdown` / `spacer` / `stack` / `frame` / `rows` views with
213
- semantic style tokens) that the host compiles into private components. A
214
- plugin can add helper rows above or below the editor — for example a status
215
- widget or a quick-reference line — without touching the root layout, the
216
- editor, or focus. The host owns the row budgets: under height pressure the
217
- lowest-importance widgets collapse first, and the editor always survives.
540
+ 详细文档:
218
541
 
219
- ```ts
220
- service.register<InputWidget>('input.widget.below', {
221
- id: 'my-widget',
222
- order: 100,
223
- }, {
224
- view: {
225
- kind: 'text',
226
- spans: [{ text: 'my-plugin ready', tone: 'success' }],
227
- },
228
- })
542
+ * [Extension API](docs/extension-api.md)
543
+ * [Extension tiers](docs/extension-tiers.md)
544
+ * [Advanced API](docs/extension-advanced.md)
545
+ * [Unstable API](docs/extension-unstable.md)
546
+ * [Plugin authoring](docs/plugin-authoring.md)
547
+ * [Capability matrix](docs/extension-capability-matrix.md)
548
+
549
+ ## 开发
550
+
551
+ ```sh
552
+ pnpm install
553
+ pnpm build
554
+ pnpm typecheck
555
+ pnpm test
556
+ ```
557
+
558
+ 测试包括 Pi TUI fork、自身 TUI 行为以及 Extension API 的 fixture / smoke test。
559
+
560
+ 终端渲染和输入路由使用 `@xterm/headless` 做自动化验证,因此大部分 UI 测试不依赖真实 TTY 或模型连接。
561
+
562
+ 性能基线:
563
+
564
+ ```sh
565
+ node --expose-gc scripts/bench.mts
229
566
  ```
230
567
 
231
- Since M5 the extension surface also covers registries (plan §10):
232
-
233
- - `registerCommand(contribution)` — slash-command OWNERSHIP metadata
234
- (`execution: 'local' | 'submission'`): a plugin-declared local command
235
- always executes directly (never steered by the busy-Enter preference);
236
- a submission command flows through the session policy. Actual execution
237
- stays in the host's commands service; `/name args...` keeps
238
- `rawInput` verbatim. Name conflicts are reported, never guessed.
239
- - `registerTheme(contribution)` — a named semantic palette selectable
240
- from the /settings theme picker; the owner's unload falls back to the
241
- built-in palette (a selected plugin theme never dangles).
242
- - `registerSetting(contribution)` — a settings row appended to the
243
- /settings panel (label + current value + choices + optional rejection);
244
- the host owns the panel.
245
- - `registerAutocomplete(contribution)` — an autocomplete provider
246
- consulted after the host's own provider returns null (deterministic
247
- order, per-provider isolation, latest-only commit).
248
- - `registerKeybinding(contribution)` — normalized-key → semantic-action
249
- binding, routed by the host's InputRouter (M6). A plugin declares a key
250
- with the public `NormalizedKey` shape (key + ctrl/alt/shift/super) and
251
- a semantic action from the host's list (`submit-draft`, `queue-draft`,
252
- `steer-draft`, `cancel-activity`, `open-search`, `toggle-fullscreen`,
253
- `cycle-permission`). The host normalizes ALL terminal input (Kitty
254
- CSI-u, modifyOtherKeys, legacy sequences) — a plugin never sees raw
255
- escape data. Reserved host lifecycle keys (Ctrl+C/D/S/F/O/T/G/J,
256
- Ctrl+Enter, Enter, Esc) cannot be claimed; plain printable keys never
257
- fire a binding (typing always wins); bindings are non-capturing and
258
- fire LAST in the precedence ladder (after questions, approvals,
259
- overlays and the editor). The action executes through the host's own
260
- paths — submission/session safety is never bypassed.
261
- - `registerMessageRenderer(contribution)` — a TRANSCRIPT message renderer
262
- (M7, chain slot): receives a semantic `MessagePresentationSnapshot`
263
- (immutable; never the mutable message or the container) and returns an
264
- `ExtensionView` or `undefined` (abdicate → the next renderer → the host
265
- fallback). Kind-scoped renderers apply to one message kind.
266
- - `registerToolRenderer(contribution)` — a TOOL card renderer (M7, keyed
267
- slot): presents the card for ONE tool name from a
268
- `ToolPresentationSnapshot` (callId, toolName, status, arguments,
269
- result, expanded); the winner (lowest priority) abdicates to the next
270
- renderer, then the host fallback. A priority tie on the same tool name
271
- is an explicit error.
272
- Renderers never stall the transcript: a throwing renderer is isolated
273
- and the chain continues, and the message cache embeds the renderer
274
- identity + registry revision, so an HMR/unload rebuilds exactly the
275
- affected components.
276
- - `showOverlay(view, options)` — a MANAGED overlay lease (M8): the plugin
277
- supplies an `ExtensionView` + sizing hints; the host mounts it through
278
- its overlay broker (modal stacking, focus, fullscreen migration). The
279
- returned lease is generation-scoped (the surface's final dispose closes
280
- every still-owned lease), close() is idempotent, and hide()/show()
281
- toggle visibility without closing. A plugin can never mount a raw
282
- component or steal focus — the host owns the terminal and the overlay
283
- stack.
284
- The [extension API v1 author guide](docs/extension-api.md) records the
285
- import rules, the full surface table, the lifecycle/render contracts, the
286
- M11 deprecation policy and the stability contract.
287
-
288
- The M10 acceptance fixture (plan §15): the repo ships a vim-mode fixture
289
- (`test/fixtures/vim-plugin/`) that validates the editor-extension seam — the
290
- packed public SDK is consumable by a third-party Cordis plugin, its
291
- replacement editor receives SEMANTIC `EditorInputEvent`s (never raw terminal
292
- bytes), and editor `create()`/`dispose()` work — importing ONLY
293
- `@xmoon76/dsh-pi-tui/extensions`. It is NOT a production Vim and NOT a
294
- Stable-API completeness proof: modal-mode behavior (insert/normal) is not
295
- part of the Stable contract, and the other public surfaces (commands,
296
- themes, settings, autocomplete, keybindings, renderers, overlays, widgets)
297
- have their own dedicated tests. Its CI gate forbids `@xmoon76/pi-tui`,
298
- `src/tui-app` and repository-relative internal paths: if a STABLE plugin
299
- ever needs a private import, the SDK is missing a capability (there is no
300
- `unsafeGetTuiApp()` escape hatch).
301
-
302
- - `registerEditor(contribution)` — the EDITOR SDK (M9, plan §14):
303
- single-winner by priority (a tie is an explicit error); the winner
304
- occupies the editor seat through the host's ATOMIC handoff (create →
305
- transfer draft/cursor → mount → focus → dispose old). A creation throw
306
- keeps the current editor working; winner unload restores the next
307
- winner / the host default editor WITH the draft preserved. The plugin
308
- editor receives an `EditorHost` (surfaceId, generation, getSnapshot,
309
- replaceText, dispatch of semantic actions submit/queue-submit/steer/
310
- open-external-editor, subscribe, invalidate) — but the host still owns
311
- busy-Enter, Ctrl+Enter, local-command classification, paste protection,
312
- approval/question capture, session guard/lock, external editor and
313
- exit: a plugin editor can never bypass those.
314
-
315
- Lifecycle is host-owned: registrations are disposed when the plugin's Cordis
316
- fiber unloads (HMR, disable), regular and fullscreen both refresh, and
317
- `handle.invalidate()/replace()` re-render through the active screen. The
318
- `@xmoon76/dsh-pi-tui/builtins` entry is the Loader-only first-party
319
- contributor (version badge, turn/step counters, todo-summary dock item) —
320
- not a stable third-party SDK. Raw terminal access, pre-host input
321
- interception and full input ownership are NOT part of the Stable tier (see
322
- the Advanced/Unstable roadmap).
323
-
324
- ## Slash commands (selection)
325
-
326
- - `/sessions [query]` — open the session picker: search-as-you-type over
327
- session ids, titles, and workspaces, rows grouped by workspace with live
328
- `filtered/total` counts, and titles loaded in the background as they are
329
- read. Enter switches to the selected session.
330
- - `/search <query>` — full-text search over persisted session logs, then
331
- switch to a hit.
332
- - `/title [title]` — with an argument, set the current session's title
333
- (pins it against automatic generation; titles appear in the `/sessions`
334
- picker); **without an argument, regenerate the title from the
335
- conversation — this overwrites the current title, including one you
336
- pinned earlier** (`/rename` is an alias).
337
- - `/tasks` — the merged task browser: background jobs AND subagents in one
338
- searchable list (type to filter rows by kind/label/status — `subagent`,
339
- `bash`, `failed`…). Subagent rows show their mode as part of the label
340
- (`subagent · <label> · continuable` / `… · one-shot`) — you know before
341
- Enter whether the viewer will be interactive. `Enter` opens the detail
342
- (child viewer for a subagent, status viewer for a job), `i` interrupts
343
- the selected subagent. `/subagents` is an alias.
344
- - `/yolo` — switch to `danger-full-access` (alias of `/permission danger-full-access`).
345
- - `/status` — show the current session's stats and identity (turn counts,
346
- token usage, workspace, installed dsh version).
347
- - `/rewind` — fork this conversation from an earlier user turn: the picker
348
- lists every completed user prompt (newest first), and choosing one creates
349
- a new child session whose history ends right before that turn, then
350
- restores the selected prompt into the editor for editing. The original
351
- session is never modified and stays reachable through `/sessions`;
352
- **workspace and external side effects (files, shell, APIs) are not
353
- reverted.** The same picker opens with `Esc Esc` on an empty editor.
354
- - `/preset`, `/model`, `/settings`, `/export`, `/fork` — see
355
- `dsh --profile pi-tui`'s command autocomplete (`/` + Tab).
356
-
357
- ## Keybindings (selection)
358
-
359
- - `Esc Esc` — conversation rewind (on an empty editor while idle): open the
360
- rewind picker and fork from an earlier user turn. One `Esc` while the
361
- agent is busy still cancels the active turn/tool/shell command; with a
362
- non-empty draft the double-Esc keeps its historical cancel behavior.
363
- - `Ctrl+F` — toggle transcript search (the `/search <query>` overlay; a
364
- second press closes it).
365
- - `Shift+Tab` — cycle the permission preset (read-only → workspace-write →
366
- danger-full-access); the footer's mode slot badges every preset
367
- (`[workspace-write]` / `[read-only]` / `[custom]`, with `[yolo]` flagging
368
- the no-approval mode).
369
- - `Ctrl+S` — steer: with queued messages, sends the whole queue (plus the
370
- draft, if any) into the running turn at once; otherwise sends the draft
371
- alone. An idle agent starts a fresh turn with everything.
372
- - `Alt+↑` — dequeue: pull every queued message back into the editor draft.
373
- - `Ctrl+T` — toggle the full todo list (in fullscreen, clicking the panel
374
- expands it to the full list and back); the dock above the editor always
375
- shows the todo summary and background tasks, and queued input renders
376
- between them.
377
- - `@` — file/folder mentions in the editor: `@` + Tab completes files from the
378
- whole workspace (fd-backed when `fd` is on PATH, with a built-in recursive
379
- fallback otherwise). The literal `@path` is submitted and the model reads
380
- the file itself. With background work running, an empty editor's `↓` opens
381
- the same merged task browser as `/tasks`:
382
- - **subagent rows** (live children, labeled with their catalog mode:
383
- `continuable` or `one-shot`) — `Enter` opens the child's viewer:
384
- a **continuable** child's viewer is **interactive** — type a
385
- follow-up and press Enter, and it is delivered as the child's NEXT
386
- turn through `ctx.subagents.followup` (FIFO — a running child is
387
- never interrupted or steered; an inactive child cold-resumes
388
- automatically), while `Esc` returns to your session. A **one-shot**
389
- child's viewer stays **read-only** (`Esc` returns; the editor shows
390
- the read-only placeholder). While a viewer is open the footer
391
- switches to the CHILD's own identity (`[subagent · continuable]`
392
- badge, label, activity, cwd, the child's own turns/steps/stats) and
393
- returns to your session's on exit. `i` interrupts the selected
394
- child. They never register job records, so this browser is their
395
- only glanceable home.
396
- - **job rows** (bash and one-shot subagent jobs) — `Enter` shows the status
397
- viewer only: a bash job's output read cursor belongs to the model's
398
- `job_output`, and a one-shot subagent job record carries no child session
399
- id, so a subagent job's transcript is reached through `/tasks` by picking
400
- the child by its label.
401
- The footer badge shows `[N tasks running · M agents · ↓ view]` while any
402
- background work is live. The queue pane above the editor shows pending
403
- input: user messages as `❯` rows, everything else (job notices, subagent
404
- reports, injected instructions) as `⏳` notices that fold into a
405
- `+N more` line beyond five — and disappear once the agent has received
406
- them.
407
-
408
- ## Launch options
409
-
410
- The TUI's startup row adds `--preset <id>` — the agent preset a fresh
411
- session starts on (falls back to `$DSH_PI_TUI_PRESET`, then the saved
412
- settings default). It exists because `/preset` only applies to a blank (not
413
- yet created) session, so launch-time selection is the other half of choosing
414
- a preset. All other flags are the dsh runner's own (`--session <id>`, …).
415
-
416
- ## Session lifecycle
417
-
418
- Opening the TUI with no `--session` creates **no session at all**: the first
419
- user message (text, slash command, `Ctrl+S` steer, or `!` shell) starts it
420
- lazily. `--session <id>` still resumes immediately, and a local `!!` command
421
- runs without needing a session.
422
-
423
- ## Verified in the P0 spike
424
-
425
- - Vendored pi-tui: the fork's own suite passes under `node --test` (run it as
426
- the sync gate after every re-vendor; the count is deliberately not copied here —
427
- `packages/pi-tui/package.json` is the single source of version facts).
428
- - `TuiApp` renders, accepts editor input, and handles Ctrl+C on a headless xterm.
429
- - The whole import chain (pi-tui, tui-app, `@deepseek-ai/dsh-cmdline`, commander)
430
- loads under the tsx ESM hook — the dsh source-launch contract.
431
- - Native modifier-key addons are optional: on Linux the loader returns `undefined`
432
- without attempting a load, and the non-TTY stdin path is guarded.
433
-
434
- ## Diagnostic log
435
-
436
- The TUI writes its own diagnostics to stderr and a log file (`ctx.logger` is
437
- invisible in this process — no exporter):
438
-
439
- - Default file: `$DSH_HOME/logs/pi-tui-<pid>.log` (default `~/.dsh/logs/`);
440
- - Line format: `[tui] <ISO time> <level> <message> k=v ...`;
441
- - Default level `info`: key lifecycle events only (boot/resume/switch/exit,
442
- divergence-guard warnings, errors); `debug` additionally logs every guard
443
- check before a send.
444
-
445
- Configuration (environment variables):
446
-
447
- | Variable | Meaning | Default |
448
- |---|---|---|
449
- | `DSH_PI_TUI_LOG` | Log file path; `off` disables file logging | `$DSH_HOME/logs/pi-tui-<pid>.log` |
450
- | `DSH_PI_TUI_LOG_LEVEL` | `debug` / `info` / `warn` / `error` | `info` |
451
-
452
- Troubleshooting example:
568
+ 项目日常开发使用单独的 `pi-tui-dev` Profile 进行自测:
453
569
 
454
570
  ```sh
455
- DSH_PI_TUI_LOG_LEVEL=debug dsh --profile pi-tui
456
- tail -f ~/.dsh/logs/pi-tui-*.log
571
+ dsh plugin --profile pi-tui-dev -- add @xmoon76/dsh-pi-tui@link:$PWD
572
+ dsh --profile pi-tui-dev
573
+ ```
574
+
575
+ ## 项目结构
576
+
577
+ 仓库根目录是发布到 npm 的 `@xmoon76/dsh-pi-tui` bundle。
578
+
579
+ Pi TUI fork 位于:
580
+
581
+ ```text
582
+ packages/pi-tui/
583
+ ```
584
+
585
+ 它作为内部依赖参与构建,并随根 package 一起打包,不单独要求用户安装。
586
+
587
+ 具体的 upstream 来源、版本和本地差异以:
588
+
589
+ ```text
590
+ packages/pi-tui/package.json
591
+ packages/pi-tui/AGENTS.md
457
592
  ```
458
593
 
459
- ## Safety & operational notes
460
-
461
- - **One surface per session.** dsh has no cross-process session coordination:
462
- a session open in TWO dsh processes (TUI + web, or two TUIs) can corrupt its
463
- log. The TUI refuses to open a session already held by another live dsh
464
- process (an `owner.lock` file next to the log, with a pid/starttime probe
465
- for stale locks left by crashes — so the second surface is stopped at
466
- OPEN time, not after the damage). For writes, the TUI detects the other
467
- writer and blocks the send; the SAME action pressed again (Enter for a
468
- submit, Ctrl+S for a steer, unchanged draft) forces through — an edited
469
- draft, a swapped key, a new file revision, or a session switch invalidates
470
- the force. Never run two surfaces on one session (full contract:
471
- `docs/concurrency.md`).
472
- - **Session repair.** `node_modules/@xmoon76/dsh-pi-tui/scripts/repair-session.mjs`
473
- repairs corrupted logs (`--scan` lists damage read-only; `--yes` applies with
474
- a mandatory backup). A torn (truncated) tail is truncated at the last
475
- complete frame and reported with exact byte accounting; references to a
476
- duplicated seq are never auto-resolved — the repair refuses and asks for
477
- `--duplicate-reference=first|last|segment`. Repaired logs are re-verified
478
- with the dsh reader's own layout checks before the backup is considered
479
- redundant. (Full repair contract, incl. the frame-layout constraint:
480
- `docs/repair-session.md`.)
481
- - **Exit.** `/exit` (alias `/quit`) flushes the session with a 10s hard
482
- timeout: a hung provider cannot trap the TUI. If the flush fails or times
483
- out, the terminal prints a warning (the tail may not be persisted) and the
484
- process still exits.
485
- - **Performance.** `scripts/bench.mts` (non-default) measures ingest,
486
- projection, cold/warm rebuilds, streaming frames, theme switches, and heap;
487
- the saved baseline lives in `docs/perf-baseline.md`. Unchanged transcript
488
- messages reuse their rendered components, so the warm per-frame rebuild
489
- does not grow with history.
594
+ 为准。
595
+
596
+ 贡献者相关的仓库结构和开发约定见 [AGENTS.md](AGENTS.md)。
597
+
598
+ ## 文档
599
+
600
+ | 文档 | 内容 |
601
+ | ------------------------------------------------------ | ---------------------------- |
602
+ | [docs/README.md](docs/README.md) | 文档索引 |
603
+ | [docs/architecture.md](docs/architecture.md) | 架构和模块职责 |
604
+ | [docs/input-history.md](docs/input-history.md) | 输入历史 |
605
+ | [docs/surface-decisions.md](docs/surface-decisions.md) | TUI 交互设计决策 |
606
+ | [docs/concurrency.md](docs/concurrency.md) | Session 并发 |
607
+ | [docs/failure-model.md](docs/failure-model.md) | Async failure / cancellation |
608
+ | [docs/perf-baseline.md](docs/perf-baseline.md) | 性能基线 |
609
+ | [docs/extension-api.md](docs/extension-api.md) | Extension API |
610
+ | [AGENTS.md](AGENTS.md) | Contributor operating manual |
611
+
612
+ ## Changelog
613
+
614
+ 中文:
615
+
616
+ [CHANGELOG.md](CHANGELOG.md)
617
+
618
+ English:
619
+
620
+ [CHANGELOG.en.md](CHANGELOG.en.md)
490
621
 
491
622
  ## License
492
623
 
493
- MIT. `packages/pi-tui` retains its upstream MIT license and authorship
494
- (Copyright (c) 2025 Mario Zechner; Moonshot AI fork).
624
+ [MIT](LICENSE)