dsh-auto-collapse 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,98 +1,100 @@
1
- # dsh-auto-collapse
2
-
3
- > A DeepSeek Harness Web client plugin that auto-collapses tool cards and Think blocks into one-line summaries, so the chat keeps only what the model says.
4
- >
5
- > 中文: [README.md](./README.md)
6
-
7
- ## What it does
8
-
9
- `dsh-auto-collapse` is a pure front-end DOM plugin for the DeepSeek Harness Web chat UI. It collapses the working process into one-line summaries — tool calls and reasoning no longer fill the screen, giving the chat the collapsible look of the VSCode Codex desktop client, **and it also rewrites “Deep diving” to a configurable “Deep sleeping...”**. It never modifies message content — it only controls visibility of the working process.
10
-
11
- ## Preview
12
-
13
- ![Collapsed workflow](assets/screenshot.png)
14
-
15
- ## Features
16
-
17
- - **Turn-level auto collapse** (Level 1): when a turn finishes, the whole working process collapses into a `已处理 X秒` (processed in Xs) row, leaving only the model's final text. Click to expand the full workflow (context injections → thinking → tool calls → intermediate text → final text).
18
- - **Second-level rows**: after expanding level one, tool-call groups and think blocks each collapse into a single chip row (`正在运行 {command}` / `运行了命令` / `已思考`), click to expand/collapse; adjacent tool groups merge, while body text serves as a hard boundary (never merged across).
19
- - **Third-level think merge**: expanding `已思考` shows consecutive think rows merged into one row titled `Think · first line`, click to reveal the merged content block; raw fourth-level rows never appear.
20
- - **Native visual alignment**: 16px icon box / 14px glyph / 24px line height / 16px row gap; colors use DSH native tokens (`--dsw-alias-label-*`); think and command icons come from DSH native icons (`IconThinkOutline14` / `IconApiOutline14`).
21
- - **Stream-friendly**: in-place `assistant-step` body updates, React node replacement, and out-of-order history mounting are reconciled on every pass; running rows use a smooth text pulse motion, disabled by `prefers-reduced-motion`.
22
- - **Complete work-node coverage**: top-level `command` / `manual-compaction`, context nodes, and image-only finals follow the same turn semantics as tool calls.
23
- - **Configurable status text**: in Settings Plugins Plugin configuration, edit the status prompt (default `Deep sleeping...`); leaving it blank restores the official `Deep diving...`.
24
- - **Fully reversible**: uninstalling (HMR stop) restores every collapsed/hidden/rewritten node.
25
-
26
- ## Install
27
-
28
- Published npm package (recommended; uses the prebuilt release):
29
-
30
- ```bash
31
- dsh plugin --profile web add "dsh-auto-collapse"
32
- ```
33
-
34
- Install from GitHub when using the development version or following `main`:
35
-
36
- ```bash
37
- dsh plugin --profile web add "github:a179-sanae/dsh-auto-collapse#main"
38
- ```
39
-
40
- Restart the DSH web service (or trigger plugin HMR), then hard-refresh the page (`Ctrl+Shift+R`). No configuration needed.
41
-
42
- ## Development
43
-
44
- ### Project layout
45
-
46
- ```
47
- src/fold.ts core: FoldController (state machine) + findBlocks (block recognition) + collapse/expand logic
48
- src/client.ts browser entry (plugin registration)
49
- src/index.ts host half (host-side entry)
50
- build.mjs build script (generates lib/client.js, lib/index.js, and lib/types/*)
51
- tsconfig.build.json TypeScript declaration build configuration
52
- deploy.mjs safe deploy: validate → back up → replace → verified restart → hash check/rollback; Windows uses PowerShell, Linux/macOS use lsof + ps
53
- cordis.patch.yml profile tree mounting
54
- test/ fake-DOM contract, race, session-switch, and 40-order permutation regressions
55
- ```
56
-
57
- ### Checks
58
-
59
- ```bash
60
- npm run check
61
- ```
62
-
63
- Runs TypeScript checking, a fresh build, and the complete regression suite.
64
-
65
- ### Quick deploy (local dev)
66
-
67
- ```bash
68
- npm run deploy
69
- ```
70
-
71
- Validates the plugin/DSH package identities and the process listening on port 3080, then creates a timestamped backup, replaces the bundle, restarts DSH, and verifies the served hash. Failures restore the old bundle. Windows, Linux, and macOS are supported; Unix systems locate DSH from `npm root -g` by default and use `HOME` for the profile path. Linux/macOS require `lsof`. Override defaults with `DSH_AUTO_COLLAPSE_LIB`, `DSH_DIR`, `DSH_WEB_PORT`, and `DSH_LOG_DIR`.
72
-
73
- ### Publishing a new version
74
-
75
- Update the `version` in `package.json`, then publish to npm (the `prepack` hook builds automatically):
76
-
77
- ```bash
78
- npm publish --access public
79
- ```
80
-
81
- For local development, you can pack a tgz without publishing:
82
-
83
- ```bash
84
- npm pack --pack-destination <local-plugin-dir>
85
- ```
86
-
87
- Point the plugin dependency in the profile's `package.json` to the new tarball and reinstall the plugin.
88
-
89
- ### Key mechanisms
90
-
91
- - **Block recognition** (`findBlocks`): top-level nodes are classified as tool calls, command/manual-compaction cards, contexts, thinking, or body content; user/steering/turn-tail nodes are hard boundaries.
92
- - **Segment reconciliation**: every pass rebuilds segments from current DOM order. The last `assistant-step` containing text or media is final; earlier bodies are intermediate work. Stable flow/node keys preserve UI state without one-shot mutation bookkeeping.
93
- - **Duration**: streaming segments each track their own first running observation; historical segments parse official duration or the `timeStart`/turn-tail range. Whole minutes omit the seconds field.
94
- - **React coexistence and reversibility**: replaced nodes rebind by stable key, removed level-one rows rebuild with their expansion state, and every inline `display` value is saved before mutation and restored exactly.
95
-
96
- ## License
97
-
98
- MIT
1
+ # dsh-auto-collapse
2
+
3
+ > A DeepSeek Harness Web client plugin that auto-collapses tool cards and Think blocks into one-line summaries, so the chat keeps only what the model says.
4
+ >
5
+ > 中文: [README.md](./README.md)
6
+ >
7
+ > This plugin is listed in the Plugin marketplace: [marketplace link](https://www.dsh.so/artifact/dsh-auto-collapse/)
8
+
9
+ ## What it does
10
+
11
+ `dsh-auto-collapse` is a pure front-end DOM plugin for the DeepSeek Harness Web chat UI. It collapses the working process into one-line summaries — tool calls and reasoning no longer fill the screen, giving the chat the collapsible look of the VSCode Codex desktop client, **and it also rewrites the official status line (“深度求索中...”, formerly “Deep diving”) to a configurable “Deep sleeping...”**. It never modifies message content — it only controls visibility of the working process.
12
+
13
+ ## Preview
14
+
15
+ ![Collapsed workflow](assets/screenshot.png)
16
+
17
+ ## Features
18
+
19
+ - **Turn-level auto collapse** (Level 1): when a turn finishes, the whole working process collapses into a `已处理 X秒` (processed in Xs) row, leaving only the model's final text. Click to expand the full workflow (context injections → thinking → tool calls → intermediate text → final text). On DSH 0.1.2+, which ships a native per-turn summary row (tool-call · message · subagent counts), the Level 1 row yields to it and is no longer shown; second-level folding is unaffected.
20
+ - **Second-level rows**: after expanding level one, tool-call groups and think blocks each collapse into a single chip row (`正在运行 {command}` / `运行了命令` / `已思考`), click to expand/collapse; adjacent tool groups merge, while body text serves as a hard boundary (never merged across).
21
+ - **Third-level think merge**: expanding `已思考` shows consecutive think rows merged into one row titled `Think · first line`, click to reveal the merged content block; raw fourth-level rows never appear.
22
+ - **Native visual alignment**: 16px icon box / 14px glyph / 24px line height / 16px row gap; colors use DSH native tokens (`--dsw-alias-label-*`); think and command icons come from DSH native icons (`IconThinkOutline14` / `IconApiOutline14`).
23
+ - **Stream-friendly**: in-place `assistant-step` body updates, React node replacement, and out-of-order history mounting are reconciled on every pass; running rows use a smooth text pulse motion, disabled by `prefers-reduced-motion`.
24
+ - **Complete work-node coverage**: top-level `command` / `manual-compaction`, context nodes, and image-only finals follow the same turn semantics as tool calls.
25
+ - **Configurable status text**: in Settings → Plugins → Plugin configuration, edit the status prompt (default `Deep sleeping...`); leaving it blank restores the official copy (“深度求索中...” in current builds). The host-appended elapsed suffix (e.g. `33秒`) is preserved.
26
+ - **Fully reversible**: uninstalling (HMR stop) restores every collapsed/hidden/rewritten node.
27
+
28
+ ## Install
29
+
30
+ Published npm package (recommended; uses the prebuilt release):
31
+
32
+ ```bash
33
+ dsh plugin --profile web add "dsh-auto-collapse"
34
+ ```
35
+
36
+ Install from GitHub when using the development version or following `main`:
37
+
38
+ ```bash
39
+ dsh plugin --profile web add "github:a179-sanae/dsh-auto-collapse#main"
40
+ ```
41
+
42
+ Restart the DSH web service (or trigger plugin HMR), then hard-refresh the page (`Ctrl+Shift+R`). No configuration needed.
43
+
44
+ ## Development
45
+
46
+ ### Project layout
47
+
48
+ ```
49
+ src/fold.ts core: FoldController (state machine) + findBlocks (block recognition) + collapse/expand logic
50
+ src/client.ts browser entry (plugin registration)
51
+ src/index.ts host half (host-side entry)
52
+ build.mjs build script (generates lib/client.js, lib/index.js, and lib/types/*)
53
+ tsconfig.build.json TypeScript declaration build configuration
54
+ deploy.mjs safe deploy: validate → back up → replace → verified restart → hash check/rollback; Windows uses PowerShell, Linux/macOS use lsof + ps
55
+ cordis.patch.yml profile tree mounting
56
+ test/ fake-DOM contract, race, session-switch, and 40-order permutation regressions
57
+ ```
58
+
59
+ ### Checks
60
+
61
+ ```bash
62
+ npm run check
63
+ ```
64
+
65
+ Runs TypeScript checking, a fresh build, and the complete regression suite.
66
+
67
+ ### Quick deploy (local dev)
68
+
69
+ ```bash
70
+ npm run deploy
71
+ ```
72
+
73
+ Validates the plugin/DSH package identities and the process listening on port 3080, then creates a timestamped backup, replaces the bundle, restarts DSH, and verifies the served hash. Failures restore the old bundle. Windows, Linux, and macOS are supported; Unix systems locate DSH from `npm root -g` by default and use `HOME` for the profile path. Linux/macOS require `lsof`. Override defaults with `DSH_AUTO_COLLAPSE_LIB`, `DSH_DIR`, `DSH_WEB_PORT`, and `DSH_LOG_DIR`.
74
+
75
+ ### Publishing a new version
76
+
77
+ Update the `version` in `package.json`, then publish to npm (the `prepack` hook builds automatically):
78
+
79
+ ```bash
80
+ npm publish --access public
81
+ ```
82
+
83
+ For local development, you can pack a tgz without publishing:
84
+
85
+ ```bash
86
+ npm pack --pack-destination <local-plugin-dir>
87
+ ```
88
+
89
+ Point the plugin dependency in the profile's `package.json` to the new tarball and reinstall the plugin.
90
+
91
+ ### Key mechanisms
92
+
93
+ - **Block recognition** (`findBlocks`): top-level nodes are classified as tool calls, command/manual-compaction cards, contexts, thinking, or body content; user/steering/turn-tail nodes are hard boundaries.
94
+ - **Segment reconciliation**: every pass rebuilds segments from current DOM order. The last `assistant-step` containing text or media is final; earlier bodies are intermediate work. Stable flow/node keys preserve UI state without one-shot mutation bookkeeping.
95
+ - **Duration**: streaming segments each track their own first running observation; historical segments parse official duration or the `timeStart`/turn-tail range. Whole minutes omit the seconds field.
96
+ - **React coexistence and reversibility**: replaced nodes rebind by stable key, removed level-one rows rebuild with their expansion state, and every inline `display` value is saved before mutation and restored exactly.
97
+
98
+ ## License
99
+
100
+ MIT
package/README.md CHANGED
@@ -1,99 +1,101 @@
1
- # dsh-auto-collapse
2
-
3
- > DeepSeek Harness Web 客户端插件:把会话里的工具卡片与 Think 推理块自动折叠成一行摘要,让界面只保留模型说的话。
4
- >
5
- > English: [README.en.md](./README.en.md)
6
-
7
- ## 这是什么
8
-
9
- `dsh-auto-collapse` 是一个纯前端 DOM 插件,挂在 DeepSeek Harness Web 聊天界面上,把工作流程折叠成一行行摘要——工具调用、推理过程不再占据整屏,呈现接近 VSCode Codex 桌面端的折叠体验,**同时将“Deep diving” 修改为可配置的“Deep sleeping...”**。它不改动消息内容,只控制工作流程的显示状态。
10
-
11
- ## 效果预览
12
-
13
- ![折叠效果](assets/screenshot.png)
14
-
15
- ## 特性
16
-
17
- - **回合完成自动收起**(一级):每个回合完成后,工作过程收成一行 `已处理 X秒`,只留模型最终正文;点击展开完整工作流程(上下文注入 → 思考 → 工具调用 → 过程正文 → 最终正文)。
18
- - **二级折叠行**:展开一级后,工具调用组与思考块各自折叠成一行 chip(`正在运行 {命令}` / `运行了命令` / `已思考`),点击展开/收起;相邻工具组合并,正文输出是硬边界(不会跨正文合并)。
19
- - **三级思考合并**:展开 `已思考` 后,连续思考合并为一个三级思考行(标题 `Think · 第一句`),点击展开合并内容块;原始四级行不出现。
20
- - **原生视觉对齐**:图标盒 16px / glyph 14px / 行高 24px / 行距 16px,颜色使用 DSH 原生 token(`--dsw-alias-label-*`),思考与命令图标取自 DSH 原生图标(`IconThinkOutline14` / `IconApiOutline14`)。
21
- - **展开/收起过渡动画**:点击驱动的展开(淡入 + 4px 上移,合并思考正文带高度展开)与收起(镜像淡出,后代随祖先 seat 整体消失、无跳变)均为 180ms;仅用户点击触发动画,流式协调器决策保持瞬时。
22
- - **流式友好**:同一个 `assistant-step` 原地补正文、React 换节点和历史乱序挂载都会重新协调;running 状态带文字平滑呼吸动画,`prefers-reduced-motion` 下停止动画(过渡动画同样禁用)。
23
- - **完整工作类型**:除 tool-call 外,顶层 `command` / `manual-compaction`、context 和纯图片 final 都按同一回合语义处理。
24
- - **可配置状态提示词**:在 设置 插件 → 插件配置 中可以编辑“状态提示词”,默认 `Deep sleeping...`;留空保存后恢复官方 `Deep diving...`。
25
- - **可逆**:卸载(HMR stop)时完整还原所有折叠/隐藏/改写。
26
-
27
- ## 安装
28
-
29
- 已发布 npm 包(推荐,使用构建好的版本):
30
-
31
- ```bash
32
- dsh plugin --profile web add "dsh-auto-collapse"
33
- ```
34
-
35
- 从 GitHub 安装(开发版或需要跟随 `main` 分支时):
36
-
37
- ```bash
38
- dsh plugin --profile web add "github:a179-sanae/dsh-auto-collapse#main"
39
- ```
40
-
41
- 安装后重启 DSH web 服务(或触发插件 HMR),页面 `Ctrl+Shift+R` 硬刷新即可生效。无需任何配置。
42
-
43
- ## 开发
44
-
45
- ### 项目结构
46
-
47
- ```
48
- src/fold.ts 核心:FoldController(状态机)+ findBlocks(块识别)+ 折叠/展开逻辑
49
- src/client.ts 浏览器端入口(注册插件)
50
- src/index.ts host half(宿主端,Host half)
51
- build.mjs 构建脚本(Build script):生成 lib/client.js、lib/index.js 与 lib/types/*
52
- tsconfig.build.json TypeScript 声明构建配置(Declaration build config
53
- deploy.mjs 安全部署(Safe deploy):校验 → 备份 → 替换 → 身份核验重启 → 哈希验证/回滚(DSH web 输出持久化到 ~/.dsh/logs/web.{out,err}.log);Windows 使用 PowerShell,Linux/macOS 使用 lsof + ps
54
- cordis.patch.yml profile 树挂载
55
- test/ fake DOM 契约、竞态、会话切换与 40 组乱序排列回归
56
- ```
57
-
58
- ### 检查
59
-
60
- ```bash
61
- npm run check
62
- ```
63
-
64
- 依次执行 TypeScript 检查、构建和全部回归测试。
65
-
66
- ### 快速部署(本机开发)
67
-
68
- ```bash
69
- npm run deploy
70
- ```
71
-
72
- 脚本先核验插件/DSH 包名和 3080 监听进程身份,再做时间戳备份、替换、重启与服务端哈希验证;失败自动恢复旧 bundle。支持 Windows、Linux 和 macOS;Unix 系统默认从 `npm root -g` 定位 DSH 包,并使用 `HOME` 定位 profile。Linux/macOS 需要安装 `lsof`。可用 `DSH_AUTO_COLLAPSE_LIB`、`DSH_DIR`、`DSH_WEB_PORT`、`DSH_LOG_DIR` 覆盖默认路径。
73
-
74
- ### 发布新版本
75
-
76
- 更新 `package.json` 中的 `version` 后,发布到 npm(`prepack` 钩子会自动构建):
77
-
78
- ```bash
79
- npm publish --access public
80
- ```
81
-
82
- 本机开发也可以只打包为 tgz:
83
-
84
- ```bash
85
- npm pack --pack-destination <本地插件目录>
86
- ```
87
-
88
- 将 profile 的 `package.json` 中插件依赖更新为新 tgz 路径后重新安装插件。
89
-
90
- ### 关键机制
91
-
92
- - **块识别**(`findBlocks`):顶层节点按 tool-call、command/manual-compaction、context、thinking 和正文分类;user/steering/turn-tail 是不可跨越的硬边界。
93
- - **segment 协调**:每轮根据当前 DOM 顺序重建 segment;最后一个含文本或媒体的 `assistant-step` 是 final,其余正文是中间过程。稳定 flow/key 复用展开状态,不依赖一次性 mutation 事件。
94
- - **时长**:流式回合按 segment 分别记录 running 起点;历史回合从官方时长或 `timeStart`/turn-tail 解析。格式 `X秒` / `X分Y秒`,整分省略秒位。
95
- - **React 共存与可逆性**:节点替换后按稳定 key 重新绑定;一级行被移除会按原展开状态重建;所有 inline `display` 在首次改写前保存并精确恢复。
96
-
97
- ## 许可
98
-
99
- MIT
1
+ # dsh-auto-collapse
2
+
3
+ > DeepSeek Harness Web 客户端插件:把会话里的工具卡片与 Think 推理块自动折叠成一行摘要,让界面只保留模型说的话。
4
+ >
5
+ > English: [README.en.md](./README.en.md)
6
+ >
7
+ > 本插件已收录到插件市场(Plugin marketplace):[市场链接](https://www.dsh.so/artifact/dsh-auto-collapse/)
8
+
9
+ ## 这是什么
10
+
11
+ `dsh-auto-collapse` 是一个纯前端 DOM 插件,挂在 DeepSeek Harness Web 聊天界面上,把工作流程折叠成一行行摘要——工具调用、推理过程不再占据整屏,呈现接近 VSCode Codex 桌面端的折叠体验,**同时将官方运行状态文案(“深度求索中...” / 旧版 “Deep diving”)修改为可配置的“Deep sleeping...”**。它不改动消息内容,只控制工作流程的显示状态。
12
+
13
+ ## 效果预览
14
+
15
+ ![折叠效果](assets/screenshot.png)
16
+
17
+ ## 特性
18
+
19
+ - **回合完成自动收起**(一级):每个回合完成后,工作过程收成一行 `已处理 X秒`,只留模型最终正文;点击展开完整工作流程(上下文注入 思考 → 工具调用 → 过程正文 → 最终正文)。DSH 0.1.2+ 自带原生回合摘要行(工具调用 · 消息 · subagent 计数)时,一级行向它让位不再单独显示,二级折叠不受影响。
20
+ - **二级折叠行**:展开一级后,工具调用组与思考块各自折叠成一行 chip(`正在运行 {命令}` / `运行了命令` / `已思考`),点击展开/收起;相邻工具组合并,正文输出是硬边界(不会跨正文合并)。
21
+ - **三级思考合并**:展开 `已思考` 后,连续思考合并为一个三级思考行(标题 `Think · 第一句`),点击展开合并内容块;原始四级行不出现。
22
+ - **原生视觉对齐**:图标盒 16px / glyph 14px / 行高 24px / 行距 16px,颜色使用 DSH 原生 token(`--dsw-alias-label-*`),思考与命令图标取自 DSH 原生图标(`IconThinkOutline14` / `IconApiOutline14`)。
23
+ - **展开/收起过渡动画**:点击驱动的展开(淡入 + 4px 上移,合并思考正文带高度展开)与收起(镜像淡出,后代随祖先 seat 整体消失、无跳变)均为 180ms;仅用户点击触发动画,流式协调器决策保持瞬时。
24
+ - **流式友好**:同一个 `assistant-step` 原地补正文、React 换节点和历史乱序挂载都会重新协调;running 状态带文字平滑呼吸动画,`prefers-reduced-motion` 下停止动画(过渡动画同样禁用)。
25
+ - **完整工作类型**:除 tool-call 外,顶层 `command` / `manual-compaction`、context 和纯图片 final 都按同一回合语义处理。
26
+ - **可配置状态提示词**:在 设置 → 插件 → 插件配置 中可以编辑“状态提示词”,默认 `Deep sleeping...`;留空保存后恢复官方原文(当前版本为“深度求索中...”)。宿主追加的用时后缀(如 `33秒`)保留。
27
+ - **可逆**:卸载(HMR stop)时完整还原所有折叠/隐藏/改写。
28
+
29
+ ## 安装
30
+
31
+ 已发布 npm 包(推荐,使用构建好的版本):
32
+
33
+ ```bash
34
+ dsh plugin --profile web add "dsh-auto-collapse"
35
+ ```
36
+
37
+ 从 GitHub 安装(开发版或需要跟随 `main` 分支时):
38
+
39
+ ```bash
40
+ dsh plugin --profile web add "github:a179-sanae/dsh-auto-collapse#main"
41
+ ```
42
+
43
+ 安装后重启 DSH web 服务(或触发插件 HMR),页面 `Ctrl+Shift+R` 硬刷新即可生效。无需任何配置。
44
+
45
+ ## 开发
46
+
47
+ ### 项目结构
48
+
49
+ ```
50
+ src/fold.ts 核心:FoldController(状态机)+ findBlocks(块识别)+ 折叠/展开逻辑
51
+ src/client.ts 浏览器端入口(注册插件)
52
+ src/index.ts host half(宿主端,Host half
53
+ build.mjs 构建脚本(Build script):生成 lib/client.js、lib/index.js lib/types/*
54
+ tsconfig.build.json TypeScript 声明构建配置(Declaration build config)
55
+ deploy.mjs 安全部署(Safe deploy):校验 → 备份 → 替换 → 身份核验重启 → 哈希验证/回滚(DSH web 输出持久化到 ~/.dsh/logs/web.{out,err}.log);Windows 使用 PowerShell,Linux/macOS 使用 lsof + ps
56
+ cordis.patch.yml profile 树挂载
57
+ test/ fake DOM 契约、竞态、会话切换与 40 组乱序排列回归
58
+ ```
59
+
60
+ ### 检查
61
+
62
+ ```bash
63
+ npm run check
64
+ ```
65
+
66
+ 依次执行 TypeScript 检查、构建和全部回归测试。
67
+
68
+ ### 快速部署(本机开发)
69
+
70
+ ```bash
71
+ npm run deploy
72
+ ```
73
+
74
+ 脚本先核验插件/DSH 包名和 3080 监听进程身份,再做时间戳备份、替换、重启与服务端哈希验证;失败自动恢复旧 bundle。支持 Windows、Linux 和 macOS;Unix 系统默认从 `npm root -g` 定位 DSH 包,并使用 `HOME` 定位 profile。Linux/macOS 需要安装 `lsof`。可用 `DSH_AUTO_COLLAPSE_LIB`、`DSH_DIR`、`DSH_WEB_PORT`、`DSH_LOG_DIR` 覆盖默认路径。
75
+
76
+ ### 发布新版本
77
+
78
+ 更新 `package.json` 中的 `version` 后,发布到 npm(`prepack` 钩子会自动构建):
79
+
80
+ ```bash
81
+ npm publish --access public
82
+ ```
83
+
84
+ 本机开发也可以只打包为 tgz:
85
+
86
+ ```bash
87
+ npm pack --pack-destination <本地插件目录>
88
+ ```
89
+
90
+ profile 的 `package.json` 中插件依赖更新为新 tgz 路径后重新安装插件。
91
+
92
+ ### 关键机制
93
+
94
+ - **块识别**(`findBlocks`):顶层节点按 tool-call、command/manual-compaction、context、thinking 和正文分类;user/steering/turn-tail 是不可跨越的硬边界。
95
+ - **segment 协调**:每轮根据当前 DOM 顺序重建 segment;最后一个含文本或媒体的 `assistant-step` 是 final,其余正文是中间过程。稳定 flow/key 复用展开状态,不依赖一次性 mutation 事件。
96
+ - **时长**:流式回合按 segment 分别记录 running 起点;历史回合从官方时长或 `timeStart`/turn-tail 解析。格式 `X秒` / `X分Y秒`,整分省略秒位。
97
+ - **React 共存与可逆性**:节点替换后按稳定 key 重新绑定;一级行被移除会按原展开状态重建;所有 inline `display` 在首次改写前保存并精确恢复。
98
+
99
+ ## 许可
100
+
101
+ MIT
package/lib/client.js CHANGED
@@ -36,6 +36,7 @@ var __dshcfBundle = (() => {
36
36
  // src/fold.ts
37
37
  var STYLE_ID = "dshcf-style";
38
38
  var DEFAULT_STATUS_TEXT = "Deep sleeping...";
39
+ var TURN_STATUS_COPY_RE = /Deep diving[.…]*|深度求索中\s*[.…]*/;
39
40
  var ANIM_DURATION_MS = 180;
40
41
  var ANIM_EASING = "ease-out";
41
42
  var DISPLAY_OWNED_PROP = "--dshcf-display-owned";
@@ -58,6 +59,8 @@ var __dshcfBundle = (() => {
58
59
  cordis_stop: "Stop",
59
60
  cordis_undefine: "Remove"
60
61
  };
62
+ var TURN_PROCESS_KIND = "turn-process";
63
+ var NATIVE_OPEN_ATTR = "data-open";
61
64
  var CHIP_CSS = `
62
65
  .dshcf-chip {
63
66
  box-sizing: border-box;
@@ -395,6 +398,7 @@ var __dshcfBundle = (() => {
395
398
  this.bodyTextCache = /* @__PURE__ */ new WeakMap();
396
399
  /** 自上次 pass 以来子树发生变化的 flow 顶层消息;pass 开头统一失效。 */
397
400
  this.dirtyMessages = /* @__PURE__ */ new Set();
401
+ /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
398
402
  /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
399
403
  * 用 Map 不用 WeakMap——switchFlow/stop 需要遍历全量 cancel。 */
400
404
  this.pendingAnims = /* @__PURE__ */ new Map();
@@ -413,6 +417,20 @@ var __dshcfBundle = (() => {
413
417
  * attributes 批次(流式文本、data-state 翻转)不改变块结构,跳过全量
414
418
  * querySelectorAll 重扫(issue #14:长会话下每轮重扫造成主线程卡顿)。 */
415
419
  this.structureDirty = true;
420
+ /** 本轮 pass 的原生 turn 摘要快照(turn id → 开合态):每轮重建,不跨轮复用;
421
+ * data-open 翻转不改变块结构也能驱动重算,见 buildNativeTurnMap。 */
422
+ this.nativeTurns = /* @__PURE__ */ new Map();
423
+ /** 被折叠掏空后藏起的中间包装层(真机 44px vs 28px 真因:think 行全隐后
424
+ * 其父容器变零高度空壳,仍作为 flex item 参与父级 gap,凭空多出 16px)。
425
+ * 内容恢复(展开/流式追加/块转世)时同函数恢复显示;switchFlow 清空。 */
426
+ this.emptiedWrappers = /* @__PURE__ */ new Set();
427
+ /** 本轮 pass 内有过 display 实写(hide/restore 瞬时路径)。空洞发现的触发
428
+ * 条件之一:纯 display 收放不产生 childList,不触发 structureDirty。
429
+ * settle 触发的后续 pass 则由 settleFired 覆盖。读后即清(见发现调用点)。 */
430
+ this.displayTouched = false;
431
+ /** 有 fade 自然结算过(chipSettle 跑过)。结算本身不写 display,但它意味着
432
+ * 某行刚变隐藏——空洞可能刚形成。 */
433
+ this.settleFired = false;
416
434
  /** 滚动稳定化(issue #14):flow 最近的滚动容器缓存(按 flow 身份失效)。 */
417
435
  this.scrollContainer = null;
418
436
  this.scrollContainerFlow = null;
@@ -448,7 +466,10 @@ var __dshcfBundle = (() => {
448
466
  childList: true,
449
467
  subtree: true,
450
468
  attributes: true,
451
- attributeFilter: ["data-selected", "data-state"],
469
+ // 原生 TurnProcess 开合只改 attribute(成员 data-turn-process-hidden、
470
+ // 摘要按钮 data-open),必须进过滤器才能即时调度;插件自身从不写
471
+ // 这两个属性,不会自激。
472
+ attributeFilter: ["data-selected", "data-state", "data-turn-process-hidden", "data-open"],
452
473
  // 流式文本更新(React 改 text node 的 data)属于 characterData
453
474
  // mutation:不观察则二级摘要/滚动跟随只能靠偶发结构变化驱动,
454
475
  // 变成“隔几秒跳一次”。所有文本写入都有守卫(值不变不写),
@@ -518,6 +539,17 @@ var __dshcfBundle = (() => {
518
539
  return;
519
540
  }
520
541
  }
542
+ for (const el of [...this.emptiedWrappers]) {
543
+ if (!el.isConnected) {
544
+ this.emptiedWrappers.delete(el);
545
+ continue;
546
+ }
547
+ if (!this.isHollow(el)) {
548
+ this.restoreElement(el);
549
+ if (el.style.display === "none") el.style.display = "";
550
+ this.emptiedWrappers.delete(el);
551
+ }
552
+ }
521
553
  }
522
554
  stop() {
523
555
  this.disposed = true;
@@ -652,6 +684,7 @@ var __dshcfBundle = (() => {
652
684
  this.currentBlocks = new Map(blocks.map((block) => [block.key, block]));
653
685
  const segments = buildSegments(flow, blocks, (el) => this.hasBodyCached(el));
654
686
  const liveSegmentKeys = new Set(segments.map((segment) => segment.key));
687
+ this.nativeTurns = buildNativeTurnMap(flow);
655
688
  const scrollAnchor = this.captureScrollAnchor(flow);
656
689
  for (const segment of segments) {
657
690
  if (!segment.running) continue;
@@ -668,6 +701,12 @@ var __dshcfBundle = (() => {
668
701
  if (!snapshot.closed || snapshot.running || !snapshot.hasWork) continue;
669
702
  completedKeys.add(snapshot.key);
670
703
  this.completedOnce.add(snapshot.key);
704
+ if (segmentHasNativeTurn(snapshot, this.nativeTurns)) {
705
+ const prev = this.segmentStates.get(snapshot.key);
706
+ prev?.row?.remove();
707
+ if (prev !== void 0) this.segmentStates.delete(snapshot.key);
708
+ continue;
709
+ }
671
710
  let state = this.segmentStates.get(snapshot.key);
672
711
  if (state === void 0) {
673
712
  state = { key: snapshot.key, row: null, expanded: false, snapshot };
@@ -697,6 +736,21 @@ var __dshcfBundle = (() => {
697
736
  seenBlocks.add(block.key);
698
737
  this.reconcileBlock(block, segmentByBlock.get(block.key) ?? null, desiredHidden);
699
738
  }
739
+ if (rebuildBlocks || this.displayTouched || this.settleFired) {
740
+ const hosts = /* @__PURE__ */ new Set();
741
+ for (const block of this.currentBlocks.values()) {
742
+ if (block.host.isConnected) hosts.add(block.host);
743
+ }
744
+ for (const segment of segments) {
745
+ if (segment.finalStep?.isConnected) hosts.add(segment.finalStep);
746
+ for (const middle of segment.middleSteps) {
747
+ if (middle.isConnected) hosts.add(middle);
748
+ }
749
+ }
750
+ this.discoverHollowWrappers(hosts, desiredHidden);
751
+ }
752
+ this.displayTouched = false;
753
+ this.settleFired = false;
700
754
  for (const segment of segments) {
701
755
  const state = this.segmentStates.get(segment.key);
702
756
  const collapse = state !== void 0 && !state.expanded;
@@ -719,6 +773,11 @@ var __dshcfBundle = (() => {
719
773
  if (segment.finalStep !== null) this.retainDisplayControl(segment.finalStep, desiredHidden);
720
774
  }
721
775
  this.cleanupStaleChips(seenBlocks);
776
+ for (const [el, record] of [...this.pendingAnims]) {
777
+ if (record.target === "hidden" && record.kind === "fade" && this.pendingAnims.get(el) === record && this.isAnimOverdue(record.anim)) {
778
+ this.finishFadeCollapse(el, record);
779
+ }
780
+ }
722
781
  this.restoreUnusedDisplays(desiredHidden);
723
782
  for (const state of this.segmentStates.values()) this.placeProcessedRow(flow, state);
724
783
  this.stabilizeScrollAfterFold(scrollAnchor);
@@ -747,7 +806,7 @@ var __dshcfBundle = (() => {
747
806
  * 找不到时不缓存(内容增长后祖先可能变为可滚动),每 pass 重探的代价
748
807
  * 只是几次 clean-layout 的 computed style / scrollHeight 读取。 */
749
808
  findScrollContainer(flow) {
750
- if (this.scrollContainerFlow === flow && this.scrollContainer !== null && this.scrollContainer.isConnected) {
809
+ if (this.scrollContainerFlow === flow && this.scrollContainer !== null && this.scrollContainer.isConnected && nodeWithin(flow, this.scrollContainer)) {
751
810
  return this.scrollContainer;
752
811
  }
753
812
  let node = flow.parentElement;
@@ -808,6 +867,8 @@ var __dshcfBundle = (() => {
808
867
  this.bodyTextCache = /* @__PURE__ */ new WeakMap();
809
868
  this.dirtyMessages.clear();
810
869
  this.auditDisplays.clear();
870
+ this.nativeTurns.clear();
871
+ this.emptiedWrappers.clear();
811
872
  this.structureDirty = true;
812
873
  this.restoreAllDisplays();
813
874
  restoreTurnStatus(this.turnStatusTexts);
@@ -859,7 +920,29 @@ var __dshcfBundle = (() => {
859
920
  }
860
921
  if (row.parentElement !== flow || row.nextElementSibling !== target) target.before(row);
861
922
  }
923
+ /** 原生收起时隐藏本块插件自有 overlay(chip/合并行+内容块):只做
924
+ * display 直写,不清钉住之外的账本、不删展开态,供原生再展开后复用。
925
+ * 在途 WAAPI 动画不追踪取消——收尾回调只做幂等终态对齐,无残留。 */
926
+ hideOverlayForNativeCollapse(block) {
927
+ const existing = this.chips.get(block.key)?.chip;
928
+ if (existing !== void 0 && existing.style.display !== "none") {
929
+ existing.style.marginBottom = "";
930
+ existing.style.display = "none";
931
+ }
932
+ const row = this.mergedThinks.get(block.host);
933
+ if (row !== void 0 && row.style.display !== "none") {
934
+ row.style.display = "none";
935
+ const body = row.nextElementSibling;
936
+ if (body instanceof HTMLElement && body.classList.contains("dshcf-merged-body") && body.style.display !== "none") {
937
+ body.style.display = "none";
938
+ }
939
+ }
940
+ }
862
941
  reconcileBlock(block, segment, desiredHidden) {
942
+ if (blockNativelyCollapsed(block, this.nativeTurns)) {
943
+ this.hideOverlayForNativeCollapse(block);
944
+ return;
945
+ }
863
946
  const state = segment === null ? void 0 : this.segmentStates.get(segment.key);
864
947
  const segmentAnimatableBlocks = segment === null ? void 0 : this.animatableSegmentBlocks.get(segment.key);
865
948
  const animate = this.animatableKeys.has(block.key) || segment !== null && this.animatableKeys.has(segment.key) && (segmentAnimatableBlocks === void 0 || segmentAnimatableBlocks.has(block.key));
@@ -881,7 +964,7 @@ var __dshcfBundle = (() => {
881
964
  existing.style.display = "none";
882
965
  }
883
966
  }
884
- this.releaseMergedThink(block.host, animate);
967
+ this.syncEmptiedWrappers(block, desiredHidden);
885
968
  return;
886
969
  }
887
970
  let expanded = this.blockExpanded.get(block.key) ?? false;
@@ -901,6 +984,7 @@ var __dshcfBundle = (() => {
901
984
  if (chipWasHidden && animate && !(hostAnimate && block.mount === "inside")) this.revealVisual(chip);
902
985
  if (expanded || !this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
903
986
  const chipSettle = () => {
987
+ this.settleFired = true;
904
988
  if (!this.hasPendingCollapse(block)) this.unpinChipMargin(chip);
905
989
  };
906
990
  for (const container of block.containers) {
@@ -922,9 +1006,123 @@ var __dshcfBundle = (() => {
922
1006
  } else {
923
1007
  if (this.releaseMergedThink(block.host, animate, chipSettle)) this.pinChipMargin(chip);
924
1008
  }
1009
+ this.syncEmptiedWrappers(block, desiredHidden);
925
1010
  chip.classList.toggle("dshcf-has-body", block.mount === "inside" && this.hasBodyCached(block.host));
926
1011
  updateChip(chip, block.rows, expanded);
927
1012
  }
1013
+ /**
1014
+ * 掏空包装层对账(真机 44px vs 28px 真因修复)。
1015
+ *
1016
+ * 折叠把某容器的子行全部 display:none 后,该容器变零高度空壳,但仍是
1017
+ * 父级 flex 的 item 并参与 gap(如正文体 flex-column gap:16px),凭空多出
1018
+ * 一份间距。隐藏这类空壳可让 gap 塌缩;子内容恢复可见(展开/流式追加/
1019
+ * 块转世)时恢复显示。
1020
+ *
1021
+ * 安全边界:
1022
+ * - 只看 block.host 内部、不含 host 本人(宿主显隐归 segment 逻辑);
1023
+ * - 跳过结构 seat(data-chat-flow-kind / data-chat-anchor-key)与插件
1024
+ * 自身 overlay(dshcf- 前缀类);
1025
+ * - 只隐藏内容空洞(无可见子内容):零高度无文本的空壳 display:none 与
1026
+ * 保持显示像素一致,只塌缩父级 gap;任何一方恢复内容即恢复显示。
1027
+ */
1028
+ /**
1029
+ * 空洞包装层兜底发现:按宿主直扫 div(仅结构变化轮次调用)。
1030
+ * 只做隐藏侧:空洞即藏并记入 tracked;恢复侧由 syncEmptiedWrappers 的
1031
+ * tracked 复核与 audit 承担。与按行 walk 共用同一安全边界(结构 seat /
1032
+ * 插件 overlay / 宿主本人不碰)。
1033
+ */
1034
+ discoverHollowWrappers(hosts, desiredHidden) {
1035
+ for (const host of hosts) {
1036
+ if (!host.isConnected) continue;
1037
+ let divs;
1038
+ try {
1039
+ divs = [...host.querySelectorAll("div")];
1040
+ } catch {
1041
+ continue;
1042
+ }
1043
+ for (const el of divs) {
1044
+ if (el === host || !(el instanceof HTMLElement)) continue;
1045
+ const cls = el.className;
1046
+ if (typeof cls === "string" && cls.split(" ").some((c) => c.startsWith("dshcf-"))) continue;
1047
+ if (el.hasAttribute("data-chat-flow-kind") || el.hasAttribute("data-chat-anchor-key")) continue;
1048
+ if (this.isHollow(el)) {
1049
+ this.hideElement(el, desiredHidden, false);
1050
+ this.emptiedWrappers.add(el);
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+ syncEmptiedWrappers(block, desiredHidden) {
1056
+ const flow = this.flow;
1057
+ if (flow === null) return;
1058
+ const scopeOf = (el) => {
1059
+ let cur = el?.parentElement ?? null;
1060
+ while (cur instanceof HTMLElement && cur.parentElement !== flow) {
1061
+ cur = cur.parentElement;
1062
+ }
1063
+ return cur instanceof HTMLElement && cur.parentElement === flow ? cur : null;
1064
+ };
1065
+ const candidates = [];
1066
+ const seen = /* @__PURE__ */ new Set();
1067
+ const scopes = /* @__PURE__ */ new Set();
1068
+ const collect = (el) => {
1069
+ const scope = scopeOf(el);
1070
+ if (scope === null) return;
1071
+ scopes.add(scope);
1072
+ let node = el?.parentElement ?? null;
1073
+ while (node instanceof HTMLElement && node !== scope) {
1074
+ if (!seen.has(node)) {
1075
+ seen.add(node);
1076
+ candidates.push(node);
1077
+ }
1078
+ node = node.parentElement;
1079
+ }
1080
+ };
1081
+ for (const row of block.rows) collect(row);
1082
+ for (const container of block.containers) collect(container);
1083
+ for (const el of [...this.emptiedWrappers]) {
1084
+ if (!el.isConnected) {
1085
+ this.emptiedWrappers.delete(el);
1086
+ continue;
1087
+ }
1088
+ const scope = scopeOf(el);
1089
+ if (scope === null || !scopes.has(scope)) continue;
1090
+ if (!seen.has(el)) {
1091
+ seen.add(el);
1092
+ candidates.push(el);
1093
+ }
1094
+ }
1095
+ for (const el of candidates) {
1096
+ if (!el.isConnected) {
1097
+ this.emptiedWrappers.delete(el);
1098
+ continue;
1099
+ }
1100
+ const cls = el.className;
1101
+ if (typeof cls === "string" && cls.split(" ").some((c) => c.startsWith("dshcf-"))) continue;
1102
+ if (el.hasAttribute("data-chat-flow-kind") || el.hasAttribute("data-chat-anchor-key")) continue;
1103
+ if (this.isHollow(el)) {
1104
+ this.hideElement(el, desiredHidden, false);
1105
+ this.emptiedWrappers.add(el);
1106
+ } else if (this.emptiedWrappers.delete(el)) {
1107
+ this.restoreElement(el);
1108
+ }
1109
+ }
1110
+ }
1111
+ /** 内容空洞判定:无可见子内容(文本/元素任一可见即非空洞)。隐藏者是谁
1112
+ * 不重要——零高度无文本节点 display:none 与保持显示像素一致(只塌缩父级
1113
+ * gap,正是要修的幻影);任何一方恢复内容,下轮 pass/audit 即恢复显示。
1114
+ * isDisplayed 走 getComputedStyle(桩内退化为内联,语义一致)。 */
1115
+ isHollow(el) {
1116
+ for (const child of [...el.childNodes]) {
1117
+ if (child.nodeType === 3) {
1118
+ if ((child.textContent ?? "").trim() !== "") return false;
1119
+ continue;
1120
+ }
1121
+ if (!(child instanceof HTMLElement)) continue;
1122
+ if (isDisplayed(child)) return false;
1123
+ }
1124
+ return true;
1125
+ }
928
1126
  ensureChip(block) {
929
1127
  let record = this.chips.get(block.key);
930
1128
  const validParent = record !== void 0 && (block.mount === "inside" ? record.chip.parentElement === block.host : record.chip.parentElement === block.host.parentElement);
@@ -1287,6 +1485,7 @@ var __dshcfBundle = (() => {
1287
1485
  }
1288
1486
  el.style.display = "none";
1289
1487
  this.writtenDisplay.set(el, "none");
1488
+ this.displayTouched = true;
1290
1489
  return false;
1291
1490
  }
1292
1491
  restoreElement(el, animate = false) {
@@ -1302,11 +1501,17 @@ var __dshcfBundle = (() => {
1302
1501
  }
1303
1502
  const original = this.originalDisplay.get(el);
1304
1503
  if (!animate || !this.canAnimate(el) || this.hasAnimatingAncestor(el)) {
1305
- if (el.style.display !== original) el.style.display = original;
1504
+ if (el.style.display !== original) {
1505
+ el.style.display = original;
1506
+ this.displayTouched = true;
1507
+ }
1306
1508
  this.releaseDisplayLedger(el);
1307
1509
  return;
1308
1510
  }
1309
- if (el.style.display !== original) el.style.display = original;
1511
+ if (el.style.display !== original) {
1512
+ el.style.display = original;
1513
+ this.displayTouched = true;
1514
+ }
1310
1515
  this.writtenDisplay.set(el, original);
1311
1516
  this.startReveal(el);
1312
1517
  }
@@ -1378,28 +1583,48 @@ var __dshcfBundle = (() => {
1378
1583
  ],
1379
1584
  { duration: ANIM_DURATION_MS, easing: ANIM_EASING, fill: "forwards" }
1380
1585
  );
1381
- const record = { anim, target: "hidden", kind: "fade" };
1586
+ const record = { anim, target: "hidden", kind: "fade", settle };
1382
1587
  this.pendingAnims.set(el, record);
1383
- anim.onfinish = () => {
1384
- if (this.pendingAnims.get(el) !== record) return;
1385
- this.pendingAnims.delete(el);
1386
- if (this.displayForeign(el)) {
1387
- this.releaseDisplayLedger(el);
1388
- this.schedule();
1389
- anim.cancel();
1390
- return;
1391
- }
1392
- if (el.style.display !== "none") el.style.display = "none";
1393
- this.writtenDisplay.set(el, "none");
1394
- settle?.();
1395
- anim.cancel();
1396
- this.schedule();
1397
- };
1588
+ anim.onfinish = () => this.finishFadeCollapse(el, record);
1398
1589
  anim.oncancel = () => {
1399
1590
  if (this.pendingAnims.get(el) !== record) return;
1400
1591
  this.pendingAnims.delete(el);
1401
1592
  };
1402
1593
  }
1594
+ /**
1595
+ * 收起 fade 终态结算(onfinish 与僵尸收割共用)。仅被同元素新动画
1596
+ * supersede 时早退;被 sweep 删账(元素已断连)或收割时仍执行 settle——
1597
+ * 回调经 controller 可达,动画必然触发 onfinish,早退会把 chip 内联 16px
1598
+ * 钉住永久残留。重挂载同节点不可能(React 只建新节点),重启动的新动画
1599
+ * 由第一条守卫覆盖,无误伤。
1600
+ */
1601
+ finishFadeCollapse(el, record) {
1602
+ const cur = this.pendingAnims.get(el);
1603
+ if (cur !== void 0 && cur !== record) return;
1604
+ this.pendingAnims.delete(el);
1605
+ if (this.displayForeign(el)) {
1606
+ this.releaseDisplayLedger(el);
1607
+ this.schedule();
1608
+ record.anim.cancel();
1609
+ return;
1610
+ }
1611
+ if (el.style.display !== "none") el.style.display = "none";
1612
+ this.writtenDisplay.set(el, "none");
1613
+ record.settle?.();
1614
+ record.anim.cancel();
1615
+ this.schedule();
1616
+ }
1617
+ /** 动画是否已播完(终态未结算):finish 事件丢失时的兜底判定。WAAPI 桩
1618
+ * 无 playState/effect 时一律 false,走正常事件路径,测试行为不变。 */
1619
+ isAnimOverdue(anim) {
1620
+ const a = anim;
1621
+ if (a.playState === "finished") return true;
1622
+ if (typeof a.currentTime === "number") {
1623
+ const end = a.effect?.getComputedTiming?.().endTime;
1624
+ if (typeof end === "number" && a.currentTime >= end) return true;
1625
+ }
1626
+ return false;
1627
+ }
1403
1628
  /** 轻量视觉 reveal(opacity + 4px 微位移):用于插件全资元素的即时显示
1404
1629
  * 路径——chip(一级展开时出现)与 merged-think 行(二级展开时出现)。
1405
1630
  * 这些元素的 display 完全由插件直写、无 React 协调竞争,因此不入
@@ -1417,7 +1642,12 @@ var __dshcfBundle = (() => {
1417
1642
  }
1418
1643
  restoreUnusedDisplays(desired) {
1419
1644
  for (const el of [...this.controlledDisplay]) {
1420
- if (!desired.has(el)) this.restoreElement(el);
1645
+ if (desired.has(el)) continue;
1646
+ if (!el.isConnected && this.pendingAnims.get(el)?.target === "hidden") {
1647
+ this.releaseDisplayLedger(el);
1648
+ continue;
1649
+ }
1650
+ this.restoreElement(el);
1421
1651
  }
1422
1652
  }
1423
1653
  /** 收集 audit 需要观察的 display 集合;只读取内联样式,不触发布局计算。 */
@@ -1663,6 +1893,39 @@ var __dshcfBundle = (() => {
1663
1893
  if (typeof getComputedStyle === "function") return getComputedStyle(el).display !== "none";
1664
1894
  return el.style.display !== "none";
1665
1895
  }
1896
+ function buildNativeTurnMap(flow) {
1897
+ const states = /* @__PURE__ */ new Map();
1898
+ for (const el of flow.children) {
1899
+ if (!(el instanceof HTMLElement)) continue;
1900
+ if (el.getAttribute("data-chat-flow-kind") !== TURN_PROCESS_KIND) continue;
1901
+ const turn = el.getAttribute("data-chat-turn");
1902
+ if (turn === null || turn === "") continue;
1903
+ const button = el.querySelector("button[data-turn-process]");
1904
+ if (button === null) continue;
1905
+ const openAttr = button.getAttribute(NATIVE_OPEN_ATTR) !== null;
1906
+ const expandedAttr = button.getAttribute("aria-expanded");
1907
+ const open = expandedAttr === null ? openAttr : expandedAttr === "true";
1908
+ states.set(turn, { collapsed: !open });
1909
+ }
1910
+ return states;
1911
+ }
1912
+ function elementTurn(el) {
1913
+ const turn = el.getAttribute("data-chat-turn");
1914
+ return turn === null || turn === "" ? null : turn;
1915
+ }
1916
+ function segmentHasNativeTurn(segment, nativeTurns) {
1917
+ if (nativeTurns.size === 0) return false;
1918
+ for (const block of segment.blocks) {
1919
+ const turn = elementTurn(block.host);
1920
+ if (turn !== null && nativeTurns.has(turn)) return true;
1921
+ }
1922
+ return false;
1923
+ }
1924
+ function blockNativelyCollapsed(block, nativeTurns) {
1925
+ if (nativeTurns.size === 0) return false;
1926
+ const turn = elementTurn(block.host);
1927
+ return turn !== null && nativeTurns.get(turn)?.collapsed === true;
1928
+ }
1666
1929
  function stableElementKey(el, fallbackIndex) {
1667
1930
  const kind = el.getAttribute("data-chat-flow-kind") ?? "node";
1668
1931
  const key = el.getAttribute("data-chat-flow-key") ?? el.getAttribute("data-chat-anchor-key") ?? `${kind}:${fallbackIndex}`;
@@ -1776,6 +2039,7 @@ var __dshcfBundle = (() => {
1776
2039
  };
1777
2040
  for (const el of children) {
1778
2041
  const kind = el.getAttribute("data-chat-flow-kind");
2042
+ if (kind === TURN_PROCESS_KIND) continue;
1779
2043
  if (kind === "user" || kind === "steering" || kind === "turn-tail") {
1780
2044
  flushCarry();
1781
2045
  run = null;
@@ -2127,14 +2391,14 @@ var __dshcfBundle = (() => {
2127
2391
  const statuses = flow.matches('[role="status"]') ? [flow, ...flow.querySelectorAll('[role="status"]')] : [...flow.querySelectorAll('[role="status"]')];
2128
2392
  for (const status of statuses) {
2129
2393
  for (const node of status.childNodes) {
2130
- if (node instanceof Text && node.data.includes("Deep diving")) {
2394
+ if (node instanceof Text && TURN_STATUS_COPY_RE.test(node.data)) {
2131
2395
  let record = originals.get(node);
2132
2396
  if (record === void 0) {
2133
2397
  record = { original: node.data, written: "" };
2134
2398
  originals.set(node, record);
2135
2399
  }
2136
2400
  if (node.data !== record.written) record.original = node.data;
2137
- const next = node.data.replace(/Deep diving[.…]*/, statusText);
2401
+ const next = node.data.replace(TURN_STATUS_COPY_RE, statusText);
2138
2402
  if (node.data !== next) {
2139
2403
  node.data = next;
2140
2404
  record.written = next;
@@ -2337,7 +2601,7 @@ var __dshcfBundle = (() => {
2337
2601
  [
2338
2602
  React.createElement("span", { className: "dshcf-settings-headText" }, [
2339
2603
  React.createElement("span", { className: "dshcf-settings-name" }, "\u72B6\u6001\u63D0\u793A\u8BCD"),
2340
- React.createElement("span", { className: "dshcf-settings-description" }, "\u81EA\u5B9A\u4E49\u72B6\u6001\u63D0\u793A\u8BCD\uFF0C\u53EF\u4EE5\u66FF\u6362\u539F\u6709\u7684Deep diving...\u4E00\u884C\uFF0C\u7531\u63D2\u4EF6dsh-auto-collapse\u63D0\u4F9B")
2604
+ React.createElement("span", { className: "dshcf-settings-description" }, "\u81EA\u5B9A\u4E49\u72B6\u6001\u63D0\u793A\u8BCD\uFF0C\u53EF\u4EE5\u66FF\u6362\u539F\u6709\u7684\u8FD0\u884C\u72B6\u6001\u6587\u6848\uFF08\u6DF1\u5EA6\u6C42\u7D22\u4E2D...\uFF09\uFF0C\u7531\u63D2\u4EF6dsh-auto-collapse\u63D0\u4F9B")
2341
2605
  ]),
2342
2606
  dirty ? React.createElement("span", { className: "dshcf-settings-pending" }, "\u672A\u4FDD\u5B58") : null,
2343
2607
  ChevronIcon(open)
@@ -2358,11 +2622,11 @@ var __dshcfBundle = (() => {
2358
2622
  className: "dshcf-settings-input",
2359
2623
  type: "text",
2360
2624
  value: text,
2361
- placeholder: "Deep diving...",
2625
+ placeholder: "\u6DF1\u5EA6\u6C42\u7D22\u4E2D...",
2362
2626
  disabled: !writable,
2363
2627
  onChange: (event) => edit(event.target.value)
2364
2628
  }),
2365
- React.createElement("p", { className: "dshcf-settings-hint" }, "\u4E3A\u7A7A\u65F6\u6062\u590D\u9ED8\u8BA4Deep diving...\u63D0\u793A\u8BCD\u72B6\u6001")
2629
+ React.createElement("p", { className: "dshcf-settings-hint" }, "\u4E3A\u7A7A\u65F6\u6062\u590D\u5B98\u65B9\u9ED8\u8BA4\u6587\u6848\uFF08\u6DF1\u5EA6\u6C42\u7D22\u4E2D...\uFF09")
2366
2630
  ]),
2367
2631
  React.createElement("div", { className: "dshcf-settings-footer" }, [
2368
2632
  failed ? React.createElement("p", { className: "dshcf-settings-failed", role: "status" }, "\u672C\u90E8\u7F72\u6CA1\u6709\u63A5\u53D7\u8FD9\u4E9B\u503C\uFF0C\u5DF2\u4FDD\u7559\u4F9B\u4F60\u4FEE\u6539\u3002") : null,
package/lib/index.js CHANGED
@@ -1,24 +1,55 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
2
  const name = "dsh-auto-collapse";
4
3
  const inject = [];
5
4
  const DEFAULT_STATUS_TEXT = "Deep sleeping...";
6
- const AUTO_COLLAPSE_SETTINGS_NAMESPACE = settingsNamespace("dsh-auto-collapse");
5
+ const AUTO_COLLAPSE_SETTINGS_NAMESPACE = "dsh-auto-collapse";
7
6
  const AUTO_COLLAPSE_SETTINGS_SCHEMA = z.object({
8
7
  statusText: z.string().default(DEFAULT_STATUS_TEXT)
9
8
  });
9
+ const FIBER_DISPOSED = 4;
10
+ const FIBER_UNLOADING = 5;
11
+ function isUnloading(ctx) {
12
+ const state = ctx?.fiber?.state;
13
+ return state === FIBER_UNLOADING || state === FIBER_DISPOSED;
14
+ }
15
+ function installSettingsSection(ctx, ns, schema, entry, hooks) {
16
+ ;
17
+ ctx.inject(["settings"], (sctx) => {
18
+ const scope = sctx.settings.register(ns, schema, {
19
+ base: entry,
20
+ ...hooks.validate === void 0 ? {} : { validate: hooks.validate }
21
+ });
22
+ hooks.setSource(() => scope.get());
23
+ sctx.effect(() => () => {
24
+ if (isUnloading(ctx)) return;
25
+ hooks.setSource(() => entry);
26
+ hooks.onChange();
27
+ });
28
+ hooks.onChange();
29
+ scope.watch(() => {
30
+ if (isUnloading(ctx)) return;
31
+ hooks.onChange();
32
+ });
33
+ });
34
+ }
10
35
  function apply(ctx, config = {}) {
11
36
  let current = () => ({ statusText: config.statusText ?? DEFAULT_STATUS_TEXT });
12
- installSettingsSection(ctx, AUTO_COLLAPSE_SETTINGS_NAMESPACE, AUTO_COLLAPSE_SETTINGS_SCHEMA, {
13
- statusText: config.statusText ?? DEFAULT_STATUS_TEXT
14
- }, {
15
- setSource: (source) => {
16
- current = source;
37
+ installSettingsSection(
38
+ ctx,
39
+ AUTO_COLLAPSE_SETTINGS_NAMESPACE,
40
+ AUTO_COLLAPSE_SETTINGS_SCHEMA,
41
+ {
42
+ statusText: config.statusText ?? DEFAULT_STATUS_TEXT
17
43
  },
18
- onChange: () => {
19
- void current;
44
+ {
45
+ setSource: (source) => {
46
+ current = source;
47
+ },
48
+ onChange: () => {
49
+ void current;
50
+ }
20
51
  }
21
- });
52
+ );
22
53
  }
23
54
  export {
24
55
  apply,
@@ -12,10 +12,11 @@
12
12
  * - 全部完成 → 标题 = 类型总结(编辑了文件 / 运行了命令 / 已思考 /
13
13
  * 上下文注入),摘要清空;出错 → 红色,中断 → 琥珀。
14
14
  *
15
- * 另外把官方 ChatView 尾部的运行状态行文字 "Deep diving..." 替换为
16
- * 可配置的状态提示词(默认 "Deep sleeping...";流光特效在 CSS 上,
17
- * 替换文本节点不影响)。React 重渲染会恢复原文,pass() 每轮自愈改回。
18
- * 设置为空时不替换,等价于恢复官方 "Deep diving..."。
15
+ * 另外把官方 ChatView 尾部的运行状态行文字(新版 "深度求索中..." / 旧版
16
+ * "Deep diving...",见 TURN_STATUS_COPY_RE)替换为可配置的状态提示词
17
+ * (默认 "Deep sleeping...";流光特效在 CSS 上,替换文本节点不影响,
18
+ * 宿主追加的用时后缀原样保留)。React 重渲染会恢复原文,pass() 每轮自愈改回。
19
+ * 设置为空时不替换,等价于恢复官方原文。
19
20
  *
20
21
  * 点击一行展开,再点收起;折叠态下若有行被选中(详情联动)自动展开。
21
22
  *
@@ -80,6 +81,7 @@ export declare class FoldController {
80
81
  private bodyTextCache;
81
82
  /** 自上次 pass 以来子树发生变化的 flow 顶层消息;pass 开头统一失效。 */
82
83
  private dirtyMessages;
84
+ /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
83
85
  /** 在途显示动画(元素 → 记录):冲突仲裁、记账对齐与生命周期清理的依据。
84
86
  * 用 Map 不用 WeakMap——switchFlow/stop 需要遍历全量 cancel。 */
85
87
  private pendingAnims;
@@ -98,6 +100,20 @@ export declare class FoldController {
98
100
  * attributes 批次(流式文本、data-state 翻转)不改变块结构,跳过全量
99
101
  * querySelectorAll 重扫(issue #14:长会话下每轮重扫造成主线程卡顿)。 */
100
102
  private structureDirty;
103
+ /** 本轮 pass 的原生 turn 摘要快照(turn id → 开合态):每轮重建,不跨轮复用;
104
+ * data-open 翻转不改变块结构也能驱动重算,见 buildNativeTurnMap。 */
105
+ private nativeTurns;
106
+ /** 被折叠掏空后藏起的中间包装层(真机 44px vs 28px 真因:think 行全隐后
107
+ * 其父容器变零高度空壳,仍作为 flex item 参与父级 gap,凭空多出 16px)。
108
+ * 内容恢复(展开/流式追加/块转世)时同函数恢复显示;switchFlow 清空。 */
109
+ private emptiedWrappers;
110
+ /** 本轮 pass 内有过 display 实写(hide/restore 瞬时路径)。空洞发现的触发
111
+ * 条件之一:纯 display 收放不产生 childList,不触发 structureDirty。
112
+ * settle 触发的后续 pass 则由 settleFired 覆盖。读后即清(见发现调用点)。 */
113
+ private displayTouched;
114
+ /** 有 fade 自然结算过(chipSettle 跑过)。结算本身不写 display,但它意味着
115
+ * 某行刚变隐藏——空洞可能刚形成。 */
116
+ private settleFired;
101
117
  /** 滚动稳定化(issue #14):flow 最近的滚动容器缓存(按 flow 身份失效)。 */
102
118
  private scrollContainer;
103
119
  private scrollContainerFlow;
@@ -165,7 +181,39 @@ export declare class FoldController {
165
181
  private createProcessedRow;
166
182
  private syncProcessedRow;
167
183
  private placeProcessedRow;
184
+ /** 原生收起时隐藏本块插件自有 overlay(chip/合并行+内容块):只做
185
+ * display 直写,不清钉住之外的账本、不删展开态,供原生再展开后复用。
186
+ * 在途 WAAPI 动画不追踪取消——收尾回调只做幂等终态对齐,无残留。 */
187
+ private hideOverlayForNativeCollapse;
168
188
  private reconcileBlock;
189
+ /**
190
+ * 掏空包装层对账(真机 44px vs 28px 真因修复)。
191
+ *
192
+ * 折叠把某容器的子行全部 display:none 后,该容器变零高度空壳,但仍是
193
+ * 父级 flex 的 item 并参与 gap(如正文体 flex-column gap:16px),凭空多出
194
+ * 一份间距。隐藏这类空壳可让 gap 塌缩;子内容恢复可见(展开/流式追加/
195
+ * 块转世)时恢复显示。
196
+ *
197
+ * 安全边界:
198
+ * - 只看 block.host 内部、不含 host 本人(宿主显隐归 segment 逻辑);
199
+ * - 跳过结构 seat(data-chat-flow-kind / data-chat-anchor-key)与插件
200
+ * 自身 overlay(dshcf- 前缀类);
201
+ * - 只隐藏内容空洞(无可见子内容):零高度无文本的空壳 display:none 与
202
+ * 保持显示像素一致,只塌缩父级 gap;任何一方恢复内容即恢复显示。
203
+ */
204
+ /**
205
+ * 空洞包装层兜底发现:按宿主直扫 div(仅结构变化轮次调用)。
206
+ * 只做隐藏侧:空洞即藏并记入 tracked;恢复侧由 syncEmptiedWrappers 的
207
+ * tracked 复核与 audit 承担。与按行 walk 共用同一安全边界(结构 seat /
208
+ * 插件 overlay / 宿主本人不碰)。
209
+ */
210
+ private discoverHollowWrappers;
211
+ private syncEmptiedWrappers;
212
+ /** 内容空洞判定:无可见子内容(文本/元素任一可见即非空洞)。隐藏者是谁
213
+ * 不重要——零高度无文本节点 display:none 与保持显示像素一致(只塌缩父级
214
+ * gap,正是要修的幻影);任何一方恢复内容,下轮 pass/audit 即恢复显示。
215
+ * isDisplayed 走 getComputedStyle(桩内退化为内联,语义一致)。 */
216
+ private isHollow;
169
217
  private ensureChip;
170
218
  private suppressBlock;
171
219
  private retainDisplayControl;
@@ -249,6 +297,17 @@ export declare class FoldController {
249
297
  * 淡完 onfinish 写 display:none 并保持双条目(镜像 hideElement 终态契约)。
250
298
  * fill:'forwards' 占位到终态写入后释放;无几何锁、无 gap 补偿。 */
251
299
  private startFadeCollapse;
300
+ /**
301
+ * 收起 fade 终态结算(onfinish 与僵尸收割共用)。仅被同元素新动画
302
+ * supersede 时早退;被 sweep 删账(元素已断连)或收割时仍执行 settle——
303
+ * 回调经 controller 可达,动画必然触发 onfinish,早退会把 chip 内联 16px
304
+ * 钉住永久残留。重挂载同节点不可能(React 只建新节点),重启动的新动画
305
+ * 由第一条守卫覆盖,无误伤。
306
+ */
307
+ private finishFadeCollapse;
308
+ /** 动画是否已播完(终态未结算):finish 事件丢失时的兜底判定。WAAPI 桩
309
+ * 无 playState/effect 时一律 false,走正常事件路径,测试行为不变。 */
310
+ private isAnimOverdue;
252
311
  /** 轻量视觉 reveal(opacity + 4px 微位移):用于插件全资元素的即时显示
253
312
  * 路径——chip(一级展开时出现)与 merged-think 行(二级展开时出现)。
254
313
  * 这些元素的 display 完全由插件直写、无 React 协调竞争,因此不入
@@ -1,11 +1,17 @@
1
1
  /** Host 插件名。 */
2
2
  export declare const name = "dsh-auto-collapse";
3
- /** Host 侧不注入额外服务;settings 是可选的运行能力,由 installSettingsSection 惰性接入。 */
3
+ /** Host 侧不注入额外服务;settings 是可选的运行能力,由下方接线惰性接入。 */
4
4
  export declare const inject: string[];
5
5
  /** 插件配置。 */
6
6
  export interface Config {
7
7
  /** 自定义状态提示词;留空恢复官方 "Deep diving..."。 */
8
8
  statusText?: string;
9
9
  }
10
+ /** settings 消费者钩子,与官方 SettingsSectionHooks 同形。 */
11
+ export interface SettingsSectionHooks<T> {
12
+ setSource(source: () => T): void;
13
+ onChange(): void;
14
+ validate?(value: T): void;
15
+ }
10
16
  /** Host 插件体:注册设置命名空间。 */
11
17
  export declare function apply(ctx: unknown, config?: Config): void;
package/package.json CHANGED
@@ -1,61 +1,68 @@
1
- {
2
- "name": "dsh-auto-collapse",
3
- "version": "0.1.5",
4
- "description": "DeepSeek Harness Web 客户端插件:把会话里的工具卡片与 Think 推理块折叠成一行,折叠态实时显示当前正在进行的工作(工具名 + 正在执行的命令/参数、或思考内容),运行中带流光动画;点击展开/收起。让界面只保留模型说的话。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/types/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/types/index.d.ts",
11
- "default": "./lib/index.js"
12
- },
13
- "./client": {
14
- "types": "./lib/types/client.d.ts",
15
- "default": "./lib/client.js"
16
- },
17
- "./package.json": "./package.json"
18
- },
19
- "dsh": {
20
- "bundle": {
21
- "patch": "./cordis.patch.yml"
22
- },
23
- "client": {
24
- "inject": [
25
- "@deepseek-ai/dsh-client-runtime",
26
- "@deepseek-ai/dsh-client-ui-settings"
27
- ],
28
- "platform": "web"
29
- }
30
- },
31
- "scripts": {
32
- "build": "node build.mjs",
33
- "deploy": "node deploy.mjs",
34
- "test": "node test/run-all.mjs",
35
- "typecheck": "tsc --noEmit --pretty false",
36
- "check": "npm run typecheck && npm test",
37
- "prepack": "node build.mjs"
38
- },
39
- "files": [
40
- "lib",
41
- "cordis.patch.yml"
42
- ],
43
- "keywords": [
44
- "dsh",
45
- "deepseek-harness",
46
- "client-plugin",
47
- "ui",
48
- "collapse",
49
- "auto-collapse",
50
- "chat"
51
- ],
52
- "license": "MIT",
53
- "devDependencies": {
54
- "esbuild": "0.25.0",
55
- "typescript": "5.9.3"
56
- },
57
- "peerDependencies": {
58
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
59
- "@deepseek-ai/schemastery": "^3.18.1"
60
- }
61
- }
1
+ {
2
+ "name": "dsh-auto-collapse",
3
+ "version": "0.1.7",
4
+ "description": "DeepSeek Harness Web 客户端插件:把会话里的工具卡片与 Think 推理块折叠成一行,折叠态实时显示当前正在进行的工作(工具名 + 正在执行的命令/参数、或思考内容),运行中带流光动画;点击展开/收起。让界面只保留模型说的话。",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/a179-sanae/dsh-auto-collapse.git"
9
+ },
10
+ "homepage": "https://github.com/a179-sanae/dsh-auto-collapse#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/a179-sanae/dsh-auto-collapse/issues"
13
+ },
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./client": {
22
+ "types": "./lib/types/client.d.ts",
23
+ "default": "./lib/client.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "inject": [
33
+ "@deepseek-ai/dsh-client-modules",
34
+ "@deepseek-ai/dsh-client-ui-settings"
35
+ ],
36
+ "platform": "web"
37
+ }
38
+ },
39
+ "scripts": {
40
+ "build": "node build.mjs",
41
+ "deploy": "node deploy.mjs",
42
+ "test": "node test/run-all.mjs",
43
+ "typecheck": "tsc --noEmit --pretty false",
44
+ "check": "npm run typecheck && npm test",
45
+ "prepack": "node build.mjs"
46
+ },
47
+ "files": [
48
+ "lib",
49
+ "cordis.patch.yml"
50
+ ],
51
+ "keywords": [
52
+ "dsh",
53
+ "deepseek-harness",
54
+ "client-plugin",
55
+ "ui",
56
+ "collapse",
57
+ "auto-collapse",
58
+ "chat"
59
+ ],
60
+ "license": "MIT",
61
+ "devDependencies": {
62
+ "esbuild": "0.25.0",
63
+ "typescript": "5.9.3"
64
+ },
65
+ "peerDependencies": {
66
+ "@deepseek-ai/schemastery": "^3.18.1"
67
+ }
68
+ }