dsh-ssh-tui 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.en.md +101 -27
  2. package/README.md +83 -27
  3. package/docs/remote-ops.md +80 -1
  4. package/docs/terminals.md +17 -4
  5. package/docs/windows.md +128 -0
  6. package/lib/attach.js +14 -1
  7. package/lib/attach.js.map +1 -1
  8. package/lib/auth-failure.js +92 -0
  9. package/lib/auth-failure.js.map +1 -0
  10. package/lib/commands.js +2 -0
  11. package/lib/commands.js.map +1 -1
  12. package/lib/copy-text.js +2 -0
  13. package/lib/copy-text.js.map +1 -1
  14. package/lib/diag.js +3 -0
  15. package/lib/diag.js.map +1 -1
  16. package/lib/dialogs.js.map +1 -1
  17. package/lib/display-sock.js +313 -2
  18. package/lib/display-sock.js.map +1 -1
  19. package/lib/dsh-compat.js +46 -0
  20. package/lib/dsh-compat.js.map +1 -1
  21. package/lib/i18n/en.js +49 -14
  22. package/lib/i18n/en.js.map +1 -1
  23. package/lib/i18n/index.js +5 -0
  24. package/lib/i18n/index.js.map +1 -1
  25. package/lib/i18n/zh.js +49 -14
  26. package/lib/i18n/zh.js.map +1 -1
  27. package/lib/index.js +76 -15
  28. package/lib/index.js.map +1 -1
  29. package/lib/line-mode.js +4 -0
  30. package/lib/line-mode.js.map +1 -1
  31. package/lib/paint.js +32 -1
  32. package/lib/paint.js.map +1 -1
  33. package/lib/picker.js +104 -15
  34. package/lib/picker.js.map +1 -1
  35. package/lib/plan.js +8 -0
  36. package/lib/plan.js.map +1 -1
  37. package/lib/platform.js +81 -0
  38. package/lib/platform.js.map +1 -1
  39. package/lib/preset-rows.js +54 -24
  40. package/lib/preset-rows.js.map +1 -1
  41. package/lib/question-wait.js +418 -0
  42. package/lib/question-wait.js.map +1 -0
  43. package/lib/selection.js +26 -8
  44. package/lib/selection.js.map +1 -1
  45. package/lib/term-text.js +79 -8
  46. package/lib/term-text.js.map +1 -1
  47. package/lib/terminal-input.js +13 -0
  48. package/lib/terminal-input.js.map +1 -1
  49. package/lib/tui.js +713 -51
  50. package/lib/tui.js.map +1 -1
  51. package/lib/types/attach.d.ts +8 -0
  52. package/lib/types/auth-failure.d.ts +48 -0
  53. package/lib/types/commands.d.ts +6 -0
  54. package/lib/types/diag.d.ts +2 -0
  55. package/lib/types/dialogs.d.ts +16 -0
  56. package/lib/types/display-sock.d.ts +78 -2
  57. package/lib/types/dsh-compat.d.ts +44 -12
  58. package/lib/types/i18n/index.d.ts +13 -3
  59. package/lib/types/index.d.ts +7 -0
  60. package/lib/types/paint.d.ts +21 -0
  61. package/lib/types/picker.d.ts +22 -0
  62. package/lib/types/platform.d.ts +22 -0
  63. package/lib/types/preset-rows.d.ts +18 -10
  64. package/lib/types/question-wait.d.ts +221 -0
  65. package/lib/types/selection.d.ts +12 -4
  66. package/lib/types/settings-subagent.d.ts +3 -3
  67. package/lib/types/subagent-model.d.ts +3 -3
  68. package/lib/types/term-text.d.ts +14 -0
  69. package/lib/types/terminal-input.d.ts +10 -0
  70. package/lib/types/transcript-types.d.ts +22 -0
  71. package/lib/types/tui.d.ts +149 -3
  72. package/lib/types/update-check.d.ts +19 -4
  73. package/lib/types/workspace-changes.d.ts +135 -0
  74. package/lib/update-check.js +25 -8
  75. package/lib/update-check.js.map +1 -1
  76. package/lib/workspace-changes.js +120 -0
  77. package/lib/workspace-changes.js.map +1 -0
  78. package/package.json +104 -69
package/README.en.md CHANGED
@@ -6,7 +6,9 @@
6
6
  [![dshfind](https://dshfind.com/api/badge/cyjyyd/dsh-ssh-tui)](https://dshfind.com/en/plugins/cyjyyd/dsh-ssh-tui?ref=badge)
7
7
 
8
8
  A DeepSeek Harness terminal for jump hosts, headless servers, and high-latency
9
- SSH. Plain ANSI and incremental redraws. No browser required.
9
+ SSH. Plain ANSI and incremental redraws. No browser required. **When the SSH
10
+ session drops mid-turn, the Host keeps the turn; reconnecting with the same
11
+ command attaches back to it, so the session is not lost.**
10
12
 
11
13
  中文部署指南:[README.md](README.md)
12
14
 
@@ -14,6 +16,20 @@ If you mostly work over SSH — a jump host, a test box, a keyboard-only
14
16
  session — start here. A local desktop terminal with themes and layout
15
17
  you already like can stay as it is.
16
18
 
19
+ How this differs from what already exists:
20
+
21
+ - **Official headless**: the same task prints only its last reply to stdout; the
22
+ rest stays in the session log.
23
+ - **Official `dsh-ssh` (from 0.1.6)**: the Harness runs locally and files and
24
+ processes run remotely, which needs a helper installed ahead of time. Use this
25
+ plugin when you are already SSH'd onto the machine. The two stack; neither
26
+ replaces the other.
27
+ - **Other terminal skins** (`dsh-TUI` and the like): a good-looking local
28
+ terminal, competing on themes and layout. This plugin competes on incremental
29
+ redraws over a slow link, a Host that survives the drop, and a plain line mode.
30
+
31
+ How to get back in after a drop is under [After an SSH drop](#after-an-ssh-drop).
32
+
17
33
  A SuperGrok / X Premium subscription goes through the standalone plugin
18
34
  [dsh-llm-xai-oauth](https://github.com/cyjyyd/dsh-llm-xai-oauth) (headless, web, or this TUI). It reuses a local grok-bridge token; no xAI API key.
19
35
 
@@ -31,6 +47,8 @@ dsh --profile tui
31
47
  Current `dsh` requires `--profile` (`dsh plugin add …` errors without it).
32
48
  Swap `tui` for another profile name.
33
49
 
50
+ **On the 0.2.0-rc host line?** 0.8.0 is the release that supports it (older versions are refused by the launcher's version check), but it is published under `next` for now — `latest` is still 0.7.4 until 2026-10-01. Until then install with `dsh plugin --profile tui add dsh-ssh-tui@next`; on the 0.1.7-rc line `@latest` (0.7.4) is enough, and moving to 0.8.0 later costs nothing. **0.1.5 and older hosts are no longer supported as of 0.8.0.**
51
+
34
52
  **Updates must use `@latest`.** `dsh plugin` forwards the rest of the line to
35
53
  pnpm in the profile directory. A bare `add dsh-ssh-tui` keeps the version
36
54
  already pinned in `pnpm-lock.yaml` (often 0.3.7). Do not put `--profile`
@@ -69,8 +87,12 @@ Reproducible, no model in the loop: `npm run screenshots:slow` writes
69
87
  `docs/screenshots/slow-link.json`. This capture is 14 paints, about
70
88
  **18.0 KB**, **8.8 s** at 2 kB/s. Byte ledger for this event sequence.
71
89
 
72
- 0.7 highlights: drag-select any part of a model reply to copy it (OSC 52 into your local
73
- clipboard; a tool card still expands on click) · the footer is one priority-ordered chip
90
+ 0.7 highlights: drag-select any part of a model reply to copy it (hold the button, or Shift;
91
+ OSC 52 into your local clipboard; a tool card still expands on click; an SSH session copies
92
+ into the local terminal and no longer warns that it cannot) · a reply is a selectable card too:
93
+ one `↑` on an empty input lands on the newest reply-or-card (marked `▶`, in screen order;
94
+ `Alt+4` selects the latest reply), `/copy` then takes that one as written rather than the
95
+ wrapped screen text, and `Enter` opens it full-screen · the footer is one priority-ordered chip
74
96
  strip, and `⚠` opens `/doctor` · the quota bar is on screen from the first frame and names
75
97
  its window (`5Hr`/`1Wk`/`1Mo`, smallest window by default, `?%` with a 15-second retry until
76
98
  a reading arrives) · `/mode` groups presets and filters with `/` · the compact view names
@@ -82,8 +104,10 @@ pins the palette (truecolor / 256 / 8 / none).
82
104
  ## Requirements
83
105
 
84
106
  - Node.js >= 22.19
85
- - `@deepseek-ai/dsh` CLI: `npm i -g @deepseek-ai/dsh` (CI covers `0.1.5-rc.1` / `0.1.5-rc.3` / `0.1.7-rc.1`: `0.1.5-rc.1` runs typecheck and the unit suite, the other two add the real-PTY probes. `0.1.5-alpha.1` / `0.1.5-alpha.2` / `0.1.3-alpha.2` share the same handle API + `agent/assistant-stream` shims as the 0.1.5-rc line. **`0.1.2-rc` and older are no longer supported** — if the install is refused, upgrade to `0.1.5-rc` or `0.1.7-rc` first. `0.1.3-alpha.1` exists only as a GitHub tag and was never published to npm)
107
+ - `@deepseek-ai/dsh` CLI: `npm i -g @deepseek-ai/dsh` (this repo develops on `0.1.7-rc.1`; CI covers `0.1.5-rc.1` / `0.1.5-rc.3` / `0.1.7-rc.1` / `0.1.7-rc.2`: `0.1.5-rc.1` runs typecheck and the unit suite, the other three add the real-PTY probes, and every non-default leg rewrites the manifest with `scripts/ci-pin-line.mjs` before installing from scratch. `0.1.5-alpha.1` / `0.1.5-alpha.2` / `0.1.3-alpha.2` share the same handle API + `agent/assistant-stream` shims as the 0.1.5-rc line. **`0.1.2-rc` and older are no longer supported** — if the install is refused, upgrade to `0.1.5-rc` or `0.1.7-rc` first. `0.1.3-alpha.1` exists only as a GitHub tag and was never published to npm)
86
108
  - pnpm (used by `dsh plugin` to manage profile dependencies)
109
+ - an ANSI terminal (SSH directly, or PowerShell / Windows Terminal on Windows)
110
+ - **Windows**: install, troubleshooting and how to read `/doctor` are in [docs/windows.md](docs/windows.md)
87
111
 
88
112
  ## Install
89
113
 
@@ -114,7 +138,7 @@ dsh --profile headless "Reply with exactly: tui-install-ok. Do not use tools."
114
138
  dsh --profile tui
115
139
  ```
116
140
 
117
- From a checkout: `bash scripts/smoke-headless.sh` (prints an outcome summary, never a token).
141
+ From a checkout: `node scripts/smoke-headless.mjs` (or `bash scripts/smoke-headless.sh`; prints an outcome summary, never a token).
118
142
 
119
143
  ### After an SSH drop
120
144
 
@@ -130,6 +154,22 @@ dsh --profile tui --resume # picker (live hosts first)
130
154
  dsh --profile tui --resume <session-id> # attach if live, else resume the log
131
155
  ```
132
156
 
157
+ The picker gives every live session one of two words and the Host's pid:
158
+ **attachable** (one keystroke in) or **occupied** (another window has it). When it
159
+ says occupied, `--resume <session-id>` is refused rather than kicking that window:
160
+ take it over from the **picker** instead — select the row and confirm with `y` /
161
+ Enter, and the other window exits.
162
+
163
+ **A cut link does not block you.** When SSH dies the server usually does not
164
+ know yet: sshd waits for TCP keepalive (which can be hours), the launcher stays
165
+ connected to the display channel, and the lock still reads `attached`. So the
166
+ Host asks the **terminal itself** (a cursor round trip through the relay — see
167
+ `scripts/tui-cut-probe.mjs`). A terminal that cannot answer means that window is
168
+ gone, the picker says **attachable**, one keystroke attaches, and so does
169
+ `--resume <session-id>`. Only a terminal that confirms it is still there needs
170
+ the takeover question above — and that answer is resolved *before* the list is
171
+ painted, so a row never says occupied and then changes its mind.
172
+
133
173
  A second Host on the same `sessionId` is refused (it would steal the jsonl
134
174
  and approvals). Locks live under `$DSH_HOME/tui-locks/`; the display socket
135
175
  under `$DSH_HOME/tui-socks/`. A leftover lock from a crash is stolen if the
@@ -155,8 +195,11 @@ New sessions inherit the directory you launched from. Resuming a session
155
195
  `目录:srv` (last path segment); click it to print the full path.
156
196
 
157
197
  On start, if npm has a newer `dsh-ssh-tui`, a first-launch picker offers
158
- **Update now / Later / Skip this version**. Update now runs
159
- `dsh plugin --profile tui add dsh-ssh-tui@latest` and asks you to restart.
198
+ **Update now / Later / Skip this version**. Update now installs the exact
199
+ version just looked up (`dsh plugin --profile tui add dsh-ssh-tui@<version>`),
200
+ not `@latest`: pnpm resolves `@latest` a second time, and a release still inside
201
+ its `minimumReleaseAge` window is then skipped silently — the install succeeds
202
+ and you are left on the old one. It asks you to restart afterwards.
160
203
  Set `DSH_TUI_NO_UPDATE_CHECK=1` to skip. `/status` also shows the plugin version, the link chip, the quota window, and whether the subagent model is in the same family as the parent route.
161
204
 
162
205
  From git:
@@ -164,9 +207,12 @@ From git:
164
207
  ```sh
165
208
  git clone https://github.com/cyjyyd/dsh-ssh-tui.git
166
209
  cd dsh-ssh-tui
167
- bash scripts/install.sh # installs into the `tui` profile
210
+ node scripts/install.mjs # installs into the `tui` profile
168
211
  ```
169
212
 
213
+ `bash scripts/install.sh` is a thin wrapper around that script, so it does the
214
+ same thing where bash exists. On Windows run the Node form — there is no bash.
215
+
170
216
  Or manually:
171
217
 
172
218
  ```sh
@@ -175,6 +221,14 @@ npm run build
175
221
  dsh plugin --profile tui add "link:$(pwd)"
176
222
  ```
177
223
 
224
+ The PowerShell equivalent, since `$(pwd)` is a POSIX substitution:
225
+
226
+ ```powershell
227
+ npm install --no-audit --no-fund
228
+ npm run build
229
+ dsh plugin --profile tui add "link:$((Get-Location).Path)"
230
+ ```
231
+
178
232
  ### The preset roster `/mode` needs
179
233
 
180
234
  A terminal profile built on `dsh-base` composes no preset roster (only the Web
@@ -191,8 +245,8 @@ Three ways to repair it (idempotent, pick one):
191
245
  1. **In-app**: `/mode fix` writes the block below and tells you to restart.
192
246
  This is the generic path — an npm install and the in-app "Update now" both go
193
247
  through `dsh plugin add` and never run the repository scripts.
194
- 2. From a checkout: `bash scripts/ensure-profile-rows.sh [profile]`
195
- (default `tui`).
248
+ 2. From a checkout: `node scripts/profile-rows.mjs [profile]`
249
+ (default `tui`; `bash scripts/ensure-profile-rows.sh` is the same command).
196
250
  3. By hand, in `$DSH_HOME/profiles/<profile>/cordis.patch.yml`:
197
251
 
198
252
  ```yaml
@@ -213,7 +267,7 @@ this block lists the roster row twice, and the second mount fails with
213
267
 
214
268
  Without the row, the TUI prints a boot line ("No agent-preset roster is
215
269
  composed…"), `/mode` reports the patch path plus the `/mode fix` entry point,
216
- and `scripts/verify.sh` says the same. Preset identity is per module instance,
270
+ and `node scripts/verify.mjs` (`scripts/verify.sh`) says the same. Preset identity is per module instance,
217
271
  so a dsh install tree carrying two copies of `@deepseek-ai/dsh-scope` (an
218
272
  npm-nested checkout can) fails the mount with `refusing to compose an unscoped
219
273
  context`; a global `npm i -g` install is not affected.
@@ -221,7 +275,7 @@ context`; a global `npm i -g` install is not affected.
221
275
  For the optional **智能路由模式 (routing-suite)** mode, also run:
222
276
 
223
277
  ```sh
224
- bash scripts/install-routing-suite.sh
278
+ node scripts/install-routing-suite.mjs
225
279
  ```
226
280
 
227
281
  The script adds `dsh-routing-suite`, registers its preset for the `/mode`
@@ -238,8 +292,8 @@ dsh --profile tui
238
292
  Verify and uninstall:
239
293
 
240
294
  ```sh
241
- bash scripts/verify.sh
242
- bash scripts/uninstall.sh
295
+ node scripts/verify.mjs
296
+ node scripts/uninstall.mjs
243
297
  ```
244
298
 
245
299
  ## First-launch setup
@@ -348,14 +402,15 @@ You can reopen the wizard at any time with:
348
402
 
349
403
  | Key | Action |
350
404
  | --- | --- |
351
- | `Enter` | send; while running, steer; with empty input, toggle the selected card. Oversized tool bodies open a dedicated inspect view; `Esc` returns |
405
+ | `Enter` | send; while running, steer; with empty input, toggle the selected card, or open the full view when the selected row is a reply (`Esc` returns). Oversized tool bodies open a dedicated inspect view too |
352
406
  | `Tab` | complete the highlighted slash command |
353
- | `↑` / `↓` | empty input: move among cards; otherwise history (↓ past the newest item restores the live draft). Same as `Ctrl+N` / `Ctrl+P` |
354
- | `Ctrl+R` | expand the latest card; once a card is selected, expand or collapse all |
407
+ | `↑` / `↓` | empty input with cards in the session: walk up from the newest **reply or card** in screen order (the `▶` marker is only ever on the selected row); with no card at all (plain Q&A, or right after `/clear`) the key keeps its history meaning — select a reply there with `Alt+4` or `Ctrl+N`/`Ctrl+P`. Otherwise history (↓ past the newest item restores the live draft) |
408
+ | `Ctrl+R` | expand the latest card; once something is selected, expand or collapse all (a selected reply keeps its selection) |
355
409
  | `Ctrl+T` | fold the input box (display-only) |
356
- | `Alt+1` / `2` / `3` / `4` | jump to latest thinking / plan / subagent / reply |
357
- | `/find [kind] query` | search and jump to the full matching message (`thinking` `plan` `subagent` `reply` `prompt` `tool`). `Ctrl+/` or `Alt+/` opens it |
358
- | `/copy` | copy the focused card as plain text to the local clipboard (latest reply if none; OSC 52) |
410
+ | `Alt+1` / `2` / `3` / `4` | jump to latest thinking / plan / subagent / reply (a reply is selected, ready for `/copy`) |
411
+ | `/find [kind] query` | search and jump to the full matching message (`thinking` `plan` `subagent` `reply` `prompt` `tool`), which is also selected. `Ctrl+/` or `Alt+/` opens it |
412
+ | `/copy` | copy the selected card or reply **as written** to the local clipboard (latest reply if none is selected; OSC 52). The selection survives, so pressing it twice copies the same thing. Inside a full view the copy key (`Ctrl+Shift+C`) takes **the body on screen** — a tool body, a changes diff, or the reply as written — and the overlay echoes the confirmation |
413
+ | `/retryauth [on\|off]` | auto-retry once when the *provider* rejects a request (HTTP 401/403 while a local credential is configured); off by default. The three-step diagnosis is in [`docs/remote-ops.md`](docs/remote-ops.md) §4.8 |
359
414
  | `Ctrl+G` / `Alt+N` | next search hit; `Alt+P` previous |
360
415
  | `Esc` | drop selection → scroll to bottom → cancel the running turn |
361
416
  | `Ctrl+C` | cancel the running turn; press twice when idle to exit |
@@ -623,8 +678,11 @@ skips token chunks and lays out only the visible tail on the first paint.
623
678
 
624
679
  Each paint is one `stdout.write` of dirty rows only, so a jump host or
625
680
  corporate proxy does not see one SSH packet per line. Local ttys use 80 ms.
626
- Over SSH the TUI probes CSI 6n once and picks 80 / 160 / 250 / 400 ms from
627
- the round-trip. `DSH_TUI_PAINT_MS` always wins (40–1000). The stats line
681
+ Over SSH the TUI probes CSI 6n and picks 80 / 160 / 250 / 400 ms from the
682
+ round-trip. It keeps measuring — 8 s after the attach, then every 20 s (a
683
+ measurement that moved is confirmed 5 s later) — and both the chip and the paint
684
+ tier follow the median of the last three, so a jittery entry cannot pin either
685
+ at the slowest tier. `DSH_TUI_PAINT_MS` always wins (40–1000). The stats line
628
686
  starts with `SSH ●●●○ 90ms` (1 pip red, 2 yellow, 3+ green). The probe
629
687
  does not write into the transcript.
630
688
 
@@ -639,13 +697,16 @@ Find your symptom; each answer is what to do, not a change log.
639
697
 
640
698
  - **`dsh-ssh-tui: both stdin and stdout must be TTYs`** — start it from a real terminal or
641
699
  SSH session; a pipe, CI, or `&` background job will not do.
700
+ - **Windows install, the scripts, and how to read `/doctor`** — [docs/windows.md](docs/windows.md)
701
+ has a 30-second install, a "what to check first when it will not start" table, and what the
702
+ `●` / `⚠` / `✖` marks in a `/doctor` report mean.
642
703
  - **Windows: `host display socket did not appear`** — upgrade
643
704
  (`dsh plugin --profile tui add dsh-ssh-tui@latest`); older builds waited 15 seconds and
644
705
  timed out on the named pipe. If it still fails, attach `/diag` to an issue.
645
706
  - **Windows: the in-app update reports `spawn dsh ENOENT`** — an older updater spawned a
646
- bare `dsh`, which on Windows is a `dsh.cmd` shim. Run the same upgrade once from the
647
- command line (`dsh plugin --profile tui add dsh-ssh-tui@latest`); the in-app update works
648
- from then on.
707
+ bare `dsh`, which on Windows is a `dsh.cmd` shim. Run
708
+ `dsh plugin --profile tui add dsh-ssh-tui@latest` once from the command line; the in-app
709
+ update works from then on, and it installs the version it looked up rather than `@latest`.
649
710
  - **pnpm refuses to run the build script of a git dependency** — add the key pnpm prints to
650
711
  `allowBuilds` in the profile's `pnpm-workspace.yaml`, then reinstall.
651
712
  - **After an upgrade `/mode` reports a missing service, or the preset tools vanish** — run
@@ -686,8 +747,17 @@ Find your symptom; each answer is what to do, not a change log.
686
747
  painter asks for the narrow text form; a font without the glyph still makes the terminal
687
748
  fall back to a wider colour emoji.
688
749
  - **The screen cannot keep up on a slow link** — `DSH_TUI_PAINT_MS` sets the paint interval
689
- (40–1000 ms: smaller is snappier and sends more); unset, it follows the round-trip
690
- measured at start-up (80 / 160 / 250 / 400 ms).
750
+ (40–1000 ms: smaller is snappier and sends more); unset, it follows the round-trip,
751
+ which is re-measured while the session runs (80 / 160 / 250 / 400 ms, median of the last
752
+ three) — see `scripts/tui-rtt-probe.mjs`.
753
+ - **The model “thinks, then says it is done”** — that is an **empty turn from upstream**: some
754
+ gateways map a Gemini/Claude thought part onto `reasoning_content` and then finish with
755
+ `finish_reason: stop` and no content at all, so the harness assembles a reply that is only
756
+ thinking and the turn legitimately ends. The TUI now says so in the transcript
757
+ (“upstream ended this turn after thinking only … it is not really done — press Enter or send
758
+ another message to continue”) instead of letting 完成 read as an answer. Measured on one
759
+ gateway (`google-ai-pro` / `gemini-3.8-flash-high`): about **29%** of turns, with the rest of
760
+ the same session replying normally.
691
761
  - **Screen reader, or you want a log** — start with `DSH_TUI_LINE_MODE=1`: plain appended
692
762
  lines, no cursor control, safe to `tee`.
693
763
  - **Title bar or bell does nothing** — the terminal needs OSC 0 and BEL; `DSH_TUI_NO_BELL=1`
@@ -695,6 +765,10 @@ Find your symptom; each answer is what to do, not a change log.
695
765
  - **Whole-row backgrounds are too loud on a dark terminal** — `DSH_TUI_COLOR_DEPTH=none`
696
766
  drops them; diffs still read through `+`/`-`.
697
767
 
768
+ - **Box drawing and dots render as garbage** — the console code page is not UTF-8. The UI
769
+ redraws its chrome in ASCII automatically (rules as `-`, status dots as `*`, the warning as
770
+ `!`) and `/diag` says so on its terminal line; `chcp 65001` or Windows Terminal restores the
771
+ glyphs. `DSH_TUI_ASCII=1` forces ASCII, `=0` forces Unicode. See [docs/windows.md](docs/windows.md).
698
772
  - **Terminal compatibility** — what each terminal is allowed and promised (Windows Terminal,
699
773
  conhost, GNOME, XFCE, Konsole, xterm, tmux, screen, the Linux console) is tabulated in
700
774
  [docs/terminals.md](docs/terminals.md); `/diag` prints the verdict it used, and
package/README.md CHANGED
@@ -6,13 +6,21 @@
6
6
  [![dshfind](https://dshfind.com/api/badge/cyjyyd/dsh-ssh-tui)](https://dshfind.com/zh/plugins/cyjyyd/dsh-ssh-tui?ref=badge)
7
7
 
8
8
  给跳板机、无桌面服务器、高延迟 SSH 用的 DeepSeek Harness 终端。纯 ANSI、增量重绘,
9
- 不需要浏览器。
9
+ 不需要浏览器。**SSH 掉线时,正在跑的回合留在 Host 里,重连用同一条命令接回,会话不丢。**
10
10
 
11
11
  English: [README.en.md](README.en.md)
12
12
 
13
13
  如果你主要在 SSH 里写代码——公司跳板、测试机、只有键盘的会话——可以从这里开始。
14
14
  本机桌面终端若更在意主题和布局,也可以继续用你已经习惯的界面。
15
15
 
16
+ 和现成的东西不重叠:
17
+
18
+ - **官方 headless**:同一条任务只把最后一条回复打到 stdout,过程全在日志里。
19
+ - **官方 `dsh-ssh`(0.1.6 起)**:本机跑 Harness、远端跑文件和进程,要预装 helper。人已经 SSH 在那台机器上时用本插件;两者可以叠用,不是替代。
20
+ - **其它终端皮肤**(`dsh-TUI` 等):本机漂亮终端,比主题和布局。本插件比的是弱网下的增量重绘、掉线后 Host 留下、纯文本行模式。
21
+
22
+ 掉线之后怎么接回来,见下面的[「SSH 断了之后」](#ssh-断了之后)。
23
+
16
24
  已经在付 SuperGrok / X Premium 的话,用独立插件 [dsh-llm-xai-oauth](https://github.com/cyjyyd/dsh-llm-xai-oauth) 把订阅接进 dsh(headless / web / 本 TUI 都能用),复用本机 grok-bridge token,不需要 xAI API Key。
17
25
 
18
26
  本插件已被 [dshfind 插件目录](https://dshfind.com/zh/plugins/cyjyyd/dsh-ssh-tui) 收录:
@@ -28,6 +36,11 @@ dsh --profile tui
28
36
 
29
37
  当前 `dsh` 必须带 `--profile`(`dsh plugin add …` 会报缺选项)。装进别的 profile 把 `tui` 换成那个名字即可。
30
38
 
39
+ > **宿主已升到 0.2.0-rc 的用户请看这里**:0.8.0 支持 `0.2.0-rc` 线(旧版会因声明窗口不符被 launcher 拒绝),
40
+ > 但它目前只挂在 `next` 上——`latest` 仍是 0.7.4,2026-10-01 起才切到 0.8.0。这段时间请用
41
+ > `dsh plugin --profile tui add dsh-ssh-tui@next` 安装;还在 `0.1.7-rc` 线的话 `@latest`(0.7.4) 就够用,
42
+ > 等 0.8.0 上 `latest` 再升也不迟。**0.1.5 及更早的宿主从 0.8.0 起不再支持**。
43
+
31
44
  **更新必须带 `@latest`。** `dsh plugin` 只是把后面的参数转给 profile 目录里的 pnpm。写成 `add dsh-ssh-tui`(没有版本)时,pnpm 会沿用 `pnpm-lock.yaml` 里已经钉死的版本(常见就是一直停在 0.3.7)。也不要把 `--profile` 写到 `add` 后面:`dsh plugin add --profile tui add dsh-ssh-tui` 不是合法用法。卸载:`dsh plugin --profile tui remove dsh-ssh-tui`。
32
45
 
33
46
  ## 官方 headless 和这个 TUI
@@ -88,7 +101,11 @@ dsh --profile tui
88
101
  标题(还没出现就保持「处理中」),运行中的工具摘要在 `└` 下自动折行(最多 3 行,末行
89
102
  加省略号),带计时和 Esc 中断;回复开始流式输出时自动让位。
90
103
 
91
- - 0.7 起:模型回复可**拖选自由复制**(按住拖过一段,走 OSC 52 写回本机剪贴板;工具卡仍是点击展开);
104
+ - 0.7 起:模型回复可**拖选自由复制**(按住鼠标拖过一段,走 OSC 52 写回本机剪贴板;按住 Shift 拖选
105
+ 结果相同;工具卡仍是点击展开)。回复也是**可选中卡片**:空输入 `↑` 落到最新的一条回复或卡片(`▶` 标记,
106
+ 走屏幕顺序;`Alt+4` 直接选中最新回复),
107
+ `/copy` 复制的是选中的那一条(原文,不是折行后的屏幕文本),`Enter` 打开全文;
108
+ SSH 会话里复制落到本机终端,不再误报「本终端不接收 OSC 52」;
92
109
  底栏收敛成一条带优先级的芯片带(先丢文字后丢组,`⚠` 可点击打开 `/doctor`);**额度条常驻**并标注窗口
93
110
  (`5Hr`/`1Wk`/`1Mo`,默认显示最小窗口,未取到时显示 `?%` 并每 15 秒重试);`/mode` 分组显示并可用 `/` 过滤;
94
111
  极简视图逐文件列 `+/-`;工具 diff 为**行级**、只高亮变化字符、≥100 列时并排显示;
@@ -98,10 +115,10 @@ dsh --profile tui
98
115
  ## 环境要求
99
116
 
100
117
  - Node.js ≥ 22.19
101
- - DeepSeek Harness CLI:`npm i -g @deepseek-ai/dsh`(CI 覆盖 `0.1.5-rc.1` / `0.1.5-rc.3` / `0.1.7-rc.1`:`0.1.5-rc.1` 跑 typecheck 与单元套件,另两条连真 PTY 探针一起跑。`0.1.5-alpha.1` / `0.1.5-alpha.2` / `0.1.3-alpha.2` 与 0.1.5-rc 线共用同一套 handle API + `agent/assistant-stream` 兼容层。**`0.1.2-rc` 及更早的线不再支持**,装不上请先升到 `0.1.5-rc` 或 `0.1.7-rc`。`0.1.3-alpha.1` 只在 GitHub 有 tag,npm 未发布,无法本地装包验证)
118
+ - DeepSeek Harness CLI:`npm i -g @deepseek-ai/dsh`(本仓库默认开发线是 `0.2.0-rc.1`,CI 覆盖 `0.1.7-rc.1` / `0.1.7-rc.2` / `0.2.0-rc.1` 三条 dsh 线加一条 Windows 腿:`0.1.7-rc.1` 跑 typecheck 与单元套件,另两条连真 PTY 探针一起跑;非默认腿由 `scripts/ci-pin-line.mjs` 改写 manifest 后从零安装。**`0.1.5` 及更早的线自 0.8.0 起不再支持**(`dshReleases` 里保留 `incompatible` 的明确表态,装不上请升到 `0.1.7-rc` 或 `0.2.0-rc`;`0.1.5` 那条线需要的是 `<0.1.6` 的旧代 API)。`0.1.3-alpha.1` 只在 GitHub 有 tag、npm 未发布,无法本地装包验证)
102
119
  - pnpm(`dsh plugin` 通过 pnpm 管理 profile 依赖)
103
120
  - 支持 ANSI 的终端(推荐 SSH 直连;Windows 用 PowerShell / Windows Terminal)
104
- - Windows:Host 与显示端之间的本地通道使用命名管道
121
+ - Windows:安装、排障与 `/doctor` 的读法见 [docs/windows.md](docs/windows.md)。Host 与显示端之间的本地通道使用命名管道
105
122
  `\\.\pipe\dsh-tui-<DSH_HOME 摘要 8 位>-<会话名>-<会话摘要 8 位>`(Windows 只能监听命名管道,
106
123
  不能监听 `.sock` 文件;名字里同时带 DSH_HOME 与会话 id 的摘要,所以不同 home、不同会话都不会撞名,
107
124
  结束进程即自动回收)。Host 的 stderr 记录在 `%USERPROFILE%\.dsh\tui-socks\<会话名>-<摘要>.err`,
@@ -150,6 +167,17 @@ dsh --profile tui --resume # 选择器(活进程优先接入
150
167
  dsh --profile tui --resume <session-id> # 有活进程则接入,否则从日志恢复
151
168
  ```
152
169
 
170
+ 选择器里每个活着的会话只标两个词:**可接入**(一键接入)或**已占用**(别的窗口在用)。
171
+ 后面跟的是 Host 的 pid。被标成「已占用」时 `--resume <session-id>` 会被拒绝而不是把对方踢掉——
172
+ 要接管就在**选择器里**选中它,再按 `y` / Enter 确认(那个窗口随即退出)。
173
+
174
+ **断线残留不会挡住你**:SSH 被切断时服务端常常还不知道(sshd 要等 TCP keepalive 才发现,可能几小时),
175
+ 那个窗口的 launcher 仍然连着显示通道,锁里写的还是 `attached`。所以 Host 会反过来向**终端本身**要答案
176
+ (经 relay 做一次光标往返,见 `scripts/tui-cut-probe.mjs`):终端答不上来就说明那个窗口已经没了,
177
+ 选择器直接标「可接入」,一键接入;`--resume <session-id>` 也直接接入。
178
+ 只有终端确认还活着时才需要上面的接管确认。这个判断在**首帧之前**完成,所以列表不会先显示「已占用」
179
+ 再改口。
180
+
153
181
  同一 `sessionId` 不能同时开第二份 Host(会抢 jsonl 和审批)。锁在
154
182
  `$DSH_HOME/tui-locks/`,显示通道在 `$DSH_HOME/tui-socks/`。进程死后残留锁会在
155
183
  下次启动时核对 pid,已死则自动从日志接管。调试可设 `DSH_TUI_NO_SESSION_LOCK=1`。
@@ -163,6 +191,11 @@ dsh --profile tui --resume <session-id> # 有活进程则接入,否则从
163
191
  倍以上的(那是别人请求的回复,相对判定所以 `ssh localhost` 的 2ms 链路照样算得出来)。
164
192
  代价是每次接入多约 0.4–0.6 秒探测时间,换来的是 50ms 链路不再出现 2ms / 1900ms 的跳变。
165
193
 
194
+ **链路会一直重测,不是只测一次**:接入 8 秒后补测一次,之后每 20 秒一次(变化超过 50ms 时
195
+ 5 秒后再确认一次),footer 芯片与绘制档位取**最近三次的中位数**——单次抖动(或接入那一刻正好
196
+ 在抖)不会把芯片和绘制预算钉在最慢一档,真正的变化两次就能定下来。终端完全不答 DSR 时退避到
197
+ 10 分钟一次。见 `scripts/tui-rtt-probe.mjs`(CI 的 0.1.7 腿)。
198
+
166
199
  这些回复也不可能再进输入框:relay 整条 stdin 管道常驻过滤(含跨 read 拆分的),Host 键
167
200
  处理前再过滤一次;连 Host 启动那几百毫秒里敲的键也会被暂存、接入后补发,不再被丢掉。
168
201
  `DSH_TUI_DEBUG=1` 时会打印每次采样与丢弃原因。
@@ -182,22 +215,25 @@ Host 在后台跑完这一轮;审批和提问等接上后再弹。空闲断线
182
215
  完全没有显示器且一直空闲的兜底仍由 `DSH_TUI_DETACHED_IDLE_MS`(默认 6 小时)负责。可选:用 tmux 包一层。
183
216
  常驻与接管的可复制配方(tmux / screen / systemd --user / 长任务)见 [`docs/remote-ops.md`](docs/remote-ops.md);重连后转录里的「已重连 N 次 · 断开 Xs」与「离开 …」两行的语义也在那里。
184
217
 
185
- 启动时若 npm 上有更新,会弹出选单(类似 Codex / Claude Code 首启):**现在更新 / 稍后 / 跳过此版本**。选「现在更新」会运行 `dsh plugin --profile tui add dsh-ssh-tui@latest`,完成后提示退出再启动。`DSH_TUI_NO_UPDATE_CHECK=1` 可关掉。`/status` 里也能看到当前插件版本、链路芯片、额度窗口,以及子代理模型是否与父路由同族。
218
+ 启动时若 npm 上有更新,会弹出选单(类似 Codex / Claude Code 首启):**现在更新 / 稍后 / 跳过此版本**。选「现在更新」安装的是刚才查到的那个版本号(`dsh plugin --profile tui add dsh-ssh-tui@<版本>`),不写 `@latest`:pnpm 对 `@latest` 会再解析一次,刚发布、还在 `minimumReleaseAge` 窗口里的版本会被静默跳过、装成旧的还显示成功。完成后提示退出再启动。`DSH_TUI_NO_UPDATE_CHECK=1` 可关掉。`/status` 里也能看到当前插件版本、链路芯片、额度窗口,以及子代理模型是否与父路由同族。
186
219
 
187
- 仓库内也可:`bash scripts/smoke-headless.sh`(记录出口摘要,不打印 token)。
220
+ 仓库内也可:`node scripts/smoke-headless.mjs`(`bash scripts/smoke-headless.sh` 等价;记录出口摘要,不打印 token)。
188
221
 
189
222
  ### 方式一:从 git clone 安装
190
223
 
191
224
  ```bash
192
225
  git clone https://github.com/cyjyyd/dsh-ssh-tui.git
193
226
  cd dsh-ssh-tui
194
- bash scripts/install.sh # 默认安装到 tui profile
227
+ node scripts/install.mjs # 默认安装到 tui profile(bash scripts/install.sh 等价)
195
228
  ```
196
229
 
230
+ Windows 没有 bash,用同一条 `node scripts/install.mjs`;完整的 Windows 步骤、
231
+ 「装不上先看什么」和 `/doctor` 的读法见 [docs/windows.md](docs/windows.md)。
232
+
197
233
  安装到其它 profile(例如自定义 `work` profile):
198
234
 
199
235
  ```bash
200
- bash scripts/install.sh work
236
+ node scripts/install.mjs work
201
237
  ```
202
238
 
203
239
  脚本会依次:安装依赖 → 构建 `lib/` → 通过 `dsh plugin --profile <name> add link:<repo>`
@@ -216,7 +252,8 @@ bash scripts/install.sh work
216
252
 
217
253
  1. **运行中的应用内修复**:`/mode fix` 写入下面这段并提示重启。npm 安装和
218
254
  「现在更新」走的是 `dsh plugin add`,不会执行仓库脚本,所以这是最通用的一条。
219
- 2. 仓库安装:`bash scripts/ensure-profile-rows.sh [profile]`(默认 `tui`)。
255
+ 2. 仓库安装:`node scripts/profile-rows.mjs [profile]`(默认 `tui`;
256
+ `bash scripts/ensure-profile-rows.sh` 是它的薄封装,两者等价)。
220
257
  3. 手动在该 profile 的 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 里加入:
221
258
 
222
259
  ```yaml
@@ -234,7 +271,7 @@ profile(例如同时装了 `@deepseek-ai/dsh-web-app` 的 profile)。改完
234
271
  会以 `service "agentPresets" has been registered` 失败),先把 profile 补丁里的这段删掉。
235
272
 
236
273
  名单缺席时,TUI 启动会打一行提示(中文界面:「未挂载 agent-presets 名单…」),
237
- `/mode` 会打印补丁路径和 `/mode fix` 修复入口,`scripts/verify.sh` 也会给出提示。
274
+ `/mode` 会打印补丁路径和 `/mode fix` 修复入口,`node scripts/verify.mjs`(`scripts/verify.sh` 等价)也会给出提示。
238
275
  另外,preset 的 scope 身份按模块实例判定:一个 dsh 安装树里若存在两份
239
276
  `@deepseek-ai/dsh-scope`(npm 嵌套安装的 checkout 可能如此),名单挂载会以
240
277
  `refusing to compose an unscoped context` 失败;全局安装(`npm i -g`)不受影响。
@@ -248,7 +285,15 @@ npm run build
248
285
  dsh plugin --profile tui add "link:$(pwd)"
249
286
  ```
250
287
 
251
- Windows(PowerShell)等价写法;仓库里的 `scripts/*.sh` 是 POSIX 脚本,Windows 直接用下面三条命令:
288
+ Windows(PowerShell)不用 bash,直接跑 Node 脚本(`scripts/*.sh` 只是转去调用它们的薄封装):
289
+
290
+ ```powershell
291
+ cd dsh-ssh-tui
292
+ node scripts/install.mjs # 装依赖、构建、链接进 tui profile、挂上 /mode 需要的行
293
+ node scripts/verify.mjs # 检查组合是否生效
294
+ ```
295
+
296
+ 不想用脚本时,三条命令等价:
252
297
 
253
298
  ```powershell
254
299
  cd dsh-ssh-tui
@@ -261,8 +306,8 @@ dsh plugin --profile tui add "link:$((Get-Location).Path)"
261
306
 
262
307
  ```bash
263
308
  dsh plugin --profile work add dsh-ssh-tui
264
- # 或仓库脚本
265
- bash scripts/install-npm.sh work
309
+ # 或仓库脚本(bash scripts/install-npm.sh 等价)
310
+ node scripts/install-npm.mjs work
266
311
  ```
267
312
 
268
313
  ### 智能路由模式(dsh-routing-suite)
@@ -270,8 +315,8 @@ bash scripts/install-npm.sh work
270
315
  需要“智能路由模式”时,安装 `dsh-routing-suite` 并注册其 preset:
271
316
 
272
317
  ```bash
273
- bash scripts/install-routing-suite.sh # 默认 tui profile
274
- bash scripts/install-routing-suite.sh work # 其它 profile
318
+ node scripts/install-routing-suite.mjs # 默认 tui profile
319
+ node scripts/install-routing-suite.mjs work # 其它 profile(bash scripts/install-routing-suite.sh 等价)
275
320
  ```
276
321
 
277
322
  脚本会执行 `dsh plugin --profile <name> add dsh-routing-suite`,并把包内的
@@ -300,16 +345,17 @@ dsh --profile tui --no-color
300
345
 
301
346
  | 键 | 作用 |
302
347
  | --- | --- |
303
- | `Enter` | 发送;运行中则插入指示;空输入且已选卡片时展开/收起。工具正文超出窗口则单独全览,`Esc` 返回 |
348
+ | `Enter` | 发送;运行中则插入指示;空输入且已选中卡片时展开/收起,选中的是回复则打开全文(`Esc` 返回)。工具正文超出窗口也单独全览 |
304
349
  | `1..9` / `Enter` | 回答 `ask_user_question` 提问:`1..9` 直接选,`Enter` 取当前高亮项(默认第一项),`Esc` 才取消 |
305
- | `↑` / `↓` | 空输入:在卡片间移动;有输入:历史(↓ 越过最新一条会回到当前草稿)。与 `Ctrl+N` / `Ctrl+P` 相同 |
306
- | `Ctrl+R` | 展开最新一条卡片;已用 ↑/↓ 选中时全部展开或全部收起 |
350
+ | `↑` / `↓` | 空输入且会话里已有卡片时:在当前屏幕顺序的最新一条(**回复或卡片**)开始往上走,标记 `▶` 只出现在选中的那一条;没有卡片时(纯问答、或刚 `/clear`)仍是有输入时的历史召回——那种会话用 `Alt+4` 或 `Ctrl+N`/`Ctrl+P` 选回复。有输入:历史(↓ 越过最新一条会回到当前草稿) |
351
+ | `Ctrl+R` | 展开最新一条卡片;已选中时全部展开或全部收起(选中的回复不会被抢走焦点,打开覆盖层时该键交给覆盖层) |
307
352
  | `Ctrl+T` | 折叠输入框(只影响显示) |
308
- | `Alt+1` / `2` / `3` / `4` | 跳到最新思考 / 计划 / 子代理 / 回复 |
309
- | `/find [类] 关键字` | 搜索并跳到该条完整消息(反色高亮)。类:`思考` `计划` `子代理` `回复` `提示词`。`Ctrl+/` 或 `Alt+/` 打开 |
353
+ | `Alt+1` / `2` / `3` / `4` | 跳到最新思考 / 计划 / 子代理 / 回复(回复会被选中,可直接 `/copy`) |
354
+ | `/find [类] 关键字` | 搜索并跳到该条完整消息(反色高亮),命中的那条同时成为选中项。类:`思考` `计划` `子代理` `回复` `提示词`。`Ctrl+/` 或 `Alt+/` 打开 |
310
355
  | `Ctrl+G` / `Alt+N` | 下一条搜索结果;`Alt+P` 上一条 |
311
- | `/copy` | 把焦点卡片纯文本写入本机剪贴板(无焦点则最近一条回复;支持 OSC 52 的终端/tmux) |
312
- | 鼠标左键 | 点击卡片标题展开/收起;点 markdown 链接则复制 URL |
356
+ | `/copy` | 把选中的卡片或回复按**原文**写入本机剪贴板(无选中则最近一条回复;支持 OSC 52 的终端/tmux)。复制后保留选中,连按两次拿到的是同一条。在「全文」覆盖层里按复制键(`Ctrl+Shift+C`)复制的是**屏幕上那份正文**——工具卡是工具全文、改动卡是那份 diff、回复是原文;覆盖层底部会回显「已复制 …(全文)」 |
357
+ | `/retryauth [on\|off]` | "提供商侧鉴权失败"(HTTP 401/403,且本机凭据已配)时要不要自动重试一次;默认关。判断口诀与排查三步见 [`docs/remote-ops.md`](docs/remote-ops.md) 4.8 |
358
+ | 鼠标左键 | 点击卡片标题展开/收起;点 markdown 链接则复制 URL;**拖过回复**按屏幕所见复制(不含选中标记 `▶`) |
313
359
  | `PgUp` / `PgDn`、滚轮 | 转录回看 |
314
360
  | `Esc` | 取消选择 → 回底部 → 取消当前轮次 |
315
361
  | `Ctrl+C` | 中断当前轮次;空闲连按两次退出 |
@@ -471,7 +517,7 @@ DeepSeek 上、B 会话跑在 xAI 上;只记软件级默认值的话,resume
471
517
  ## 验证
472
518
 
473
519
  ```bash
474
- bash scripts/verify.sh # 检查 profile 组合与 CLI 语法
520
+ node scripts/verify.mjs # 检查 profile 组合与 CLI 语法(bash scripts/verify.sh 等价)
475
521
  npm test # 单元 + 集成(含屏幕网格护栏、重连接管、选择器首帧)
476
522
  python3 scripts/pty-acceptance.py # 真 PTY:模拟 40ms SSH 链路 + 滞留的光标回复
477
523
  node scripts/tui-probe.mjs # 真 PTY:真 dsh --profile tui 走一遍启动/缩放//diag/打字//exit
@@ -509,8 +555,8 @@ PROBE_HOME=$H node scripts/tui-probe.mjs --session <id> # 只读式驱动已
509
555
  ## 卸载
510
556
 
511
557
  ```bash
512
- bash scripts/uninstall.sh # 默认 tui profile
513
- bash scripts/uninstall.sh work # 指定 profile
558
+ node scripts/uninstall.mjs # 默认 tui profile(bash scripts/uninstall.sh 等价)
559
+ node scripts/uninstall.mjs work # 指定 profile
514
560
  ```
515
561
 
516
562
  卸载只移除 profile 中的插件依赖与 bundle 层,不会删除会话数据。
@@ -568,10 +614,12 @@ npm run build
568
614
  ### 启动与安装
569
615
 
570
616
  - **`dsh-ssh-tui: both stdin and stdout must be TTYs`**:必须在真实终端 / SSH 会话里启动;管道、CI、`&` 后台都不行。
617
+ - **Windows 安装、脚本与 `/doctor` 的读法**:[docs/windows.md](docs/windows.md) 有 30 秒安装、
618
+ 「装不上先看什么」的对照表,以及 `/doctor` 报告里 `●` / `⚠` / `✖` 各代表什么。
571
619
  - **Windows 报 `host display socket did not appear`**:升级到最新版:
572
620
  `dsh plugin --profile tui add dsh-ssh-tui@latest`。仍失败请附 `/diag` 输出提 issue。
573
621
  - **Windows 上应用内更新报 `spawn dsh ENOENT`**:旧的更新器直接 `spawn dsh`,而 Windows 上装的是 `dsh.cmd` 垫片;
574
- 在命令行跑一次同样的升级即可(`dsh plugin --profile tui add dsh-ssh-tui@latest`),之后应用内更新正常。
622
+ 在命令行跑一次 `dsh plugin --profile tui add dsh-ssh-tui@latest`,之后应用内更新正常(它装的是查到的版本号,不再写 `@latest`)。
575
623
  - **pnpm 拒绝 git 依赖的构建脚本**:把 pnpm 打印的 key 加进 profile 的 `pnpm-workspace.yaml` 的 `allowBuilds`,再重装。
576
624
  - **升级后 `/mode` 报「服务不可用」、preset 工具消失**:敲 `/doctor`。它逐项判定部署组合(补丁能否解析、
577
625
  名单与 code-runtime 是否组合、有没有行被挂载两次、dsh 版本是否在兼容表内、是否装着两份 `@deepseek-ai/dsh-scope`),
@@ -597,11 +645,19 @@ npm run build
597
645
  - **中文 / emoji 挤压相邻字符**:换一款覆盖这些字形的等宽字体(如 Noto Sans Mono CJK)。程序按 2 格预算这些符号
598
646
  并请求文本字形;字体缺字时终端会回退到彩色 emoji 字形,视觉上仍可能偏宽。
599
647
  - **弱网下画面跟不上**:`DSH_TUI_PAINT_MS` 控制发画间隔(40–1000 ms,越小越快也越费流量);
600
- 不设时按启动测得的 RTT 自动取 80 / 160 / 250 / 400 ms。
648
+ 不设时按测得的 RTT 自动取 80 / 160 / 250 / 400 ms;链路每 20 秒重测(刚变化过就 5 秒后再测一次),
649
+ 取最近三次的中位数,所以接入那一刻的抖动不会把绘制档位钉在最慢一档。
650
+ - **模型“只思考一下就说完成”**:这是**上游返回了空回合**——有些网关把 Gemini/Claude 的 thought 部分映射成
651
+ `reasoning_content`,然后以 `finish_reason: stop` 收尾却没有任何正文,harness 如实组装成“只含思考的助手消息”,
652
+ 回合就合法结束了。TUI 现在会显式写一行「上游只返回了思考…这不是真正的完成(finish_reason: stop);按 Enter 或再输入一句即可继续」,
653
+ 不再让你以为它答完了。实测某网关(`google-ai-pro` / `gemini-3.8-flash-high`)约 **29%** 的回合如此(同会话其它回合正常)。
601
654
  - **要给屏幕阅读器或日志用**:`DSH_TUI_LINE_MODE=1` 启动纯行模式——只追加纯文本行、不发光标控制,可直接 `tee` 存档。
602
655
  - **标题栏 / 铃声不生效**:终端需支持 OSC 0 与 BEL;`DSH_TUI_NO_BELL=1` 可关闭铃声。
603
656
  - **深色终端下整行底色太抢眼**:`DSH_TUI_COLOR_DEPTH=none` 去掉底色,diff 仍用 `+`/`-` 区分。
604
657
 
658
+ - **框线、圆点显示成乱码**:控制台代码页不是 UTF-8。界面会自动改画 ASCII(横线 `-`、状态点 `*`、
659
+ 警告 `!`),`/diag` 的「终端」一行会注明;想要原字形就 `chcp 65001` 或改用 Windows Terminal。
660
+ `DSH_TUI_ASCII=1` 强制 ASCII,`=0` 强制 Unicode。详见 [docs/windows.md](docs/windows.md)。
605
661
  - **终端兼容性**:每个终端允许发什么、承诺什么(Windows Terminal / conhost / GNOME / XFCE / Konsole /
606
662
  xterm / tmux / screen / Linux 控制台)见 [docs/terminals.md](docs/terminals.md);
607
663
  判定依据写在 `/diag` 的「终端」一行,判定错了用 `DSH_TUI_TERM_CAPS` 覆盖。