dsh-ssh-tui 0.8.0 → 0.8.2-rc.1

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 (126) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.en.md +74 -8
  3. package/README.md +451 -577
  4. package/docs/display-mode.md +122 -0
  5. package/docs/release-notes-0.7.4.md +91 -0
  6. package/docs/release-notes-0.8.0.md +56 -0
  7. package/docs/release-notes-0.8.1.md +79 -0
  8. package/docs/release-notes-0.8.2-rc.1.md +159 -0
  9. package/docs/remote-ops.md +51 -0
  10. package/docs/terminals.md +53 -0
  11. package/docs/windows.md +13 -0
  12. package/lib/approval-state.js +148 -0
  13. package/lib/approval-state.js.map +1 -0
  14. package/lib/attach.js +4 -4
  15. package/lib/attach.js.map +1 -1
  16. package/lib/auth-failure.js +74 -0
  17. package/lib/auth-failure.js.map +1 -1
  18. package/lib/color-depth.js +26 -0
  19. package/lib/color-depth.js.map +1 -1
  20. package/lib/commands.js +2 -0
  21. package/lib/commands.js.map +1 -1
  22. package/lib/copy-text.js +30 -8
  23. package/lib/copy-text.js.map +1 -1
  24. package/lib/dialogs.js +124 -0
  25. package/lib/dialogs.js.map +1 -1
  26. package/lib/display-mode.js +147 -0
  27. package/lib/display-mode.js.map +1 -0
  28. package/lib/display-sock.js +490 -35
  29. package/lib/display-sock.js.map +1 -1
  30. package/lib/footer-accents.js +28 -0
  31. package/lib/footer-accents.js.map +1 -0
  32. package/lib/footer-budget.js +328 -0
  33. package/lib/footer-budget.js.map +1 -0
  34. package/lib/footer-format.js +433 -0
  35. package/lib/footer-format.js.map +1 -0
  36. package/lib/footer-layout.js +407 -0
  37. package/lib/footer-layout.js.map +1 -0
  38. package/lib/footer-meters.js +185 -0
  39. package/lib/footer-meters.js.map +1 -0
  40. package/lib/footer.js +19 -561
  41. package/lib/footer.js.map +1 -1
  42. package/lib/glyph-measure.js +92 -0
  43. package/lib/glyph-measure.js.map +1 -0
  44. package/lib/i18n/en.js +83 -7
  45. package/lib/i18n/en.js.map +1 -1
  46. package/lib/i18n/zh.js +85 -7
  47. package/lib/i18n/zh.js.map +1 -1
  48. package/lib/index.js +73 -5
  49. package/lib/index.js.map +1 -1
  50. package/lib/paint.js +75 -17
  51. package/lib/paint.js.map +1 -1
  52. package/lib/picker.js +14 -13
  53. package/lib/picker.js.map +1 -1
  54. package/lib/plan-projection.js +291 -0
  55. package/lib/plan-projection.js.map +1 -0
  56. package/lib/plan.js +61 -11
  57. package/lib/plan.js.map +1 -1
  58. package/lib/platform.js +96 -0
  59. package/lib/platform.js.map +1 -1
  60. package/lib/question-state.js +83 -0
  61. package/lib/question-state.js.map +1 -0
  62. package/lib/representation.js +283 -0
  63. package/lib/representation.js.map +1 -0
  64. package/lib/screen.js +105 -0
  65. package/lib/screen.js.map +1 -0
  66. package/lib/selection.js +5 -3
  67. package/lib/selection.js.map +1 -1
  68. package/lib/session-blank.js +81 -0
  69. package/lib/session-blank.js.map +1 -0
  70. package/lib/session-list.js +102 -81
  71. package/lib/session-list.js.map +1 -1
  72. package/lib/startup.js +56 -0
  73. package/lib/startup.js.map +1 -1
  74. package/lib/stats.js +101 -3
  75. package/lib/stats.js.map +1 -1
  76. package/lib/subagent-model.js +8 -7
  77. package/lib/subagent-model.js.map +1 -1
  78. package/lib/term-text.js +436 -69
  79. package/lib/term-text.js.map +1 -1
  80. package/lib/terminal-input.js +132 -10
  81. package/lib/terminal-input.js.map +1 -1
  82. package/lib/theme.js +335 -0
  83. package/lib/theme.js.map +1 -0
  84. package/lib/throughput.js +305 -0
  85. package/lib/throughput.js.map +1 -0
  86. package/lib/tool-present.js +67 -15
  87. package/lib/tool-present.js.map +1 -1
  88. package/lib/tui.js +4186 -1133
  89. package/lib/tui.js.map +1 -1
  90. package/lib/types/approval-state.d.ts +75 -0
  91. package/lib/types/attach.d.ts +6 -2
  92. package/lib/types/auth-failure.d.ts +55 -0
  93. package/lib/types/color-depth.d.ts +21 -0
  94. package/lib/types/commands.d.ts +6 -0
  95. package/lib/types/copy-text.d.ts +18 -1
  96. package/lib/types/dialogs.d.ts +147 -1
  97. package/lib/types/display-mode.d.ts +99 -0
  98. package/lib/types/display-sock.d.ts +90 -0
  99. package/lib/types/footer-accents.d.ts +22 -0
  100. package/lib/types/footer-budget.d.ts +204 -0
  101. package/lib/types/footer-format.d.ts +221 -0
  102. package/lib/types/footer-layout.d.ts +292 -0
  103. package/lib/types/footer-meters.d.ts +151 -0
  104. package/lib/types/footer.d.ts +19 -269
  105. package/lib/types/glyph-measure.d.ts +41 -0
  106. package/lib/types/index.d.ts +13 -0
  107. package/lib/types/paint.d.ts +48 -0
  108. package/lib/types/plan-projection.d.ts +162 -0
  109. package/lib/types/plan.d.ts +18 -2
  110. package/lib/types/platform.d.ts +83 -0
  111. package/lib/types/question-state.d.ts +96 -0
  112. package/lib/types/representation.d.ts +498 -0
  113. package/lib/types/screen.d.ts +126 -0
  114. package/lib/types/session-blank.d.ts +51 -0
  115. package/lib/types/session-list.d.ts +34 -0
  116. package/lib/types/startup.d.ts +30 -0
  117. package/lib/types/stats.d.ts +64 -1
  118. package/lib/types/subagent-model.d.ts +7 -6
  119. package/lib/types/term-text.d.ts +100 -24
  120. package/lib/types/terminal-input.d.ts +37 -0
  121. package/lib/types/theme.d.ts +109 -0
  122. package/lib/types/throughput.d.ts +163 -0
  123. package/lib/types/tool-present.d.ts +43 -2
  124. package/lib/types/transcript-types.d.ts +63 -2
  125. package/lib/types/tui.d.ts +960 -12
  126. package/package.json +73 -67
@@ -0,0 +1,122 @@
1
+ # 管道宿主模式(`DSH_TUI_DISPLAY=stdio`)
2
+
3
+ > 这个插件是**终端 UI**:它要在一个真实 TTY 里画。桌面版 Harness 的启动器是 Electron-as-Node,
4
+ > 没有 console,所以**它不需要这个插件**,插件在那边保持惰性(`docs/desktop.md`)。
5
+ > 这份文档讲的是**另一条路**:宿主自己会开终端控件(PTY 面板、xterm.js、GUI 里嵌的终端),
6
+ > 它可以把插件当成"一个在管道上说话的子进程"来用。
7
+
8
+ ## 一句话
9
+
10
+ 父进程声明 `DSH_TUI_DISPLAY=stdio`,插件就不再要求 TTY:它把 stdin 当**输入通道**、stdout 当
11
+ **画面通道**、并接受父进程用 `CSI 8 ; rows ; cols t` 报告的面板尺寸。父进程只要会"喂字节 + 解 ANSI",
12
+ 就能承载完整 TUI——包括会话续接、断线重连、`/exit`。
13
+
14
+ ## 两个入口(机制 + 语法糖)
15
+
16
+ | 入口 | 用途 |
17
+ |---|---|
18
+ | `DSH_TUI_DISPLAY=stdio`(环境变量) | **机制**。父进程设置它,等于声明"stdin/stdout 是显示通道",插件的无终端保护会照此放行 |
19
+ | `--display stdio`(命令行) | **语法糖**。写在 profile 行里时 `--help` 会显示它,拼错是报错而不是静默惰性 |
20
+
21
+ 两者同时存在时**环境变量优先**(父进程描述的是它马上要用的通道;标志可能来自包装脚本的残留)。
22
+ 不认识的值会打印一行说明,然后按没有终端处理——不会静默变惰性。
23
+
24
+ ## 父进程要做的五件事
25
+
26
+ 1. **管道起进程**(不要 PTY)。`dsh --profile tui` 用 `spawn(..., { stdio: ['pipe','pipe','pipe'] })`;
27
+ 顺带一句话:能开 PTY 的宿主**不需要这个模式**——直接给真 TTY,走默认路径即可。
28
+ 2. **声明初始尺寸**:管道不报告尺寸,插件读 `COLUMNS` / `LINES`(没给就是 80×24)。
29
+ 建议同时给 `TERM=xterm-256color`、`COLORTERM=truecolor`:能力表(alt-screen、颜色、鼠标、
30
+ OSC 8)都从环境推出来,`TERM` 缺失会退化成"哑终端"。
31
+ 3. **把按键写成字节**到子进程 stdin(UTF-8)。字节流就是终端输入:普通字符、`\r`、`\x03`(Ctrl+C)、
32
+ 方向键的 `ESC [ A` 之类都原样可用。
33
+ 4. **把 stdout 解成画面**:输出就是 ANSI(绝对定位 + 只画脏行 + `DEC 2026` 同步刷新)。
34
+ 喂给 xterm.js 之类即可;**不要**自己做行缓冲或重排——它们假定了固定宽度和行地址。
35
+ 5. **报告尺寸变化**:管道没有 `resize` 事件,也没有 `SIGWINCH`。面板变大变小时,往 stdin 写
36
+
37
+ ```
38
+ CSI 8 ; <rows> ; <cols> t 例:\x1b[8;40;120t
39
+ ```
40
+
41
+ 这就是终端回答"窗口多少格"的序列,插件把它当尺寸帧转给绘制的 Host,并以新宽度重画。
42
+ 它**不会**被当成输入(半个到达的序列会被暂存到下次读取,不会把 `ESC` 泄进提示词)。
43
+ 一次写完整条序列:拆成两次写时,前半段会被暂存,但"暂存"只保证不泄成按键。
44
+
45
+ ## 光标探测(可选,但建议)
46
+
47
+ 插件会通过 `CSI 6n` 问光标位置,用来(1)测链路往返、(2)判断 `①`/`—` 这类**歧义宽度**字形在这个
48
+ 字体里占几格。父进程如果像终端模拟器那样回答:
49
+
50
+ ```
51
+ CSI <row> ; <col> R 例:\x1b[1;1R
52
+ ```
53
+
54
+ 那么接入是快的,并且宽度能按真实字体判定。**不回答也能用**:测量会在采样预算(约 0.7s)后放弃,
55
+ 歧义宽度退回按 locale 推断,字形探针直接跳过。需要彻底关掉字形探针(`DSH_TUI_NO_GLYPH_PROBE=1`)
56
+ 的场合是"断言 attach 字节流"的测试。
57
+
58
+ ## 退出与清理
59
+
60
+ - 用户在 TUI 里 `/exit`,或按 Ctrl+C,子进程自己收尾:恢复管道上的终端模式(管道上没有可恢复的
61
+ 东西,所以是空操作)、写 goodbye 帧、退出码 0。
62
+ - 插件会**短暂保持 raw 模式一个往返**再放开终端,用来吞掉"还在路上的光标回答"——否则慢链路上
63
+ 这个回答会被 tty 回显成 `^[[25;1R` 文本留在提示符旁。宿主不需要配合,只要别在这几百毫秒里
64
+ 强杀进程。
65
+ - 会话本身在 Host 进程里,窗口/管道断开不等于会话结束:重新按同一个 session id 接入即可续上。
66
+
67
+ ## 最小示例(Node)
68
+
69
+ ```js
70
+ import { spawn } from 'node:child_process'
71
+
72
+ const child = spawn(process.execPath, [cliPath, '--profile', 'tui', `--resume=${sessionId}`], {
73
+ env: {
74
+ ...process.env,
75
+ DSH_HOME: home,
76
+ DSH_TUI_DISPLAY: 'stdio', // 父进程声明:我在管道上说话
77
+ TERM: 'xterm-256color',
78
+ COLORTERM: 'truecolor',
79
+ COLUMNS: '100', // 管道不报尺寸,父进程声明
80
+ LINES: '30',
81
+ },
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ })
84
+
85
+ // 画面 → 终端控件
86
+ child.stdout.on('data', bytes => terminal.write(bytes))
87
+ // 按键 → 子进程
88
+ terminal.onData(text => child.stdin.write(text))
89
+ // 探测 → 像终端模拟器那样回答(不回答也能跑,只是慢)
90
+ let seen = ''
91
+ child.stdout.on('data', chunk => {
92
+ seen += chunk
93
+ let at = seen.indexOf('\x1b[6n')
94
+ while (at !== -1) {
95
+ child.stdin.write('\x1b[1;1R')
96
+ seen = seen.slice(at + 4)
97
+ at = seen.indexOf('\x1b[6n')
98
+ }
99
+ })
100
+ // 面板尺寸变化 → 报告给子进程
101
+ resizeObserver(size => child.stdin.write(`\x1b[8;${size.rows};${size.cols}t`))
102
+ ```
103
+
104
+ ## 可执行的规格
105
+
106
+ | 位置 | 覆盖内容 |
107
+ |---|---|
108
+ | `tests/stdio-pipe-e2e.test.mjs` | 真 relay + 真 Host,**两端都是管道**:attach 不报 `setRawMode`、初始尺寸、按键、`CSI 8 t` 重画、goodbye 退出 |
109
+ | `tests/display-mode.test.mjs` | 模式解析(env/标志/拼错)、尺寸报告的解析与跨读暂存 |
110
+ | `scripts/tui-stdio-probe.mjs` | 端到端:起真 profile、走管道,断言画面、输入回显、`CSI 8 t` 后按新宽度重画、`/exit` 干净退出。CI 的 Linux 与 Windows 两条腿都跑 |
111
+ | `tests/no-terminal.test.mjs` | 没有终端时插件惰性(桌面版那条路),以及 `requireTerminal: true` 是唯一会抛错的路径 |
112
+
113
+ ## 限制(写清楚,免得宿主踩)
114
+
115
+ - **没有真实 `resize`**:只能靠 `CSI 8 ; rows ; cols t`。不发的宿主会一直停在 `COLUMNS`/`LINES` 声明的尺寸。
116
+ - **没有 `SIGWINCH`**:同上。
117
+ - **能力表来自环境**:`TERM`/`COLORTERM` 没给,就没有 alt-screen、没有颜色、没有鼠标——画面能跑,
118
+ 但不是用户期待的样子。用 `DSH_TUI_TERM_CAPS` 可以显式覆盖(见 `docs/terminals.md`)。
119
+ - **只读诊断**:`DSH_TUI_LINE_MODE=1` 是给日志型面板的另一条路——每个事件一行纯文本,不画屏。
120
+ - **桌面版进程本身**:如果宿主是那个 Electron-as-Node 启动器,**它开不出这条通道**(没有 console,
121
+ 但真正的问题在它自己的子进程模型)。这种情况请用 npm 安装的 CLI 在真终端/SSH 里跑
122
+ `dsh --profile tui`,见 `docs/desktop.md`。
@@ -0,0 +1,91 @@
1
+ # 0.7.4 发版说明(草稿)
2
+
3
+ > **状态:未发版。** 版本号、tag、GitHub Release、npm 都还没动(`docs/release.md` 第 1 条)。
4
+ > 本文件是发版时要粘进 GitHub Release 的正文草稿 + 当次核对清单,写完就不必临场回想。
5
+ > 上一次发版:**0.7.3**(2026-09-24,commit `e505768` / tag `v0.7.3` / npm `latest`+`next`)。
6
+
7
+ ## 头等事
8
+
9
+ 这一版是「把已经做出来的东西做对」:0.7.3 之后落的六个功能各自被真机与对抗式审查拧了一遍,
10
+ 修掉了 8 个实际缺陷(含两个只有读代码才看得见的:现场思考卡被挤出焦点环、覆盖层里的复制
11
+ 拿到的是另一份正文)。行为上最明显的变化是**回复变成了可选中的卡片**。
12
+
13
+ ## 回复可以选中、可以复制(本版主线)
14
+
15
+ `/copy` 取的是"被选中的那一行",而可选列表里**从来没有回复**——所以只要按过一次 ↑/↓ 选中
16
+ 任何卡片,`/copy` 就再也拿不到回复,最新回复只是"没有选中时"的兜底。现在:
17
+
18
+ - 空输入 `↑`/`↓` 在**回复与卡片**之间按屏幕顺序走,选中的那一行带 `▶` 标记;`Alt+4` 直接选中最新回复。
19
+ - `/copy`(或 `Ctrl+Shift+C`)复制的是**选中的那一条的原文**——不是折行后的屏幕文本;
20
+ 复制后保留选中,连按两次拿到的是同一条。
21
+ - 在选中的回复上按 `Enter`:打开「回复全文」(可滚动、`Esc` 返回),长回复里的表格与代码块
22
+ 不再被窗口宽度切碎。**覆盖层里也能复制**,取的是屏幕上那份正文,并在覆盖层底部回显「已复制 …」。
23
+ - 工具卡 / 子代理 / 改动卡的「全文」覆盖层里,复制键取的同样是**屏幕上那份正文**(工具全文、那份 diff、
24
+ 子代理日志),不再是卡片摘要。
25
+ - **拖选**照旧:被选中回复第一行前面的 `▶` 是装饰,拖过它不会把标记复制进去。
26
+
27
+ ## 底栏与链路
28
+
29
+ - 链路质量**在会话运行中持续重测**并按中位数读数:进会话那一瞬间的网络抖动不再把整场会话钉在最低档
30
+ (绘制节奏与每帧字节预算都跟着这档走)。
31
+ - 选择器把「可接入 / 已占用」两个词固定下来,并且**在首帧之前**就把"已经挂掉的窗口"判完——列表不会先
32
+ 显示「已占用」再改口。判定走一次真实往返(光标探针),因为断掉的 SSH 链路会让 launcher 还连着显示通道。
33
+ - 接管(把别人的窗口接过来)现在**要确认**,`Esc` 是取消而不是退出。
34
+
35
+ ## 空回合不再被误读为完成
36
+
37
+ 上游网关偶尔只回思考、`finish_reason: stop`、正文为零(实测某网关 10/35 个回合如此),
38
+ 宿主如实组装成"只有思考的助手消息",状态栏显示完成/空闲。现在转录里会补一行明确提示
39
+ (含 `finish_reason: stop` 与"按 Enter 继续"),回放/中断/报错的回合不提示。
40
+
41
+ ## 上游兼容
42
+
43
+ - `dsh` 声明窗口:`>=0.1.7-rc.1 <0.1.8`(`next`)与本条线并行;`0.1.7-rc.2` 已按完整流程验过并声明
44
+ `compatible`(干净树安装、`tsc` 0 错、套件全绿、六个真 PTY 探针全过),CI 里单独一条腿。
45
+ - CI 现有 5 条腿:`0.1.7-rc.1`(默认,提交的 manifest)、`0.1.7-rc.2`、`0.1.5-rc.3`、`0.1.5-rc.1`
46
+ (只跑类型与单元)、`test-windows`(ConPTY 真机)。默认腿新增一条 **Reply copy probe**:真 PTY 跑一轮
47
+ 脚本化模型,断言拖选复制、`/find` 高亮、以及**选中回复后按复制键拿到原文**。
48
+
49
+ ## 发版核对清单(执行时逐条打勾)
50
+
51
+ ```sh
52
+ # 0. 版本号(改这一处,然后重新生成锁)
53
+ # package.json "version": 0.7.4
54
+ npm install --package-lock-only --cache <可写目录> # 只重算锁,不动 node_modules
55
+
56
+ # 1. 门禁(本地)
57
+ npx tsc --noEmit -p tsconfig.json
58
+ node --test "tests/*.test.mjs" # 期望:1009 pass / 0 fail / 4 skip
59
+ node scripts/tui-mock-probe.mjs # 脚本化一轮:拖选 + 选中回复复制 + /find
60
+ node scripts/tui-mock-probe.mjs --busy # 窗口中途关闭
61
+ node scripts/probe-home.mjs --probe # boot/resize/diag/doctor/copy/preset/exit
62
+ node scripts/probe-home.mjs --probe --script tui-term-probe.mjs
63
+ node scripts/probe-home.mjs --probe --script tui-cut-probe.mjs
64
+ node scripts/probe-home.mjs --probe --script tui-rtt-probe.mjs
65
+ node scripts/probe-home.mjs --probe --script tui-drop-probe.mjs
66
+ npm pack --dry-run # 189 个文件、无 tgz 落盘
67
+
68
+ # 2. GitHub(先)
69
+ git commit -am "Release 0.7.4"
70
+ git push origin main
71
+ git tag -a v0.7.4 -m "0.7.4" && git push origin v0.7.4
72
+ # → 建 GitHub Release(正文用本文件"头等事"起的那几段 + 当日热度基线)
73
+ # → 等 CI 五条腿全 success(tag 与 main 都要看)
74
+
75
+ # 3. npm(CI 全绿之后;token 需先刷新)
76
+ npm whoami --registry https://registry.npmjs.org/ # 必须是登录态,否则不要往下走
77
+ npm publish --registry https://registry.npmjs.org/ --tag latest
78
+ npm dist-tag add dsh-ssh-tui@0.7.4 next # 0.7.3 时 latest 与 next 同值
79
+ ```
80
+
81
+ **注意**:本机 `~/.npmrc` 的默认 registry 是 npmmirror,发布必须显式
82
+ `--registry=https://registry.npmjs.org/`;`_authToken` 目前是过期状态(`npm whoami` → `E401` /
83
+ `ENEEDAUTH`),刷新之前不要执行第 3 步。
84
+
85
+ ## 已知限制(写进 Release)
86
+
87
+ - 真实 Windows 上仍只能人工确认的两条:宿主控制台窗口确实隐藏;`%USERPROFILE%` 与 `$HOME` 不一致的账户行为。
88
+ - 纯问答会话(尚无任何卡片)或刚 `/clear` 时,空输入 `↑` 仍是**历史召回**而不是选中回复——这是刻意保留的;
89
+ 那种会话用 `Alt+4` / `Ctrl+N` / `Ctrl+P` 选回复。选中后 `Enter` 正常打开全文。
90
+ - 把 `Ctrl+Shift+C` 折叠成 `Ctrl+C` 的老终端上,覆盖层里那个键等于关闭;请用 `ssh-tui.keys` 改绑 `copy`
91
+ (改绑后的键在覆盖层内同样生效)。
@@ -0,0 +1,56 @@
1
+ # 0.8.0 发版说明
2
+
3
+ > **发布方式:只上 `next`,`latest` 保持 0.7.4。** 计划 2026-10-01 再把 `0.8.0` 提升为 `latest`。
4
+ > 上一次发版:**0.7.4**(2026-09-29,npm `latest` + `next`)。
5
+
6
+ ## 头等事:跟上宿主的 0.2.0-rc 线,同时摘掉 0.1.5
7
+
8
+ 上游把 `next` 指向 **0.2.0-rc.1**。这条线保留了 0.1.7 引入的两处结构(设置表单由 loader entry 自己的
9
+ `Config` 投影、终端 profile 在进程级组合代理),所以**插件源代码一行没改**就跑了:类型面 0 错、七条真机探针
10
+ 全过。真正需要改的是三处"宿主代际判定"——它们原来用 `0.1.7` 前缀判断,把 `0.2.x` 读成了旧代,会让
11
+ profile 挂错行。现在统一读一个按版本元组比较的共用规则。
12
+
13
+ **0.1.5 这条线不再支持。** 还在 0.1.5 的用户会从商店读到明确的 `incompatible`(而不是沉默),
14
+ 请先升级宿主到 `0.1.7-rc` 或 `0.2.0-rc` 再装这一版。
15
+
16
+ ## 401/403 不再混为一谈,并可自动重试一次
17
+
18
+ 宿主的 `llm/retry` **不重试 AUTH 类错误**,所以一次**提供商侧**的瞬时 403 会直接终结整轮,而屏幕上只有
19
+ `permission_error` 一行。实测过一次:同一会话 37 轮在同一条路由上成功,失败只有间隔 45 秒的两次,
20
+ 随后同一把 key 打同一模型 200。
21
+
22
+ - 失败时补一行来源判断:`提供商拒绝了请求(HTTP 403)…{ENV} 已配置`(意思是**别改 key,重发即可**)
23
+ 或 `本机没有可用的 {ENV}`(意思是先去配)。判据是真的解析一次凭据,不是猜。
24
+ - `/retryauth on|off`(**默认关**):只在"提供商拒绝 + 本机有凭据 + 这一轮还没重试过"时自动重发上一条
25
+ 消息**一次**。默认关是因为重试会把整段上下文再发一遍,长会话很贵。
26
+ - 排查三步(三种错误信封、两条文件检查、一条直连 `curl`)写进 `docs/remote-ops.md` §4.8;
27
+ 附带一个会误导人的坑:`GET /provider/v1/models` 是公开端点,带不带 key 都 200,不能用它验鉴权。
28
+
29
+ ## 上游兼容
30
+
31
+ | 通道 | 版本 | 表态 |
32
+ |---|---|---|
33
+ | `latest` | `0.1.7-rc.2` | `compatible`:完整 CI 腿 |
34
+ | `next` | `0.2.0-rc.1` | `compatible`:本版默认线(干净树安装、`tsc` 0 错、套件 1021 通过 / 0 失败、七条真机探针) |
35
+ | 0.1.7 线 | `0.1.7-rc.1` | `compatible`:typecheck + 套件 |
36
+ | 已摘 | `0.1.3-alpha.*` / `0.1.5-*` | **`incompatible`**(明确表态,范围不再收;请求升级到 `0.1.7-rc` 或 `0.2.0-rc`) |
37
+
38
+ CI 四条腿:`0.2.0-rc.1`(默认,6 条探针)、`0.1.7-rc.2`(完整)、`0.1.7-rc.1`(类型+套件)、Windows(ConPTY 真机)。
39
+ 验证细节见 [`docs/upstream-0.2.0.md`](upstream-0.2.0.md)。
40
+
41
+ ## 安装 / 升级
42
+
43
+ ```sh
44
+ dsh plugin --profile tui add dsh-ssh-tui@next # 0.8.0(宿主 0.2.0-rc 或 0.1.7-rc 都可以)
45
+ dsh plugin --profile tui add dsh-ssh-tui@latest # 0.7.4(仍是默认;10-01 之后 latest 会变成 0.8.0)
46
+ ```
47
+
48
+ 装完退出再启动。`/status` 可确认当前版本;装不上先看 `/doctor`。
49
+
50
+ ## 已知限制
51
+
52
+ - **0.1.5 及更早不再支持**(见上)。
53
+ - 真实 Windows 上仍只能人工确认的两条:宿主那个控制台窗口确实是隐藏的;`%USERPROFILE%` 与 `$HOME`
54
+ 不一致的账户行为。
55
+ - `/retryauth` 的重试会把整段上下文再发一遍(长会话成本由你决定是否开启);它只对"提供商侧鉴权失败"
56
+ 生效,本机缺凭据时不会重试。
@@ -0,0 +1,79 @@
1
+ # 0.8.1 发版说明
2
+
3
+ > **发布方式:`latest` + `next`。** 上一次发版:**0.8.0**(2026-09-29,`next`,随后提前提升为 `latest`)。
4
+ > 这是一版**修稳定性**的补丁:桌面版装上它不会再把宿主带崩;终端侧的几处"残留"也一并清掉。
5
+
6
+ ## 头等事:桌面版装了**不会再弄坏宿主**
7
+
8
+ 0.8.0 在没有终端的进程里是**抛错**退出插件的。桌面版 Harness 的启动器是 Electron-as-Node(GUI 子系统,
9
+ 没有 console),于是插件管理把它显示成"1 entry did not activate / 插件失败",在桌面版上还会把 GUI 带崩。
10
+
11
+ 0.8.1 起按"能不能画"分四种情形,只有真能画时才挂载:
12
+
13
+ | 情形 | 行为 |
14
+ |---|---|
15
+ | 本进程就是分离的 TUI Host | 照常服务(它的终端在随后接入的那个窗口里) |
16
+ | stdin 与 stdout 都是 TTY | 照常挂载 TUI |
17
+ | 桌面版启动器(Electron-as-Node / `app.asar` / `DeepSeek Harness.exe`) | **不挂载任何东西**(不取锁、不装定时器、不查更新),记**一行**说明,措辞是"此处不启用"而不是报错 |
18
+ | 其它没有终端的情形(管道 `… \| tee`、cron、服务) | 同上,文案换成"没有终端"。**退出码 0** |
19
+
20
+ 需要"没终端就报错"的场合(脚本断言某个终端 profile 真能起来)在 profile 行加 `requireTerminal: true`
21
+ ——那是唯一会抛错的路径,而且只在显式要求时。桌面版用户不需要这个插件:用应用本身或 `dsh web`;
22
+ 想在终端里用它,用 npm 安装的 CLI(`npm i -g @deepseek-ai/dsh`)在真实终端/SSH 里跑 `dsh --profile tui`。
23
+
24
+ ## 新增:宿主自带终端控件时,可以把这个 TUI 当子进程用
25
+
26
+ 桌面版自己**不需要**它,但"宿主会开终端控件"(PTY 面板、xterm.js、GUI 里嵌的终端)是另一条路:
27
+ 声明 `DSH_TUI_DISPLAY=stdio`(或 `--display stdio`),插件就不再要求 TTY——画面走 stdout、按键走 stdin、
28
+ 面板尺寸由宿主用 `CSI 8 ; rows ; cols t` 报告(管道没有 `resize` 事件也没有 `SIGWINCH`)。协议、父进程
29
+ 必须做的五件事、Node 最小示例与限制写进 [`display-mode.md`](display-mode.md)(这一版起随包发布)。
30
+
31
+ ## 终端侧修掉的三处"残留"
32
+
33
+ 1. **会卡住的"处理中"卡片**:渲染缓存把"活尾巴"(等待卡、流式缓冲、还没有回复的突发摘要)收进了
34
+ 最后一行的缓存条目,于是每一帧都把那一份**冻结在旧秒数**的副本重放出来。真实会话里攒到过四条
35
+ (0s / 12s / 19s / 30s)夹在工具卡中间,`0s` 那条永远不动。现在缓存条目只包含它自己那行画的内容。
36
+ 2. **退出后提示符旁的 `①—…“”·•Ⅰ` 与 `^[[25;1R`**:歧义字形探针打印的那行自己没有擦(调用方可能一帧
37
+ 都没画),而它的光标回答要一个往返才回来——慢链路上那时终端已经交还给 shell,tty 就把回答回显成了
38
+ 文本。现在探针自己擦行、归还终端前多留一个往返吞掉回答、并且不再对已经交还的终端探测;探针期间
39
+ 暂存 Host 的输出,避免擦除打在刚画好的帧上。
40
+ 3. **极简视图**:单文件编辑卡不再重复自己的标题与 diffstat;失败的工具行补回状态球(`●`),与其它卡片行一致。
41
+
42
+ ## 性能与渲染(0.8.0 之后陆续落地)
43
+
44
+ - 每行渲染缓存 + 逐行脏比较:5000 行会话的整帧重画从约 1050ms 降到约 22ms,按键帧从约 1040ms 降到约 13ms;
45
+ 多行帧用 `DEC 2026` 同步刷新,避免慢链路上一行行"落下来"的撕裂(这就是本插件存在的意义的那个不闪屏)。
46
+ - 歧义宽度字形(`①`–`⑩`、`—`、`“”`、CJK 标点…)改为**问终端**:接入时打印探针并用 `CSI 6n` 读回列号,
47
+ 答案随帧下发给绘制的 Host;终端不回答就退回按 locale 推断,`DSH_TUI_NO_GLYPH_PROBE=1` 可整体关掉探针。
48
+
49
+ ## 上游兼容
50
+
51
+ | 通道 | 版本 | 表态 |
52
+ |---|---|---|
53
+ | `latest` / `next` | `0.2.0-rc.2` | `compatible`:**本版默认线**(干净树安装、`tsc` 0 错、套件 1086 项 1082 通过 / 0 失败 / 4 跳过、真机探针全过;桌面版基线) |
54
+ | 线内 | `0.2.0-rc.1` | `compatible`:typecheck + 套件 |
55
+ | 线内 | `0.1.7-rc.2` | `compatible`:完整探针 |
56
+ | 线内 | `0.1.7-rc.1` | `compatible`:typecheck + 套件 |
57
+ | 已摘 | `0.1.5-*` 及更早 | **`incompatible`**(明确表态,请求升级到 `0.1.7-rc` 或 `0.2.0-rc`) |
58
+
59
+ 声明窗口:`>=0.1.7-rc.1 <0.1.8 || >=0.2.0-rc.1 <0.2.1`。CI 四条腿:`0.2.0-rc.2`(默认,完整探针)、
60
+ `0.1.7-rc.2`(完整)、`0.2.0-rc.1` 与 `0.1.7-rc.1`(typecheck + 套件)、外加 Windows leg(ConPTY 真机)
61
+ ——两条腿新增**无 PTY** 的管道探针(`scripts/tui-stdio-probe.mjs`)。
62
+
63
+ ## 安装 / 升级
64
+
65
+ ```sh
66
+ dsh plugin --profile tui add dsh-ssh-tui@latest # 0.8.1
67
+ ```
68
+
69
+ 装完退出再启动。`/status` 可确认当前版本;装不上先看 `/doctor`。
70
+
71
+ ## 已知限制
72
+
73
+ - **0.1.5 及更早不再支持**(见上)。
74
+ - 桌面版**应用本身**仍然不能跑终端 profile(它的启动器没有 console)——这是上游的事,见
75
+ [`docs/desktop.md`](https://github.com/cyjyyd/dsh-ssh-tui/blob/main/docs/desktop.md)。本版保证的是
76
+ "装了它不会把宿主弄坏",以及"宿主愿意开终端控件时能用"。
77
+ - 管道宿主**必须**自己报告尺寸变化(`CSI 8 ; rows ; cols t`),否则会停在启动时声明的 `COLUMNS`/`LINES`。
78
+ - 真实 Windows 上仍只能人工确认的两条:宿主那个控制台窗口确实是隐藏的;`%USERPROFILE%` 与 `$HOME`
79
+ 不一致的账户行为。
@@ -0,0 +1,159 @@
1
+ # 0.8.2-rc.1 发版说明(候选版)
2
+
3
+ > **发布方式:只上 `next`。** 这是候选版(RC):`latest` 保持 **0.8.1**,只有显式装 `@next` 或指定
4
+ > 版本号的人才会拿到它。上一次发版:**0.8.1**(2026-09-30,`latest` + `next`)。
5
+ > 这一版是**架构收尾 + 稳定性**:把 0.8.x 期间分批落地的终端架构(screen / 表示管线 / 工作区区域)
6
+ > 冻结下来,并修掉两处**只有全新安装或弱链路才会遇到**的问题。
7
+
8
+ ## 头等事:全新安装**第一次就会出现配置向导**(此前根本不会出现)
9
+
10
+ 0.8.1 及以前,一台**没有任何配置**的机器上装好插件、启动 `dsh --profile tui`,看到的是:
11
+ 一个空工作区、一行都没有的提示,模型调用必然失败 —— **配置向导永远不会自动出现**。
12
+
13
+ 原因不在向导本身(它是好的),而在触发它的判断:前端拉起后台 Host 时**永远**带上
14
+ `--resume=<id>`(连它自己刚生成的那个新 id 也带),而首启判定把 `--resume` 当成"这台机器配置过",
15
+ 于是直接跳过了首启流程。
16
+
17
+ 0.8.2 起这条判断只看**凭据存不存在**:没有凭据、环境里也没有 key,就是首启,向导自动打开(Enter 走步骤、
18
+ Esc 随时退出),并且工作区会同时留下一行说明告诉你怎么手动重开 `/setup`。**第二次启动不会再出现**。
19
+
20
+ 这同时修掉一个连带问题:向导不再借用 composer 的输入框。0.8.1 里进入向导会**清掉你已经打了一半的草稿**;
21
+ 现在向导有自己的字段、自己的光标,工作区里的草稿、历史、正在进行的输入一个字都不动。
22
+
23
+ ## 工具卡与输入法:两处"画面串行"的修复
24
+
25
+ - **工具卡内容含多行代码时不再窜到输入区**。`bash` 卡片的标题就是命令行原文,遇到 heredoc(`<<'PY'`)会带
26
+ 着换行进入"一行"——终端因此真的换了行,而这一版之前按绝对行号绘制的帧随后把下面每一行都写错位:卡片内容
27
+ 落在输入框和页脚上,甚至两段文字拼在同一行。现在标题行会把换行折成空格,单行裁剪器也不再把换行交给终端。
28
+ - **Windows Terminal + 微软输入法:预输入不再在处理中卡片那一行闪烁**。终端把输入法的预输入画在光标处,
29
+ 而"写一行"会把光标留在该行末尾;跑一轮时实时区域每 tick 重画又正好在输入框上方,所以光标停在处理中卡片
30
+ 上。现在每写完一行就把光标交还输入框,预输入只会出现在它该在的地方。
31
+
32
+ ## resume 不再黑屏,思考卡片不再"发白",`q` 真的能关掉报告
33
+
34
+ - **resume 进入时的 1–3 秒黑屏**。前端先在备用屏打出"正在载入历史会话…",relay 接入时宿主又写了一次
35
+ `?1049h`——终端已经在备用屏时这个序列会**清屏**,而重建会话日志期间按设计不合成任何帧,于是那块加载
36
+ 提示被擦掉、屏幕空着,直到历史重建完成(实测 20k 事件:9.1 s 清屏、12.6 s 首帧落地,中间 3.5 s 全黑)。
37
+ 现在这条"进屏"序列跟着第一帧一起发出,同一 tick 内清屏并重画;没有东西要等的重连行为不变。
38
+ - **思考卡片的文字不再发白**。"思考 / 已思考"这一角色此前只写 `2;3` 两个**属性**、不写颜色——"暗"完全
39
+ 靠 faint,而不实现 faint 的终端(报告的 256 色终端)于是退回**默认前景色**,看起来和旁边的回复一样亮。
40
+ 四个调色板现在各自给出 muted 颜色,属性保留给支持的终端(`mono` 仍然只用属性)。
41
+ - **`q` / `Q` 能关掉报告**。`inspectClosesOn` 一直声明接受它们("全屏报告理应响应 q"),但可打印字符的
42
+ 提示分支在它之前返回,于是这两个键只会打印"这是报告视图,按 Esc 返回"。
43
+
44
+ ## 状态行不再把"绘制节奏"当成"链路延迟"
45
+
46
+ 现场报告(`/diag`):Linux + xterm-256color + SSH,`探测 未知(终端未回 DSR)`——这台终端**多数时候不回答
47
+ `CSI 6n`**,于是状态行常年显示 `SSH ○○○○ 160ms`。两个部分要分开看:
48
+
49
+ - **空心圆是对的**:没测到往返就是 `unknown`,不该假装知道链路好坏。
50
+ - **`160ms` 是错的**:那不是延迟,是**回退的绘制节奏**(探测不到时用的安全默认值)。它出现在"延迟"该在的
51
+ 位置上,读者无法分辨"离我 160ms"和"从来没测过"。这一格现在改为 `SSH ○○○○ 未测` / `SSH ○○○○ n/a`;
52
+ 测到过的链路照旧显示真实值,绘制节奏仍然出现在 `/diag` 那一行、并且写明是"绘制"。
53
+
54
+ 另外两个"看起来像 bug"的现象是设计使然,不是缺陷:
55
+
56
+ - **有回 DSR 时数值也不怎么动**:显示的是最近 3 次测量的**中位数**,重测间隔 20 秒;稳定的 SSH 链路上
57
+ 中位数本来就该是一条直线。Windows 本机之所以看得见跳动,是因为那些数字只有 1–12ms,同样抖动在两位数
58
+ 里显得大。
59
+ - **连续 3 次探测失败后不再每 20 秒重试**:退避到 10 分钟——避免"不回答 DSR 的终端被永远每 20 秒扫一次"。
60
+
61
+ ## 命令提示会自己让位(排队提示除外)
62
+
63
+ 命令的确认("已复制 412 字")与警告(命令失败)从此**显示约 6 秒后自动让位**,恢复原来的遥测行;按 Enter
64
+ 提交仍会立刻清掉。**唯一的例外**是"运行中已提交,将在下个步骤生效"这条:它显示到那条消息**真正被提交**
65
+ 为止——它是"你有话在排队"的唯一标记,提前消失等于把排队这件事藏起来。
66
+
67
+ ## 弱链路 / SSH:画面更稳,流式不再整屏重画
68
+
69
+ 这一版把 0.8.x 期间逐块落地的渲染架构冻结下来,对**远程、跳板机、弱网**最直接的三条:
70
+
71
+ - **流式输出不再每个 tick 整屏重画**:正在生成的回复是一个**投影**,不再是往历史里追加行,所以每个
72
+ token 批次只重画它自己那几行。实测(100×20):一次流式 delta 寻址 **3 行 / 478 字节 / 0 次全屏清屏**。
73
+ - **报告不再污染对话记录**:`/status`、`/help`、`/doctor`、`/subagents`、`/diag` 现在是**独立的屏**,
74
+ 打开不改变历史窗口位置,翻页只重画正文,Esc 回到你原来的位置,也不会在重连后被重新画出来。
75
+ - **断线重接后表示一致**:提问、审批、计划三种卡片从**会话日志**折叠出来,因此掉线重接、换窗口、
76
+ `--resume` 之后读到的状态与当时一致,而不是"重连后多出一张卡"或"状态回到未知"。
77
+
78
+ ## `/clear` 只清可见视图
79
+
80
+ `/clear` 不删除任何东西:不删会话日志、不动模型上下文、不动计划 artifact。它只是把**可见视图**切到
81
+ 当前时刻之后(并且用一行提示告诉你这件事)。`/resume` 进来仍然是完整历史。`/find` 只在你**看得见**的
82
+ 范围里搜索,这一点也在提示里说明。
83
+
84
+ ## 计划与实时处理的抢占关系
85
+
86
+ 计划条(Plan Dock)不再被"处理中"的实时区域盖住:实时区域现在是**自己的行**,而不是画在历史上面。
87
+ 计划条高度上限为工作区一半,所以它不会把输入框和光标挤出屏幕(中文输入法的预输入串也不会再落到
88
+ 计划卡上)。终端很矮时计划条按三档降级(完整 / 紧凑 / 极简),不会直接消失。
89
+
90
+ ## 反馈去哪了("我执行了命令但没有任何提示")
91
+
92
+ - 控制面的确认(切主题、切视图、preset 写入…)进**页脚**,显示到下一次输入为止;
93
+ - 警告与失败进**身份行**(原来 `目录:` 那一行),不盖住你正在读的内容;
94
+ - 只有**会改变历史**的事实才进对话记录。
95
+ 页脚挤不下时确认会退到身份行显示,所以再也不会有"执行了但什么都没有"。
96
+
97
+ ## 上游 400:现在会告诉你它是什么
98
+
99
+ `command-code` 这类思考模式网关偶尔会**成串**拒收请求(实测:一次长会话里 13 个失败轮次中 12 个是这种),
100
+ 以前你只看到一行原始错误。现在:
101
+
102
+ - 明确说出**网关拒收的是请求本身、不是你的 key**;
103
+ - 指名已知的那一族缺陷(上游 #1780 / #231,思考模式的 reasoning 回传);
104
+ - 给出可操作的两条绕法(该路线 `/effort off`,或换同一网关的 command-code-messages);
105
+ - **不会自动重试** —— 实测这类失败会成串出现(连续 8 轮),重发通常值得,但同一个请求确定性失败时
106
+ 自动重试会白花一整段上下文。什么时候重发由你决定。
107
+
108
+ ## 数字(本版实测,100×20)
109
+
110
+ | 动作 | 毫秒(中位) | 字节 | 寻址行 | 全屏清屏 |
111
+ |---|---|---|---|---|
112
+ | 流式 delta | 1.43 | 501 | 3 | **0** |
113
+ | 等待卡时钟 | 1.25 | 465 | 3 | **0** |
114
+ | 报告屏翻页 | 1.19 | 2457 | 17 | **0** |
115
+ | 菜单光标移动 | 0.97 | 588 | 4 | **0** |
116
+ | 向导字段打字 | 0.22 | 140 | 1 | **0** |
117
+
118
+ 发版候选(`c2fe073`)自检,逐条可复现:
119
+
120
+ | 自检 | 结果 | 命令 |
121
+ |---|---|---|
122
+ | 冻结不变量 | 八条全绿 · 七条退役路径 **0 命中** | `npm run freeze` |
123
+ | 性能基线 | 上表五行 · 全部 **0 次整屏清屏** | `npm run bench` |
124
+ | 全套测试(Linux) | 1376 项 · **1372 通过 · 0 失败 · 4 跳过** | `npm test` |
125
+ | 全套测试(Windows 实机) | 1376 项 · **1365 通过 · 0 失败 · 11 跳过**(跳过均为平台互斥用例) | `npm test` |
126
+ | 打包 | **243 个文件** · tarball **1.4 MB** · 解包 **4.2 MB** · shasum `92b7f517…` · 无凭据 / 无 home / 无 sqlite | `npm pack --dry-run` |
127
+
128
+ 跳过只代表"这台机器上没跑",不代表通过:`verify-batch` 见到 `SKIP:` 就收尾 `INCOMPLETE`(见
129
+ [`release.md`](release.md))。
130
+
131
+ ## 兼容
132
+
133
+ 上游兼容窗口与 0.8.1 相同(`0.2.0-rc.1` / `0.2.0-rc.2` / `0.1.7-rc.1` / `0.1.7-rc.2`)。
134
+ 这一版**不改协议、不加命令、不动上游接口**。
135
+
136
+ ## 安装 / 升级
137
+
138
+ ```sh
139
+ dsh plugin --profile tui add dsh-ssh-tui@0.8.2-rc.1
140
+ ```
141
+
142
+ 装完退出再启动。`/status` 可确认当前版本;装不上先看 `/doctor`。
143
+
144
+ ## 已知限制
145
+
146
+ - **Windows / ConPTY 实机人工验收:已完成(2026-10-03,维护者)**。用一份单独安装的 CLI dsh 逐条走过
147
+ 12 项清单,逐项结论见 [`checkpoints.md`](checkpoints.md)「Windows 实机轮」。分三类:
148
+ **可自动化的部分全绿**(`verify-batch --home <dir>`:12 PASS · 2 SKIP · 0 FAIL);**两处 ConPTY 盲区**
149
+ 明确打印 `SKIP:` 并附理由(ConPTY 自己回 `CSI 6n`,"慢链路"与"终端静默但没断连"这两个形状它扮演不了);
150
+ **没有自动化、只能人眼的两目已由维护者确认通过**——微软拼音 IME 组合、多行中文粘贴(清单与走法见
151
+ [`release.md`](release.md)「发布前人工门槛」)。两处 `SKIP` 是**覆盖边界**,不是通过:它们
152
+ **在 Linux 腿上是真跑的**(`link` 第一段、`cut` 的"静默终端"那一半),在 ConPTY 上才无信号。
153
+ - **`未测` 那一格在 Windows 本机看不到**:ConPTY 代答光标位置,本机链路芯片因此永远是"已测量"。
154
+ 该形状由 `scripts/tui-unmeasured-probe.mjs` 用管道父进程覆盖(两条腿:不回话读 `未测`、每条必答读
155
+ 真值),已进 CI 两条腿。
156
+ - **拖放探针此前约 1/7 概率误报**(探针自身与自己的 `/status` 重试竞态,不是产品行为)。本版已修,
157
+ 并连跑 20 次验证。
158
+ - `--resume` 与首启的关系见上文:本版修的是"新机器不显示向导",不是"resume 不再触发向导"。
159
+ - 依赖旧版 inbox 通知(inbox P3)与新协议的命令不在本版范围内。
@@ -215,6 +215,55 @@ AUTH 类错误**,所以一次瞬时 403 会直接终结整轮。要让它在"
215
215
  重试**每次发送最多一次**(重试自己那一轮不会再触发),且只对"提供商拒绝 + 本机有凭据"这一类生效——
216
216
  本机缺凭据时重试毫无意义。
217
217
 
218
+ ## 4.9 两种 400:一种是我们能压的,一种是上游的 bug
219
+
220
+ 2026-09-29 在一个长会话里数出 **6 次 400**,全部落在同一条路由(`command-code` + `deepseek/deepseek-v4.1-flash`,
221
+ effort=max),但**分两族、两个不同的成因**:
222
+
223
+ | 族 | 正文 | 次数与时间(UTC) | 归属 |
224
+ |---|---|---|---|
225
+ | A | `json: unknown field "summary"`(HTTP 400 `InvalidParameter`) | 1 次(09-28 01:45,turn 26) | pi-ai 的 `openai-responses` 适配器在设了强度时会发 `reasoning.summary`,而这家的 Go 网关不认这个字段 |
226
+ | B | `The reasoning_text in the thinking mode must be passed back to the API.`(`invalid_request_error`) | **5 次**(09-29 07:24 turn 36;14:02 / 14:05 / 14:13 / 14:16 turn 45–48) | **上游 harness 的 bug**:多轮会话把上一轮的 reasoning 块丢了,而思考模式下网关要求回传 |
227
+
228
+ **A 族**:全局 CLI 里的 pi-ai 副本被本机打过一个补丁(一直在,`.bak` 也在),
229
+ `dist/api/openai-responses.js` 第 260 行:
230
+
231
+ ```js
232
+ - summary: options?.reasoningSummary || "auto", // 原文:没设也会发 "auto"
233
+ + ...(options?.reasoningSummary ? { summary: options.reasoningSummary } : {}), // 补丁:只在显式设置时才发
234
+ ```
235
+
236
+ 单轮探针也验证过:带 `reasoning:{effort:"max"}`(不含 `summary`)打 `/provider/v1/responses` 返回 200。
237
+ **升级 dsh 族会覆盖这个文件,补丁会丢**——补丁丢了的症状就是 A 族 400 重新出现,照上面这行重新打即可
238
+ (打之前先 `cp openai-responses.js openai-responses.js.bak`)。
239
+
240
+ **B 族不是我们的问题,而且已被上游记录**:
241
+
242
+ - <https://github.com/deepseek-ai/deepseek-harness/discussions/1780>(切到第三方模型后多轮对话报同一个错)
243
+ - <https://github.com/deepseek-ai/deepseek-harness/discussions/231>(多轮会话丢 reasoning 块 → 400,
244
+ openai-responses 自定义提供商 —— 与我们的路由形态一致)
245
+
246
+ 本机实验支持"是客户端丢块,不是网关挑剔":同一模型、同一 `effort=max`,第一轮拿到 `reasoning` 项(id `rs_…`),
247
+ 第二轮**带上/不带**它都成功(两轮短对话测不出差异);失败只出现在长会话的**续写**上,正是"历史重建时丢块"的特征。
248
+
249
+ 绕开办法:把这条路由的**思考关掉**(`/effort off`,没有思考就没有要回传的东西),或改用同一网关的
250
+ **`command-code-messages`**(anthropic-messages 风味,思考块带签名、harness 会正确回传),或者等上游修。
251
+
252
+ **插件会在失败时直接说这句话**(与 A 族同理):B 族 400 一到,TUI 会补一行
253
+ 「这是上游的已知缺陷…绕开办法:`/effort off` 或 `command-code-messages`」并附 #1780/#231——它**不会**触发
254
+ `/retryauth` 的自动重试,因为这个 400 是确定性的(重发必然再失败)。
255
+
256
+ **A 族要主动维护**:补丁在依赖里,升级族就会丢。升级后跑一次
257
+
258
+ ```bash
259
+ node scripts/patch-pi-ai-summary.mjs --check # 退出码 1 = 有文件需要修
260
+ node scripts/patch-pi-ai-summary.mjs # 修,自动留 <file>.bak(幂等)
261
+ ```
262
+
263
+ 它覆盖 `openai-responses.js` 与 `azure-openai-responses.js`,并会在"上游改了形状、既不匹配原文也不匹配补丁后"
264
+ 时**拒绝动手**并让你先读文件(而不是假装已经好了)。2026-09-29 首次运行修好了 `azure-*`(三处安装各一份)
265
+ 与 `/usr/lib/node_modules` 那份未打的副本。
266
+
218
267
  ## 5. 明确不做(以及为什么)
219
268
 
220
269
  - **内置 `--daemon` 常驻模式**:Host 不是服务。它是"某个会话的写者",靠 idle-exit 把写锁交还;把它变成常驻服务会让 Web UI 与其它窗口长期打不开同一会话。
@@ -225,6 +274,8 @@ AUTH 类错误**,所以一次瞬时 403 会直接终结整轮。要让它在"
225
274
 
226
275
  - `/diag`:这条会话的通道、锁、Host 身份、判定链("会接入后台 Host,不要另开第二个窗口")。
227
276
  - `/retryauth [on|off]`:要不要在"提供商侧鉴权失败"时自动重试一次(见 4.8);`/doctor` 看路由与凭据是否就位。
277
+ - 桌面版(官方 GUI 应用)与终端配置的关系、两个上游缺陷:见 [`desktop.md`](desktop.md)。
278
+ - **400 先分族**:`unknown field "summary"` 是本地 pi-ai 补丁丢了(4.9 A 族),`reasoning_text must be passed back` 是上游 harness 的 bug(4.9 B 族)。
228
279
  - `/doctor`:profile 组合、依赖、兼容与 `dsh-scope` 副本数;`/doctor --fix` 修 profile 补丁(写前备份)。
229
280
  - 真机验收脚本:`node scripts/tui-probe.mjs`(启动/缩放//diag//doctor//copy error//preset/鼠标模式/退出)、
230
281
  `node scripts/tui-drop-probe.mjs`(杀掉窗口再接管,断言转录保留、可输入、无乱码)与
package/docs/terminals.md CHANGED
@@ -124,3 +124,56 @@ Windows 与 Linux 的差异只在**改键**文件里:`/keys` 写出的键名
124
124
  - **`/diag` 的终端行来自 Host 的环境**:重新接入(reattach)后它描述的是 Host 启动时所处的终端。
125
125
  退出时的「离开备用屏」序列因此**无条件发出**——它落在没进过备用屏的终端上会被忽略,
126
126
  而漏发会把用户留在无法滚动的屏幕里。
127
+
128
+ ## 字符宽度:哪些算两格,以及为什么
129
+
130
+ 终端里"一个字符占几格"没有统一答案,这个 TUI 的口径是:
131
+
132
+ | 类别 | 例子 | 预算 | 说明 |
133
+ |---|---|---|---|
134
+ | CJK / 全角 | `中`、`,`、`(` | 2 | 无争议 |
135
+ | emoji 符号 | `✖`、`⚠`、`▶`、`✅` | 2 | 终端用彩色 emoji 字形画它,比等宽字体宽;`pinEmojiCells` 用 VS15 + 预留空格把它**钉**在 2 格里 |
136
+ | **带圈/带括号的字母数字** | `①`-`⑳`、`⑴`、`⒈`、`ⓐ`、`❶`、`➀`、`Ⅰ` | **看终端** | 见下 |
137
+ | 本 TUI 自己的装饰 | `─`、`●`、`❯`、`·`、`▸`、`✓`、`⠋` | **1** | 它们来自等宽字体,按 2 格算会画出半宽横线、把光标挪到文字右边一格 |
138
+
139
+ **这两行(带圈字符、标点)是 2026-09-29 修的一个真实缺陷**:模型会写 `①`-`⑩`,而 CJK 字体的终端把它们画成两格,我们却按一格预算——于是每一行都比实际短一格。
140
+ 与 emoji 不同,这类字符**没有变体选择符**可以"钉"宽度,只能跟着终端走。判定规则:
141
+
142
+ 1. `DSH_TUI_AMBIGUOUS_WIDTH=1|2` 明确指定(覆盖一切);
143
+ 2. 否则看 locale:`LC_ALL` / `LC_CTYPE` / `LANG` 是 **zh / ja / ko** ⇒ 按 2 格(该终端极可能用 CJK 字体);
144
+ 3. 其它 ⇒ 按 1 格。
145
+
146
+ **UI 语言不参与判定**:一个中文读者在纯西文终端上看到的仍是窄字形,输入中文并不会改变字体度量。
147
+ 若你的终端在英文 locale 下也把这类字符画成两格(或在中文 locale 下画成一格),用上面那个环境变量显式纠正即可。
148
+
149
+ ### 先实测,再回落(2026-09-30 起)
150
+
151
+ locale 只是**猜测**,而"① 渲染错位"报了三次都出在猜上。现在由**显示中继**(持有终端的那一侧)直接问终端:
152
+ 把 `①—…“”·•Ⅰ` 打到刚清空的一行上,发 `CSI 6n`,读回光标列——那个列就是终端刚刚花掉的格数(`src/glyph-measure.ts`)。
153
+
154
+ 测量走 `TerminalInputPump.askPosition`,**应答由 pump 吞掉**——这不是实现细节:没人消费的光标应答会被终端回显成字面量 `^[[1;5R`,
155
+ 第一版把探测放在启动器里裸写,就在进入 TUI 的过程中把这类异常字符重新漏了出来。测完的结论经**协议帧 `FRAME_METRICS`(12)** 交给
156
+ 正在绘制的 Host,所以已在运行的旧 Host、resume 会话也能当场采纳并整体重绘(旧帧的行是按旧表算的,必须重绘而不是增量)。
157
+
158
+ - 终端推进 **2 格** → 按 2 格预算,不补空格;
159
+ - 终端推进 **1 格**(字形却画得更宽 → 就是你看到的"①和后一个字挤在一起") → 仍按 2 格预算,但**补一个预留空格**:
160
+ 渲染时在字形后插一个空格,让终端实际花掉两格。这正是 emoji 那套"钉住"的另一半;圈号没有变体选择符可用,所以只剩这半招。
161
+ **宽度契约**:钉住前后 `displayWidth` 必须相等(那个空格算作同一个两格单元),否则填/截/光标会集体偏一格。
162
+ - 终端不答或答得既不匹配"全宽"也不匹配"全窄"(混排字体) → 什么都不决定,回落 locale 猜测。
163
+ - 想让探针完全不写字节(给断言 attach 字节序列的 harness):`DSH_TUI_NO_GLYPH_PROBE=1`。
164
+
165
+ 另外两处必须留意:**绘制发生在分离 Host 里,而 Host 的 stdout 是中继 socket、不是终端**——所以实测由持有终端的窗口进程做,
166
+ 结论经 `DSH_TUI_AMBIGUOUS_WIDTH` 传给 Host(spawn 时注入,见 `hostChildEnv`);没终端时一律按窄的一侧算(管道、日志、测试)。
167
+ 手工纠正仍然可用: `DSH_TUI_AMBIGUOUS_WIDTH=1|2`。
168
+
169
+ 涉及范围(`src/term-text.ts` 的 `AMBIGUOUS_WIDE_RANGES`),两类:
170
+
171
+ 1. **圈号/字母数字**:Enclosed Alphanumerics(U+2460–U+24FF,`①`-`⑳`/`⑴`/`⒈`/`ⓐ`)、Dingbat 圈号(U+2776–U+2793,`❶`/`➀`)、罗马数字(U+2160–U+217F,`Ⅰ`)、
172
+ Enclosed Alphanumeric Supplement(含区域指示符,U+1F100–U+1F1FF)、Enclosed Ideographic Supplement(U+1F200–U+1F2FF)。
173
+ 2. **CJK 全角标点**:`—` `–`(U+2013–U+2014)、`‘ ’ “ ”`(U+2018–U+201D)、`…`(U+2025–U+2026)、`•`(U+2022)、`‹ ›`(U+2039–U+203A)、`·`(U+00B7)。
174
+ 这一类在中文语料里出现得比圈号多几个数量级——实测某会话里 `—` 出现 **31855 次**。
175
+
176
+ **刻意不算宽**的:Box-drawing(`─`)、几何装饰(`●` `▸`)、`❯` `✓` `✗` `★` 等 —— 它们是本 TUI 自己的字符或早已按真实终端实测为窄字形,
177
+ 按两格算会把横线画成半宽、把光标挪到文字右边。
178
+
179
+ 顺带修掉的一个连带缺陷:`foldInputView`(输入行折叠)原来把省略号标记硬写成 1 格,而 `…` 现在是 2 格 —— 折叠后的行会**溢出预算**、光标偏移一格。现在按标记的实测宽度计算。