@hyzyn/dsh-tty 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -87,8 +87,9 @@ Both were reproduced on **Windows 11 ARM (24H2) + Node 22 ARM64**. The fix:
87
87
  throughput / temperature show “无” (remote **Windows** hosts go through the PowerShell hop — see the
88
88
  status-bar section);
89
89
  - **How far this was verified**: on Windows 11 ARM a full install (`dsh plugin add @hyzyn/dsh-all`), all nine
90
- plugins mounting, `dsh web` serving, and the browser half being delivered (the same artifact macOS serves:
91
- 12,312,246 bytes, identical new-code markers), plus `[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`.
90
+ plugins mounting, `dsh web` serving, and the browser half being delivered (the same artifact macOS serves;
91
+ size and new-code markers compared after a build rather than pinning a byte count, which changes on every
92
+ rebuild), plus `[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`.
92
93
  x64 Windows is covered by the CI matrix (build / typecheck / test, see Development).
93
94
 
94
95
  ## Agent tools (P1)
@@ -252,7 +253,7 @@ slot (`ssh2`’s sftp subsystem, host half in `src/sftp.ts`):
252
253
  panel nor interrupts browsing); when another panel (such as the containers panel) already holds the slot on
253
254
  the same tab, or the panel is not open, it falls back to the original centered dialog, **without pushing
254
255
  anyone else’s panel out**. The title / collapse / ✕ come from tty’s mount slot;
255
- - **Follows the tab (0.18.4)**: the File Browser talks to the host of the tab that opened it, so it belongs to
256
+ - **Follows the tab (0.19.0)**: the File Browser talks to the host of the tab that opened it, so it belongs to
256
257
  that tab: switching away hides it (in-flight transfers keep running and the scene is restored when you come
257
258
  back), and closing the tab tears it down. **Every entry follows the same rule** — the connection bar’s
258
259
  “SFTP”, the 📂 on a connection-book entry and “File Browser” in the SSH dialog / settings card all become
@@ -395,7 +396,13 @@ and the agent tools all reuse the same scheduling.
395
396
  “Terminal N”); while connecting it first echoes a grey `Connecting user@host …`, and once ready the status
396
397
  bar shows `SSH user@host connected`; a failed connection (connection timeout / authentication rejected /
397
398
  host unreachable) comes back in an `error` frame with the reason, and since the tab spec was saved with the
398
- tab, clicking the terminal area reopens it from the original spec;
399
+ tab, clicking the terminal area reopens it from the original spec; the status bar describes the **currently
400
+ active tab** — switching or closing a tab immediately swaps in that tab’s own state (the failure reason and
401
+ the exit code are kept on the tab itself), so the red text no longer stays on screen after you close the tab
402
+ that could not connect (host-level messages such as “connection lost — reconnecting” belong to no tab and
403
+ are not wiped by a tab switch); conversely, **a background tab’s own failure is only recorded on that tab**
404
+ (its tab-bar status dot turns red and the terminal overlay carries the full text) and never takes over the
405
+ active tab’s status;
399
406
  - **Agent forwarding (0.4.0)**: with “agent forwarding” ticked in the SSH dialog, the remote side can use the
400
407
  local ssh-agent’s keys (such as `git clone` of a private repository remotely). It can be enabled with any
401
408
  authentication method (credentials still never touch disk); if no ssh-agent is running locally the
@@ -430,14 +437,17 @@ and the agent tools all reuse the same scheduling.
430
437
  box takes focus); the only difference is presentation — there it is an **inline** list rather than an overlay,
431
438
  because that card is a long scrollable form where an overlay would be clipped;
432
439
  - **Host-key TOFU pinning (0.3.0)**: after the first successful connection the host’s (host:port) sha256
433
- fingerprint is recorded in `hostKeys` (persisted with settings); every later connection is verified, a
434
- matching fingerprint is allowed, and **a changed fingerprint rejects the connection outright** (defense
435
- against impersonation), with a reset pointer in the error message. After a host reinstall or key change,
440
+ fingerprint is recorded in `hostKeys` (persisted with settings); every later connection is verified, any
441
+ fingerprint in the set matches, and **a changed fingerprint rejects the connection outright** (defense
442
+ against impersonation), with a reset pointer in the error message. A host’s multiple keys (0.19.0, e.g.
443
+ rsa + ed25519) are each recorded and merged into one record — algorithm negotiation changes no longer
444
+ cause false alarms. After a host reinstall or key change,
436
445
  delete the record under Settings → Plugins → Terminal Panel → “SSH host key
437
446
  records” and reconnect (the record list supports deletion). **“Import from known_hosts”
438
- (0.4.1)**: parses `~/.ssh/known_hosts` in one click to pre-fill existing host fingerprints in bulk (host
447
+ (0.4.1)**: parses `~/.ssh/known_hosts` in one click to pre-fill existing host fingerprints in bulk, keeping
448
+ all of a host’s rsa/ed25519 entries (no longer first-entry-only); host
439
449
  names from the connection book are also used to restore `|1|` hashed entries, and non-default ports are
440
- parsed as `[host]:port`);
450
+ parsed as `[host]:port`;
441
451
  - **Connection test (0.11.0)**: a “Test” button on each connection-book row of the settings card, plus a
442
452
  “Test connection” button in the SSH connection dialog — both perform **link diagnostics only** (no session,
443
453
  no `maxSessions` slot, no shell): first a TCP pre-check (DNS + connect, failures classified as
@@ -496,7 +506,7 @@ session belongs to (visually aligned with FinalShell’s session monitor bar):
496
506
  | `cwd` | host startup directory | Fallback working directory (the client’s current session cwd wins) |
497
507
  | `reconnectGraceSec` | 120 | Seconds a session is kept alive after an abnormal disconnect (0~3600): the session survives a page refresh/network blip waiting for a reconnect, and the reaper ends it on timeout; `0` = the old behavior, end immediately on disconnect |
498
508
  | `sshHosts` | `[]` | SSH connection book (selectable in the panel “+” menu): entries `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, persist=false}`; saved as a whole-set replacement, the same name overwrites; `password` / `passphrase` support `env:VAR` references so no plaintext is stored; with persistence on, clicking an entry opens a tmux persistent session by default, and `persist=false` is an **opt-out** |
499
- | `hostKeys` | `[]` | SSH host key records (TOFU, maintained automatically): entries `{host, port, fingerprint}`; unique by host:port, appended automatically on the first connection, and a changed fingerprint rejects the connection; the settings card can delete them to reset |
509
+ | `hostKeys` | `[]` | SSH host key records (TOFU, maintained automatically): entries `{host, port, fingerprints[]}` (the legacy single `fingerprint` field is migrated and merged on read); unique by host:port, one record holds all of a host’s keys, appended automatically on the first connection, any matching fingerprint is allowed, and a full mismatch rejects the connection; the settings card can delete them to reset |
500
510
  | `shellIntegration` | true | Injects the OSC 133/7 shell integration (command boundary markers + cwd reporting; `tty_capture{last}` depends on it); zsh/bash supported, other shells skipped automatically; can be turned off when compatibility problems appear |
501
511
  | `tunnels` | `[]` | Port-forwarding tunnels: entries `{name, bookName, direction=local\|remote, localPort?, remoteHost?, remotePort?, localTargetHost?, localTargetPort?, enabled}`; `bookName` references a connection-book entry for host and authentication; maintained graphically in the “Port forwarding” block of the card |
502
512
  | `sftpStyle` | `dialog` | SFTP File Browser UI style: `dialog` single pane (remote directory + upload/download/drag & drop) / `dual` two panes (local left / remote right, inline `⇨/⇦` server-side direct transfer); reopen SFTP for it to take effect |
@@ -610,7 +620,7 @@ which stays visible, clickable and typable instead of being covered by a full-sc
610
620
  the pain before 0.15).
611
621
 
612
622
  A mount slot is **connection-scoped**: its credentials / target come from the terminal tab that opened it.
613
- So since 0.18.4 every pane records its **owner tab** (`options.ownerSid`, defaulting to the active tab at
623
+ So since 0.19.0 every pane records its **owner tab** (`options.ownerSid`, defaulting to the active tab at
614
624
  mount time): switching to another tab **hides** the pane (`data-dock-hidden`; its DOM and your rendered tree
615
625
  survive, in-flight transfers keep running) and switching back restores the scene; closing the owner tab tears
616
626
  the pane down. Without this, the pane stayed put across a tab switch — its title read `SFTP · cdc-test-161`
@@ -663,10 +673,11 @@ ctx.inject(['ttyPanel'], (c) => {
663
673
  - The title bar (title / collapse / ✕) is provided by tty, and consumers only own their own body; a throwing
664
674
  `onClose` is only logged with `console.warn`, without affecting closing the panel.
665
675
 
666
- > Contract versions: `ttyConnbar.version === 1`, `ttyTerminal.version === 2` (1 = `open` only,
667
- > 2 = adds `mount`), `ttyPanel.version === 1`. Consumers **decide capabilities by version number**; do not
668
- > rely on assumptions beyond `typeof fn === 'function'`; on an older tty the `inject` still fires, but the
669
- > corresponding fields are absent.
676
+ > Contract versions: `ttyConnbar.version === 1`, `ttyTerminal.version === 3` (1 = `open` only,
677
+ > 2 = adds `mount`, 3 = `open` reuses an existing live tab for the same connection + command by default),
678
+ > `ttyPanel.version === 2` (1 = `mountPane` + `isOpen`, 2 = adds `minimize`). Consumers **decide capabilities
679
+ > by version number**; do not rely on assumptions beyond `typeof fn === 'function'`; on an older tty the
680
+ > `inject` still fires, but the corresponding fields are absent.
670
681
 
671
682
  ## Frame protocol (/api/dsh-tty/ws, JSON text frames; v3 = one connection with many sessions + reconnect)
672
683
 
@@ -705,8 +716,8 @@ pnpm --filter @hyzyn/dsh-tty build # tsc host + esbuild browser half (cli
705
716
  pnpm --filter @hyzyn/dsh-tty typecheck
706
717
  pnpm --filter @hyzyn/dsh-tty probe # M0 probe: PTY primitive verification (needs a real PTY)
707
718
  pnpm --filter @hyzyn/dsh-tty integration # integration tests: real plugin × real DSH service composition
708
- pnpm --filter @hyzyn/dsh-tty live # liveness smoke against a running dsh web
709
- pnpm --filter @hyzyn/dsh-tty tui # TUI smoke: vim/nano full-screen rendering
719
+ pnpm --filter @hyzyn/dsh-tty live # liveness smoke: start dsh web first (default ws://127.0.0.1:3080; DSH_TTY_WS_URL overrides)
720
+ pnpm --filter @hyzyn/dsh-tty tui # TUI smoke: vim/htop full-screen rendering (start dsh web first, default :3090; DSH_TTY_WS_URL overrides)
710
721
  pnpm --filter @hyzyn/dsh-tty ssh-smoke # SSH smoke: in-memory SSH server (ssh2.Server) × real spawnSsh end to end (build first)
711
722
  pnpm --filter @hyzyn/dsh-tty preview # visual preview: headless Chrome screenshots per scene (see below)
712
723
  ```
@@ -732,14 +743,14 @@ node scripts/preview.mjs --theme=light # light theme
732
743
 
733
744
  Coverage: local terminal / multi-tab + SSH connection bar / the “+” menu / SSH dialog (new, edit, probe)/
734
745
  settings card (also side by side with docker)/ SFTP (single pane, dual pane, placement fallback)/
735
- **mount slot follows the tab** (`dock-pane-tab`, the 0.18.4 regression)/ minimized badge / exit and error
746
+ **mount slot follows the tab** (`dock-pane-tab`, the 0.19.0 regression)/ minimized badge / exit and error
736
747
  overlays / tunnel popover / search box / toast / embedded terminals (alone and alongside the panel)/
737
748
  docker panel and “containers → terminal drawer”.
738
749
 
739
750
  A scene may attach a **function-shaped** assertion to `window.__previewAssert` (returning `null` means pass,
740
751
  a string / array means fail); the script runs it and folds the result into `✓/✗`. An assertion that only
741
752
  lives in the fixture, seen by nobody unless someone pulls `diag` by hand, is a regression that is not really
742
- pinned — which is exactly what bit the 0.18.4 “panel does not follow the tab” fix: the assertion was written
753
+ pinned — which is exactly what bit the 0.19.0 “panel does not follow the tab” fix: the assertion was written
743
754
  already, but because it was mixed into a `diag` object containing a function, the whole evaluation failed
744
755
  silently and everything reported ✓.
745
756
 
@@ -818,15 +829,22 @@ verify things like “is there still a white panel after switching light/dark th
818
829
  sandbox/chroot); downloads go through browser memory (for very large files prefer `scp`/`rsync` in the
819
830
  terminal); the agent tool `sftp_read` is ≤1MB and rejects binaries, and `sftp_write` is ≤1MB per call (use
820
831
  panel upload or the terminal for larger content); the `sftp_*` tools accept only connection-book entry
821
- names, and inline credentials are for the panel dialog only.
832
+ names, and inline credentials are for the panel dialog only; overwrite writes go through a same-directory
833
+ temp part `.dsh-part-<uuid>` + rename (atomic) — if the host process crashes / loses power, a part orphan
834
+ may remain, and the next overwrite upload to the same directory automatically cleans parts older than 24h.
822
835
  - **SSH host keys are TOFU-pinned**: the first connection records the sha256 fingerprint automatically
823
- (trust on first use), after which a matching fingerprint is allowed and a change is rejected — no longer an
836
+ (trust on first use), after which any recorded fingerprint matching is allowed and a full mismatch is
837
+ rejected — no longer an
824
838
  unconditional accept-and-log. Note TOFU’s inherent boundary: if the first connection already met a MITM,
825
839
  what was recorded is a fake fingerprint; `hostKeys` is persisted with settings, and a fingerprint change
826
- requires a human to confirm in the settings card and delete the record; `hostKeys` stores only **one**
827
- fingerprint per host:port — when a host offers several key types (rsa/ed25519/ecdsa) and algorithm
828
- negotiation changes, it may report a false change, and deleting the record and reconnecting recalibrates
829
- it; known_hosts import likewise takes the first entry per host.
840
+ requires a human to confirm in the settings card and delete the record; a host’s multiple key types
841
+ (rsa/ed25519/ecdsa) are merged into one record’s fingerprint set (0.19.0), so algorithm negotiation
842
+ changes no longer report a false “fingerprint changed”; known_hosts import likewise keeps every
843
+ fingerprint of a host.
844
+ - **Browser tab persistence contains no plaintext credentials** (0.19.0): the spec copies that SSH tabs
845
+ write into sessionStorage / localStorage have plaintext `password` / `passphrase` stripped (`env:`
846
+ references are kept) and are flagged with `credsStripped` — on restore/respawn the terminal asks for
847
+ re-entry. The in-memory spec of the live session is unaffected.
830
848
  - **SSH passwords / passphrases should use `env:VAR` references**: the connection book is persisted in the
831
849
  settings file, so plaintext `password` / `passphrase` is an exposure surface; prefer `env:VAR` +
832
850
  dsh-env-manager, or `agent` authentication outright (credentials never touch disk).
package/README.md CHANGED
@@ -83,8 +83,8 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
83
83
  - **服务器状态条**:本地只拿得到 CPU / 内存 / 在线时长(`node:os`),磁盘 / TCP / 网速 / 温度
84
84
  显示「无」(**远端** Windows 主机走 PowerShell 那一跳,见「服务器状态条」一节);
85
85
  - **验证到什么程度**:Windows 11 ARM 上跑过完整安装(`dsh plugin add @hyzyn/dsh-all`)、九个插件
86
- 装载、`dsh web` 起服务与浏览器半体交付(与 macOS 上服务的是同一份产物:12 312 246 字节、
87
- 新代码标记一致)、`[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`;x64 Windows 由 CI
86
+ 装载、`dsh web` 起服务与浏览器半体交付(与 macOS 上服务的是同一份产物,构建后比对大小与
87
+ 内容标记一致;不写死字节数——每次重建都会变)、`[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`;x64 Windows 由 CI
88
88
  的三平台矩阵覆盖(build / typecheck / test,见「开发」)。
89
89
 
90
90
  ## agent 工具(P1)
@@ -228,7 +228,7 @@ subsystem,宿主半体 `src/sftp.ts`):
228
228
  一条标题栏(折叠不关面板、不中断浏览);同一标签的挂载位已被别的面板(如容器面板)
229
229
  占用、或面板没开时,退回原来的居中对话框,**不会把别人的面板挤掉**。标题 / 折叠 / ✕ 由
230
230
  tty 的挂载位提供;
231
- - **跟着标签切(0.18.4)**:文件浏览连的是**打开它的那个标签**那台主机,所以它归属该标签:
231
+ - **跟着标签切(0.19.0)**:文件浏览连的是**打开它的那个标签**那台主机,所以它归属该标签:
232
232
  切到别的标签时整块收起(在途传输照跑,切回来还在原地),标签关掉时一并收掉。**所有入口
233
233
  用同一条规则**——连接栏「SFTP」、连接簿条目的 📂、SSH 对话框 / 设置卡片的「文件浏览」都
234
234
  归属打开那一刻的活动标签;只有「面板开着但一个标签都没有」才算不隶属任何标签(永远可见)。
@@ -354,7 +354,11 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
354
354
  「终端 N」);连接中先回显灰字 `Connecting user@host …`,就绪后状态栏
355
355
  显示 `SSH user@host 已连接`;连接失败(连接超时 / 认证被拒 / 主机
356
356
  不可达)以 `error` 帧带回原因,标签规格已随标签保存,点终端区域可按
357
- 原规格重开;
357
+ 原规格重开;状态栏描述的是**当前活动标签**——切标签 / 关标签立刻换成
358
+ 该标签自己的状态(失败原因、退出码都记在标签身上),所以关掉那个连不上
359
+ 的标签后,红字不会继续挂在头上(宿主级消息如「连接断开 — 自动重连中」
360
+ 不属于任何标签,不会被切标签抹掉);反过来说,**后台标签自己的失败只记在
361
+ 它身上**(标签栏状态点转红、终端区浮层带原文),不会顶掉活动标签的显示;
358
362
  - **agent forwarding(0.4.0)**:SSH 对话框勾选「agent forwarding」后远程
359
363
  可用本地 ssh-agent 的钥匙(远程 `git clone` 私有仓库等)。任意认证方式下
360
364
  都可开(凭证仍不落盘);本机未运行 ssh-agent 时连接会明确报错而非静默
@@ -384,12 +388,14 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
384
388
  可滚动的长表单,浮层在那边会被裁掉;
385
389
  - **主机指纹 TOFU 钉扎(0.3.0)**:首次连接成功后把该主机(host:port)的
386
390
  sha256 指纹记录进 `hostKeys`(随 settings 持久化);之后每次连接校验,
387
- 指纹一致放行,**指纹变更直接拒绝连接**(防中间人冒充),错误信息带重置
388
- 指引。主机重装/换钥匙后,到 设置 → 插件 → 终端面板 → 「SSH 主机密钥
389
- 记录」删除对应记录再重连即可(记录列表支持删除)。**「从 known_hosts
390
- 导入」(0.4.1)**:一键解析 `~/.ssh/known_hosts` 把已有主机指纹批量
391
- 预填充(连接簿里的主机名还会用于还原 `|1|` hashed 条目,非默认端口按
392
- `[host]:port` 解析);
391
+ 集合内任一指纹匹配即放行,**指纹变更直接拒绝连接**(防中间人冒充),错误
392
+ 信息带重置指引。同一主机的多把钥匙(0.19.0,如 rsa + ed25519)各记一条
393
+ 指纹、合并存于同一记录——算法协商变化不再误报变更。主机重装/换钥匙后,
394
+ 到 设置 → 插件 → 终端面板 → 「SSH 主机密钥记录」删除对应记录再重连即可
395
+ (记录列表支持删除)。**「从 known_hosts 导入」(0.4.1)**:一键解析
396
+ `~/.ssh/known_hosts` 把已有主机指纹批量预填充,同主机的 rsa/ed25519 等多条
397
+ 记录全部保留(不再只留首条);连接簿里的主机名还会用于还原 `|1|` hashed
398
+ 条目,非默认端口按 `[host]:port` 解析;
393
399
  - **连接测试(0.11.0)**:设置卡片连接簿条目行内「测试」按钮,SSH 连接
394
400
  对话框另有「试连」按钮——两者都只做**链路诊断**(不建会话、不占
395
401
  `maxSessions` 名额、不开 shell):先 TCP 预检(DNS + 建连,失败分类为
@@ -444,7 +450,7 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
444
450
  | `cwd` | 宿主启动目录 | 兜底工作目录(客户端当前会话 cwd 优先) |
445
451
  | `reconnectGraceSec` | 120 | 异常断开后会话保活秒数(0~3600):刷新页面/网络抖动后会话存活等待重连,超时由回收器结束;`0` = 旧行为,断开立即结束 |
446
452
  | `sshHosts` | `[]` | SSH 连接簿(面板「+」菜单可选):条目 `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, persist=false}`;保存时整体替换、同名覆盖;`password` / `passphrase` 支持 `env:VAR` 引用,避免明文入库;持久化开启时条目点击默认以 tmux 持久会话打开,`persist=false` 是**取消项** |
447
- | `hostKeys` | `[]` | SSH 主机指纹记录(TOFU,自动维护):条目 `{host, port, fingerprint}`;按 host:port 唯一,首次连接自动追加,指纹变更拒绝连接;设置卡片可删除重置 |
453
+ | `hostKeys` | `[]` | SSH 主机指纹记录(TOFU,自动维护):条目 `{host, port, fingerprints[]}`(旧版单 `fingerprint` 字段读取时自动迁移合并);按 host:port 唯一,一机多把钥匙共用一条记录,首次连接自动追加、任一指纹匹配放行、全部不匹配拒绝连接;设置卡片可删除重置 |
448
454
  | `shellIntegration` | true | 注入 OSC 133/7 shell 集成(命令边界标记 + cwd 上报;`tty_capture{last}` 依赖它);zsh/bash 支持,其他 shell 自动跳过;出兼容问题时可关闭 |
449
455
  | `tunnels` | `[]` | 端口转发隧道:条目 `{name, bookName, direction=local\|remote, localPort?, remoteHost?, remotePort?, localTargetHost?, localTargetPort?, enabled}`;`bookName` 引用连接簿条目提供主机与认证;卡片「端口转发」区块可视化维护 |
450
456
  | `sftpStyle` | `dialog` | SFTP 文件浏览界面风格:`dialog` 单窗体(远程目录 + 上传/下载/拖拽)/ `dual` 双栏(左本机 / 右远程,行内 `⇨/⇦` 宿主服务端直传);重新打开 SFTP 生效 |
@@ -564,7 +570,7 @@ ctx.inject(['ttyTerminal'], (c) => {
564
570
  连接栏点「容器」,容器面板挂在终端**右侧**,终端继续可见、可点、可输入,而不是被整屏
565
571
  弹窗盖住(0.15 之前那正是用户的痛点)。
566
572
 
567
- 挂载位是**连接级**的:它的凭证 / 目标来自**打开它的那个终端标签**。所以 0.18.4 起每块
573
+ 挂载位是**连接级**的:它的凭证 / 目标来自**打开它的那个终端标签**。所以 0.19.0 起每块
568
574
  pane 记一个「归属标签」(`options.ownerSid`,默认 = 挂载那一刻的活动标签):切到别的
569
575
  标签时整块**收起**(`data-dock-hidden`,DOM 与你 render 的树都保活,在途传输继续跑),
570
576
  切回来恢复现场;归属标签被关掉时 pane 一并收掉。不这么做的话,切完标签面板还停在原处
@@ -627,7 +633,7 @@ ctx.inject(['ttyPanel'], (c) => {
627
633
  })
628
634
  ```
629
635
 
630
- > 契约版本:`ttyConnbar.version === 1`、`ttyTerminal.version === 2`(1 = 只有 `open`,
636
+ > 契约版本:`ttyConnbar.version === 1`、`ttyTerminal.version === 3`(1 = 只有 `open`,
631
637
  > 2 = 增加 `mount`,3 = `open` 默认复用同「连接 + 命令」的活标签)、`ttyPanel.version === 2`(1 = `mountPane` + `isOpen`,2 = 增加
632
638
  > `minimize`)。消费方**按版本号判断能力**,不要用
633
639
  > `typeof fn === 'function'` 之外的假设;老版本 tty 上 `inject` 依然会触发,但没有
@@ -669,8 +675,8 @@ pnpm --filter @hyzyn/dsh-tty build # tsc 宿主 + esbuild 浏览器半体
669
675
  pnpm --filter @hyzyn/dsh-tty typecheck
670
676
  pnpm --filter @hyzyn/dsh-tty probe # M0 探针:PTY 原语验证(需真实 PTY)
671
677
  pnpm --filter @hyzyn/dsh-tty integration # 集成测试:真实插件 × 真实 DSH 服务组合
672
- pnpm --filter @hyzyn/dsh-tty live # 对运行中的 dsh web 做存活冒烟
673
- pnpm --filter @hyzyn/dsh-tty tui # TUI 冒烟:vim/nano 全屏渲染
678
+ pnpm --filter @hyzyn/dsh-tty live # 存活冒烟:需先起 dsh web(默认连 ws://127.0.0.1:3080,DSH_TTY_WS_URL 可覆盖)
679
+ pnpm --filter @hyzyn/dsh-tty tui # TUI 冒烟:vim/htop 全屏渲染(需先起 dsh web,默认连 :3090,DSH_TTY_WS_URL 可覆盖)
674
680
  pnpm --filter @hyzyn/dsh-tty ssh-smoke # SSH 冒烟:内存 SSH server(ssh2.Server)× 真实 spawnSsh 端到端(需先 build)
675
681
  pnpm --filter @hyzyn/dsh-tty preview # 视觉预览:headless Chrome 逐场景截图(见下)
676
682
  ```
@@ -695,12 +701,12 @@ node scripts/preview.mjs --theme=light # 浅色主题
695
701
 
696
702
  覆盖:本地终端 / 多标签 + SSH 连接栏 / 「+」菜单 / SSH 对话框(新建、编辑、试连)/
697
703
  设置卡片(含与 docker 并排对照)/ SFTP(单窗体、双栏、落点回退)/
698
- **挂载位跟着标签切**(`dock-pane-tab`,0.18.4 的回归)/ 最小化徽标 / 退出与错误遮罩 /
704
+ **挂载位跟着标签切**(`dock-pane-tab`,0.19.0 的回归)/ 最小化徽标 / 退出与错误遮罩 /
699
705
  隧道弹层 / 搜索框 / toast / 嵌入式终端(单独与面板共存)/ docker 面板与「容器 → 终端抽屉」。
700
706
 
701
707
  场景可以把**函数形态**的断言挂到 `window.__previewAssert`(返回 `null` = 通过,返回
702
708
  字符串 / 数组 = 失败),脚本会跑掉它并把结果计入 `✓/✗`。断言光挂在夹具里、只有手工
703
- 取 `diag` 时才有人看,回归等于没钉——0.18.4 修「面板不跟标签切」时就吃到这个亏:断言
709
+ 取 `diag` 时才有人看,回归等于没钉——0.19.0 修「面板不跟标签切」时就吃到这个亏:断言
704
710
  早写好了,但因为混在带函数的 `diag` 对象里,整条求值静默失败,一路都是 ✓。
705
711
 
706
712
  夹具还会把 `--dsw-*` 皮肤变量与真实界面一并渲染,因此能验
@@ -748,14 +754,18 @@ node scripts/preview.mjs --theme=light # 浅色主题
748
754
  反馈补丁兼容。
749
755
  - **端口转发边界**:本地监听固定 127.0.0.1(不暴露局域网);remote 方向服务端监听还受服务端 sshd `GatewayPorts` 限制;隧道的 SSH 连接与终端会话独立,均走 TOFU 钉扎与连接簿认证;隧道规格变更(端口/目标/启停)经「保存」热生效,热改连接簿凭证则在下次重连时生效。
750
756
  - **会话持久化(tmux)边界**:持久标签由 tmux server(专用 socket `dsh-tty`)托管——宿主被硬杀 / 保活回收 / 浏览器丢失标签规格时,tmux 会话会**留存**(这正是恢复能力的前提),直到机器重启或手动 `tmux -L dsh-tty kill-server`;agent 命令粒度工具(capture{last}/expect)依赖 tmux ≥3.3 的 DCS `allow-passthrough`,更低版本持久化可用但该能力降级(SSH 远程会话不注入 shell 集成钩子,capture{last} 本就不可用,与持久化无关);恢复接回重画的是当前可见屏,断线前的滚动历史在 tmux 自己的 history buffer(copy-mode)里,不在外层 xterm 滚动区;持久标签的 `exit` 帧退出码是 tmux 客户端的(0),shell 退出码经 OSC 133;D 标记照常可用;`tmux.conf` 只在 tmux server 首启时读取(改配置后需 `tmux -L dsh-tty kill-server` 让下次 spawn 重建 server);`grace=0` 的「断开立即结束」对持久标签同样会 kill-session(tmux 会话不存活);持久 SSH 会话要求远程装有 tmux(未装自动降级普通会话,连接栏常驻「未持久化」标记),且远程 `~/.tmux.conf` 不影响专用 socket 的独立 conf(`-f /dev/null`);SSH 持久会话名随 settings 留存;**两个窗口同时接回同一持久会话**时共享同一个宿主 PTY(0.10.1,单客户端扇出,名额不翻倍),两侧行数以最近调整尺寸的窗口为准(尺寸不一致时较大一侧由 onResize 自愈重画)。
751
- - **SFTP 边界**:文件权限 = 对应 SSH 账号的终端权限(无额外沙箱/chroot);下载经浏览器内存(超大文件建议终端 `scp`/`rsync`);agent 工具 `sftp_read` ≤1MB 且拒绝二进制、`sftp_write` 单次 ≤1MB(大内容走面板上传或终端);`sftp_*` 工具只收连接簿条目名,内联凭证仅供面板对话框使用。
757
+ - **SFTP 边界**:文件权限 = 对应 SSH 账号的终端权限(无额外沙箱/chroot);下载经浏览器内存(超大文件建议终端 `scp`/`rsync`);agent 工具 `sftp_read` ≤1MB 且拒绝二进制、`sftp_write` 单次 ≤1MB(大内容走面板上传或终端);`sftp_*` 工具只收连接簿条目名,内联凭证仅供面板对话框使用;覆盖写走同目录临时分片 `.dsh-part-<uuid>` + rename 原子落盘——宿主进程崩溃 / 断电时可能留下分片孤儿(下次向同目录覆盖上传时会自动清理超过 24h 的残留)。
752
758
  - **SSH host key 为 TOFU 钉扎**:首次连接自动记录 sha256 指纹(trust on
753
- first use),之后指纹一致放行、变更拒绝——不再是无条件放行的
759
+ first use),之后任一记录指纹匹配放行、全部不匹配拒绝——不再是无条件放行的
754
760
  accept-and-log。注意 TOFU 的固有边界:首次连接若已遭遇 MITM 则记录的
755
761
  就是伪指纹;`hostKeys` 随 settings 落盘,指纹变更需人工在设置卡片确认
756
- 并删除记录;`hostKeys` 按 host:port 只存**一条**指纹——同一主机提供多种
757
- 密钥类型(rsa/ed25519/ecdsa)且算法协商变化时可能误报变更,删除记录
758
- 重连即可重新校准;known_hosts 导入同为每主机首条优先。
762
+ 并删除记录;同一主机的多把钥匙(rsa/ed25519/ecdsa)合并为一条记录的多
763
+ 指纹集合(0.19.0),算法协商变化不再误报「指纹变更」;known_hosts 导入
764
+ 同样保留同主机的全部指纹。
765
+ - **浏览器标签持久化不含明文凭证**(0.19.0):SSH 标签写入
766
+ sessionStorage / localStorage 的规格副本会剥掉明文 `password` / `passphrase`
767
+ (`env:` 引用保留),并打 `credsStripped` 标记——恢复/重开时终端里提示
768
+ 重新输入。本会话内存里的规格不受影响。
759
769
  - **SSH 密码 / 口令建议 `env:VAR` 引用**:连接簿随 settings 文件落盘,
760
770
  `password` / `passphrase` 明文入库有泄露面;建议 `env:VAR` +
761
771
  dsh-env-manager 托管,或直接用 `agent` 认证(凭证不落盘)。