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.md ADDED
@@ -0,0 +1,920 @@
1
+ # dsh-live-trace
2
+
3
+ **English** · [中文](README.zh.md)
4
+
5
+ Two **read-only** windows onto a running DeepSeek Harness session, rendered in a
6
+ terminal that is *not* the one running your agent:
7
+
8
+ | Command | What it is |
9
+ | :--- | :--- |
10
+ | **`dsh-live-trace`** | the dashboard: a live trace of turns, steps, tools, output, tokens |
11
+ | **`dsh-live-working`** | the orca: one animated scene showing what the agent is doing right now |
12
+
13
+ Both attach to the same running Harness process over the same local socket and
14
+ neither ever sends anything to the agent.
15
+
16
+ ![The dashboard, with reasoning, a bash call and its output](picture/tui2.png)
17
+
18
+ ![The orca on the telephone, taking a subagent's reply](picture/call2.png)
19
+
20
+ | Thinking | Sleeping |
21
+ | :---: | :---: |
22
+ | ![The orca thinking, in the room](picture/type1.png) | ![The orca asleep under a dusk sky](picture/sleep1.png) |
23
+
24
+ ## `dsh-live-trace` — the dashboard
25
+
26
+ The Harness Web UI shows a conversation. This shows the *machine*: which turn and
27
+ step the loop is on, which tool is executing, what came back, what the model is
28
+ streaming right now, how long it has been running, and how many tokens have been
29
+ spent — updating continuously, so you can tell at a glance whether the agent is
30
+ working or stuck.
31
+
32
+ ```
33
+ ┌─ dsh-live-trace ─────────────────────────────────────────────────────────────┐
34
+ │ Session: …8-4222-8d66-0bb3b8c30c45 · live trace demo Status: ⠇ running │
35
+ ├──────────────────────────────────────────────────────────────────────────────┤
36
+ │ 10:19:00 [TURN 3] ─────────────────────────────────────────────────────── │
37
+ │ 10:19:00 [STEP 1] model call started │
38
+ │ 10:19:00 [USER] 把登录逻辑抽到 src/auth.ts,并补上测试 │
39
+ │ 10:19:02 [ASSISTANT] 先读一下现有的登录代码,确认调用点,再决定抽象边界。 │
40
+ │ 10:19:02 [TOOL] read_file path="src/login.ts" │
41
+ │ 10:19:02 [RESULT] ✓ 返回 234 行 0.2s │
42
+ │ 10:19:03 [TOOL] bash command="npm test" description="Run the test…" │
43
+ │ 10:19:03 [RESULT] ✓ 测试通过 (12 passed) 0.2s │
44
+ │ 10:19:05 [APPROVAL] bash — runs outside the sandbox │
45
+ │ 10:19:06 [APPROVAL] decision: allowed-once │
46
+ │ 10:19:06 [STEP 2] model call started │
47
+ │ 10:19:06 [ASSISTANT] 已完成:登录逻辑抽到了 src/auth.ts,12 个测试通过。 │
48
+ │ 10:19:06 [TURN 3 END] completed │
49
+ ├──────────────────────────────────────────────────────────────────────────────┤
50
+ │ ⠇ running T3 · S2 12.3s Tokens 4.2K/1.0M (↑3.8K ↓180) q:quit │
51
+ └──────────────────────────────────────────────────────────────────────────────┘
52
+ ```
53
+
54
+ Four panels share the same chrome:
55
+
56
+ | Panel | Key | Shows |
57
+ | :--- | :--- | :--- |
58
+ | **trace** | `1` / `t` | the chronological event log, with Markdown-rendered model prose and one block per tool call |
59
+ | **sessions** | `2` / `s` | every concurrent `dsh` session, live; this is also the landing screen when more than one is running |
60
+ | **edits** | `3` / `d` | every file the model changed, with a unified diff |
61
+ | **commands** | `4` / `c` | every shell command, its output, exit status, and duration |
62
+
63
+ ```
64
+ ┌─ dsh-live-trace · commands ──────────────────────────────────────────────────┐
65
+ │ Session: …a-412e-ac3d-c0f6a5bc824f · explain the plugin Status: ● idle │
66
+ ├──────────────────────────────────────────────────────────────────────────────┤
67
+ │ COMMANDS 2 run │
68
+ │ ──────────────────────────────────────────────────────────────────────────── │
69
+ │ ✓ ls -1 dsh-live-trace && echo "--- entry ---" && head -4 … 0.1s exit 0 │
70
+ │ Inspect the plugin package layout │
71
+ │ README.md bin cordis.patch.yml │
72
+ │ … 13 more lines │
73
+ │ │
74
+ │ ▸ ✓ cd dsh-live-trace && node --test test/width.test.js … 0.3s exit 0 │
75
+ │ Run the measurement and tool-helper tests │
76
+ │ ✔ every wrapped line fits the width budget (30.5ms) │
77
+ │ ℹ tests 22 │
78
+ ├──────────────────────────────────────────────────────────────────────────────┤
79
+ │ ● idle T1 2.9s Tokens 21K/1.0M ↓7 1:trace d:edits ?:help │
80
+ └──────────────────────────────────────────────────────────────────────────────┘
81
+ ```
82
+
83
+ Model prose is rendered as Markdown — headings, lists, tables, inline styles —
84
+ and every fenced code block is syntax highlighted, including the text that is
85
+ still streaming. **Thinking is collapsed by default**: a few lines and a marker
86
+ saying how much more there is; `e` opens every block. Shell commands are shown
87
+ the way a terminal shows them, with the command itself syntax highlighted:
88
+
89
+ ```
90
+ │ ┊ 先确认 login() 的所有调用点,再决定抽象边界。 │
91
+ │ ┊ 1. 只有两处直接调用,都在 src/routes/ 里。 │
92
+ │ ┊ … 5 more lines e to expand │
93
+ │ 11:58:09 [TOOL] bash Run the test suite ✓ exit 0 0.2s │
94
+ │ $ npm test │
95
+ │ 测试通过 (12 passed) │
96
+ ```
97
+
98
+ `$` is dim, the command name is a function colour, flags are attributes,
99
+ operators and quoted strings each get their own token colour.
100
+
101
+ ```
102
+ │ │ 检查 │ 结果 │
103
+ │ │ 测试 │ 12 passed │
104
+ │ │ lint │ 2 warnings │
105
+ │ │ bash │
106
+ │ │ $ npm test │
107
+ │ │ ✓ 12 passed │
108
+ ```
109
+
110
+ It is **not** a TUI chat client. It never sends anything to the agent.
111
+
112
+ ---
113
+
114
+ ## How it works
115
+
116
+ ```
117
+ ┌──────────────────────────── dsh web / dsh headless / dsh tui ─────────────────┐
118
+ │ Host process │
119
+ │ │
120
+ │ Cordis event bus ──► dsh-live-trace plugin ──► TraceHub ──► unix socket │
121
+ │ session/event (normalize) (state) $DSH_HOME/ │
122
+ │ agent/assistant-stream live-trace/ │
123
+ │ agent/status, agent/error sockets/*.sock │
124
+ └───────────────────────────────────────────────────────────────────────────────┘
125
+ ▲
126
+ newline-delimited JSON
127
+ │
128
+ ┌──────────────────────────── another terminal window ─────────────┐ │
129
+ │ dsh-live-trace (separate process, read-only) ──────────────────┘ │
130
+ │ alternate screen · ANSI renderer · scroll · replay on attach │
131
+ └─────────────────────────────────────────────────────────────────────┘
132
+ ```
133
+
134
+ **One correction to the obvious design.** A Cordis plugin is not a separate
135
+ process: `ctx.on('session/event', …)` fires inside the process running the
136
+ Harness. So the split is:
137
+
138
+ - a **Host plugin** (`index.js`) that observes in-process and publishes
139
+ normalized records on a Unix domain socket, and
140
+ - a **standalone viewer** (`bin/dsh-live-trace.js`) that runs in its own
141
+ terminal, connects to that socket, and renders.
142
+
143
+ The plugin is read-only in the strongest sense available: it appends no session
144
+ events, registers no tool hooks, rewrites no prompt, and never writes to stdout.
145
+ It only listens.
146
+
147
+ A Unix socket (rather than a TCP port) is deliberate: it cannot be reached from
148
+ another host, it cannot collide with the Web UI's port, it needs no
149
+ authentication story, and its file permissions are the access control.
150
+
151
+ ### Event vocabulary actually used
152
+
153
+ The Harness publishes a small, exact set of events. The dashboard maps them as
154
+ follows; anything else is a plugin's own event type and is hidden unless
155
+ `showUnknownEvents: true`.
156
+
157
+ | Source event | Label | Notes |
158
+ | :--- | :--- | :--- |
159
+ | `session/created` / `session/disposed` | `[SESSION]` | session lifecycle |
160
+ | `turn/start` / `turn/end` | `[TURN n]` / `[TURN n END]` | turn opening renders as a rule |
161
+ | `step/start` / `step/end` | `[STEP n]` / `[STEP n END]` | one model call per step |
162
+ | `user/message` | `[USER]` / `[CONTEXT]` | injected context is labelled by its `source.kind` |
163
+ | `assistant/message` | `[ASSISTANT]` | text plus a `thinking:` excerpt; carries token usage |
164
+ | `assistant/attempt` | `[ATTEMPT]` | an attempt that committed no message |
165
+ | `tool/call` + `tool/result` | `[TOOL]` | **one block**: name, the command it ran, its output, `✓`/`✗ exit N`, and duration. The two records share a `key`, so the result upgrades the running row in place. |
166
+ | `tool/result` `meta.diffs` | `[TOOL]` + *edits* | applied file hunks; a newly created file (no prior text) is rebuilt from the call's `content` argument |
167
+ | `approval/asked` / `approval/decided` | `[APPROVAL]` | also raises **waiting for approval** |
168
+ | `session/title` | `[TITLE]` | also updates the header |
169
+ | `permission/preset`, `sandbox/mode`, `approval/policy` | `[POLICY]` | one line each at startup |
170
+ | `agent/assistant-stream` (live) | `thinking` / `writing` | coalesced, never per chunk; reasoning and visible text get their own rows |
171
+ | `agent/status`, `agent/error` (live) | status bar / `[ERROR]` | |
172
+ | `request/context` | *(footer)* | supplies the context-window denominator |
173
+
174
+ There is no `assistant/chunk` event on the bus — live streaming is
175
+ `agent/assistant-stream`, whose chunk frames are accumulated and published on a
176
+ fixed cadence (`streamIntervalMs`, default 500 ms) so a fast model cannot flood
177
+ the terminal. The durable `assistant/message` settles the stream.
178
+
179
+ ---
180
+
181
+ ## Install
182
+
183
+ There are two halves: the **plugin**, which the Harness loads so it can publish
184
+ session events, and the **viewer**, which is the command you run.
185
+
186
+ ### The plugin
187
+
188
+ Published as [`dsh-live-trace`](https://www.npmjs.com/package/dsh-live-trace) on
189
+ npm. The plugin must be selected by the profile your Harness boots.
190
+
191
+ **A. From npm**
192
+
193
+ ```sh
194
+ dsh plugin --profile web add dsh-live-trace
195
+ ```
196
+
197
+ then add `"dsh-live-trace"` to `dsh.profile.bundles` in
198
+ `$DSH_HOME/profiles/web/package.json`.
199
+
200
+ **B. File-level installer (offline, no package manager)**
201
+
202
+ ```sh
203
+ node /path/to/dsh-live-trace/scripts/install-profile.mjs --profile web
204
+ ```
205
+
206
+ It adds `dsh-live-trace` to the profile's `dsh.profile.bundles`, adds a
207
+ `link:` dependency, and symlinks the package into the profile's `node_modules`.
208
+ Restart the Harness (or let hot reload pick it up).
209
+
210
+ **C. From a checkout, pnpm-managed**
211
+
212
+ ```sh
213
+ dsh plugin --profile web add /path/to/dsh-live-trace
214
+ ```
215
+
216
+ then add `"dsh-live-trace"` to `dsh.profile.bundles` in
217
+ `$DSH_HOME/profiles/web/package.json`. This path needs `pnpm` on `PATH`.
218
+
219
+ To undo either: `node scripts/install-profile.mjs --profile web --uninstall`.
220
+
221
+ ### The viewer
222
+
223
+ ```sh
224
+ npm install -g dsh-live-trace
225
+ ```
226
+
227
+ That puts three commands on `PATH`: `dsh-live-trace` (the dashboard),
228
+ `dsh-live-working` (the orca) and `dsh-glyph-probe` (which prints what your
229
+ font can render). From a checkout instead, run them out of `bin/` or link them
230
+ by hand:
231
+
232
+ ```sh
233
+ npm install -g .
234
+ ```
235
+
236
+ Verify the composition without starting anything:
237
+
238
+ ```sh
239
+ dsh --profile web --dump-config | grep -A4 'dsh-live-trace'
240
+ ```
241
+
242
+ ### Run the dashboard
243
+
244
+ ```sh
245
+ dsh-live-trace
246
+ ```
247
+
248
+ With the plugin loaded, the Harness publishes a discovery record under
249
+ `$DSH_HOME/live-trace/servers/` naming its socket, working directory, and
250
+ sessions. The viewer prunes dead records, prefers a process in the current
251
+ working directory, and binds to the newest active session.
252
+
253
+ ```
254
+ dsh-live-trace --list # what was discovered, then exit
255
+ dsh-live-trace --session <id> # bind one session
256
+ dsh-live-trace --socket <path> # bypass discovery entirely
257
+ dsh-live-trace --runtime-dir <path> # non-default $DSH_HOME
258
+ dsh-live-trace --plain # one line per event, pipe-friendly
259
+ dsh-live-trace --wait # poll until a Harness appears
260
+ ```
261
+
262
+ ### Keys
263
+
264
+ | Key | Action |
265
+ | :--- | :--- |
266
+ | `1` / `t` | trace panel |
267
+ | `2` / `s` | session picker (`esc` returns to the panel underneath) |
268
+ | `3` / `d` | file changes panel |
269
+ | `4` / `c` | commands panel |
270
+ | `tab` | cycle panels |
271
+ | `↑` / `↓` | scroll the trace, or move the selection in a list panel |
272
+ | `PgUp` / `PgDn` | scroll one page |
273
+ | `Home` / `End` | oldest / newest |
274
+ | `enter` | bind the highlighted session |
275
+ | `esc` | leave the picker, then the panel; quits only from the trace |
276
+ | `e` | expand or collapse every thinking block |
277
+ | `m` | toggle Markdown rendering (show the raw source) |
278
+ | wheel | scroll three lines per notch (any panel) |
279
+ | click | select the row under the pointer |
280
+ | `p` | pause or resume following new entries |
281
+ | `r` | ask the plugin to replay this session |
282
+ | `?` | key help |
283
+ | `q`, `Ctrl-C` | quit |
284
+
285
+ ### Landing screen
286
+
287
+ `dsh-live-trace` opens the **session picker** when more than one session is
288
+ running and no `--session` was given — with several concurrent `dsh` sessions,
289
+ choosing is the first thing you need to do. With exactly one session it goes
290
+ straight to the trace, and an explicit `--session <id>` always wins.
291
+
292
+ ## Multi-session switching
293
+
294
+ Every session the Harness knows is listed live, with its activity, turn, step,
295
+ how long it has been quiet, its title, and its working directory. `↑`/`↓` moves
296
+ the selection and `enter` binds it; the plugin then replays that session's
297
+ backlog, so switching never leaves you with a blank panel. If the session you
298
+ are following ends, the viewer follows the next active one automatically.
299
+
300
+ ---
301
+
302
+ ## Configuration
303
+
304
+ Every key is optional; the plugin validates defensively and falls back to the
305
+ default rather than refusing to start. Put them in
306
+ `$DSH_HOME/profiles/<profile>/cordis.patch.yml`:
307
+
308
+ ```yaml
309
+ - id: dsh-live-trace
310
+ config:
311
+ enabled: true
312
+ streamIntervalMs: 500 # 50…60000 — coalesced streaming cadence
313
+ backlogSize: 2000 # 10…100000 — entries retained per session
314
+ heartbeatMs: 5000 # discovery/heartbeat cadence
315
+ replayLimit: 500 # entries replayed to a late-joining viewer
316
+ showSystemMessages: false # render the system prompt as an entry
317
+ showRequestMetadata: false # render request/header as entries
318
+ showUnknownEvents: false # render unrecognized plugin events
319
+ mutedEventTypes: # `*` is a prefix match
320
+ - 'session-log-deepseek/*'
321
+ textLimit: 4000 # cap on one entry's primary text
322
+ outputLines: 200 # 1…100000 — command output lines retained
323
+ outputChars: 20000 # 200…1000000 — command output characters retained
324
+ runtimeDir: null # override $DSH_HOME/live-trace
325
+ socketPath: null # override the derived socket path
326
+ ```
327
+
328
+ The plugin declares no `Config` schema on purpose: it must load in a profile that
329
+ cannot resolve a schema package, and a mistyped option should degrade to its
330
+ default rather than block startup.
331
+
332
+ ---
333
+
334
+ ## Wire protocol (v1)
335
+
336
+ Newline-delimited JSON over the socket. Every record carries `v` and `kind`;
337
+ unknown kinds are ignored in both directions, so either half can be upgraded
338
+ first.
339
+
340
+ **Plugin → viewer:** `hello` (server identity, session list, active session),
341
+ `sessions`, `entry` (one normalized trace line), `stream` (coalesced live text),
342
+ `stream-end`, `status`, `usage`, `edits`, `heartbeat`, `error`.
343
+
344
+ **Viewer → plugin:** `select` (bind a session; omitted id means "the server's
345
+ default"), `replay`, `ping`.
346
+
347
+ An `entry` may carry a `key`; two records with the same key are one row, the
348
+ later one replacing the earlier in place. That is how a tool call and its
349
+ settlement stay one block instead of two half-lines.
350
+
351
+ Session-scoped records reach only viewers bound to that session, and `sessions`
352
+ records are personalized with the `boundSessionId` each viewer actually holds.
353
+ A viewer that names no session keeps asking until one exists, then follows it —
354
+ so opening the dashboard before the first session works, and a session that ends
355
+ hands the view to the next one.
356
+
357
+ ---
358
+
359
+ ## Verification
360
+
361
+ `npm test` runs 243 tests (`node --test`), including:
362
+
363
+ - **Renderer invariants** — for widths 24…200 and heights 8…60, every frame row is
364
+ *exactly* the terminal width, including with CJK text, emoji, and content that
365
+ tries to inject escape sequences.
366
+ - **Measurement** — `displayWidth` counts Han/Kana/Hangul/emoji as two cells,
367
+ combining marks as zero; wrapping and truncation never split a wide character.
368
+ - **Normalization** — every mapped event type, plus malformed arguments,
369
+ unknown errors, and unknown event types.
370
+ - **Markdown and highlighting** — a hard character-preservation invariant for
371
+ every supported language, the width bound across six widths for a document
372
+ battery and an adversarial fuzz, unterminated fences (the streaming case), and
373
+ tables.
374
+ - **Tool blocks, diffs, and commands** — call/result pairing by key, the shell
375
+ exit-status marker contract (`[exit code: N]`, `[killed by signal: X]`),
376
+ diff-metadata narrowing, whole-file reconstruction for a created file, and
377
+ per-path accumulation across repeated edits.
378
+ - **Panels** — every panel keeps the exact-width contract at every size, and
379
+ each one explains an empty session instead of showing a blank box.
380
+ - **Windowing** — a windowed frame paints byte-for-byte the rows the unbounded
381
+ render would, at every size and scroll offset, and the scrollback ring buffer
382
+ trims without losing its key index.
383
+ - **Input** — arrow/navigation keys, SGR mouse reports split across reads,
384
+ wheel mapping, click press-vs-release, motion rejection, and a bounded buffer
385
+ for unterminated sequences.
386
+ - **Transport** — NDJSON framing across split chunks, per-viewer session
387
+ routing, replay on attach, reconnect after a server restart, and teardown.
388
+ - **Integration against the real Harness** — boots the shipped
389
+ `@deepseek-ai/dsh-session` plugin in a real Cordis context, creates and appends
390
+ real sessions, and asserts what a viewer receives over a real socket. It skips
391
+ cleanly when no Harness install is present.
392
+ - **End-to-end CLI** — spawns the real `dsh-live-trace` binary against a real
393
+ observer socket: discovery, `--list`, the alternate-screen board quitting on
394
+ `q`, 80-column row widths in the captured output, plain mode, and the
395
+ no-observer diagnostic.
396
+ - **Teardown leaves nothing behind** — three mount/unmount cycles accumulate no
397
+ instance, socket, or registry record; and a separate program that boots
398
+ everything and disposes must **exit on its own**, so a live socket or interval
399
+ would hang it. Removing the disposal call from that program makes it hang,
400
+ which is the negative control proving the check has teeth.
401
+
402
+ Beyond the suite, the package was verified against an installed Harness:
403
+
404
+ 1. `dsh --profile web --dump-config` composes the plugin row and applies the
405
+ user's patch layer to it.
406
+ 2. A real `dsh web` process starts the plugin, which creates its socket and
407
+ discovery record; `dsh-live-trace --list` finds it and the board attaches.
408
+ 3. A real `dsh headless` agent loop (driven by `scripts/mock-provider.mjs`, an
409
+ offline Messages-API server) streams genuine turn, step, tool, result, and
410
+ token events into the board, including a real `write` whose applied diff
411
+ reaches the edits panel.
412
+
413
+ ## `dsh-live-working` — the orca
414
+
415
+ The dashboard tells you what *happened*. This tells you what is happening: a
416
+ pixel-art orca at a desk with a keyboard, a stack of books and a red telephone,
417
+ animated to match the session.
418
+
419
+ ```
420
+ ▄█接 2 号子代理████████████████▄
421
+ ██把 auth 模块里的校验逻辑抽出来██
422
+ ▀██████████████████████████▀
423
+ ▄█▀
424
+ ▀▀ ▄▄██▄▄▄▄
425
+ ▀▀▀▀▀▀▀▀ ▄▄
426
+ ████████ ██████▄▄
427
+ ▄▄▀▀ ████████▄▄
428
+ ████████ ████████████
429
+ ██████████ ▄▄██████████████████▄▄▄▄ ▀▀██████████
430
+ █ █ ▄▄████████████████████████▄▄▄▄ ████████▀▀
431
+ ▄▄█████████████████████████████████▄▄ ▄▄██████
432
+ ██████████████████████████████████████▄▄▄▄██████
433
+ ████████████████████████████████████████████████
434
+ ██████████████████████████████████████████████
435
+ ▀▀██████████████████████████████████████████▀▀
436
+ ▀▀██████████████████████████████████████▄▄▄▄▄▄
437
+ ▄▄██████████████████████████████████████████
438
+ ████████████████████████████████████████████
439
+ ▄██████████▄ ██████████████████████ ████████████████
440
+ ████████████ ██████████████████████ ████████████████
441
+ ██████████████████████████████████████████████████████████████████
442
+ ██████████████████████████████████████████████████████████████████
443
+ ███ ███
444
+ ```
445
+
446
+ The scene is **sized to your terminal** — up to 200 cells wide, and the orca is
447
+ drawn at 1×, 2× or 3× depending on the room, so a wide terminal gets a big
448
+ creature rather than the same one floating in the middle of a large empty desk.
449
+ Pixel art is only ever scaled by whole numbers; a fractional scale produces
450
+ uneven blocks that look like a rendering fault.
451
+
452
+ The sprite is sampled from the reference artwork at its **native 64×40 pixel
453
+ grid** (the artwork's blocks are five source pixels across), so no row of the
454
+ original is dropped. The grey marks above the whale in the source are the three
455
+ `Z` glyphs of a *sleeping* whale illustration — they are excluded when sampling,
456
+ so the creature sits on a clean transparent background and the scene draws its
457
+ own `z`s, and only while it is actually asleep.
458
+
459
+ ### What it does, and why
460
+
461
+ | State | Trigger | Animation |
462
+ | :--- | :--- | :--- |
463
+ | `sleep` | no work; **or** a running command that has printed nothing for 8s | eyes closed, a slow bob, `z`s drifting up |
464
+ | `thinking` | reasoning is streaming, or the agent is waiting for approval | hands off the keys, a trail of thought dots |
465
+ | `typing` | prose or code is streaming | keys light in a sweep |
466
+ | `writing` | a write/edit tool is running | a sheet on the desk gains a line |
467
+ | `waiting` | a shell command is running and still talking | a terminal block with a blinking cursor |
468
+ | `reading` | a read/glob/grep tool is running | **squints, and blinks every few seconds**, holding a book open |
469
+ | `searching` | a web search or fetch is running | **a page turns**, sweeping right to left across the spine |
470
+ | `calling` | a subagent was started | **the red telephone is picked up and held to the ear at an angle**, hands off the keyboard, and a bubble |
471
+ | `ringing` | a subagent finished | the telephone **rings and shakes**, and the bubble shows what it came back with |
472
+
473
+ ### Two backdrops: `room` and `nature`
474
+
475
+ `--scene room` (the default) is a desk by a window: a wall with a window in it,
476
+ and the sky confined to the pane. `--scene nature` takes the wall away — the
477
+ backdrop *is* the outdoors, so the weather covers the whole scene, the ground
478
+ runs to the bottom of the screen and the desk stands on it, and conifers stand
479
+ along the horizon. Press `b` to swap between them at any time.
480
+
481
+ Both are built from the same sky. The window and the outdoors both composite
482
+ their contents on their own canvas before blitting, so the containment
483
+ guarantee — nothing escapes the area it was given — holds for either.
484
+
485
+ ### The room
486
+
487
+ **The room is lit by its window.** The wall, its shading, the skirting and the
488
+ floor are derived from the sky each frame, so the room darkens at night and
489
+ brightens at noon instead of being a fixed mid grey — a bright grey slab beside
490
+ a black sky was the single worst thing about the night scene. The wall runs
491
+ `181,176,166` at noon to `64,66,75` at midnight, and never falls below a floor,
492
+ so the room stays readable.
493
+
494
+ The scene is a room, not a sprite floating on whatever the terminal's background
495
+ happens to be: a wall with faint paper stripes, a skirting board where it meets
496
+ the floor, and the desk standing on that floor. The window is an **opening in
497
+ the wall**, so there is something for everything else to be read against — the
498
+ sleeping `z`s in particular, which used to be drawn over the sky and disappear
499
+ into a cloud — and then, once moved onto the wall, were a mid grey on a mid
500
+ grey wall. They have a colour of their own now.
501
+
502
+ Everything the window contains — sky, sun and moon, clouds, rain, snow, fog — is
503
+ composed on the window's own canvas and blitted in, so none of it can drift out
504
+ of the opening and across the wall.
505
+
506
+ ### Out of the window
507
+
508
+ In the wall is a window onto a world that keeps its own time. **One in-game
509
+ minute passes per real second**, so a full day takes twenty-four real minutes
510
+ and the sky is never still: a colour that runs from midnight blue through dawn
511
+ orange to noon blue and back, the sun and moon taking turns on an arc across the
512
+ panes, stars that only come out at night.
513
+
514
+ It rains there too. Weather changes on its own every three in-game hours, picked
515
+ from the spell's number rather than a random number generator, so two viewers
516
+ watching the same session see the same sky. It fades in and out rather than
517
+ snapping on, and **snow falls at night where rain falls by day** — the same
518
+ weather, one temperature apart.
519
+
520
+ The window is a picture, not a colour swatch:
521
+
522
+ - **The sky is a gradient.** A terminal has no alpha, but it has a grid: two
523
+ colours and a 4x4 Bayer dither make the sky darker overhead and lighter
524
+ towards the horizon, and turn the horizon warm at dawn and dusk while the top
525
+ stays cold. Two flat colours read as a band; the dither reads as a sky.
526
+ - **There are hills.** A rolling ridge along the bottom of the pane gives the
527
+ window somewhere to be, and the sun and moon set behind it.
528
+ - **The sun and moon have a halo**, dithered so it fades out instead of stopping
529
+ dead.
530
+ - **Clouds are cumulus**, three overlapping ellipses with a flat underside —
531
+ not the horizontal bars they started as.
532
+ - **Fog is a haze, not a band.** It covers a quarter of the pane instead of a
533
+ third, is at most half-filled, thins out as it rises, and takes its colour
534
+ from the sky rather than being a fixed light grey — so it fades into the
535
+ weather instead of reading as a grey stripe. Measured, it went from 23% of the
536
+ pane to 5%.
537
+ - **Stars come in two brightnesses**, and twinkle on their own clock.
538
+
539
+ The weather **takes light out of the sky**: a clear noon is 535 of a possible
540
+ 765, cloudy 439, rain 331, and a storm 240. Heavy weather also hides the sun or
541
+ the moon, which is what makes it read as heavy. Stars come out only when the sky
542
+ is genuinely dark — keying them off the phase instead put white dots in a bright
543
+ orange dawn. The footer carries the in-game clock and the current weather.
544
+
545
+ **It can rain out loud.** `--sound` (or `n` at any time) plays rain through
546
+ whichever of `aplay`, `paplay`, `sox` or `ffplay` is installed, and only while
547
+ it is actually raining. It is **off by default**, because a command that starts
548
+ playing audio on its own is a command people stop running.
549
+
550
+ **The level is a separate control.** `--rain-volume <0-100>` sets it (default
551
+ `40`), `-` and `+` move it by 5 while the command runs, and the footer shows the
552
+ current level next to the sound state. It starts at 40 rather than at full scale
553
+ on purpose: the recording peaks at -6.1 dBFS, which is a foreground level, and
554
+ at 40% it peaks around -14 dBFS — ambience rather than something to switch off.
555
+ The synthesised noise is scaled by the same fraction, so it went from a mean
556
+ sample of 2023 to 809 (peak 5898 to 2359, -14.9 to -22.9 dBFS): the same rain,
557
+ less of it.
558
+
559
+ The level reaches the speakers on every path except one. In **file** mode
560
+ `paplay` gets `--volume`, `sox` gets `-v`, and `ffplay` gets `-volume`; the
561
+ synthesised stream carries its level in its own samples, so every player honours
562
+ it there. **`aplay` has no volume argument at all**, so a recording played
563
+ through `aplay` is heard at whatever level it was recorded at. That is not
564
+ quietly ignored: the footer says `aplay cannot change a file level` instead of
565
+ printing a percentage that would be a lie. If you want a quieter recording on an
566
+ `aplay`-only machine, pass a quieter `--rain-file` or install `paplay`, `sox` or
567
+ `ffplay`.
568
+
569
+ The default source is the bundled recording. `--rain-file` names another. If a
570
+ file is missing — or the asset cannot be read — it falls back to **synthesised**
571
+ noise: a low-passed run of pseudo-random samples, streamed forever with no loop
572
+ point. That is what keeps the feature working on a machine with nothing but a
573
+ player.
574
+
575
+ Changing the level while a recording is playing restarts the player, because the
576
+ level lives in its command line; the synthesised stream picks the new level up
577
+ on its next chunk without a gap.
578
+
579
+ Changing the level restarts the player, because the level is an argument to it.
580
+ The old player is stopped first, but its exit event lands *after* the new one is
581
+ already running — so state updates are ignored unless they come from the process
582
+ that is still current, and every process ever started is tracked and killed
583
+ together. Without both, a level change left two rain sounds playing and an
584
+ orphaned player that kept going after the command exited.
585
+
586
+ A player that dies the moment it starts has no audio device; it is retried three
587
+ times and then left alone, rather than restarted forever.
588
+
589
+ **A rain loop ships with the package.** `assets/rain.ogg` (30 s, mono, 64 kbps,
590
+ 242 KB) is the default source, so `--sound` needs no arguments. It was cut from
591
+ the 8-hour recording `42130539966-1-192.mp4` — its audio is aac 48 kHz stereo —
592
+ at the one-hour mark, and the tail was crossfaded into the head so the loop has
593
+ no click: the two ends match in level to within 20% and the sample gap at the
594
+ seam is 0.35% of full scale. It peaks at -6.1 dBFS; at the default level of 40%
595
+ that is about -14 dBFS. `--rain-file` overrides it.
596
+
597
+ **About a 2.2 GB `42130539966-1-192.mp4` sitting in this directory:** it is not
598
+ part of the package (`package.json`'s `files` list does not include it, and
599
+ `.gitignore` ignores `*.mp4`), and this machine has no `ffprobe`, `ffmpeg`,
600
+ `aplay`, `paplay`, `sox` or `mpv` — `ffplay` is present, but it can play a file,
601
+ not inspect one — so the video can be neither inspected nor played here.
602
+ `--rain-file` takes an audio file; an `.mp4` is a video container
603
+ and would need its audio track extracted first, which needs `ffmpeg`:
604
+
605
+ ```
606
+ ffmpeg -i 42130539966-1-192.mp4 -vn -ac 1 -ar 44100 rain.wav
607
+ dsh-live-working --sound --rain-file rain.wav
608
+ ```
609
+
610
+ For a real recording, pass `--rain-file`; `--rain-volume` still applies to it on
611
+ every player that has a volume argument:
612
+
613
+ ```
614
+ dsh-live-working --sound --rain-volume 25 --rain-file ~/sounds/rain-loop.wav
615
+ ```
616
+
617
+ A file that does not exist is dropped with a fall back to the synthesised noise
618
+ rather than going silent. `ffplay` loops a file natively; the other players are
619
+ restarted when the file ends, but only while it is still raining. Licensing is
620
+ your call and your responsibility — this ships no audio of its own:
621
+
622
+ - [Wikimedia Commons: Sounds of rain](https://commons.wikimedia.org/wiki/Category:Sounds_of_rain) — freely licensed, per-file terms
623
+ - [Freesound](https://freesound.org/) — filter by licence; CC0 needs no attribution
624
+ - [Creazilla: Ambience Rainstorm](https://creazilla.com/media/audio/15525146/ambience-rainstorm) — royalty-free
625
+ - [Internet Archive: Red Library — Nature Rain](https://archive.org/details/Red_Library_Nature_Rain)
626
+
627
+ ```
628
+ 工作台 · 等待命令 06:13 雾 c4c9e25b082a T23·S19 关闭本帮助:?
629
+ ```
630
+
631
+ ### The orca's own motion
632
+
633
+ - **The tail is drawn, one pose per state.** The source art has a single
634
+ upright tail and nowhere to move it: the flukes reach the last row of the body
635
+ and the right edge of the sprite, and the desk sits on the bottom line. Six
636
+ transforms were tried against it — shear, fold, rotate, tapered shear, rigid
637
+ slide, levelled fold — and every one traded one artefact for another: a frayed
638
+ edge, a torn silhouette, a tail under the table, a spike above it, or a hole
639
+ showing the background through. There are now **two poses**, composed from the
640
+ body plus a tail:
641
+
642
+ - `up` is lifted straight out of the source art, pixel for pixel, so the awake
643
+ orca is exactly what the reference drew;
644
+ - `sleep` is drawn: the flukes come down and lie along the body at the desk
645
+ line, tapering to a rounded tip, with its edge generated rather than handed
646
+ to the renderer to guess.
647
+
648
+ A drawn pose cannot lose a pixel, tear, or leave a gap — those failures are
649
+ impossible by construction rather than merely fixed.
650
+
651
+ - **The awake tail sways from its base.** The whole tail used to shift rigidly,
652
+ which pulled it away from the body and let the background show through the
653
+ seam; the columns nearest the body are now anchored and only the outer part
654
+ swings, so the tail pivots instead of sliding. It holds still while the orca is
655
+ on the telephone, because the flippers are busy.
656
+
657
+ - **At night it rubs its eyes** — every so often while working it shuts them and
658
+ lifts a flipper to its face, then carries on where it was. Only at night, and
659
+ never while asleep or on the telephone.
660
+
661
+ ### The desk
662
+
663
+ The desk is staged rather than pasted on: the keyboard is a small dark bar on
664
+ the desk, and a **framed window in the upper right shows the text being typed** —
665
+ the tail of the model's output, or the arguments of the tool it is running. The
666
+ window's frame is drawn whenever the text is, because text without a frame reads
667
+ as a stray bubble floating at the top of the terminal.
668
+
669
+ The orca's **flippers move** while it types, in a blue a shade off its body so
670
+ the limb reads as a limb. The sampled reference art is a side view with no
671
+ separate pectoral fin, so the paddles are drawn and animated on top of the keys;
672
+ they are **tucked away when it sleeps**. A telephone call stops the typing
673
+ entirely — the handset is held against the face, up by the eye and down past the
674
+ chin, rather than laid across the body or standing upright on the desk.
675
+
676
+ The book is shaded, which is what makes a page turn legible: the two pages of a
677
+ spread are lit differently, the gutter between them falls into shadow, and a
678
+ leaf caught mid-turn is edge-on and darker than either.
679
+
680
+ The telephone bubble reads `接 N 号子代理,<instructions>` (or `Subagent #N —
681
+ <instructions>` in English), where N counts the subagents in the session and the
682
+ instructions are the arguments the call was given.
683
+
684
+ ### The telephone
685
+
686
+ Dispatching a subagent is a telephone call, and the whole exchange is modelled
687
+ that way:
688
+
689
+ - **The bubble shows the call as it was written**, not just the instruction
690
+ inside it: `task(description="…", prompt="…")`, with the tool name and every
691
+ argument. With several subagents out at once the header becomes a queue —
692
+ `接 3 号子代理(共 5 个,排队 2 个)`.
693
+ - **The message is spoken**, a character at a time, and the bubble shows three
694
+ rows of it. Longer messages scroll. The header is pinned above them: it is
695
+ what says who is on the line, so losing it to a long instruction would leave
696
+ the bubble anonymous.
697
+ - **The telephone rings when a subagent hangs up.** The handset shakes and ring
698
+ arcs appear either side of the phone, and the bubble shows the subagent's own
699
+ closing text — its answer, not a summary of it.
700
+ - **A background dispatch is an acknowledgement, not an answer.** Starting a
701
+ subagent in the background returns `started subagent <id>` immediately; the
702
+ answer arrives later as a message from that agent. Only the second one counts,
703
+ so a background subagent keeps its place in the queue instead of looking
704
+ finished the instant it started.
705
+ - **The handset is picked up and put down**, not teleported: it eases off the
706
+ cradle, rotates up to the ear, and retraces the same path on the way down,
707
+ with the cord slackening as it rises.
708
+ - **History is not news.** The viewer replays the backlog when it starts, and
709
+ every record carries its own timestamp. Stamping events with the wall clock
710
+ instead made an hour of history look like it was all happening right now, so
711
+ the telephone rang for subagents that had finished minutes earlier. Every
712
+ timestamp now comes from the event.
713
+ - **Work handed out and nothing left to do means a nap.** With subagents out and
714
+ no other tool running, the orca dozes at the desk exactly as it does when idle,
715
+ and the ringing telephone is what wakes it.
716
+
717
+ The bubble is anchored over the orca's head with its tail pointing at the
718
+ speaker, and clipped to stay clear of the preview window. A speech bubble pinned
719
+ to the corner of the scene points at nothing and reads as a stray box floating
720
+ outside the interface — which is how it was reported, twice.
721
+
722
+ ```bash
723
+ dsh-live-working # follow the server's default session
724
+ dsh-live-working -s <session-id> # follow a specific one
725
+ dsh-live-working --state reading # pin one animation, ignore the session
726
+ dsh-live-working --list # show observers and sessions
727
+ ```
728
+
729
+ With more than one session running it opens on a **session picker** rather than
730
+ guessing which one you meant; `s` reopens it, `↑`/`↓` choose, `enter` binds,
731
+ `esc` cancels.
732
+
733
+ Keys: `1`-`8` pin an animation, `0` returns to automatic, `s` choose a session,
734
+ `n` toggles the rain sound, `-` and `+` change its level, `l` switches language,
735
+ `?` toggles help, `q` quits.
736
+
737
+ ```
738
+ dsh-live-working — a live orca animation for a DeepSeek Harness session
739
+
740
+ States: sleep, thinking, typing, writing, waiting, reading, searching, calling
741
+ ```
742
+
743
+ `node scripts/demo-working.mjs` cycles every state without needing a session;
744
+ `node scripts/demo-working.mjs reading` holds one.
745
+
746
+ ### How much resolution a terminal cell can carry
747
+
748
+ A character cell is about twice as tall as it is wide, so the choice of glyph
749
+ decides both the resolution and the *shape* of the pixels:
750
+
751
+ | packing | pixels/cell | pixel shape | glyphs | notes |
752
+ | :--------- | :---------- | :---------- | :-------------------- | :--------------------------------------- |
753
+ | `half` | 1 x 2 | square | `▀ ▄ █` | what this dashboard draws with |
754
+ | `quadrant` | 2 x 2 | 1:2 tall | `▘ ▝ ▖ ▗ ▚ ▞ ▛ ▜ ▙ ▟` | finer edges, but everything is stretched |
755
+ | `braille` | 2 x 4 | square | `U+2800..U+28FF` | 4x the pixels, but every one is a dot |
756
+
757
+ `half` is the only packing that is both **square** and **solid**, which is what
758
+ solid pixel art needs. `quadrant` doubles the horizontal count but its pixels
759
+ are twice as tall as they are wide, so a picture drawn for square pixels comes
760
+ out stretched. `braille` gives four times the pixels with square pixels, but the
761
+ dots have gaps around them, so a solid fills as a stipple — excellent for a
762
+ plot, poor for a whale.
763
+
764
+ Whether any of them is usable depends on your font, and a font without the
765
+ glyphs does not fail loudly: it substitutes boxes or blanks and the picture
766
+ quietly falls apart. So look:
767
+
768
+ ```
769
+ dsh-glyph-probe
770
+ ```
771
+
772
+ It prints a sample of each packing and a disc rasterised at that packing's own
773
+ resolution. The disc that comes out round and seamless is the one your font
774
+ supports.
775
+
776
+ ### Can the program shrink the terminal's font?
777
+
778
+ **No.** There is no escape sequence for it. `ESC[?3h` switches between 80 and
779
+ 132 columns rather than changing the font, and VTE/GNOME Terminal has no way to
780
+ do it at all — you can read that straight out of [the source
781
+ discussion](https://stackoverflow.com/revisions/0162962e-7b66-4d60-8df4-4278446e6220/view-source).
782
+ kitty's [text sizing
783
+ protocol](https://github.com/kovidgoyal/kitty/blob/f13c8cd4/docs/text-sizing-protocol.rst)
784
+ scales individual runs of text and is kitty-only.
785
+
786
+ Even where it might work, leaving it to the application is a bad trade: a crash,
787
+ a `SIGKILL`, a closed terminal or a dropped SSH session would leave the font
788
+ small with nothing left to restore it.
789
+
790
+ What a terminal *will* do is **report its geometry**, and that half is worth
791
+ having — `CSI 16t` returns the cell size in pixels and `CSI 14t` the text area.
792
+ `dsh-glyph-probe` asks for both and does the arithmetic:
793
+
794
+ ```
795
+ Cell size 9x18 px (aspect 0.50)
796
+ Cells 148x40
797
+ Drawing budget 11,840 pixels (two per cell)
798
+ Half the font 333x80 cells -> 53,280 pixels
799
+ ```
800
+
801
+ Shrinking the font does not change the window's pixels; it changes how many
802
+ **cells** fit into them, and every extra cell is two more pixels of drawing.
803
+ That is the real gain, and it is the user's to make.
804
+
805
+ Two things are worth knowing before reaching for a higher-resolution packing:
806
+
807
+ - **The whale is at its source resolution.** The reference art is a 64x40
808
+ sprite, so it has no more detail to give; upscaling it only makes bigger
809
+ blocks. A finer whale needs finer artwork, not a finer renderer.
810
+ - **The scene already fills the terminal.** It renders at up to 200 cells wide
811
+ and uses every row it can, so the cheapest way to more precision is a larger
812
+ window — more cells is more pixels, whatever the glyphs are.
813
+
814
+ ## Both session lists lead with the title
815
+
816
+ `dsh-live-working`'s chooser and `dsh-live-trace`'s session panel both show the
817
+ session's title first, with the shortened id after it. The id is what you type
818
+ and the title is not unique, so both are shown — but the title is what you
819
+ recognise, so it comes first. A session without a title falls back to its id.
820
+
821
+ ## Layout rules that were bugs once
822
+
823
+ - **Live thinking is a block, not a line.** Reasoning that is still streaming is
824
+ wrapped and shown tail-first, `--thinking-lines` deep when collapsed and four
825
+ times that when expanded. It used to be a single row holding the last few
826
+ characters of the whole block, which is unreadable exactly when you want it.
827
+ - **Live thinking goes through the Markdown renderer.** It used to be wrapped as
828
+ plain text, so `**bold**`, backticks and `[links](url)` were shown as their own
829
+ syntax while the committed block below it rendered them — two different
830
+ formats for the same reasoning.
831
+ - **The header never loses the session.** Segments carry a priority: the session
832
+ id and the status label are essential and are never dropped, while the title,
833
+ the path and the error text are dropped — title first — and then the status
834
+ text is shortened rather than the session being squeezed out.
835
+
836
+ ## Performance
837
+
838
+ `npm run bench` renders frames against growing logs. Because a frame only ever
839
+ shows the tail, per-frame cost must stay flat:
840
+
841
+ ```
842
+ entries= 50 per-frame= 2.3ms
843
+ entries= 1000 per-frame= 2.9ms
844
+ entries= 10000 per-frame= 9.2ms
845
+ ```
846
+
847
+ An earlier implementation rendered every entry per frame — 2100 ms per frame at
848
+ 3000 entries, which is what made the wheel feel laggy on a long session. The
849
+ benchmark fails if the worst case exceeds 30 ms.
850
+
851
+ ### Reproduce the live demo offline
852
+
853
+ ```sh
854
+ node scripts/mock-provider.mjs &
855
+ DSH_HOME=/tmp/dsh-demo dsh --profile headless "explain the plugin" &
856
+ DEEPSEEK_BASE_URL=http://127.0.0.1:8799 DEEPSEEK_API_KEY=sk-mock dsh-live-trace
857
+ ```
858
+
859
+ ### See the board without any Harness
860
+
861
+ ```sh
862
+ npm run demo # scripts/demo.mjs — a scripted trace on the real renderer
863
+ ```
864
+
865
+ ---
866
+
867
+ ## Design notes
868
+
869
+ - **Zero runtime dependencies.** The renderer is hand-written ANSI rather than
870
+ `blessed`/`chalk`. `blessed` mis-measures CJK (the harness's primary audience
871
+ writes Chinese), is unmaintained, and would force a network install into the
872
+ profile; a `Segment[]`-based renderer keeps colors out of string parsing and
873
+ makes the 80-column contract testable without a TTY.
874
+ - **The plugin cannot break the agent.** Every hub call is wrapped so an
875
+ observer bug becomes a dropped line, never a failed turn. Nothing is written to
876
+ stdout, which belongs to whatever surface the Harness is driving.
877
+ - **Cleanup is owned by the Cordis fiber.** Every `ctx.on` disposer is collected,
878
+ every timer cleared, the socket closed and unlinked, and the discovery record
879
+ removed — but only if it is still ours, so a hot-reload generation cannot
880
+ delete its successor's record.
881
+ - **Repetition is collapsed, not hidden.** Consecutive identical entries render
882
+ once as `×N` with the newest timestamp.
883
+ - **One row per tool call.** The plugin gives a call and its result the same
884
+ `key` and stamps the duration on the settlement, so the trace shows one block
885
+ per command rather than two lines the reader has to pair up by hand.
886
+ - **Reasoning and output are separate.** They are different things to watch, so
887
+ the live indicator gives each its own row instead of one overwriting the
888
+ other.
889
+ - **A frame only renders what it shows.** The trace is bottom-anchored, so the
890
+ renderer walks entries backwards until the window is covered, and memoizes
891
+ each entry's rows by identity. Both are needed: windowing bounds the work,
892
+ the cache keeps a steady repaint nearly free.
893
+ - **The mouse is claimed deliberately.** Many terminals report the wheel as
894
+ arrow keys, which is indistinguishable from a keypress and scrolls one line
895
+ per notch. Claiming the mouse with SGR coordinates makes scrolling exact;
896
+ `--no-mouse` gives the old behaviour back, and Shift+drag still selects text.
897
+ - **Elapsed time means the current turn**, or time in the current state when no
898
+ turn is open. Session age would be misleading for a resumed session.
899
+
900
+ ## Limits
901
+
902
+ - The socket transport is Unix-domain only; Windows named pipes are not
903
+ implemented.
904
+ - `--plain` mode prints entries but not the live streaming preview.
905
+ - The footer's `S<n>/<m>` denominator is the highest step seen in the current
906
+ turn, not a planned total — the Harness does not publish one.
907
+ - The diff panel has no line numbers: the Harness reports applied hunks with
908
+ three lines of context, not positional ranges, so inventing numbers would be
909
+ guessing.
910
+ - Markdown rendering is a purpose-built subset (headings, lists, tables, quotes,
911
+ rules, fences, inline styles) with its own highlighter for 14 languages, not a
912
+ full CommonMark implementation. `m` shows the raw source at any time.
913
+ - A file **created** by the model is reconstructed from the call's `content`
914
+ argument, because a create has no prior text and therefore no hunks.
915
+ - The dashboard is read-only by design: there is no way to approve, cancel, or
916
+ steer from it.
917
+
918
+ ## License
919
+
920
+ MIT