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.
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/README.zh.md +790 -0
- package/assets/rain.ogg +0 -0
- package/bin/dsh-glyph-probe.js +51 -0
- package/bin/dsh-live-trace.js +29 -0
- package/bin/dsh-live-working.js +14 -0
- package/cordis.patch.yml +31 -0
- package/icon.svg +12 -0
- package/index.js +328 -0
- package/lib/client.js +178 -0
- package/lib/instance.js +68 -0
- package/lib/normalize.js +850 -0
- package/lib/paths.js +66 -0
- package/lib/protocol.js +115 -0
- package/lib/registry.js +232 -0
- package/lib/tools.js +257 -0
- package/lib/tracker.js +648 -0
- package/lib/transport.js +231 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -0
- package/picture/call1.png +0 -0
- package/picture/call2.png +0 -0
- package/picture/sleep1.png +0 -0
- package/picture/sleep2.png +0 -0
- package/picture/tui1.png +0 -0
- package/picture/tui2.png +0 -0
- package/picture/type1.png +0 -0
- package/picture/type2.png +0 -0
- package/scripts/bench-render.mjs +69 -0
- package/scripts/demo-working.mjs +130 -0
- package/scripts/demo.mjs +284 -0
- package/scripts/install-profile.mjs +174 -0
- package/scripts/mock-provider.mjs +211 -0
- package/src/cli/cellsize.js +120 -0
- package/src/cli/format.js +73 -0
- package/src/cli/highlight.js +932 -0
- package/src/cli/i18n.js +457 -0
- package/src/cli/main.js +630 -0
- package/src/cli/markdown.js +753 -0
- package/src/cli/renderer.js +1044 -0
- package/src/cli/screen.js +270 -0
- package/src/cli/theme.js +221 -0
- package/src/cli/view-state.js +396 -0
- package/src/cli/views.js +406 -0
- package/src/cli/width.js +337 -0
- package/src/cli/working/art.js +413 -0
- package/src/cli/working/main.js +569 -0
- package/src/cli/working/packing.js +159 -0
- package/src/cli/working/picker.js +75 -0
- package/src/cli/working/props.js +385 -0
- package/src/cli/working/scene.js +837 -0
- package/src/cli/working/sky.js +641 -0
- package/src/cli/working/sound.js +400 -0
- 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
|
+

|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
| Thinking | Sleeping |
|
|
21
|
+
| :---: | :---: |
|
|
22
|
+
|  |  |
|
|
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
|