dsh-sidebar-suite 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/DETAIL.md +18 -1
- package/README.md +25 -2
- package/lib/client.js +402 -10
- package/lib/temp-session-host.mjs +69 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +3 -3
- package/temp-session-config.txt +7 -0
- package//344/275/277/347/224/250/350/257/264/346/230/216.md +17 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
# 更新日志 / Changelog
|
|
2
2
|
|
|
3
|
-
> 版本号以 npm registry **实际发布记录**为准(当前:`0.0.0-stage`、`0.1.3`、`0.2.4`、`0.2.5`、`0.2.6`、`0.2.7`、`0.2.8`、`0.2.9`、`0.3.0`)。
|
|
3
|
+
> 版本号以 npm registry **实际发布记录**为准(当前:`0.0.0-stage`、`0.1.3`、`0.2.4`、`0.2.5`、`0.2.6`、`0.2.7`、`0.2.8`、`0.2.9`、`0.3.0`、`0.4.0`)。
|
|
4
|
+
> ⚠️ **`0.3.0` 已弃用 / deprecated**:它会让 DSH 卡在 `Loading plugins…` 且主窗口不显示(见 0.4.0 章节第一条)。请升级到 `0.4.0` 或更高版本。`0.3.0` hangs DSH on the loading screen; upgrade to 0.4.0 or later.
|
|
4
5
|
> `0.2.0`、`0.2.1`、`0.2.2`、`0.2.3` 是开发过程中的本地迭代号,**从未单独发布**;它们的功能一次性随 `0.2.4` 上线,所以在下面合并记录。
|
|
5
6
|
>
|
|
6
7
|
> Versions mirror the npm registry. `0.2.0`–`0.2.3` were local iteration numbers that were never published on their own; all of their features shipped together in `0.2.4`.
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
|
11
|
+
## 0.4.0 — 临时会话可以「转正」或「立即删除」/ Promote or delete a temp session(2026-11-11)
|
|
12
|
+
|
|
13
|
+
**这次改了运行时代码**(`lib/client.js` 与宿主半 `lib/temp-session-host.mjs`),所以**升级后要托盘退出 DSH 再重开一次**。This release touches both halves; quit DSH from the tray and reopen once after upgrading.
|
|
14
|
+
|
|
15
|
+
- **⛔ 修复启动死循环(严重,0.3.0 起就有)** — 0.3.0 的页签条插在控制栏与宿主「扩展」面板之间,但 `ensureBar()` 仍要求控制栏的 `nextSibling` 就是那个面板,于是每次 `paint()` 把控制栏搬一次、`ensureTabStrip()` 又搬回来;root 的 childList 变化触发插件自己的 `MutationObserver` → `scheduleSync()` → `paint()`,微任务队列永不排空。表现是**渲染进程 100%、主窗口隐藏、停在 `Loading plugins…`,整个 DSH 打不开**。修复后位置判断改成「顺序在前」而不是「紧邻」,`ensureTabStrip()` 同理;并加了一道「同一宏任务内最多 16 次 sync」的熔断,将来任何重绘自激也不会再把应用锁死。三种布局、以及插件页签接管侧栏的场景都做了真机验收(窗口正常、CDP 秒回、root 子节点顺序 4 次采样不变、8 秒内各进程 CPU 增量 ≤ 0.17s)。**`0.3.0` 已在 npm 标记弃用,请升级到 0.4.0。** The 0.3.0 tab strip sat between the control bar and the panels nav while `ensureBar()` still demanded adjacency, so every repaint moved the bar and fired the plugin's own observer — an endless microtask loop that hid the window on "Loading plugins…". Position checks now compare order instead of adjacency, plus a per-macrotask sync cap. The published 0.3.0 is affected and has been deprecated on npm.
|
|
16
|
+
- **测试** — 新增 `tests/test_033.mjs`(13 项):让 DOM 桩真的把 `MutationObserver` 回调投递回来,一次普通的宿主 DOM 变化就会复现自激。修复前 6 项失败(撞上 200 次变更上限,三种布局全部中招),修复后每次重绘 0 次 root 变更、节点顺序不变。`npm test` 链扩为 11 套。A new suite delivers observer callbacks the way a browser does; the old code spins past a 200-mutation cap, the fixed code settles on zero mutations.
|
|
17
|
+
- **右键临时会话行,弹出「保留为正式会话 / 立即删除」** — 之前临时会话是「要么全删,要么全留」:想留下某一个,只能去把 `DELETE_ON_EXIT` 关掉,结果**所有**临时会话都变成永久。现在单个会话可以就地处理。Right-clicking a temporary row now offers both options, so one session no longer forces a global setting.
|
|
18
|
+
- **只接管临时会话那一行** — 右键普通会话行时插件**完全不碰事件**(不 `preventDefault`、不 `stopPropagation`),宿主或别的插件的右键菜单照常工作。The handler never touches rows it does not own.
|
|
19
|
+
- **「保留为正式会话」后端零改动** — 走的是 0.2.0 就有的 `POST /api/dsh-temp-session.untrack`,只是过去没有入口。Promotion reuses the existing untrack route.
|
|
20
|
+
- **「立即删除」新增一个路由** — `POST /api/dsh-temp-session.remove?sessionId=…`,复用 `purgeAll` 的删除与兜底:先删目录,确认真的没了;文件被占用就先从列表隐藏(`archiveSession`),下次启动再删;**删不掉就仍然留在 tracked 里**,保证下次启动会重试,而不是悄悄变成一个点不开的幽灵。A new single-id route reuses the same delete/hide/retry ladder as the bulk cleanup.
|
|
21
|
+
- **这条路由只接受「临时会话」** — 命中 `state.tracked` 之外的 id 直接拒绝,且在任何文件操作之前拒绝。它能从页面调到,所以绝不能变成「按 id 删任意会话」的接口。It refuses anything outside the tracked set, before touching the filesystem.
|
|
22
|
+
- **菜单本身** — 顶部一行是被右键那一行的标题(截断 48 字,因为会话标题由宿主拥有),下面两个动作各带一句后果说明;靠近视口右/下边缘会自动翻转回界内;`Esc` 关闭并把焦点还给那一行、方向键在两项间移动、点菜单外 / 滚动 / 改窗口大小都会关掉。The menu echoes the row title, explains each consequence, flips near viewport edges, and closes on Esc, outside click, scroll or resize.
|
|
23
|
+
- **`TEMP_ROW_MENU=on` 新开关** — 万一和别的插件抢右键菜单,把它改成 `off` 即可(详情页也有同一个开关,不用手改文件)。A new switch, also exposed on the detail page, in case another plugin owns the context menu.
|
|
24
|
+
- **测试** — 新增 `tests/test_032.mjs`(60 项):跑真实 `lib/client.js`,按宿主的方式造出 `data-row-key="session:<id>"` 行,然后像用户那样右键、点动作、按 Esc、点外部;覆盖「非临时行完全不被拦截」「保留后属性消失」「删除后调用正确的路由」「文件被占用时提示隐藏而非删除」「失败不吞掉」以及尺寸翻转。顺手把 DOM 桩抽成 `tests/dom-stub.mjs`,由 `test_031` 与 `test_032` 共用(`test_031` 仍是 53 项)。`npm test` 链扩为 10 套。One new suite drives the real bundle against fabricated session rows and clicks through the menu; the fake DOM is now shared by both rendering suites.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
10
28
|
## 0.3.0 — 第三方插件可以注册自己的页签 / Third-party tabs(2026-11-11)
|
|
11
29
|
|
|
12
30
|
**这次改了运行时代码**(`lib/client.js`),所以**升级后要托盘退出 DSH 再重开一次**。This release touches `lib/client.js`; quit DSH from the tray and reopen once after upgrading.
|
package/DETAIL.md
CHANGED
|
@@ -157,7 +157,7 @@ For local development, point the same command at this folder or a packed `.tgz`.
|
|
|
157
157
|
|
|
158
158
|
**为什么删除是安全的**:`dsh-session-query-sqlite` 的 `_reconcile()` 会拿「磁盘上实际存在的会话」和索引行对比,多出来的行直接 `persistentDeletes`,因此删目录之后搜索索引会自愈,不会留下点不开的幽灵会话。
|
|
159
159
|
|
|
160
|
-
配置:`SHOW_TEMP_BUTTON` / `TEMP_IN_CHAT_MODE` / `TEMP_BUTTON_STYLE` / `EXPAND_ALL_SESSIONS` / `DELETE_ON_EXIT` / `SCHEDULE_DELETE` / `SCHEDULE_TIME` / `MAX_KEPT` —— **全部
|
|
160
|
+
配置:`SHOW_TEMP_BUTTON` / `TEMP_IN_CHAT_MODE` / `TEMP_BUTTON_STYLE` / `EXPAND_ALL_SESSIONS` / `TEMP_ROW_MENU` / `DELETE_ON_EXIT` / `SCHEDULE_DELETE` / `SCHEDULE_TIME` / `MAX_KEPT` —— **全部 9 项都能在详情页设**,用记事本改等号右边也行,保存后约 3 秒自动生效,不用重启。
|
|
161
161
|
|
|
162
162
|
**详情页设置(0.2.2)**:插件详情页顶部卡片新增「临时会话自动删除」区块 —— 「退出 DSH 时删除」和「每天定时清理 + 时间」两个独立开关。它不走 `configForms`,而是 `GET/POST /api/dsh-temp-session.config` 交给 Host 半执行,因为**只有一个数据源**:`temp-session-config.txt`。Host 半按行替换(`KEY=value` 只换 `= ` 右边),注释、空行、键顺序原样保留,用户手写的内容不会丢;缺失的键追加到末尾。保存后立即 `restartScheduler()`,无需重启。
|
|
163
163
|
|
|
@@ -171,12 +171,29 @@ For local development, point the same command at this folder or a packed `.tgz`.
|
|
|
171
171
|
- 退出删除(`DELETE_ON_EXIT`)不受 `skipActiveMs` 保护:退出本身就是用户要求清理,而且进程马上消失,没有「下次再说」的机会。
|
|
172
172
|
- 两个开关互相独立,可以同时开:白天不关 DSH 的人要「每晚 3 点清」,每天关机的人要「退出即清」。
|
|
173
173
|
|
|
174
|
+
**单条转正 / 立即删除(0.4.0)**
|
|
175
|
+
|
|
176
|
+
之前临时会话是「要么全删、要么全留」:想留下某一个,只能关掉 `DELETE_ON_EXIT`,代价是**所有**临时会话一起变成永久。0.4.0 补上了单行入口 —— 右键带「临时」标签的会话行。
|
|
177
|
+
|
|
178
|
+
四个设计取舍:
|
|
179
|
+
|
|
180
|
+
- **入口用右键,不在行里加按钮**。会话行由 React 拥有,往里塞节点会被下一次重渲染清掉;`data-dsbs-temp-row` 是属性,`applyTempRows()` 随时能补回来,节点不行。右键是零 DOM 侵入的入口。
|
|
181
|
+
- **只拦自己的行**。`onTempContextMenu` 先解析 `data-row-key`,id 不在 `tempIds` 里就直接 `return`,`preventDefault()` 根本不会执行。所以宿主或别的插件的右键菜单在普通会话上照常工作 —— 这是这条功能唯一的兼容性风险,用「不碰」消掉它。真撞上了还有 `TEMP_ROW_MENU=off` 整条让出。
|
|
182
|
+
- **「保留」复用已有后端**。0.2.0 起就有 `POST /api/dsh-temp-session.untrack`,只是当时没有 UI 入口。所以转正是纯前端改动,Host 半一行没动。
|
|
183
|
+
- **「删除」新增路由,且只能删 tracked**。`POST /api/dsh-temp-session.remove?sessionId=…` 走 `removeOne()`:先 `removeSession()` 删目录,再用 `sessionDirsSync()` 确认真的空了;没删掉就 `hideSession()` 归档隐藏。关键在**只接受 `state.tracked` 里的 id,且在调用任何文件操作之前就拒绝** —— 这条路由能从页面调到,不能变成「按 id 删任意会话」的通用删除接口。删除失败时不从 tracked 移除,下次启动继续重试(与 `purgeAll` 一致),否则会留下「已经不算临时、却还在磁盘上」的孤儿。
|
|
184
|
+
|
|
185
|
+
菜单本身:`role="menu"` + `role="menuitem"`,首项自动获得焦点;方向键在两项间循环,`Esc` 关闭并把焦点还给那一行,点外部 / `scroll` / `resize` 都会关闭(菜单是 `position:fixed`,不关就会飘在错误的行旁边)。视口边缘翻转用 `getBoundingClientRect()` 实测,读不到时退回 `offsetWidth`/`offsetHeight` 与保守的 220×96。
|
|
186
|
+
|
|
187
|
+
Plain English: right-clicking a temporary row opens a two-item menu. "Keep" simply calls the untrack route that has existed since 0.2.0; "Delete now" goes through a new single-id route that refuses anything outside the tracked set before touching the filesystem, hides the row when a file is locked, and keeps the id tracked so the next launch retries. The menu never touches rows this plugin does not own.
|
|
188
|
+
|
|
174
189
|
**限制 / Limits**
|
|
175
190
|
|
|
176
191
|
- 退出前会话仍写在磁盘上(原生窗口的前提)。
|
|
177
192
|
- 强杀 DSH 时退出钩子来不及跑,残留会话在下一次启动时删除。
|
|
178
193
|
- 文件被占用导致删不掉时,先归档隐藏,下一次启动再删。
|
|
179
194
|
- CHAT 官网模式下临时按钮默认隐藏(那条链路是 DeepSeek 网页账号,和 DSH 会话不是一回事);`TEMP_IN_CHAT_MODE=show` 可强显,`TEMP_BUTTON_STYLE=own` 可完全避开与侧栏顶部控件的抢位。
|
|
195
|
+
- 右键菜单只作用于**临时会话行**。若另一个插件也接管了这些行的右键,把 `TEMP_ROW_MENU` 改成 `off` 让出位置(详情页有同一个开关)。
|
|
196
|
+
- 菜单动作是异步的:点「立即删除」后,行从列表里消失要等宿主自己刷新(插件会尝试调 `uiWorkspace().sessions.reload/refresh/invalidate`,没有这个 API 时静默跳过)。
|
|
180
197
|
|
|
181
198
|
**侧栏顶部按钮的 DOM 约束(0.2.1)**:`dsh-deepseek-chat` 的侧栏适配器(`lib/client.js` 里的 `attachSidebarAdapter`)会从 `sidebar.brand.name` 插槽往上走五层,然后要求侧栏根节点下 `<button>` 子元素**恰好 1 个**(`controls()` 里的 `buttons.length === 1 && buttons[0] === row.nextElementSibling`)。0.2.0 把「临时」按钮作为根节点的第 2 个 `<button>` 插进去,适配器直接 `return null`,于是它在切换器下渲染「当前侧栏不兼容:顶部新会话仍使用 HARNESS」并且不再接管「新会话」。0.2.1 把按钮装进 `[data-dsbs-temp-slot]`(`<div>`),根节点的 `<button>` 计数回到 1,适配器恢复。回归测试见 `_probe/chat-compat.mjs`:它从插件 bundle 里抽出真实的 `attachSidebarAdapter` 跑一遍假 DOM。
|
|
182
199
|
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The full detail document is [DETAIL.md](DETAIL.md).
|
|
|
24
24
|
|
|
25
25
|
| 设计取向 / Design goal | 落在哪 / What it means |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| **强实用性 / Seriously practical** | 三种布局一键切换、默认布局在详情页顶部直接改、会话列表默认全展开(不用再点「展开其余」)、「临时」开真会话且**默认在退出 DSH 时删除**(`DELETE_ON_EXIT=on
|
|
27
|
+
| **强实用性 / Seriously practical** | 三种布局一键切换、默认布局在详情页顶部直接改、会话列表默认全展开(不用再点「展开其余」)、「临时」开真会话且**默认在退出 DSH 时删除**(`DELETE_ON_EXIT=on`;**每天定时清理默认关**,需要时在详情页开启并设时间)、**临时会话可右键「保留为正式会话」或「立即删除」**(0.4.0)、详情页三种反馈入口 |
|
|
28
28
|
| **美观性 / Looks native** | 颜色与层级全取宿主设计变量、深浅主题自动跟随;细字号控制栏、迷你布局示意图、「临时」用 CSS 标签不破坏行高;不重构宿主 DOM、不弹窗、不另起一套皮肤 |
|
|
29
29
|
|
|
30
30
|
完整的「强实用性 / 美观性」逐条说明见 [DETAIL.md](DETAIL.md) 的「这是一款什么插件」一节。
|
|
@@ -209,6 +209,27 @@ DSH 自己的会话列表每个工作区只先显示 5 条,剩下的折叠成
|
|
|
209
209
|
- **用的是 DSH 原生会话窗口**:原生消息流、原生输入框、原生的工具调用/文件读写/命令执行,模型就是你当前在 DSH 里选的那个。插件不再自己画窗口,也不再自己调模型。
|
|
210
210
|
- 唯一的区别是**退出 DSH 时它会被删掉**。行首会有一个「**临时**」小标签,方便你一眼认出来。
|
|
211
211
|
|
|
212
|
+
### 右键:保留为正式会话 / 立即删除(0.4.0)
|
|
213
|
+
|
|
214
|
+
**在侧栏里右键任何一个带「临时」标签的会话行**,会弹出一个两项菜单:
|
|
215
|
+
|
|
216
|
+
| 选项 | 结果 |
|
|
217
|
+
|---|---|
|
|
218
|
+
| **保留为正式会话** | 它从此不算临时 —— 退出 DSH 不会删、定时清理不会碰,和普通会话完全一样 |
|
|
219
|
+
| **立即删除** | 当场删掉这个会话的磁盘文件,不用等退出(不可恢复) |
|
|
220
|
+
|
|
221
|
+
出现的时机与边界:
|
|
222
|
+
|
|
223
|
+
- **只对临时会话行生效**。右键普通会话时插件**完全不碰那个事件**,宿主或别的插件的右键菜单照常弹出。
|
|
224
|
+
- 菜单第一行是那一行的标题(太长会截断),每个选项下面有一句后果说明,避免误点「立即删除」。
|
|
225
|
+
- `Esc` 关闭、方向键切换、`Enter` 执行;点菜单外面、滚动、改变窗口大小都会关掉。
|
|
226
|
+
- 靠近屏幕右边缘或底边缘时会自动往回收,不会被切掉。
|
|
227
|
+
|
|
228
|
+
**想关掉它**:详情页的「临时会话右键菜单」开关,或把 `TEMP_ROW_MENU` 改成 `off`。如果哪天有别的插件也想接管右键,用这个开关把位置让出去即可。
|
|
229
|
+
|
|
230
|
+
> 「删不掉」时不会假装成功:文件被别的进程占用时,先把这行从列表里隐藏,并提示「下次启动再删」,而不是留一个点不开的幽灵会话。
|
|
231
|
+
> If a file is locked, the row is hidden and retried next launch instead of pretending the delete worked.
|
|
232
|
+
|
|
212
233
|
### 它是怎么被删掉的(说清楚,不夸大)
|
|
213
234
|
|
|
214
235
|
DSH **没有公开的「删除会话」接口**(我核对过 `app.asar`:`session/create`、`session/rename`、`workspace/archiveSession` 都有,就是没有 delete;官方界面上的「归档」只是把会话从列表里藏起来,文件还在磁盘上)。所以删除是插件自己做的,两道保险:
|
|
@@ -233,7 +254,7 @@ DSH **没有公开的「删除会话」接口**(我核对过 `app.asar`:`ses
|
|
|
233
254
|
|
|
234
255
|
### 全部设置图形化 + 配置热重载(0.2.8)
|
|
235
256
|
|
|
236
|
-
详情页最上面那张卡现在能改**全部
|
|
257
|
+
详情页最上面那张卡现在能改**全部 9 项**配置,不用再碰文本:
|
|
237
258
|
|
|
238
259
|
| 设置 | 对应键 |
|
|
239
260
|
| --- | --- |
|
|
@@ -241,6 +262,7 @@ DSH **没有公开的「删除会话」接口**(我核对过 `app.asar`:`ses
|
|
|
241
262
|
| CHAT 官网模式:隐藏 / 显示 | `TEMP_IN_CHAT_MODE` |
|
|
242
263
|
| 按钮摆法:并排 / 独占一行 | `TEMP_BUTTON_STYLE` |
|
|
243
264
|
| 会话默认全展开 | `EXPAND_ALL_SESSIONS` |
|
|
265
|
+
| 临时会话右键菜单 | `TEMP_ROW_MENU` |
|
|
244
266
|
| 退出 DSH 时删除 | `DELETE_ON_EXIT` |
|
|
245
267
|
| 每天定时清理 + 时间 | `SCHEDULE_DELETE` / `SCHEDULE_TIME` |
|
|
246
268
|
| 最多保留几个 | `MAX_KEPT` |
|
|
@@ -261,6 +283,7 @@ DSH **没有公开的「删除会话」接口**(我核对过 `app.asar`:`ses
|
|
|
261
283
|
| `TEMP_IN_CHAT_MODE` | `hide` | 装了 `dsh-deepseek-chat` 时,处于 CHAT 官网模式要不要显示按钮 |
|
|
262
284
|
| `TEMP_BUTTON_STYLE` | `pair` | `pair` = 和「新会话」并排;`own` = 单独占一行,完全不动「新会话」 |
|
|
263
285
|
| `EXPAND_ALL_SESSIONS` | `on` | 会话列表全部展开并隐藏「展开其余 N 个会话」;`off` = 恢复宿主折叠 |
|
|
286
|
+
| `TEMP_ROW_MENU` | `on` | 右键临时会话是否弹出「保留为正式会话 / 立即删除」;`off` = 不接管右键 |
|
|
264
287
|
| `DELETE_ON_EXIT` | `on` | 退出时删除;改 `off` 就不再删,临时会话等于普通会话 |
|
|
265
288
|
| `SCHEDULE_DELETE` | `off` | 每天定时清理;`on` = 到点把临时会话删掉 |
|
|
266
289
|
| `SCHEDULE_TIME` | `03:00` | 定时清理的时间(24 小时制 `HH:MM`,本机时间) |
|
package/lib/client.js
CHANGED
|
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({
|
|
|
21
21
|
var ENTRY = 'dsh-sidebar-suite'
|
|
22
22
|
// Reported to tab consumers so they can feature-detect. Kept in lockstep
|
|
23
23
|
// with package.json by tests/test_030.mjs.
|
|
24
|
-
var VERSION = '0.
|
|
24
|
+
var VERSION = '0.4.0'
|
|
25
25
|
var NS = 'sidebar-suite'
|
|
26
26
|
var LS_KEY = 'dsh-sidebar-suite:state'
|
|
27
27
|
// Where the third-party tab choice lives. Kept out of LS_KEY on purpose:
|
|
@@ -106,6 +106,15 @@ window.__ModuleLoader__.load({
|
|
|
106
106
|
'temp.feedbackHint': 'Report it on the plugin detail page.',
|
|
107
107
|
'temp.restartHint': 'Fully quit DSH (tray icon → Quit) and reopen it — a hot reload does not reload the plugin host.',
|
|
108
108
|
'temp.badge': 'TEMP',
|
|
109
|
+
'temp.menu.label': 'Temporary session actions',
|
|
110
|
+
'temp.menu.keep': 'Keep as a normal session',
|
|
111
|
+
'temp.menu.keep.hint': 'It will no longer be deleted when DSH quits.',
|
|
112
|
+
'temp.menu.delete': 'Delete now',
|
|
113
|
+
'temp.menu.delete.hint': 'Removes this session from disk. It cannot be undone.',
|
|
114
|
+
'temp.menu.kept': 'Kept as a normal session — it now survives quitting DSH.',
|
|
115
|
+
'temp.menu.deleted': 'Temporary session deleted.',
|
|
116
|
+
'temp.menu.hidden': 'Files were locked, so the row was hidden instead. Deletion is retried next launch.',
|
|
117
|
+
'temp.menu.failed': 'Action failed: ',
|
|
109
118
|
'policy.title': 'Temporary session cleanup',
|
|
110
119
|
'policy.help': 'Choose when temporary sessions are removed. Defaults: delete on quit = on, daily cleanup = off. Saved to the plugin settings file and applied immediately — no restart.',
|
|
111
120
|
'policy.onQuit': 'Delete when DSH quits',
|
|
@@ -160,6 +169,15 @@ window.__ModuleLoader__.load({
|
|
|
160
169
|
'temp.feedbackHint': '可在插件详情页反馈。',
|
|
161
170
|
'temp.restartHint': '请从托盘图标彻底退出 DSH 再打开 —— 热重载不会重载插件后端。',
|
|
162
171
|
'temp.badge': '临时',
|
|
172
|
+
'temp.menu.label': '临时会话操作',
|
|
173
|
+
'temp.menu.keep': '保留为正式会话',
|
|
174
|
+
'temp.menu.keep.hint': '退出 DSH 时不再删除它。',
|
|
175
|
+
'temp.menu.delete': '立即删除',
|
|
176
|
+
'temp.menu.delete.hint': '把这个会话从磁盘删掉,无法恢复。',
|
|
177
|
+
'temp.menu.kept': '已保留为正式会话:退出 DSH 时不会再删除它。',
|
|
178
|
+
'temp.menu.deleted': '临时会话已删除。',
|
|
179
|
+
'temp.menu.hidden': '文件被占用,已先把它从列表里隐藏,下次启动再删。',
|
|
180
|
+
'temp.menu.failed': '操作失败:',
|
|
163
181
|
'policy.title': '临时会话自动删除',
|
|
164
182
|
'policy.help': '决定临时会话什么时候被清掉。默认:退出 DSH 时删除=开,每天定时清理=关。写入插件设置文件,改完立即生效,不用重启。',
|
|
165
183
|
'policy.onQuit': '退出 DSH 时删除',
|
|
@@ -206,6 +224,8 @@ window.__ModuleLoader__.load({
|
|
|
206
224
|
'options.styleOwn': '独占一行 / Own row',
|
|
207
225
|
'options.expandAll': '会话默认全展开 / Always expand sessions',
|
|
208
226
|
'options.expandHint': '关掉后恢复 DSH 原来的折叠行为;已经展开的列表要切换一次工作区才会收回去。 / Turning it off restores host folding; already-expanded groups collapse after you switch workspaces once.',
|
|
227
|
+
'options.rowMenu': '临时会话右键菜单 / Right-click menu on temp sessions',
|
|
228
|
+
'options.rowMenuHint': '右键临时会话可「保留为正式会话」或「立即删除」。若和别的插件抢右键菜单,关掉这一项即可。 / Right-clicking a temp session offers "keep as normal" or "delete now". Turn this off if another plugin owns the context menu.',
|
|
209
229
|
'options.maxKept': '最多保留 / Keep at most',
|
|
210
230
|
'options.maxKeptHint': '个临时会话(1–200) / temp sessions (1–200)',
|
|
211
231
|
'options.purge': '立即清理临时会话 / Clean up now',
|
|
@@ -314,6 +334,19 @@ window.__ModuleLoader__.load({
|
|
|
314
334
|
// In the collapsed rail there is no horizontal room for two buttons.
|
|
315
335
|
'html[data-dsbs-temp-solo="true"] [data-dsbs-temp-slot]{display:none!important}',
|
|
316
336
|
'html[data-dsbs-temp-solo="true"]:not([data-dsbs-chat-mode]) [data-dsbs-newchat] button[class*="newSession"]{width:auto!important;height:38px!important;font-size:14px!important;line-height:22px!important;margin:0 calc(var(--dsh-sidebar-paint-inset,3px) + 1px) 10px!important}',
|
|
337
|
+
// ---- per-row menu on temporary sessions (0.4.0) -------------------
|
|
338
|
+
// Right-clicking a temporary row offers "keep as normal" and "delete
|
|
339
|
+
// now". Styled from host tokens like every other surface here; the row
|
|
340
|
+
// itself gains no children, so React re-renders cannot wipe it.
|
|
341
|
+
'[data-dsbs-temp-menu]{position:fixed;z-index:9400;min-width:196px;max-width:min(280px,calc(100vw - 16px));box-sizing:border-box;padding:4px;display:flex;flex-direction:column;gap:1px;border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base,#1f1f1f));color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));box-shadow:0 12px 32px rgba(0,0,0,.32)}',
|
|
342
|
+
'[data-dsbs-temp-menu][hidden]{display:none!important}',
|
|
343
|
+
'[data-dsbs-temp-menu-head]{padding:4px 8px 6px;margin-bottom:2px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.9));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
344
|
+
'[data-dsbs-temp-menu] button{appearance:none;-webkit-appearance:none;display:block;width:100%;box-sizing:border-box;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1.45;text-align:left;padding:6px 8px;border-radius:7px;cursor:pointer;white-space:normal}',
|
|
345
|
+
'[data-dsbs-temp-menu] button:hover,[data-dsbs-temp-menu] button:focus-visible{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));outline:none}',
|
|
346
|
+
'[data-dsbs-temp-menu] button:focus-visible{outline:2px solid var(--dsw-alias-label-primary,currentColor);outline-offset:-2px}',
|
|
347
|
+
'[data-dsbs-temp-menu] button[data-dsbs-temp-action="delete"]{color:var(--dsw-alias-state-business-danger,#d9534f)}',
|
|
348
|
+
'[data-dsbs-temp-menu-sub]{display:block;font-size:10px;line-height:1.4;opacity:.68}',
|
|
349
|
+
'html[data-dsbs-temp-menu="false"] [data-dsbs-temp-menu]{display:none!important}',
|
|
317
350
|
// A temporary session is a NORMAL DSH session, so its sidebar row is the
|
|
318
351
|
// host's own row. Nothing is added to the DOM: we tag the row and let CSS
|
|
319
352
|
// draw the badge, which survives every React re-render of the list.
|
|
@@ -714,6 +747,12 @@ window.__ModuleLoader__.load({
|
|
|
714
747
|
var configPollPending = false
|
|
715
748
|
var tempChatGuard = null
|
|
716
749
|
var tempIds = [] // session ids the Host half will delete on quit
|
|
750
|
+
// 0.4.0 — per-row menu. `tempMenuEnabled` mirrors TEMP_ROW_MENU and is the
|
|
751
|
+
// escape hatch if another plugin wants the context menu on these rows.
|
|
752
|
+
var tempMenuEnabled = true
|
|
753
|
+
var tempRowMenu = null
|
|
754
|
+
var tempMenuRow = null
|
|
755
|
+
var tempMenuDismiss = null
|
|
717
756
|
var tempBusy = false
|
|
718
757
|
var tempToastTimer = 0
|
|
719
758
|
|
|
@@ -982,6 +1021,275 @@ window.__ModuleLoader__.load({
|
|
|
982
1021
|
}
|
|
983
1022
|
}
|
|
984
1023
|
|
|
1024
|
+
// ---- per-row menu on temporary rows (0.4.0) -----------------------
|
|
1025
|
+
// A temporary session used to be all-or-nothing: either it vanished at
|
|
1026
|
+
// quit, or you turned DELETE_ON_EXIT off and every temp session became a
|
|
1027
|
+
// normal one. Now a single row can be promoted ("keep as a normal
|
|
1028
|
+
// session") or deleted on the spot.
|
|
1029
|
+
//
|
|
1030
|
+
// Both actions already existed on the Host half — `untrack` and the new
|
|
1031
|
+
// `remove` route — so this is purely a missing entrance. It never touches
|
|
1032
|
+
// rows that are not temporary: no preventDefault, no stopPropagation, the
|
|
1033
|
+
// host's own context menu keeps working everywhere else.
|
|
1034
|
+
|
|
1035
|
+
/** Nearest ancestor carrying data-row-key="session:<id>". */
|
|
1036
|
+
function tempRowOf(node) {
|
|
1037
|
+
var el = node
|
|
1038
|
+
var guard = 0
|
|
1039
|
+
while (el && el !== document && guard < 60) {
|
|
1040
|
+
guard += 1
|
|
1041
|
+
var key = el.getAttribute ? el.getAttribute('data-row-key') : ''
|
|
1042
|
+
if (key && key.indexOf('session:') === 0) return el
|
|
1043
|
+
el = el.parentNode
|
|
1044
|
+
}
|
|
1045
|
+
return null
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function tempMenuButtons() {
|
|
1049
|
+
var out = []
|
|
1050
|
+
if (!tempRowMenu) return out
|
|
1051
|
+
var list = null
|
|
1052
|
+
try { list = tempRowMenu.querySelectorAll('button[data-dsbs-temp-action]') } catch (error) { void error }
|
|
1053
|
+
if (!list) return out
|
|
1054
|
+
for (var i = 0; i < list.length; i++) out.push(list[i])
|
|
1055
|
+
return out
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function closeTempMenu(restoreFocus) {
|
|
1059
|
+
if (!tempRowMenu) return false
|
|
1060
|
+
var row = tempMenuRow
|
|
1061
|
+
try { if (tempRowMenu.parentNode) tempRowMenu.parentNode.removeChild(tempRowMenu) } catch (error) { void error }
|
|
1062
|
+
tempRowMenu = null
|
|
1063
|
+
tempMenuRow = null
|
|
1064
|
+
detachTempMenuDismiss()
|
|
1065
|
+
if (restoreFocus && row && typeof row.focus === 'function') {
|
|
1066
|
+
try { row.focus() } catch (error) { void error }
|
|
1067
|
+
}
|
|
1068
|
+
return true
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function detachTempMenuDismiss() {
|
|
1072
|
+
var handlers = tempMenuDismiss
|
|
1073
|
+
if (!handlers) return
|
|
1074
|
+
tempMenuDismiss = null
|
|
1075
|
+
try { document.removeEventListener('pointerdown', handlers.onDown, true) } catch (error) { void error }
|
|
1076
|
+
try { document.removeEventListener('keydown', handlers.onKey, true) } catch (error) { void error }
|
|
1077
|
+
try { window.removeEventListener('resize', handlers.onViewport) } catch (error) { void error }
|
|
1078
|
+
try { window.removeEventListener('scroll', handlers.onViewport, true) } catch (error) { void error }
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function attachTempMenuDismiss() {
|
|
1082
|
+
if (tempMenuDismiss) return
|
|
1083
|
+
var handlers = {
|
|
1084
|
+
onDown: function (event) {
|
|
1085
|
+
if (!tempRowMenu) return
|
|
1086
|
+
var target = event && event.target
|
|
1087
|
+
if (target && tempRowMenu.contains && tempRowMenu.contains(target)) return
|
|
1088
|
+
closeTempMenu(false)
|
|
1089
|
+
},
|
|
1090
|
+
onKey: function (event) {
|
|
1091
|
+
if (!tempRowMenu || !event) return
|
|
1092
|
+
var key = event.key
|
|
1093
|
+
if (key === 'Escape') {
|
|
1094
|
+
event.preventDefault()
|
|
1095
|
+
event.stopPropagation()
|
|
1096
|
+
closeTempMenu(true)
|
|
1097
|
+
return
|
|
1098
|
+
}
|
|
1099
|
+
if (key === 'Tab') { closeTempMenu(false); return }
|
|
1100
|
+
if (key !== 'ArrowDown' && key !== 'ArrowUp') return
|
|
1101
|
+
var buttons = tempMenuButtons()
|
|
1102
|
+
if (!buttons.length) return
|
|
1103
|
+
event.preventDefault()
|
|
1104
|
+
var active = document.activeElement
|
|
1105
|
+
var at = buttons.indexOf(active)
|
|
1106
|
+
var next
|
|
1107
|
+
if (key === 'ArrowDown') next = at < 0 ? 0 : (at + 1) % buttons.length
|
|
1108
|
+
else next = at <= 0 ? buttons.length - 1 : at - 1
|
|
1109
|
+
try { buttons[next].focus() } catch (error) { void error }
|
|
1110
|
+
},
|
|
1111
|
+
// The menu is positioned in viewport coordinates, so any scroll or
|
|
1112
|
+
// resize would leave it floating next to the wrong row.
|
|
1113
|
+
onViewport: function () { closeTempMenu(false) },
|
|
1114
|
+
}
|
|
1115
|
+
tempMenuDismiss = handlers
|
|
1116
|
+
try { document.addEventListener('pointerdown', handlers.onDown, true) } catch (error) { void error }
|
|
1117
|
+
try { document.addEventListener('keydown', handlers.onKey, true) } catch (error) { void error }
|
|
1118
|
+
try { window.addEventListener('resize', handlers.onViewport) } catch (error) { void error }
|
|
1119
|
+
try { window.addEventListener('scroll', handlers.onViewport, true) } catch (error) { void error }
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function makeTempMenuItem(action, labelKey, hintKey, onClick) {
|
|
1123
|
+
var button = document.createElement('button')
|
|
1124
|
+
button.type = 'button'
|
|
1125
|
+
button.setAttribute('role', 'menuitem')
|
|
1126
|
+
button.setAttribute('data-dsbs-temp-action', action)
|
|
1127
|
+
var label = document.createElement('span')
|
|
1128
|
+
label.textContent = t(labelKey)
|
|
1129
|
+
button.appendChild(label)
|
|
1130
|
+
var hint = t(hintKey)
|
|
1131
|
+
if (hint) {
|
|
1132
|
+
var sub = document.createElement('span')
|
|
1133
|
+
sub.setAttribute('data-dsbs-temp-menu-sub', '')
|
|
1134
|
+
sub.textContent = hint
|
|
1135
|
+
button.appendChild(sub)
|
|
1136
|
+
}
|
|
1137
|
+
button.addEventListener('click', function (event) {
|
|
1138
|
+
if (event) {
|
|
1139
|
+
event.preventDefault()
|
|
1140
|
+
event.stopPropagation()
|
|
1141
|
+
}
|
|
1142
|
+
closeTempMenu(false)
|
|
1143
|
+
onClick()
|
|
1144
|
+
})
|
|
1145
|
+
return button
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function openTempMenu(row, id, x, y) {
|
|
1149
|
+
closeTempMenu(false)
|
|
1150
|
+
var menu = document.createElement('div')
|
|
1151
|
+
menu.setAttribute('data-dsbs-temp-menu', '')
|
|
1152
|
+
menu.setAttribute('role', 'menu')
|
|
1153
|
+
menu.setAttribute('aria-label', t('temp.menu.label'))
|
|
1154
|
+
// The row's own text is the only title available (the host owns the
|
|
1155
|
+
// session list), and it is what the user just pointed at.
|
|
1156
|
+
var text = (row.textContent || '').replace(/\s+/g, ' ').trim()
|
|
1157
|
+
if (text) {
|
|
1158
|
+
var head = document.createElement('div')
|
|
1159
|
+
head.setAttribute('data-dsbs-temp-menu-head', '')
|
|
1160
|
+
head.textContent = text.length > 48 ? text.slice(0, 48) + '…' : text
|
|
1161
|
+
menu.appendChild(head)
|
|
1162
|
+
}
|
|
1163
|
+
menu.appendChild(makeTempMenuItem('keep', 'temp.menu.keep', 'temp.menu.keep.hint', function () {
|
|
1164
|
+
keepTempSession(id)
|
|
1165
|
+
}))
|
|
1166
|
+
menu.appendChild(makeTempMenuItem('delete', 'temp.menu.delete', 'temp.menu.delete.hint', function () {
|
|
1167
|
+
deleteTempSession(id)
|
|
1168
|
+
}))
|
|
1169
|
+
document.body.appendChild(menu)
|
|
1170
|
+
|
|
1171
|
+
// Measure first, then flip near the viewport edges. Both reads are
|
|
1172
|
+
// wrapped: a detached or unmeasured node must not break the menu.
|
|
1173
|
+
var width = 0
|
|
1174
|
+
var height = 0
|
|
1175
|
+
try {
|
|
1176
|
+
var rect = menu.getBoundingClientRect ? menu.getBoundingClientRect() : null
|
|
1177
|
+
width = (rect && rect.width) || menu.offsetWidth || 0
|
|
1178
|
+
height = (rect && rect.height) || menu.offsetHeight || 0
|
|
1179
|
+
} catch (error) { void error }
|
|
1180
|
+
if (!width) width = 220
|
|
1181
|
+
if (!height) height = 96
|
|
1182
|
+
var vw = window.innerWidth || 1024
|
|
1183
|
+
var vh = window.innerHeight || 768
|
|
1184
|
+
var left = Number.isFinite(x) ? x : 0
|
|
1185
|
+
var top = Number.isFinite(y) ? y : 0
|
|
1186
|
+
if (left + width > vw - 8) left = Math.max(8, vw - width - 8)
|
|
1187
|
+
if (top + height > vh - 8) top = Math.max(8, vh - height - 8)
|
|
1188
|
+
menu.style.left = Math.round(left) + 'px'
|
|
1189
|
+
menu.style.top = Math.round(top) + 'px'
|
|
1190
|
+
|
|
1191
|
+
tempRowMenu = menu
|
|
1192
|
+
tempMenuRow = row
|
|
1193
|
+
attachTempMenuDismiss()
|
|
1194
|
+
var buttons = tempMenuButtons()
|
|
1195
|
+
if (buttons.length) {
|
|
1196
|
+
try { buttons[0].focus() } catch (error) { void error }
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Promote one session: it stops being temporary, nothing is deleted. */
|
|
1201
|
+
function keepTempSession(id) {
|
|
1202
|
+
tempCall('api/dsh-temp-session.untrack?sessionId=' + encodeURIComponent(id), { method: 'POST' })
|
|
1203
|
+
.then(function (data) {
|
|
1204
|
+
if (!data || data.ok !== true) throw new Error((data && data.error) || 'untrack failed')
|
|
1205
|
+
applyTempIdList(data.ids, id)
|
|
1206
|
+
toast(t('temp.menu.kept'), false)
|
|
1207
|
+
scheduleSync()
|
|
1208
|
+
})
|
|
1209
|
+
.catch(function (error) {
|
|
1210
|
+
toast(t('temp.menu.failed') + String((error && error.message) || error), true)
|
|
1211
|
+
})
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** Delete one session now, with the same fallback `purgeAll` uses. */
|
|
1215
|
+
function deleteTempSession(id) {
|
|
1216
|
+
tempCall('api/dsh-temp-session.remove?sessionId=' + encodeURIComponent(id), { method: 'POST' })
|
|
1217
|
+
.then(function (data) {
|
|
1218
|
+
if (!data || data.ok !== true) throw new Error((data && data.error) || 'delete failed')
|
|
1219
|
+
applyTempIdList(data.ids, id)
|
|
1220
|
+
toast(t(data.hidden ? 'temp.menu.hidden' : 'temp.menu.deleted'), false)
|
|
1221
|
+
refreshHostSessions()
|
|
1222
|
+
scheduleSync()
|
|
1223
|
+
})
|
|
1224
|
+
.catch(function (error) {
|
|
1225
|
+
toast(t('temp.menu.failed') + String((error && error.message) || error), true)
|
|
1226
|
+
})
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** Adopt the id list the Host returned, or just drop the one id. */
|
|
1230
|
+
function applyTempIdList(ids, removedId) {
|
|
1231
|
+
if (Array.isArray(ids)) tempIds = ids.slice()
|
|
1232
|
+
else tempIds = tempIds.filter(function (item) { return item !== removedId })
|
|
1233
|
+
applyTempRows()
|
|
1234
|
+
if (tempRowMenu && tempMenuRow) {
|
|
1235
|
+
var key = tempMenuRow.getAttribute('data-row-key') || ''
|
|
1236
|
+
if (tempIds.indexOf(key.slice('session:'.length)) < 0) closeTempMenu(false)
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Ask the host's own session list to re-read. There is no documented API
|
|
1242
|
+
* for this, so every common name is tried and failure is silent: a row
|
|
1243
|
+
* that outlives its files disappears on the next list refresh anyway.
|
|
1244
|
+
*/
|
|
1245
|
+
function refreshHostSessions() {
|
|
1246
|
+
try {
|
|
1247
|
+
var ui = uiWorkspace()
|
|
1248
|
+
var sessions = ui && ui.sessions
|
|
1249
|
+
var names = ['reload', 'refresh', 'reloadSessions', 'invalidate']
|
|
1250
|
+
for (var i = 0; i < names.length; i++) {
|
|
1251
|
+
if (sessions && typeof sessions[names[i]] === 'function') {
|
|
1252
|
+
sessions[names[i]]()
|
|
1253
|
+
return true
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
void error
|
|
1258
|
+
}
|
|
1259
|
+
return false
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function onTempContextMenu(event) {
|
|
1263
|
+
if (runtime.disposed || !tempMenuEnabled || !event) return
|
|
1264
|
+
var row = tempRowOf(event.target)
|
|
1265
|
+
if (!row) return
|
|
1266
|
+
var key = row.getAttribute('data-row-key') || ''
|
|
1267
|
+
var id = key.slice('session:'.length)
|
|
1268
|
+
// Not one of ours: leave the event completely alone so the host (or
|
|
1269
|
+
// whichever plugin owns that row) still gets its own menu.
|
|
1270
|
+
if (!id || tempIds.indexOf(id) < 0) return
|
|
1271
|
+
event.preventDefault()
|
|
1272
|
+
event.stopPropagation()
|
|
1273
|
+
openTempMenu(row, id, event.clientX, event.clientY)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
var tempMenuUnmount = null
|
|
1277
|
+
function mountTempRowMenu() {
|
|
1278
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return
|
|
1279
|
+
document.addEventListener('contextmenu', onTempContextMenu)
|
|
1280
|
+
tempMenuUnmount = function () {
|
|
1281
|
+
try { document.removeEventListener('contextmenu', onTempContextMenu) } catch (error) { void error }
|
|
1282
|
+
closeTempMenu(false)
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function applyTempMenuSetting(data) {
|
|
1287
|
+
var next = !data || data.rowMenu !== false
|
|
1288
|
+
if (next === tempMenuEnabled) return
|
|
1289
|
+
tempMenuEnabled = next
|
|
1290
|
+
if (!next) closeTempMenu(false)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
985
1293
|
function refreshTempIds() {
|
|
986
1294
|
return tempCall('api/dsh-temp-session.list')
|
|
987
1295
|
.then(function (data) {
|
|
@@ -1089,6 +1397,8 @@ window.__ModuleLoader__.load({
|
|
|
1089
1397
|
expandAll = !data || data.expandAllSessions !== false
|
|
1090
1398
|
expandAllKnown = true
|
|
1091
1399
|
applyExpandFlag()
|
|
1400
|
+
// 0.4.0 — same treatment: a Host without this key keeps the menu on.
|
|
1401
|
+
applyTempMenuSetting(data)
|
|
1092
1402
|
syncChatMode()
|
|
1093
1403
|
expandAllSessions()
|
|
1094
1404
|
scheduleSync()
|
|
@@ -1282,11 +1592,32 @@ window.__ModuleLoader__.load({
|
|
|
1282
1592
|
return bar
|
|
1283
1593
|
}
|
|
1284
1594
|
|
|
1595
|
+
// A paint must be able to leave the tree exactly as it found it. Asking
|
|
1596
|
+
// for an exact neighbour is not enough: the 0.3.0 tab strip (and an
|
|
1597
|
+
// active plugin's panel) legitimately sit between the control bar and
|
|
1598
|
+
// the host's panels nav, so `bar.nextSibling === nav` can never be true
|
|
1599
|
+
// once those exist. Moving the bar on every paint mutates the observed
|
|
1600
|
+
// root, which fires the MutationObserver, which schedules another paint
|
|
1601
|
+
// -- a microtask loop that never returns to the event loop and hangs DSH
|
|
1602
|
+
// on "Loading plugins..." with the renderer at 100%. Compare ORDER, not
|
|
1603
|
+
// adjacency.
|
|
1604
|
+
function isBefore(node, reference) {
|
|
1605
|
+
if (!node || !reference || !node.parentNode || node.parentNode !== reference.parentNode) return false
|
|
1606
|
+
var list = node.parentNode.children
|
|
1607
|
+
var at = -1
|
|
1608
|
+
var before = -1
|
|
1609
|
+
for (var i = 0; i < list.length; i++) {
|
|
1610
|
+
if (list[i] === node) at = i
|
|
1611
|
+
if (list[i] === reference) before = i
|
|
1612
|
+
}
|
|
1613
|
+
return at >= 0 && before >= 0 && at < before
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1285
1616
|
function ensureBar(fx) {
|
|
1286
1617
|
var bar = runtime.bar
|
|
1287
1618
|
if (bar && bar.parentNode !== fx.root) {
|
|
1288
1619
|
try {
|
|
1289
|
-
bar.parentNode.removeChild(bar)
|
|
1620
|
+
if (bar.parentNode) bar.parentNode.removeChild(bar)
|
|
1290
1621
|
} catch (error) {
|
|
1291
1622
|
void error
|
|
1292
1623
|
}
|
|
@@ -1298,7 +1629,7 @@ window.__ModuleLoader__.load({
|
|
|
1298
1629
|
}
|
|
1299
1630
|
var anchor = fx.nav || fx.regionArea
|
|
1300
1631
|
if (anchor && anchor.parentNode === fx.root) {
|
|
1301
|
-
if (bar.
|
|
1632
|
+
if (bar.parentNode !== fx.root || !isBefore(bar, anchor)) fx.root.insertBefore(bar, anchor)
|
|
1302
1633
|
} else if (bar.parentNode !== fx.root) {
|
|
1303
1634
|
fx.root.appendChild(bar)
|
|
1304
1635
|
}
|
|
@@ -1566,7 +1897,7 @@ window.__ModuleLoader__.load({
|
|
|
1566
1897
|
var strip = runtime.tabStrip
|
|
1567
1898
|
if (strip && strip.parentNode !== fx.root) {
|
|
1568
1899
|
try {
|
|
1569
|
-
strip.parentNode.removeChild(strip)
|
|
1900
|
+
if (strip.parentNode) strip.parentNode.removeChild(strip)
|
|
1570
1901
|
} catch (error) {
|
|
1571
1902
|
void error
|
|
1572
1903
|
}
|
|
@@ -1579,12 +1910,17 @@ window.__ModuleLoader__.load({
|
|
|
1579
1910
|
strip.setAttribute('aria-label', t('tabs.label'))
|
|
1580
1911
|
runtime.tabStrip = strip
|
|
1581
1912
|
}
|
|
1582
|
-
// Sits under the control bar so the two read as one header.
|
|
1583
|
-
//
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
if (
|
|
1587
|
-
|
|
1913
|
+
// Sits under the control bar so the two read as one header. A plugin
|
|
1914
|
+
// panel may sit after it, so "immediately after the bar" is too strict
|
|
1915
|
+
// a condition for the steady state: demanding an exact previousSibling
|
|
1916
|
+
// moves the strip on every paint and spins the observer loop.
|
|
1917
|
+
if (strip.parentNode === fx.root && (!runtime.bar || isBefore(runtime.bar, strip))) return strip
|
|
1918
|
+
var anchor = runtime.bar && runtime.bar.parentNode === fx.root
|
|
1919
|
+
? runtime.bar.nextSibling
|
|
1920
|
+
: (fx.nav || fx.regionArea)
|
|
1921
|
+
if (anchor === strip) return strip
|
|
1922
|
+
if (anchor && anchor.parentNode === fx.root) fx.root.insertBefore(strip, anchor)
|
|
1923
|
+
else fx.root.appendChild(strip)
|
|
1588
1924
|
return strip
|
|
1589
1925
|
}
|
|
1590
1926
|
|
|
@@ -2033,8 +2369,32 @@ window.__ModuleLoader__.load({
|
|
|
2033
2369
|
}
|
|
2034
2370
|
|
|
2035
2371
|
var pending = false
|
|
2372
|
+
var syncStreak = 0
|
|
2373
|
+
var streakTimer = null
|
|
2374
|
+
var streakDeferred = false
|
|
2375
|
+
// Safety net for the observer feedback loop: a paint that mutates the
|
|
2376
|
+
// observed root chains MutationObserver -> scheduleSync -> paint in
|
|
2377
|
+
// microtasks forever, so the renderer never gets a turn and DSH hangs on
|
|
2378
|
+
// "Loading plugins...". A healthy startup needs only a handful of syncs
|
|
2379
|
+
// between two macrotasks. Cap the streak, yield once, then resume; this
|
|
2380
|
+
// cannot lose a state change, and it keeps unrelated future regressions
|
|
2381
|
+
// from taking the whole app down.
|
|
2036
2382
|
function scheduleSync() {
|
|
2037
2383
|
if (runtime.disposed || pending) return
|
|
2384
|
+
if (!streakTimer) {
|
|
2385
|
+
streakTimer = setTimeout(function () {
|
|
2386
|
+
streakTimer = null
|
|
2387
|
+
syncStreak = 0
|
|
2388
|
+
var again = streakDeferred
|
|
2389
|
+
streakDeferred = false
|
|
2390
|
+
if (again && !runtime.disposed) scheduleSync()
|
|
2391
|
+
}, 0)
|
|
2392
|
+
}
|
|
2393
|
+
if (syncStreak >= 16) {
|
|
2394
|
+
streakDeferred = true
|
|
2395
|
+
return
|
|
2396
|
+
}
|
|
2397
|
+
syncStreak += 1
|
|
2038
2398
|
pending = true
|
|
2039
2399
|
Promise.resolve().then(function () {
|
|
2040
2400
|
pending = false
|
|
@@ -2488,6 +2848,8 @@ window.__ModuleLoader__.load({
|
|
|
2488
2848
|
inChatMode: data.inChatMode === 'show' ? 'show' : 'hide',
|
|
2489
2849
|
buttonStyle: data.buttonStyle === 'own' ? 'own' : 'pair',
|
|
2490
2850
|
expandAllSessions: data.expandAllSessions !== false,
|
|
2851
|
+
// Older Host halves have no rowMenu key; the menu ships on.
|
|
2852
|
+
rowMenu: data.rowMenu !== false,
|
|
2491
2853
|
maxKept: kept > 0 ? Math.min(Math.round(kept), 200) : 20,
|
|
2492
2854
|
configRevision: typeof data.configRevision === 'number' ? data.configRevision : null,
|
|
2493
2855
|
configFile: typeof data.configFile === 'string' ? data.configFile : '',
|
|
@@ -2690,6 +3052,10 @@ window.__ModuleLoader__.load({
|
|
|
2690
3052
|
persist({ expandAllSessions: next })
|
|
2691
3053
|
}),
|
|
2692
3054
|
h('div', { style: dim }, t('options.expandHint')),
|
|
3055
|
+
policyRow(t('options.rowMenu'), policy.rowMenu === true, function (next) {
|
|
3056
|
+
persist({ rowMenu: next })
|
|
3057
|
+
}),
|
|
3058
|
+
h('div', { style: dim }, t('options.rowMenuHint')),
|
|
2693
3059
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
|
|
2694
3060
|
h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('options.maxKept')),
|
|
2695
3061
|
h('input', {
|
|
@@ -2993,6 +3359,14 @@ window.__ModuleLoader__.load({
|
|
|
2993
3359
|
}
|
|
2994
3360
|
}, 'sidebar-suite: dismissal')
|
|
2995
3361
|
|
|
3362
|
+
effect(function () {
|
|
3363
|
+
// 0.4.0 — right-click on a temporary row. One delegated listener on the
|
|
3364
|
+
// document, because the host recreates session rows on every list
|
|
3365
|
+
// refresh; per-row listeners would need re-binding after each render.
|
|
3366
|
+
mountTempRowMenu()
|
|
3367
|
+
return tempMenuUnmount
|
|
3368
|
+
}, 'sidebar-suite: temp row menu')
|
|
3369
|
+
|
|
2996
3370
|
effect(function () {
|
|
2997
3371
|
// Safety net: the sidebar root is created and destroyed by the host, so
|
|
2998
3372
|
// a root replacement is invisible to an observer that watches the old
|
|
@@ -3038,6 +3412,13 @@ window.__ModuleLoader__.load({
|
|
|
3038
3412
|
teardown = function () {
|
|
3039
3413
|
if (runtime.disposed) return
|
|
3040
3414
|
runtime.disposed = true
|
|
3415
|
+
try {
|
|
3416
|
+
if (streakTimer) clearTimeout(streakTimer)
|
|
3417
|
+
streakTimer = null
|
|
3418
|
+
streakDeferred = false
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
void error
|
|
3421
|
+
}
|
|
3041
3422
|
try {
|
|
3042
3423
|
clearTimeout(tempToastTimer)
|
|
3043
3424
|
var toastNode = document.querySelector('[data-dsbs-temp-toast]')
|
|
@@ -3091,6 +3472,17 @@ window.__ModuleLoader__.load({
|
|
|
3091
3472
|
} catch (error) {
|
|
3092
3473
|
void error
|
|
3093
3474
|
}
|
|
3475
|
+
try {
|
|
3476
|
+
// 0.4.0 — the row menu lives in <body> and its document listeners
|
|
3477
|
+
// outlive the sidebar if they are not dropped explicitly.
|
|
3478
|
+
closeTempMenu(false)
|
|
3479
|
+
detachTempMenuDismiss()
|
|
3480
|
+
if (tempMenuUnmount) { tempMenuUnmount(); tempMenuUnmount = null }
|
|
3481
|
+
tempRowMenu = null
|
|
3482
|
+
tempMenuRow = null
|
|
3483
|
+
} catch (error) {
|
|
3484
|
+
void error
|
|
3485
|
+
}
|
|
3094
3486
|
try {
|
|
3095
3487
|
if (runtime.bar && runtime.bar.parentNode) runtime.bar.parentNode.removeChild(runtime.bar)
|
|
3096
3488
|
} catch (error) {
|
|
@@ -58,6 +58,10 @@ const DEFAULT_CONFIG = {
|
|
|
58
58
|
// 0.2.4: the host's workspace list folds after five sessions and paints a
|
|
59
59
|
// "展开其余 N 个会话" button. This plugin expands every group instead.
|
|
60
60
|
expandAllSessions: true,
|
|
61
|
+
// 0.4.0: right-clicking a temporary session row offers "keep as a normal
|
|
62
|
+
// session" and "delete now". Off means the row behaves like a plain session
|
|
63
|
+
// row again — the escape hatch if another plugin owns the context menu.
|
|
64
|
+
rowMenu: true,
|
|
61
65
|
deleteOnExit: true,
|
|
62
66
|
// Daily cleanup (0.2.2). Independent of deleteOnExit: a user who keeps DSH
|
|
63
67
|
// open for days wants "delete at 03:00 every night", and a user who closes it
|
|
@@ -148,6 +152,7 @@ const CONFIG_KEYS = {
|
|
|
148
152
|
inChatMode: { key: 'TEMP_IN_CHAT_MODE', text: (value) => String(value) },
|
|
149
153
|
buttonStyle: { key: 'TEMP_BUTTON_STYLE', text: (value) => String(value) },
|
|
150
154
|
expandAllSessions: { key: 'EXPAND_ALL_SESSIONS', text: (value) => (value ? 'on' : 'off') },
|
|
155
|
+
rowMenu: { key: 'TEMP_ROW_MENU', text: (value) => (value ? 'on' : 'off') },
|
|
151
156
|
deleteOnExit: { key: 'DELETE_ON_EXIT', text: (value) => (value ? 'on' : 'off') },
|
|
152
157
|
scheduleDelete: { key: 'SCHEDULE_DELETE', text: (value) => (value ? 'on' : 'off') },
|
|
153
158
|
scheduleTime: { key: 'SCHEDULE_TIME', text: (value) => String(value) },
|
|
@@ -185,6 +190,7 @@ function parseConfig() {
|
|
|
185
190
|
if (kv.EXPAND_ALL_SESSIONS !== undefined) {
|
|
186
191
|
next.expandAllSessions = kv.EXPAND_ALL_SESSIONS.toLowerCase() !== 'off'
|
|
187
192
|
}
|
|
193
|
+
if (kv.TEMP_ROW_MENU !== undefined) next.rowMenu = kv.TEMP_ROW_MENU.toLowerCase() !== 'off'
|
|
188
194
|
if (kv.DELETE_ON_EXIT !== undefined) next.deleteOnExit = kv.DELETE_ON_EXIT.toLowerCase() !== 'off'
|
|
189
195
|
if (kv.SCHEDULE_DELETE !== undefined) next.scheduleDelete = kv.SCHEDULE_DELETE.toLowerCase() !== 'off'
|
|
190
196
|
if (kv.SCHEDULE_TIME !== undefined) {
|
|
@@ -224,7 +230,7 @@ function refreshConfigIfChanged() {
|
|
|
224
230
|
state.configStamp = stamp
|
|
225
231
|
const next = parseConfig()
|
|
226
232
|
const fields = [
|
|
227
|
-
'showButton', 'inChatMode', 'buttonStyle', 'expandAllSessions',
|
|
233
|
+
'showButton', 'inChatMode', 'buttonStyle', 'expandAllSessions', 'rowMenu',
|
|
228
234
|
'deleteOnExit', 'scheduleDelete', 'scheduleTime', 'maxKept',
|
|
229
235
|
]
|
|
230
236
|
const changed = fields.some((field) => next[field] !== state.config[field])
|
|
@@ -255,6 +261,7 @@ function saveConfig(patch) {
|
|
|
255
261
|
if (input.inChatMode === 'show' || input.inChatMode === 'hide') next.inChatMode = input.inChatMode
|
|
256
262
|
if (input.buttonStyle === 'own' || input.buttonStyle === 'pair') next.buttonStyle = input.buttonStyle
|
|
257
263
|
if (typeof input.expandAllSessions === 'boolean') next.expandAllSessions = input.expandAllSessions
|
|
264
|
+
if (typeof input.rowMenu === 'boolean') next.rowMenu = input.rowMenu
|
|
258
265
|
if (typeof input.deleteOnExit === 'boolean') next.deleteOnExit = input.deleteOnExit
|
|
259
266
|
if (typeof input.scheduleDelete === 'boolean') next.scheduleDelete = input.scheduleDelete
|
|
260
267
|
const clock = normalizeClock(input.scheduleTime)
|
|
@@ -324,6 +331,7 @@ function publicConfig() {
|
|
|
324
331
|
inChatMode: state.config.inChatMode,
|
|
325
332
|
buttonStyle: state.config.buttonStyle,
|
|
326
333
|
expandAllSessions: state.config.expandAllSessions,
|
|
334
|
+
rowMenu: state.config.rowMenu,
|
|
327
335
|
deleteOnExit: state.config.deleteOnExit,
|
|
328
336
|
scheduleDelete: state.config.scheduleDelete,
|
|
329
337
|
scheduleTime: state.config.scheduleTime,
|
|
@@ -536,6 +544,59 @@ async function purgeAll(reason, options) {
|
|
|
536
544
|
return report
|
|
537
545
|
}
|
|
538
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Delete one tracked session from the sidebar menu (0.4.0).
|
|
549
|
+
*
|
|
550
|
+
* Same steps as `purgeAll` for a single id: remove the directories, verify they
|
|
551
|
+
* are really gone, fall back to hiding the row when a file is locked. Two
|
|
552
|
+
* deliberate differences:
|
|
553
|
+
*
|
|
554
|
+
* - **Only tracked ids are accepted.** This route is reachable from the page, so
|
|
555
|
+
* it must never become "delete any session by id". An untracked id is refused
|
|
556
|
+
* before any filesystem call.
|
|
557
|
+
* - **A failed delete keeps the id tracked**, exactly like `purgeAll`, so the
|
|
558
|
+
* next launch retries it instead of leaving an undeletable row that silently
|
|
559
|
+
* stopped being temporary.
|
|
560
|
+
*/
|
|
561
|
+
async function removeOne(sessionId) {
|
|
562
|
+
await ensureReady()
|
|
563
|
+
const id = normalizeId(sessionId)
|
|
564
|
+
if (id.length === 0) return { ok: false, error: 'invalid session id' }
|
|
565
|
+
if (!state.tracked.includes(id)) return { ok: false, error: 'not a temporary session' }
|
|
566
|
+
let removed = 0
|
|
567
|
+
try {
|
|
568
|
+
removed = await removeSession(id)
|
|
569
|
+
} catch (error) {
|
|
570
|
+
void error
|
|
571
|
+
}
|
|
572
|
+
const gone = removed > 0 && sessionDirsSync(id).length === 0
|
|
573
|
+
let hidden = false
|
|
574
|
+
if (!gone) hidden = await hideSession(id)
|
|
575
|
+
const report = {
|
|
576
|
+
reason: 'single',
|
|
577
|
+
at: new Date().toISOString(),
|
|
578
|
+
tried: 1,
|
|
579
|
+
removed: gone ? 1 : 0,
|
|
580
|
+
hidden: hidden ? 1 : 0,
|
|
581
|
+
skipped: 0,
|
|
582
|
+
failed: [],
|
|
583
|
+
}
|
|
584
|
+
if (gone || hidden) {
|
|
585
|
+
state.tracked = state.tracked.filter((item) => item !== id)
|
|
586
|
+
await saveStore()
|
|
587
|
+
state.lastPurge = report
|
|
588
|
+
return { ok: true, removed: gone, hidden, ids: state.tracked.slice(), count: state.tracked.length }
|
|
589
|
+
}
|
|
590
|
+
report.failed.push(id)
|
|
591
|
+
state.lastPurge = report
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: 'delete failed — the session files are locked',
|
|
595
|
+
ids: state.tracked.slice(),
|
|
596
|
+
count: state.tracked.length,
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
539
600
|
/** Delete every tracked session synchronously — the quit path has no `await`. */
|
|
540
601
|
function purgeSync(reason) {
|
|
541
602
|
if (state.tracked.length === 0) return { reason, tried: 0, removed: 0, kept: [] }
|
|
@@ -723,6 +784,7 @@ function status() {
|
|
|
723
784
|
chatModePolicy: state.config.inChatMode,
|
|
724
785
|
buttonStyle: state.config.buttonStyle,
|
|
725
786
|
expandAllSessions: state.config.expandAllSessions,
|
|
787
|
+
rowMenu: state.config.rowMenu,
|
|
726
788
|
deleteOnExit: state.config.deleteOnExit,
|
|
727
789
|
scheduleDelete: state.config.scheduleDelete,
|
|
728
790
|
scheduleTime: state.config.scheduleTime,
|
|
@@ -860,6 +922,12 @@ export function createTempSessionHost(initialCtx) {
|
|
|
860
922
|
if (sessionId.length === 0) return json({ ok: false, error: 'sessionId required' }, 400)
|
|
861
923
|
return json(await untrack(sessionId))
|
|
862
924
|
}],
|
|
925
|
+
// 0.4.0 — delete one temporary session from the row menu.
|
|
926
|
+
['/api/dsh-temp-session.remove', ['POST', 'GET'], async (request) => {
|
|
927
|
+
const { sessionId } = queryOf(request)
|
|
928
|
+
if (sessionId.length === 0) return json({ ok: false, error: 'sessionId required' }, 400)
|
|
929
|
+
return json(await removeOne(sessionId))
|
|
930
|
+
}],
|
|
863
931
|
['/api/dsh-temp-session.purge', ['POST', 'GET'], async () => {
|
|
864
932
|
await ensureReady()
|
|
865
933
|
return json({ ok: true, ...(await purgeAll('manual')) })
|
package/locale/en.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "侧栏布局大师 · Sidebar Suite",
|
|
4
|
-
"description": "一款专门针对改进 DSH 左侧栏体验的插件:三种布局一键切换、会话默认全展开、临时会话默认在退出 DSH
|
|
4
|
+
"description": "一款专门针对改进 DSH 左侧栏体验的插件:三种布局一键切换、会话默认全展开、临时会话默认在退出 DSH 时删除(每天定时清理默认关,可在本页开启并设时间)、临时会话可右键保留为正式会话或立即删除、设置与反馈都在本页(全部 9 项配置都有开关,改配置文件约 3 秒自动生效、免重启),强实用性;界面沿用宿主原生配色、圆角与间距,只加属性不重构宿主结构,克制又好看。|A plugin dedicated to a better DSH sidebar: one-click layouts, always-expanded sessions, temp sessions deleted when DSH quits by default (daily cleanup off by default and opt-in) and a right-click menu to keep or delete one temp session, in-page settings/feedback — every config key has a switch and file edits hot-reload — seriously practical; native colours, radii and spacing with zero host-DOM rewrites — restrained and good-looking.",
|
|
5
5
|
"detail": "DETAIL.md"
|
|
6
6
|
}
|
|
7
7
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "侧栏布局大师 · Sidebar Suite",
|
|
4
|
-
"description": "一款专门针对改进 DSH 左侧栏体验的插件:三种布局一键切换、会话默认全展开、临时会话默认在退出 DSH
|
|
4
|
+
"description": "一款专门针对改进 DSH 左侧栏体验的插件:三种布局一键切换、会话默认全展开、临时会话默认在退出 DSH 时删除(每天定时清理默认关,可在本页开启并设时间)、临时会话可右键保留为正式会话或立即删除、设置与反馈都在本页(全部 9 项配置都有开关,改配置文件约 3 秒自动生效、免重启),强实用性;界面沿用宿主原生配色、圆角与间距,只加属性不重构宿主结构,克制又好看。|A plugin dedicated to a better DSH sidebar: one-click layouts, always-expanded sessions, temp sessions deleted when DSH quits by default (daily cleanup off by default and opt-in) and a right-click menu to keep or delete one temp session, in-page settings/feedback — every config key has a switch and file edits hot-reload — seriously practical; native colours, radii and spacing with zero host-DOM rewrites — restrained and good-looking.",
|
|
5
5
|
"detail": "DETAIL.md"
|
|
6
6
|
}
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidebar-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Yonan",
|
|
6
6
|
"repository": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
|
-
"description": "一款专门针对改进 DSH 左侧栏体验的插件:三种布局一键切换、默认布局在详情页顶部直接设、会话列表默认全展开、「临时会话」开原生会话且默认在退出 DSH 时删除(DELETE_ON_EXIT
|
|
17
|
+
"description": "一款专门针对改进 DSH 左侧栏体验的插件:三种布局一键切换、默认布局在详情页顶部直接设、会话列表默认全展开、「临时会话」开原生会话且默认在退出 DSH 时删除(DELETE_ON_EXIT 默认开;每天定时清理默认关,可在详情页开启并自设时间)、详情页三种反馈入口、临时会话可右键「保留为正式会话」或「立即删除」、全部 9 项配置都有图形开关且改文件免重启 —— 强实用性;界面沿用宿主原生配色、圆角与间距,只加属性与一条控制栏、不重构宿主结构 —— 克制美观。|A plugin dedicated to the DSH sidebar experience: one-click layouts, always-expanded sessions, native temp sessions deleted when DSH quits by default (DELETE_ON_EXIT on; daily cleanup off by default and opt-in) in-page settings/feedback, a right-click menu to keep or delete one temp session, a control for every config key and hot-reloaded config edits — seriously practical; native colours, radii and spacing with zero host-DOM rewrites — restrained and good-looking.",
|
|
18
18
|
"keywords": [
|
|
19
19
|
"dsh-plugin",
|
|
20
20
|
"dsh",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"type": "module",
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node tests/test_020.mjs && node tests/test_022.mjs && node tests/test_023.mjs && node tests/test_024.mjs && node tests/test_025.mjs && node tests/test_028.mjs && node tests/test_029.mjs && node tests/test_030.mjs && node tests/test_031.mjs",
|
|
30
|
+
"test": "node tests/test_020.mjs && node tests/test_022.mjs && node tests/test_023.mjs && node tests/test_024.mjs && node tests/test_025.mjs && node tests/test_028.mjs && node tests/test_029.mjs && node tests/test_030.mjs && node tests/test_031.mjs && node tests/test_032.mjs && node tests/test_033.mjs",
|
|
31
31
|
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/temp-session-host.mjs && npm pack --dry-run"
|
|
32
32
|
},
|
|
33
33
|
"main": "lib/index.js",
|
package/temp-session-config.txt
CHANGED
|
@@ -33,6 +33,13 @@ TEMP_BUTTON_STYLE=pair
|
|
|
33
33
|
# off = keep DSH's own folding and leave that button visible
|
|
34
34
|
EXPAND_ALL_SESSIONS=on
|
|
35
35
|
|
|
36
|
+
# 右键临时会话时,是否弹出「保留为正式会话 / 立即删除」菜单 / Right-click menu
|
|
37
|
+
# on = 弹出(默认)。选「保留为正式会话」后,这个会话就不再算临时,退出时不会删。
|
|
38
|
+
# off = 不接管右键菜单(别的插件抢右键时用这个)
|
|
39
|
+
# on = show the menu (default). "Keep as a normal session" makes it permanent.
|
|
40
|
+
# off = leave the context menu alone (use this if another plugin owns it)
|
|
41
|
+
TEMP_ROW_MENU=on
|
|
42
|
+
|
|
36
43
|
# 退出 DSH 时删除临时会话 / Delete temporary sessions when DSH quits
|
|
37
44
|
# on = 退出时删除(默认);off = 不删除,临时会话就变成普通会话
|
|
38
45
|
DELETE_ON_EXIT=on
|
|
@@ -183,9 +183,16 @@ DSH **没有公开的「删除会话」接口**(官方界面的「归档」只
|
|
|
183
183
|
|
|
184
184
|
为什么必须这样:要用原生窗口、还要能中途切走再切回,它就只能是真会话。如果要求「连磁盘都不许写」,那就只能是不在侧栏、不能多任务的纯内存方案 —— 和你要的正好相反。
|
|
185
185
|
|
|
186
|
-
### 4.5
|
|
186
|
+
### 4.5 想留住某一个 / 想全都不删
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
**只想留住某一个(推荐,0.4.0)**:在侧栏里**右键那个带「临时」标签的会话**,选「保留为正式会话」。
|
|
189
|
+
|
|
190
|
+
它立刻变成普通会话 —— 退出不删、定时清理也不碰,**其它临时会话照旧按时删掉**。同一个菜单里还有「立即删除」,用来当场删掉这一个,不用等退出。
|
|
191
|
+
|
|
192
|
+
- 右键**普通会话**时插件完全不插手,你的其它右键菜单照常出现。
|
|
193
|
+
- 右键没反应?看配置第 41 行 `TEMP_ROW_MENU` 是不是被改成 `off` 了(详情页的「临时会话右键菜单」开关写的是同一行)。
|
|
194
|
+
|
|
195
|
+
**想全部保留**:把配置第 45 行 `DELETE_ON_EXIT=off`(见下一节)。关掉后所有临时会话都不再删除,等于普通会话。
|
|
189
196
|
|
|
190
197
|
---
|
|
191
198
|
|
|
@@ -201,18 +208,19 @@ DSH **没有公开的「删除会话」接口**(官方界面的「归档」只
|
|
|
201
208
|
| **19** | `TEMP_IN_CHAT_MODE=hide` | `hide` / `show` | 装了 `dsh-deepseek-chat` 时,处于 CHAT 官网模式要不要显示按钮 |
|
|
202
209
|
| **27** | `TEMP_BUTTON_STYLE=pair` | `pair` / `own` | `pair` = 和「新会话」并排;`own` = 单独占一整行,完全不改「新会话」 |
|
|
203
210
|
| **34** | `EXPAND_ALL_SESSIONS=on` | `on` / `off` | `on` = 会话列表全部展开、隐藏「展开其余 N 个会话」;`off` = 恢复 DSH 原来的折叠 |
|
|
204
|
-
| **
|
|
205
|
-
| **
|
|
206
|
-
| **
|
|
207
|
-
| **
|
|
211
|
+
| **41** | `TEMP_ROW_MENU=on` | `on` / `off` | `on` = 右键临时会话可「保留为正式会话 / 立即删除」;`off` = 不接管右键(别的插件抢右键时用) |
|
|
212
|
+
| **45** | `DELETE_ON_EXIT=on` | `on` / `off` | **退出 DSH 时**删不删 |
|
|
213
|
+
| **51** | `SCHEDULE_DELETE=off` | `on` / `off` | **每天定时清理**要不要开 |
|
|
214
|
+
| **56** | `SCHEDULE_TIME=03:00` | `HH:MM`(如 `03:00`、`23:30`) | 定时清理的时间点,24 小时制、本机时间 |
|
|
215
|
+
| **60** | `MAX_KEPT=20` | 1–200 | 最多同时留几个临时会话,超了先删最早的 |
|
|
208
216
|
|
|
209
|
-
> 这一整节都可以不动手改文件 —— 0.2.8 起详情页有**全部
|
|
217
|
+
> 这一整节都可以不动手改文件 —— 0.2.8 起详情页有**全部 9 项的图形化开关**,见 5.1。两种改法写的是**同一份文件**,改完都在约 3 秒内生效,不会打架。
|
|
210
218
|
|
|
211
219
|
### 5.1 全部设置都在详情页里改(推荐,不用改文件)
|
|
212
220
|
|
|
213
221
|
打开 DSH → 插件 → `dsh-sidebar-suite` → 详情页最上面那张卡。0.2.8 起这里是**两块设置**:
|
|
214
222
|
|
|
215
|
-
**第一块「侧栏与临时会话」**(这一节表里
|
|
223
|
+
**第一块「侧栏与临时会话」**(这一节表里 9 个键里的前 5 个 + 保留上限,全都能点):
|
|
216
224
|
|
|
217
225
|
```
|
|
218
226
|
侧栏与临时会话 · Sidebar & temp session options
|
|
@@ -222,6 +230,7 @@ DSH **没有公开的「删除会话」接口**(官方界面的「归档」只
|
|
|
222
230
|
CHAT 官网模式 [隐藏按钮] / [照常显示]
|
|
223
231
|
按钮摆法 [并排] / [独占一行]
|
|
224
232
|
会话默认全展开 [开] / [关]
|
|
233
|
+
临时会话右键菜单 [开] / [关]
|
|
225
234
|
最多保留 [ 20 ] 个临时会话(1–200)
|
|
226
235
|
设置版本 12 · …/temp-session-config.txt
|
|
227
236
|
```
|