dsh-sidebar-suite 0.4.0 → 0.5.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 CHANGED
@@ -1,17 +1,57 @@
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`、`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.
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`、`0.4.1`、`0.5.0`)。
4
+ > ⚠️ **`0.3.0` 已弃用 / deprecated**:它会让 DSH 卡在 `Loading plugins…` 且主窗口不显示(见 0.4.0 章节「修复启动死循环」)。请升级到 `0.5.0` 或更高版本。`0.3.0` hangs DSH on the loading screen; upgrade to 0.5.0 or later.
5
5
  > `0.2.0`、`0.2.1`、`0.2.2`、`0.2.3` 是开发过程中的本地迭代号,**从未单独发布**;它们的功能一次性随 `0.2.4` 上线,所以在下面合并记录。
6
6
  >
7
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`.
8
8
 
9
9
  ---
10
10
 
11
+ ## 0.5.0 — 砍掉折叠布局、加入官方默认,并修好控制栏位置(2026-11-12)
12
+
13
+ **这次改了客户端样式表与布局状态机(`lib/client.js`)、配置 schema(`lib/index.js`)**,所以**升级后要从托盘彻底退出 DSH 再重开一次**;刷新 / 热重载不算。This release changes the injected stylesheet and the layout state machine; quit DSH from the tray and reopen once after upgrading.
14
+
15
+ > ⚠️ **破坏性变更 / Breaking**:`mode: a`(折叠)已移除,`getLayout().mode` 现在只返回 `b` / `c` / `n`,`Config.mode` 的合法值集合也变了,所以版本号跳到 `0.5.0`。旧配置里的 `mode: a` **不会报错、也不会变成另一个布局**:客户端每次读取都把它映射到新的「官方默认」`n`;`lib/index.js` 的 schema 仍保留 `a` 作为只读别名,只为让老 profile 通过校验。The removed `a` value is normalized to `n` on read.
16
+
17
+ ### 布局精简 / Fewer, clearer layouts
18
+
19
+ - **砍掉第一种布局 `a` 折叠** — 它用两个独立开关控制两组,能产生 4 种状态:两组都收起 = 空白侧栏(死状态)、两组都展开 = 与 DSH 原生一模一样(「点了等于没点」),剩下「只留一组」两态又已被 `b` 标签页完全覆盖。它没有任何「有用且独有」的状态,却额外带来一套与互斥胶囊语义冲突的双开关、以及窄侧栏里只能靠单/双箭头区分的两个图标。前两轮反馈的「按钮跑到列表下方」「展开合起来回跳」也都出在这套折叠按钮上。Removed: its four toggle states held two dead states and two states already covered by layout `b`.
20
+ - **新增第三种布局 `n` 官方默认(放在胶囊最后)** — 两组都常驻,插件对布局**零干预**:既不写 CSS 隐藏规则,运行时也不隐藏任何一组。想看回 DSH 原本的侧栏,一个按钮就能退回去。Added as the last pill option: both groups stay exactly as the host drew them, with no CSS or runtime override.
21
+ - **官方默认下工具条不会消失** — 该布局没有任何动作按钮(竖线与右侧按钮自动隐藏),但**布局胶囊始终保留**。这对 `showQuickSwitch: false` 的用户尤其重要:循环按钮本来就不显示,若连胶囊一起藏掉就只剩「去详情页改配置」一条退路,等于「进去了出不来」。The pill stays visible so there is always a way back.
22
+ - **三种布局的新顺序**:`b` 标签页 → `c` 浮层 → `n` 官方默认。出厂默认仍是 `b`(插件本就是为了消除抢高度),`n` 作为随时可退回的兜底。
23
+ - **控件与字典一并清理** — 删掉 `fold-ext` / `fold-ws` 两个按钮、`fold` / `fold2` 两个图标、`data-dsbs-fold-*` 两个 html 标志、4 组中英字典键,以及 `foldExt` / `foldWs` 两个运行时状态;详情页示意图改成「两组并排常驻」。teardown 仍会清理旧版本可能残留的那两个 html 属性。
24
+ - **对三方插件的影响** — `getLayout()` 的 `mode` 取值集合变化(见上)。消费方本来就不该写 `if (layout.mode === 'a')`;`registerTab()` 拿到的容器与懒挂载行为不变,官方默认下插件页被激活时仍按既定规则接管整列(那是消费方的显式动作,不是布局覆盖)。
25
+ - **验证** — 新增 `tests/test_036.mjs`:`MODES` 顺序、无死状态、官方默认下两组都不隐藏、胶囊始终可见、动作按钮与竖线隐藏、`mode: a` → `n` 的迁移,以及「点胶囊能切回来」这条防困死断言。受影响的历史测试(`test_025` / `029` / `031` / `033`)同步更新,`npm test` 扩为 14 套。
26
+
27
+ ### 顺带修好的控制栏位置(原计划的 0.4.2)
28
+
29
+ - **问题**:布局工具条(三个布局图标 + 收起箭头)出现在**整个插件列表的下方**;点右侧「收起扩展 / 收起工作区」箭头后位置还会变,展开与合起时上下跳,交互很别扭。The layout toolbar sat below the whole panel list and jumped around when a group was folded.
30
+ - **更正上一次的判断**:本条下面曾经写过「宿主折叠时把面板重挂到更深一层,`root` 变了所以控制栏被搬走」——**这个解释是错的**。解包 DSH `app.asar` 里的 `SidebarRoot.tsx` / `SidebarRoot.module.css` 后确认,宿主 JSX 结构固定,折叠分组并不会重挂 `scrollArea` 的两块面板。The earlier "re-parenting" explanation was wrong; the host JSX is stable.
31
+ - **真正的根因**:rc.2 的 `div.scrollArea` 是 CSS Grid,**四行** `grid-template-rows: 8px max-content 2px max-content`,宿主把 `nav.panelList` 显式钉在 `grid-row: 2`、`div.regionArea` 钉在 `grid-row: 4`。插件注入的节点没有指定行,只能被自动排到**隐式的新行(第 5 行及以后)**,也就是永远排在两个宿主面板之后。`0.4.1` 只补了 `grid-column:1/-1`(修好了 4px 挤压),没解决行;上一次尝试再加 `order:-30/-20/-10` 也无效 —— **`order` 无法覆盖宿主显式指定的 `grid-row`**。The host pins its panels to explicit grid rows, so unpositioned children are auto-placed after them; `order` cannot override an explicit row.
32
+ - **修法**:把控制栏与页签条**移出 grid**,放进它的父级 `div.scrollRegion`(始终是 flex 列,React 不会重建这一层)。在那里 DOM 顺序就是视觉顺序,展开(grid)与收起成图标条(宿主把 `scrollArea` 切成 flex 列)两种状态都一样。`resolveSidebar()` 新增 `scrollArea` / `headerHost` 两个解析结果,仍只匹配语义后缀、不硬编码 CSS-module 哈希。第三方 tab 的页面仍是 grid item,保留 `grid-column:1/-1` 与 4px/6px 内边距;浮层(布局 c)的定位基准同步从 `scrollArea` 改为 `scrollRegion`,否则会盖住工具条。The bar and tab strip now live in the flex header host; only a plugin page stays in the grid.
33
+ - **验证(这次是真机,不是猜)**:用独立 `--user-data-dir` 起了第二个 DSH 实例开了调试端口,**没有关闭或干扰用户正在用的窗口**。CDP 实测:`gridDisplay=grid`、`gridTemplateRows=8px 448px 2px 0px`、`navGridRow=2`、`regionGridRow=4`;修复后 `barParent=wtg9ga_scrollRegion`、`barIsGridItem=false`、工具条底边 172px 与 grid 顶边 172px 严丝合缝、`scrollRegion` 子节点顺序 `[BAR, TABS, scrollArea, scrollFade]`、`scrollArea` 里只剩宿主自己的两个面板并**未被污染**。截图确认工具条在「新会话」下方、插件列表上方,独占一行且无重叠。再依次点 `fold` / `fold2`、两组都隐藏、恢复,共 5 个状态采样,工具条父容器与 top/bottom **完全不变**。
34
+ - **回归测试**:`tests/dom-stub.mjs` 新增 `gridHost` 选项复刻真实的两层树;`tests/test_035.mjs` 重写为 23 项,专测「工具条不是 grid item、永远在滚动视口之前」;`tests/test_034.mjs` 的样式断言同步改为「只有第三方 tab 页面仍带 grid 规则」。**把 `lib/client.js` 临时回退到已发布的 0.4.1 跑同一套测试,12 项断言失败**(其中工具条被塞进 `scrollArea`、位置在列表之后),换回修复版全过。`npm test` 13 套全绿、`npm run check` 通过。
35
+
36
+ ---
37
+
38
+ ## 0.4.1 — 适配 DSH 0.2.2-rc.2 的网格侧栏 / Fit the rc.2 grid sidebar(2026-11-12)
39
+
40
+ **这次改了客户端样式表(`lib/client.js`)**,所以**升级后要托盘退出 DSH 再重开一次**;刷新 / 热重载不算。This release changes the injected stylesheet; quit DSH from the tray and reopen once after upgrading.
41
+
42
+ > ⚠️ **npm 上的 `0.4.0` 不含这个修复。** 如果你在 DSH `0.2.2-rc.2` 上看到布局按钮压住「工作区」标题,请升级到 `0.4.1`。The published 0.4.0 does not contain this fix; upgrade to 0.4.1 if the layout buttons overlap the "Workspaces" heading on DSH 0.2.2-rc.2.
43
+
44
+ - **修正下面 0.4.0 章节里「rc.2 运行时零改动」的结论**:DOM 契约(类名、`data-slot`、节点顺序)确实没变,但**布局容器变了** —— rc.2 把 `div.wtg9ga_scrollArea` 从纵向 flex 改成了 CSS Grid(真机实测列宽 `4px 264px 6px`),并把宿主的面板显式钉在第 2 列。当时只验了节点是否注入,没验几何,所以漏掉了这一层。The class names were unchanged, but the host switched its sidebar root from a flex column to a grid.
45
+ - **修复控制栏压住「工作区」标题** — 插件注入的控制栏 / 页签条 / 插件面板用默认 `grid-column:auto` 落进只有 4px 宽的第 1 列,按钮溢出后盖到宿主「工作区」标题上,右侧还留下一条孤零零的竖分隔线。现在三者统一 `grid-column:1/-1` 独占整行,并把 grid 的 4px / 6px 装饰列还原成左右内边距,与宿主「新会话」按钮对齐。`grid-column` 在旧版 flex 侧栏里是惰性属性,老宿主不受影响。Spanning every column gives each injected header its own full-width row; the property is inert on older flex roots.
46
+ - **验证** — CDP 真机 14 项验收全通过:控制栏从 4px 恢复到 274px、`scrollWidth === clientWidth`、布局按钮与「工作区」标题的重叠面积为 0、注册第三方 tab 后页签条与面板都是全宽、dispose 后无残留、root 子节点顺序不变。`tests/test_034.mjs` 扩为 20 项,新增源码与运行时样式表断言,防止以后有人删掉这条规则。全套测试与 `npm run check` 全绿。
47
+
48
+ ---
49
+
11
50
  ## 0.4.0 — 临时会话可以「转正」或「立即删除」/ Promote or delete a temp session(2026-11-11)
12
51
 
13
52
  **这次改了运行时代码**(`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
53
 
54
+ - **发布后兼容性复核(2026-11-12,运行时零改动)** — DSH 升级到 `0.2.2-rc.2` 后,用 CDP 连真机复核:`sidebarSuite` 服务正常发布(`0.4.0`)、`[data-slot="sidebar.workspaces"]` 与 `nav.wtg9ga_panelList` 仍能解析出 `div.wtg9ga_scrollArea`、控制栏 / 页签条 /「临时」按钮全部注入、第三方 tab 可注册并懒挂载、订阅通知正常、`/api/dsh-temp-session.status` 返回 200 且 9 项配置齐全、root 子节点顺序多次采样不变。**新版 DSH 没有改这些半稳定约定,因此不需要改客户端代码。** 新增 `tests/test_034.mjs`(15 项)把宿主 DOM 契约固化下来,`tests/dom-stub.mjs` 的假侧栏改用 rc.2 实测类名,`npm test` 扩为 12 套。Post-release verification on DSH 0.2.2-rc.2: the host markup contract is unchanged, no runtime fix was needed, and a new regression suite now pins it.
15
55
  - **⛔ 修复启动死循环(严重,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
56
  - **测试** — 新增 `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
57
  - **右键临时会话行,弹出「保留为正式会话 / 立即删除」** — 之前临时会话是「要么全删,要么全留」:想留下某一个,只能去把 `DELETE_ON_EXIT` 关掉,结果**所有**临时会话都变成永久。现在单个会话可以就地处理。Right-clicking a temporary row now offers both options, so one session no longer forces a global setting.
package/DETAIL.md CHANGED
@@ -1,18 +1,20 @@
1
1
  # 侧栏布局大师 · Sidebar Suite
2
2
 
3
- > **中文**:**这是一款专门针对改进 DSH 左侧栏体验的插件。** 左侧栏里「扩展」和「工作区」永远在抢同一块高度;装了几个插件之后,找一个入口要滚半天;项目一多,扩展就被挤没。它只解决这一处,但要把它做到**既强实用、又好看**:三种布局一键切换、**默认布局就在本页最上方**、会话列表默认全部展开、临时会话默认在退出 DSH 时删除(每天定时清理默认关,可在本页开启);界面全程沿用宿主原生配色、圆角与间距,不另起一套皮肤。
3
+ > **中文**:**这是一款专门针对改进 DSH 左侧栏体验的插件。** 左侧栏里「扩展」和「工作区」永远在抢同一块高度;装了几个插件之后,找一个入口要滚半天;项目一多,扩展就被挤没。它只解决这一处,但要把它做到**既强实用、又好看**:标签页 / 浮层 / 官方默认三种布局一键切换、**默认布局就在本页最上方**、会话列表默认全部展开、临时会话默认在退出 DSH 时删除(每天定时清理默认关,可在本页开启);界面全程沿用宿主原生配色、圆角与间距,不另起一套皮肤。
4
4
  >
5
5
  > **English**: **This is a plugin dedicated to improving the DSH sidebar experience.** The panels group and the workspaces group fight for the same vertical space; once you install a few plugins, finding anything means scrolling; add more projects and the panels vanish entirely. It fixes only that one surface, but fixes it properly — **seriously practical and native-looking**: three layouts in one click, the **startup-default picker at the very top of this page**, an always-expanded session list, temp sessions deleted when DSH quits by default (daily cleanup off unless enabled), and a UI built entirely from the host's own colours, radii and spacing instead of a custom skin.
6
6
 
7
7
  ---
8
8
 
9
- ## 三种布局 / Three layouts
9
+ ## 布局 / Layouts
10
10
 
11
11
  | 模式 / Mode | 中文 | English |
12
12
  | --- | --- | --- |
13
- | `a` 折叠 / Fold | 两组各自收起,想留哪块留哪块。 | Each group folds independently — keep exactly what you need. |
14
13
  | `b` 标签页 / Toggle (**默认 / default**) | 两组共用一栏,用顶部按钮互斥切换,侧栏再长也不打架。 | Both groups share one space; the bar swaps between them. The sidebar stays short. |
15
14
  | `c` 浮层 / Overlay | 工作区常驻,扩展以浮层盖在上面,随时展开/收起。 | Workspaces stay put; panels open as a floating layer you can show or hide. |
15
+ | `n` 官方默认 / Default | 两组都常驻,插件既不写隐藏样式也不在运行时隐藏任何一组。 | Both groups stay exactly as DSH drew them; no layout override at all. |
16
+
17
+ > **0.5.0 砍掉了旧的 `a` 折叠布局。** 它用两个独立开关控制两组,4 种组合里有 2 种是无效状态:两组都收起 = 空白侧栏,两组都展开 = 与官方默认完全相同;剩下「只留一组」两态又已被 `b` 标签页覆盖。控件语义也与「互斥胶囊」冲突,窄侧栏里两个箭头只能靠单/双折线区分。旧配置里的 `mode: a` 现在自动按官方默认打开。Removed because it had no useful, unique state and added two dead states.
16
18
 
17
19
  每张布局卡片都画了示意图,选中的那张标着「当前默认 / Current」。点一下就通过官方配置层写入,**立刻生效、下次启动照旧**。
18
20
  Each card draws a small diagram of its layout, and the selected one is marked as the current default. A click writes through the official config layer — **effective immediately and on every future launch**.
@@ -76,8 +78,8 @@ The report always carries: plugin version (read from `package.json`, so it canno
76
78
 
77
79
  ## 控制栏在哪 / Where the controls are
78
80
 
79
- 侧栏顶部是一条分段控件:`[折叠 | 标签页 | 浮层] │ 动作按钮`。前三个互斥,装在同一枚胶囊里,选中的那个白底高亮;竖线右边是当前布局专用的动作按钮(展开/折叠扩展、显示工作区、换标签页、浮出面板)。
80
- The toolbar is one segmented control: `[fold | tabs | overlay] │ actions`. The first three are mutually exclusive inside a single pill with the active one highlighted; right of the divider sit the buttons that act on the chosen layout.
81
+ 侧栏顶部是一条分段控件:`[标签页 | 浮层 | 官方默认] │ 动作按钮`。三个互斥,装在同一枚胶囊里,选中的那个白底高亮;竖线右边是当前布局专用的动作按钮(换标签页、浮出面板)。**官方默认没有任何动作按钮**(竖线一并隐藏),但胶囊保留 —— 这是 `showQuickSwitch: false` 时唯一的退出口。
82
+ The toolbar is one segmented control: `[toggle | overlay | default] │ actions`. The three are mutually exclusive inside a single pill with the active one highlighted; right of the divider sit the buttons that act on the chosen layout. Default has no actions, but the pill always stays visible so the user can switch back.
81
83
 
82
84
  `换布局 / Cycle` 默认不出现(`showQuickSwitch`),打开后和其它动作按钮共用同一套图标。`Cycle` is off by default and shares the icon set when enabled.
83
85
 
@@ -115,7 +117,7 @@ export function apply(ctx) {
115
117
  三种布局的差异 / Under the three layouts:
116
118
 
117
119
  - B 标签页:`[扩展][工作区][你的页…]` 并排,都是页。
118
- - A 折叠、C 浮层:**只有第三方页**进条;内置两组仍由原按钮控制,因为这两种布局不是页签模型。
120
+ - C 浮层、N 官方默认:**只有第三方页**进条;内置两组仍由宿主自己显示,因为这两种布局不是页签模型。官方默认下插件页被激活时仍会接管整列(这是消费方的显式动作,不是布局覆盖)。
119
121
  - 没有任何第三方注册:页签条本身带 `hidden`,不占高度、不参与布局,看到的和 0.2.x 一模一样(只是多一个隐藏的空容器)。With no consumer the strip carries `hidden`: zero height, no layout impact — visually identical to 0.2.x.
120
122
 
121
123
  选中态存 `localStorage["dsh-sidebar-suite:active-tab"]`,不写插件配置;再点当前页签退回原来的组。
@@ -126,8 +128,8 @@ export function apply(ctx) {
126
128
  dsh plugin --profile desktop add dsh-sidebar-suite
127
129
  ```
128
130
 
129
- 本地开发可把包名换成本目录路径或 `.tgz` 安装包。已在 **DSH 0.2.2-rc.1(desktop profile)** 验证。
130
- For local development, point the same command at this folder or a packed `.tgz`. Tested on **DSH 0.2.2-rc.1 (desktop profile)**.
131
+ 本地开发可把包名换成本目录路径或 `.tgz` 安装包。已在 **DSH 0.2.2-rc.2(desktop profile)** 真机验证;`0.2.2-rc.1` 同样兼容但已过时。
132
+ For local development, point the same command at this folder or a packed `.tgz`. Tested on **DSH 0.2.2-rc.2 (desktop profile)**; 0.2.2-rc.1 still works but is superseded.
131
133
 
132
134
  **升级插件包后仍需重启 / Upgrading the plugin package still needs a restart** — 安装新版或改 `lib/` 下的代码后,请完全退出并重新打开 DSH(客户端代码不热重载)。**但改配置不用重启**:0.2.8 起 `temp-session-config.txt` 的改动、以及详情页的任何开关,都在约 3 秒内原地生效。 / Config edits are the exception: from 0.2.8 they hot-reload in place within seconds.
133
135
 
@@ -136,9 +138,10 @@ For local development, point the same command at this folder or a packed `.tgz`.
136
138
 
137
139
  ## 已知限制 / Known limits
138
140
 
139
- - `[data-slot="sidebar.workspaces"]` 与 `panelList` 是宿主的半稳定约定;若宿主改版,插件会退化成「有控制栏但无效果」而不是抛错。These host markup contracts are semi-stable; a host change degrades gracefully instead of throwing.
141
+ - `[data-slot="sidebar.workspaces"]` 与 `panelList` 是宿主的半稳定约定;若宿主改版,插件会退化成「有控制栏但无效果」而不是抛错。**0.2.2-rc.2 实测**:根节点 `wtg9ga_scrollArea`、扩展面板 `nav.wtg9ga_panelList`、工作区容器 `wtg9ga_regionArea` —— CSS-module 哈希前缀 `wtg9ga_` 会在 DSH 重新构建时变,插件只匹配 `panelList` 这个语义后缀与上面那个 `data-slot`,从不硬编码完整类名;`tests/test_034.mjs` 会验证换哈希、纯哈希类名、以及缺少 workspaces 插槽时的行为。These host markup contracts are semi-stable; a host change degrades gracefully instead of throwing. On 0.2.2-rc.2 the real classes are `wtg9ga_scrollArea` / `nav.wtg9ga_panelList` / `wtg9ga_regionArea`; only the semantic suffix and the slot are matched, never the generated prefix.
142
+ - **侧栏根节点可能是 flex,也可能是 grid(0.4.1 / 0.5.0)** — 0.2.2-rc.2 把 `wtg9ga_scrollArea` 从纵向 flex 改成三列 CSS Grid(`4px 264px 6px`),而且是**四行** `8px / max-content / 2px / max-content`:宿主把 `nav.panelList` 钉在 `grid-row:2`、`wtg9ga_regionArea` 钉在 `grid-row:4`。0.4.1 让注入节点 `grid-column:1/-1` 修好了 4px 挤压与标题重叠;但没有指定行的节点仍被自动排进隐式行、落在两个宿主面板**之后**,`order` 也压不过显式的 `grid-row`。所以控制栏与页签条改为放进 grid 的父级 `wtg9ga_scrollRegion`(稳定的 flex 列,React 不重建),DOM 顺序即视觉顺序;只有第三方 tab 页面仍留在 grid 里,保留跨列规则与 4px / 6px 内边距。**不改宿主节点、不依赖生成类名、不做版本嗅探**;旧 flex 根上这套定位同样成立。The host root may be flex (rc.1) or a grid with explicitly pinned rows (rc.2); the header lives in the flex wrapper above that grid, and only a plugin page still spans its columns.
140
143
  - 浮层定位依赖 `:has()`,需要 Chromium 内核(DSH 桌面端即是)。Overlay positioning uses `:has()`, so it needs a Chromium-based client.
141
- - 标签页模式会整块隐藏工作区区域,里面的其它入口也一并隐藏 —— 这是「一组一栏」的刻意设计。Toggle mode hides the whole workspaces region, including anything else the host renders inside it.
144
+ - 标签页模式会整块隐藏工作区区域,里面的其它入口也一并隐藏 —— 这是「一组一栏」的刻意设计;官方默认则两组都不隐藏。Toggle mode hides the whole workspaces region; the official default hides neither group.
142
145
 
143
146
  ## 临时会话 / Temporary session(0.2.0)
144
147
 
@@ -147,7 +150,7 @@ For local development, point the same command at this folder or a packed `.tgz`.
147
150
 
148
151
  「临时」按钮创建的**是一个真正的 DSH 会话**,因此:在左侧栏可见、用原生会话窗口、继承当前模型和工具链、可以随意切换任务;只有「退出即删除」这一点是插件加的。
149
152
 
150
- 实现路径(全部在 `app.asar` 0.2.2-rc.1 上核实):
153
+ 实现路径(在 `app.asar` 0.2.2-rc.1 上核实,2026-11-12 在 0.2.2-rc.2 真机上复核,调用链无变化):
151
154
 
152
155
  - **创建**:Client 半调用 `ctx.get('uiWorkspace').createNamed({ workspaceId })` 再 `openSession(id)` —— 与宿主「新会话」按钮同一条链路(`dsh-client-ui-workspace` 的 `createInWorkspace` → `sessions.create` → `session/create`)。不传 `sessionId`,所以不会复用你已有的空白会话。
153
156
  - **标记**:把会话 id POST 给 Host 半,写进 `~/.dsh/dsh-sidebar-suite/temp-sessions.json`;侧栏行按 `data-row-key="session:<id>"` 打 `data-dsbs-temp-row` 属性,用 CSS `::after` 画「临时」标签(只加属性、不加节点,React 重渲染不会弄丢)。
package/README.md CHANGED
@@ -5,16 +5,16 @@
5
5
  > 两个数字不是一回事:左边是 **npm 近 30 天安装量**;右边是 **GitHub Release 附件的下载总数** —— GitHub 只统计这个,`git clone` 和 npm 安装都不计入。附件是 2026-11-10 才补齐的,所以右边从 0 开始,每有一次真实下载才 +1。
6
6
  > The left badge counts npm installs (last 30 days); the right one counts downloads of **attached release files only** — clones and npm installs are not included. Assets were first attached on 2026-11-10, so that number starts at zero.
7
7
 
8
- > 让左侧栏从「抢地盘」变成「听你的」。三种布局、一键切换、零改动宿主结构,默认布局在插件详情页顶部直接改。
8
+ > 让左侧栏从「抢地盘」变成「听你的」。标签页 / 浮层 / 官方默认三种布局、一键切换、零改动宿主结构,默认布局在插件详情页顶部直接改。
9
9
  > Tame the DSH sidebar: three layouts, one click, zero DOM rewrites — set your startup default right at the top of the plugin page.
10
10
 
11
- **中文**:DSH 左侧栏把「扩展面板」和「工作区」堆在同一列,互相抢高度。本项目提供三种布局解决这个问题,并带来一个中英双语、带图形化设置的详情页。
11
+ **中文**:DSH 左侧栏把「扩展面板」和「工作区」堆在同一列,互相抢高度。本项目提供两种改写方案(标签页 / 浮层)与一个随时可退回的「官方默认」,并带来一个中英双语、带图形化设置的详情页。
12
12
  **English**: The panels group and the workspaces group compete for the same vertical space. This bundle offers three layouts for them and ships a bilingual detail page with a graphical settings picker.
13
13
 
14
14
  详情页长文档见 [DETAIL.md](DETAIL.md)(中英双语 / bilingual, 含收款码 / includes the payment QR)。
15
15
  The full detail document is [DETAIL.md](DETAIL.md).
16
16
 
17
- **一步步照着用**:[使用说明.md](使用说明.md) —— 安装、三种布局、临时会话、配置文件逐行说明、常见故障与反馈,中英双语随包附带(0.2.5 起)。
17
+ **一步步照着用**:[使用说明.md](使用说明.md) —— 安装、布局说明、临时会话、配置文件逐行说明、常见故障与反馈,中英双语随包附带(0.2.5 起)。
18
18
  **Step-by-step guide**: [使用说明.md](使用说明.md) — install, layouts, temp sessions, every config line, troubleshooting and feedback; shipped inside the npm package since 0.2.5.
19
19
 
20
20
  ## 这是一款什么插件 / What this plugin is
@@ -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`;**每天定时清理默认关**,需要时在详情页开启并设时间)、**临时会话可右键「保留为正式会话」或「立即删除」**(0.4.0)、详情页三种反馈入口 |
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) 的「这是一款什么插件」一节。
@@ -57,13 +57,15 @@ The point-by-point list of both design goals is in the "What this plugin is" sec
57
57
 
58
58
  > Report a bug, request a feature, or just say something feels off. Use **Issues** for bugs, **Discussions** for questions, **marketplace comments** for the channel the plugin market actually defines, or the **Feedback** box on the plugin detail page, which packs your version, OS, layout and last error into copyable text.
59
59
 
60
- ## 三种布局 / Three layouts
60
+ ## 布局 / Layouts
61
61
 
62
62
  | 模式 / Mode | 中文 | English |
63
63
  | --- | --- | --- |
64
- | `a` 折叠 / Fold | 两组各自收起。 | Each group folds on its own. |
65
64
  | `b` 标签页 / Toggle(默认 / default) | 两组共用一栏,标签互斥切换。 | The groups share one space; the bar switches between them. |
66
65
  | `c` 浮层 / Overlay | 工作区常驻,扩展以浮层展开。 | Workspaces stay put; panels open as an overlay. |
66
+ | `n` 官方默认 / Default | 两组都常驻,插件对布局零干预。 | Both groups stay visible — the official sidebar, with no override at all. |
67
+
68
+ > **0.5.0 移除了旧的 `a` 折叠布局**:两个独立开关能造出「空白侧栏」和「与 DSH 原生一模一样」两种无效状态,其余两态又已被 `b` 完全覆盖。旧配置里的 `mode: a` 会自动按 `n` 打开,无需手动改;`getLayout().mode` 现在只返回 `b` / `c` / `n`。The folded layout was removed in 0.5.0; old `a` configs now open in Default.
67
69
 
68
70
  ## 默认模式 / Default mode
69
71
 
@@ -98,8 +100,8 @@ Scan with WeChat Pay — **purely voluntary, no rewards promised**. The author a
98
100
 
99
101
  ## 控制栏 / Where the controls are
100
102
 
101
- 侧栏顶部是一条**分段控件**:三个互斥布局(折叠 / 标签页 / 浮层)装在同一枚灰色胶囊里,选中的那个是白底高亮;右侧隔一道竖线是当前布局专用的动作按钮(展开/折叠扩展、显示工作区、换标签页、浮出面板)。0.2.9 起全部换成矢量图标,窄侧栏只留图标(悬停有文字提示),侧栏宽于 300px 时动作按钮把文字长回来。
102
- The toolbar is one segmented control: the three mutually exclusive layouts share a single pill with the active one highlighted, and the buttons that act on the chosen layout sit on the far side of a divider. Since 0.2.9 the icons are real vectors — icon-only in a narrow column (with tooltips), and the action buttons grow their labels back once the column passes 300 px.
103
+ 侧栏顶部是一条**分段控件**:三个互斥布局(标签页 / 浮层 / 官方默认)装在同一枚灰色胶囊里,选中的那个是白底高亮;右侧隔一道竖线是当前布局专用的动作按钮(换标签页、浮出面板)。0.2.9 起全部换成矢量图标,窄侧栏只留图标(悬停有文字提示),侧栏宽于 300px 时动作按钮把文字长回来。**官方默认下没有动作按钮,但胶囊始终保留** —— 否则把 `showQuickSwitch` 关掉的用户就切不回来了。
104
+ The toolbar is one segmented control: the three mutually exclusive layouts share a single pill with the active one highlighted, and the buttons that act on the chosen layout sit on the far side of a divider. Since 0.2.9 the icons are real vectors — icon-only in a narrow column (with tooltips), and the action buttons grow their labels back once the column passes 300 px. The official default has no action buttons, but the pill always stays visible so there is a way back.
103
105
 
104
106
  `换布局 / Cycle` 默认不出现,可用 `showQuickSwitch: true` 打开;它和其余动作按钮一起走同一套图标。
105
107
  `Cycle` stays hidden unless `showQuickSwitch: true`; it uses the same icon set as the other action buttons.
@@ -139,10 +141,10 @@ export function apply(ctx) {
139
141
  | 布局 | 表现 |
140
142
  |---|---|
141
143
  | B 标签页 | 和控制栏下面那排「扩展 / 工作区」并排 |
142
- | A 折叠、C 浮层 | 只有插件页签进条;内置两组仍由原按钮控制(这两种布局本来就不是"页签"模型) |
144
+ | C 浮层、N 官方默认 | 只有插件页签进条;内置两组仍由宿主自己显示(这两种布局不是"页签"模型) |
143
145
  | 没人注册 | 这排页签**完全不出现**,界面和 0.2.x 一模一样 |
144
146
 
145
- 选中哪一页记在 `localStorage["dsh-sidebar-suite:active-tab"]`;**再点一次当前页签就退回原来的组**(布局 A / C 下这是唯一的退出口)。
147
+ 选中哪一页记在 `localStorage["dsh-sidebar-suite:active-tab"]`;**再点一次当前页签就退回原来的组**(布局 C / 官方默认下这是唯一的退出口)。
146
148
 
147
149
  服务其余方法:`unregisterTab(id)`、`getTabs()`、`activateTab(id)`、`getActiveTab()`、`subscribe(fn)`(`fn(tabs, reason, activeId)`)、`getLayout()`。
148
150
 
@@ -173,7 +175,8 @@ For local development, point the same command at this folder or a packed `.tgz`
173
175
  **离线安装包 / Offline archives**:[GitHub Releases](https://github.com/YONAN1269/dsh-sidebar-suite/releases) 的每个版本都附一个 `.zip` 源码包和一个 `.tgz` npm 安装包。把 `.tgz` 下到本机后执行 `dsh plugin --profile desktop add <本地 .tgz 的完整路径>` 就能离线安装;Release 页面会显示每个附件真实的下载次数。
174
176
  Every release ships a `.zip` source archive and an npm `.tgz`; point the install command at the downloaded `.tgz` for an offline install. GitHub counts downloads of these attached files.
175
177
 
176
- **已在 DSH 0.2.2-rc.1(desktop profile)验证 / Tested on DSH 0.2.2-rc.1 (desktop profile).**
178
+ **已在 DSH 0.2.2-rc.2(desktop profile)真机验证 / Tested on DSH 0.2.2-rc.2 (desktop profile).**
179
+ 2026-11-12 用 CDP 连上真机复核:服务发布、`[data-slot="sidebar.workspaces"]` 与 `nav.wtg9ga_panelList` 解析、控制栏 / 页签条 /「临时」按钮注入、第三方 tab 注册与懒挂载、Host 配置路由返回 200、root 子节点顺序稳定 —— **DOM 契约未变**。但 rc.2 把侧栏根节点从纵向 flex 换成了 CSS Grid(三列 `4px 264px 6px`、四行,且宿主面板被钉在 `grid-row:2` 与 `grid-row:4`),只验节点是否注入、不验几何就会漏掉布局问题:**0.4.1** 让注入节点 `grid-column:1/-1` 独占整行(修好按钮压住「工作区」标题);**未发布的 0.4.2** 进一步把控制栏 / 页签条移出 grid、放进父级 `scrollRegion` flex 列,否则它们会被自动排到隐式行、显示在整列列表下方。`0.2.2-rc.1` 也兼容(旧 flex 根上这套定位同样成立),但已是旧版本。Verified against a live 0.2.2-rc.2 renderer: the markup contract is unchanged, but the host pins its panels to explicit grid rows, so injected header boxes must live in the flex wrapper above the grid.
177
180
  本包刻意不声明 `engines.dsh`:当前 DSH 是预发布版,semver 默认规则会把常规版本范围误判为不兼容。
178
181
  This bundle deliberately omits `engines.dsh`: DSH is currently a prerelease, and semver's default rules would reject any ordinary range.
179
182
 
@@ -187,7 +190,7 @@ The client half is not hot-reloaded — fully quit and reopen DSH after installi
187
190
 
188
191
  - `[data-slot="sidebar.workspaces"]` 与 `panelList` 是半稳定宿主约定,变动时插件降级而不抛错。These host contracts are semi-stable; changes degrade gracefully.
189
192
  - 浮层定位依赖 `:has()`,需要 Chromium 内核。Overlay positioning needs `:has()` (Chromium-based client).
190
- - 标签页模式整块隐藏工作区区域,区域内其它入口也一并隐藏,这是刻意的「一组一栏」设计。Toggle hides the whole workspaces region, which is intentional.
193
+ - 标签页模式整块隐藏工作区区域,区域内其它入口也一并隐藏,这是刻意的「一组一栏」设计。官方默认则相反:两组都不隐藏。Toggle hides the whole workspaces region, which is intentional; the official default hides neither group.
191
194
 
192
195
  ## 会话列表默认全部展开 / Always-expanded session list(0.2.4)
193
196
 
package/lib/client.js CHANGED
@@ -5,9 +5,14 @@
5
5
  // (`[data-slot="sidebar.workspaces"]` inside its own region). They compete for
6
6
  // the same vertical space, so this plugin offers three layouts for them:
7
7
  //
8
- // a both groups fold independently
9
8
  // b the groups share one space and the control bar switches between them
10
9
  // c the workspaces stay put and the panels open as an overlay
10
+ // n official default: both groups stay visible, no override at all
11
+ //
12
+ // 0.5.0 removed the old `a` (independent folding). Its four toggle states
13
+ // included an empty sidebar and a state identical to the official default,
14
+ // while the two useful states were already covered by `b`. Profiles that still
15
+ // hold `mode: a` are normalized to `n` on read, never written back.
11
16
  //
12
17
  // All layout state lives in attributes on <html>, and the host's markup is
13
18
  // never restructured or moved: this plugin only ADDS one control bar as a
@@ -21,7 +26,7 @@ window.__ModuleLoader__.load({
21
26
  var ENTRY = 'dsh-sidebar-suite'
22
27
  // Reported to tab consumers so they can feature-detect. Kept in lockstep
23
28
  // with package.json by tests/test_030.mjs.
24
- var VERSION = '0.4.0'
29
+ var VERSION = '0.5.0'
25
30
  var NS = 'sidebar-suite'
26
31
  var LS_KEY = 'dsh-sidebar-suite:state'
27
32
  // Where the third-party tab choice lives. Kept out of LS_KEY on purpose:
@@ -61,27 +66,24 @@ window.__ModuleLoader__.load({
61
66
  // same <html> attributes, so the first live run owns the sidebar.
62
67
  var OWNER_KEY = '__dshSidebarSuiteOwner'
63
68
 
64
- var DEFAULTS = { mode: 'b', showQuickSwitch: true, tab: 'ext', foldExt: false, foldWs: false }
65
- var MODES = ['a', 'b', 'c']
69
+ var DEFAULTS = { mode: 'b', showQuickSwitch: true, tab: 'ext' }
70
+ // 0.5.0: the folded layout `a` is gone. Its id survives only as a read
71
+ // alias so old profiles migrate to a visually equivalent layout.
72
+ var MODES = ['b', 'c', 'n']
73
+ var LEGACY_MODES = { a: 'n' }
66
74
 
67
75
  var DICTS = {
68
76
  en: {
69
77
  'bar.label': 'Sidebar layout',
70
78
  'bar.local': 'Settings are stored on this device only (the official config layer is unavailable).',
71
- 'mode.a': 'Fold',
72
- 'mode.a.hint': 'Each group folds on its own.',
73
79
  'mode.b': 'Toggle',
74
80
  'mode.b.hint': 'The two groups share one space; choose which one is shown.',
75
81
  'mode.c': 'Overlay',
76
82
  'mode.c.hint': 'Workspaces stay visible; panels open as an overlay.',
83
+ 'mode.n': 'Default',
84
+ 'mode.n.hint': 'Both groups stay visible — the official sidebar, with no layout override.',
77
85
  'quick.label': 'Cycle',
78
86
  'quick.hint': 'Cycle the sidebar layout.',
79
- 'fold.ext.collapse': 'Hide panels',
80
- 'fold.ext.expand': 'Show panels',
81
- 'fold.ext.hint': 'Hide or show the panels group.',
82
- 'fold.ws.collapse': 'Hide workspaces',
83
- 'fold.ws.expand': 'Show workspaces',
84
- 'fold.ws.hint': 'Hide or show the workspaces group.',
85
87
  'tab.to.ext': 'Show panels',
86
88
  'tab.to.ext.hint': 'Switch the shared space to the panels group.',
87
89
  'tab.to.ws': 'Show workspaces',
@@ -94,7 +96,7 @@ window.__ModuleLoader__.load({
94
96
  'pop.close': 'Close panels',
95
97
  'pop.hint': 'Open or close the panels overlay.',
96
98
  'settings.title': 'Default layout on startup',
97
- 'settings.help': 'Pick the layout DSH starts with. This writes the plugin config; switching from the sidebar bar is session-only.',
99
+ 'settings.help': 'Pick the layout DSH starts with. This writes the plugin config; switching from the sidebar bar is session-only. The Fold layout was removed in 0.5.0; profiles still set to a now open in Default.',
98
100
  'settings.unavailable': 'The config service is unavailable right now, so the default cannot be changed here.',
99
101
  'settings.current': 'Current default',
100
102
  'temp.label': 'Temp',
@@ -131,20 +133,14 @@ window.__ModuleLoader__.load({
131
133
  zh: {
132
134
  'bar.label': '侧栏布局',
133
135
  'bar.local': '设置仅保存在本机(官方配置层不可用)。',
134
- 'mode.a': '折叠',
135
- 'mode.a.hint': '两组各自收起。',
136
136
  'mode.b': '标签页',
137
137
  'mode.b.hint': '两组共用一栏,用标签页切换显示。',
138
138
  'mode.c': '浮层',
139
139
  'mode.c.hint': '工作区常驻,扩展以浮层展开。',
140
+ 'mode.n': '官方默认',
141
+ 'mode.n.hint': '两组都常驻,插件不改动侧栏布局。',
140
142
  'quick.label': '换布局',
141
143
  'quick.hint': '循环切换侧栏布局。',
142
- 'fold.ext.collapse': '收起扩展',
143
- 'fold.ext.expand': '展开扩展',
144
- 'fold.ext.hint': '收起或展开扩展区。',
145
- 'fold.ws.collapse': '收起工作区',
146
- 'fold.ws.expand': '展开工作区',
147
- 'fold.ws.hint': '收起或展开工作区。',
148
144
  'tab.to.ext': '显示扩展',
149
145
  'tab.to.ext.hint': '把共用的一栏切换到扩展区。',
150
146
  'tab.to.ws': '显示工作区',
@@ -157,7 +153,7 @@ window.__ModuleLoader__.load({
157
153
  'pop.close': '收起扩展',
158
154
  'pop.hint': '打开或收起扩展浮层。',
159
155
  'settings.title': '启动时的默认布局',
160
- 'settings.help': '选择 DSH 启动时使用的布局。这里会写入插件配置;在侧栏上临时切换不会改动它。',
156
+ 'settings.help': '选择 DSH 启动时使用的布局。这里会写入插件配置;在侧栏上临时切换不会改动它。0.5.0 起旧的「折叠」布局已移除,配置里仍写 a 的会自动切到「官方默认」。',
161
157
  'settings.unavailable': '配置服务暂时不可用,无法在这里修改默认布局。',
162
158
  'settings.current': '当前默认',
163
159
  'temp.label': '临时',
@@ -206,7 +202,7 @@ window.__ModuleLoader__.load({
206
202
  'policy.help': '决定临时会话什么时候被清掉。默认:退出 DSH 时删除=开,每天定时清理=关;两个开关互不影响,改完立即生效,不用重启。 / Choose when temporary sessions are removed. Defaults: delete on quit = on, daily cleanup = off; the two switches are independent and apply immediately — no restart.',
207
203
  'settings.intro': '一款专门针对改进 DSH 左侧栏体验的插件 —— 强实用性 + 美观性:沿用宿主原生观感,不另起一套皮肤。 / A plugin dedicated to the DSH sidebar experience — seriously practical and good-looking, built from native host design tokens.',
208
204
  'settings.title': '启动时的默认布局 · Default layout on startup',
209
- 'settings.help': '选择 DSH 启动时使用的布局,这里会写入插件配置;在侧栏上临时切换只影响本次会话。 / Pick the layout DSH starts with — this writes the plugin config, while switching from the sidebar bar stays session-only.',
205
+ 'settings.help': '选择 DSH 启动时使用的布局,这里会写入插件配置;在侧栏上临时切换只影响本次会话。0.5.0 起旧的「折叠」布局已移除,配置文件里仍写着 a 的会自动切到「官方默认」。 / Pick the layout DSH starts with — this writes the plugin config, while switching from the sidebar bar stays session-only. The Fold layout was removed in 0.5.0; profiles still set to a now open in Default.',
210
206
  'settings.unavailable': '配置服务暂时不可用,无法在这里修改默认布局。 / The config service is unavailable right now, so the default cannot be changed here.',
211
207
  'settings.current': '当前默认 / Current',
212
208
  'settings.support': '喜欢这个插件?请作者喝杯咖啡 · Enjoy it? Buy me a coffee',
@@ -265,12 +261,12 @@ window.__ModuleLoader__.load({
265
261
  '[data-dsbs-bar] button[data-dsbs-btn="quick"]{margin-left:auto;color:var(--dsw-alias-label-tertiary,inherit);padding:2px 6px}',
266
262
  'html[data-dsbs-narrow="true"] [data-dsbs-btn="quick"]{display:none}',
267
263
  'html[data-dsbs-rail="true"] [data-dsbs-bar]{display:none}',
268
- // a — each group folds on its own
269
- 'html[data-dsbs-mode="a"][data-dsbs-fold-ext="true"] nav[data-dsbs-ext]{display:none}',
270
- 'html[data-dsbs-mode="a"][data-dsbs-fold-ws="true"] div:has(> [data-slot="sidebar.workspaces"]){display:none}',
271
264
  // b — the two groups are mutually exclusive
272
265
  'html[data-dsbs-mode="b"][data-dsbs-tab="ext"] div:has(> [data-slot="sidebar.workspaces"]){display:none}',
273
266
  'html[data-dsbs-mode="b"][data-dsbs-tab="ws"] nav[data-dsbs-ext]{display:none}',
267
+ // n — official default: deliberately NO rule. The point of this layout
268
+ // is that neither group is hidden; do not add a display declaration for
269
+ // data-dsbs-mode="n" here or in the runtime state machine below.
274
270
  // c — panels become an overlay above the workspaces
275
271
  'html[data-dsbs-mode="c"]:not([data-dsbs-pop="true"]) nav[data-dsbs-ext]{display:none}',
276
272
  'html[data-dsbs-mode="c"][data-dsbs-pop="true"] nav[data-dsbs-ext]{display:flex;flex-direction:column;gap:2px;position:absolute;z-index:60;left:4px;right:4px;top:var(--dsbs-pop-top,64px);max-height:60vh;overflow:auto;padding:6px;border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base,#fff));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3));box-shadow:0 12px 32px rgba(0,0,0,.24)}',
@@ -373,16 +369,12 @@ window.__ModuleLoader__.load({
373
369
  '[data-dsbs-seg] button[aria-pressed="false"]{opacity:.70}',
374
370
  '[data-dsbs-seg] button[aria-pressed="false"]:hover{opacity:.9;background:color-mix(in srgb,currentColor 9%,transparent)}',
375
371
  '[data-dsbs-bar] button[data-dsbs-glyph]::before{content:"";position:absolute;inset:0;margin:auto;width:16px;height:16px;background:currentColor;opacity:.88;-webkit-mask-image:var(--dsbs-icon);-webkit-mask-size:16px 16px;-webkit-mask-position:center;-webkit-mask-repeat:no-repeat;mask-image:var(--dsbs-icon);mask-size:16px 16px;mask-position:center;mask-repeat:no-repeat}',
376
- // a — one column of stacked rows
377
- '[data-dsbs-bar] [data-dsbs-glyph="a"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%222.5%22 width=%2211%22 height=%2211%22 rx=%222.5%22/%3E%3Cpath d=%22M5.5 6.5h5M5.5 9.5h3%22/%3E%3C/svg%3E")}',
372
+ // n — official default: both stacked groups stay visible
373
+ '[data-dsbs-bar] [data-dsbs-glyph="n"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%222.5%22 width=%2211%22 height=%2211%22 rx=%222.5%22/%3E%3Cpath d=%22M5.5 6.5h5M5.5 9.5h3%22/%3E%3C/svg%3E")}',
378
374
  // b — one column split down the middle
379
375
  '[data-dsbs-bar] [data-dsbs-glyph="b"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%222.5%22 width=%2211%22 height=%2211%22 rx=%222.5%22/%3E%3Cpath d=%22M8 3v10%22/%3E%3C/svg%3E")}',
380
376
  // c — a panel floating above the column
381
377
  '[data-dsbs-bar] [data-dsbs-glyph="c"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%226.5%22 width=%229%22 height=%227%22 rx=%221.8%22/%3E%3Cpath d=%22M7.2 6.5V4.4a1.6 1.6 0 011.6-1.6h3.1a1.6 1.6 0 011.6 1.6v3.9%22/%3E%3C/svg%3E")}',
382
- // fold — collapse one list (single chevron)
383
- '[data-dsbs-bar] [data-dsbs-glyph="fold"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 3.5h10M8 6v6M5.6 9.6L8 12l2.4-2.4%22/%3E%3C/svg%3E")}',
384
- // fold all — two chevrons, so the two fold buttons never look alike
385
- '[data-dsbs-bar] [data-dsbs-glyph="fold2"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 3h10M5.6 6.4L8 8.8l2.4-2.4M5.6 10.2L8 12.6l2.4-2.4%22/%3E%3C/svg%3E")}',
386
378
  // swap — trade the two tabs
387
379
  '[data-dsbs-bar] [data-dsbs-glyph="swap"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M2.5 5.5h9.5M9.5 3.2l2.5 2.3-2.5 2.3%22/%3E%3Cpath d=%22M13.5 10.5H4M6.5 8.2l-2.5 2.3 2.5 2.3%22/%3E%3C/svg%3E")}',
388
380
  // pop — detach the panel
@@ -432,6 +424,26 @@ window.__ModuleLoader__.load({
432
424
  'html[data-dsbs-narrow="true"] [data-dsbs-tabs] button[data-dsbs-hasicon] [data-dsbs-tabbadge]{display:none}',
433
425
  'html[data-dsbs-rail="true"] [data-dsbs-tabs]{padding:2px 2px 0}',
434
426
  'html[data-dsbs-rail="true"] [data-dsbs-tabs] button{max-width:40px;padding:4px 5px}',
427
+
428
+ // ---- grid-based sidebar roots (DSH 0.2.2-rc.2) --------------------
429
+ // rc.2 makes div.scrollArea a CSS grid with three explicit columns
430
+ // (4px | 1fr | 6px) AND four explicit rows: nav.panelList is pinned to
431
+ // grid-row 2, div.regionArea to grid-row 4. 0.4.1 only spanned the
432
+ // columns, which fixed the 4px squeeze but still left every injected
433
+ // child auto-placed in a new implicit row AFTER both host panels -- the
434
+ // control bar appeared below the whole list. `order` cannot override an
435
+ // explicit grid-row, so the bar and tab strip now live OUTSIDE the grid
436
+ // (in the flex column div.scrollRegion). Only a third-party tab page is
437
+ // still a grid item; it keeps the full-width span and the 4px/6px
438
+ // padding that gives the two decorative columns back.
439
+ '[data-dsbs-tabpanel]{grid-column:1/-1;min-width:0;max-width:100%;box-sizing:border-box;padding-left:4px;padding-right:6px}',
440
+ '[data-dsbs-bar],[data-dsbs-tabs]{min-width:0;max-width:100%;box-sizing:border-box;flex:none}',
441
+ // In the flex header host DOM order already is visual order. These are a
442
+ // belt-and-braces guard for a future host that grids that wrapper too:
443
+ // the injected header still precedes the scrolling viewport.
444
+ '[data-dsbs-bar]{order:-30}',
445
+ '[data-dsbs-tabs]{order:-20}',
446
+ '[data-dsbs-tabpanel]{order:-10}',
435
447
  ].join('\n')
436
448
 
437
449
  function assign(target) {
@@ -494,7 +506,47 @@ window.__ModuleLoader__.load({
494
506
  }
495
507
  }
496
508
  }
497
- return { root: root, nav: nav, regionArea: regionArea, workspaces: workspaces }
509
+
510
+ // DSH 0.2.2-rc.2 renders (see the host's SidebarRoot.tsx):
511
+ // div.scrollRegion flex column, never rebuilt by React
512
+ // > div.scrollArea CSS grid, 4 explicit rows
513
+ // > nav.panelList pinned to grid-row 2
514
+ // > div.regionArea pinned to grid-row 4
515
+ // A child injected into scrollArea without its own grid-row is
516
+ // auto-placed into a NEW implicit row AFTER both host panels, so the
517
+ // control bar shows up under the whole list. `order` cannot beat an
518
+ // explicit grid-row -- that is why the 0.4.2 `order` attempt failed.
519
+ // The reliable place for our header is scrollRegion, the flex column
520
+ // that owns scrollArea: DOM order is visual order there in BOTH the
521
+ // grid (expanded) and flex-column (rail) states, and nothing re-parents
522
+ // that wrapper when a group is folded. Semantic class suffixes only;
523
+ // the generated CSS-module hash prefix must never be hard-coded.
524
+ var scrollArea = null
525
+ var scan = root
526
+ while (scan && scan !== document.body && scan !== document.documentElement) {
527
+ if (scan.tagName === 'DIV' && /scrollArea/.test(String(scan.className || ''))) {
528
+ scrollArea = scan
529
+ break
530
+ }
531
+ scan = scan.parentElement
532
+ }
533
+ // Keep `root` pinned to the outer scrolling container even when a fold
534
+ // puts the nav one level deeper; popover geometry and the observer use it.
535
+ if (scrollArea && scrollArea !== root && scrollArea.contains(workspaces)
536
+ && (!nav || scrollArea.contains(nav))) {
537
+ root = scrollArea
538
+ }
539
+ var headerHost = null
540
+ if (scrollArea && scrollArea.parentElement
541
+ && /scrollRegion/.test(String(scrollArea.parentElement.className || ''))) {
542
+ headerHost = scrollArea.parentElement
543
+ }
544
+ // Pre-rc.2 hosts have no grid. With no headerHost the old flex root is
545
+ // still correct and the `order` rules keep the header in front there.
546
+ return {
547
+ root: root, scrollArea: scrollArea, headerHost: headerHost,
548
+ nav: nav, regionArea: regionArea, workspaces: workspaces
549
+ }
498
550
  }
499
551
 
500
552
  function readLocal() {
@@ -516,14 +568,21 @@ window.__ModuleLoader__.load({
516
568
  }
517
569
  }
518
570
 
571
+ // 0.5.0: `a` (independent folding) was removed. A profile that still has
572
+ // `mode: a` must not silently land on a different-looking layout, so every
573
+ // read maps it to the official default. The alias is never written back,
574
+ // never rendered as a button and never returned by getLayout().
575
+ function normalizeMode(raw) {
576
+ var mode = LEGACY_MODES[raw] || raw
577
+ return MODES.indexOf(mode) >= 0 ? mode : DEFAULTS.mode
578
+ }
579
+
519
580
  function adoptValues(raw) {
520
581
  var out = assign({}, DEFAULTS)
521
582
  if (!raw || typeof raw !== 'object') return out
522
- if (MODES.indexOf(raw.mode) >= 0) out.mode = raw.mode
583
+ out.mode = normalizeMode(raw.mode)
523
584
  if (typeof raw.showQuickSwitch === 'boolean') out.showQuickSwitch = raw.showQuickSwitch
524
585
  if (raw.tab === 'ext' || raw.tab === 'ws') out.tab = raw.tab
525
- if (typeof raw.foldExt === 'boolean') out.foldExt = raw.foldExt
526
- if (typeof raw.foldWs === 'boolean') out.foldWs = raw.foldWs
527
586
  return out
528
587
  }
529
588
 
@@ -690,6 +749,8 @@ window.__ModuleLoader__.load({
690
749
  // instead of sniffing classes off <html>.
691
750
  getLayout: function () {
692
751
  return {
752
+ // 0.5.0: 'b' (toggle), 'c' (overlay), 'n' (official default).
753
+ // 'a' no longer exists; old configs are normalized on read.
693
754
  mode: config.values.mode,
694
755
  tab: config.values.tab,
695
756
  activeTab: runtime.activeTab,
@@ -1532,10 +1593,13 @@ window.__ModuleLoader__.load({
1532
1593
  tempChatGuard = setInterval(syncChatMode, 1500)
1533
1594
  }
1534
1595
 
1535
- // 0.2.9: the three layouts are one segmented pill (they are mutually
1536
- // exclusive), and the actions that belong to the chosen layout sit after
1537
- // a divider. Glyphs are CSS pseudo elements, so makeButton() keeps owning
1538
- // the plain text node that relabel() rewrites.
1596
+ // The layouts are one segmented pill (mutually exclusive); the actions
1597
+ // belonging to the chosen layout sit after a divider. 0.5.0 removed the
1598
+ // two independent fold toggles, which could produce an empty sidebar.
1599
+ // The official default has no action at all, but the pill STAYS visible:
1600
+ // with showQuickSwitch off it is the only way back out of that layout.
1601
+ // Glyphs are CSS pseudo elements, so makeButton() keeps owning the plain
1602
+ // text node that relabel() rewrites.
1539
1603
  function buildBar() {
1540
1604
  var bar = document.createElement('div')
1541
1605
  bar.setAttribute('data-dsbs-bar', '')
@@ -1552,7 +1616,7 @@ window.__ModuleLoader__.load({
1552
1616
  var modeButton = makeButton('mode-' + mode, 'mode.' + mode, 'mode.' + mode + '.hint', (function (value) {
1553
1617
  return function () { setMode(value) }
1554
1618
  })(mode))
1555
- modeButton.setAttribute('data-dsbs-glyph', mode === 'a' ? 'a' : mode === 'b' ? 'b' : 'c')
1619
+ modeButton.setAttribute('data-dsbs-glyph', mode)
1556
1620
  seg.appendChild(modeButton)
1557
1621
  }
1558
1622
  bar.appendChild(seg)
@@ -1560,8 +1624,6 @@ window.__ModuleLoader__.load({
1560
1624
  var separator = makeSeparator()
1561
1625
  bar.appendChild(separator)
1562
1626
 
1563
- var foldExt = makeButton('fold-ext', 'fold.ext.collapse', 'fold.ext.hint', function () { setField('foldExt', !config.values.foldExt) })
1564
- var foldWs = makeButton('fold-ws', 'fold.ws.collapse', 'fold.ws.hint', function () { setField('foldWs', !config.values.foldWs) })
1565
1627
  var tabSwap = makeButton('tab-swap', 'tab.to.ws', 'tab.to.ws.hint', function () {
1566
1628
  setField('tab', config.values.tab === 'ext' ? 'ws' : 'ext')
1567
1629
  })
@@ -1574,21 +1636,14 @@ window.__ModuleLoader__.load({
1574
1636
  var index = MODES.indexOf(config.values.mode)
1575
1637
  setMode(MODES[(index + 1) % MODES.length])
1576
1638
  })
1577
- foldExt.setAttribute('data-dsbs-glyph', 'fold')
1578
- // Two fold buttons sit side by side in layout a, so they must not
1579
- // share a glyph — a single chevron vs. a double one is the only cue
1580
- // available once the labels collapse to icons.
1581
- foldWs.setAttribute('data-dsbs-glyph', 'fold2')
1582
1639
  tabSwap.setAttribute('data-dsbs-glyph', 'swap')
1583
1640
  pop.setAttribute('data-dsbs-glyph', 'pop')
1584
1641
  quick.setAttribute('data-dsbs-glyph', 'quick')
1585
- bar.appendChild(foldExt)
1586
- bar.appendChild(foldWs)
1587
1642
  bar.appendChild(tabSwap)
1588
1643
  bar.appendChild(pop)
1589
1644
  bar.appendChild(quick)
1590
1645
 
1591
- runtime.parts = { separator: separator, context: [foldExt, foldWs, tabSwap, pop] }
1646
+ runtime.parts = { separator: separator, context: [tabSwap, pop] }
1592
1647
  return bar
1593
1648
  }
1594
1649
 
@@ -1614,8 +1669,15 @@ window.__ModuleLoader__.load({
1614
1669
  }
1615
1670
 
1616
1671
  function ensureBar(fx) {
1672
+ // rc.2's scrollArea is a grid with explicitly pinned host rows, so the
1673
+ // bar lives one level up in scrollRegion (flex column). Targeting the
1674
+ // scrolling viewport as the anchor means "above the list" in every
1675
+ // fold state without ever becoming a grid item.
1676
+ var host = fx.headerHost || fx.root
1677
+ var anchor = (fx.scrollArea && fx.scrollArea.parentNode === host)
1678
+ ? fx.scrollArea : host.firstChild
1617
1679
  var bar = runtime.bar
1618
- if (bar && bar.parentNode !== fx.root) {
1680
+ if (bar && bar.parentNode !== host) {
1619
1681
  try {
1620
1682
  if (bar.parentNode) bar.parentNode.removeChild(bar)
1621
1683
  } catch (error) {
@@ -1627,12 +1689,13 @@ window.__ModuleLoader__.load({
1627
1689
  bar = buildBar()
1628
1690
  runtime.bar = bar
1629
1691
  }
1630
- var anchor = fx.nav || fx.regionArea
1631
- if (anchor && anchor.parentNode === fx.root) {
1632
- if (bar.parentNode !== fx.root || !isBefore(bar, anchor)) fx.root.insertBefore(bar, anchor)
1633
- } else if (bar.parentNode !== fx.root) {
1634
- fx.root.appendChild(bar)
1635
- }
1692
+ // Compare ORDER, never adjacency: the tab strip sits between the bar
1693
+ // and the anchor in the steady state. Demanding `nextSibling ===
1694
+ // anchor` moved a node on every paint and hung DSH in 0.3.0.
1695
+ if (anchor === bar) return bar
1696
+ if (bar.parentNode === host && anchor && anchor.parentNode === host
1697
+ && isBefore(bar, anchor)) return bar
1698
+ host.insertBefore(bar, anchor)
1636
1699
  return bar
1637
1700
  }
1638
1701
 
@@ -1894,8 +1957,17 @@ window.__ModuleLoader__.load({
1894
1957
  }
1895
1958
 
1896
1959
  function ensureTabStrip(fx) {
1960
+ // The strip shares the control bar's flex header host, staying out of
1961
+ // the grid for exactly the same reason.
1962
+ var host = fx.headerHost || fx.root
1963
+ var bar = runtime.bar
1964
+ // rc.2: all injected header boxes sit before the scrolling viewport.
1965
+ // Older flex roots have no scrollArea, so follow the bar directly.
1966
+ var anchor = (fx.scrollArea && fx.scrollArea.parentNode === host)
1967
+ ? fx.scrollArea
1968
+ : ((bar && bar.parentNode === host) ? bar.nextSibling : null)
1897
1969
  var strip = runtime.tabStrip
1898
- if (strip && strip.parentNode !== fx.root) {
1970
+ if (strip && strip.parentNode !== host) {
1899
1971
  try {
1900
1972
  if (strip.parentNode) strip.parentNode.removeChild(strip)
1901
1973
  } catch (error) {
@@ -1910,17 +1982,14 @@ window.__ModuleLoader__.load({
1910
1982
  strip.setAttribute('aria-label', t('tabs.label'))
1911
1983
  runtime.tabStrip = strip
1912
1984
  }
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)
1985
+ // Steady state: in this host, after the bar, before the scrolling
1986
+ // viewport. Order checks only -- exact adjacency spins the observer.
1987
+ var inPlace = strip.parentNode === host
1988
+ && (!bar || bar.parentNode !== host || isBefore(bar, strip))
1989
+ && (!anchor || anchor.parentNode !== host || isBefore(strip, anchor))
1990
+ if (inPlace || anchor === strip) return strip
1991
+ if (anchor && anchor.parentNode === host) host.insertBefore(strip, anchor)
1992
+ else host.appendChild(strip)
1924
1993
  return strip
1925
1994
  }
1926
1995
 
@@ -2020,9 +2089,19 @@ window.__ModuleLoader__.load({
2020
2089
 
2021
2090
  function placeTabPanel(panel, fx) {
2022
2091
  if (panel.parentNode === fx.root) return
2023
- var anchor = fx.nav || fx.regionArea
2024
- if (anchor && anchor.parentNode === fx.root) fx.root.insertBefore(panel, anchor)
2025
- else fx.root.appendChild(panel)
2092
+ var anchor = (fx.nav && fx.nav.parentNode === fx.root) ? fx.nav
2093
+ : ((fx.regionArea && fx.regionArea.parentNode === fx.root) ? fx.regionArea : null)
2094
+ if (anchor) {
2095
+ fx.root.insertBefore(panel, anchor)
2096
+ return
2097
+ }
2098
+ // With the root pinned to the outer scroll area the host panels are no
2099
+ // longer direct children, so the old `appendChild` fallback put the
2100
+ // plugin page after everything. Keep it directly under the header.
2101
+ var after = (runtime.tabStrip && runtime.tabStrip.parentNode === fx.root)
2102
+ ? runtime.tabStrip.nextSibling
2103
+ : ((runtime.bar && runtime.bar.parentNode === fx.root) ? runtime.bar.nextSibling : null)
2104
+ fx.root.insertBefore(panel, after)
2026
2105
  }
2027
2106
 
2028
2107
  function syncTabPanels(fx, activeId) {
@@ -2144,9 +2223,13 @@ window.__ModuleLoader__.load({
2144
2223
  // the heartbeat stays cheap.
2145
2224
  if (config.values.mode !== 'c' || !runtime.popOpen) return
2146
2225
  try {
2147
- var rootRect = fx.root.getBoundingClientRect()
2226
+ // The overlay is position:absolute relative to scrollRegion (the
2227
+ // flex header host), not to scrollArea. With the bar now above the
2228
+ // grid, measuring against the grid would yield a negative offset
2229
+ // and clamp the popover on top of the control bar.
2230
+ var anchorRect = (fx.headerHost || fx.root).getBoundingClientRect()
2148
2231
  var barRect = runtime.bar.getBoundingClientRect()
2149
- var top = Math.round(barRect.bottom - rootRect.top + 4)
2232
+ var top = Math.round(barRect.bottom - anchorRect.top + 4)
2150
2233
  if (!isFinite(top) || top < 0) top = 0
2151
2234
  fx.root.style.setProperty('--dsbs-pop-top', top + 'px')
2152
2235
  } catch (error) {
@@ -2160,8 +2243,6 @@ window.__ModuleLoader__.load({
2160
2243
  html.setAttribute('data-dsbs-mode', state.mode)
2161
2244
  html.setAttribute('data-dsbs-tab', state.tab)
2162
2245
  html.setAttribute('data-dsbs-quick', state.showQuickSwitch ? 'on' : 'off')
2163
- setFlag(html, 'data-dsbs-fold-ext', state.foldExt === true)
2164
- setFlag(html, 'data-dsbs-fold-ws', state.foldWs === true)
2165
2246
  setFlag(html, 'data-dsbs-pop', runtime.popOpen && state.mode === 'c')
2166
2247
  setFlag(html, 'data-dsbs-rail', runtime.rail)
2167
2248
 
@@ -2189,21 +2270,14 @@ window.__ModuleLoader__.load({
2189
2270
  // With no panels to show, the buttons that act on that group are
2190
2271
  // pointless, and hiding the workspaces would leave an empty column.
2191
2272
  var hasExt = !!fx.nav
2192
- var showFold = state.mode === 'a'
2193
2273
  var showTabs = state.mode === 'b'
2194
2274
  var showPop = state.mode === 'c'
2195
- setVisible('fold-ext', showFold && hasExt)
2196
- setVisible('fold-ws', showFold)
2197
2275
  setVisible('tab-swap', showTabs && hasExt)
2198
2276
  setVisible('pop', showPop && hasExt)
2199
2277
  setVisible('quick', state.showQuickSwitch === true)
2200
2278
 
2201
- // The label states what the click will do, so "收起扩展" and "展开扩展"
2279
+ // The label states what the click will do, so "展开扩展" and "收起扩展"
2202
2280
  // never coexist with a stale state.
2203
- var foldExtButton = barButton('fold-ext')
2204
- if (foldExtButton) foldExtButton.setAttribute('data-dsbs-label', state.foldExt ? 'fold.ext.expand' : 'fold.ext.collapse')
2205
- var foldWsButton = barButton('fold-ws')
2206
- if (foldWsButton) foldWsButton.setAttribute('data-dsbs-label', state.foldWs ? 'fold.ws.expand' : 'fold.ws.collapse')
2207
2281
  var popButton = barButton('pop')
2208
2282
  if (popButton) popButton.setAttribute('data-dsbs-label', runtime.popOpen && showPop ? 'pop.close' : 'pop.open')
2209
2283
  var tabSwapButton = barButton('tab-swap')
@@ -2231,8 +2305,6 @@ window.__ModuleLoader__.load({
2231
2305
  var modeButton = barButton('mode-' + MODES[i])
2232
2306
  if (modeButton) modeButton.setAttribute('aria-pressed', state.mode === MODES[i] ? 'true' : 'false')
2233
2307
  }
2234
- if (foldExtButton) foldExtButton.setAttribute('aria-expanded', state.foldExt ? 'false' : 'true')
2235
- if (foldWsButton) foldWsButton.setAttribute('aria-expanded', state.foldWs ? 'false' : 'true')
2236
2308
  if (popButton) {
2237
2309
  popButton.setAttribute('aria-expanded', runtime.popOpen && showPop ? 'true' : 'false')
2238
2310
  }
@@ -2264,16 +2336,20 @@ window.__ModuleLoader__.load({
2264
2336
  if (pluginTab) {
2265
2337
  extHidden = true
2266
2338
  wsHidden = true
2267
- } else if (state.mode === 'a') {
2268
- extHidden = state.foldExt === true
2269
- wsHidden = state.foldWs === true
2270
2339
  } else if (state.mode === 'b') {
2271
2340
  extHidden = state.tab !== 'ext'
2272
2341
  // No panels: keep the workspaces visible rather than the empty half.
2273
2342
  wsHidden = hasExt && state.tab === 'ext'
2274
- } else {
2343
+ } else if (state.mode === 'c') {
2275
2344
  extHidden = !runtime.popOpen
2276
2345
  wsHidden = false
2346
+ } else {
2347
+ // n — official default. No CSS rule hides either group, and no
2348
+ // runtime state may hide one either: both stay exactly as DSH drew
2349
+ // them. A registered plugin tab still takes the column (that is an
2350
+ // explicit consumer action, not a layout override).
2351
+ extHidden = false
2352
+ wsHidden = false
2277
2353
  }
2278
2354
  setGroupHidden(fx.nav, 'ext', extHidden)
2279
2355
  setGroupHidden(fx.regionArea, 'ws', wsHidden)
@@ -2357,8 +2433,6 @@ window.__ModuleLoader__.load({
2357
2433
  runtime.railSaved = {
2358
2434
  mode: config.values.mode,
2359
2435
  tab: config.values.tab,
2360
- foldExt: config.values.foldExt,
2361
- foldWs: config.values.foldWs,
2362
2436
  }
2363
2437
  } else if (runtime.railSaved) {
2364
2438
  // Leaving the rail restores what the user had before the column was
@@ -2547,7 +2621,7 @@ window.__ModuleLoader__.load({
2547
2621
  try {
2548
2622
  var snapshot = controller && controller.getSnapshot && controller.getSnapshot()
2549
2623
  var value = snapshot && snapshot.value
2550
- return MODES.indexOf(value && value.mode) >= 0 ? value.mode : DEFAULTS.mode
2624
+ return normalizeMode(value && value.mode)
2551
2625
  } catch (error) {
2552
2626
  return DEFAULTS.mode
2553
2627
  }
@@ -2612,12 +2686,13 @@ window.__ModuleLoader__.load({
2612
2686
  }, extra || {}) }, first || null, second || null)
2613
2687
  }
2614
2688
  function mini(mode) {
2615
- if (mode === 'a') {
2616
- return frame(null, box({ width: '100%', height: '7px' }), box({ width: '100%', height: '7px' }))
2617
- }
2618
2689
  if (mode === 'b') {
2619
2690
  return frame(null, box({ width: '60%', height: '4px' }), box({ width: '100%', height: '11px' }))
2620
2691
  }
2692
+ if (mode === 'n') {
2693
+ // Official default: two full-width rows, nothing hidden or overlaid.
2694
+ return frame(null, box({ width: '100%', height: '7px' }), box({ width: '100%', height: '7px' }))
2695
+ }
2621
2696
  return frame(null,
2622
2697
  box({ width: '100%', height: '100%', opacity: '0.35' }),
2623
2698
  box({
@@ -3542,6 +3617,8 @@ window.__ModuleLoader__.load({
3542
3617
  var attributes = [
3543
3618
  'data-dsbs-mode',
3544
3619
  'data-dsbs-tab',
3620
+ // Legacy flags from <= 0.4.1. Clear them if an older client run
3621
+ // left them behind; 0.5.0 never sets these itself.
3545
3622
  'data-dsbs-fold-ext',
3546
3623
  'data-dsbs-fold-ws',
3547
3624
  'data-dsbs-pop',
package/lib/index.js CHANGED
@@ -27,6 +27,8 @@ const DEFAULTS = {
27
27
  mode: 'b',
28
28
  showQuickSwitch: true,
29
29
  tab: 'ext',
30
+ // Legacy keys from <= 0.4.1. The Client half no longer reads them, but they
31
+ // stay in the schema so profiles written before 0.5.0 still validate.
30
32
  foldExt: false,
31
33
  foldWs: false,
32
34
  }
@@ -135,14 +137,17 @@ export const Config = schema === null
135
137
  ? undefined
136
138
  : schema.object({
137
139
  mode: schema
138
- .union(['a', 'b', 'c'])
140
+ // 0.5.0: `a` is a removed legacy value. It stays in the union only so
141
+ // profiles written before 0.5.0 still validate; the Client half maps it
142
+ // to `n` on every read and never writes it back.
143
+ .union(['b', 'c', 'n', 'a'])
139
144
  .default(DEFAULTS.mode)
140
- .description('Sidebar layout: a folds each group, b switches the two groups with tabs, c keeps the workspaces and opens the panels as an overlay.')
145
+ .description('Sidebar layout: b switches the two groups with tabs, c keeps the workspaces and opens the panels as an overlay, n is the official default with no overrides. `a` is a removed legacy alias mapped to n.')
141
146
  .volatile(),
142
147
  showQuickSwitch: schema
143
148
  .boolean()
144
149
  .default(DEFAULTS.showQuickSwitch)
145
- .description('Show the button that cycles through the three layouts in the sidebar control bar.')
150
+ .description('Show the button that cycles through the sidebar layouts in the control bar.')
146
151
  .volatile(),
147
152
  tab: schema
148
153
  .union(['ext', 'ws'])
@@ -152,12 +157,12 @@ export const Config = schema === null
152
157
  foldExt: schema
153
158
  .boolean()
154
159
  .default(DEFAULTS.foldExt)
155
- .description('Folded layout: whether the panels group starts folded.')
160
+ .description('Legacy, unused since 0.5.0. Kept so older profiles still validate.')
156
161
  .volatile(),
157
162
  foldWs: schema
158
163
  .boolean()
159
164
  .default(DEFAULTS.foldWs)
160
- .description('Folded layout: whether the workspaces group starts folded.')
165
+ .description('Legacy, unused since 0.5.0. Kept so older profiles still validate.')
161
166
  .volatile(),
162
167
  })
163
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidebar-suite",
3
- "version": "0.4.0",
3
+ "version": "0.5.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 默认开;每天定时清理默认关,可在详情页开启并自设时间)、详情页三种反馈入口、临时会话可右键「保留为正式会话」或「立即删除」、全部 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.",
17
+ "description": "一款专门针对改进 DSH 左侧栏体验的插件:标签页 / 浮层 / 官方默认三种布局一键切换、默认布局在详情页顶部直接设、会话列表默认全展开、「临时会话」开原生会话且默认在退出 DSH 时删除(DELETE_ON_EXIT 默认开;每天定时清理默认关,可在详情页开启并自设时间)、详情页三种反馈入口、临时会话可右键「保留为正式会话」或「立即删除」、全部 9 项配置都有图形开关且改文件免重启 —— 强实用性;界面沿用宿主原生配色、圆角与间距,只加属性与一条控制栏、不重构宿主结构 —— 克制美观。|A plugin dedicated to the DSH sidebar experience: one-click layouts (toggle, overlay, official default), 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 && node tests/test_032.mjs && node tests/test_033.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 && node tests/test_034.mjs && node tests/test_035.mjs && node tests/test_036.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",
@@ -1,6 +1,10 @@
1
1
  # dsh-sidebar-suite 使用说明
2
2
 
3
- **适用版本:v0.2.8**(最后更新 2026-11-11)
3
+ **适用插件版本:v0.5.0**(最后更新 2026-11-12)
4
+
5
+ **已验证的 DSH:0.2.2-rc.2(当前最新)**;`0.2.2-rc.1` 也兼容,但已是旧版本。DSH 从 rc.2 起把侧栏根节点换成了 CSS Grid,**v0.5.0 已完整适配**,布局工具条也固定在「新会话」下方、不再跑到列表底部。
6
+
7
+ > ⚠️ **v0.5.0 移除了旧的「折叠」布局**:胶囊里现在是 `标签页 / 浮层 / 官方默认` 三项。配置文件里若还写着 `mode: a`,**不用手动改** —— 插件会自动当作「官方默认」打开;下次在详情页点一下布局卡片就会写成新值。`0.3.0` 已弃用别装;`0.4.1` 及更早版本仍可能看到布局按钮压住标题或跑到列表下方,升级到 v0.5.0 后**从托盘彻底退出 DSH 再打开一次**即可。
4
8
 
5
9
  **这是一款专门针对改进 DSH 左侧栏体验的插件** —— 只管左侧栏,不碰会话内容、不换模型;设计上既要「强实用性」(少点几下、少滚几屏),也要「美观性」(沿用 DSH 原生观感,不另起一套皮肤)。
6
10
 
@@ -57,9 +61,11 @@
57
61
 
58
62
  | 模式 | 名字 | 长什么样 | 适合 |
59
63
  | --- | --- | --- | --- |
60
- | `a` | 折叠 | 两组各自可以收起/展开 | 只用一个,另一组收起来 |
61
64
  | `b` | 标签页(**出厂默认**) | 两组共用一栏,点标签互斥切换 | 侧栏窄、想省地方 |
62
65
  | `c` | 浮层 | 工作区常驻,扩展面板浮在上面 | 经常两个都要看 |
66
+ | `n` | 官方默认 | 两组都常驻,插件不改布局 | 想看回 DSH 原本的侧栏 |
67
+
68
+ > `a`(折叠)从 v0.5.0 起已移除:两个独立开关能造出「空白栏」和「跟原生一模一样」两种没用的状态,其余两态又和标签页重复。旧配置里的 `a` 会自动按官方默认打开。
63
69
 
64
70
  ### 3.2 临时切一下(只影响这次)
65
71
 
@@ -67,14 +73,14 @@
67
73
 
68
74
  ```
69
75
  ┌─────────────────┐
70
- │ ▤ ▥ ▢ │ │ ⇄ 显示扩展
76
+ │ ▥ ▢ ▤ │ │ ⇄ 换标签页
71
77
  └─────────────────┘
72
- 折叠 标签页 浮层 动作按钮
78
+ 标签页 浮层 官方默认 动作按钮
73
79
  ```
74
80
 
75
- - 点胶囊里的三个图标 = 切换布局(`▤` 折叠 / `▥` 标签页 / `▢` 浮层)。**只对本次会话有效**,下次启动回到默认值。
81
+ - 点胶囊里的三个图标 = 切换布局(`▥` 标签页 / `▢` 浮层 / `▤` 官方默认)。**只对本次会话有效**,下次启动回到默认值。
76
82
  - 每个按钮鼠标悬停都会显示文字说明,不用记图标。
77
- - 竖线右边的按钮 = 在当前布局下切换「看哪一组」;侧栏拖宽超过 300px 后,这些按钮会把文字显示出来。
83
+ - 竖线右边的按钮 = 在当前布局下切换「看哪一组」;侧栏拖宽超过 300px 后,这些按钮会把文字显示出来。**选「官方默认」时右侧没有动作按钮,胶囊也照样在**,随时能点回来。
78
84
  - 侧栏很窄(< 300px)时只显示图标;再窄(< 100px,折成图标条)时连图标都会精简。
79
85
 
80
86
  > 故意不做记忆:临时切换不写盘、不写 localStorage,避免你在窄屏随手一点把默认布局改坏了。
@@ -90,30 +96,30 @@
90
96
  **路线 B(改文件)**
91
97
 
92
98
  - 文件:`C:\Users\12691\.dsh\profiles\desktop\cordis.patch.yml`
93
- - 第 **721** 行,原文:
99
+ - 用记事本打开,找到 `dsh-sidebar-suite` 那一块的 `config:`,里面有这一行(本机在第 **715** 行;行号会随 profile 内容变化,以 `mode:` 这几个字为准):
94
100
 
95
101
  ```yaml
96
102
  mode: b
97
103
  ```
98
104
 
99
- - 改成 `mode: a` 或 `mode: c`
105
+ - 改成 `mode: c`(浮层)或 `mode: n`(官方默认);`b` 是标签页。
100
106
  - 保存 → 彻底退出 DSH → 重开(这里改的是 `cordis.patch.yml`,不是插件设置卡;改插件设置不用重启)
101
107
 
102
- 顺手看下第 **717–725** 行,这一块现在长这样(**只改第 721 行**,其它别动):
108
+ 这一块现在长这样(**只改 `mode:` 那一行**,其它别动):
103
109
 
104
110
  ```yaml
105
- 717 - id: dsh-sidebar-suite
106
- 718 disabled: false
107
- 719 name: dsh-sidebar-suite
108
- 720 config:
109
- 721 mode: b # ← 只改这一行:a = 折叠, b = 标签页, c = 浮层
110
- 722 showQuickSwitch: false
111
- 723 tab: ws
112
- 724 foldExt: true
113
- 725 foldWs: true
111
+ - id: dsh-sidebar-suite
112
+ disabled: false
113
+ name: dsh-sidebar-suite
114
+ config:
115
+ mode: b # ← 只改这一行:b = 标签页, c = 浮层, n = 官方默认
116
+ showQuickSwitch: false
117
+ tab: ws
118
+ foldExt: true # 旧版遗留值,v0.5.0 起不再读取,留着不影响
119
+ foldWs: false # 同上
114
120
  ```
115
121
 
116
- (`showQuickSwitch: false` 就是「换布局」那个按钮不显示;想让它出现就改成 `true`。)
122
+ (`showQuickSwitch: false` 就是「换布局」那个按钮不显示;想让它出现就改成 `true`。哪怕它是关的,布局胶囊也一直在,随时能切回来。)
117
123
 
118
124
  ---
119
125
 
@@ -133,8 +139,8 @@
133
139
  | 你选的布局 | 页签条长什么样 |
134
140
  |---|---|
135
141
  | 标签页(默认) | `扩展 / 工作区 / 插件页` 三种并排 |
136
- | 折叠 | 只有插件页进条;扩展和工作区还是用原来那两个折叠按钮 |
137
142
  | 浮层 | 只有插件页进条;扩展还是用「浮出面板」按钮 |
143
+ | 官方默认 | 只有插件页进条;两组都常驻,没有布局动作按钮 |
138
144
 
139
145
  **一个都没装的话,这排页签根本不会出现** —— 界面和 0.2.x 一模一样,不会平白多一行东西。
140
146
 
@@ -456,7 +462,7 @@ C:\Users\12691\.dsh\backups\20261110-cookie-pileup\ ← 含 Cookies.bak 和
456
462
  | **想看用户反馈到哪了** | [本仓库 Issues](https://github.com/YONAN1269/dsh-sidebar-suite/issues) / [github.com/notifications](https://github.com/notifications);市场评论见 9.1 |
457
463
  | **在插件市场里留言** | 详情页「反馈」框的「**插件市场评论区**」按钮 |
458
464
  | **会话不想再点「展开其余」** | 默认已经全部展开;想恢复折叠见 5.5 |
459
- | 换布局(这次) | 侧栏顶部点 `折叠 / 标签页 / 浮层` |
465
+ | 换布局(这次) | 侧栏顶部点 `标签页 / 浮层 / 官方默认` |
460
466
  | 换布局(以后每次) | 插件 → dsh-sidebar-suite → 详情页最上面三张卡片 |
461
467
  | **设临时会话什么时候自动删** | 详情页「**临时会话自动删除**」那块的两个开关(见 5.1) |
462
468
  | **让它每天凌晨 3 点清一次** | 同上:开「每天定时清理」,时间选 `03:00` |