dsh-ssh-tui 0.5.10 → 0.6.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.
- package/README.en.md +27 -4
- package/README.md +71 -6
- package/lib/attach.js +156 -0
- package/lib/attach.js.map +1 -0
- package/lib/commands.js +73 -0
- package/lib/commands.js.map +1 -0
- package/lib/diag.js +292 -0
- package/lib/diag.js.map +1 -0
- package/lib/dialogs.js +82 -0
- package/lib/dialogs.js.map +1 -0
- package/lib/display-sock.js +276 -96
- package/lib/display-sock.js.map +1 -1
- package/lib/dsh-compat.js +25 -4
- package/lib/dsh-compat.js.map +1 -1
- package/lib/i18n/en.js +55 -0
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +2 -0
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +55 -0
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +60 -100
- package/lib/index.js.map +1 -1
- package/lib/launcher-exit.js +41 -0
- package/lib/launcher-exit.js.map +1 -0
- package/lib/paint.js +20 -56
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +152 -37
- package/lib/picker.js.map +1 -1
- package/lib/rows.js +120 -0
- package/lib/rows.js.map +1 -0
- package/lib/session-index.js +4 -1
- package/lib/session-index.js.map +1 -1
- package/lib/session-list.js +372 -167
- package/lib/session-list.js.map +1 -1
- package/lib/session-lock.js +20 -9
- package/lib/session-lock.js.map +1 -1
- package/lib/stats.js +136 -0
- package/lib/stats.js.map +1 -0
- package/lib/terminal-input.js +474 -0
- package/lib/terminal-input.js.map +1 -0
- package/lib/tui.js +252 -311
- package/lib/tui.js.map +1 -1
- package/lib/types/attach.d.ts +106 -0
- package/lib/types/commands.d.ts +103 -0
- package/lib/types/diag.d.ts +78 -0
- package/lib/types/dialogs.d.ts +79 -0
- package/lib/types/display-sock.d.ts +49 -3
- package/lib/types/dsh-compat.d.ts +7 -0
- package/lib/types/i18n/index.d.ts +4 -0
- package/lib/types/index.d.ts +2 -4
- package/lib/types/launcher-exit.d.ts +20 -0
- package/lib/types/paint.d.ts +6 -5
- package/lib/types/picker.d.ts +27 -7
- package/lib/types/rows.d.ts +69 -0
- package/lib/types/session-list.d.ts +43 -3
- package/lib/types/stats.d.ts +98 -0
- package/lib/types/terminal-input.d.ts +164 -0
- package/lib/types/tui.d.ts +33 -8
- package/package.json +11 -5
package/README.en.md
CHANGED
|
@@ -126,8 +126,15 @@ A busy hangup pauses the turn by default (cancelled). After attach, send another
|
|
|
126
126
|
message to continue. `/disconnect continue`, `ssh-tui.disconnect: continue`,
|
|
127
127
|
or `DSH_TUI_DISCONNECT=continue` leaves the turn running in the background;
|
|
128
128
|
approvals and questions wait until a Display attaches. Idle hangup exits
|
|
129
|
-
immediately.
|
|
130
|
-
|
|
129
|
+
immediately. A leftover Host holds the session's kernel write lock
|
|
130
|
+
(`session.lock`), which is exactly what makes the Web UI refuse the same session
|
|
131
|
+
(`resume failed for session … is already owned by an active write handle`), so
|
|
132
|
+
once the turn it stayed for has finished it waits at most one more minute
|
|
133
|
+
(`DSH_TUI_IDLE_EXIT_MS`, or `ssh-tui.idleExit` in settings.yaml, in
|
|
134
|
+
milliseconds; `0`/`off` restores the old behavior) and then exits, handing the
|
|
135
|
+
lock back — long enough for the old window to reattach, after which `--resume`
|
|
136
|
+
reopens the flushed log. A Host that never finishes its turn still falls back to
|
|
137
|
+
`DSH_TUI_DETACHED_IDLE_MS` (6h). Optional: wrap the TUI in tmux.
|
|
131
138
|
|
|
132
139
|
New sessions inherit the directory you launched from. Resuming a session
|
|
133
140
|
`chdir`s into that session's recorded working directory. The footer shows
|
|
@@ -250,6 +257,15 @@ The plugin runs on Linux, macOS, and Windows (Node ≥ 22.19):
|
|
|
250
257
|
events on 0.1.2 hosts, or from the packed stream inside `assistant/message`
|
|
251
258
|
on 0.1.5. Steps with no usable timing fall back to `首字 1.2s`.
|
|
252
259
|
- Reconnect: channel readiness is a real connect, so a `.sock` file left behind by a killed Host is no longer listed as attachable (it used to fail the first attach with `write EPIPE`); a Host still cancelling/flushing keeps itself alive when a relay HELLOs mid-hangup; a launcher that hits a vanished peer retries once automatically — silently, with the TTY kept in raw mode and queued bytes dropped so a stale cursor reply is never echoed as `^[[17;1R`; and a probe that misses its window is retried while the Host keeps the last measurement, so the footer chip does not fall back to four hollow circles.
|
|
260
|
+
- Reporting a problem: `/diag` prints a local, read-only snapshot — plugin/dsh/node
|
|
261
|
+
versions, platform, session id, whether this process is the launcher or the detached
|
|
262
|
+
Host, `DSH_HOME`, the display channel address and whether it answers (including the
|
|
263
|
+
leftover-socket-file verdict), the Host pid/identity/lock state, the lock file path, link
|
|
264
|
+
RTT and paint interval, the session log format and size, other locks on the machine, and
|
|
265
|
+
a **verdict chain** ("attaches to the leftover Host (pid N), do not open a second
|
|
266
|
+
window", "the pid was recycled", "a leftover socket file that does not answer — the
|
|
267
|
+
source of the first-attach EPIPE"). Nothing leaves the machine; paste it into an issue.
|
|
268
|
+
- Leftover Host lifetime: the write lock a dropped-but-still-running Host holds is what makes the Web UI refuse the same session (`resume failed for session … is already owned by an active write handle`), and a drop that raced a reattach used to leave the honored-reattach flag set, so the *next* drop was ignored and the session lock kept its stale state. The flag is cleared once a turn runs again, and once the turn the Host stayed for settles it exits within `DSH_TUI_IDLE_EXIT_MS` (default 60s, `0` disables) instead of holding the session for six hours.
|
|
253
269
|
- Subagent route: the identity row always carries `sub:<model>` — the route
|
|
254
270
|
every child inherits — suffixed with the effort when `/subeffort` set one,
|
|
255
271
|
and prefixed with the provider when `settings.yaml` pinned one
|
|
@@ -488,8 +504,11 @@ on-screen item (`0` starts a new session). `↑`/`↓` (or `Ctrl+P`/`Ctrl+N`)
|
|
|
488
504
|
move the highlight; `Enter` resumes the focused row. Typing (or `/` /
|
|
489
505
|
`Ctrl+F`) filters by title, session id, or cwd; `PgUp`/`PgDn` page; `Esc`
|
|
490
506
|
first leaves the filter, then cancels. The history list itself is not
|
|
491
|
-
capped.
|
|
492
|
-
|
|
507
|
+
capped. Reading is lazy: the first nine sessions are inspected and painted
|
|
508
|
+
only once their titles are known — no raw id is ever shown and then
|
|
509
|
+
replaced — and older sessions are read when you reach for them (press
|
|
510
|
+
`↓`/`PgDn`/`End` past the last row, or filter, which looks deeper on its
|
|
511
|
+
own). Labels are cached in `$DSH_HOME/tui-session-index.json`.
|
|
493
512
|
New and resume launches paint a splash immediately; the frontend relay
|
|
494
513
|
spawns the Host without waiting for the plugin loader. Resuming a session
|
|
495
514
|
skips token chunks and lays out only the visible tail on the first paint.
|
|
@@ -512,6 +531,10 @@ process. Do not start a second Host.
|
|
|
512
531
|
src/picker.ts launch history picker (9-row page, uncapped list, filter)
|
|
513
532
|
src/tui.ts SshTui (re-exports leaf helpers)
|
|
514
533
|
src/paint.ts incremental paint, SSH cadence, picker window
|
|
534
|
+
src/stats.ts session stats ledger (turns/steps, LLM and tool time, TTFT, decode counts)
|
|
535
|
+
src/rows.ts transcript row operations and the visible window (the row array stays in tui.ts)
|
|
536
|
+
src/dialogs.ts dialog key rules (questions, confirm, inspect overlay)
|
|
537
|
+
src/commands.ts slash-command catalog and suggestions
|
|
515
538
|
src/auto-approval.ts rule-table first pass
|
|
516
539
|
src/approval-reviewer.ts AI review prompt and JSON parse
|
|
517
540
|
src/i18n/ zh/en UI catalogs
|
package/README.md
CHANGED
|
@@ -141,10 +141,27 @@ dsh --profile tui --resume <session-id> # 有活进程则接入,否则从
|
|
|
141
141
|
`$DSH_HOME/tui-locks/`,显示通道在 `$DSH_HOME/tui-socks/`。进程死后残留锁会在
|
|
142
142
|
下次启动时核对 pid,已死则自动从日志接管。调试可设 `DSH_TUI_NO_SESSION_LOCK=1`。
|
|
143
143
|
|
|
144
|
+
一个会话同时只有一块屏幕:新窗口接入时 Host 会通知旧窗口「你已被接管」(`FRAME_REPLACED`),
|
|
145
|
+
旧窗口退出,不会两个窗口互相抢显示。
|
|
146
|
+
|
|
147
|
+
链路探测(`CSI 6n`,终端回 `CSI row;col R`)做了三层防护:Host 收到 HELLO 之前就先测(不把
|
|
148
|
+
整屏重绘的时间算成链路);每次重问前等链路安静**一整个应答窗口**(350ms,超时后放宽到
|
|
149
|
+
800ms),这样「上一个请求的回复」必然已经落地并被丢掉;采样取中位数,并丢掉比中位数快 4
|
|
150
|
+
倍以上的(那是别人请求的回复,相对判定所以 `ssh localhost` 的 2ms 链路照样算得出来)。
|
|
151
|
+
代价是每次接入多约 0.4–0.6 秒探测时间,换来的是 50ms 链路不再出现 2ms / 1900ms 的跳变。
|
|
152
|
+
|
|
153
|
+
这些回复也不可能再进输入框:relay 整条 stdin 管道常驻过滤(含跨 read 拆分的),Host 键
|
|
154
|
+
处理前再过滤一次;连 Host 启动那几百毫秒里敲的键也会被暂存、接入后补发,不再被丢掉。
|
|
155
|
+
`DSH_TUI_DEBUG=1` 时会打印每次采样与丢弃原因。
|
|
156
|
+
|
|
144
157
|
忙碌时默认断线会暂停当前轮次(取消),接上后再发一句才会继续。`/disconnect continue` 或
|
|
145
158
|
`ssh-tui.disconnect: continue`(也可用 `DSH_TUI_DISCONNECT=continue`)则不取消,
|
|
146
159
|
Host 在后台跑完这一轮;审批和提问等接上后再弹。空闲断线直接退出,不占后台。
|
|
147
|
-
|
|
160
|
+
留下的 Host 持有该会话的内核写锁(`session.lock`),而 Web 端打开同一会话时正是被这把锁挡下的
|
|
161
|
+
(`resume failed for session … is already owned by an active write handle`)。所以它**跑完留下来的那一轮后最多再等 1 分钟**
|
|
162
|
+
(`DSH_TUI_IDLE_EXIT_MS`,或 settings.yaml 的 `ssh-tui.idleExit`,毫秒;设 `0`/`off` 恢复旧行为)
|
|
163
|
+
就自行退出并让出锁:这段时间够原窗口重连接入,之后 `--resume` 重新打开已落盘的日志。
|
|
164
|
+
完全没有显示器且一直空闲的兜底仍由 `DSH_TUI_DETACHED_IDLE_MS`(默认 6 小时)负责。可选:用 tmux 包一层。
|
|
148
165
|
|
|
149
166
|
启动时若 npm 上有更新,会弹出选单(类似 Codex / Claude Code 首启):**现在更新 / 稍后 / 跳过此版本**。选「现在更新」会运行 `dsh plugin --profile tui add dsh-ssh-tui@latest`,完成后提示退出再启动。`DSH_TUI_NO_UPDATE_CHECK=1` 可关掉。`/status` 里也能看到当前插件版本、链路芯片、额度窗口,以及子代理模型是否与父路由同族。
|
|
150
167
|
|
|
@@ -222,7 +239,7 @@ dsh --profile tui --provider <id>
|
|
|
222
239
|
dsh --profile tui --no-color
|
|
223
240
|
```
|
|
224
241
|
|
|
225
|
-
选择器操作:一页固定 9 条,空筛选时 `1-9` 对应屏幕上每一项,`0` 新建。`↑`/`↓`(或 `Ctrl+P`/`Ctrl+N`)移动高亮,`Enter` 恢复当前项。输入文字(或 `/` / `Ctrl+F`)按标题、会话 ID、工作目录筛选;`PgUp`/`PgDn` 翻页,`Esc`
|
|
242
|
+
选择器操作:一页固定 9 条,空筛选时 `1-9` 对应屏幕上每一项,`0` 新建。`↑`/`↓`(或 `Ctrl+P`/`Ctrl+N`)移动高亮,`Enter` 恢复当前项。输入文字(或 `/` / `Ctrl+F`)按标题、会话 ID、工作目录筛选;`PgUp`/`PgDn` 翻页,`Esc` 先退出筛选再取消。历史列表本身不截断。**读取是懒加载的**:首批固定 9 条,全部拿到标题后才上屏(屏幕上不会出现先显示会话 id、过一会儿又变成标题的行);更早的会话只有你去够的时候才读——在最后一条继续按 `↓`/`PgDn`,或按 `End`,或输入筛选(筛选会自己往更早的历史里找)。标签缓存在 `$DSH_HOME/tui-session-index.json`。新建/恢复会立刻画出启动屏,前端不再等插件加载完才拉 Host。恢复历史会话时跳过 token chunk,首屏只排可见尾部。
|
|
226
243
|
|
|
227
244
|
## 交互与快捷键
|
|
228
245
|
|
|
@@ -246,7 +263,7 @@ dsh --profile tui --no-color
|
|
|
246
263
|
`/mode` 切换官方 preset:标准 (`standard`)、PTC (`ptc`;dsh 0.1.1 上仍是 `code`)、极简 (`minimal`)、创造 (`cordis`),以及本地安装的其它模式。
|
|
247
264
|
|
|
248
265
|
斜杠命令:`/help`、`/find`、`/copy`、`/model`、`/effort`、`/provider`、`/language`(`/lang`)、`/view`、`/disconnect`、`/approval`(`auto` / `off` / `status`)、`/submodel`、`/subeffort`、`/mode`、`/resume`、
|
|
249
|
-
`/status`、`/subagents`、`/usage`(`/balance`、`/quota` 同义)、`/setup`、`/clear`,
|
|
266
|
+
`/status`、`/diag`、`/subagents`、`/usage`(`/balance`、`/quota` 同义)、`/setup`、`/clear`,
|
|
250
267
|
界面语言:`/language` 打开选择器,或 `/language zh` / `/language en` 直接切。优先 `DSH_TUI_LANG`,其次 `$DSH_HOME/settings.yaml` 的 `ssh-tui.language`,再跟 `LANG`/`LC_MESSAGES`。未知和 `C` locale 默认中文。
|
|
251
268
|
工作区视图:`/view` 在 **详细**(默认,看见思考和单条工具)和 **极简** 之间切换,写入
|
|
252
269
|
`ssh-tui.view`。极简对齐 Codex:藏思考,按「回复 → 已调用 N 个工具 → 已编辑 N 个文件 →
|
|
@@ -346,6 +363,9 @@ web 端与 TUI 共用同一份设置。`/model` 换提供商后**下一步请求
|
|
|
346
363
|
|
|
347
364
|
```bash
|
|
348
365
|
bash scripts/verify.sh # 检查 profile 组合与 CLI 语法
|
|
366
|
+
npm test # 单元 + 集成(含屏幕网格护栏、重连接管、选择器首帧)
|
|
367
|
+
python3 scripts/pty-acceptance.py # 真 PTY:模拟 40ms SSH 链路 + 滞留的光标回复
|
|
368
|
+
node scripts/tui-probe.mjs # 真 PTY:真 dsh --profile tui 走一遍启动/缩放//diag/打字//exit
|
|
349
369
|
```
|
|
350
370
|
|
|
351
371
|
或手动:
|
|
@@ -355,6 +375,28 @@ dsh --profile tui --dump-config | grep -A12 'id: ssh-tui'
|
|
|
355
375
|
dsh --profile tui --help
|
|
356
376
|
```
|
|
357
377
|
|
|
378
|
+
`pty-acceptance.py` 用真 PTY 跑一遍接入:终端像 SSH 客户端那样隔一个 RTT 才回
|
|
379
|
+
`CSI 6n`,并且有两条「上一个 launcher 发出、仍在路上」的滞留回复(一条已在队列里,一条
|
|
380
|
+
落在探测窗口中间),按键在探测还没结束时就敲下去。脚本检查输入是否原样(且只送一次)
|
|
381
|
+
送达 Host、测得的 RTT 是否接近模拟值、屏幕上有没有被回显的 `^[[17;1R`。第一个参数可改
|
|
382
|
+
模拟延迟(秒):`python3 scripts/pty-acceptance.py 0.12`。
|
|
383
|
+
|
|
384
|
+
`tui-probe.mjs` 直接用真 PTY 驱动 `dsh --profile tui`:等启动横幅与空闲状态、缩放窗口后
|
|
385
|
+
断言刚才那一帧不是已结束的选择器的重绘、跑 `/diag` 并检查判定链、打字是否上屏、`/exit`
|
|
386
|
+
是否把终端(含备用屏)交还。`npm test` 里的屏幕级用例用 `@xterm/headless` 断言**屏幕网格**
|
|
387
|
+
(残留行、越界寻址、光标越界、缩放风暴、选择器交还备用屏、footer 链路芯片),探针补的是
|
|
388
|
+
只在真宿主上才存在的部分。跑探针请用一次性 home,避免碰到真实会话:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
H=$(mktemp -d); mkdir -p "$H/sessions" "$H/tui-locks" "$H/tui-socks"
|
|
392
|
+
ln -s ~/.dsh/profiles "$H/profiles"; cp ~/.dsh/settings.yaml ~/.dsh/.credentials.yaml "$H/"
|
|
393
|
+
PROBE_HOME=$H node scripts/tui-probe.mjs # 自建会话,退出时删掉
|
|
394
|
+
PROBE_HOME=$H node scripts/tui-probe.mjs --session <id> # 只读式驱动已存在会话,绝不删除
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
探针拒绝在 `sessions/`、`tui-locks/`、`tui-socks/` 分居两处(含符号链接)的 home 上运行——
|
|
398
|
+
那种布局会让 Host 持有真实会话的写锁却对用户的选择器不可见(2026-09-11 事故形态)。
|
|
399
|
+
|
|
358
400
|
## 卸载
|
|
359
401
|
|
|
360
402
|
```bash
|
|
@@ -383,10 +425,14 @@ src/picker.ts 启动历史会话选择器(可见页 9 条,列表不截
|
|
|
383
425
|
src/session-list.ts 历史会话扫描与标签(共享给 /resume)
|
|
384
426
|
src/session-lock.ts 同会话防双开
|
|
385
427
|
src/update-check.ts npm 最新版提示(不自动升级)
|
|
386
|
-
src/tui.ts
|
|
428
|
+
src/tui.ts 终端渲染、交互、标题/铃声(SshTui;叶子函数再导出)
|
|
387
429
|
src/paint.ts 增量绘制、SSH 节拍、选择器窗口
|
|
388
430
|
src/term-text.ts 宽度/折行/markdown
|
|
389
431
|
src/footer.ts 底栏、占用环、/status
|
|
432
|
+
src/stats.ts 会话统计账本(回合/步数、LLM 与工具耗时、TTFT、decode 计数、用量去重)
|
|
433
|
+
src/rows.ts 转录行操作与可见窗口(内存上限、工具卡合并、计划行、滚动切片;行数组仍在 tui.ts)
|
|
434
|
+
src/dialogs.ts 对话框状态机(问题列表/确认/查看覆盖层的按键规则)
|
|
435
|
+
src/commands.ts 斜杠命令目录与补全建议
|
|
390
436
|
src/plan.ts 计划条、待办、/find
|
|
391
437
|
src/tool-present.ts 工具卡、diff
|
|
392
438
|
src/auto-approval.ts 规则初审
|
|
@@ -436,8 +482,27 @@ npm run build
|
|
|
436
482
|
同时重连时的整屏重绘会让终端的回复错过探测窗口,Host 收到"未知"就把已有测量擦成 `SSH ○○○○`。
|
|
437
483
|
现在:自动重试默认**静默**(`DSH_TUI_DEBUG=1` 才打印);重试/等 Host 启动期间保持 raw 模式并丢弃排队字节,
|
|
438
484
|
不再回显;探测失败会自动补测一次,Host 侧也不会用"未知"覆盖已知测量。
|
|
439
|
-
- **resume
|
|
440
|
-
|
|
485
|
+
- **resume 选择器第一批列表出现不认识的会话 / 会话一多就要等很久**:早期版本第一轮画的是"骨架"
|
|
486
|
+
(活着的 Host 只拿会话 id 当标题),而为了不闪出裸 id 又得等标题;等于把一次全量扫描的时间摆在用户面前。
|
|
487
|
+
现在首批固定 9 条、**全部读到标题后才上屏**(期间显示「正在读取历史会话…」,屏幕上不会出现先裸 id、
|
|
488
|
+
一秒后变标题的行),更早的会话按需读取:在最后一条继续按 `↓`/`PgDn`、按 `End`,或直接输入筛选
|
|
489
|
+
(筛选会自己往更早历史里找,凑够一页就停)。计数行会显示还有多少条未加载。
|
|
490
|
+
- **要给支持者一份可读的排障信息**:在会话里敲 `/diag`。它只读本地信息、不外传,输出
|
|
491
|
+
插件/dsh/node 版本、平台、会话 id、当前进程是启动器还是后台 Host、`DSH_HOME`、显示通道地址与
|
|
492
|
+
可连接性(含"残留 socket 文件"判定)、Host 的 pid/身份核对/锁状态/agent 状态、锁文件路径、
|
|
493
|
+
链路 RTT 与绘制间隔、会话日志格式与大小、本机其它会话的锁,最后是**判定链**——例如
|
|
494
|
+
"会接入后台 Host(pid N),不要另起第二个窗口"、"pid 被回收成别的程序"、
|
|
495
|
+
"通道上留着不响应的 socket 文件,这正是第一次接 EPIPE 的来源"。报 issue 时贴这段即可。
|
|
496
|
+
- **TUI 里进得去、Web 里打不开同一个会话**:会话的写锁(`session.lock`)是内核锁,同时只允许一个写者。
|
|
497
|
+
SSH 断开时那个还在跑轮的 Host 会留着锁,于是 Web 端打开时报
|
|
498
|
+
`resume failed for session "…" is already owned by an active write handle`——从 0.5.10 起
|
|
499
|
+
该 Host 在轮次结束后最多再等 1 分钟就退出并放锁(`DSH_TUI_IDLE_EXIT_MS`/`ssh-tui.idleExit`)。
|
|
500
|
+
若锁仍被某个 Host 占着,`--resume` 接入它即可正常继续(选择器里那条会话会标「可接入」)。
|
|
501
|
+
- **断线重连后锁被标成「已暂停」/「接入中」,但窗口其实已经关了**:0.5.10 修掉了一个状态机 bug。
|
|
502
|
+
重连接的 relay 在 hangup 收尾期间 HELLO 后,Host 会记下"这次 hangup 要采纳它"的标记;但 hangup
|
|
503
|
+
走保留 Host 分支时没有清掉这个标记,于是**下一次真实掉线整个被忽略**:锁仍写着上一个状态
|
|
504
|
+
(例如「已暂停」),空闲退出计时器也不会武装。现在只要轮次重新开始(说明那场竞争早已结束),
|
|
505
|
+
标记就会归零,掉线、锁状态与空闲退出都以真实事件为准。
|
|
441
506
|
- **状态栏看不到子代理模型**:0.3.6 起 `sub:<模型>` 只在子代理模型与父模型不同时才拼进身份行,
|
|
442
507
|
而默认子代理模型就是 `deepseek-v4-flash`,父模型也是它时整段消失;0.5.8 起恢复为始终显示,
|
|
443
508
|
并找回提供商前缀与 `/subeffort` 的括号后缀(提供商要先在 `settings.yaml` 的 `ssh-tui-subagent.provider`
|
package/lib/attach.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launcher's attach/reconnect state machine, kept out of the bundle entry
|
|
3
|
+
* so it can be driven with fakes.
|
|
4
|
+
*
|
|
5
|
+
* This is the code that decides whether a launcher keeps fighting for a
|
|
6
|
+
* session's display. It used to answer a bare `close` from the Host with a
|
|
7
|
+
* retry — and since the Host answers a *replacement* by closing the previous
|
|
8
|
+
* relay, two SSH windows kicked each other off the display in a loop: every
|
|
9
|
+
* lap repainted the whole screen, every lap left the TTY cooked long enough to
|
|
10
|
+
* echo the DSR replies still in flight as `^[[17;1R`, and neither window could
|
|
11
|
+
* be typed into. `replaced` is now an exit, and the burst breaker below stops
|
|
12
|
+
* any leftover launcher from an older release that would ignore it.
|
|
13
|
+
*/
|
|
14
|
+
/** A relay that dies this soon after connecting reached a Host that was leaving. */
|
|
15
|
+
export const ATTACH_RECOVERY_WINDOW_MS = 5_000;
|
|
16
|
+
/** How long to let a mid-dispose Host finish before starting a fresh one. */
|
|
17
|
+
export const ATTACH_RECOVERY_WAIT_MS = 3_000;
|
|
18
|
+
/** Automatic re-attaches allowed inside this window before we stop trying. */
|
|
19
|
+
export const RECOVERY_BURST_WINDOW_MS = 10_000;
|
|
20
|
+
export const RECOVERY_BURST_LIMIT = 3;
|
|
21
|
+
/** How long to wait for a freshly spawned Host to accept on its channel. */
|
|
22
|
+
export const HOST_START_TIMEOUT_MS = 15_000;
|
|
23
|
+
/** Pause between "is the old Host gone yet" checks while recovering. */
|
|
24
|
+
const RECOVERY_POLL_MS = 150;
|
|
25
|
+
/** True when a relay error means the peer vanished rather than a real fault. */
|
|
26
|
+
export function attachPeerVanished(error, elapsedMs) {
|
|
27
|
+
if (elapsedMs >= ATTACH_RECOVERY_WINDOW_MS)
|
|
28
|
+
return false;
|
|
29
|
+
const code = error?.code;
|
|
30
|
+
if (code === undefined)
|
|
31
|
+
return false;
|
|
32
|
+
return code === 'EPIPE' || code === 'ECONNRESET' || code === 'ECONNREFUSED' || code === 'ERR_STREAM_DESTROYED';
|
|
33
|
+
}
|
|
34
|
+
export function createAttacher(deps) {
|
|
35
|
+
const now = deps.now ?? (() => Date.now());
|
|
36
|
+
const windowMs = deps.burst?.windowMs ?? RECOVERY_BURST_WINDOW_MS;
|
|
37
|
+
const limit = deps.burst?.limit ?? RECOVERY_BURST_LIMIT;
|
|
38
|
+
const recoveryWindow = [];
|
|
39
|
+
const recoveryAllowed = () => {
|
|
40
|
+
const current = now();
|
|
41
|
+
while (recoveryWindow.length > 0 && current - (recoveryWindow[0] ?? current) >= windowMs) {
|
|
42
|
+
recoveryWindow.shift();
|
|
43
|
+
}
|
|
44
|
+
if (recoveryWindow.length >= limit)
|
|
45
|
+
return false;
|
|
46
|
+
recoveryWindow.push(current);
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
const spawnHostAndRelay = async (sessionId, recover) => {
|
|
50
|
+
const live = deps.locksDisabled?.() === true ? undefined : await deps.inspectLiveHost(sessionId);
|
|
51
|
+
if (live?.kind === 'attachable') {
|
|
52
|
+
// The user asked for this session and is waiting: say so.
|
|
53
|
+
await attachExisting(sessionId, live.sock, recover, { announce: true });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (live?.kind === 'zombie') {
|
|
57
|
+
throw new Error(deps.messages.zombie(sessionId, live.pid));
|
|
58
|
+
}
|
|
59
|
+
const spawned = deps.spawnHost(sessionId);
|
|
60
|
+
// The Host boots with the TTY in cooked mode: quiet it first so a reply
|
|
61
|
+
// still in flight cannot be echoed over the boot splash, and keep whatever
|
|
62
|
+
// the user types meanwhile instead of dropping it.
|
|
63
|
+
deps.beginCapture?.();
|
|
64
|
+
try {
|
|
65
|
+
await deps.waitForDisplaySock(spawned);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
spawned.exitWatch.dispose();
|
|
69
|
+
}
|
|
70
|
+
const seed = deps.endCapture?.() ?? '';
|
|
71
|
+
await attachExisting(sessionId, spawned.sock, recover, { announce: true, seed });
|
|
72
|
+
};
|
|
73
|
+
/** Wait out a Host that was mid-dispose, then take the normal path again. */
|
|
74
|
+
const recoverAttach = async (sessionId) => {
|
|
75
|
+
if (!recoveryAllowed())
|
|
76
|
+
throw new Error(deps.messages.flapping(sessionId));
|
|
77
|
+
if (deps.debug === true)
|
|
78
|
+
deps.report(deps.messages.recovering(sessionId));
|
|
79
|
+
// Between attempts the TTY is back in cooked mode: without this, a cursor
|
|
80
|
+
// reply still owed to the relay that just ended is echoed as `^[[17;1R`
|
|
81
|
+
// for the whole (up to 3s) wait.
|
|
82
|
+
deps.quiet();
|
|
83
|
+
const deadline = now() + ATTACH_RECOVERY_WAIT_MS;
|
|
84
|
+
for (;;) {
|
|
85
|
+
const live = deps.locksDisabled?.() === true ? undefined : await deps.inspectLiveHost(sessionId);
|
|
86
|
+
if (live === undefined || now() >= deadline)
|
|
87
|
+
break;
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, RECOVERY_POLL_MS));
|
|
89
|
+
}
|
|
90
|
+
await spawnHostAndRelay(sessionId, false);
|
|
91
|
+
};
|
|
92
|
+
const attachExisting = async (sessionId, sock, recover = true, options = {}) => {
|
|
93
|
+
const announce = options.announce === true;
|
|
94
|
+
const seed = options.seed ?? '';
|
|
95
|
+
// The status line is for a user who is waiting on their own terminal (boot
|
|
96
|
+
// and picker attaches). Automatic retries and takeovers stay silent: their
|
|
97
|
+
// terminal is usually a *dead* link, and anything written there sits in the
|
|
98
|
+
// connection until that link comes back — the stray text the user sees when
|
|
99
|
+
// they resume in a new window. `DSH_TUI_DEBUG=1` prints it either way.
|
|
100
|
+
if (announce || deps.debug === true)
|
|
101
|
+
deps.report(deps.messages.connecting(sessionId), announce);
|
|
102
|
+
// Always before a relay: the previous one restored cooked mode on its way
|
|
103
|
+
// out, and a cursor reply still in flight is *echoed* there as `^[[17;1R`
|
|
104
|
+
// over the screen. The error path below used to skip this and leave the
|
|
105
|
+
// TTY echoing for the whole recovery wait.
|
|
106
|
+
deps.quiet();
|
|
107
|
+
const startedAt = now();
|
|
108
|
+
let result;
|
|
109
|
+
try {
|
|
110
|
+
result = await deps.relay(sock, seed, announce);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (!recover || !attachPeerVanished(error, now() - startedAt))
|
|
114
|
+
throw error;
|
|
115
|
+
await recoverAttach(sessionId);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (result.reason === 'replaced') {
|
|
119
|
+
// A newer Display claimed this session — the user opened another window
|
|
120
|
+
// on it, or this launcher outlived an SSH drop. Exit instead of
|
|
121
|
+
// re-attaching: that retry is what started the fight.
|
|
122
|
+
//
|
|
123
|
+
// No message either. This window no longer owns any screen, and the link
|
|
124
|
+
// is usually the dead one (that is why the user is resuming elsewhere):
|
|
125
|
+
// anything written here sits in the connection and is flushed onto that
|
|
126
|
+
// terminal when it comes back — the leak the user sees on entry. The
|
|
127
|
+
// takeover is already visible where it matters, in the new window.
|
|
128
|
+
if (deps.debug === true)
|
|
129
|
+
deps.report(deps.messages.replaced(sessionId));
|
|
130
|
+
// Swallow whatever the probe is still owed. This path exits right after
|
|
131
|
+
// the RTT measurement, so the terminal's cursor replies can still be in
|
|
132
|
+
// flight; a launcher that exits with them queued hands the TTY back to
|
|
133
|
+
// the shell, which echoes them as `^[[17;1R` — the garbage the user sees
|
|
134
|
+
// on top of the shell prompt right after a launch.
|
|
135
|
+
deps.quiet();
|
|
136
|
+
deps.exit(0);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (recover && result.reason === 'host-closed' && now() - startedAt < ATTACH_RECOVERY_WINDOW_MS) {
|
|
140
|
+
// Accepted, then closed with no goodbye: the Host we reached was on its
|
|
141
|
+
// way out. A second manual attempt used to be the only way in.
|
|
142
|
+
await recoverAttach(sessionId);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
deps.quiet();
|
|
146
|
+
deps.exit(0);
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
attachExisting,
|
|
150
|
+
attachOrSpawn: (sessionId, recover = true) => spawnHostAndRelay(sessionId, recover),
|
|
151
|
+
get recoveries() {
|
|
152
|
+
return recoveryWindow.length;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=attach.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.js","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"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"}
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slash-command catalog the input box suggests: this plugin's own commands
|
|
3
|
+
* plus the host's, merged and ranked the way the suggestion list shows them.
|
|
4
|
+
*/
|
|
5
|
+
import { t } from './i18n/index.js';
|
|
6
|
+
/** This plugin's own commands, in suggestion order. */
|
|
7
|
+
export const LOCAL_COMMANDS = [
|
|
8
|
+
{ name: 'help', key: 'cmd.help' },
|
|
9
|
+
{ name: 'model', key: 'cmd.model' },
|
|
10
|
+
{ name: 'effort', key: 'cmd.effort' },
|
|
11
|
+
{ name: 'provider', key: 'cmd.provider' },
|
|
12
|
+
{ name: 'submodel', key: 'cmd.submodel' },
|
|
13
|
+
{ name: 'subeffort', key: 'cmd.subeffort' },
|
|
14
|
+
{ name: 'mode', key: 'cmd.mode' },
|
|
15
|
+
{ name: 'quit', key: 'cmd.quit' },
|
|
16
|
+
{ name: 'exit', key: 'cmd.quit', aliasOf: 'quit' },
|
|
17
|
+
{ name: 'clear', key: 'cmd.clear' },
|
|
18
|
+
{ name: 'status', key: 'cmd.status' },
|
|
19
|
+
{ name: 'diag', key: 'cmd.diag' },
|
|
20
|
+
{ name: 'disconnect', key: 'cmd.disconnect' },
|
|
21
|
+
{ name: 'approval', key: 'cmd.approval' },
|
|
22
|
+
{ name: 'view', key: 'cmd.view' },
|
|
23
|
+
{ name: 'usage', key: 'cmd.usage' },
|
|
24
|
+
{ name: 'balance', key: 'cmd.usage', aliasOf: 'usage' },
|
|
25
|
+
{ name: 'quota', key: 'cmd.usage', aliasOf: 'usage' },
|
|
26
|
+
{ name: 'subagents', key: 'cmd.subagents' },
|
|
27
|
+
{ name: 'resume', key: 'cmd.resume' },
|
|
28
|
+
{ name: 'setup', key: 'cmd.setup' },
|
|
29
|
+
{ name: 'find', key: 'cmd.find' },
|
|
30
|
+
{ name: 'copy', key: 'cmd.copy' },
|
|
31
|
+
{ name: 'language', key: 'cmd.language' },
|
|
32
|
+
{ name: 'lang', key: 'cmd.language', aliasOf: 'language' },
|
|
33
|
+
{ name: 'dialog-test', key: 'cmd.dialog-test' },
|
|
34
|
+
];
|
|
35
|
+
export function commandDescription(name, aliasOf) {
|
|
36
|
+
if (aliasOf !== undefined)
|
|
37
|
+
return t('cmd.aliasOf', { name: aliasOf });
|
|
38
|
+
return t(`cmd.${name}`);
|
|
39
|
+
}
|
|
40
|
+
export function localizedCommands() {
|
|
41
|
+
return LOCAL_COMMANDS.map(command => ({
|
|
42
|
+
name: command.name,
|
|
43
|
+
description: commandDescription(command.name, 'aliasOf' in command ? command.aliasOf : undefined),
|
|
44
|
+
...('aliasOf' in command ? { aliasOf: command.aliasOf } : {}),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Suggestions for the current input: only a `/`-prefixed line suggests
|
|
49
|
+
* anything, aliases are held back until a prefix is typed, and the host's
|
|
50
|
+
* commands follow this plugin's (a local name wins the duplicate). A typed
|
|
51
|
+
* prefix ranks names that start with it above names that merely contain it.
|
|
52
|
+
*/
|
|
53
|
+
export function commandSuggestions(input, foreign) {
|
|
54
|
+
if (!input.startsWith('/'))
|
|
55
|
+
return [];
|
|
56
|
+
const prefix = input.slice(1).toLowerCase();
|
|
57
|
+
const local = localizedCommands()
|
|
58
|
+
.filter(command => command.name !== 'dialog-test' && (prefix !== '' || command.aliasOf === undefined))
|
|
59
|
+
.map(command => ({ name: command.name, description: command.description, local: true }));
|
|
60
|
+
const seen = new Set(local.map(command => command.name));
|
|
61
|
+
const all = [...local, ...foreign.filter(command => !seen.has(command.name))];
|
|
62
|
+
const filtered = prefix === ''
|
|
63
|
+
? all
|
|
64
|
+
: all.filter(command => command.name.startsWith(prefix) || command.name.includes(prefix));
|
|
65
|
+
if (prefix === '')
|
|
66
|
+
return filtered;
|
|
67
|
+
return filtered.sort((a, b) => {
|
|
68
|
+
const aStart = a.name.startsWith(prefix) ? 0 : 1;
|
|
69
|
+
const bStart = b.name.startsWith(prefix) ? 0 : 1;
|
|
70
|
+
return aStart - bStart;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAA;AAEnC,uDAAuD;AACvD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE;IACjC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE;IACnC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,EAAE;IACrC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,cAAc,EAAE;IACzC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,cAAc,EAAE;IACzC,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,eAAe,EAAE;IAC3C,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE;IACjC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE;IACjC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE;IAClD,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE;IACnC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,EAAE;IACrC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE;IACjC,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,gBAAgB,EAAE;IAC7C,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,cAAc,EAAE;IACzC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE;IACjC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE;IACnC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE;IACrD,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,eAAe,EAAE;IAC3C,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,EAAE;IACrC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE;IACnC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE;IACjC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE;IACjC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,cAAc,EAAE;IACzC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,UAAU,EAAE;IAC1D,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,iBAAiB,EAAE;CACvC,CAAA;AAcV,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,OAAgB;IAC/D,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;IACrE,OAAO,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,WAAW,EAAE,kBAAkB,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;QACjG,GAAG,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9D,CAAC,CAAC,CAAA;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAa,EACb,OAAqC;IAErC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IACrC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;IAC3C,MAAM,KAAK,GAAG,iBAAiB,EAAE;SAC9B,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,MAAM,KAAK,EAAE,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;SACrG,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC1F,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IACxD,MAAM,GAAG,GAAG,CAAC,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC7E,MAAM,QAAQ,GAAG,MAAM,KAAK,EAAE;QAC5B,CAAC,CAAC,GAAG;QACL,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IAC3F,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAA;IAClC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC5B,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAChD,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAChD,OAAO,MAAM,GAAG,MAAM,CAAA;IACxB,CAAC,CAAC,CAAA;AACJ,CAAC"}
|