dsh-session-accordion 0.0.0-stage → 2.5.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/LICENSE +21 -0
- package/README.md +378 -2
- package/client.js +477 -0
- package/cordis.patch.yml +4 -0
- package/index.js +8 -0
- package/package.json +54 -4
- package/scripts/check.mjs +79 -0
- package/scripts/install.ps1 +70 -0
- package/scripts/install.sh +95 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DSH Jack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,379 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DSH 会话交互折叠功能 · DSH Session Turn Accordion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
4
|
+
[](https://dshbase.com/zh/plugins/dsh-session-accordion/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/ayuanxx-max/dsh-session-accordion/releases)
|
|
7
|
+
[](https://github.com/ayuanxx-max/dsh-session-accordion/stargazers)
|
|
8
|
+
|
|
9
|
+
> **30 秒理解**:把 DSH 会话里每一轮对话折叠成一行(时间胶囊 + 问题摘要),只保留最新一条展开。纯客户端插件,不改核心。
|
|
10
|
+
>
|
|
11
|
+
> 兼容 DSH Desktop / Web;安装见下方「安装」,卸载见「卸载」。
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
**会话折叠**
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
**分叉树排列**
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
📖 **[会话管理指南(折叠 + 分叉树组合用法)](docs/session-management-guide.md)**
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## 平台支持与路线图
|
|
27
|
+
|
|
28
|
+
| 平台 | 状态 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| **Windows 11(DSH Desktop)** | ✅ **稳定运行**(当前版本) |
|
|
31
|
+
| Ubuntu | 🧪 **测试中**,尚未承诺稳定 |
|
|
32
|
+
| macOS | 📋 **计划中** |
|
|
33
|
+
|
|
34
|
+
> 修复与功能调整的**优先级按评论 / 留言的反馈量排序**——遇到问题请直接开 issue 或留言,
|
|
35
|
+
> 同类反馈越多越先修。欢迎提交你所在平台的环境信息。
|
|
36
|
+
|
|
37
|
+
### 相关插件
|
|
38
|
+
|
|
39
|
+
- [dsh-session-fork-tree](https://github.com/ayuanxx-max/dsh-session-fork-tree) —— 分叉会话以树状排列在侧边栏
|
|
40
|
+
- [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) —— DSH 插件精选列表
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
把 DSH Web 会话里的每一次交互压成一行,只保留最近一次展开;点提问行展开,执行过程另行单独点开。
|
|
44
|
+
|
|
45
|
+
Fold every interaction in a DSH Web chat into a single line and keep only the latest one open. Click a question row to expand it; the execution process opens only when you click its own header.
|
|
46
|
+
|
|
47
|
+
[中文](#中文) · [English](#english)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 中文
|
|
52
|
+
|
|
53
|
+
### 这是什么
|
|
54
|
+
|
|
55
|
+
一个 DSH(DeepSeek Harness)客户端插件。它不修改 `app.asar`、不碰核心 UI 源码,只在渲染层做三件事:
|
|
56
|
+
|
|
57
|
+
1. 每个历史交互整轮收起,折叠成**固定一行**:左侧时间胶囊 `MM-DD HH:mm` + 单行提问摘要(超出省略)。
|
|
58
|
+
2. 只保留**最近一次交互**展开;打开任何会话、切换工作区/会话、产生新交互,都会把展开态重置到最近一条(之前手动展开的也会被收起)。
|
|
59
|
+
3. 点折叠行 → 该轮展开,同时把其它已展开的轮收起;再点提问 → 收回去。
|
|
60
|
+
|
|
61
|
+
执行过程(思考、工具调用、步骤)不跟着展开:核心本身在回答结束时会收起过程,只有你单独点「过程摘要」那一行才会展开。
|
|
62
|
+
|
|
63
|
+
### 效果
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
┌──────────────────────────────────────────────────────────┐
|
|
67
|
+
│ 10-28 13:12 把这批日志按时间排序、去重,然后… › │ ← 折叠(一行)
|
|
68
|
+
├──────────────────────────────────────────────────────────┤
|
|
69
|
+
│ 10-28 12:58 帮我看下 esp32 的引脚冲突 › │
|
|
70
|
+
├──────────────────────────────────────────────────────────┤
|
|
71
|
+
│ 你的提问(全文) │ ← 展开(最近一次)
|
|
72
|
+
│ …回答正文… │
|
|
73
|
+
│ ▸ 12 步 · 思考 8s · 工具 4 次(点开才展开) │
|
|
74
|
+
└──────────────────────────────────────────────────────────┘
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 安装
|
|
78
|
+
|
|
79
|
+
**npm(推荐)**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 桌面版(profile=desktop)
|
|
83
|
+
dsh plugin --profile desktop add dsh-session-accordion@2.5.6
|
|
84
|
+
|
|
85
|
+
# 或免全局安装
|
|
86
|
+
npx -y @deepseek-ai/dsh plugin --profile desktop add dsh-session-accordion@2.5.6
|
|
87
|
+
|
|
88
|
+
# Web UI(profile=web)
|
|
89
|
+
dsh plugin --profile web add dsh-session-accordion@2.5.6
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
安装后**完全退出 DSH 再启动**;卸载:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
dsh plugin --profile desktop remove dsh-session-accordion
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> 以下为源码方式(开发 / 离线环境):
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
前置:能跑 DSH Web UI 的宿主 —— Windows / macOS 桌面版,或 Linux 上的 `dsh web`。profile 名随宿主不同:桌面版默认 `desktop`,`dsh web` 默认 `web`,自定义 profile 用你自己的名字。
|
|
102
|
+
|
|
103
|
+
#### Windows(PowerShell)
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
git clone https://github.com/ayuanxx-max/dsh-session-accordion.git
|
|
107
|
+
$repo = (Resolve-Path .\dsh-session-accordion).Path
|
|
108
|
+
$nm = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules"
|
|
109
|
+
New-Item -ItemType Junction -Path "$nm\dsh-session-accordion" -Target $repo
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Linux / macOS(bash)
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
git clone https://github.com/ayuanxx-max/dsh-session-accordion.git
|
|
116
|
+
repo="$(cd dsh-session-accordion && pwd)"
|
|
117
|
+
profile="${DSH_PROFILE:-web}" # 桌面版改成 desktop
|
|
118
|
+
mkdir -p "$HOME/.dsh/profiles/$profile/node_modules"
|
|
119
|
+
ln -sfn "$repo" "$HOME/.dsh/profiles/$profile/node_modules/dsh-session-accordion"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **符号链接必须是绝对路径**:相对路径在 profile 目录变化后会失效。
|
|
123
|
+
> 也可以一条命令完成(含 package.json 备份与合并):`bash scripts/install.sh --profile web --apply`。
|
|
124
|
+
> Windows 对应:`powershell -ExecutionPolicy Bypass -File scripts\install.ps1 -Profile desktop -Apply`。
|
|
125
|
+
|
|
126
|
+
#### 方式 B:直接装 Release 包(无符号链接,Linux 下最省事)
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# pnpm 会把这枚 tarball 复制进 profile,不依赖符号链接 / junction
|
|
130
|
+
dsh plugin --profile web add /path/to/dsh-session-accordion-2.5.3.tgz
|
|
131
|
+
# 再把 "dsh-session-accordion" 加进 dsh.profile.bundles,重启 DSH
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> 升级时下载新 tarball 再执行一次即可,不需要 `git pull`。
|
|
135
|
+
|
|
136
|
+
#### 登记到 profile(三种系统相同)
|
|
137
|
+
|
|
138
|
+
```jsonc
|
|
139
|
+
// ~/.dsh/profiles/<profile>/package.json
|
|
140
|
+
{
|
|
141
|
+
"dependencies": {
|
|
142
|
+
"dsh-session-accordion": "link:/absolute/path/to/dsh-session-accordion"
|
|
143
|
+
},
|
|
144
|
+
"dsh": {
|
|
145
|
+
"profile": {
|
|
146
|
+
"bundles": ["dsh-session-accordion"]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
4. **完全退出 DSH 再启动**(profile 层改动,刷新网页无效)。
|
|
153
|
+
5. 设置 → 对话 → **工作步骤收起时机 = 回答结束后**(插件依赖这个前置,过程才会自动收起)。
|
|
154
|
+
|
|
155
|
+
> 复制进 `node_modules` 也能用,但链接方式升级最省事:`git pull` 后重启即可。
|
|
156
|
+
|
|
157
|
+
### 卸载
|
|
158
|
+
|
|
159
|
+
1. 从 `package.json` 的 `dependencies` 与 `dsh.profile.bundles` 中删掉 `dsh-session-accordion`。
|
|
160
|
+
2. 删掉链接:Windows `Remove-Item node_modules\dsh-session-accordion`;Linux/macOS `rm ~/.dsh/profiles/<profile>/node_modules/dsh-session-accordion`(只删链接,不动源码)。
|
|
161
|
+
3. 重启 DSH。
|
|
162
|
+
|
|
163
|
+
### 工作原理
|
|
164
|
+
|
|
165
|
+
| 关键点 | 说明 |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| 挂载点 | 客户端插件(`dsh.client`,`platform: web`,`immediately: true`),随 Web 应用加载一次,天然覆盖所有工作区与会话 |
|
|
168
|
+
| 轮次分组 | 聊天流容器 `[data-slot="conversation.chat.flow"]` 下,每个渲染节点带 `[data-chat-turn="<turnId>"]`;同 turnId 归为一轮 |
|
|
169
|
+
| 提问识别 | 轮内包含 `class` 以 `_bubble` 结尾的元素(用户/steering 气泡共用)的节点 |
|
|
170
|
+
| 过程识别 | `data-chat-flow-kind="turn-process"` 的节点,其内部 `button[aria-expanded]` 是核心原生折叠开关 |
|
|
171
|
+
| 折叠方式 | 给整轮除提问外的节点打 `data-ac-hidden`(`display:none`),提问行用伪元素画时间胶囊与摘要,原内容 `visibility:hidden` |
|
|
172
|
+
| 状态 | 按 `turnId` 存在内存 `Map`;未知轮次默认「最后一轮展开」,因此切换会话、历史回看都自动收敛 |
|
|
173
|
+
| 时间 | 优先读 React fiber 上的 `props.node.data.time`(不受悬停影响),退回原生时钟文本,都没有就不显示 |
|
|
174
|
+
| 自愈 | `MutationObserver` + 1.5 秒兜底巡检,React 重渲染后重新贴状态 |
|
|
175
|
+
|
|
176
|
+
设计取向:**只叠加,不侵入**。核心的折叠状态、流式输出、滚动锚定都不接管;结构认不出时插件什么都不做,只在 console 打一条 `warn`。
|
|
177
|
+
|
|
178
|
+
### 诊断
|
|
179
|
+
|
|
180
|
+
页面 DevTools Console:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
__acReport()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
输出每一轮的 `id / expanded / stamp / summary / items`。结构变化(DSH 升级后 DOM 改名)时,看这个能立刻定位是「分组失败」还是「时间取不到」。
|
|
187
|
+
|
|
188
|
+
### 兼容性
|
|
189
|
+
|
|
190
|
+
| 平台 / 引擎 | 状态 | 说明 |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| Windows(DSH 桌面版,Electron/Chromium) | ✅ 主力验证 | 本轮开发基准 |
|
|
193
|
+
| macOS(桌面版;Tauri/WebView 需较新内核) | ✅ 设计兼容 | 只用标准 `mask-image` + 内联 SVG,无平台 API |
|
|
194
|
+
| Ubuntu / Debian 等 Linux(Chrome / Edge / Chromium) | ✅ CI 验证安装链路 | 安装脚本、打包、安装冒烟测试跑在 `ubuntu-latest` 上 |
|
|
195
|
+
| Linux 上的 Firefox | ⚠️ 未实测 | 用到 `mask-image`(Firefox 支持无前缀写法);最坏情况只失去渐隐,不影响折叠 |
|
|
196
|
+
|
|
197
|
+
- **时区**:时间胶囊用浏览器时区。Linux 服务器多为 UTC,此时显示的是 UTC;在 Console 执行
|
|
198
|
+
`window.__dshAccordion = { timeZone: 'Asia/Shanghai' }` 后刷新页面即可覆盖。
|
|
199
|
+
- **动态效果**:尊重系统"减少动态效果"(`prefers-reduced-motion`),开启后不播放展开淡入。
|
|
200
|
+
- **字体**:时间胶囊与摘要列按像素定宽,Ubuntu / SF 字体下宽度略有差异,不影响布局。
|
|
201
|
+
|
|
202
|
+
### 已知限制
|
|
203
|
+
|
|
204
|
+
- 依赖 DSH 聊天面板的内部 DOM 属性(`data-chat-turn` 等),属**非公开契约**,上游改版可能失效;失效时插件静默不动作,不会破坏界面。
|
|
205
|
+
- 折叠行是自绘的一行,行内链接需先展开再点。
|
|
206
|
+
- 时间取自提问节点的事件时间;极少数节点没有 `time` 时不显示时间,只显示摘要。
|
|
207
|
+
- 未发布到 npm,按上面的本地 link 方式安装。
|
|
208
|
+
|
|
209
|
+
### 同类项目
|
|
210
|
+
|
|
211
|
+
DSH 生态里已有若干「回合折叠」插件,取舍不同:
|
|
212
|
+
|
|
213
|
+
| 项目 | 形态 | 与本项目的差别 |
|
|
214
|
+
| --- | --- | --- |
|
|
215
|
+
| [a179-sanae/dsh-auto-collapse](https://github.com/a179-sanae/dsh-auto-collapse) | 把正文之间的思考/工具/上下文合成二级折叠,保留原生回合摘要 | 答案始终可见;不限制同时展开的轮数 |
|
|
216
|
+
| [leon-zym/dsh-fold-turns](https://github.com/leon-zym/dsh-fold-turns) | 完成回合折成一行 `Worked for …`,保留用户消息与回答 | 已标记弃用(DSH 0.1.5+ 原生支持过程折叠) |
|
|
217
|
+
| [tntcannon5000/dsh-chat-fold](https://github.com/tntcannon5000/dsh-chat-fold) | 在最终回答上方插入 disclosure | 回答不隐藏;不是手风琴 |
|
|
218
|
+
| **本项目** | **整轮(提问 + 回答 + 过程)压成一行,只允许一条展开** | 空间优先:点开才看该轮内容 |
|
|
219
|
+
|
|
220
|
+
选择建议:想「一眼扫完所有答案」用上面那类;想「一屏看完整个会话骨架、按需展开」用本项目。
|
|
221
|
+
|
|
222
|
+
### 许可证
|
|
223
|
+
|
|
224
|
+
MIT,见 [LICENSE](LICENSE)。
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## English
|
|
229
|
+
|
|
230
|
+
### What it is
|
|
231
|
+
|
|
232
|
+
A client plugin for DSH (DeepSeek Harness). It never edits `app.asar` or any core UI source; it only layers three behaviours on top of the rendered transcript:
|
|
233
|
+
|
|
234
|
+
1. Every finished interaction collapses into **one fixed line**: a time pill (`MM-DD HH:mm`) plus a single-line summary of your question, ellipsized when long.
|
|
235
|
+
2. Only the **latest interaction** stays open; opening any session, switching workspace/session, or starting a new turn resets the open state to the newest turn (manual expansions are folded too).
|
|
236
|
+
3. Clicking a collapsed row expands that turn and folds any other open turn; clicking the question again folds it back.
|
|
237
|
+
|
|
238
|
+
The execution process (thinking, tool calls, steps) does not expand with the turn: DSH folds it when the answer completes, and it opens only when you click its own summary row.
|
|
239
|
+
|
|
240
|
+
### What it looks like
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
┌──────────────────────────────────────────────────────────┐
|
|
244
|
+
│ 10-28 13:12 Sort these logs by time, dedupe, then… › │ ← folded (one line)
|
|
245
|
+
├──────────────────────────────────────────────────────────┤
|
|
246
|
+
│ 10-28 12:58 Check the ESP32 pin conflicts › │
|
|
247
|
+
├──────────────────────────────────────────────────────────┤
|
|
248
|
+
│ Your full question │ ← expanded (latest)
|
|
249
|
+
│ …answer body… │
|
|
250
|
+
│ ▸ 12 steps · 8s thinking · 4 tool calls (click to open) │
|
|
251
|
+
└──────────────────────────────────────────────────────────┘
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Install
|
|
255
|
+
|
|
256
|
+
Prerequisite: a host that runs the DSH Web UI — the Windows/macOS desktop app, or `dsh web` on Linux. The profile name depends on the host: `desktop` for the desktop app, `web` for `dsh web`, or your own name for a custom profile.
|
|
257
|
+
|
|
258
|
+
#### Windows (PowerShell)
|
|
259
|
+
|
|
260
|
+
```powershell
|
|
261
|
+
git clone https://github.com/ayuanxx-max/dsh-session-accordion.git
|
|
262
|
+
$repo = (Resolve-Path .\dsh-session-accordion).Path
|
|
263
|
+
$nm = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules"
|
|
264
|
+
New-Item -ItemType Junction -Path "$nm\dsh-session-accordion" -Target $repo
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Linux / macOS (bash)
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
git clone https://github.com/ayuanxx-max/dsh-session-accordion.git
|
|
271
|
+
repo="$(cd dsh-session-accordion && pwd)"
|
|
272
|
+
profile="${DSH_PROFILE:-web}" # use desktop for the desktop app
|
|
273
|
+
mkdir -p "$HOME/.dsh/profiles/$profile/node_modules"
|
|
274
|
+
ln -sfn "$repo" "$HOME/.dsh/profiles/$profile/node_modules/dsh-session-accordion"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
> **The symlink must use an absolute path**; a relative target breaks as soon as the profile directory moves.
|
|
278
|
+
> One-shot alternative (backs up and merges `package.json`): `bash scripts/install.sh --profile web --apply`.
|
|
279
|
+
> Windows equivalent: `powershell -ExecutionPolicy Bypass -File scripts\install.ps1 -Profile desktop -Apply`.
|
|
280
|
+
|
|
281
|
+
#### Option B: install the release tarball (no symlink / junction)
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# pnpm copies the tarball into the profile; no symlink or junction involved
|
|
285
|
+
dsh plugin --profile web add /path/to/dsh-session-accordion-2.5.3.tgz
|
|
286
|
+
# then add "dsh-session-accordion" to dsh.profile.bundles and restart DSH
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
> To upgrade, download the new tarball and run the command again; no `git pull` needed.
|
|
290
|
+
|
|
291
|
+
#### Register in the profile (same on every OS)
|
|
292
|
+
|
|
293
|
+
```jsonc
|
|
294
|
+
// ~/.dsh/profiles/<profile>/package.json
|
|
295
|
+
{
|
|
296
|
+
"dependencies": {
|
|
297
|
+
"dsh-session-accordion": "link:/absolute/path/to/dsh-session-accordion"
|
|
298
|
+
},
|
|
299
|
+
"dsh": {
|
|
300
|
+
"profile": {
|
|
301
|
+
"bundles": ["dsh-session-accordion"]
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
4. **Quit DSH completely and start it again** (profile-layer change; refreshing the page is not enough).
|
|
308
|
+
5. Settings → Chat → **When to collapse work details = On completion**. The plugin relies on this so the process folds by itself.
|
|
309
|
+
|
|
310
|
+
> Copying the repo into `node_modules` also works, but a symlink keeps upgrades to `git pull` + restart.
|
|
311
|
+
|
|
312
|
+
### Uninstall
|
|
313
|
+
|
|
314
|
+
1. Remove `dsh-session-accordion` from `dependencies` and `dsh.profile.bundles`.
|
|
315
|
+
2. Remove the link: Windows `Remove-Item node_modules\dsh-session-accordion`; Linux/macOS `rm ~/.dsh/profiles/<profile>/node_modules/dsh-session-accordion` (the link only; the source directory stays).
|
|
316
|
+
3. Restart DSH.
|
|
317
|
+
|
|
318
|
+
### How it works
|
|
319
|
+
|
|
320
|
+
| Point | Detail |
|
|
321
|
+
| --- | --- |
|
|
322
|
+
| Mount | A client plugin (`dsh.client`, `platform: web`, `immediately: true`) loaded once with the web app, so it covers every workspace and session |
|
|
323
|
+
| Turn grouping | Inside `[data-slot="conversation.chat.flow"]`, each rendered node carries `[data-chat-turn="<turnId>"]`; nodes sharing a turn id form one turn |
|
|
324
|
+
| User node | The node whose subtree contains an element with a class ending in `_bubble` (user and steering bubbles share it) |
|
|
325
|
+
| Process node | The node with `data-chat-flow-kind="turn-process"`; its `button[aria-expanded]` is the built-in fold control |
|
|
326
|
+
| Folding | Marks every non-user node of the turn with `data-ac-hidden` (`display:none`); the user row draws its pill and summary with pseudo-elements while the real bubble is `visibility:hidden` |
|
|
327
|
+
| State | An in-memory `Map` keyed by `turnId`; unknown turns default to "last one expanded", which is why session switches and history reloads converge on their own |
|
|
328
|
+
| Timestamp | Read from the React fiber (`props.node.data.time`) first, then the native clock text; nothing is invented if neither exists |
|
|
329
|
+
| Self-healing | `MutationObserver` plus a 1.5 s safety sweep re-applies state after React re-renders |
|
|
330
|
+
|
|
331
|
+
Design stance: **layer, never invade**. Core folding, streaming output and scroll anchoring stay untouched; when the structure cannot be recognised the plugin does nothing and logs a single `warn`.
|
|
332
|
+
|
|
333
|
+
### Diagnostics
|
|
334
|
+
|
|
335
|
+
In the page DevTools console:
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
__acReport()
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
It prints `id / expanded / stamp / summary / items` per turn. After an upstream UI change this tells you immediately whether grouping failed or the timestamp went missing.
|
|
342
|
+
|
|
343
|
+
### Compatibility
|
|
344
|
+
|
|
345
|
+
| Platform / engine | Status | Notes |
|
|
346
|
+
| --- | --- | --- |
|
|
347
|
+
| Windows (DSH desktop, Electron/Chromium) | ✅ primary target | Development baseline |
|
|
348
|
+
| macOS (desktop app; Tauri/WebView needs a recent engine) | ✅ designed to work | Standard `mask-image` plus inline SVG only, no platform APIs |
|
|
349
|
+
| Ubuntu / Debian and other Linux (Chrome / Edge / Chromium) | ✅ install path verified in CI | installer, packaging and install smoke test run on `ubuntu-latest` |
|
|
350
|
+
| Firefox on Linux | ⚠️ not exercised | uses `mask-image` (Firefox ships the unprefixed property); worst case the fade is lost, folding still works |
|
|
351
|
+
|
|
352
|
+
- **Time zone**: the time pill uses the browser time zone. A Linux server is often UTC, so rows show UTC;
|
|
353
|
+
run `window.__dshAccordion = { timeZone: 'Asia/Shanghai' }` in the console and reload to override.
|
|
354
|
+
- **Motion**: respects `prefers-reduced-motion`; the expand fade is skipped when it is enabled.
|
|
355
|
+
- **Fonts**: pill and summary columns are pixel-sized, so Ubuntu/SF fonts shift widths slightly without breaking the layout.
|
|
356
|
+
|
|
357
|
+
### Known limitations
|
|
358
|
+
|
|
359
|
+
- Relies on internal DOM attributes of the DSH chat panel (`data-chat-turn`, …). That is a **private contract** and may break on an upstream redesign; the plugin then fails safe by doing nothing.
|
|
360
|
+
- The folded row is self-drawn, so links inside a question need one click to expand first.
|
|
361
|
+
- Timestamps come from the question node's event time; the rare node without `time` shows a summary only.
|
|
362
|
+
- Not published to npm; install it through the local link shown above.
|
|
363
|
+
|
|
364
|
+
### Similar projects
|
|
365
|
+
|
|
366
|
+
The DSH ecosystem already has a few turn-folding plugins. They make a different trade-off:
|
|
367
|
+
|
|
368
|
+
| Project | Shape | Difference from this one |
|
|
369
|
+
| --- | --- | --- |
|
|
370
|
+
| [a179-sanae/dsh-auto-collapse](https://github.com/a179-sanae/dsh-auto-collapse) | Merges reasoning/tool/context blocks between prose into a secondary disclosure, keeping the native turn summary | The answer always stays visible; several turns can be open at once |
|
|
371
|
+
| [leon-zym/dsh-fold-turns](https://github.com/leon-zym/dsh-fold-turns) | Folds a completed turn into a `Worked for …` row, keeping the user message and answer | Marked deprecated (DSH 0.1.5+ folds processes natively) |
|
|
372
|
+
| [tntcannon5000/dsh-chat-fold](https://github.com/tntcannon5000/dsh-chat-fold) | Inserts a disclosure above the final answer | The answer stays visible; not an accordion |
|
|
373
|
+
| **This project** | **Folds the whole turn (question + answer + process) into one row and allows only one open turn** | Space first: click a row to read that turn |
|
|
374
|
+
|
|
375
|
+
Pick the former if you want to skim every answer at once; pick this one if you want the whole session skeleton on one screen and open turns on demand.
|
|
376
|
+
|
|
377
|
+
### License
|
|
378
|
+
|
|
379
|
+
MIT, see [LICENSE](LICENSE).
|
package/client.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 轮次手风琴(整轮折叠 / 提问单行摘要 / 日期时间 / 会话级重置)
|
|
3
|
+
*
|
|
4
|
+
* 行为(全工作区 / 全会话):
|
|
5
|
+
* 1. 打开任何会话:只展开**最近一条**交互,历史交互各占一行
|
|
6
|
+
* (时间胶囊 + 提问摘要 + chevron),过程与回答全部隐藏;
|
|
7
|
+
* 2. 产生新交互:新的一轮自动展开,之前所有展开的轮次(包括手动展开的)一律折叠;
|
|
8
|
+
* 3. 切换工作区/会话:状态整体重置,仍然只展开最近一条。
|
|
9
|
+
*
|
|
10
|
+
* 唯一展开态由 `expandedId`(单个 turnId)表示;会话切换/历史消失/新轮追加
|
|
11
|
+
* 三种事件都会把 `expandedId` 重新指向列表最后一条。
|
|
12
|
+
*
|
|
13
|
+
* 时间来源:React fiber 的 `props.node.data.time` → 原生 `*_timeStart` 文本 → 无。
|
|
14
|
+
*
|
|
15
|
+
* 安全阀:结构认不出就不动作;异常只 warn;不改核心配置、不碰 app.asar。
|
|
16
|
+
*/
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: 'dsh-session-accordion',
|
|
19
|
+
factory() {
|
|
20
|
+
const STYLE_ID = 'dsh-auto-collapse-style';
|
|
21
|
+
const FLOW_SEL = '[data-slot="conversation.chat.flow"]';
|
|
22
|
+
const ITEM_SEL = '[data-chat-turn]';
|
|
23
|
+
const HIDE_ATTR = 'data-ac-hidden';
|
|
24
|
+
const LINE_ATTR = 'data-ac-line';
|
|
25
|
+
const USER_ATTR = 'data-ac-user';
|
|
26
|
+
const TEXT_ATTR = 'data-ac-text';
|
|
27
|
+
const TIME_ATTR = 'data-ac-time';
|
|
28
|
+
const FRESH_ATTR = 'data-ac-fresh';
|
|
29
|
+
|
|
30
|
+
const CHEVRON = 'url("data:image/svg+xml,' +
|
|
31
|
+
'%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2712%27%20height=%2712%27' +
|
|
32
|
+
'%20viewBox=%270%200%2024%2024%27%20fill=%27none%27%20stroke=%27%23909090%27' +
|
|
33
|
+
'%20stroke-width=%272%27%20stroke-linecap=%27round%27%20stroke-linejoin=%27round%27' +
|
|
34
|
+
'%3E%3Cpolyline%20points=%279%206%2015%2012%209%2018%27/%3E%3C/svg%3E")';
|
|
35
|
+
|
|
36
|
+
/** 当前会话说:唯一展开的 turnId;null = 全部折叠。 */
|
|
37
|
+
let expandedId = null;
|
|
38
|
+
/** 上一帧看到的 turnId 序列,用来判定「追加 / 消失 / 新会话」。 */
|
|
39
|
+
let seenIds = [];
|
|
40
|
+
/** 上一帧的聊天流容器,容器换人 = 换了会话视图。 */
|
|
41
|
+
let flowRef = null;
|
|
42
|
+
|
|
43
|
+
const textCache = new WeakMap();
|
|
44
|
+
let pending = false;
|
|
45
|
+
let started = false;
|
|
46
|
+
let logged = false;
|
|
47
|
+
|
|
48
|
+
function ensureStyle() {
|
|
49
|
+
if (document.getElementById(STYLE_ID) !== null) return;
|
|
50
|
+
const tag = document.createElement('style');
|
|
51
|
+
tag.id = STYLE_ID;
|
|
52
|
+
tag.textContent = [
|
|
53
|
+
'[' + HIDE_ATTR + '="1"]{display:none!important}',
|
|
54
|
+
|
|
55
|
+
'[' + LINE_ATTR + '="1"]{position:relative;height:2em;margin:1px 0;overflow:hidden;',
|
|
56
|
+
' cursor:pointer;border-radius:8px;transition:background-color .12s ease;',
|
|
57
|
+
' background-repeat:no-repeat;background-position:right 9px center;background-size:12px 12px;',
|
|
58
|
+
' background-image:' + CHEVRON + '}',
|
|
59
|
+
'[' + LINE_ATTR + '="1"]>*{visibility:hidden!important}',
|
|
60
|
+
|
|
61
|
+
'[' + LINE_ATTR + '="1"]::before{content:attr(' + TIME_ATTR + ');position:absolute;',
|
|
62
|
+
' left:8px;top:50%;transform:translateY(-50%);width:86px;text-align:center;',
|
|
63
|
+
' font-size:11px;line-height:18px;border-radius:999px;white-space:nowrap;',
|
|
64
|
+
' font-variant-numeric:tabular-nums;letter-spacing:.02em;',
|
|
65
|
+
' color:var(--dsw-alias-label-tertiary,#9aa0a6);',
|
|
66
|
+
' background:rgba(127,127,127,.16);pointer-events:none}',
|
|
67
|
+
'[' + LINE_ATTR + '="1"]:not([' + TIME_ATTR + '])::before{content:none}',
|
|
68
|
+
|
|
69
|
+
'[' + LINE_ATTR + '="1"]::after{content:attr(' + TEXT_ATTR + ');position:absolute;',
|
|
70
|
+
' left:104px;right:28px;top:50%;transform:translateY(-50%);',
|
|
71
|
+
' font-size:.92em;line-height:1.5em;white-space:nowrap;overflow:hidden;',
|
|
72
|
+
' text-overflow:ellipsis;color:var(--dsw-alias-label-secondary,#c9c9c9)}',
|
|
73
|
+
'[' + LINE_ATTR + '="1"]:not([' + TIME_ATTR + '])::after{left:12px}',
|
|
74
|
+
|
|
75
|
+
'[' + LINE_ATTR + '="1"]:hover{background-color:rgba(127,127,127,.10)}',
|
|
76
|
+
'[' + LINE_ATTR + '="1"]:hover::after{color:var(--dsw-alias-label-primary,#fff)}',
|
|
77
|
+
'[' + LINE_ATTR + '="1"]:active{background-color:rgba(127,127,127,.18)}',
|
|
78
|
+
|
|
79
|
+
'@keyframes dsh-ac-fade{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}',
|
|
80
|
+
'[' + FRESH_ATTR + '="1"]{animation:dsh-ac-fade .16s ease-out}',
|
|
81
|
+
'[' + LINE_ATTR + '="1"]::after{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}',
|
|
82
|
+
'@media (prefers-reduced-motion:reduce){[' + FRESH_ATTR + '="1"]{animation:none!important}}',
|
|
83
|
+
].join('');
|
|
84
|
+
(document.head || document.documentElement).appendChild(tag);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findFlow() {
|
|
88
|
+
const flows = document.querySelectorAll(FLOW_SEL);
|
|
89
|
+
let best = null;
|
|
90
|
+
let bestCount = 0;
|
|
91
|
+
for (let i = 0; i < flows.length; i++) {
|
|
92
|
+
const count = flows[i].querySelectorAll(ITEM_SEL).length;
|
|
93
|
+
if (count > bestCount) {
|
|
94
|
+
best = flows[i];
|
|
95
|
+
bestCount = count;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (best !== null) return best;
|
|
99
|
+
const items = document.querySelectorAll(ITEM_SEL);
|
|
100
|
+
if (items.length === 0) return null;
|
|
101
|
+
return items[0].parentElement;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function groups(flow) {
|
|
105
|
+
const all = flow.querySelectorAll(ITEM_SEL);
|
|
106
|
+
const out = [];
|
|
107
|
+
const byId = new Map();
|
|
108
|
+
for (let i = 0; i < all.length; i++) {
|
|
109
|
+
const el = all[i];
|
|
110
|
+
if (el.closest(ITEM_SEL) !== el) continue;
|
|
111
|
+
const id = el.getAttribute('data-chat-turn');
|
|
112
|
+
if (!id) continue;
|
|
113
|
+
let group = byId.get(id);
|
|
114
|
+
if (group === undefined) {
|
|
115
|
+
group = { id, items: [] };
|
|
116
|
+
byId.set(id, group);
|
|
117
|
+
out.push(group);
|
|
118
|
+
}
|
|
119
|
+
group.items.push(el);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function pick(group) {
|
|
125
|
+
let process = null;
|
|
126
|
+
for (let i = 0; i < group.items.length; i++) {
|
|
127
|
+
const el = group.items[i];
|
|
128
|
+
if (el.getAttribute('data-chat-flow-kind') !== 'turn-process') continue;
|
|
129
|
+
if (process === null || el.querySelector('button[aria-expanded]') !== null) process = el;
|
|
130
|
+
}
|
|
131
|
+
let user = null;
|
|
132
|
+
for (let i = 0; i < group.items.length; i++) {
|
|
133
|
+
const el = group.items[i];
|
|
134
|
+
if (el === process) continue;
|
|
135
|
+
if (el.querySelector('[class*="_bubble"]') !== null) {
|
|
136
|
+
user = el;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (user === null) {
|
|
141
|
+
for (let i = 0; i < group.items.length; i++) {
|
|
142
|
+
if (group.items[i] !== process) {
|
|
143
|
+
user = group.items[i];
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (user === null) user = group.items[0];
|
|
149
|
+
return {
|
|
150
|
+
process,
|
|
151
|
+
user,
|
|
152
|
+
rest: group.items.filter(function (el) {
|
|
153
|
+
return el !== user;
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function pad2(value) {
|
|
159
|
+
return value < 10 ? '0' + value : String(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Optional time-zone override: window.__dshAccordion = { timeZone: 'Asia/Shanghai' }. */
|
|
163
|
+
function timeZoneOf() {
|
|
164
|
+
try {
|
|
165
|
+
const cfg = window.__dshAccordion;
|
|
166
|
+
return cfg && typeof cfg.timeZone === 'string' && cfg.timeZone ? cfg.timeZone : '';
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function fmtTime(value) {
|
|
173
|
+
if (value === undefined || value === null) return '';
|
|
174
|
+
let ms;
|
|
175
|
+
if (value instanceof Date) ms = value.getTime();
|
|
176
|
+
else if (typeof value === 'number') ms = value < 1e12 ? value * 1000 : value;
|
|
177
|
+
else ms = new Date(String(value)).getTime();
|
|
178
|
+
if (!isFinite(ms)) return '';
|
|
179
|
+
const d = new Date(ms);
|
|
180
|
+
if (isNaN(d.getTime())) return '';
|
|
181
|
+
const zone = timeZoneOf();
|
|
182
|
+
if (zone) {
|
|
183
|
+
try {
|
|
184
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
185
|
+
timeZone: zone, month: '2-digit', day: '2-digit',
|
|
186
|
+
hour: '2-digit', minute: '2-digit', hour12: false,
|
|
187
|
+
}).formatToParts(d);
|
|
188
|
+
const pick = function (type) {
|
|
189
|
+
for (let i = 0; i < parts.length; i++) {
|
|
190
|
+
if (parts[i].type === type) return parts[i].value;
|
|
191
|
+
}
|
|
192
|
+
return '';
|
|
193
|
+
};
|
|
194
|
+
const out = pick('month') + '-' + pick('day') + ' ' + pick('hour') + ':' + pick('minute');
|
|
195
|
+
if (/^[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$/.test(out)) return out;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// unknown zone or no Intl support (older WebView): fall through to local time
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' +
|
|
201
|
+
pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function fiberTime(el) {
|
|
205
|
+
let fiber = null;
|
|
206
|
+
const keys = Object.keys(el);
|
|
207
|
+
for (let i = 0; i < keys.length; i++) {
|
|
208
|
+
if (keys[i].indexOf('__reactFiber$') === 0 || keys[i].indexOf('__reactInternalInstance$') === 0) {
|
|
209
|
+
fiber = el[keys[i]];
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let depth = 0;
|
|
214
|
+
while (fiber !== null && fiber !== undefined && depth < 40) {
|
|
215
|
+
const props = fiber.memoizedProps;
|
|
216
|
+
if (props !== null && typeof props === 'object') {
|
|
217
|
+
if (props.node && props.node.data && props.node.data.time !== undefined) return props.node.data.time;
|
|
218
|
+
if (props.time !== undefined && typeof props.time === 'number') return props.time;
|
|
219
|
+
}
|
|
220
|
+
fiber = fiber.return;
|
|
221
|
+
depth += 1;
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function clockText(user) {
|
|
227
|
+
const clocks = user.querySelectorAll('[class*="_timeStart"],[class*="_timeEnd"]');
|
|
228
|
+
for (let i = 0; i < clocks.length; i++) {
|
|
229
|
+
const text = (clocks[i].textContent || '').trim();
|
|
230
|
+
if (text) return text;
|
|
231
|
+
}
|
|
232
|
+
return '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stampOf(user, bubble) {
|
|
236
|
+
const stamp = fmtTime(fiberTime(bubble !== null ? bubble : user));
|
|
237
|
+
if (stamp) return stamp;
|
|
238
|
+
const native = clockText(user);
|
|
239
|
+
if (!native) return '';
|
|
240
|
+
if (/^\d{1,2}[::]\d{2}/.test(native)) {
|
|
241
|
+
const now = new Date();
|
|
242
|
+
return pad2(now.getMonth() + 1) + '-' + pad2(now.getDate()) + ' ' + native;
|
|
243
|
+
}
|
|
244
|
+
return native;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function summaryOf(bubble, fallback) {
|
|
248
|
+
const source = bubble !== null ? bubble : fallback;
|
|
249
|
+
if (source === null) return '';
|
|
250
|
+
const raw = source.textContent || '';
|
|
251
|
+
const cached = textCache.get(source);
|
|
252
|
+
if (cached !== undefined && cached.len === raw.length) return cached.text;
|
|
253
|
+
let node = null;
|
|
254
|
+
try {
|
|
255
|
+
node = source.cloneNode(true);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
node = null;
|
|
258
|
+
}
|
|
259
|
+
let text = raw;
|
|
260
|
+
if (node !== null) {
|
|
261
|
+
const junk = node.querySelectorAll('[class*="_actions"],[class*="_action"],[class*="_toolbar"]');
|
|
262
|
+
for (let i = 0; i < junk.length; i++) {
|
|
263
|
+
if (junk[i].parentNode !== null) junk[i].parentNode.removeChild(junk[i]);
|
|
264
|
+
}
|
|
265
|
+
text = node.textContent || '';
|
|
266
|
+
}
|
|
267
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
268
|
+
if (text.length > 180) text = text.slice(0, 180) + '…';
|
|
269
|
+
textCache.set(source, { len: raw.length, text });
|
|
270
|
+
return text;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function markFresh(el) {
|
|
274
|
+
if (el.getAttribute(FRESH_ATTR) === '1') return;
|
|
275
|
+
el.setAttribute(FRESH_ATTR, '1');
|
|
276
|
+
setTimeout(function () {
|
|
277
|
+
el.removeAttribute(FRESH_ATTR);
|
|
278
|
+
}, 200);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resetState() {
|
|
282
|
+
expandedId = null;
|
|
283
|
+
seenIds = [];
|
|
284
|
+
flowRef = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 依据「容器变化 / 历史消失 / 新轮追加」决定当前唯一展开的轮次。
|
|
289
|
+
* @returns 本轮 turnId 序列(供渲染使用)
|
|
290
|
+
*/
|
|
291
|
+
function trackExpanded(flow, list) {
|
|
292
|
+
const ids = list.map(function (group) {
|
|
293
|
+
return group.id;
|
|
294
|
+
});
|
|
295
|
+
if (flow === null || ids.length === 0) {
|
|
296
|
+
resetState();
|
|
297
|
+
return ids;
|
|
298
|
+
}
|
|
299
|
+
const last = ids[ids.length - 1];
|
|
300
|
+
if (flow !== flowRef) {
|
|
301
|
+
expandedId = last;
|
|
302
|
+
} else {
|
|
303
|
+
const prev = seenIds;
|
|
304
|
+
let missing = false;
|
|
305
|
+
for (let i = 0; i < prev.length; i++) {
|
|
306
|
+
if (ids.indexOf(prev[i]) === -1) {
|
|
307
|
+
missing = true;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let appended = prev.length > 0 && ids.length > prev.length;
|
|
312
|
+
if (appended) {
|
|
313
|
+
for (let i = 0; i < prev.length; i++) {
|
|
314
|
+
if (ids[i] !== prev[i]) {
|
|
315
|
+
appended = false;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (missing || appended) {
|
|
321
|
+
expandedId = last;
|
|
322
|
+
} else if (expandedId !== null && ids.indexOf(expandedId) === -1) {
|
|
323
|
+
expandedId = last;
|
|
324
|
+
} else if (expandedId === null && prev.length === 0) {
|
|
325
|
+
expandedId = last;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
flowRef = flow;
|
|
329
|
+
seenIds = ids;
|
|
330
|
+
return ids;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function apply() {
|
|
334
|
+
ensureStyle();
|
|
335
|
+
const flow = findFlow();
|
|
336
|
+
if (flow === null) {
|
|
337
|
+
resetState();
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
// `conversation.chat.flow` 是 display:contents 锚点:它自己没有盒子,
|
|
341
|
+
// getClientRects() 恒为空,可见性只能靠行节点探测。
|
|
342
|
+
const probe = flow.querySelector(ITEM_SEL);
|
|
343
|
+
if (probe === null || probe.getClientRects().length === 0) {
|
|
344
|
+
resetState();
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
const list = groups(flow);
|
|
348
|
+
trackExpanded(flow, list);
|
|
349
|
+
if (!logged && list.length > 0) {
|
|
350
|
+
logged = true;
|
|
351
|
+
console.info('[auto-collapse] active: turns=' + list.length + ' expanded=' + expandedId);
|
|
352
|
+
}
|
|
353
|
+
for (let i = 0; i < list.length; i++) {
|
|
354
|
+
const group = list[i];
|
|
355
|
+
const expanded = group.id === expandedId;
|
|
356
|
+
const parts = pick(group);
|
|
357
|
+
const user = parts.user;
|
|
358
|
+
if (user !== null) {
|
|
359
|
+
user.setAttribute(USER_ATTR, '1');
|
|
360
|
+
user.removeAttribute(HIDE_ATTR);
|
|
361
|
+
if (expanded) {
|
|
362
|
+
user.removeAttribute(LINE_ATTR);
|
|
363
|
+
user.removeAttribute(TEXT_ATTR);
|
|
364
|
+
user.removeAttribute(TIME_ATTR);
|
|
365
|
+
} else {
|
|
366
|
+
const bubble = user.querySelector('[class*="_bubble"]');
|
|
367
|
+
user.setAttribute(LINE_ATTR, '1');
|
|
368
|
+
const stamp = stampOf(user, bubble);
|
|
369
|
+
if (stamp) user.setAttribute(TIME_ATTR, stamp);
|
|
370
|
+
else user.removeAttribute(TIME_ATTR);
|
|
371
|
+
const body = summaryOf(bubble, user);
|
|
372
|
+
user.setAttribute(TEXT_ATTR, body || '(无正文)');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (let j = 0; j < parts.rest.length; j++) {
|
|
376
|
+
const el = parts.rest[j];
|
|
377
|
+
if (expanded) {
|
|
378
|
+
if (el.hasAttribute(HIDE_ATTR)) {
|
|
379
|
+
el.removeAttribute(HIDE_ATTR);
|
|
380
|
+
markFresh(el);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
el.setAttribute(HIDE_ATTR, '1');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return list.length;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function toggle(el) {
|
|
391
|
+
const id = el.getAttribute('data-chat-turn');
|
|
392
|
+
if (!id) return;
|
|
393
|
+
expandedId = expandedId === id ? null : id;
|
|
394
|
+
apply();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function onClick(event) {
|
|
398
|
+
const target = event.target;
|
|
399
|
+
if (!(target instanceof Element)) return;
|
|
400
|
+
if (target.closest('a,button') !== null) return;
|
|
401
|
+
const line = target.closest('[' + LINE_ATTR + '="1"]');
|
|
402
|
+
if (line !== null) {
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
toggle(line);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const user = target.closest('[' + USER_ATTR + '="1"]');
|
|
408
|
+
if (user === null) return;
|
|
409
|
+
const selection = window.getSelection();
|
|
410
|
+
if (selection !== null && String(selection).length > 0) return;
|
|
411
|
+
toggle(user);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function schedule() {
|
|
415
|
+
if (pending) return;
|
|
416
|
+
pending = true;
|
|
417
|
+
requestAnimationFrame(function () {
|
|
418
|
+
pending = false;
|
|
419
|
+
try {
|
|
420
|
+
apply();
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.warn('[auto-collapse] apply failed', err);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function report() {
|
|
428
|
+
const flow = findFlow();
|
|
429
|
+
if (flow === null) return { flow: false, expandedId };
|
|
430
|
+
return {
|
|
431
|
+
flow: true,
|
|
432
|
+
expandedId,
|
|
433
|
+
turns: groups(flow).map(function (group) {
|
|
434
|
+
const parts = pick(group);
|
|
435
|
+
const bubble = parts.user === null ? null : parts.user.querySelector('[class*="_bubble"]');
|
|
436
|
+
return {
|
|
437
|
+
id: group.id,
|
|
438
|
+
expanded: group.id === expandedId,
|
|
439
|
+
stamp: parts.user === null ? '' : stampOf(parts.user, bubble),
|
|
440
|
+
summary: parts.user === null ? '' : summaryOf(bubble, parts.user).slice(0, 60),
|
|
441
|
+
items: group.items.length,
|
|
442
|
+
};
|
|
443
|
+
}),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function start() {
|
|
448
|
+
if (started) return;
|
|
449
|
+
started = true;
|
|
450
|
+
ensureStyle();
|
|
451
|
+
document.addEventListener('click', onClick, true);
|
|
452
|
+
try {
|
|
453
|
+
new MutationObserver(schedule).observe(document.documentElement, {
|
|
454
|
+
childList: true,
|
|
455
|
+
subtree: true,
|
|
456
|
+
});
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.warn('[auto-collapse] observer unavailable', err);
|
|
459
|
+
}
|
|
460
|
+
setInterval(schedule, 1500);
|
|
461
|
+
setTimeout(schedule, 300);
|
|
462
|
+
window.__acReport = report;
|
|
463
|
+
console.info('[auto-collapse] ready — window.__acReport() 查看结构诊断');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
inject: [],
|
|
468
|
+
apply() {
|
|
469
|
+
try {
|
|
470
|
+
start();
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.warn('[auto-collapse] start failed', err);
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
},
|
|
477
|
+
});
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-accordion",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "2.5.6",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "DSH Web chat turn accordion: every interaction folds into one line, only the newest turn opens, and session switches or new turns reset the open state.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"dsh",
|
|
10
|
+
"deepseek-harness",
|
|
11
|
+
"cordis",
|
|
12
|
+
"plugin",
|
|
13
|
+
"chat",
|
|
14
|
+
"accordion",
|
|
15
|
+
"collapse",
|
|
16
|
+
"transcript"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./index.js",
|
|
20
|
+
"./client": "./client.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"client.js",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"scripts/install.sh",
|
|
29
|
+
"scripts/install.ps1",
|
|
30
|
+
"scripts/check.mjs"
|
|
31
|
+
],
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
},
|
|
36
|
+
"client": {
|
|
37
|
+
"platform": "web",
|
|
38
|
+
"immediately": true,
|
|
39
|
+
"inject": []
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/ayuanxx-max/dsh-session-accordion.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/ayuanxx-max/dsh-session-accordion#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/ayuanxx-max/dsh-session-accordion/issues"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Cross-platform manifest sanity check, run by CI on Linux, macOS and Windows.
|
|
2
|
+
// Fails on Windows-only assumptions (CRLF text, bad casing, missing files) so a
|
|
3
|
+
// Linux checkout cannot silently break.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
|
|
8
|
+
const root = process.cwd();
|
|
9
|
+
const fail = [];
|
|
10
|
+
const ok = [];
|
|
11
|
+
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
|
12
|
+
const exists = (rel) => fs.existsSync(path.join(root, rel));
|
|
13
|
+
|
|
14
|
+
const pkg = JSON.parse(read('package.json'));
|
|
15
|
+
|
|
16
|
+
// 1. Package identity (marketplaces require kebab-case).
|
|
17
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(pkg.name)) fail.push(`package name is not kebab-case: ${pkg.name}`);
|
|
18
|
+
else ok.push(`name ${pkg.name}`);
|
|
19
|
+
|
|
20
|
+
if (!/^\d+\.\d+\.\d+$/.test(pkg.version)) fail.push(`version is not semver: ${pkg.version}`);
|
|
21
|
+
else ok.push(`version ${pkg.version}`);
|
|
22
|
+
|
|
23
|
+
// 2. Declared files must exist with exact casing (Linux is case-sensitive).
|
|
24
|
+
for (const rel of pkg.files || []) {
|
|
25
|
+
const hit = fs.existsSync(path.join(root, rel));
|
|
26
|
+
if (!hit) fail.push(`files[] entry missing: ${rel}`);
|
|
27
|
+
}
|
|
28
|
+
ok.push(`files[] entries ${pkg.files.length}`);
|
|
29
|
+
|
|
30
|
+
// 3. Entry points resolve.
|
|
31
|
+
const exportsMap = pkg.exports || {};
|
|
32
|
+
for (const [key, target] of Object.entries(exportsMap)) {
|
|
33
|
+
const rel = String(target).replace(/^\.\//, '');
|
|
34
|
+
if (!exists(rel)) fail.push(`export "${key}" points at a missing file: ${rel}`);
|
|
35
|
+
}
|
|
36
|
+
if (!exportsMap['./client']) fail.push('missing "./client" export (the dsh client half)');
|
|
37
|
+
else ok.push('client export');
|
|
38
|
+
|
|
39
|
+
// 4. dsh bundle + client manifest.
|
|
40
|
+
const patch = pkg.dsh?.bundle?.patch;
|
|
41
|
+
if (!patch) fail.push('dsh.bundle.patch is not declared');
|
|
42
|
+
else if (!exists(patch)) fail.push(`dsh.bundle.patch points at a missing file: ${patch}`);
|
|
43
|
+
else ok.push(`bundle patch ${patch}`);
|
|
44
|
+
|
|
45
|
+
if (pkg.dsh?.client?.platform !== 'web') fail.push('dsh.client.platform must be "web"');
|
|
46
|
+
else ok.push('client platform web');
|
|
47
|
+
|
|
48
|
+
// 5. Text files must be LF + BOM-free so a Linux checkout and the npm tarball
|
|
49
|
+
// behave the same as the Windows worktree.
|
|
50
|
+
const textFiles = ['client.js', 'index.js', 'cordis.patch.yml', 'package.json', 'README.md', 'LICENSE', 'scripts/install.sh'];
|
|
51
|
+
for (const rel of textFiles) {
|
|
52
|
+
if (!exists(rel)) continue;
|
|
53
|
+
const buf = fs.readFileSync(path.join(root, rel));
|
|
54
|
+
if (buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) fail.push(`BOM found: ${rel}`);
|
|
55
|
+
const crlf = buf.indexOf(Buffer.from('\r\n'));
|
|
56
|
+
if (crlf !== -1 && !rel.endsWith('.ps1')) fail.push(`CRLF found in ${rel} (expected LF)`);
|
|
57
|
+
}
|
|
58
|
+
ok.push(`checked ${textFiles.length} text files for BOM/CRLF`);
|
|
59
|
+
|
|
60
|
+
// 6. No Windows-absolute paths leaked into machine-readable files.
|
|
61
|
+
for (const rel of ['package.json', 'cordis.patch.yml', 'client.js', 'index.js']) {
|
|
62
|
+
if (!exists(rel)) continue;
|
|
63
|
+
if (/[A-Za-z]:\\\\|[A-Za-z]:\\/.test(read(rel))) fail.push(`Windows absolute path in ${rel}`);
|
|
64
|
+
}
|
|
65
|
+
ok.push('no Windows absolute paths');
|
|
66
|
+
|
|
67
|
+
// 7. POSIX installer must exist and use an absolute symlink target variable.
|
|
68
|
+
const sh = exists('scripts/install.sh') ? read('scripts/install.sh') : '';
|
|
69
|
+
if (!sh) fail.push('scripts/install.sh is missing');
|
|
70
|
+
else if (!sh.includes('ln -sfn')) fail.push('scripts/install.sh does not create a symlink');
|
|
71
|
+
else ok.push('posix installer present');
|
|
72
|
+
|
|
73
|
+
for (const line of ok) console.log(`ok ${line}`);
|
|
74
|
+
for (const line of fail) console.error(`FAIL ${line}`);
|
|
75
|
+
if (fail.length) {
|
|
76
|
+
console.error(`\n${fail.length} problem(s)`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
console.log('\nmanifest OK');
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# dsh-session-accordion - Windows installer (PowerShell 5.1+ / 7+).
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# powershell -ExecutionPolicy Bypass -File scripts\install.ps1 [-Profile <name>] [-DryRun] [-Apply]
|
|
5
|
+
#
|
|
6
|
+
# Creates a junction in <profile>\node_modules and, with -Apply, patches the
|
|
7
|
+
# profile package.json (a backup is written first). Never copies the repo.
|
|
8
|
+
|
|
9
|
+
[CmdletBinding()]
|
|
10
|
+
param(
|
|
11
|
+
[string]$Profile = $env:DSH_PROFILE,
|
|
12
|
+
[switch]$DryRun,
|
|
13
|
+
[switch]$Apply
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
|
18
|
+
$dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }
|
|
19
|
+
$pkgName = 'dsh-session-accordion'
|
|
20
|
+
|
|
21
|
+
if (-not $Profile) {
|
|
22
|
+
if (Test-Path (Join-Path $dshHome 'profiles\desktop')) { $Profile = 'desktop' }
|
|
23
|
+
elseif (Test-Path (Join-Path $dshHome 'profiles\web')) { $Profile = 'web' }
|
|
24
|
+
else { throw "cannot detect a DSH profile under $dshHome\profiles; pass -Profile <name>" }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$profileDir = Join-Path $dshHome "profiles\$Profile"
|
|
28
|
+
$pkgJson = Join-Path $profileDir 'package.json'
|
|
29
|
+
$nodeModules = Join-Path $profileDir 'node_modules'
|
|
30
|
+
$link = Join-Path $nodeModules $pkgName
|
|
31
|
+
|
|
32
|
+
if (-not (Test-Path $profileDir)) { throw "profile not found: $profileDir" }
|
|
33
|
+
if (-not (Test-Path $pkgJson)) { throw "package.json not found: $pkgJson" }
|
|
34
|
+
|
|
35
|
+
Write-Output "repo : $repo"
|
|
36
|
+
Write-Output "profile : $Profile ($profileDir)"
|
|
37
|
+
Write-Output "link : $link"
|
|
38
|
+
|
|
39
|
+
if ($DryRun) { Write-Output '-DryRun: nothing was changed'; return }
|
|
40
|
+
|
|
41
|
+
if (-not (Test-Path $nodeModules)) { New-Item -ItemType Directory -Path $nodeModules | Out-Null }
|
|
42
|
+
if (Test-Path $link) { Remove-Item $link -Recurse -Force }
|
|
43
|
+
New-Item -ItemType Junction -Path $link -Target $repo | Out-Null
|
|
44
|
+
Write-Output 'junction created'
|
|
45
|
+
|
|
46
|
+
if ($Apply) {
|
|
47
|
+
Copy-Item $pkgJson "$pkgJson.bak-before-accordion" -Force
|
|
48
|
+
$data = Get-Content $pkgJson -Raw | ConvertFrom-Json
|
|
49
|
+
if (-not $data.dependencies) { $data | Add-Member -NotePropertyName dependencies -NotePropertyValue ([pscustomobject]@{}) }
|
|
50
|
+
$data.dependencies | Add-Member -NotePropertyName $pkgName -NotePropertyValue ("link:" + $repo) -Force
|
|
51
|
+
if (-not $data.dsh) { $data | Add-Member -NotePropertyName dsh -NotePropertyValue ([pscustomobject]@{}) }
|
|
52
|
+
if (-not $data.dsh.profile) { $data.dsh | Add-Member -NotePropertyName profile -NotePropertyValue ([pscustomobject]@{}) }
|
|
53
|
+
$bundles = @($data.dsh.profile.bundles) | Where-Object { $_ -and $_ -ne $pkgName }
|
|
54
|
+
$data.dsh.profile | Add-Member -NotePropertyName bundles -NotePropertyValue (@($bundles) + $pkgName) -Force
|
|
55
|
+
$json = $data | ConvertTo-Json -Depth 20
|
|
56
|
+
# UTF-8 without BOM: a BOM makes package.json unreadable for node/pnpm on Linux and macOS.
|
|
57
|
+
[IO.File]::WriteAllText($pkgJson, $json + "`n", (New-Object System.Text.UTF8Encoding($false)))
|
|
58
|
+
Write-Output "package.json patched (backup: $pkgJson.bak-before-accordion)"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Write-Output @"
|
|
62
|
+
|
|
63
|
+
Next steps:
|
|
64
|
+
1. If -Apply was not used, add to the profile package.json:
|
|
65
|
+
dependencies: "dsh-session-accordion": "link:<repo path>"
|
|
66
|
+
dsh.profile.bundles: "dsh-session-accordion"
|
|
67
|
+
2. Restart DSH completely.
|
|
68
|
+
3. Settings -> Chat -> "When to collapse work details" = On completion.
|
|
69
|
+
4. DevTools -> __acReport() for diagnostics.
|
|
70
|
+
"@
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dsh-session-accordion — POSIX installer (Linux / macOS).
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash scripts/install.sh [--profile <name>] [--dry-run] [--apply]
|
|
6
|
+
#
|
|
7
|
+
# --profile <name> DSH profile to install into. Default: $DSH_PROFILE,
|
|
8
|
+
# else "web" when that profile exists, else "desktop".
|
|
9
|
+
# --dry-run Print what would happen, change nothing.
|
|
10
|
+
# --apply Also patch the profile package.json (needs `node`).
|
|
11
|
+
#
|
|
12
|
+
# The script only creates a symlink in <profile>/node_modules; it never copies
|
|
13
|
+
# the repository, so `git pull` + a DSH restart is enough to upgrade.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
17
|
+
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
|
18
|
+
PROFILE="${DSH_PROFILE:-}"
|
|
19
|
+
DRY_RUN=0
|
|
20
|
+
APPLY=0
|
|
21
|
+
PKG_NAME="dsh-session-accordion"
|
|
22
|
+
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--profile) PROFILE="${2:-}"; shift 2 ;;
|
|
26
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
27
|
+
--apply) APPLY=1; shift ;;
|
|
28
|
+
-h|--help) sed -n '2,16p' "$0"; exit 0 ;;
|
|
29
|
+
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
if [ -z "$PROFILE" ]; then
|
|
34
|
+
if [ -d "$DSH_HOME/profiles/web" ]; then PROFILE="web"
|
|
35
|
+
elif [ -d "$DSH_HOME/profiles/desktop" ]; then PROFILE="desktop"
|
|
36
|
+
else echo "cannot detect a DSH profile under $DSH_HOME/profiles; pass --profile <name>" >&2; exit 1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
PROFILE_DIR="$DSH_HOME/profiles/$PROFILE"
|
|
41
|
+
PKG_JSON="$PROFILE_DIR/package.json"
|
|
42
|
+
LINK="$PROFILE_DIR/node_modules/$PKG_NAME"
|
|
43
|
+
|
|
44
|
+
[ -d "$PROFILE_DIR" ] || { echo "profile not found: $PROFILE_DIR" >&2; exit 1; }
|
|
45
|
+
[ -f "$PKG_JSON" ] || { echo "package.json not found: $PKG_JSON" >&2; exit 1; }
|
|
46
|
+
|
|
47
|
+
echo "repo : $REPO"
|
|
48
|
+
echo "profile : $PROFILE ($PROFILE_DIR)"
|
|
49
|
+
echo "link : $LINK"
|
|
50
|
+
|
|
51
|
+
if [ "$DRY_RUN" = "1" ]; then
|
|
52
|
+
echo "--dry-run: nothing was changed"
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
mkdir -p "$PROFILE_DIR/node_modules"
|
|
57
|
+
if [ -L "$LINK" ] || [ -e "$LINK" ]; then
|
|
58
|
+
echo "replacing existing $LINK"
|
|
59
|
+
rm -rf "$LINK"
|
|
60
|
+
fi
|
|
61
|
+
ln -sfn "$REPO" "$LINK"
|
|
62
|
+
echo "symlink created"
|
|
63
|
+
|
|
64
|
+
if [ "$APPLY" = "1" ]; then
|
|
65
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
66
|
+
echo "warning: node not found on PATH; patch $PKG_JSON by hand (see notes below)" >&2
|
|
67
|
+
else
|
|
68
|
+
cp "$PKG_JSON" "$PKG_JSON.bak-before-accordion"
|
|
69
|
+
node - "$PKG_JSON" "$REPO" "$PKG_NAME" <<'NODE'
|
|
70
|
+
const fs = require('fs');
|
|
71
|
+
const [file, repo, name] = process.argv.slice(2);
|
|
72
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
73
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
74
|
+
pkg.dependencies[name] = 'link:' + repo;
|
|
75
|
+
pkg.dsh = pkg.dsh || {};
|
|
76
|
+
pkg.dsh.profile = pkg.dsh.profile || {};
|
|
77
|
+
const bundles = new Set(pkg.dsh.profile.bundles || []);
|
|
78
|
+
bundles.add(name);
|
|
79
|
+
pkg.dsh.profile.bundles = Array.from(bundles);
|
|
80
|
+
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
81
|
+
console.log('package.json patched (backup: ' + file + '.bak-before-accordion)');
|
|
82
|
+
NODE
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
cat <<'NOTE'
|
|
87
|
+
|
|
88
|
+
Next steps:
|
|
89
|
+
1. Add the bundle to the profile when --apply was not used:
|
|
90
|
+
dependencies: "dsh-session-accordion": "link:<repo path>"
|
|
91
|
+
dsh.profile.bundles: "dsh-session-accordion"
|
|
92
|
+
2. Restart DSH completely (profile-layer change; refreshing the page is not enough).
|
|
93
|
+
3. Settings -> Chat -> "When to collapse work details" = On completion.
|
|
94
|
+
4. Diagnostics: open DevTools and run __acReport().
|
|
95
|
+
NOTE
|