dsh-zen-remote 2.3.4 → 2.3.6

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 (53) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/docs/interface.md +2 -2
  4. package/docs/plugin-bridge.md +2 -2
  5. package/docs/remote-access.en.md +3 -3
  6. package/docs/remote-access.md +3 -3
  7. package/lib/adapters/dsh-better-sidebar/index.js +31 -2
  8. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  9. package/lib/client.js +185 -48
  10. package/lib/index.js +13 -0
  11. package/lib/index.js.map +1 -1
  12. package/lib/intercept.js +43 -1
  13. package/lib/intercept.js.map +1 -1
  14. package/lib/plugin-adapter-core.js +30 -1
  15. package/lib/plugin-adapter-core.js.map +1 -1
  16. package/lib/plugin-route-intercept.js +506 -11
  17. package/lib/plugin-route-intercept.js.map +1 -1
  18. package/lib/plugin-tunnel.js +210 -18
  19. package/lib/plugin-tunnel.js.map +1 -1
  20. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +5 -2
  21. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  22. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  23. package/lib/types/client/effects/turn-fold.d.ts +42 -0
  24. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  25. package/lib/types/client/locales.d.ts +2 -0
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/styles/content.css.d.ts +1 -1
  28. package/lib/types/client/styles/content.css.d.ts.map +1 -1
  29. package/lib/types/client/styles/layout.css.d.ts +1 -1
  30. package/lib/types/client/styles/layout.css.d.ts.map +1 -1
  31. package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -1
  32. package/lib/types/index.d.ts.map +1 -1
  33. package/lib/types/intercept.d.ts +6 -0
  34. package/lib/types/intercept.d.ts.map +1 -1
  35. package/lib/types/plugin-adapter-core.d.ts.map +1 -1
  36. package/lib/types/plugin-route-intercept.d.ts +8 -0
  37. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  38. package/lib/types/plugin-tunnel.d.ts +37 -2
  39. package/lib/types/plugin-tunnel.d.ts.map +1 -1
  40. package/package.json +37 -37
  41. package/scripts/check-info-sheet-stacking.mjs +85 -0
  42. package/src/adapters/dsh-better-sidebar/index.ts +31 -2
  43. package/src/client/MobileSessionInfo.tsx +29 -2
  44. package/src/client/effects/turn-fold.ts +97 -26
  45. package/src/client/locales.ts +4 -0
  46. package/src/client/styles/content.css.ts +44 -14
  47. package/src/client/styles/layout.css.ts +9 -3
  48. package/src/client/styles/turn-fold.css.ts +9 -0
  49. package/src/index.ts +13 -0
  50. package/src/intercept.ts +46 -0
  51. package/src/plugin-adapter-core.ts +29 -2
  52. package/src/plugin-route-intercept.ts +516 -12
  53. package/src/plugin-tunnel.ts +230 -20
package/README.md CHANGED
@@ -66,7 +66,7 @@ dsh plugin --profile web add dsh-zen-remote
66
66
  // ~/.dsh/profiles/desktop/package.json
67
67
  {
68
68
  "dependencies": {
69
- "dsh-zen-remote": "^2.3.4" // for local development: "link:/path/to/dsh-zen-remote"
69
+ "dsh-zen-remote": "^2.3.6" // for local development: "link:/path/to/dsh-zen-remote"
70
70
  }
71
71
  }
72
72
  ```
package/README.zh-CN.md CHANGED
@@ -66,7 +66,7 @@ dsh plugin --profile web add dsh-zen-remote
66
66
  // ~/.dsh/profiles/desktop/package.json
67
67
  {
68
68
  "dependencies": {
69
- "dsh-zen-remote": "^2.3.4" // 本地开发换成 "link:/path/to/dsh-zen-remote"
69
+ "dsh-zen-remote": "^2.3.6" // 本地开发换成 "link:/path/to/dsh-zen-remote"
70
70
  }
71
71
  }
72
72
  ```
package/docs/interface.md CHANGED
@@ -96,7 +96,7 @@ v1.0.0(fork 自 [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-
96
96
 
97
97
  ### 会话信息卡
98
98
 
99
- 会话页头部的 ⓘ 打开一张底部信息卡,把原来占位置的统计栏收纳进来:轮数/步数/首字延迟/模型耗时/工具耗时/token 与缓存命中六格统计、Chat/Trajectory 切换、以及导出日志/重命名/Fork 会话/归档四个操作。
99
+ 会话页头部的 ⓘ 打开一张底部信息卡,把原来占位置的统计栏收纳进来:轮次/步数、输出速度、首字延迟、模型耗时、工具耗时、缓存命中与 Token 收支六格统计、Chat/Trajectory 切换、以及导出日志/重命名/Fork 会话/归档四个操作。
100
100
 
101
101
  ### 回合过程折叠
102
102
 
@@ -385,7 +385,7 @@ PWA,再进来就是冷启动。插件自己的边缘手势也触发不了—
385
385
  ## 兼容插件
386
386
 
387
387
  - [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(0.19+:它的全部 tab 都在 DSH 0.1.5 官方右侧栏里,手机上宿主自己切全屏,本插件在会话页头部给它留入口位、左缘回退手势收它、文件树里点 @ 引用后自动收起——面板全屏盖住会话页,草稿变化看不见,收起本身就是点击反馈;≤ 0.18 自绘右栏的老版本:全宽抽屉 + 安全区 + 底部关闭 pill,只在 DSH < 0.1.5 上还会走到)——本机实测 **0.19.1**(老版规则实测 0.15.0)
388
- - dsh-better-sidebar 的**远程会话处理**(PB-12 起分两档):**0.25.0、0.26.0、0.26.1、0.28.0-alpha.0、0.28.0、0.28.1 与 0.28.2** 七版由 zen-remote 的内置适配器(`src/adapters/dsh-better-sidebar/`)**代理**——它的文件浏览、编辑器、git 变动、任务等界面在远程会话里读写的是主服务端的数据(整机数据,需主服务端主人在设置页同意;`open.external` 远程拒绝、`settings.*` 与 `/sidebar/bundle` 留本机、侧聊转发(子会话继承父会话共享);0.26 新方法已一并接入(`fs.readPage` 分页读、`sidechat.dequeue` 撤回排队消息、`sidechat.model`/`sidechat.models` 线程切模型、`/sidebar/write` 编辑器保存;0.28.1 再添 `git.history` 分页提交历史读;0.28.2 接口面不变,只把 detached 会话的 cwd 改为读存储 header(不再整份解码会话日志);`ws/sidechat` 推送不桥接,远程侧聊走 0.26 自带的 15s 兜底轮询,正确性无损))。**其他版本维持隐藏**:那些版本的界面都带 `{sessionId, cwd}` 语境调它自己的 `/sidebar/api/*` 接口,远程会话里这些请求落到子客户端本机——文件树会显示本机同名文件、编辑会真的写本机文件;远程会话(`html[data-zr-remote-session="1"]`)里按插件自己的挂载/渲染标记隐藏这些界面,选择器在 `src/client/styles/remote-session.css.ts`,未安装时一条都不命中。因为它接管了宿主的「文件」标签,未桥接的远程会话里暂时没有侧栏文件浏览;聊天或交付物里的文件链接(`dsh-resource://file/**`)会被它的编辑器注册抢先接住,展开一个空的右栏(根治要等 zen-remote 自己的远程文件树)。隐藏档的定位仍是**隐藏而非代理**:插件组件保持挂载并继续静默发只读请求,但所有写控件都在被隐藏的界面里,不会误写本机。PB-14 起显示规则按桥接状态生效:已桥接(0.25.0/0.26.0 且主服务端同意)时远程会话里**照常显示**(面板数据经适配器来自主服务端,隐藏规则全部让位);未桥接时维持隐藏,已打开的 tab 空态改为一行说明占位(better-sidebar 未在远程会话桥接——中英双语,随浏览器语言),空白工作台浮动按钮一并隐藏
388
+ - dsh-better-sidebar 的**远程会话处理**(PB-12 起分两档):**0.25.0、0.26.0、0.26.1、0.28.0-alpha.0、0.28.0、0.28.1 与 0.28.2** 七版由 zen-remote 的内置适配器(`src/adapters/dsh-better-sidebar/`)**代理**——它的文件浏览、编辑器、git 变动、任务等界面在远程会话里读写的是主服务端的数据(整机数据,需主服务端主人在设置页同意;`open.external` 远程拒绝、`settings.*` 与 `/sidebar/bundle` 留本机、侧聊转发(子会话继承父会话共享);0.26 新方法已一并接入(`fs.readPage` 分页读、`sidechat.dequeue` 撤回排队消息、`sidechat.model`/`sidechat.models` 线程切模型、`/sidebar/write` 编辑器保存;0.28.1 再添 `git.history` 分页提交历史读;0.28.2 接口面不变,只把 detached 会话的 cwd 改为读存储 header(不再整份解码会话日志);`ws/sidechat` 侧聊推送也已桥接(PB-26b:升级 URL 不带参数、会话 id 在连接后的 JSON 帧里,客户端把 watch 帧里的虚拟 id 还原成真实 id 才转发,插件推送的 `threadId` 换算回虚拟 id 才给浏览器,未共享的线程 id 在两侧都会被拒、连接即断)))。**其他版本维持隐藏**:那些版本的界面都带 `{sessionId, cwd}` 语境调它自己的 `/sidebar/api/*` 接口,远程会话里这些请求落到子客户端本机——文件树会显示本机同名文件、编辑会真的写本机文件;远程会话(`html[data-zr-remote-session="1"]`)里按插件自己的挂载/渲染标记隐藏这些界面,选择器在 `src/client/styles/remote-session.css.ts`,未安装时一条都不命中。因为它接管了宿主的「文件」标签,未桥接的远程会话里暂时没有侧栏文件浏览;聊天或交付物里的文件链接(`dsh-resource://file/**`)会被它的编辑器注册抢先接住,展开一个空的右栏(根治要等 zen-remote 自己的远程文件树)。隐藏档的定位仍是**隐藏而非代理**:插件组件保持挂载并继续静默发只读请求,但所有写控件都在被隐藏的界面里,不会误写本机。PB-14 起显示规则按桥接状态生效:已桥接(0.25.0/0.26.0 且主服务端同意)时远程会话里**照常显示**(面板数据经适配器来自主服务端,隐藏规则全部让位);未桥接时维持隐藏,已打开的 tab 空态改为一行说明占位(better-sidebar 未在远程会话桥接——中英双语,随浏览器语言),空白工作台浮动按钮一并隐藏
389
389
  - [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams)(AgentTeams 活动浮层:手机端挪到会话头部下方、避开安全区,会话列表页隐藏;子代理会话头部保留可点的父会话面包屑用于切回)——本机实测 **0.1.9**
390
390
  - [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开;旧包名 `dsh-usage-stats` 已废弃,两个名字同时留在 profile 里会因重复的 `usage-stats` 行让 DSH 起不来)——本机实测 **0.2.9**
391
391
  - [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.3**
@@ -63,7 +63,7 @@ export function apply(ctx: Context) {
63
63
 
64
64
  声明里出现未知字段直接报错(防拼错)。`methods`、`maxBody`、`upgrade` 与 2.4 的四个字段(`locator`/`prefix`/`deny`/`rewriteIds`)只在**显式给出**时进摘要——2.1 时代(没有这些字段)的声明,摘要与当年完全一致,升级不破坏已桥接状态。2.1 只匹配 **exact 路径**;2.4 起声明 `prefix: true` 的路由额外命中其子路径(见上表)。
65
65
 
66
- 两个字段同写时 `deny` 优先:远程命中的请求先被 `route-denied` 拒下,`rewriteIds` 的改写不可达(本地请求两边都不触发,响应原样交回)。`upgrade` 路由上的 `rewriteIds` 在隧道半边被忽略——隧道搬运的是不透明字节,无从改写;同一声明路由被当作普通 HTTP 请求命中时照常生效。
66
+ 两个字段同写时 `deny` 优先:远程命中的请求先被 `route-denied` 拒下,`rewriteIds` 的改写不可达(本地请求两边都不触发,响应原样交回)。`upgrade` 路由上的 `rewriteIds`:query 定位的升级路由在隧道半边被忽略——隧道搬运的是不透明字节,无从改写(同一声明路由被当作普通 HTTP 请求命中时照常生效);json-body 定位的升级路由(PB-26b)则照常作用在**下行文本帧**上——帧先解 JSON、深扫或按位改写后重新封帧再给浏览器。
67
67
 
68
68
  ## 你的插件要守的约定
69
69
 
@@ -107,7 +107,7 @@ content-type: application/json; charset=utf-8
107
107
 
108
108
  ## WebSocket 路由(2.2 起)
109
109
 
110
- 路由声明 `upgrade: true` 后即成为 WebSocket 升级端点:远程会话里浏览器对该路径的 `new WebSocket(...)` 由子客户端本机接住,经中继的**字节隧道**接到服务端本机插件——升级请求头(`sec-websocket-key` / `sec-websocket-version`,及可选的 `sec-websocket-protocol` / `sec-websocket-extensions`)与双向字节各自**原样搬运,两端都不解析 WebSocket 帧**:101 响应、每个帧、关闭字节都是不透明字节,真正的握手由插件路由在服务端本机完成。三路由的协议形状见 `docs/remote-access.md` 的「插件桥」一节;决策记录见 `docs/adr/0002`。
110
+ 路由声明 `upgrade: true` 后即成为 WebSocket 升级端点:远程会话里浏览器对该路径的 `new WebSocket(...)` 由子客户端本机接住,经中继的**字节隧道**接到服务端本机插件——升级请求头(`sec-websocket-key` / `sec-websocket-version`,及可选的 `sec-websocket-protocol` / `sec-websocket-extensions`)与双向字节各自**原样搬运,两端都不解析 WebSocket 帧**:101 响应、每个帧、关闭字节都是不透明字节,真正的握手由插件路由在服务端本机完成。例外(PB-26b):`locator: { in: 'json-body' }` 的升级路由(会话 id 在连接后的 JSON 文本帧里,握手本身是不带体的 GET)逐帧处理——客户端把每个文本帧的虚拟 id 还原成真实 id 再按序转发,服务端对每个文本帧按共享表核对后才交给插件,`rewriteIds` 声明(或适配器的响应钩子)也会作用在下行文本帧上;query 定位的升级路由保持不透明字节直通。三路由的协议形状见 `docs/remote-access.md` 的「插件桥」一节;决策记录见 `docs/adr/0002`。
111
111
 
112
112
  浏览器侧没有新接口:拿到的仍是原生 `WebSocket`,感知不到隧道。升级无法转发时(未桥接、会话不可达等原因)浏览器看到的就是一次握手失败——子客户端在 socket 上回答与 HTTP 改道相同的 503 `zr-plugin-unbridged` 形状(reason 取值见上表),表现为 `error` / `close` 事件。
113
113
 
@@ -335,7 +335,7 @@ The `/v1/http` route is a second allowlist, keyed by **route name** (not part of
335
335
  - Terminals are deliberate (a desktop client is a trusted device): what opens is a **server-side** PTY running as the server user, without the agent sandbox or approval restrictions; closing the session's remote access kills its terminal streams with it;
336
336
  - `workspaceFiles/read` and friends are not contained to the session directory (DSH itself does not contain them) — anything the server process can read is readable, under the same trusted-device premise as terminals;
337
337
  - The plugin bridge's whole-machine data requires the server owner's per-plugin consent — the toggles in the settings page's Plugin bridge section, everything OFF by default; the loopback carries the operator's cookie, so a bridged plugin sees the same permissions the local browser sees. The bridge's boundary is therefore the DECLARED route set (exact equality, plus a `prefix: true` route's sub-paths under the PB-23b raw-byte tail red line) plus the share table — there is **no** parameter-level scanning;
338
- - **Adapters (better-sidebar)**: beyond cooperative declarations, zen-remote ships a built-in adapter for `dsh-better-sidebar` **the seven versions 0.25.0 through 0.28.2** (`src/adapters/` — non-cooperative third-party plugins are bridged by it, with prefix/path-segment route matching and session-id rewriting in JSON bodies and path segments). A prefix hit's tail passes the shape red line: `?`/`#`/`..` (encoded forms included) or whitespace answers 400 `bad-request` (PB-23b, the same rule as the cooperative segment). **Consenting to its whole-machine toggle hands the paired desktop client whole-disk file read/write on the server, git write operations (commit, push, revert, …), and model spend through commit-message generation** — the product decision is "both directions forward, no path fencing", and a server owner should treat that toggle as terminal-grade trust in the paired machine; `open.external` (launching processes on the host) refuses for remote sessions, `settings.*` and `/sidebar/bundle` stay on the sub-client's own machine; the two WebSockets (`/sidebar/ws/fs-watch` file-tree watch, `/sidebar/ws/agent-opens` open push) bridge over the byte tunnel (frames pass through untouched — the browser never reads the session id inside a push); side chat (`sidechat.*`) forwards — its child sessions carry `meta.parentSession`, so they inherit the parent's share through the ancestor chain, and the `childId` in `sidechat.start`'s answer is virtualized before it reaches the browser; 0.28's (alpha.0 and stable) `changes.ops` long-poll (`wait: true`, parks up to 25 s — past the relay's 15 s header wait) has `wait` stripped on the server, so a remote session's "this round's files" view polls on 0.26's 2.5 s cadence; 0.26's `ws/sidechat` push socket is **not** bridged — remote side chat falls back to 0.26's built-in 15s polling, correctness intact, only the push cadence degrades to polling; `sidechat.models` shows the **sub-client's own** model catalog in the picker while the choice goes through `sidechat.model` and takes effect on the **server** — when the two machines' catalogs differ, what the server actually applies is authoritative;
338
+ - **Adapters (better-sidebar)**: beyond cooperative declarations, zen-remote ships a built-in adapter for `dsh-better-sidebar` **the seven versions 0.25.0 through 0.28.2** (`src/adapters/` — non-cooperative third-party plugins are bridged by it, with prefix/path-segment route matching and session-id rewriting in JSON bodies and path segments). A prefix hit's tail passes the shape red line: `?`/`#`/`..` (encoded forms included) or whitespace answers 400 `bad-request` (PB-23b, the same rule as the cooperative segment). **Consenting to its whole-machine toggle hands the paired desktop client whole-disk file read/write on the server, git write operations (commit, push, revert, …), and model spend through commit-message generation** — the product decision is "both directions forward, no path fencing", and a server owner should treat that toggle as terminal-grade trust in the paired machine; `open.external` (launching processes on the host) refuses for remote sessions, `settings.*` and `/sidebar/bundle` stay on the sub-client's own machine; the three WebSockets (`/sidebar/ws/fs-watch` file-tree watch, `/sidebar/ws/agent-opens` open push, `/sidebar/ws/sidechat` side-chat push) bridge over the byte tunnel — the first two carry the session id in their upgrade URL's query and their frames pass through untouched (the browser never reads the session id inside a push); `ws/sidechat`'s upgrade URL carries NO parameter at all — the ids ride the JSON text frames sent after the handshake (`{"op":"watch","threadIds":[…]}`), through the per-frame gate (PB-26b): the client rewrites every virtual id in a watch frame back to its real form before forwarding (an empty array = unsubscribe-all, passes verbatim), the server share-checks EVERY text frame before the plugin sees one (an unshared thread id never reaches the plugin — the connection dies), and the plugin's pushed `{"threadId":…}` top-level id is virtualized before it reaches the browser (better-sidebar's subscription comparison demands the exact virtual id); side chat (`sidechat.*`) forwards — its child sessions carry `meta.parentSession`, so they inherit the parent's share through the ancestor chain, and the `childId` in `sidechat.start`'s answer is virtualized before it reaches the browser; 0.28's (alpha.0 and stable) `changes.ops` long-poll (`wait: true`, parks up to 25 s — past the relay's 15 s header wait) has `wait` stripped on the server, so a remote session's "this round's files" view polls on 0.26's 2.5 s cadence; `sidechat.models` shows the **sub-client's own** model catalog in the picker while the choice goes through `sidechat.model` and takes effect on the **server** — when the two machines' catalogs differ, what the server actually applies is authoritative;
339
339
  - Concurrent streams are budgeted per device (32), and so are invokes kept in flight at once — past the per-device cap the invoke answers 429 `too-many-invokes`. Error codes shaped `*/internal` (`gateway/internal` and friends) are returned as the bare code with no message — that kind of message quotes server-side internals. Event answers are matched against the subscription and device they were forwarded to. Approval/question forwarding is **first come, first served**: whichever of the server's own UI and the sub-client answers first wins; the later answer is refused by the gateway and the client syncs to "already handled". The relay **never answers on the client's behalf**: an unshared session's approval/question events are not forwarded at all, and with the server's own UI offline too, the approval simply keeps waiting — the gateway re-delivers still-pending events to the next `$events` subscriber. A wait, never a rejection (an abstention would fail every pending approval of the session at once and break the push → wake → approve flow). Notification-type events (EMIT frames: session-list summaries, account expirations and other server-wide state) of an unshared session are never forwarded.
340
340
 
341
341
 
@@ -345,7 +345,7 @@ The `/v1/http` route is a second allowlist, keyed by **route name** (not part of
345
345
  | --- | --- | --- | --- |
346
346
  | New (has the 0.26 adapter) | 0.26 | 0.26 | Fully bridged; every new feature works |
347
347
  | New | 0.25 | 0.25 | Bridged as before; behavior identical to the previous release |
348
- | New | 0.25 | 0.26 | Bridged; the 0.26-only methods and `/sidebar/write` get 404 from the 0.25 server, `ws/sidechat` is refused so side chat falls back to 15s polling — single-feature degradation: core side chat works, no mis-routing, nothing written locally |
348
+ | New | 0.25 | 0.26 | Bridged; the 0.26-only methods and `/sidebar/write` get 404 from the 0.25 server, and `ws/sidechat` dies on open (the 0.25 server has no such upgrade route) so side chat falls back to 15s polling — single-feature degradation: core side chat works, no mis-routing, nothing written locally |
349
349
  | New | 0.26 | 0.25 | Bridged; the UI never sends the new requests — equivalent to the 0.25 experience |
350
350
  | Old (≤ 2.2.1) | 0.26 | any | Not in the adapter's version table → unbridged (hidden treatment) |
351
351
  | Old | 0.25 | 0.26 | Bridged as 0.25; new methods are refused with the adapter's `adapter-method-unknown`, and `/sidebar/write` carrying a virtual id errors against the local plugin (no file written) — single-feature degradation |
@@ -388,7 +388,7 @@ A bridged plugin's panel request, rerouted by the sub-client's local backend (th
388
388
 
389
389
  ### The plugin bridge's WebSocket byte tunnel: `POST /v1/plugin-tunnel/{open,send,close}`
390
390
 
391
- Routes declared `upgrade: true` travel over these three (the declaration side is in `docs/plugin-bridge.md`). The relay **never parses WebSocket**: the server→browser direction is **one NDJSON streaming response**, the browser→server direction is **ordered chunked POSTs**, and both directions are opaque bytes — the real upgrade handshake (the 101 and every frame) is produced by the plugin route on the server machine and crosses the tunnel untouched (`docs/adr/0002`).
391
+ Routes declared `upgrade: true` travel over these three (the declaration side is in `docs/plugin-bridge.md`). The relay **never parses WebSocket**: the server→browser direction is **one NDJSON streaming response**, the browser→server direction is **ordered chunked POSTs**, and both directions are opaque bytes — the real upgrade handshake (the 101 and every frame) is produced by the plugin route on the server machine and crosses the tunnel untouched (`docs/adr/0002`). One exception (PB-26b): a `locator: { in: 'json-body' }` upgrade route (better-sidebar's `ws/sidechat`) has a body-less GET for its handshake and carries its ids in the post-connection JSON text frames, so the share table has no packet to open at open time — such a tunnel's **upstream direction is split into RFC 6455 frames**, every text frame located and share-checked one by one (an unshared id answers 403 `not-shared` and tears the tunnel down; the frame NEVER reaches the plugin; an empty unsubscribe-all array and id-less frames pass), and the verified frames cross verbatim; the downstream direction stays opaque. Query-locator upgrade routes keep zero parsing overhead and unchanged behavior.
392
392
 
393
393
  | Route | Headers | Body | Success answer |
394
394
  | --- | --- | --- | --- |
@@ -372,7 +372,7 @@ sudo cloudflared service install # 通了再装成常驻服务
372
372
  - 终端是有意开放的(桌面应用端是可信设备):开的是**服务端**的 PTY,以服务端用户身份运行、不受智能体沙箱与审批限制;关闭会话的远程时终端流一并终止;
373
373
  - `workspaceFiles/read` 等文件预览接口不限制在会话目录内(DSH 自身就不限制),服务端进程可读的文件都能读——信任前提与终端相同;
374
374
  - 插件桥的整机数据需要主服务端主人逐插件同意——设置页「插件桥」里的开关,缺省全部关闭;回环执行带操作者 cookie,插件接口看到的权限与本机浏览器相同,因此桥接的边界就是声明登记的路径(exact 相等,`prefix: true` 路由含其子路径,前缀尾巴还要过 PB-23b 的原样字节形状红线)加会话共享表,**不做**参数级扫描;
375
- - **适配器(better-sidebar)**:协作声明之外,zen-remote 为 `dsh-better-sidebar` **0.25.0 至 0.28.2 的七个版本**内置了一个适配器(`src/adapters/`,不配合的第三方插件由它代为桥接,路由按前缀/路径段匹配、会话 id 在 JSON 体与路径段里改写)。前缀命中的尾巴过形状红线:`?`/`#`/`..`(含编码形态)或空白一律 400 `bad-request`(PB-23b,与协作段同一条)。**同意它的整机数据开关,等于让已配对的桌面应用端读写主服务端整盘文件、执行 git 写操作(提交、推送、回滚……),并通过提交信息生成消耗主服务端的模型额度**——产品上的决定就是「读写都接、不加路径围栏」,主服务端主人应当把它当成给对方一台本机终端级别的信任来开;`open.external`(在宿主机起进程)对远程会话拒绝,`settings.*` 与 `/sidebar/bundle` 留在子客户端本机;两条 WebSocket(`/sidebar/ws/fs-watch` 文件树监视、`/sidebar/ws/agent-opens` 打开推送)经字节隧道桥接(帧原样透传,浏览器不消费推送里的会话 id);侧聊(`sidechat.*`)转发——侧聊子会话带 `meta.parentSession`,按祖先链自动继承父会话的共享,`sidechat.start` 响应里的 `childId` 会换算成虚拟 id 回给浏览器;0.28(alpha.0 与正式版)的 `changes.ops` 长轮询(`wait: true`,最长挂 25 秒,超过中继 15 秒的响应头等待)在服务端去掉 `wait`,远程会话的「本轮文件」退回 0.26 的 2.5 秒轮询;0.26 新增的 `ws/sidechat` 侧聊推送**不桥接**——远程侧聊走 0.26 自带的 15s 兜底轮询,正确性无损,只是推送降级为轮询节奏;`sidechat.models` 的模型选择器显示的是**子客户端本机**的模型目录,而选择经 `sidechat.model` 作用在**服务端**——两机目录不一致时以服务端实际生效为准;
375
+ - **适配器(better-sidebar)**:协作声明之外,zen-remote 为 `dsh-better-sidebar` **0.25.0 至 0.28.2 的七个版本**内置了一个适配器(`src/adapters/`,不配合的第三方插件由它代为桥接,路由按前缀/路径段匹配、会话 id 在 JSON 体与路径段里改写)。前缀命中的尾巴过形状红线:`?`/`#`/`..`(含编码形态)或空白一律 400 `bad-request`(PB-23b,与协作段同一条)。**同意它的整机数据开关,等于让已配对的桌面应用端读写主服务端整盘文件、执行 git 写操作(提交、推送、回滚……),并通过提交信息生成消耗主服务端的模型额度**——产品上的决定就是「读写都接、不加路径围栏」,主服务端主人应当把它当成给对方一台本机终端级别的信任来开;`open.external`(在宿主机起进程)对远程会话拒绝,`settings.*` 与 `/sidebar/bundle` 留在子客户端本机;三条 WebSocket(`/sidebar/ws/fs-watch` 文件树监视、`/sidebar/ws/agent-opens` 打开推送、`/sidebar/ws/sidechat` 侧聊推送)经字节隧道桥接——前两条的升级 URL 带会话 query,帧原样透传(浏览器不消费推送里的会话 id);`ws/sidechat` 的升级 URL 不带任何参数,会话 id 在连接建立后的 JSON 文本帧里(`{"op":"watch","threadIds":[…]}`),走逐帧闸门(PB-26b):客户端把 watch 帧里的虚拟 id 逐个还原成真实 id 再转发(空数组 = 全部退订,原样放行),服务端对每个文本帧先按共享表核对、通过才交给插件(未共享的线程 id 到不了插件,连接即断),插件推回的 `{"threadId":…}` 顶层 id 换算回虚拟 id 才给浏览器(better-sidebar 的订阅比对要求虚拟 id 精确匹配);侧聊(`sidechat.*`)转发——侧聊子会话带 `meta.parentSession`,按祖先链自动继承父会话的共享,`sidechat.start` 响应里的 `childId` 会换算成虚拟 id 回给浏览器;0.28(alpha.0 与正式版)的 `changes.ops` 长轮询(`wait: true`,最长挂 25 秒,超过中继 15 秒的响应头等待)在服务端去掉 `wait`,远程会话的「本轮文件」退回 0.26 的 2.5 秒轮询;`sidechat.models` 的模型选择器显示的是**子客户端本机**的模型目录,而选择经 `sidechat.model` 作用在**服务端**——两机目录不一致时以服务端实际生效为准;
376
376
  - 每台设备并发流数有上限(32)、同时在途的 invoke 数也有上限(32,超出 429 `too-many-invokes`);`*/internal` 形状的错误码(`gateway/internal` 等)只回码不带消息——那类消息引用的是服务端内部细节。事件应答按「转发时的订阅 + 设备」核对归属。审批/提问的转发是**先到先得**:服务端自己的界面和子客户端谁先应答谁生效,后答的一方被网关拒绝、同步成「已处理」。中继**不代答**:未共享会话的审批/提问事件不会被转发,服务端界面也不在线时这个审批就一直等待——网关会把仍未应答的事件转交给下一个连上来的订阅,等,而不是替你拒绝(代答会把整批待审批一次性判死,砸掉「推送 → 唤醒 → 审批」链路)。未共享会话的**通知类**事件(EMIT 帧:会话列表摘要、账号到期之类的服务端全局状态)一律不转发;唯一的例外是已共享会话(含其子会话)的 `api-session/status`——它只描述单个会话的运行/空闲,逐帧按共享表判定,子客户端靠它在主机侧一轮开始或结束时同步运行状态徽标(断线期间的翻转由重连后补拉列表纠正)。
377
377
 
378
378
 
@@ -382,7 +382,7 @@ sudo cloudflared service install # 通了再装成常驻服务
382
382
  | --- | --- | --- | --- |
383
383
  | 新(含 0.26 适配) | 0.26 | 0.26 | 全量桥接,新功能全可用 |
384
384
  | 新 | 0.25 | 0.25 | 照常桥接,行为与旧版一致 |
385
- | 新 | 0.25 | 0.26 | 桥接;0.26 新方法与 `/sidebar/write` 被 0.25 服务端 404、`ws/sidechat` 被拒后走 15s 轮询——单功能降级,核心侧聊通,不误路由不写本机 |
385
+ | 新 | 0.25 | 0.26 | 桥接;0.26 新方法与 `/sidebar/write` 被 0.25 服务端 404,`ws/sidechat` 因 0.25 服务端没有该升级路由而开而即断、退回 15s 轮询——单功能降级,核心侧聊通,不误路由不写本机 |
386
386
  | 新 | 0.26 | 0.25 | 桥接;UI 不发新请求,等同 0.25 体验 |
387
387
  | 旧(≤ 2.2.1) | 0.26 | 任意 | 不在适配器版本表 → 未桥接隐藏档 |
388
388
  | 旧 | 0.25 | 0.26 | 按 0.25 桥接;新方法被适配器以 `adapter-method-unknown` 拒绝,`/sidebar/write` 带虚拟 id 落本机插件报错(不写文件)——单功能降级 |
@@ -425,7 +425,7 @@ sudo cloudflared service install # 通了再装成常驻服务
425
425
 
426
426
  ### 插件桥的 WebSocket 字节隧道:`POST /v1/plugin-tunnel/{open,send,close}`
427
427
 
428
- 声明 `upgrade: true` 的插件路由走这三条路由(声明侧见 `docs/plugin-bridge.md`)。中继**不解析 WebSocket**:服务端→浏览器方向是**一条 NDJSON 流式响应**,浏览器→服务端方向是**按序分块的普通 POST**,双向都是不透明字节——真正的升级握手(101 与每个帧)由插件路由在服务端本机完成,字节原样穿过隧道(`docs/adr/0002`)。
428
+ 声明 `upgrade: true` 的插件路由走这三条路由(声明侧见 `docs/plugin-bridge.md`)。中继**不解析 WebSocket**:服务端→浏览器方向是**一条 NDJSON 流式响应**,浏览器→服务端方向是**按序分块的普通 POST**,双向都是不透明字节——真正的升级握手(101 与每个帧)由插件路由在服务端本机完成,字节原样穿过隧道(`docs/adr/0002`)。唯一例外(PB-26b):`locator: { in: 'json-body' }` 的升级路由(如 better-sidebar 的 `ws/sidechat`)的升级握手是不带体的 GET、会话 id 在连接后的 JSON 文本帧里,共享表没有开包可查——这类隧道的**上行方向按 RFC 6455 切帧**,每个文本帧先定位其会话 id、逐个按共享表核对(未共享的 id 回 403 `not-shared` 并拆隧道,帧绝不交给插件;空数组退订与无 id 帧放行),通过后帧原样写入上游;下行方向仍是不透明字节。query 定位的升级路由零解析开销,行为不变。
429
429
 
430
430
  | 路由 | 请求头 | 请求体 | 成功回答 |
431
431
  | --- | --- | --- | --- |
@@ -15,8 +15,11 @@
15
15
  * model spend (commit-message generation), documented as such; NO path
16
16
  * fencing; `open.external` refuses for a remote session; `settings.*` and
17
17
  * `/sidebar/bundle` stay local; the adapter registers as whole-machine
18
- * data (consent per plugin, default off). PB-15: the 0.26.0 WebSocket
19
- * `/sidebar/ws/sidechat` is deliberately NOT declared (not bridged).
18
+ * data (consent per plugin, default off). PB-15 left the 0.26.0 WebSocket
19
+ * `/sidebar/ws/sidechat` undeclared; PB-26b bridges it — its ids ride the
20
+ * post-handshake JSON frames (`{"op":"watch","threadIds":[...]}`), which
21
+ * the json-body upgrade gate locates, classifies and rewrites frame by
22
+ * frame on both halves.
20
23
  *
21
24
  * The `cwd` handling is the one subtle part: better-sidebar resolves a
22
25
  * request's working directory as the session's own `header.cwd` first and
@@ -143,6 +146,16 @@ export const dshBetterSidebarAdapter = {
143
146
  // file paths, not session ids).
144
147
  { path: '/sidebar/ws/agent-opens', match: 'exact', methods: ['GET'], upgrade: true, locator: { in: 'query', name: 'sessionId' } },
145
148
  { path: '/sidebar/ws/fs-watch', match: 'exact', methods: ['GET'], upgrade: true, locator: { in: 'query', name: 'sessionId' } },
149
+ // The side-chat push feed (PB-26b; lib/index.js's third registerUpgrade):
150
+ // the upgrade URL carries NO query — the browser's first frame after the
151
+ // handshake is `{"op":"watch","threadIds":[...]}` and the plugin pushes
152
+ // `{"threadId":"..."}` per event. The json-body locator rides the FRAME
153
+ // gate: the client half classifies and rewrites every watch frame (an
154
+ // empty list — unsubscribe-all — crosses untouched), the server's send
155
+ // gate share-checks each frame before the plugin sees it, and the
156
+ // client.response hook below virtualizes the pushed threadId so the
157
+ // browser's `wantedRef.includes(frame.threadId)` comparison fires.
158
+ { path: '/sidebar/ws/sidechat', match: 'exact', methods: ['GET'], upgrade: true, locator: { in: 'json-body', fields: ['threadIds'] } },
146
159
  ],
147
160
  client: {
148
161
  request: (ctx) => {
@@ -158,6 +171,22 @@ export const dshBetterSidebarAdapter = {
158
171
  return { allow: true };
159
172
  },
160
173
  response: (ctx) => {
174
+ // PB-26b: the sidechat ws PUSH frames name their thread on the TOP
175
+ // level — `{"threadId":"<real id>"}`. The browser compares
176
+ // `wantedRef.current.includes(frame.threadId)` against the VIRTUAL ids
177
+ // it watched, so a real id here would silently miss every push; the
178
+ // virtualized form rides in its place. A frame with no usable tid (a
179
+ // non-object, or an id that is already virtual) crosses untouched.
180
+ if (ctx.pathname === '/sidebar/ws/sidechat') {
181
+ if (ctx.body !== null && typeof ctx.body === 'object' && !Array.isArray(ctx.body)) {
182
+ const record = ctx.body;
183
+ const tid = record.threadId;
184
+ if (typeof tid === 'string' && tid !== '' && !ctx.isVirtual(tid)) {
185
+ return { ...record, threadId: ctx.virtualize(tid) };
186
+ }
187
+ }
188
+ return ctx.body;
189
+ }
161
190
  const name = apiMethodOf(ctx.pathname);
162
191
  if (name === '')
163
192
  return ctx.body;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/dsh-better-sidebar/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/dsh-better-sidebar/index.ts"],"names":[],"mappings":"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"}
package/lib/client.js CHANGED
@@ -3083,6 +3083,8 @@ exports.zh = {
3083
3083
  'activityOpen': '查看子代理与后台任务',
3084
3084
  'infoStatTurns': '轮次',
3085
3085
  'infoStatSteps': '步骤',
3086
+ 'infoStatTurnsSteps': '轮次 / 步数',
3087
+ 'infoStatSpeed': '输出速度',
3086
3088
  'infoStatTtft': '首字延迟',
3087
3089
  'infoStatLlm': '模型耗时',
3088
3090
  'infoStatTool': '工具耗时',
@@ -3596,6 +3598,8 @@ exports.en = {
3596
3598
  'activityOpen': 'View subagents and background tasks',
3597
3599
  'infoStatTurns': 'Turns',
3598
3600
  'infoStatSteps': 'Steps',
3601
+ 'infoStatTurnsSteps': 'Turns / Steps',
3602
+ 'infoStatSpeed': 'Output speed',
3599
3603
  'infoStatTtft': 'TTFT',
3600
3604
  'infoStatLlm': 'LLM time',
3601
3605
  'infoStatTool': 'Tool time',
@@ -7100,6 +7104,16 @@ function formatDuration(ms) {
7100
7104
  const whole = Math.round(s);
7101
7105
  return `${Math.floor(whole / 60)}m${whole % 60}s`;
7102
7106
  }
7107
+ /**
7108
+ * Decode-throughput figure: whole tokens from ten up, one decimal below.
7109
+ * Ported digit-for-digit from dsh-client-ui-chat lib/client.js
7110
+ * formatTokensPerSecond (DSH 0.2.x TurnUsagePanel; verified 2026-10-30),
7111
+ * which also owns the "{tps} tok/s" unit the cell appends.
7112
+ */
7113
+ function formatTokensPerSecond(tps) {
7114
+ const clamped = Math.max(0, tps);
7115
+ return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
7116
+ }
7103
7117
  /** Sum of the three disjoint prompt-side billing buckets. */
7104
7118
  function billedInputTokens(usage) {
7105
7119
  return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
@@ -7426,8 +7440,23 @@ function MobileSessionInfo({ sessionId, useSessions, useProjection, forkSession,
7426
7440
  });
7427
7441
  };
7428
7442
  const cells = [
7429
- { label: t('infoStatTurns'), value: stats === undefined ? NA : String(stats.turns), sub: undefined },
7430
- { label: t('infoStatSteps'), value: stats === undefined ? NA : String(stats.steps), sub: undefined },
7443
+ /* Turns and steps share one cell ("3 / 12") so the grid stays 2×3 with
7444
+ output speed taking the freed slot (UI-1, 2026-10-30). */
7445
+ {
7446
+ label: t('infoStatTurnsSteps'),
7447
+ value: stats === undefined ? NA : `${stats.turns} / ${stats.steps}`,
7448
+ sub: undefined,
7449
+ },
7450
+ /* Same guard and formula as the official TurnUsagePanel: decodeMs > 0
7451
+ is its "we have a throughput reading" test, so a session with no
7452
+ usage-reporting step shows the placeholder, never "0 tok/s". */
7453
+ {
7454
+ label: t('infoStatSpeed'),
7455
+ value: stats === undefined || stats.decodeMs <= 0
7456
+ ? NA
7457
+ : `${formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1e3))} tok/s`,
7458
+ sub: undefined,
7459
+ },
7431
7460
  {
7432
7461
  label: t('infoStatTtft'),
7433
7462
  value: stats === undefined || stats.ttftSteps === 0 ? NA : formatDuration(stats.ttftMs / stats.ttftSteps),
@@ -8162,12 +8191,18 @@ exports.LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
8162
8191
  [data-phase] [class$="_actions"] {
8163
8192
  overflow: hidden !important;
8164
8193
  }
8194
+ /* The 24h clock ("18:06" / "MM-DD 18:06") is a fixed-size item: it must
8195
+ never be the thing that shrinks. DSH 0.2.x wraps it with the usage
8196
+ button in an inline-flex _endInfo (min-width: 0), so a shrinkable
8197
+ time (the old flex: 0 1 auto + min-width: 0) got squeezed inside that
8198
+ non-wrapping box and "18:06" broke into two lines. flex: none pins its
8199
+ width; the turn-tail rules in content.css.ts let _endInfo wrap as a
8200
+ whole instead. */
8165
8201
  [data-phase] [class$="_actions"] [class$="_timeEnd"] {
8166
- flex: 0 1 auto !important;
8167
- min-width: 0 !important;
8202
+ flex: none !important;
8203
+ white-space: nowrap !important;
8168
8204
  overflow: hidden !important;
8169
8205
  text-overflow: ellipsis !important;
8170
- white-space: nowrap !important;
8171
8206
  }
8172
8207
 
8173
8208
  [data-phase] [class$="_scroll"]:has(p) {
@@ -12363,8 +12398,11 @@ function clientConfig() {
12363
12398
  __modules["client/effects/turn-fold.js"] = function (require, module, exports) {
12364
12399
  "use strict";
12365
12400
  Object.defineProperty(exports, "__esModule", { value: true });
12366
- exports.OPEN = exports.THINK = exports.PROCESS_KINDS = void 0;
12401
+ exports.BLANK = exports.OPEN = exports.THINK = exports.PROCESS_KINDS = void 0;
12367
12402
  exports.foldSuppressionTransition = foldSuppressionTransition;
12403
+ exports.hasContentOutside = hasContentOutside;
12404
+ exports.foldsWholeRow = foldsWholeRow;
12405
+ exports.isBlankStep = isBlankStep;
12368
12406
  exports.installTurnFold = installTurnFold;
12369
12407
  const client_config_ts_1 = require("./client/client-config.js");
12370
12408
  const locales_ts_1 = require("./client/locales.js");
@@ -12464,46 +12502,93 @@ exports.OPEN = 'data-mnav-fold-open';
12464
12502
  const ACTIVE_ATTR = 'data-mnav-fold-on';
12465
12503
  /** Selector of the one row kind that can fold whole — see {@link foldsWholeRow}. */
12466
12504
  const STEP_SELECTOR = '[data-chat-flow-kind="assistant-step"]';
12505
+ /** Elements that show something without a text node of their own. Every other
12506
+ * element is a wrapper: it is content only if something inside it is. */
12507
+ const SELF_VISIBLE = new Set([
12508
+ 'AUDIO', 'CANVAS', 'EMBED', 'HR', 'IFRAME', 'IMG', 'INPUT', 'OBJECT',
12509
+ 'PICTURE', 'SELECT', 'SVG', 'TEXTAREA', 'VIDEO',
12510
+ ]);
12511
+ /**
12512
+ * True when `node` renders anything a reader could see outside the subtrees
12513
+ * that match `skip`: a non-blank text node, or a {@link SELF_VISIBLE} element.
12514
+ * Structural, no class names — same discipline as the rest of this effect.
12515
+ */
12516
+ function hasContentOutside(node, skip) {
12517
+ if (node.nodeType === 3)
12518
+ return (node.textContent ?? '').trim() !== '';
12519
+ if (node.nodeType !== 1)
12520
+ return false;
12521
+ if (skip !== null && node.matches?.(skip) === true)
12522
+ return false;
12523
+ if (SELF_VISIBLE.has((node.tagName ?? '').toUpperCase()))
12524
+ return true;
12525
+ const children = node.childNodes;
12526
+ for (let i = 0; i < children.length; i++) {
12527
+ const child = children[i];
12528
+ if (child !== undefined && hasContentOutside(child, skip))
12529
+ return true;
12530
+ }
12531
+ return false;
12532
+ }
12467
12533
  /**
12468
12534
  * True when the reasoning rows are the ONLY thing this row renders, so the
12469
12535
  * whole flow item has to go: the flow column is `display: flex` with
12470
12536
  * `gap: 16px`, and an emptied-out flex item still claims its gap — the
12471
12537
  * reader would see the folded steps as a column of blank bands.
12472
- * Structural test, no class names: every ancestor from the reasoning rows'
12473
- * shared parent up to the flow item must be an only child.
12538
+ * Structural test, no class names: outside the reasoning rows the row holds
12539
+ * nothing visible — only wrappers. Each reasoning block sits in its own
12540
+ * ProcessReasoning wrapper, and since DSH 0.2.1-rc.5 a block whose display
12541
+ * text is empty once a leading `<signature>…</signature>` is stripped renders
12542
+ * its ReasoningRow as null and leaves that wrapper empty; an empty wrapper
12543
+ * beside a Think must not keep the row unfolded.
12474
12544
  */
12475
12545
  function foldsWholeRow(row, thinks) {
12476
- const first = thinks[0];
12477
- if (first === undefined)
12478
- return false;
12479
- const body = first.parentElement;
12480
- if (body === null || body.childElementCount !== thinks.length)
12546
+ if (thinks.length === 0)
12481
12547
  return false;
12482
- let node = body;
12483
- while (node !== row) {
12484
- const parent = node.parentElement;
12485
- if (parent === null)
12486
- return false;
12487
- if (parent.childElementCount !== 1)
12488
- return false;
12489
- node = parent;
12490
- }
12491
- return true;
12492
- }
12493
- /** The process items one flow row contributes: the row itself, its reasoning
12494
- * rows, or nothing at all. */
12548
+ return !hasContentOutside(row, exports.THINK);
12549
+ }
12550
+ /**
12551
+ * True for an assistant-step row that renders nothing at all — no Think, no
12552
+ * prose, no image, no notice. DSH 0.2.1-rc.5 produces one for a step whose
12553
+ * only reasoning is a `<signature>…</signature>` (ReasoningRow hides itself,
12554
+ * the wrappers stay). It is not process to count in the chip, and it is not
12555
+ * content either: left alone it is a blank band in the flow (the gap again),
12556
+ * so it is hidden on its own marker ({@link BLANK}) that the chip's open
12557
+ * override never releases. On 0.2.1-rc.2, which renders such a block as an
12558
+ * ordinary Think, the row is not blank and folds exactly as before.
12559
+ */
12560
+ function isBlankStep(row) {
12561
+ return !hasContentOutside(row, null);
12562
+ }
12563
+ /** Marker of a {@link isBlankStep} row; the stylesheet hides it while the
12564
+ * fold is live. Never counted, never opened. */
12565
+ exports.BLANK = 'data-mnav-blank';
12566
+ /** Hold {@link BLANK} on an assistant-step row exactly while it is
12567
+ * {@link isBlankStep} — set the moment it empties, dropped the moment
12568
+ * anything (a Think, prose, an image) arrives in it. */
12569
+ function markBlank(row, thinks) {
12570
+ if (thinks.length === 0 && isBlankStep(row))
12571
+ row.setAttribute(exports.BLANK, '');
12572
+ else
12573
+ row.removeAttribute(exports.BLANK);
12574
+ }
12575
+ /** The process items one flow row contributes — the row itself, its reasoning
12576
+ * rows, or nothing at all — and how many steps they are on the chip: a row
12577
+ * folded whole still counts each reasoning block it holds, so the count does
12578
+ * not depend on how the row had to be hidden. */
12495
12579
  function processItems(row) {
12496
12580
  const kind = row.getAttribute('data-chat-flow-kind');
12497
12581
  if (kind === null)
12498
- return [];
12582
+ return { items: [], steps: 0 };
12499
12583
  if (PROCESS_KIND_SET.has(kind))
12500
- return [row];
12584
+ return { items: [row], steps: 1 };
12501
12585
  if (kind !== 'assistant-step')
12502
- return [];
12586
+ return { items: [], steps: 0 };
12503
12587
  const thinks = [...row.querySelectorAll(exports.THINK)];
12588
+ markBlank(row, thinks);
12504
12589
  if (thinks.length === 0)
12505
- return [];
12506
- return foldsWholeRow(row, thinks) ? [row] : thinks;
12590
+ return { items: [], steps: 0 };
12591
+ return { items: foldsWholeRow(row, thinks) ? [row] : thinks, steps: thinks.length };
12507
12592
  }
12508
12593
  /**
12509
12594
  * The summary row sitting immediately before `anchor`, created if absent.
@@ -12536,10 +12621,12 @@ function groupsOf(flow) {
12536
12621
  if (kind === null)
12537
12622
  continue;
12538
12623
  if (current === undefined || TURN_HEAD_KINDS.has(kind)) {
12539
- current = { key: row.getAttribute('data-chat-flow-key') ?? `pos${groups.length}`, items: [] };
12624
+ current = { key: row.getAttribute('data-chat-flow-key') ?? `pos${groups.length}`, items: [], steps: 0 };
12540
12625
  groups.push(current);
12541
12626
  }
12542
- current.items.push(...processItems(row));
12627
+ const { items, steps } = processItems(row);
12628
+ current.items.push(...items);
12629
+ current.steps += steps;
12543
12630
  }
12544
12631
  return groups;
12545
12632
  }
@@ -12636,7 +12723,7 @@ function installTurnFold(ctx) {
12636
12723
  chip.dataset['running'] = '';
12637
12724
  else
12638
12725
  delete chip.dataset['running'];
12639
- label(chip, group.items.length, groupRunning);
12726
+ label(chip, group.steps, groupRunning);
12640
12727
  live.add(group.key);
12641
12728
  });
12642
12729
  for (const chip of flow.querySelectorAll(CHIP_SELECTOR)) {
@@ -12659,6 +12746,12 @@ function installTurnFold(ctx) {
12659
12746
  stale.removeAttribute(FOLD);
12660
12747
  stale.removeAttribute(exports.OPEN);
12661
12748
  }
12749
+ // {@link BLANK} is held by groupsOf on every assistant-step row of this
12750
+ // column; anything else carrying it left the column (or changed kind).
12751
+ for (const stale of flow.querySelectorAll(`[${exports.BLANK}]`)) {
12752
+ if (stale.parentElement !== flow || !stale.matches(STEP_SELECTOR))
12753
+ stale.removeAttribute(exports.BLANK);
12754
+ }
12662
12755
  };
12663
12756
  /**
12664
12757
  * Fold (or release) the reasoning-only rows this mutation batch touched,
@@ -12724,6 +12817,9 @@ function installTurnFold(ctx) {
12724
12817
  row.setAttribute(FOLD, '');
12725
12818
  else
12726
12819
  row.removeAttribute(FOLD);
12820
+ // Same frame for the blank row (see {@link isBlankStep}): hidden
12821
+ // before it paints its band, shown before its first content paints.
12822
+ markBlank(row, thinks);
12727
12823
  }
12728
12824
  };
12729
12825
  const schedule = () => {
@@ -12765,6 +12861,8 @@ function installTurnFold(ctx) {
12765
12861
  item.removeAttribute(FOLD);
12766
12862
  item.removeAttribute(exports.OPEN);
12767
12863
  }
12864
+ for (const item of document.querySelectorAll(`[${exports.BLANK}]`))
12865
+ item.removeAttribute(exports.BLANK);
12768
12866
  };
12769
12867
  const attach = () => {
12770
12868
  if (observer !== null)
@@ -12919,6 +13017,15 @@ const rules = (scope) => `
12919
13017
  display: none !important;
12920
13018
  }
12921
13019
 
13020
+ /* An assistant-step row that renders nothing at all (effects/turn-fold.ts
13021
+ isBlankStep — DSH 0.2.1-rc.5 leaves one behind for a step whose only
13022
+ reasoning is a <signature>…</signature>). Not a step: never counted on
13023
+ the chip and never released by the open override, it would only ever
13024
+ paint the flow column's 16px gap as a blank band. Literal mirror of BLANK in effects/turn-fold.ts. */
13025
+ ${scope} [data-chat-flow] > [data-mnav-blank] {
13026
+ display: none !important;
13027
+ }
13028
+
12922
13029
  /* Compact chip, sized like the composer's own pills rather than a button:
12923
13030
  it is a reading affordance in the middle of the message flow, so it has
12924
13031
  to stay quieter than the content around it. The flow column is a flex
@@ -13340,15 +13447,25 @@ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ----------
13340
13447
  max-width: none !important;
13341
13448
  }
13342
13449
 
13343
- /* Turn-tail metadata (2026-08-18): the timing stats ("18:06 · 用时 29秒 ·
13344
- 首 token 0.9秒 · 163 tok/s") are desktop-hover-revealed (official
13345
- [data-time-hover-root] gate holds them at opacity 0 until :hover /
13346
- :focus-within) — a phone never hovers, so they were simply invisible;
13347
- and even ignoring that, the row is nowrap + overflow:hidden (plus this
13348
- plugin's own legacy ellipsis clamp in layout.css.ts), which clipped the
13349
- text. On phones: always visible, own full-width line under the action
13350
- icons, wrapping freely, one type size down. !important + the extra
13351
- [class$="_actions"] hop out-specifies both upstream and layout.css.ts. */
13450
+ /* Turn-tail metadata (2026-08-18; reshaped 2026-10-30 for DSH 0.2.x): the
13451
+ tail's 24h clock is desktop-hover-revealed (official
13452
+ [data-time-hover-root] gate holds it at opacity 0 until :hover /
13453
+ :focus-within) — a phone never hovers, so it was simply invisible; and
13454
+ the row is a fixed-height nowrap + overflow:hidden flex line (plus this
13455
+ plugin's own clamp in layout.css.ts), which clipped it.
13456
+
13457
+ DSH 0.2.x (MessageIconActions, dsh-client-ui-chat) groups the usage
13458
+ button (TurnUsagePanel) and the clock (_timeEnd) inside ONE inline-flex
13459
+ _endInfo (min-width: 0; align-items: center; gap: 8px; margin-left: 8px)
13460
+ that never wraps internally. The 0.1.x-era rule here gave _timeEnd
13461
+ flex-basis: 100% + white-space: normal — inside _endInfo that no longer
13462
+ reads as "own full-width line", it reads as "shrink to nothing and
13463
+ break", and with the transcript-wide overflow-wrap: anywhere above,
13464
+ "18:06" rendered as two lines. On phones now: always visible; the row
13465
+ may grow in height and wrap; _endInfo drops to the next line as one
13466
+ unit when icons + usage + clock do not fit; the clock itself never
13467
+ shrinks or breaks. !important + the extra [class$="_actions"] hop
13468
+ out-specifies both upstream and layout.css.ts. */
13352
13469
  /* Pin the tail row's width chain before allowing wrap: with wrap enabled
13353
13470
  the row's intrinsic (shrink-to-fit) width collapses to its widest
13354
13471
  single icon, taking the whole tail down to ~16px. */
@@ -13356,9 +13473,26 @@ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ----------
13356
13473
  [data-chat-flow-kind="turn-tail"] [class$="_actions"] {
13357
13474
  width: 100%;
13358
13475
  }
13476
+ /* height: auto releases the upstream fixed height: calc(28px + …);
13477
+ overflow: visible beats layout.css.ts's overflow: hidden (same
13478
+ specificity — this file is concatenated last, so it wins the tie). A
13479
+ wrapped second line must not be cut off vertically; min-height keeps
13480
+ the one-line case at the upstream height so nothing shifts when it
13481
+ fits. */
13359
13482
  [data-chat-flow-kind="turn-tail"] [class$="_actions"] {
13360
13483
  flex-wrap: wrap;
13361
13484
  row-gap: 2px;
13485
+ height: auto !important;
13486
+ min-height: 28px;
13487
+ overflow: visible !important;
13488
+ }
13489
+ /* _endInfo moves as a whole: flex: none so the usage + clock group is
13490
+ never asked to shrink — when the row is too narrow the entire group
13491
+ wraps to the next line instead of compressing the clock inside it.
13492
+ max-width keeps an unusually long usage label inside the column. */
13493
+ [data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_endInfo"] {
13494
+ flex: none;
13495
+ max-width: 100%;
13362
13496
  }
13363
13497
  /* Wide companion to the rule below: mirrors the upstream hover-gate
13364
13498
  selector shape directly, with no assumptions about the tail's DOM
@@ -13369,13 +13503,16 @@ exports.CONTENT_CSS = `/* ---------- chat markdown content (< 768px) ----------
13369
13503
  opacity: 1 !important;
13370
13504
  transition: none !important;
13371
13505
  }
13506
+ /* overflow-wrap / word-break: normal undo the [data-chat-flow]
13507
+ overflow-wrap: anywhere inherited from the top of this block — that is
13508
+ the rule that let "18:06" break between digits once its box was
13509
+ squeezed. */
13372
13510
  [data-chat-flow-kind="turn-tail"] [class$="_actions"] [class$="_timeEnd"] {
13373
13511
  opacity: 1 !important;
13374
- flex-basis: 100% !important;
13375
- white-space: normal !important;
13376
- overflow: visible !important;
13377
- text-overflow: clip !important;
13378
- padding-left: 0 !important;
13512
+ flex: none !important;
13513
+ white-space: nowrap !important;
13514
+ overflow-wrap: normal !important;
13515
+ word-break: normal !important;
13379
13516
  font-size: 12px;
13380
13517
  line-height: 18px;
13381
13518
  }