dsh-live-trace
Verifieddsh-live-trace · v0.1.0 · MIT
Read-only live terminal dashboard for DeepSeek Harness sessions: a Host plugin that normalizes session events onto a local socket, plus a standalone full-screen `dsh-live-trace` viewer.
Install
dsh plugin add dsh-live-trace Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-live-trace
English · 中文
两扇只读的窗口,投向一个正在运行的 DeepSeek Harness 会话,渲染在不是运行你 agent 的那个终端里:
| 命令 | 是什么 |
|---|---|
dsh-live-trace |
轨迹面板:轮次、步骤、工具、输出、token 的实时轨迹 |
dsh-live-working |
小鲸鱼:一个动画场景,展示 agent 此刻在做什么 |
两者都通过同一个本地 socket 连接到同一个正在运行的 Harness 进程,并且都不会向 agent 发送任何东西。


| 思考 | 睡觉 |
|---|---|
![]() |
![]() |
dsh-live-trace — 轨迹面板
Harness Web UI 展示的是一段对话。这里展示的是机器:循环正处于哪个轮次、哪个步骤, 正在执行哪个工具,返回了什么,模型此刻在流式输出什么,已经运行了多久,以及消耗了多少 token —— 它持续更新,所以你能一眼看出 agent 是在干活还是卡住了。
┌─ dsh-live-trace ─────────────────────────────────────────────────────────────┐
│ Session: …8-4222-8d66-0bb3b8c30c45 · live trace demo Status: ⠇ running │
├──────────────────────────────────────────────────────────────────────────────┤
│ 10:19:00 [TURN 3] ─────────────────────────────────────────────────────── │
│ 10:19:00 [STEP 1] model call started │
│ 10:19:00 [USER] 把登录逻辑抽到 src/auth.ts,并补上测试 │
│ 10:19:02 [ASSISTANT] 先读一下现有的登录代码,确认调用点,再决定抽象边界。 │
│ 10:19:02 [TOOL] read_file path="src/login.ts" │
│ 10:19:02 [RESULT] ✓ 返回 234 行 0.2s │
│ 10:19:03 [TOOL] bash command="npm test" description="Run the test…" │
│ 10:19:03 [RESULT] ✓ 测试通过 (12 passed) 0.2s │
│ 10:19:05 [APPROVAL] bash — runs outside the sandbox │
│ 10:19:06 [APPROVAL] decision: allowed-once │
│ 10:19:06 [STEP 2] model call started │
│ 10:19:06 [ASSISTANT] 已完成:登录逻辑抽到了 src/auth.ts,12 个测试通过。 │
│ 10:19:06 [TURN 3 END] completed │
├──────────────────────────────────────────────────────────────────────────────┤
│ ⠇ running T3 · S2 12.3s Tokens 4.2K/1.0M (↑3.8K ↓180) q:quit │
└──────────────────────────────────────────────────────────────────────────────┘
四个面板共用同一套外观:
| 面板 | 按键 | 显示内容 |
|---|---|---|
| 轨迹 | 1 / t |
按时间顺序的事件日志,模型正文按 Markdown 渲染,每次工具调用一个块 |
| 会话 | 2 / s |
每个并发的 dsh 会话,实时;有多个在运行时,它也是首屏 |
| 改动 | 3 / d |
模型改过的每个文件,附 unified diff |
| 命令 | 4 / c |
每条 shell 命令及其输出、退出状态和耗时 |
┌─ dsh-live-trace · commands ──────────────────────────────────────────────────┐
│ Session: …a-412e-ac3d-c0f6a5bc824f · explain the plugin Status: ● idle │
├──────────────────────────────────────────────────────────────────────────────┤
│ COMMANDS 2 run │
│ ──────────────────────────────────────────────────────────────────────────── │
│ ✓ ls -1 dsh-live-trace && echo "--- entry ---" && head -4 … 0.1s exit 0 │
│ Inspect the plugin package layout │
│ README.md bin cordis.patch.yml │
│ … 13 more lines │
│ │
│ ▸ ✓ cd dsh-live-trace && node --test test/width.test.js … 0.3s exit 0 │
│ Run the measurement and tool-helper tests │
│ ✔ every wrapped line fits the width budget (30.5ms) │
│ ℹ tests 22 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ● idle T1 2.9s Tokens 21K/1.0M ↓7 1:trace d:edits ?:help │
└──────────────────────────────────────────────────────────────────────────────┘
模型正文按 Markdown 渲染 —— 标题、列表、表格、行内样式 —— 每个围栏代码块都会做语法
高亮,包括仍在流式输出的文本。思考默认是收起的:只显示几行,外加一个标记说明还有
多少;e 会展开所有块。shell 命令按终端的方式显示,命令本身带有语法高亮:
│ ┊ 先确认 login() 的所有调用点,再决定抽象边界。 │
│ ┊ 1. 只有两处直接调用,都在 src/routes/ 里。 │
│ ┊ … 5 more lines e to expand │
│ 11:58:09 [TOOL] bash Run the test suite ✓ exit 0 0.2s │
│ $ npm test │
│ 测试通过 (12 passed) │
$ 是暗色,命令名用函数色,flag 是属性色,运算符和带引号的字符串各有自己的 token
颜色。
│ │ 检查 │ 结果 │
│ │ 测试 │ 12 passed │
│ │ lint │ 2 warnings │
│ │ bash │
│ │ $ npm test │
│ │ ✓ 12 passed │
它不是 TUI 聊天客户端。它从不向 agent 发送任何东西。
工作原理
┌──────────────────────────── dsh web / dsh headless / dsh tui ─────────────────┐
│ Host process │
│ │
│ Cordis event bus ──► dsh-live-trace plugin ──► TraceHub ──► unix socket │
│ session/event (normalize) (state) $DSH_HOME/ │
│ agent/assistant-stream live-trace/ │
│ agent/status, agent/error sockets/*.sock │
└───────────────────────────────────────────────────────────────────────────────┘
▲
newline-delimited JSON
│
┌──────────────────────────── another terminal window ─────────────┐ │
│ dsh-live-trace (separate process, read-only) ──────────────────┘ │
│ alternate screen · ANSI renderer · scroll · replay on attach │
└─────────────────────────────────────────────────────────────────────┘
对显而易见的那个设计做一处纠正。 Cordis 插件不是一个独立进程:
ctx.on('session/event', …) 会在运行 Harness 的那个进程内部触发。所以拆成了两部分:
- 一个 Host 插件(
index.js),在进程内观察,并把归一化后的记录发布到一个 Unix 域 socket 上; - 一个独立查看器(
bin/dsh-live-trace.js),在它自己的终端里运行,连接那个 socket 并渲染。
插件在可做到的范围内是最强的只读:它不追加任何会话事件,不注册工具钩子,不改写 prompt,也从不写 stdout。它只是监听。
选择 Unix socket(而不是 TCP 端口)是刻意的:它无法从另一台主机访问,不会和 Web UI 的端口冲突,不需要一套认证方案,而它的文件权限本身就是访问控制。
实际用到的事件词汇
Harness 发布的事件集合很小且精确。轨迹面板的映射如下;其它都是插件自己的事件类型,
除非 showUnknownEvents: true,否则会被隐藏。
| 来源事件 | 标签 | 说明 |
|---|---|---|
session/created / session/disposed |
[SESSION] |
会话生命周期 |
turn/start / turn/end |
[TURN n] / [TURN n END] |
轮次开始会渲染成一条分隔线 |
step/start / step/end |
[STEP n] / [STEP n END] |
每个步骤一次模型调用 |
user/message |
[USER] / [CONTEXT] |
注入的上下文按其 source.kind 打标签 |
assistant/message |
[ASSISTANT] |
正文加一段 thinking: 摘录;携带 token 用量 |
assistant/attempt |
[ATTEMPT] |
一次没有提交任何消息的尝试 |
tool/call + tool/result |
[TOOL] |
一个块:名称、它运行的命令、它的输出、✓/✗ exit N 和耗时。两条记录共享同一个 key,所以结果会原地升级正在显示的那一行。 |
tool/result meta.diffs |
[TOOL] + 改动 |
已应用的文件 hunk;新建文件(此前没有文本)会从这次调用的 content 参数重建 |
approval/asked / approval/decided |
[APPROVAL] |
同时会触发等待审批 |
session/title |
[TITLE] |
同时更新头部 |
permission/preset, sandbox/mode, approval/policy |
[POLICY] |
启动时各占一行 |
agent/assistant-stream (live) |
thinking / writing |
合并后发布,绝不按 chunk;推理和可见文本各有自己的行 |
agent/status, agent/error (live) |
状态栏 / [ERROR] |
|
request/context |
页脚 | 提供上下文窗口的分母 |
总线上没有 assistant/chunk 事件 —— 实时流是 agent/assistant-stream,它的 chunk
帧会被累积,并按固定节奏(streamIntervalMs,默认 500 ms)发布,这样再快的模型也
刷不爆终端。持久事件 assistant/message 才让流落定。
安装
它分两半:插件,由 Harness 加载,用来发布会话事件;以及查看器,也就是你运行的 那个命令。
插件
在 npm 上发布为 dsh-live-trace。
插件必须被你的 Harness 启动时所用的 profile 选中。
A. 从 npm 安装
dsh plugin --profile web add dsh-live-trace
然后把 "dsh-live-trace" 加到 $DSH_HOME/profiles/web/package.json 里的
dsh.profile.bundles 中。
B. 文件级安装器(离线,无包管理器)
node /path/to/dsh-live-trace/scripts/install-profile.mjs --profile web
它会把 dsh-live-trace 加进该 profile 的 dsh.profile.bundles,添加一条 link:
依赖,并把包软链到该 profile 的 node_modules。重启 Harness(或者让热重载自动加载)。
C. 从本地检出安装,由 pnpm 管理
dsh plugin --profile web add /path/to/dsh-live-trace
然后把 "dsh-live-trace" 加到 $DSH_HOME/profiles/web/package.json 里的
dsh.profile.bundles 中。这条路径需要 PATH 上有 pnpm。
要撤销以上任一方式:node scripts/install-profile.mjs --profile web --uninstall。
查看器
npm install -g dsh-live-trace
这会把三个命令放进 PATH:dsh-live-trace(轨迹面板)、dsh-live-working
(小鲸鱼)和 dsh-glyph-probe(打印你的字体能渲染出什么)。如果是从检出目录运行,
就直接从 bin/ 里跑,或者手动 link:
npm install -g .
不启动任何东西,先验证组合是否正确:
dsh --profile web --dump-config | grep -A4 'dsh-live-trace'
运行轨迹面板
dsh-live-trace
插件加载后,Harness 会在 $DSH_HOME/live-trace/servers/ 下发布一条发现记录,写明它的
socket、工作目录和会话。查看器会清理掉已死的记录,优先选择当前工作目录里的进程,并绑定
到最新的活跃会话。
dsh-live-trace --list # what was discovered, then exit
dsh-live-trace --session <id> # bind one session
dsh-live-trace --socket <path> # bypass discovery entirely
dsh-live-trace --runtime-dir <path> # non-default $DSH_HOME
dsh-live-trace --plain # one line per event, pipe-friendly
dsh-live-trace --wait # poll until a Harness appears
按键
| 按键 | 动作 |
|---|---|
1 / t |
轨迹面板 |
2 / s |
会话选择器(esc 返回到下层那个面板) |
3 / d |
文件改动面板 |
4 / c |
命令面板 |
tab |
循环切换面板 |
↑ / ↓ |
滚动轨迹,或在列表面板里移动选中项 |
PgUp / PgDn |
翻一页 |
Home / End |
最早 / 最新 |
enter |
绑定高亮的会话 |
esc |
先离开选择器,再离开面板;只有从轨迹里才会退出 |
e |
展开或收起所有思考块 |
m |
切换 Markdown 渲染(显示原始源码) |
| wheel | 任何面板下,滚轮每格滚动三行 |
| click | 选中指针下的那一行 |
p |
暂停或恢复跟随新条目 |
r |
请求插件重放本会话 |
? |
按键帮助 |
q, Ctrl-C |
退出 |
首屏
当有多个会话在运行、又没有给 --session 时,dsh-live-trace 会打开会话选择器
—— 同时跑着好几个 dsh 会话时,做选择是你第一件要做的事。只有一个会话时它直接进入
轨迹,而显式给出 --session <id> 永远优先。
多会话切换
Harness 知道的每个会话都会实时列出,带上它的活动、轮次、步骤、安静了多久、标题和工作
目录。↑/↓ 移动选中项,enter 绑定它;插件随后重放该会话的积压内容,所以切换过去
永远不会看到空白面板。如果你正在跟随的会话结束了,查看器会自动跟随下一个活跃会话。
配置
每个键都是可选的;插件会防御性地校验,并在取值不合法时回落到默认值,而不是拒绝启动。
把它们写进 $DSH_HOME/profiles/<profile>/cordis.patch.yml:
- id: dsh-live-trace
config:
enabled: true
streamIntervalMs: 500 # 50…60000 — coalesced streaming cadence
backlogSize: 2000 # 10…100000 — entries retained per session
heartbeatMs: 5000 # discovery/heartbeat cadence
replayLimit: 500 # entries replayed to a late-joining viewer
showSystemMessages: false # render the system prompt as an entry
showRequestMetadata: false # render request/header as entries
showUnknownEvents: false # render unrecognized plugin events
mutedEventTypes: # `*` is a prefix match
- 'session-log-deepseek/*'
textLimit: 4000 # cap on one entry's primary text
outputLines: 200 # 1…100000 — command output lines retained
outputChars: 20000 # 200…1000000 — command output characters retained
runtimeDir: null # override $DSH_HOME/live-trace
socketPath: null # override the derived socket path
插件刻意不声明 Config schema:它必须能在解析不到 schema 包的 profile 里加载,而写错
的选项应该退化到默认值,而不是阻塞启动。
线路协议(v1)
socket 上跑的是按行分隔的 JSON。每条记录都带 v 和 kind;未知的 kind 在双向都会被
忽略,所以任意一半都可以先升级。
插件 → 查看器: hello(服务器身份、会话列表、活跃会话)、sessions、entry
(一行归一化的轨迹)、stream(合并后的实时文本)、stream-end、status、
usage、edits、heartbeat、error。
查看器 → 插件: select(绑定一个会话;省略 id 表示"服务器的默认会话")、
replay、ping。
一条 entry 可以带 key;两条 key 相同的记录是同一行,后一条原地替换前一条。工具调用
和它的落定之所以能保持为一个块、而不是两条半截的行,靠的就是这个。
会话作用域的记录只会到达绑定该会话的查看器,而 sessions 记录会按每个查看器实际持有的
boundSessionId 做个性化。没有指定会话的查看器会一直询问,直到出现一个会话,然后跟随
它 —— 所以在第一个会话出现之前就打开轨迹面板是可以的,而会话结束后视图会交给下一个。
验证
npm test 会运行 243 个测试(node --test),包括:
- 渲染器不变量 —— 宽度 24…200、高度 8…60 时,每一帧的每一行都恰好等于终端宽度, 包括含 CJK 文本、emoji,以及试图注入转义序列的内容。
- 测量 ——
displayWidth把汉字/假名/谚文/emoji 算作两个单元格,把组合字符算作 零;折行和截断从不切开一个宽字符。 - 归一化 —— 每一种被映射的事件类型,外加畸形参数、未知错误和未知事件类型。
- Markdown 与高亮 —— 对每种受支持的语言都有一条强制的字符保真不变量;一组文档样本 和一轮对抗性 fuzz 在六种宽度下的宽度约束;未闭合的围栏(流式场景);以及表格。
- 工具块、diff 与命令 —— 按 key 配对调用/结果、shell 退出状态标记契约
(
[exit code: N]、[killed by signal: X])、diff 元数据的收窄、新建文件的整文件 重建,以及反复编辑时按路径累积。 - 面板 —— 每个面板在每种尺寸下都守住所占宽度精确的契约,并且每个面板都会解释空 会话,而不是显示一个空盒子。
- 窗口化 —— 在每种尺寸和滚动位置下,窗口化的帧逐字节地画出无界渲染会画的行,并且 回滚环形缓冲在裁剪时不会丢失它的 key 索引。
- 输入 —— 方向/导航键、被拆分到多次 read 里的 SGR 鼠标上报、滚轮映射、点击的按下 与抬起、移动事件被拒绝,以及未终止序列的有界缓冲。
- 传输 —— 跨分片的 NDJSON 分帧、按查看器的会话路由、接入时重放、服务器重启后重 连,以及拆卸清理。
- 对真实 Harness 的集成 —— 在真实的 Cordis 上下文里启动随包发布的
@deepseek-ai/dsh-session插件,创建并追加真实会话,并断言查看器从一个真实 socket 上收到了什么。没有安装 Harness 时它会干净地跳过。 - 端到端 CLI —— 针对真实的观察器 socket 启动真正的
dsh-live-trace可执行文件: 发现流程、--list、备用屏幕面板在q时退出、捕获输出里 80 列的行宽、plain 模式, 以及找不到观察器时的诊断信息。 - 拆卸后不留东西 —— 三轮 mount/unmount 循环不会累积任何实例、socket 或注册表 记录;另一个把一切都启动再 dispose 的独立程序必须自己退出,所以一个还活着的 socket 或定时器会让它挂住。把这个程序里的 dispose 调用去掉它就会挂住,这就是证明该 检查确实有效的反向对照。
在测试套件之外,这个包还针对一个已安装的 Harness 验证过:
dsh --profile web --dump-config会组合出插件的配置行,并把用户的 patch 层应用 上去。- 一个真实的
dsh web进程会启动插件,插件创建自己的 socket 和发现记录;dsh-live-trace --list能找到它,面板也就接上了。 - 一个真实的
dsh headlessagent 循环(由scripts/mock-provider.mjs驱动,那是一个 离线的 Messages API 服务器)会把真实的轮次、步骤、工具、结果和 token 事件流进 面板,其中包括一次真实的write,它实际应用的 diff 会出现在改动面板里。
dsh-live-working — 小鲸鱼
轨迹面板告诉你发生过什么。这个告诉你正在发生什么:一只像素画小鲸鱼坐在桌前,桌上有 键盘、一摞书和一部红色电话,动画会跟着会话变化。
▄█接 2 号子代理████████████████▄
██把 auth 模块里的校验逻辑抽出来██
▀██████████████████████████▀
▄█▀
▀▀ ▄▄██▄▄▄▄
▀▀▀▀▀▀▀▀ ▄▄
████████ ██████▄▄
▄▄▀▀ ████████▄▄
████████ ████████████
██████████ ▄▄██████████████████▄▄▄▄ ▀▀██████████
█ █ ▄▄████████████████████████▄▄▄▄ ████████▀▀
▄▄█████████████████████████████████▄▄ ▄▄██████
██████████████████████████████████████▄▄▄▄██████
████████████████████████████████████████████████
██████████████████████████████████████████████
▀▀██████████████████████████████████████████▀▀
▀▀██████████████████████████████████████▄▄▄▄▄▄
▄▄██████████████████████████████████████████
████████████████████████████████████████████
▄██████████▄ ██████████████████████ ████████████████
████████████ ██████████████████████ ████████████████
██████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████████████
███ ███
场景会按你的终端调整尺寸 —— 最宽到 200 个单元格,小鲸鱼会视可用空间按 1×、2× 或 3× 绘制,所以宽终端会得到一只更大的生物,而不是同一只漂在巨大空桌子上。像素画只按整数 倍缩放;非整数倍会产生大小不一的块,看起来像渲染故障。
精灵图是按参考美术的原生 64×40 像素网格采样的(那份美术里的块宽为五个源像素),
所以原图一行都不会丢。源图里鲸鱼上方的灰色痕迹,是一张睡觉的鲸鱼插画里的三个 Z
字形 —— 采样时把它们排除掉,于是这只生物落在干净的透明背景上,由场景自己画 z,而且
只在它真的睡着时才画。
它做什么,以及为什么
| 状态 | 触发条件 | 动画 |
|---|---|---|
sleep |
没有工作;或者某条正在运行的命令已经 8 秒没有输出 | 闭眼、慢慢上下浮动、z 向上飘 |
thinking |
推理正在流式输出,或者 agent 正在等待审批 | 手离开键盘,一串思考的点 |
typing |
正文或代码正在流式输出 | 按键成排亮起 |
writing |
正在运行 write/edit 工具 | 桌上的一张纸增加一行 |
waiting |
shell 命令正在运行且仍在输出 | 一个带闪烁光标的终端块 |
reading |
正在运行 read/glob/grep 工具 | 眯起眼睛,每隔几秒眨一次,举着一本打开的书 |
searching |
正在运行网络搜索或抓取 | 翻过一页,从右向左扫过书脊 |
calling |
启动了一个子代理 | 红色电话被拿起,斜着贴在耳边,手离开键盘,并出现一个气泡 |
ringing |
一个子代理结束了 | 电话响起并震动,气泡里显示它带回了什么 |
两种背景:room 与 nature
--scene room(默认)是一张靠窗的桌子:一堵墙,墙上开着一扇窗,天空被限制在窗格里。
--scene nature 拿掉了墙 —— 背景就是户外,于是天气覆盖整个场景,地面一直延伸到屏幕
底部、桌子就立在上面,针叶树沿地平线排开。任何时候按 b 都能在两者之间切换。
两者都由同一片天空构成。窗户和户外都会先在自己的画布上合成内容,再做 blit,所以"任何 东西都不会跑出分配给它的区域"这条约束对两者都成立。
房间
房间由它的窗户照亮。 墙面、墙面的明暗、踢脚线和地板每一帧都从天空推导出来,所以
房间夜里会变暗、正午会变亮,而不是固定的中灰色 —— 黑色天空旁边一块亮灰色板,是夜景里
最糟糕的一点。墙面从正午的 181,176,166 变到午夜的 64,66,75,并且从不下探到某个
下限以下,所以房间始终清晰可读。
这个场景是一个房间,而不是一只漂在终端背景色上的精灵图:一堵带浅色纸条纹的墙,墙与
地板相接处的踢脚线,以及立在那块地板上的桌子。窗户是墙上的一个开口,于是其它一切
都有东西作为衬托来阅读 —— 尤其是睡觉的 z:它以前画在天空上,会消失在云里;搬到墙上
之后又变成了中灰色墙上的中灰色。现在它有了自己的颜色。
窗户里的一切 —— 天空、太阳和月亮、云、雨、雪、雾 —— 都在窗户自己的画布上合成后再 blit 进来,所以它们谁都不能飘出这个开口、爬到墙上。
窗外
墙上开着一扇窗,窗外是一个按自己的时间运行的世界。每过一秒真实时间,游戏内就过去 一分钟,所以一整天是二十四分钟真实时间,天空也从不静止:颜色从午夜蓝经黎明橙走到 正午蓝再走回来,太阳和月亮在窗格间沿弧线轮流出现,星星只在夜里现身。
那里也会下雨。天气每三个游戏内小时自行变化一次,取值来自这段天气的序号,而不是随机数 生成器,所以两个看着同一会话的查看器看到的是同一片天空。它是淡入淡出而不是硬切,并且 白天下的雨到夜里就变成雪 —— 同一种天气,只差一个温度。
窗户是一幅画,不是一块色卡:
- 天空是渐变的。 终端没有 alpha,但它有网格:两种颜色加一个 4x4 Bayer 抖动,让 天空在头顶更暗、靠近地平线更亮,并在黎明和黄昏把地平线染暖、同时顶部保持冷色。两种 平涂颜色看起来是一条色带;加上抖动才像天空。
- 有山。 窗格底部一道起伏的山脊让这扇窗有了归属,太阳和月亮沉到它后面。
- 太阳和月亮有光晕,经过抖动,所以是淡出而不是戛然而止。
- 云是积云:三个互相重叠的椭圆,下面一边是平的 —— 不是它们最初那样的横条。
- 雾是一片薄霭,不是一条带。 它覆盖窗格的四分之一而不是三分之一,最多只填一半, 越往上越稀,颜色取自天空而不是固定的浅灰 —— 所以它融进天气里,而不是看起来像一条 灰条纹。实测下来,它从占窗格的 23% 降到 5%。
- 星星有两种亮度,各按自己的节奏闪烁。
天气会从天空里抽走光:晴天的正午是 765 中的 535,多云 439,下雨 331,暴风雨 240。 恶劣天气还会遮住太阳或月亮,这正是它看起来"恶劣"的原因。只有天空真正暗下来时星星才会 出现 —— 若改成按昼夜阶段来决定,就会在明亮的橙色黎明里点出白点。页脚显示游戏内时钟和 当前天气。
雨可以下出声。 --sound(或任何时候按 n)会通过 aplay、paplay、sox 或
ffplay 中已安装的那个播放雨声,而且只在真的下雨时播放。它默认关闭,因为一条会
自己开始放声音的命令,是那种人们会不再运行的命令。
音量是单独的一项控制。 --rain-volume <0-100> 设定它(默认 40),命令运行期间
- 和 + 每次调整 5,页脚会在声音状态旁边显示当前音量。它从 40 而不是满量程起步是
刻意的:那段录音的峰值是 -6.1 dBFS,属于前景音量;而在 40% 时峰值约 -14 dBFS —— 是
环境音,而不是需要关掉的东西。合成的噪声按同一比例缩放,所以平均采样值从 2023 变成
809(峰值从 5898 到 2359,从 -14.9 到 -22.9 dBFS):同样的雨,只是少一些。
除了一条路径,音量在所有路径上都能到达扬声器。在 file 模式下 paplay 收到
--volume,sox 收到 -v,ffplay 收到 -volume;合成流把音量做进自己的采样里,
所以每个播放器在那里都遵守它。aplay 根本没有音量参数,所以通过 aplay 播放的
录音,听到的就是它被录制时的音量。这一点没有被悄悄忽略:页脚会写
aplay cannot change a file level,而不是打印一个会撒谎的百分比。如果你在一台只有
aplay 的机器上想要更安静的录音,就传一个更安静的 --rain-file,或者装上 paplay、
sox 或 ffplay。
默认音源是随包附带的那段录音。--rain-file 可以指定别的。如果文件不存在 —— 或者资源
读不出来 —— 就会回落到合成噪声:一段经过低通的伪随机采样流,永续播放,没有循环点。
正是靠它,这个功能在一台只有一个播放器的机器上也能用。
录音播放期间改变音量会重启播放器,因为音量存在于它的命令行里;合成流则会在下一个块 无缝地采用新音量。
改变音量会重启播放器,因为音量是它的一个参数。旧播放器会先被停掉,但它的退出事件是在 新播放器已经跑起来之后才到的 —— 所以状态更新只在来自当前那个进程时才被接受,而所有 启动过的进程都会被跟踪并一起杀掉。两者缺一,改一次音量就会留下两路雨声在响,以及一个 在命令退出后还在继续的孤儿播放器。
一个刚启动就死掉的播放器说明没有音频设备;它会重试三次,然后就不再管它,而不是永远重启 下去。
包里附带一段雨声循环。 assets/rain.ogg(30 秒、单声道、64 kbps、242 KB)是默认
音源,所以 --sound 不需要任何参数。它是从那段 8 小时的录音
42130539966-1-192.mp4(其音频为 aac 48 kHz 立体声)的一小时处剪下来的,并把尾部
交叉淡入到开头,让循环没有咔哒声:两端音量相差在 20% 以内,接缝处的采样落差是全量程的
0.35%。它的峰值是 -6.1 dBFS;在默认的 40% 音量下约为 -14 dBFS。--rain-file 可以
覆盖它。
关于这个目录里那个 2.2 GB 的 42130539966-1-192.mp4: 它不是包的一部分
(package.json 的 files 列表里没有它,.gitignore 也忽略了 *.mp4),而这台机器
上没有 ffprobe、ffmpeg、aplay、paplay、sox 或 mpv —— ffplay 倒是有,
但它只能播放文件、不能检查文件 —— 所以这个视频在这里既看不了也放不了。--rain-file
接受的是音频文件;.mp4 是视频容器,得先把音轨抽出来,而那需要 ffmpeg:
ffmpeg -i 42130539966-1-192.mp4 -vn -ac 1 -ar 44100 rain.wav
dsh-live-working --sound --rain-file rain.wav
要放真实录音,就传 --rain-file;--rain-volume 在所有带音量参数的播放器上对它依然
有效:
dsh-live-working --sound --rain-volume 25 --rain-file ~/sounds/rain-loop.wav
不存在的文件会被放弃并回落到合成噪声,而不是变成静音。ffplay 原生支持循环播放文件;
其它播放器会在文件结束时被重启,但只在还在下雨的时候。授权由你判断、也由你负责 ——
本项目自己不分发任何音频:
- Wikimedia Commons: Sounds of rain —— 自由许可,逐文件条款
- Freesound —— 按许可证筛选;CC0 无需署名
- Creazilla: Ambience Rainstorm —— 免版税
- Internet Archive: Red Library — Nature Rain
工作台 · 等待命令 06:13 雾 c4c9e25b082a T23·S19 关闭本帮助:?
小鲸鱼自身的动作
尾巴是画出来的,每个状态一个姿态。 源美术只有一条竖直的尾巴,没有可动的地方: 尾鳍顶到身体的最后一行和精灵图的右边缘,而桌子就压在最后一行上。对着它试过六种 变换 —— 剪切、折叠、旋转、锥形剪切、整体平移、水平折叠 —— 每一种都只是把一种瑕疵 换成另一种:边缘毛糙、轮廓撕裂、尾巴跑到桌子下面、桌子上面多出尖角,或者透出背景的 空洞。现在有两个姿态,由身体加一条尾巴组合而成:
up直接从源美术里逐像素取出,所以醒着的小鲸鱼就是参考图画的那个样子;sleep是画出来的:尾鳍垂下来,沿身体贴在桌线处,末端收成圆头;它的边缘是生成 出来的,而不是丢给渲染器去猜。
画出来的姿态不会丢像素、撕裂或留下空隙 —— 这些故障在结构上就不可能发生,而不只是被 修好了。
醒着的尾巴从根部摆动。 以前整条尾巴是刚性平移的,这会让它离开身体、从接缝处透出 背景;现在最靠近身体的那些列被锚定,只有外面的一部分摆动,于是尾巴是绕轴转,而不是 平移。小鲸鱼打电话时它保持不动,因为鳍正忙着。
夜里它会揉眼睛 —— 工作期间时不时闭上眼、把一只鳍抬到脸上,然后接着做原来的事。 只在夜里,而且睡着或打电话时绝不会。
桌子
桌子是"搭"出来的,而不是贴上去的:键盘是桌上一小条深色横条,而右上角一个带边框的 窗口显示正在输入的文本 —— 模型输出的尾部,或者它正在运行的工具的参数。有文本时就会 画这个窗口的边框,因为没有边框的文本看起来像一个漂在终端顶部的野气泡。
小鲸鱼打字时鳍会动,用的是一种和身体略有色差的蓝,这样这条肢体才看起来像肢体。 采样的参考美术是侧视图,没有单独的胸鳍,所以这些划水的鳍是画上去并叠在按键上的动画; 睡着时它们会被收起来。打电话会完全停止打字 —— 听筒贴在脸上,上到眼睛、下过下巴, 而不是横在身体上或者直立在桌上。
书是有明暗的,这正是让翻页看起来清楚的原因:摊开的两页受光不同,中间的书脊落在阴影 里,而翻到一半的那一页是侧对着的,比两边都暗。
电话气泡上写的是 接 N 号子代理,<instructions>(英文下是
Subagent #N — <instructions>),其中 N 是本会话里子代理的序号,instructions 是这次
调用收到的参数。
电话
派发一个子代理就是一通电话,整个交互都是照这个来建模的:
- 气泡显示的是调用原本写成的样子,不只是里面的指令:
task(description="…", prompt="…"),连工具名和每一个参数一起。同时有多个子代理在 外面时,抬头会变成一个队列 ——接 3 号子代理(共 5 个,排队 2 个)。 - 消息是一个字一个字"说"出来的,气泡一次显示三行。更长的消息会滚动。抬头被钉在 上面:它说明电话那头是谁,若被长指令挤掉,气泡就变成了无名的。
- 子代理挂断时电话会响。 听筒震动,电话两边出现铃声弧线,气泡显示子代理自己的收尾 文本 —— 它的回答,而不是对回答的概括。
- 后台派发是"收到",不是"回答"。 在后台启动一个子代理会立刻返回
started subagent <id>;回答稍后作为那个 agent 的消息到达。只有后者才算数,所以后台 子代理会保住它在队列里的位置,而不是一启动就好像已经结束。 - 听筒是被拿起和放下的,不是瞬间移动:它从容地离开支架,旋转到耳边,放下时沿同 一条路径返回,电话线随着它升起而松开。
- 历史不是新闻。 查看器启动时会重放积压内容,而每条记录都带自己的时间戳。若改用 墙上时钟给事件打时间戳,一小时的历史看起来就像全都正在发生,于是电话会为几分钟前就 已经结束的子代理响起来。现在每个时间戳都来自事件本身。
- 活派出去、没别的事可做,就是打个盹。 子代理在外面、又没有别的工具在跑时,小 鲸鱼就像空闲时一样在桌前打盹,而叫醒它的是响起的电话。
气泡锚定在小鲸鱼头顶上方,尾巴指向说话者,并会裁剪以避开预览窗口。一个钉在场景角落的 对话气泡什么都没指着,看起来就像一个漂在界面外的野盒子 —— 这正是它两次被反馈的问题。
dsh-live-working # follow the server's default session
dsh-live-working -s <session-id> # follow a specific one
dsh-live-working --state reading # pin one animation, ignore the session
dsh-live-working --list # show observers and sessions
有多个会话在运行时,它打开的是会话选择器,而不是去猜你指的是哪个;s 重新打开
它,↑/↓ 选择,enter 绑定,esc 取消。
按键:1-8 固定某个动画,0 回到自动,s 选择会话,n 开关雨声,- 和 +
调整雨声音量,l 切换语言,? 开关帮助,q 退出。
dsh-live-working — a live orca animation for a DeepSeek Harness session
States: sleep, thinking, typing, writing, waiting, reading, searching, calling
node scripts/demo-working.mjs 会循环播放每一个状态,不需要会话;
node scripts/demo-working.mjs reading 则固定在某一个状态。
一个终端单元格能承载多少分辨率
一个字符单元格的高度大约是宽度的两倍,所以选哪种字形,同时决定了像素的分辨率和形状:
| 打包方式 | 每格像素 | 像素形状 | 字形 | 说明 |
|---|---|---|---|---|
half |
1 x 2 | 正方形 | ▀ ▄ █ |
这个轨迹面板用的就是它 |
quadrant |
2 x 2 | 1:2 高 | ▘ ▝ ▖ ▗ ▚ ▞ ▛ ▜ ▙ ▟ |
边缘更细,但一切都拉长了 |
braille |
2 x 4 | 正方形 | U+2800..U+28FF |
像素是 4 倍,但每一个都是一个点 |
half 是唯一既正方形又实心的打包方式,而实心像素画正需要这个。quadrant 把
横向数量翻倍,但它的像素高是宽的两倍,所以为正方形像素画的图会显得被拉长。braille
用正方形像素给出四倍的像素,但点与点之间有间隙,所以实心区域会填成点画 —— 画图表很好,
画鲸鱼很差。
它们能不能用取决于你的字体,而缺少这些字形的字体不会大声报错:它会替换成方框或空白, 于是画面悄悄散架。所以先看一眼:
dsh-glyph-probe
它会打印每种打包方式的样例,以及一个按该打包方式自身分辨率栅格化的圆盘。看起来圆润 无缝的那个圆盘,就是你的字体支持的那一种。
程序能让终端字体变小吗?
不能。 没有这样的转义序列。ESC[?3h 只是在 80 列和 132 列之间切换,并不改变
字体,而 VTE/GNOME Terminal 根本没有办法做到 —— 这可以直接从源码讨论里读到。kitty 的文本缩放协议缩放的是单段文本,而且只适用于 kitty。
即便在可能生效的地方,把它交给应用去管也是一笔坏交易:一次崩溃、一个 SIGKILL、一个
被关掉的终端或一次断开的 SSH 会话,都会让字体留在小号状态,而没有任何东西能把它恢复。
终端愿意做的是报告自己的几何尺寸,而这一半是值得要的 —— CSI 16t 返回以像素
为单位的单元格尺寸,CSI 14t 返回文本区域。dsh-glyph-probe 两者都问,并做这道
算术:
Cell size 9x18 px (aspect 0.50)
Cells 148x40
Drawing budget 11,840 pixels (two per cell)
Half the font 333x80 cells -> 53,280 pixels
把字体缩小并不改变窗口的像素;它改变的是能塞进这些像素的单元格数量,而每多一个 单元格,就多两个像素的绘制预算。那才是真正的收益,而且这份收益该由用户自己去拿。
在伸手去够更高分辨率的打包方式之前,有两件事值得知道:
- 鲸鱼就是它源文件的分辨率。 参考美术是一个 64x40 的精灵图,给不出更多细节;放大 它只会得到更大的块。想要更精细的鲸鱼,需要更精细的美术,而不是更精细的渲染器。
- 场景已经占满终端。 它最宽按 200 个单元格渲染,并且用满它能用的每一行,所以想更 精确,最便宜的办法是开一个更大的窗口 —— 不管用什么字形,单元格更多就是像素更多。
两个会话列表都以标题开头
dsh-live-working 的选择器和 dsh-live-trace 的会话面板都把会话标题放在最前面,缩短
过的 id 跟在后面。id 是你要输入的东西,而标题并不唯一,所以两者都显示 —— 但能让你认
出来的是标题,所以它排在前面。没有标题的会话回落到它的 id。
曾经是 bug 的布局规则
- 实时思考是一个块,不是一行。 仍在流式输出的推理会折行并尾部优先显示,收起时深度
为
--thinking-lines,展开时是它的四倍。它以前只是一行,装着整个块最后几个字符, 偏偏在你想看的时候完全没法读。 - 实时思考会走 Markdown 渲染器。 它以前按纯文本折行,于是
**bold**、反引号和[links](url)都按字面语法显示,而它下面已经落定的块却会把它们渲染出来 —— 同一段 推理,两种格式。 - 头部永远不会弄丢会话。 各段带优先级:会话 id 和状态标签是必需的,永远不会被丢掉; 标题、路径和错误文本会被丢弃 —— 先丢标题 —— 然后宁可缩短状态文本,也不把会话挤掉。
性能
npm run bench 用不断增长的日志来渲染帧。因为一帧只显示尾部,每帧的开销必须保持平坦:
entries= 50 per-frame= 2.3ms
entries= 1000 per-frame= 2.9ms
entries= 10000 per-frame= 9.2ms
早先的实现每一帧都渲染所有条目 —— 3000 条时每帧 2100 ms,这就是长会话里滚轮感觉卡的 原因。最坏情况超过 30 ms 时,基准测试会失败。
离线复现实时演示
node scripts/mock-provider.mjs &
DSH_HOME=/tmp/dsh-demo dsh --profile headless "explain the plugin" &
DEEPSEEK_BASE_URL=http://127.0.0.1:8799 DEEPSEEK_API_KEY=sk-mock dsh-live-trace
不启动任何 Harness 也能看到面板
npm run demo # scripts/demo.mjs — a scripted trace on the real renderer
设计说明
- 零运行时依赖。 渲染器是手写的 ANSI,而不是
blessed/chalk。blessed测量 CJK 有误(Harness 的主要用户写中文),不再维护,而且会迫使 profile 里做一次联网 安装;基于Segment[]的渲染器把颜色挡在字符串解析之外,也让 80 列的契约不需要 TTY 就能测试。 - 插件无法弄坏 agent。 每一次 hub 调用都被包住,所以观察器的一个 bug 只会变成丢一 行,而不会变成一次失败的轮次。什么都不写 stdout —— 它属于 Harness 正在驱动的那个 界面。
- 清理由 Cordis fiber 负责。 每个
ctx.on的 disposer 都被收集,每个定时器都被 清除,socket 被关闭并 unlink,发现记录被删除 —— 但仅当它仍然是我们的:这样一代热重 载就不会删掉它继任者的记录。 - 重复会被折叠,而不是隐藏。 连续相同的条目只渲染一次,显示为
×N并带上最新的 时间戳。 - 每次工具调用一行。 插件给一次调用和它的结果同一个
key,并在结果落定时打上 耗时,所以轨迹里每条命令是一个块,而不是两行等着读者自己去配对。 - 推理和输出是分开的。 它们是两样不同的东西,所以实时指示器各给一行,而不是一个 覆盖另一个。
- 一帧只渲染它显示的内容。 轨迹是底部锚定的,所以渲染器从后往前走条目,直到覆盖住 窗口,并按身份缓存每个条目的行。两者都需要:窗口化给工作量封顶,缓存让稳定的重绘几乎 不花代价。
- 鼠标是刻意接管的。 很多终端把滚轮上报成方向键,这与按键无法区分,而且每格只滚一
行。用 SGR 坐标接管鼠标能让滚动变精确;
--no-mouse会把旧行为还回来,而 Shift+拖拽 依然可以选中文本。 - 已用时间指的是当前轮次,没有打开的轮次时则是处在当前状态的时间。对恢复过的会话来 说,会话总时长会产生误导。
限制
- socket 传输只支持 Unix 域;Windows 命名管道没有实现。
--plain模式会打印条目,但不打印实时流预览。- 页脚里的
S<n>/<m>中,分母是当前轮次里见过的最大步骤号,而不是计划总数 —— Harness 不发布计划总数。 - diff 面板没有行号:Harness 上报的是带三行上下文的已应用 hunk,而不是位置区间,所以 凭空造行号就是在猜。
- Markdown 渲染是一个专门做的子集(标题、列表、表格、引用、分隔线、围栏、行内样式),
配一个自带的、支持 14 种语言的高亮器,不是完整的 CommonMark 实现。任何时候按
m都能看到原始源码。 - 模型新建的文件是从调用的
content参数重建的,因为新建没有此前的文本,也就没有 hunk。 - 轨迹面板在设计上就是只读的:没有办法从它那里批准、取消或干预。
许可证
MIT

