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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/docs/interface.md +2 -2
- package/docs/plugin-bridge.md +2 -2
- package/docs/remote-access.en.md +3 -3
- package/docs/remote-access.md +3 -3
- package/lib/adapters/dsh-better-sidebar/index.js +31 -2
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/client.js +185 -48
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +43 -1
- package/lib/intercept.js.map +1 -1
- package/lib/plugin-adapter-core.js +30 -1
- package/lib/plugin-adapter-core.js.map +1 -1
- package/lib/plugin-route-intercept.js +506 -11
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/plugin-tunnel.js +210 -18
- package/lib/plugin-tunnel.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +5 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/effects/turn-fold.d.ts +42 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/styles/content.css.d.ts +1 -1
- package/lib/types/client/styles/content.css.d.ts.map +1 -1
- package/lib/types/client/styles/layout.css.d.ts +1 -1
- package/lib/types/client/styles/layout.css.d.ts.map +1 -1
- package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +6 -0
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/plugin-adapter-core.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +8 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/plugin-tunnel.d.ts +37 -2
- package/lib/types/plugin-tunnel.d.ts.map +1 -1
- package/package.json +37 -37
- package/scripts/check-info-sheet-stacking.mjs +85 -0
- package/src/adapters/dsh-better-sidebar/index.ts +31 -2
- package/src/client/MobileSessionInfo.tsx +29 -2
- package/src/client/effects/turn-fold.ts +97 -26
- package/src/client/locales.ts +4 -0
- package/src/client/styles/content.css.ts +44 -14
- package/src/client/styles/layout.css.ts +9 -3
- package/src/client/styles/turn-fold.css.ts +9 -0
- package/src/index.ts +13 -0
- package/src/intercept.ts +46 -0
- package/src/plugin-adapter-core.ts +29 -2
- package/src/plugin-route-intercept.ts +516 -12
- 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.
|
|
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.
|
|
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
|
-
会话页头部的 ⓘ
|
|
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`
|
|
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**
|
package/docs/plugin-bridge.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/docs/remote-access.en.md
CHANGED
|
@@ -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
|
|
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`
|
|
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
|
| --- | --- | --- | --- |
|
package/docs/remote-access.md
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
19
|
-
* `/sidebar/ws/sidechat`
|
|
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":"AAAA
|
|
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
|
-
|
|
7430
|
-
|
|
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:
|
|
8167
|
-
|
|
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:
|
|
12473
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
13344
|
-
|
|
13345
|
-
[data-time-hover-root] gate holds
|
|
13346
|
-
:focus-within) — a phone never hovers, so
|
|
13347
|
-
|
|
13348
|
-
plugin's own
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
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
|
|
13375
|
-
white-space:
|
|
13376
|
-
overflow:
|
|
13377
|
-
|
|
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
|
}
|