dsh-live-trace 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +920 -0
  3. package/README.zh.md +790 -0
  4. package/assets/rain.ogg +0 -0
  5. package/bin/dsh-glyph-probe.js +51 -0
  6. package/bin/dsh-live-trace.js +29 -0
  7. package/bin/dsh-live-working.js +14 -0
  8. package/cordis.patch.yml +31 -0
  9. package/icon.svg +12 -0
  10. package/index.js +328 -0
  11. package/lib/client.js +178 -0
  12. package/lib/instance.js +68 -0
  13. package/lib/normalize.js +850 -0
  14. package/lib/paths.js +66 -0
  15. package/lib/protocol.js +115 -0
  16. package/lib/registry.js +232 -0
  17. package/lib/tools.js +257 -0
  18. package/lib/tracker.js +648 -0
  19. package/lib/transport.js +231 -0
  20. package/locale/en.json +6 -0
  21. package/locale/zh.json +6 -0
  22. package/package.json +94 -0
  23. package/picture/call1.png +0 -0
  24. package/picture/call2.png +0 -0
  25. package/picture/sleep1.png +0 -0
  26. package/picture/sleep2.png +0 -0
  27. package/picture/tui1.png +0 -0
  28. package/picture/tui2.png +0 -0
  29. package/picture/type1.png +0 -0
  30. package/picture/type2.png +0 -0
  31. package/scripts/bench-render.mjs +69 -0
  32. package/scripts/demo-working.mjs +130 -0
  33. package/scripts/demo.mjs +284 -0
  34. package/scripts/install-profile.mjs +174 -0
  35. package/scripts/mock-provider.mjs +211 -0
  36. package/src/cli/cellsize.js +120 -0
  37. package/src/cli/format.js +73 -0
  38. package/src/cli/highlight.js +932 -0
  39. package/src/cli/i18n.js +457 -0
  40. package/src/cli/main.js +630 -0
  41. package/src/cli/markdown.js +753 -0
  42. package/src/cli/renderer.js +1044 -0
  43. package/src/cli/screen.js +270 -0
  44. package/src/cli/theme.js +221 -0
  45. package/src/cli/view-state.js +396 -0
  46. package/src/cli/views.js +406 -0
  47. package/src/cli/width.js +337 -0
  48. package/src/cli/working/art.js +413 -0
  49. package/src/cli/working/main.js +569 -0
  50. package/src/cli/working/packing.js +159 -0
  51. package/src/cli/working/picker.js +75 -0
  52. package/src/cli/working/props.js +385 -0
  53. package/src/cli/working/scene.js +837 -0
  54. package/src/cli/working/sky.js +641 -0
  55. package/src/cli/working/sound.js +400 -0
  56. package/src/cli/working/state.js +528 -0
package/README.zh.md ADDED
@@ -0,0 +1,790 @@
1
+ # dsh-live-trace
2
+
3
+ [English](README.md) · **中文**
4
+
5
+ 两扇**只读**的窗口,投向一个正在运行的 DeepSeek Harness 会话,渲染在*不是*运行你
6
+ agent 的那个终端里:
7
+
8
+ | 命令 | 是什么 |
9
+ | :--- | :--- |
10
+ | **`dsh-live-trace`** | 轨迹面板:轮次、步骤、工具、输出、token 的实时轨迹 |
11
+ | **`dsh-live-working`** | 小鲸鱼:一个动画场景,展示 agent 此刻在做什么 |
12
+
13
+ 两者都通过同一个本地 socket 连接到同一个正在运行的 Harness 进程,并且都不会向 agent
14
+ 发送任何东西。
15
+
16
+ ![轨迹面板,含推理、一次 bash 调用及其输出](picture/tui2.png)
17
+
18
+ ![小鲸鱼在打电话,接收子代理的回复](picture/call2.png)
19
+
20
+ | 思考 | 睡觉 |
21
+ | :---: | :---: |
22
+ | ![小鲸鱼在 room 里思考](picture/type1.png) | ![小鲸鱼在暮色天空下睡着](picture/sleep1.png) |
23
+
24
+ ## `dsh-live-trace` — 轨迹面板
25
+
26
+ Harness Web UI 展示的是一段对话。这里展示的是*机器*:循环正处于哪个轮次、哪个步骤,
27
+ 正在执行哪个工具,返回了什么,模型此刻在流式输出什么,已经运行了多久,以及消耗了多少
28
+ token —— 它持续更新,所以你能一眼看出 agent 是在干活还是卡住了。
29
+
30
+ ```
31
+ ┌─ dsh-live-trace ─────────────────────────────────────────────────────────────┐
32
+ │ Session: …8-4222-8d66-0bb3b8c30c45 · live trace demo Status: ⠇ running │
33
+ ├──────────────────────────────────────────────────────────────────────────────┤
34
+ │ 10:19:00 [TURN 3] ─────────────────────────────────────────────────────── │
35
+ │ 10:19:00 [STEP 1] model call started │
36
+ │ 10:19:00 [USER] 把登录逻辑抽到 src/auth.ts,并补上测试 │
37
+ │ 10:19:02 [ASSISTANT] 先读一下现有的登录代码,确认调用点,再决定抽象边界。 │
38
+ │ 10:19:02 [TOOL] read_file path="src/login.ts" │
39
+ │ 10:19:02 [RESULT] ✓ 返回 234 行 0.2s │
40
+ │ 10:19:03 [TOOL] bash command="npm test" description="Run the test…" │
41
+ │ 10:19:03 [RESULT] ✓ 测试通过 (12 passed) 0.2s │
42
+ │ 10:19:05 [APPROVAL] bash — runs outside the sandbox │
43
+ │ 10:19:06 [APPROVAL] decision: allowed-once │
44
+ │ 10:19:06 [STEP 2] model call started │
45
+ │ 10:19:06 [ASSISTANT] 已完成:登录逻辑抽到了 src/auth.ts,12 个测试通过。 │
46
+ │ 10:19:06 [TURN 3 END] completed │
47
+ ├──────────────────────────────────────────────────────────────────────────────┤
48
+ │ ⠇ running T3 · S2 12.3s Tokens 4.2K/1.0M (↑3.8K ↓180) q:quit │
49
+ └──────────────────────────────────────────────────────────────────────────────┘
50
+ ```
51
+
52
+ 四个面板共用同一套外观:
53
+
54
+ | 面板 | 按键 | 显示内容 |
55
+ | :--- | :--- | :--- |
56
+ | **轨迹** | `1` / `t` | 按时间顺序的事件日志,模型正文按 Markdown 渲染,每次工具调用一个块 |
57
+ | **会话** | `2` / `s` | 每个并发的 `dsh` 会话,实时;有多个在运行时,它也是首屏 |
58
+ | **改动** | `3` / `d` | 模型改过的每个文件,附 unified diff |
59
+ | **命令** | `4` / `c` | 每条 shell 命令及其输出、退出状态和耗时 |
60
+
61
+ ```
62
+ ┌─ dsh-live-trace · commands ──────────────────────────────────────────────────┐
63
+ │ Session: …a-412e-ac3d-c0f6a5bc824f · explain the plugin Status: ● idle │
64
+ ├──────────────────────────────────────────────────────────────────────────────┤
65
+ │ COMMANDS 2 run │
66
+ │ ──────────────────────────────────────────────────────────────────────────── │
67
+ │ ✓ ls -1 dsh-live-trace && echo "--- entry ---" && head -4 … 0.1s exit 0 │
68
+ │ Inspect the plugin package layout │
69
+ │ README.md bin cordis.patch.yml │
70
+ │ … 13 more lines │
71
+ │ │
72
+ │ ▸ ✓ cd dsh-live-trace && node --test test/width.test.js … 0.3s exit 0 │
73
+ │ Run the measurement and tool-helper tests │
74
+ │ ✔ every wrapped line fits the width budget (30.5ms) │
75
+ │ ℹ tests 22 │
76
+ ├──────────────────────────────────────────────────────────────────────────────┤
77
+ │ ● idle T1 2.9s Tokens 21K/1.0M ↓7 1:trace d:edits ?:help │
78
+ └──────────────────────────────────────────────────────────────────────────────┘
79
+ ```
80
+
81
+ 模型正文按 Markdown 渲染 —— 标题、列表、表格、行内样式 —— 每个围栏代码块都会做语法
82
+ 高亮,包括仍在流式输出的文本。**思考默认是收起的**:只显示几行,外加一个标记说明还有
83
+ 多少;`e` 会展开所有块。shell 命令按终端的方式显示,命令本身带有语法高亮:
84
+
85
+ ```
86
+ │ ┊ 先确认 login() 的所有调用点,再决定抽象边界。 │
87
+ │ ┊ 1. 只有两处直接调用,都在 src/routes/ 里。 │
88
+ │ ┊ … 5 more lines e to expand │
89
+ │ 11:58:09 [TOOL] bash Run the test suite ✓ exit 0 0.2s │
90
+ │ $ npm test │
91
+ │ 测试通过 (12 passed) │
92
+ ```
93
+
94
+ `$` 是暗色,命令名用函数色,flag 是属性色,运算符和带引号的字符串各有自己的 token
95
+ 颜色。
96
+
97
+ ```
98
+ │ │ 检查 │ 结果 │
99
+ │ │ 测试 │ 12 passed │
100
+ │ │ lint │ 2 warnings │
101
+ │ │ bash │
102
+ │ │ $ npm test │
103
+ │ │ ✓ 12 passed │
104
+ ```
105
+
106
+ 它**不是** TUI 聊天客户端。它从不向 agent 发送任何东西。
107
+
108
+ ---
109
+
110
+ ## 工作原理
111
+
112
+ ```
113
+ ┌──────────────────────────── dsh web / dsh headless / dsh tui ─────────────────┐
114
+ │ Host process │
115
+ │ │
116
+ │ Cordis event bus ──► dsh-live-trace plugin ──► TraceHub ──► unix socket │
117
+ │ session/event (normalize) (state) $DSH_HOME/ │
118
+ │ agent/assistant-stream live-trace/ │
119
+ │ agent/status, agent/error sockets/*.sock │
120
+ └───────────────────────────────────────────────────────────────────────────────┘
121
+ ▲
122
+ newline-delimited JSON
123
+ │
124
+ ┌──────────────────────────── another terminal window ─────────────┐ │
125
+ │ dsh-live-trace (separate process, read-only) ──────────────────┘ │
126
+ │ alternate screen · ANSI renderer · scroll · replay on attach │
127
+ └─────────────────────────────────────────────────────────────────────┘
128
+ ```
129
+
130
+ **对显而易见的那个设计做一处纠正。** Cordis 插件不是一个独立进程:
131
+ `ctx.on('session/event', …)` 会在运行 Harness 的那个进程内部触发。所以拆成了两部分:
132
+
133
+ - 一个 **Host 插件**(`index.js`),在进程内观察,并把归一化后的记录发布到一个 Unix
134
+ 域 socket 上;
135
+ - 一个**独立查看器**(`bin/dsh-live-trace.js`),在它自己的终端里运行,连接那个
136
+ socket 并渲染。
137
+
138
+ 插件在可做到的范围内是最强的只读:它不追加任何会话事件,不注册工具钩子,不改写
139
+ prompt,也从不写 stdout。它只是监听。
140
+
141
+ 选择 Unix socket(而不是 TCP 端口)是刻意的:它无法从另一台主机访问,不会和 Web UI
142
+ 的端口冲突,不需要一套认证方案,而它的文件权限本身就是访问控制。
143
+
144
+ ### 实际用到的事件词汇
145
+
146
+ Harness 发布的事件集合很小且精确。轨迹面板的映射如下;其它都是插件自己的事件类型,
147
+ 除非 `showUnknownEvents: true`,否则会被隐藏。
148
+
149
+ | 来源事件 | 标签 | 说明 |
150
+ | :--- | :--- | :--- |
151
+ | `session/created` / `session/disposed` | `[SESSION]` | 会话生命周期 |
152
+ | `turn/start` / `turn/end` | `[TURN n]` / `[TURN n END]` | 轮次开始会渲染成一条分隔线 |
153
+ | `step/start` / `step/end` | `[STEP n]` / `[STEP n END]` | 每个步骤一次模型调用 |
154
+ | `user/message` | `[USER]` / `[CONTEXT]` | 注入的上下文按其 `source.kind` 打标签 |
155
+ | `assistant/message` | `[ASSISTANT]` | 正文加一段 `thinking:` 摘录;携带 token 用量 |
156
+ | `assistant/attempt` | `[ATTEMPT]` | 一次没有提交任何消息的尝试 |
157
+ | `tool/call` + `tool/result` | `[TOOL]` | **一个块**:名称、它运行的命令、它的输出、`✓`/`✗ exit N` 和耗时。两条记录共享同一个 `key`,所以结果会原地升级正在显示的那一行。 |
158
+ | `tool/result` `meta.diffs` | `[TOOL]` + *改动* | 已应用的文件 hunk;新建文件(此前没有文本)会从这次调用的 `content` 参数重建 |
159
+ | `approval/asked` / `approval/decided` | `[APPROVAL]` | 同时会触发**等待审批** |
160
+ | `session/title` | `[TITLE]` | 同时更新头部 |
161
+ | `permission/preset`, `sandbox/mode`, `approval/policy` | `[POLICY]` | 启动时各占一行 |
162
+ | `agent/assistant-stream` (live) | `thinking` / `writing` | 合并后发布,绝不按 chunk;推理和可见文本各有自己的行 |
163
+ | `agent/status`, `agent/error` (live) | 状态栏 / `[ERROR]` | |
164
+ | `request/context` | *页脚* | 提供上下文窗口的分母 |
165
+
166
+ 总线上没有 `assistant/chunk` 事件 —— 实时流是 `agent/assistant-stream`,它的 chunk
167
+ 帧会被累积,并按固定节奏(`streamIntervalMs`,默认 500 ms)发布,这样再快的模型也
168
+ 刷不爆终端。持久事件 `assistant/message` 才让流落定。
169
+
170
+ ---
171
+
172
+ ## 安装
173
+
174
+ 它分两半:**插件**,由 Harness 加载,用来发布会话事件;以及**查看器**,也就是你运行的
175
+ 那个命令。
176
+
177
+ ### 插件
178
+
179
+ 在 npm 上发布为 [`dsh-live-trace`](https://www.npmjs.com/package/dsh-live-trace)。
180
+ 插件必须被你的 Harness 启动时所用的 profile 选中。
181
+
182
+ **A. 从 npm 安装**
183
+
184
+ ```sh
185
+ dsh plugin --profile web add dsh-live-trace
186
+ ```
187
+
188
+ 然后把 `"dsh-live-trace"` 加到 `$DSH_HOME/profiles/web/package.json` 里的
189
+ `dsh.profile.bundles` 中。
190
+
191
+ **B. 文件级安装器(离线,无包管理器)**
192
+
193
+ ```sh
194
+ node /path/to/dsh-live-trace/scripts/install-profile.mjs --profile web
195
+ ```
196
+
197
+ 它会把 `dsh-live-trace` 加进该 profile 的 `dsh.profile.bundles`,添加一条 `link:`
198
+ 依赖,并把包软链到该 profile 的 `node_modules`。重启 Harness(或者让热重载自动加载)。
199
+
200
+ **C. 从本地检出安装,由 pnpm 管理**
201
+
202
+ ```sh
203
+ dsh plugin --profile web add /path/to/dsh-live-trace
204
+ ```
205
+
206
+ 然后把 `"dsh-live-trace"` 加到 `$DSH_HOME/profiles/web/package.json` 里的
207
+ `dsh.profile.bundles` 中。这条路径需要 `PATH` 上有 `pnpm`。
208
+
209
+ 要撤销以上任一方式:`node scripts/install-profile.mjs --profile web --uninstall`。
210
+
211
+ ### 查看器
212
+
213
+ ```sh
214
+ npm install -g dsh-live-trace
215
+ ```
216
+
217
+ 这会把三个命令放进 `PATH`:`dsh-live-trace`(轨迹面板)、`dsh-live-working`
218
+ (小鲸鱼)和 `dsh-glyph-probe`(打印你的字体能渲染出什么)。如果是从检出目录运行,
219
+ 就直接从 `bin/` 里跑,或者手动 link:
220
+
221
+ ```sh
222
+ npm install -g .
223
+ ```
224
+
225
+ 不启动任何东西,先验证组合是否正确:
226
+
227
+ ```sh
228
+ dsh --profile web --dump-config | grep -A4 'dsh-live-trace'
229
+ ```
230
+
231
+ ### 运行轨迹面板
232
+
233
+ ```sh
234
+ dsh-live-trace
235
+ ```
236
+
237
+ 插件加载后,Harness 会在 `$DSH_HOME/live-trace/servers/` 下发布一条发现记录,写明它的
238
+ socket、工作目录和会话。查看器会清理掉已死的记录,优先选择当前工作目录里的进程,并绑定
239
+ 到最新的活跃会话。
240
+
241
+ ```
242
+ dsh-live-trace --list # what was discovered, then exit
243
+ dsh-live-trace --session <id> # bind one session
244
+ dsh-live-trace --socket <path> # bypass discovery entirely
245
+ dsh-live-trace --runtime-dir <path> # non-default $DSH_HOME
246
+ dsh-live-trace --plain # one line per event, pipe-friendly
247
+ dsh-live-trace --wait # poll until a Harness appears
248
+ ```
249
+
250
+ ### 按键
251
+
252
+ | 按键 | 动作 |
253
+ | :--- | :--- |
254
+ | `1` / `t` | 轨迹面板 |
255
+ | `2` / `s` | 会话选择器(`esc` 返回到下层那个面板) |
256
+ | `3` / `d` | 文件改动面板 |
257
+ | `4` / `c` | 命令面板 |
258
+ | `tab` | 循环切换面板 |
259
+ | `↑` / `↓` | 滚动轨迹,或在列表面板里移动选中项 |
260
+ | `PgUp` / `PgDn` | 翻一页 |
261
+ | `Home` / `End` | 最早 / 最新 |
262
+ | `enter` | 绑定高亮的会话 |
263
+ | `esc` | 先离开选择器,再离开面板;只有从轨迹里才会退出 |
264
+ | `e` | 展开或收起所有思考块 |
265
+ | `m` | 切换 Markdown 渲染(显示原始源码) |
266
+ | wheel | 任何面板下,滚轮每格滚动三行 |
267
+ | click | 选中指针下的那一行 |
268
+ | `p` | 暂停或恢复跟随新条目 |
269
+ | `r` | 请求插件重放本会话 |
270
+ | `?` | 按键帮助 |
271
+ | `q`, `Ctrl-C` | 退出 |
272
+
273
+ ### 首屏
274
+
275
+ 当有多个会话在运行、又没有给 `--session` 时,`dsh-live-trace` 会打开**会话选择器**
276
+ —— 同时跑着好几个 `dsh` 会话时,做选择是你第一件要做的事。只有一个会话时它直接进入
277
+ 轨迹,而显式给出 `--session <id>` 永远优先。
278
+
279
+ ## 多会话切换
280
+
281
+ Harness 知道的每个会话都会实时列出,带上它的活动、轮次、步骤、安静了多久、标题和工作
282
+ 目录。`↑`/`↓` 移动选中项,`enter` 绑定它;插件随后重放该会话的积压内容,所以切换过去
283
+ 永远不会看到空白面板。如果你正在跟随的会话结束了,查看器会自动跟随下一个活跃会话。
284
+
285
+ ---
286
+
287
+ ## 配置
288
+
289
+ 每个键都是可选的;插件会防御性地校验,并在取值不合法时回落到默认值,而不是拒绝启动。
290
+ 把它们写进 `$DSH_HOME/profiles/<profile>/cordis.patch.yml`:
291
+
292
+ ```yaml
293
+ - id: dsh-live-trace
294
+ config:
295
+ enabled: true
296
+ streamIntervalMs: 500 # 50…60000 — coalesced streaming cadence
297
+ backlogSize: 2000 # 10…100000 — entries retained per session
298
+ heartbeatMs: 5000 # discovery/heartbeat cadence
299
+ replayLimit: 500 # entries replayed to a late-joining viewer
300
+ showSystemMessages: false # render the system prompt as an entry
301
+ showRequestMetadata: false # render request/header as entries
302
+ showUnknownEvents: false # render unrecognized plugin events
303
+ mutedEventTypes: # `*` is a prefix match
304
+ - 'session-log-deepseek/*'
305
+ textLimit: 4000 # cap on one entry's primary text
306
+ outputLines: 200 # 1…100000 — command output lines retained
307
+ outputChars: 20000 # 200…1000000 — command output characters retained
308
+ runtimeDir: null # override $DSH_HOME/live-trace
309
+ socketPath: null # override the derived socket path
310
+ ```
311
+
312
+ 插件刻意不声明 `Config` schema:它必须能在解析不到 schema 包的 profile 里加载,而写错
313
+ 的选项应该退化到默认值,而不是阻塞启动。
314
+
315
+ ---
316
+
317
+ ## 线路协议(v1)
318
+
319
+ socket 上跑的是按行分隔的 JSON。每条记录都带 `v` 和 `kind`;未知的 kind 在双向都会被
320
+ 忽略,所以任意一半都可以先升级。
321
+
322
+ **插件 → 查看器:** `hello`(服务器身份、会话列表、活跃会话)、`sessions`、`entry`
323
+ (一行归一化的轨迹)、`stream`(合并后的实时文本)、`stream-end`、`status`、
324
+ `usage`、`edits`、`heartbeat`、`error`。
325
+
326
+ **查看器 → 插件:** `select`(绑定一个会话;省略 id 表示"服务器的默认会话")、
327
+ `replay`、`ping`。
328
+
329
+ 一条 `entry` 可以带 `key`;两条 key 相同的记录是同一行,后一条原地替换前一条。工具调用
330
+ 和它的落定之所以能保持为一个块、而不是两条半截的行,靠的就是这个。
331
+
332
+ 会话作用域的记录只会到达绑定该会话的查看器,而 `sessions` 记录会按每个查看器实际持有的
333
+ `boundSessionId` 做个性化。没有指定会话的查看器会一直询问,直到出现一个会话,然后跟随
334
+ 它 —— 所以在第一个会话出现之前就打开轨迹面板是可以的,而会话结束后视图会交给下一个。
335
+
336
+ ---
337
+
338
+ ## 验证
339
+
340
+ `npm test` 会运行 243 个测试(`node --test`),包括:
341
+
342
+ - **渲染器不变量** —— 宽度 24…200、高度 8…60 时,每一帧的每一行都*恰好*等于终端宽度,
343
+ 包括含 CJK 文本、emoji,以及试图注入转义序列的内容。
344
+ - **测量** —— `displayWidth` 把汉字/假名/谚文/emoji 算作两个单元格,把组合字符算作
345
+ 零;折行和截断从不切开一个宽字符。
346
+ - **归一化** —— 每一种被映射的事件类型,外加畸形参数、未知错误和未知事件类型。
347
+ - **Markdown 与高亮** —— 对每种受支持的语言都有一条强制的字符保真不变量;一组文档样本
348
+ 和一轮对抗性 fuzz 在六种宽度下的宽度约束;未闭合的围栏(流式场景);以及表格。
349
+ - **工具块、diff 与命令** —— 按 key 配对调用/结果、shell 退出状态标记契约
350
+ (`[exit code: N]`、`[killed by signal: X]`)、diff 元数据的收窄、新建文件的整文件
351
+ 重建,以及反复编辑时按路径累积。
352
+ - **面板** —— 每个面板在每种尺寸下都守住所占宽度精确的契约,并且每个面板都会解释空
353
+ 会话,而不是显示一个空盒子。
354
+ - **窗口化** —— 在每种尺寸和滚动位置下,窗口化的帧逐字节地画出无界渲染会画的行,并且
355
+ 回滚环形缓冲在裁剪时不会丢失它的 key 索引。
356
+ - **输入** —— 方向/导航键、被拆分到多次 read 里的 SGR 鼠标上报、滚轮映射、点击的按下
357
+ 与抬起、移动事件被拒绝,以及未终止序列的有界缓冲。
358
+ - **传输** —— 跨分片的 NDJSON 分帧、按查看器的会话路由、接入时重放、服务器重启后重
359
+ 连,以及拆卸清理。
360
+ - **对真实 Harness 的集成** —— 在真实的 Cordis 上下文里启动随包发布的
361
+ `@deepseek-ai/dsh-session` 插件,创建并追加真实会话,并断言查看器从一个真实 socket
362
+ 上收到了什么。没有安装 Harness 时它会干净地跳过。
363
+ - **端到端 CLI** —— 针对真实的观察器 socket 启动真正的 `dsh-live-trace` 可执行文件:
364
+ 发现流程、`--list`、备用屏幕面板在 `q` 时退出、捕获输出里 80 列的行宽、plain 模式,
365
+ 以及找不到观察器时的诊断信息。
366
+ - **拆卸后不留东西** —— 三轮 mount/unmount 循环不会累积任何实例、socket 或注册表
367
+ 记录;另一个把一切都启动再 dispose 的独立程序必须**自己退出**,所以一个还活着的
368
+ socket 或定时器会让它挂住。把这个程序里的 dispose 调用去掉它就会挂住,这就是证明该
369
+ 检查确实有效的反向对照。
370
+
371
+ 在测试套件之外,这个包还针对一个已安装的 Harness 验证过:
372
+
373
+ 1. `dsh --profile web --dump-config` 会组合出插件的配置行,并把用户的 patch 层应用
374
+ 上去。
375
+ 2. 一个真实的 `dsh web` 进程会启动插件,插件创建自己的 socket 和发现记录;
376
+ `dsh-live-trace --list` 能找到它,面板也就接上了。
377
+ 3. 一个真实的 `dsh headless` agent 循环(由 `scripts/mock-provider.mjs` 驱动,那是一个
378
+ 离线的 Messages API 服务器)会把真实的轮次、步骤、工具、结果和 token 事件流进
379
+ 面板,其中包括一次真实的 `write`,它实际应用的 diff 会出现在改动面板里。
380
+
381
+ ## `dsh-live-working` — 小鲸鱼
382
+
383
+ 轨迹面板告诉你*发生过*什么。这个告诉你正在发生什么:一只像素画小鲸鱼坐在桌前,桌上有
384
+ 键盘、一摞书和一部红色电话,动画会跟着会话变化。
385
+
386
+ ```
387
+ ▄█接 2 号子代理████████████████▄
388
+ ██把 auth 模块里的校验逻辑抽出来██
389
+ ▀██████████████████████████▀
390
+ ▄█▀
391
+ ▀▀ ▄▄██▄▄▄▄
392
+ ▀▀▀▀▀▀▀▀ ▄▄
393
+ ████████ ██████▄▄
394
+ ▄▄▀▀ ████████▄▄
395
+ ████████ ████████████
396
+ ██████████ ▄▄██████████████████▄▄▄▄ ▀▀██████████
397
+ █ █ ▄▄████████████████████████▄▄▄▄ ████████▀▀
398
+ ▄▄█████████████████████████████████▄▄ ▄▄██████
399
+ ██████████████████████████████████████▄▄▄▄██████
400
+ ████████████████████████████████████████████████
401
+ ██████████████████████████████████████████████
402
+ ▀▀██████████████████████████████████████████▀▀
403
+ ▀▀██████████████████████████████████████▄▄▄▄▄▄
404
+ ▄▄██████████████████████████████████████████
405
+ ████████████████████████████████████████████
406
+ ▄██████████▄ ██████████████████████ ████████████████
407
+ ████████████ ██████████████████████ ████████████████
408
+ ██████████████████████████████████████████████████████████████████
409
+ ██████████████████████████████████████████████████████████████████
410
+ ███ ███
411
+ ```
412
+
413
+ 场景会**按你的终端调整尺寸** —— 最宽到 200 个单元格,小鲸鱼会视可用空间按 1×、2× 或
414
+ 3× 绘制,所以宽终端会得到一只更大的生物,而不是同一只漂在巨大空桌子上。像素画只按整数
415
+ 倍缩放;非整数倍会产生大小不一的块,看起来像渲染故障。
416
+
417
+ 精灵图是按参考美术的**原生 64×40 像素网格**采样的(那份美术里的块宽为五个源像素),
418
+ 所以原图一行都不会丢。源图里鲸鱼上方的灰色痕迹,是一张*睡觉的*鲸鱼插画里的三个 `Z`
419
+ 字形 —— 采样时把它们排除掉,于是这只生物落在干净的透明背景上,由场景自己画 `z`,而且
420
+ 只在它真的睡着时才画。
421
+
422
+ ### 它做什么,以及为什么
423
+
424
+ | 状态 | 触发条件 | 动画 |
425
+ | :--- | :--- | :--- |
426
+ | `sleep` | 没有工作;**或者**某条正在运行的命令已经 8 秒没有输出 | 闭眼、慢慢上下浮动、`z` 向上飘 |
427
+ | `thinking` | 推理正在流式输出,或者 agent 正在等待审批 | 手离开键盘,一串思考的点 |
428
+ | `typing` | 正文或代码正在流式输出 | 按键成排亮起 |
429
+ | `writing` | 正在运行 write/edit 工具 | 桌上的一张纸增加一行 |
430
+ | `waiting` | shell 命令正在运行且仍在输出 | 一个带闪烁光标的终端块 |
431
+ | `reading` | 正在运行 read/glob/grep 工具 | **眯起眼睛,每隔几秒眨一次**,举着一本打开的书 |
432
+ | `searching` | 正在运行网络搜索或抓取 | **翻过一页**,从右向左扫过书脊 |
433
+ | `calling` | 启动了一个子代理 | **红色电话被拿起,斜着贴在耳边**,手离开键盘,并出现一个气泡 |
434
+ | `ringing` | 一个子代理结束了 | 电话**响起并震动**,气泡里显示它带回了什么 |
435
+
436
+ ### 两种背景:`room` 与 `nature`
437
+
438
+ `--scene room`(默认)是一张靠窗的桌子:一堵墙,墙上开着一扇窗,天空被限制在窗格里。
439
+ `--scene nature` 拿掉了墙 —— 背景*就是*户外,于是天气覆盖整个场景,地面一直延伸到屏幕
440
+ 底部、桌子就立在上面,针叶树沿地平线排开。任何时候按 `b` 都能在两者之间切换。
441
+
442
+ 两者都由同一片天空构成。窗户和户外都会先在自己的画布上合成内容,再做 blit,所以"任何
443
+ 东西都不会跑出分配给它的区域"这条约束对两者都成立。
444
+
445
+ ### 房间
446
+
447
+ **房间由它的窗户照亮。** 墙面、墙面的明暗、踢脚线和地板每一帧都从天空推导出来,所以
448
+ 房间夜里会变暗、正午会变亮,而不是固定的中灰色 —— 黑色天空旁边一块亮灰色板,是夜景里
449
+ 最糟糕的一点。墙面从正午的 `181,176,166` 变到午夜的 `64,66,75`,并且从不下探到某个
450
+ 下限以下,所以房间始终清晰可读。
451
+
452
+ 这个场景是一个房间,而不是一只漂在终端背景色上的精灵图:一堵带浅色纸条纹的墙,墙与
453
+ 地板相接处的踢脚线,以及立在那块地板上的桌子。窗户是**墙上的一个开口**,于是其它一切
454
+ 都有东西作为衬托来阅读 —— 尤其是睡觉的 `z`:它以前画在天空上,会消失在云里;搬到墙上
455
+ 之后又变成了中灰色墙上的中灰色。现在它有了自己的颜色。
456
+
457
+ 窗户里的一切 —— 天空、太阳和月亮、云、雨、雪、雾 —— 都在窗户自己的画布上合成后再
458
+ blit 进来,所以它们谁都不能飘出这个开口、爬到墙上。
459
+
460
+ ### 窗外
461
+
462
+ 墙上开着一扇窗,窗外是一个按自己的时间运行的世界。**每过一秒真实时间,游戏内就过去
463
+ 一分钟**,所以一整天是二十四分钟真实时间,天空也从不静止:颜色从午夜蓝经黎明橙走到
464
+ 正午蓝再走回来,太阳和月亮在窗格间沿弧线轮流出现,星星只在夜里现身。
465
+
466
+ 那里也会下雨。天气每三个游戏内小时自行变化一次,取值来自这段天气的序号,而不是随机数
467
+ 生成器,所以两个看着同一会话的查看器看到的是同一片天空。它是淡入淡出而不是硬切,并且
468
+ **白天下的雨到夜里就变成雪** —— 同一种天气,只差一个温度。
469
+
470
+ 窗户是一幅画,不是一块色卡:
471
+
472
+ - **天空是渐变的。** 终端没有 alpha,但它有网格:两种颜色加一个 4x4 Bayer 抖动,让
473
+ 天空在头顶更暗、靠近地平线更亮,并在黎明和黄昏把地平线染暖、同时顶部保持冷色。两种
474
+ 平涂颜色看起来是一条色带;加上抖动才像天空。
475
+ - **有山。** 窗格底部一道起伏的山脊让这扇窗有了归属,太阳和月亮沉到它后面。
476
+ - **太阳和月亮有光晕**,经过抖动,所以是淡出而不是戛然而止。
477
+ - **云是积云**:三个互相重叠的椭圆,下面一边是平的 —— 不是它们最初那样的横条。
478
+ - **雾是一片薄霭,不是一条带。** 它覆盖窗格的四分之一而不是三分之一,最多只填一半,
479
+ 越往上越稀,颜色取自天空而不是固定的浅灰 —— 所以它融进天气里,而不是看起来像一条
480
+ 灰条纹。实测下来,它从占窗格的 23% 降到 5%。
481
+ - **星星有两种亮度**,各按自己的节奏闪烁。
482
+
483
+ 天气会**从天空里抽走光**:晴天的正午是 765 中的 535,多云 439,下雨 331,暴风雨 240。
484
+ 恶劣天气还会遮住太阳或月亮,这正是它看起来"恶劣"的原因。只有天空真正暗下来时星星才会
485
+ 出现 —— 若改成按昼夜阶段来决定,就会在明亮的橙色黎明里点出白点。页脚显示游戏内时钟和
486
+ 当前天气。
487
+
488
+ **雨可以下出声。** `--sound`(或任何时候按 `n`)会通过 `aplay`、`paplay`、`sox` 或
489
+ `ffplay` 中已安装的那个播放雨声,而且只在真的下雨时播放。它**默认关闭**,因为一条会
490
+ 自己开始放声音的命令,是那种人们会不再运行的命令。
491
+
492
+ **音量是单独的一项控制。** `--rain-volume <0-100>` 设定它(默认 `40`),命令运行期间
493
+ `-` 和 `+` 每次调整 5,页脚会在声音状态旁边显示当前音量。它从 40 而不是满量程起步是
494
+ 刻意的:那段录音的峰值是 -6.1 dBFS,属于前景音量;而在 40% 时峰值约 -14 dBFS —— 是
495
+ 环境音,而不是需要关掉的东西。合成的噪声按同一比例缩放,所以平均采样值从 2023 变成
496
+ 809(峰值从 5898 到 2359,从 -14.9 到 -22.9 dBFS):同样的雨,只是少一些。
497
+
498
+ 除了一条路径,音量在所有路径上都能到达扬声器。在 **file** 模式下 `paplay` 收到
499
+ `--volume`,`sox` 收到 `-v`,`ffplay` 收到 `-volume`;合成流把音量做进自己的采样里,
500
+ 所以每个播放器在那里都遵守它。**`aplay` 根本没有音量参数**,所以通过 `aplay` 播放的
501
+ 录音,听到的就是它被录制时的音量。这一点没有被悄悄忽略:页脚会写
502
+ `aplay cannot change a file level`,而不是打印一个会撒谎的百分比。如果你在一台只有
503
+ `aplay` 的机器上想要更安静的录音,就传一个更安静的 `--rain-file`,或者装上 `paplay`、
504
+ `sox` 或 `ffplay`。
505
+
506
+ 默认音源是随包附带的那段录音。`--rain-file` 可以指定别的。如果文件不存在 —— 或者资源
507
+ 读不出来 —— 就会回落到**合成**噪声:一段经过低通的伪随机采样流,永续播放,没有循环点。
508
+ 正是靠它,这个功能在一台只有一个播放器的机器上也能用。
509
+
510
+ 录音播放期间改变音量会重启播放器,因为音量存在于它的命令行里;合成流则会在下一个块
511
+ 无缝地采用新音量。
512
+
513
+ 改变音量会重启播放器,因为音量是它的一个参数。旧播放器会先被停掉,但它的退出事件是在
514
+ 新播放器已经跑起来*之后*才到的 —— 所以状态更新只在来自当前那个进程时才被接受,而所有
515
+ 启动过的进程都会被跟踪并一起杀掉。两者缺一,改一次音量就会留下两路雨声在响,以及一个
516
+ 在命令退出后还在继续的孤儿播放器。
517
+
518
+ 一个刚启动就死掉的播放器说明没有音频设备;它会重试三次,然后就不再管它,而不是永远重启
519
+ 下去。
520
+
521
+ **包里附带一段雨声循环。** `assets/rain.ogg`(30 秒、单声道、64 kbps、242 KB)是默认
522
+ 音源,所以 `--sound` 不需要任何参数。它是从那段 8 小时的录音
523
+ `42130539966-1-192.mp4`(其音频为 aac 48 kHz 立体声)的一小时处剪下来的,并把尾部
524
+ 交叉淡入到开头,让循环没有咔哒声:两端音量相差在 20% 以内,接缝处的采样落差是全量程的
525
+ 0.35%。它的峰值是 -6.1 dBFS;在默认的 40% 音量下约为 -14 dBFS。`--rain-file` 可以
526
+ 覆盖它。
527
+
528
+ **关于这个目录里那个 2.2 GB 的 `42130539966-1-192.mp4`:** 它不是包的一部分
529
+ (`package.json` 的 `files` 列表里没有它,`.gitignore` 也忽略了 `*.mp4`),而这台机器
530
+ 上没有 `ffprobe`、`ffmpeg`、`aplay`、`paplay`、`sox` 或 `mpv` —— `ffplay` 倒是有,
531
+ 但它只能播放文件、不能检查文件 —— 所以这个视频在这里既看不了也放不了。`--rain-file`
532
+ 接受的是音频文件;`.mp4` 是视频容器,得先把音轨抽出来,而那需要 `ffmpeg`:
533
+
534
+ ```
535
+ ffmpeg -i 42130539966-1-192.mp4 -vn -ac 1 -ar 44100 rain.wav
536
+ dsh-live-working --sound --rain-file rain.wav
537
+ ```
538
+
539
+ 要放真实录音,就传 `--rain-file`;`--rain-volume` 在所有带音量参数的播放器上对它依然
540
+ 有效:
541
+
542
+ ```
543
+ dsh-live-working --sound --rain-volume 25 --rain-file ~/sounds/rain-loop.wav
544
+ ```
545
+
546
+ 不存在的文件会被放弃并回落到合成噪声,而不是变成静音。`ffplay` 原生支持循环播放文件;
547
+ 其它播放器会在文件结束时被重启,但只在还在下雨的时候。授权由你判断、也由你负责 ——
548
+ 本项目自己不分发任何音频:
549
+
550
+ - [Wikimedia Commons: Sounds of rain](https://commons.wikimedia.org/wiki/Category:Sounds_of_rain) —— 自由许可,逐文件条款
551
+ - [Freesound](https://freesound.org/) —— 按许可证筛选;CC0 无需署名
552
+ - [Creazilla: Ambience Rainstorm](https://creazilla.com/media/audio/15525146/ambience-rainstorm) —— 免版税
553
+ - [Internet Archive: Red Library — Nature Rain](https://archive.org/details/Red_Library_Nature_Rain)
554
+
555
+ ```
556
+ 工作台 · 等待命令 06:13 雾 c4c9e25b082a T23·S19 关闭本帮助:?
557
+ ```
558
+
559
+ ### 小鲸鱼自身的动作
560
+
561
+ - **尾巴是画出来的,每个状态一个姿态。** 源美术只有一条竖直的尾巴,没有可动的地方:
562
+ 尾鳍顶到身体的最后一行和精灵图的右边缘,而桌子就压在最后一行上。对着它试过六种
563
+ 变换 —— 剪切、折叠、旋转、锥形剪切、整体平移、水平折叠 —— 每一种都只是把一种瑕疵
564
+ 换成另一种:边缘毛糙、轮廓撕裂、尾巴跑到桌子下面、桌子上面多出尖角,或者透出背景的
565
+ 空洞。现在有**两个姿态**,由身体加一条尾巴组合而成:
566
+
567
+ - `up` 直接从源美术里逐像素取出,所以醒着的小鲸鱼就是参考图画的那个样子;
568
+ - `sleep` 是画出来的:尾鳍垂下来,沿身体贴在桌线处,末端收成圆头;它的边缘是生成
569
+ 出来的,而不是丢给渲染器去猜。
570
+
571
+ 画出来的姿态不会丢像素、撕裂或留下空隙 —— 这些故障在结构上就不可能发生,而不只是被
572
+ 修好了。
573
+
574
+ - **醒着的尾巴从根部摆动。** 以前整条尾巴是刚性平移的,这会让它离开身体、从接缝处透出
575
+ 背景;现在最靠近身体的那些列被锚定,只有外面的一部分摆动,于是尾巴是绕轴转,而不是
576
+ 平移。小鲸鱼打电话时它保持不动,因为鳍正忙着。
577
+
578
+ - **夜里它会揉眼睛** —— 工作期间时不时闭上眼、把一只鳍抬到脸上,然后接着做原来的事。
579
+ 只在夜里,而且睡着或打电话时绝不会。
580
+
581
+ ### 桌子
582
+
583
+ 桌子是"搭"出来的,而不是贴上去的:键盘是桌上一小条深色横条,而**右上角一个带边框的
584
+ 窗口显示正在输入的文本** —— 模型输出的尾部,或者它正在运行的工具的参数。有文本时就会
585
+ 画这个窗口的边框,因为没有边框的文本看起来像一个漂在终端顶部的野气泡。
586
+
587
+ 小鲸鱼打字时**鳍会动**,用的是一种和身体略有色差的蓝,这样这条肢体才看起来像肢体。
588
+ 采样的参考美术是侧视图,没有单独的胸鳍,所以这些划水的鳍是画上去并叠在按键上的动画;
589
+ **睡着时它们会被收起来**。打电话会完全停止打字 —— 听筒贴在脸上,上到眼睛、下过下巴,
590
+ 而不是横在身体上或者直立在桌上。
591
+
592
+ 书是有明暗的,这正是让翻页看起来清楚的原因:摊开的两页受光不同,中间的书脊落在阴影
593
+ 里,而翻到一半的那一页是侧对着的,比两边都暗。
594
+
595
+ 电话气泡上写的是 `接 N 号子代理,<instructions>`(英文下是
596
+ `Subagent #N — <instructions>`),其中 N 是本会话里子代理的序号,instructions 是这次
597
+ 调用收到的参数。
598
+
599
+ ### 电话
600
+
601
+ 派发一个子代理就是一通电话,整个交互都是照这个来建模的:
602
+
603
+ - **气泡显示的是调用原本写成的样子**,不只是里面的指令:
604
+ `task(description="…", prompt="…")`,连工具名和每一个参数一起。同时有多个子代理在
605
+ 外面时,抬头会变成一个队列 —— `接 3 号子代理(共 5 个,排队 2 个)`。
606
+ - **消息是一个字一个字"说"出来的**,气泡一次显示三行。更长的消息会滚动。抬头被钉在
607
+ 上面:它说明电话那头是谁,若被长指令挤掉,气泡就变成了无名的。
608
+ - **子代理挂断时电话会响。** 听筒震动,电话两边出现铃声弧线,气泡显示子代理自己的收尾
609
+ 文本 —— 它的回答,而不是对回答的概括。
610
+ - **后台派发是"收到",不是"回答"。** 在后台启动一个子代理会立刻返回
611
+ `started subagent <id>`;回答稍后作为那个 agent 的消息到达。只有后者才算数,所以后台
612
+ 子代理会保住它在队列里的位置,而不是一启动就好像已经结束。
613
+ - **听筒是被拿起和放下的**,不是瞬间移动:它从容地离开支架,旋转到耳边,放下时沿同
614
+ 一条路径返回,电话线随着它升起而松开。
615
+ - **历史不是新闻。** 查看器启动时会重放积压内容,而每条记录都带自己的时间戳。若改用
616
+ 墙上时钟给事件打时间戳,一小时的历史看起来就像全都正在发生,于是电话会为几分钟前就
617
+ 已经结束的子代理响起来。现在每个时间戳都来自事件本身。
618
+ - **活派出去、没别的事可做,就是打个盹。** 子代理在外面、又没有别的工具在跑时,小
619
+ 鲸鱼就像空闲时一样在桌前打盹,而叫醒它的是响起的电话。
620
+
621
+ 气泡锚定在小鲸鱼头顶上方,尾巴指向说话者,并会裁剪以避开预览窗口。一个钉在场景角落的
622
+ 对话气泡什么都没指着,看起来就像一个漂在界面外的野盒子 —— 这正是它两次被反馈的问题。
623
+
624
+ ```bash
625
+ dsh-live-working # follow the server's default session
626
+ dsh-live-working -s <session-id> # follow a specific one
627
+ dsh-live-working --state reading # pin one animation, ignore the session
628
+ dsh-live-working --list # show observers and sessions
629
+ ```
630
+
631
+ 有多个会话在运行时,它打开的是**会话选择器**,而不是去猜你指的是哪个;`s` 重新打开
632
+ 它,`↑`/`↓` 选择,`enter` 绑定,`esc` 取消。
633
+
634
+ 按键:`1`-`8` 固定某个动画,`0` 回到自动,`s` 选择会话,`n` 开关雨声,`-` 和 `+`
635
+ 调整雨声音量,`l` 切换语言,`?` 开关帮助,`q` 退出。
636
+
637
+ ```
638
+ dsh-live-working — a live orca animation for a DeepSeek Harness session
639
+
640
+ States: sleep, thinking, typing, writing, waiting, reading, searching, calling
641
+ ```
642
+
643
+ `node scripts/demo-working.mjs` 会循环播放每一个状态,不需要会话;
644
+ `node scripts/demo-working.mjs reading` 则固定在某一个状态。
645
+
646
+ ### 一个终端单元格能承载多少分辨率
647
+
648
+ 一个字符单元格的高度大约是宽度的两倍,所以选哪种字形,同时决定了像素的分辨率和*形状*:
649
+
650
+ | 打包方式 | 每格像素 | 像素形状 | 字形 | 说明 |
651
+ | :--------- | :---------- | :---------- | :-------------------- | :--------------------------------------- |
652
+ | `half` | 1 x 2 | 正方形 | `▀ ▄ █` | 这个轨迹面板用的就是它 |
653
+ | `quadrant` | 2 x 2 | 1:2 高 | `▘ ▝ ▖ ▗ ▚ ▞ ▛ ▜ ▙ ▟` | 边缘更细,但一切都拉长了 |
654
+ | `braille` | 2 x 4 | 正方形 | `U+2800..U+28FF` | 像素是 4 倍,但每一个都是一个点 |
655
+
656
+ `half` 是唯一既**正方形**又**实心**的打包方式,而实心像素画正需要这个。`quadrant` 把
657
+ 横向数量翻倍,但它的像素高是宽的两倍,所以为正方形像素画的图会显得被拉长。`braille`
658
+ 用正方形像素给出四倍的像素,但点与点之间有间隙,所以实心区域会填成点画 —— 画图表很好,
659
+ 画鲸鱼很差。
660
+
661
+ 它们能不能用取决于你的字体,而缺少这些字形的字体不会大声报错:它会替换成方框或空白,
662
+ 于是画面悄悄散架。所以先看一眼:
663
+
664
+ ```
665
+ dsh-glyph-probe
666
+ ```
667
+
668
+ 它会打印每种打包方式的样例,以及一个按该打包方式自身分辨率栅格化的圆盘。看起来圆润
669
+ 无缝的那个圆盘,就是你的字体支持的那一种。
670
+
671
+ ### 程序能让终端字体变小吗?
672
+
673
+ **不能。** 没有这样的转义序列。`ESC[?3h` 只是在 80 列和 132 列之间切换,并不改变
674
+ 字体,而 VTE/GNOME Terminal 根本没有办法做到 —— 这可以直接从[源码讨论](https://stackoverflow.com/revisions/0162962e-7b66-4d60-8df4-4278446e6220/view-source)里读到。kitty 的[文本缩放协议](https://github.com/kovidgoyal/kitty/blob/f13c8cd4/docs/text-sizing-protocol.rst)缩放的是单段文本,而且只适用于 kitty。
675
+
676
+ 即便在可能生效的地方,把它交给应用去管也是一笔坏交易:一次崩溃、一个 `SIGKILL`、一个
677
+ 被关掉的终端或一次断开的 SSH 会话,都会让字体留在小号状态,而没有任何东西能把它恢复。
678
+
679
+ 终端*愿意*做的是**报告自己的几何尺寸**,而这一半是值得要的 —— `CSI 16t` 返回以像素
680
+ 为单位的单元格尺寸,`CSI 14t` 返回文本区域。`dsh-glyph-probe` 两者都问,并做这道
681
+ 算术:
682
+
683
+ ```
684
+ Cell size 9x18 px (aspect 0.50)
685
+ Cells 148x40
686
+ Drawing budget 11,840 pixels (two per cell)
687
+ Half the font 333x80 cells -> 53,280 pixels
688
+ ```
689
+
690
+ 把字体缩小并不改变窗口的像素;它改变的是能塞进这些像素的**单元格**数量,而每多一个
691
+ 单元格,就多两个像素的绘制预算。那才是真正的收益,而且这份收益该由用户自己去拿。
692
+
693
+ 在伸手去够更高分辨率的打包方式之前,有两件事值得知道:
694
+
695
+ - **鲸鱼就是它源文件的分辨率。** 参考美术是一个 64x40 的精灵图,给不出更多细节;放大
696
+ 它只会得到更大的块。想要更精细的鲸鱼,需要更精细的美术,而不是更精细的渲染器。
697
+ - **场景已经占满终端。** 它最宽按 200 个单元格渲染,并且用满它能用的每一行,所以想更
698
+ 精确,最便宜的办法是开一个更大的窗口 —— 不管用什么字形,单元格更多就是像素更多。
699
+
700
+ ## 两个会话列表都以标题开头
701
+
702
+ `dsh-live-working` 的选择器和 `dsh-live-trace` 的会话面板都把会话标题放在最前面,缩短
703
+ 过的 id 跟在后面。id 是你要输入的东西,而标题并不唯一,所以两者都显示 —— 但能让你认
704
+ 出来的是标题,所以它排在前面。没有标题的会话回落到它的 id。
705
+
706
+ ## 曾经是 bug 的布局规则
707
+
708
+ - **实时思考是一个块,不是一行。** 仍在流式输出的推理会折行并尾部优先显示,收起时深度
709
+ 为 `--thinking-lines`,展开时是它的四倍。它以前只是一行,装着整个块最后几个字符,
710
+ 偏偏在你想看的时候完全没法读。
711
+ - **实时思考会走 Markdown 渲染器。** 它以前按纯文本折行,于是 `**bold**`、反引号和
712
+ `[links](url)` 都按字面语法显示,而它下面已经落定的块却会把它们渲染出来 —— 同一段
713
+ 推理,两种格式。
714
+ - **头部永远不会弄丢会话。** 各段带优先级:会话 id 和状态标签是必需的,永远不会被丢掉;
715
+ 标题、路径和错误文本会被丢弃 —— 先丢标题 —— 然后宁可缩短状态文本,也不把会话挤掉。
716
+
717
+ ## 性能
718
+
719
+ `npm run bench` 用不断增长的日志来渲染帧。因为一帧只显示尾部,每帧的开销必须保持平坦:
720
+
721
+ ```
722
+ entries= 50 per-frame= 2.3ms
723
+ entries= 1000 per-frame= 2.9ms
724
+ entries= 10000 per-frame= 9.2ms
725
+ ```
726
+
727
+ 早先的实现每一帧都渲染所有条目 —— 3000 条时每帧 2100 ms,这就是长会话里滚轮感觉卡的
728
+ 原因。最坏情况超过 30 ms 时,基准测试会失败。
729
+
730
+ ### 离线复现实时演示
731
+
732
+ ```sh
733
+ node scripts/mock-provider.mjs &
734
+ DSH_HOME=/tmp/dsh-demo dsh --profile headless "explain the plugin" &
735
+ DEEPSEEK_BASE_URL=http://127.0.0.1:8799 DEEPSEEK_API_KEY=sk-mock dsh-live-trace
736
+ ```
737
+
738
+ ### 不启动任何 Harness 也能看到面板
739
+
740
+ ```sh
741
+ npm run demo # scripts/demo.mjs — a scripted trace on the real renderer
742
+ ```
743
+
744
+ ---
745
+
746
+ ## 设计说明
747
+
748
+ - **零运行时依赖。** 渲染器是手写的 ANSI,而不是 `blessed`/`chalk`。`blessed` 测量
749
+ CJK 有误(Harness 的主要用户写中文),不再维护,而且会迫使 profile 里做一次联网
750
+ 安装;基于 `Segment[]` 的渲染器把颜色挡在字符串解析之外,也让 80 列的契约不需要 TTY
751
+ 就能测试。
752
+ - **插件无法弄坏 agent。** 每一次 hub 调用都被包住,所以观察器的一个 bug 只会变成丢一
753
+ 行,而不会变成一次失败的轮次。什么都不写 stdout —— 它属于 Harness 正在驱动的那个
754
+ 界面。
755
+ - **清理由 Cordis fiber 负责。** 每个 `ctx.on` 的 disposer 都被收集,每个定时器都被
756
+ 清除,socket 被关闭并 unlink,发现记录被删除 —— 但仅当它仍然是我们的:这样一代热重
757
+ 载就不会删掉它继任者的记录。
758
+ - **重复会被折叠,而不是隐藏。** 连续相同的条目只渲染一次,显示为 `×N` 并带上最新的
759
+ 时间戳。
760
+ - **每次工具调用一行。** 插件给一次调用和它的结果同一个 `key`,并在结果落定时打上
761
+ 耗时,所以轨迹里每条命令是一个块,而不是两行等着读者自己去配对。
762
+ - **推理和输出是分开的。** 它们是两样不同的东西,所以实时指示器各给一行,而不是一个
763
+ 覆盖另一个。
764
+ - **一帧只渲染它显示的内容。** 轨迹是底部锚定的,所以渲染器从后往前走条目,直到覆盖住
765
+ 窗口,并按身份缓存每个条目的行。两者都需要:窗口化给工作量封顶,缓存让稳定的重绘几乎
766
+ 不花代价。
767
+ - **鼠标是刻意接管的。** 很多终端把滚轮上报成方向键,这与按键无法区分,而且每格只滚一
768
+ 行。用 SGR 坐标接管鼠标能让滚动变精确;`--no-mouse` 会把旧行为还回来,而 Shift+拖拽
769
+ 依然可以选中文本。
770
+ - **已用时间指的是当前轮次**,没有打开的轮次时则是处在当前状态的时间。对恢复过的会话来
771
+ 说,会话总时长会产生误导。
772
+
773
+ ## 限制
774
+
775
+ - socket 传输只支持 Unix 域;Windows 命名管道没有实现。
776
+ - `--plain` 模式会打印条目,但不打印实时流预览。
777
+ - 页脚里的 `S<n>/<m>` 中,分母是当前轮次里见过的最大步骤号,而不是计划总数 ——
778
+ Harness 不发布计划总数。
779
+ - diff 面板没有行号:Harness 上报的是带三行上下文的已应用 hunk,而不是位置区间,所以
780
+ 凭空造行号就是在猜。
781
+ - Markdown 渲染是一个专门做的子集(标题、列表、表格、引用、分隔线、围栏、行内样式),
782
+ 配一个自带的、支持 14 种语言的高亮器,不是完整的 CommonMark 实现。任何时候按 `m`
783
+ 都能看到原始源码。
784
+ - 模型**新建**的文件是从调用的 `content` 参数重建的,因为新建没有此前的文本,也就没有
785
+ hunk。
786
+ - 轨迹面板在设计上就是只读的:没有办法从它那里批准、取消或干预。
787
+
788
+ ## 许可证
789
+
790
+ MIT