dsh-quick-toc 0.7.6 → 0.7.8

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.en.md CHANGED
@@ -2,20 +2,31 @@
2
2
 
3
3
  All notable changes to **dsh-quick-toc** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.7.8] - 2026-09-27
6
+
7
+ ### Changed
8
+ - **DSH 0.1.7-rc.2 is supported**: it shares the whole plugin interface with rc.1 (the configuration card still registers into `plugins.item`, `plugins.bundle.config` and the rc line's `settings.plugin.item`, the settings service is still `configForms`, and the Plugins page's slot contract is byte-for-byte rc.1's), so no code change was needed; it was exercised point by point in an isolated environment (host boot, plugin status, the Plugins page's entry and card, a card write landing in the settings document, sessions, panel and curtain).
9
+
10
+ ### Fixed
11
+ - **Opening the curtain's search column no longer re-lays out the outline every frame**: the column used to transition its own width, and because the outline is the flex item next to it, every frame re-laid out the whole rendered list (measured on a 34-row list: one width step per frame, ~18ms frames and a 76ms first hitch; the cost grows with the list). The layout now flips once and the movement rides on a transform (the column slides in from off the right edge while the outline is flipped back to where it sat and released), which measured under 32ms for the first frame, with the resting geometry unchanged.
12
+
13
+ ## [0.7.7] - 2026-09-27
14
+
15
+ ### Fixed
16
+ - **On the 0.1.7 line the Plugins page had no configuration entry for this plugin**: 0.1.7 removed the whole Settings → Plugins → Plugin configuration page, leaving the configuration card on the bundle page alone (`plugins.bundle.config`, keyed on the npm package name), with no entry for this plugin in the Plugins page's Official group — the settings were unreachable from there. The package now also registers `plugins.item` (`id` `quick-toc`, `order` 50, titled after the panel), and opening that entry shows the card already expanded (both 0.1.7 seats render it with a `view`, so the card starts open there; the rc line's Plugin configuration cell passes no props and keeps the collapsed card list).
17
+ - **Opening the curtain could play its whole drop animation before any of it was seen**: opening the curtain re-lays out the list at the curtain's width, and on a long session that commit is itself a long task, so an animation clock started together with it had already run its 0.46s by the first painted frame (0.7.4 fixed another cause of the same failure: the auto-loader pulling the whole session in at once). The animation is now re-armed once the open state is in the DOM — that layout finishes first, the clock starts after it.
18
+
5
19
  ## [0.7.6] - 2026-09-24
6
20
 
7
21
  ### Fixed
8
- - **On the 0.1.7 line the settings card's writes only reached the browser**: on a profile whose entry id differs from the first candidate name (the real profile names the entry `dsh-quick-toc` while the first candidate is `quick-toc`), the `configForms.describe()` view was still EMPTY at the moment the plugin applied, so the code that looks up the settings namespace guessed a candidate name the host does not serve and bound a form that never became ready. The card still opened and still took clicks, but every write fell back to this browser's mirror while the settings document (the profile's `cordis.patch.yml` on 0.1.7) stayed untouched — and the console stayed silent. Now an empty view means "the document has not arrived", not "the host serves nothing": the plugin no longer guesses, it subscribes to `describe()` and binds once the document is there, and only then registers the card; a host that never serves a document still gets the card after 4 seconds, reading and writing the browser mirror (the documented fallback layer). Verified on both lines: on 0.1.7-rc.1 the profile's `cordis.patch.yml` gained `lang: en`, on 0.1.5-rc.3 `settings.yaml`'s `dsh-quick-toc:` became `{ handle: 0.6, autoLoad: false, lang: en }`.
22
+ - **On the 0.1.7 line the settings card's writes only reached the browser**: before the host's settings document had arrived (on a profile whose entry id differs from the candidate names) the plugin bound a form that does not exist — the card opened and took clicks, but every write fell back to the browser mirror with the settings document untouched and no console error. It now waits for the document, and a host that never serves one still gets the card after 4 seconds (the browser mirror, its documented fallback layer).
9
23
 
10
24
  ## [0.7.5] - 2026-09-24
11
25
 
12
26
  ### Changed
13
- - **DSH 0.1.7-rc.1 is supported**: it is the 0.1.7 interface set moved onto the rc channel (`settingsScope`, `settings.plugin.item`, `sessions.open` and `installSection` are all gone; it shares `configForms`, `plugins.bundle.config`, `uiWorkspace` and `settings.configure` with 0.1.7-alpha.x), and no code change was needed. Measured in an isolated environment: the host booted with no error, and the panel, the curtain, Esc, the dock switch and the collapse were all exercised on a real session with no browser error. `dsh.compatibility.dshReleases` records it.
27
+ - **DSH 0.1.7-rc.1 is supported**: it is the 0.1.7 interface set moved onto the rc channel and shares its settings service and configuration-card slots with 0.1.7-alpha.x, so no code change was needed; it was exercised point by point in an isolated environment (host boot, panel, curtain, Esc, dock switch and collapse).
14
28
  - **The declared compatibility floor moved to 0.1.5-rc.3**: the older 0.1.5-rc.1 and 0.1.5-rc.2 are no longer declared (`engines.dsh` and both peer ranges follow), and the README's compatibility table matches.
15
- - **The loader row id shipped in this package now equals the settings namespace** (`quick-toc` to `dsh-quick-toc`): from DSH 0.1.7 on, the settings document is a set of forms addressed by **profile entry id**, and the old `settings.yaml` is renamed to an import source whose sections are matched to entries of the same name; an id that does not match the namespace the plugin registered leaves an existing installation's saved preferences behind in that import file (the plugin then reads empty values). With matching names this package can live in the profile's `dsh.profile.bundles` (0.1.7's plugin manager only offers a configuration page for packages listed there), and on 0.1.5-rc.3 the id is only a loader key, so the same value is harmless. **Being in `bundles` and a profile patch `insert:` are mutually exclusive**: the same entry id twice makes the host refuse to boot.
16
-
17
- ### Fixed
18
- - **A binding fallback for the settings scope**: the host's settings document can still be loading when a plugin applies (0.1.7-rc.1 answers `status: "loading"` for that form), and the code used to accept only a one-shot "ready" snapshot; it now falls back to the first candidate form that EXISTS and lets the subscription take over once the document arrives.
29
+ - **The loader row id now equals the settings namespace** (`quick-toc` to `dsh-quick-toc`): from DSH 0.1.7 on, settings are addressed by profile entry id, and an id that does not match the namespace the plugin registered leaves an existing installation's saved preferences unreadable in the import file. With matching names this package can also live in the profile's `dsh.profile.bundles` (0.1.7's plugin manager only offers a configuration page for packages listed there), but **never together with a profile-patch `insert:` of the same name** (that makes the host refuse to boot).
19
30
 
20
31
  ## [0.7.4] - 2026-09-23
21
32
 
package/CHANGELOG.md CHANGED
@@ -2,20 +2,31 @@
2
2
 
3
3
  **dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.7.8] - 2026-09-27
6
+
7
+ ### 变更
8
+ - **支持 DSH 0.1.7-rc.2**:与 rc.1 共用同一套插件接口(配置卡片仍挂在 `plugins.item`、`plugins.bundle.config` 与 rc 线的 `settings.plugin.item` 上,设置服务仍是 `configForms`,插件管理页的槽位契约与 rc.1 逐字节相同),代码无需改动;隔离环境里逐项实测通过(宿主启动、插件运行状态、插件页条目与卡片、卡片写入落到设置文档、会话、面板与幕布)。
9
+
10
+ ### 修复
11
+ - **幕布打开搜索栏时不再逐帧重排大纲**:搜索栏原本过渡自己的宽度,而大纲是与它同行的伸缩项,于是每一帧都要重排整个已渲染列表(34 行的列表实测:每帧一次宽度步进、约 18ms 一帧、首帧卡顿 76ms;列表越长越重)。现在布局只翻转一次,位移交给 transform(搜索栏从右缘外滑入,大纲反向翻转回原位置再释放),实测首帧卡顿降到 32ms 以内,静止时的几何与之前完全一致。
12
+
13
+ ## [0.7.7] - 2026-09-27
14
+
15
+ ### 修复
16
+ - **0.1.7 线上插件页缺少本插件的配置入口**:0.1.7 移除了「设置 → 插件 → 插件配置」整页,配置卡片只剩组合包页面上的 `plugins.bundle.config`(以 npm 包名为键)一处,插件页「官方」分组里没有本插件的条目,读者在那里找不到设置入口。现在同时注册 `plugins.item`(`id` 为 `quick-toc`、`order` 50、标题取面板名),点开该条目即见已展开的卡片(0.1.7 的两处座位渲染时都带 `view`,卡片因此默认展开;rc 线「插件配置」里那处不带 props,仍是收拢的卡片列表)。
17
+ - **打开幕布时展开动画可能整段播完才被看到**:打开幕布会让列表按幕布的宽度整体重排,长会话里这次提交本身就是一段长任务,动画时钟若与这次提交同时开始,0.46 秒会在首帧绘制之前走完(0.7.4 修的是同一类问题的另一种成因:当时是自动加载把整段会话一次性拉了进来)。现在动画在打开状态写入 DOM 之后重新点火一次,先完成这次布局再开始计时。
18
+
5
19
  ## [0.7.6] - 2026-09-24
6
20
 
7
21
  ### 修复
8
- - **0.1.7 线上设置卡片的写入只落在浏览器里**:入口 id 与候选名不一致的 profile 上(例如真实 profile 把入口命名为 `dsh-quick-toc`,而候选表第一个是 `quick-toc`),插件加载的那一刻 `configForms.describe()` 的视图**还没到**(实测返回空表),于是找设置命名空间的那段代码按候选名猜了一个**宿主并不服务**的名字,绑上一个永远不就绪的表单——卡片照常能开能点,但所有写入都退回本浏览器的镜像,设置文档(0.1.7 上是 profile 的 `cordis.patch.yml`)永远不动,而且控制台没有任何报错。现在:视图为空时**不再猜名字**(空 ≠ 宿主不服务),改为订阅 `describe()` 并在文档到位后绑定,绑定成功才注册卡片;宿主始终不给文档时 4 秒后仍注册卡片、退回本地镜像这个既有的降级层。0.1.7-rc.1 与 0.1.5-rc.3 两条线都实测确认写入落盘(前者 `cordis.patch.yml` 新增 `lang: en`,后者 `settings.yaml` 的 `dsh-quick-toc:` 变成 `{ handle: 0.6, autoLoad: false, lang: en }`)。
22
+ - **0.1.7 线上配置卡片的写入只落在浏览器里**:宿主设置文档还没到位时(入口 id 与候选名不一致的 profile 上)插件绑到了一个并不存在的表单——卡片能开能点,但写入都退回本地镜像、设置文档不动,控制台也没有报错。现在等文档到位再绑定,宿主始终不给文档时 4 秒后仍提供卡片(退回本地镜像这个既有降级层)。
9
23
 
10
24
  ## [0.7.5] - 2026-09-24
11
25
 
12
26
  ### 变更
13
- - **支持 DSH 0.1.7-rc.1**:它是 0.1.7 那套接口搬到 rc 通道(`settingsScope`、`settings.plugin.item`、`sessions.open`、`installSection` 都已不存在,与 0.1.7-alpha.x 共用 `configForms`、`plugins.bundle.config`、`uiWorkspace`、`settings.configure`),代码不需要改动。隔离环境实测:宿主启动无报错,面板、幕布、Esc、停靠切换与收起在一段真实会话上逐项通过,浏览器无报错。`dsh.compatibility.dshReleases` 随之增加该版本。
27
+ - **支持 DSH 0.1.7-rc.1**:它是 0.1.7 那套接口搬到 rc 通道,与 0.1.7-alpha.x 共用同一套设置服务与配置卡片槽位,代码无需改动;隔离环境里逐项实测通过(宿主启动、面板、幕布、Esc、停靠切换与收起)。
14
28
  - **声明的兼容下限收到 0.1.5-rc.3**:更早的 0.1.5-rc.1、0.1.5-rc.2 不再声明(`engines.dsh` 与两条 peer 区间同步收紧),README 兼容表跟上。
15
- - **插件自带的 loader 行 id 与设置命名空间对齐**(`quick-toc` → `dsh-quick-toc`):DSH 0.1.7 起设置文档改为按**入口 id** 寻址的表单,旧 `settings.yaml` 被改名为导入源、按同名把旧配置映射到入口;id 与插件注册的命名空间不一致时,已保存的偏好会被留在导入文件里(插件读到空值)。同名之后本插件可以放进 profile 的 `dsh.profile.bundles`(0.1.7 的插件管理页只在 bundles 里的包上给出配置页),0.1.5-rc.3 上 id 只是加载器键、同样无害。**装进 `bundles` 与 profile patch 的 `insert:` 不能同时存在**(同名 entry id 会让宿主直接启动失败)。
16
-
17
- ### 修复
18
- - **设置作用域的绑定兜底**:宿主设置文档在插件加载时可能仍在载入(0.1.7-rc.1 实测该表单的 `status` 为 `loading`),原先只认一次「就绪」快照;现在会退到第一个存在的候选表单,由订阅在文档到位后接续。
29
+ - **加载行 id 与设置命名空间对齐**(`quick-toc` → `dsh-quick-toc`):DSH 0.1.7 起设置按入口 id 寻址,名字与插件注册的命名空间不一致时,已保存的偏好会留在导入文件里读不到。同名之后本插件也可以放进 profile 的 `dsh.profile.bundles`(0.1.7 的插件管理页只在 bundles 里的包上给出配置页),但**不能与 profile patch 里的同名 `insert:` 并存**(会让宿主启动失败)。
19
30
 
20
31
  ## [0.7.4] - 2026-09-23
21
32
 
package/README.en.md CHANGED
@@ -42,7 +42,7 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
42
42
 
43
43
  | Plugin | Supported DSH |
44
44
  | --- | --- |
45
- | **0.7.x** (latest, 0.7.6) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1 |
45
+ | **0.7.x** (latest, 0.7.8) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2 |
46
46
  | 0.6.x (0.6.3) | 0.1.5-rc.1, 0.1.5-rc.2 |
47
47
  | 0.5.x (0.5.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
48
48
  | 0.4.x (0.4.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
@@ -115,7 +115,7 @@ A missing `turnOutline` means the host has no such projection, and the staged `j
115
115
  - Interface text lives in one string table at the top of `lib/client.js` (`DICTS`, one Chinese and one English copy) and the language setting picks between them: on `follow the host` the host's translate function (`ctx.locale.bind("dsh-quick-toc")`, fed by the same `DICTS` tables) is asked first and the built-in table is only the fallback. The two tables must stay key-aligned; the only Chinese-only entry is `time.beforeYesterday` (English carries the date for anything older than yesterday), while `time.yesterday` exists in both (`yesterday`).
116
116
  - Preferences sit in two layers behind one module-level store: the Host settings document (language, docked edge, levels, scale, handle position, remembered reading position, fuzzy, hover and diagnostic; authoritative on a loopback page) and `localStorage` (the mirror, plus the real storage for top offset / width / height, which describe this screen and are saved per browser). **The host layer itself is one of two dialects, whichever the running host offers**: on 0.1.5-rc.3 it is `ctx.settingsScope.bind({ namespace: "dsh-quick-toc" })` (addressed by namespace), while on either 0.1.7 line (alpha and rc) it is `ctx.configForms.get(<profile entry id>)` (addressed by profile entry id, the client recognising its own entry by the schema the host serves). Both expose the same methods over the same snapshot fields, so the store stays one. Writes are routed per field: host fields go through `scope.mutate` (folded in locally, reconciled when the host answers), everything else goes to `localStorage`; a value that IS the field's default is written by removing that entry from the user layer instead (the same thing as "not customized"). On a non-loopback page DSH marks settings read-only, so those nine preferences fall back to the mirror and behave exactly as in 0.5.x; values left by a 0.5.x install are imported into the host layer once, the first time it answers (and only while the user layer is empty). Once the host has answered, that layer is authoritative — which is why a local key such as `localStorage['dsh-quick-toc.debug']` no longer takes effect on a loopback page: use the switch in the card.
117
117
  - The curtain and the docked panel **share one list DOM**: the curtain only moves the panel itself into a full-width container that drops from the lower edge of the tab strip (in curtain mode the panel swaps to a different geometry — relative positioning, 100% width and height, no panel scale), and that container stays mounted as a zero-size pass-through while the curtain is up, so opening or closing it never rebuilds the list or loses the scroll position. Cross-session search goes through the client `ctx.get("sessions")` service's `search(query, signal)` (the host's session full-text index, at most 20 hits per search); each session's reading position lives in this browser (`dsh-quick-toc.readPos.v1`, valid for half an hour).
118
- - The configuration card is registered into one of two slots, whichever the running host offers: on 0.1.5-rc.3 it is `settings.plugin.item` (keyed on the plugin's namespace; the slot is dispatched only for namespaces the Host actually serves, so a deployment without the host half simply shows no card and the panel is unaffected), and on either 0.1.7 line (alpha and rc) it is `plugins.bundle.config` (keyed on the npm package name, so the card appears on that bundle's page in the sidebar's Plugins list). Both registrations happen only once a settings scope has bound, and the card and the panel share the store above, which is what makes the two live in both directions.
118
+ - The configuration card is registered into all three seats at once, and which one it lands in is the running host's business: on 0.1.5-rc.3 it is `settings.plugin.item` (keyed on the plugin's namespace; the slot is dispatched only for namespaces the Host actually serves, so a deployment without the host half simply shows no card and the panel is unaffected); on either 0.1.7 line (alpha and rc) it is two places on the sidebar's Plugins page — the plugin's own entry in the Official group (`plugins.item`, `id` `quick-toc`, `label` from "Conversation Outline", `order` 50) and the configuration area of its bundle page (`plugins.bundle.config`, keyed on the npm package name). Both 0.1.7 seats hand the card a `view`, so it starts expanded there (opening the entry or the bundle page IS the expansion gesture); the rc line's configuration cell passes no props at all and keeps the collapsed list shape. Every registration happens only once a settings scope has bound, and the card and the panel share the store above, which is what makes the two live in both directions.
119
119
  - Changes to `lib/client.js` show up after a page refresh (client modules are served under a content hash and DSH's client HMR pushes reloads); changes to `lib/index.js` (the host half) need a DSH restart
120
120
 
121
121
  ## License
package/README.md CHANGED
@@ -42,7 +42,7 @@
42
42
 
43
43
  | 插件版本 | 支持的 DSH 版本 |
44
44
  | --- | --- |
45
- | **0.7.x**(最新,0.7.6) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1 |
45
+ | **0.7.x**(最新,0.7.8) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2 |
46
46
  | 0.6.x(0.6.3) | 0.1.5-rc.1、0.1.5-rc.2 |
47
47
  | 0.5.x(0.5.1) | 0.1.5-rc.1、0.1.5-rc.2 |
48
48
  | 0.4.x(0.4.1) | 0.1.5-rc.1、0.1.5-rc.2 |
@@ -115,7 +115,7 @@ dsh plugin --profile web add <插件目录路径>
115
115
  - 界面文字来自 `lib/client.js` 顶部的一张字符串表(`DICTS`,中英各一份),语言设置决定用哪一份:`跟随宿主` 时优先问宿主的翻译函数(`ctx.locale.bind("dsh-quick-toc")`,注册的表就来自 `DICTS`),拿不到才回退到内置表。中英两份表的键必须对齐(仅 `time.beforeYesterday`(前天)是中文独有——英文对更早的时间直接用日期;`time.yesterday` 两语言都有,英文作 `yesterday`)。
116
116
  - 偏好存储分两层,由一个模块级 store 统一读出:宿主设置文档(语言/停靠边/层级/缩放/把手位置/记住阅读位置/模糊/悬停/诊断九项,回环页面上是权威层)与 `localStorage`(镜像 + 顶边距/宽度/高度三项的正式存储——它们描述"这一块屏幕",按浏览器保存)。**宿主那一层按运行中的宿主二选一**:0.1.5-rc.3 上是 `ctx.settingsScope.bind({ namespace: "dsh-quick-toc" })`(按命名空间寻址),0.1.7 两条线(alpha 与 rc)上改成 `ctx.configForms.get(<profile entry id>)`(按 profile 条目 id 寻址,客户端按宿主实际服务的 schema 认出自己那一条);两者暴露的方法与快照字段相同,所以 store 只有一套。写入按字段路由:宿主字段走 `scope.mutate`(本地同步折叠、宿主应答后对账),其余写 `localStorage`;写入的值若正好是该字段的默认值,则改为把用户层里的这一项删掉(等同于"未自定义")。非回环页面 DSH 将设置标记为只读,此时这九项偏好退回本地镜像,行为与 0.5.x 一致;0.5.x 留下的本地值会在宿主层首次应答时导入一次(仅当用户层为空)。宿主应答之后宿主层就是权威层,因此 `localStorage['dsh-quick-toc.debug']` 这类本地键在回环页面上不再生效——诊断开关请用卡片里那一条。
117
117
  - 幕布与停靠面板**共用同一份列表 DOM**:幕布只是把面板本体搬进一个从标签条下沿落下的整宽容器(面板在幕布态换一套几何:相对定位、宽高 100%、不套用面板缩放),容器本身常驻挂载、收起时是一个 0×0 的直通盒,所以开关幕布不会重建列表、也不会丢掉滚动位置。跨会话检索走客户端 `ctx.get("sessions")` 的 `search(query, signal)`(走宿主的会话全文索引,一次最多 20 条);每个会话的阅读位置存在浏览器本地(`dsh-quick-toc.readPos.v1`,半小时内有效)。
118
- - 插件配置卡片也按宿主二选一注册:0.1.5-rc.3 上挂 `settings.plugin.item` 槽(`key` 为插件命名空间,该槽按宿主实际提供的命名空间派发,宿主半未加载时卡片自然不出现,面板不受影响);0.1.7 两条线(alpha 与 rc)上改挂 `plugins.bundle.config` 槽(`key` 为 npm 包名,卡片出现在侧边栏「插件」页该 bundle 的页面上)。两条注册都只在设置作用域绑定成功后才做,卡片与面板共享上面那个 store,因此两边实时互通。
118
+ - 插件配置卡片按宿主实际提供的槽位注册,三处一起挂、装上哪一处由宿主决定:0.1.5-rc.3 上是 `settings.plugin.item`(`key` 为插件命名空间,该槽按宿主实际服务的命名空间派发,宿主半未加载时卡片自然不出现,面板不受影响);0.1.7 两条线(alpha 与 rc)上是侧边栏「插件」页的两处——「官方」分组里插件自己的条目(`plugins.item`,`id` 为 `quick-toc`,`label` 取「对话大纲」,`order` 50)与该 bundle 页面上的配置区(`plugins.bundle.config`,`key` 为 npm 包名)。0.1.7 的插件页渲染这两处时都会带上 `view`,卡片因此默认展开(点进条目或 bundle 页本身就是展开动作);rc 线的配置单元不带任何 props,卡片保持收拢的列表形态。所有注册都只在设置作用域绑定成功后才做,卡片与面板共享上面那个 store,因此两边实时互通。
119
119
  - 修改 `lib/client.js` 后刷新页面即可看到变化(客户端模块按内容哈希发版,DSH 的客户端 HMR 也会推送重载);改 `lib/index.js`(宿主半)需重启 DSH
120
120
 
121
121
  ## License
package/lib/client.js CHANGED
@@ -2174,6 +2174,21 @@ window.__ModuleLoader__.load({
2174
2174
  setSheetOpen(true);
2175
2175
  }
2176
2176
  };
2177
+ // The drop is armed by the class the open commit writes, so its clock starts inside
2178
+ // that commit. On a long outline the commit is a long task of its own (the whole list
2179
+ // re-laid out at the curtain's width), and an animation whose 0.46s runs while its own
2180
+ // first frame is still being laid out is over before anything is painted — the failure
2181
+ // 0.7.4 fixed for the host-driven load, and the same shape of failure when the cost is
2182
+ // ours. Re-arming it here, with the open state already in the DOM and before the
2183
+ // browser paints it, puts the layout and the animation on the same frame.
2184
+ react.useLayoutEffect(function () {
2185
+ if (!sheetOpen) return;
2186
+ var node = sheetRef.current;
2187
+ if (!node || !node.style) return;
2188
+ node.style.animation = "none";
2189
+ void node.offsetWidth; // pay the commit's layout HERE, before the drop starts
2190
+ node.style.animation = "";
2191
+ }, [sheetOpen]);
2177
2192
  // "return" only needs to exist for one painted frame: the panel renders at
2178
2193
  // its collapsed spot, and releasing it lets the left/clip transition carry
2179
2194
  // it back open. The tuck must NOT be released here — it is held until the
@@ -2399,6 +2414,75 @@ window.__ModuleLoader__.load({
2399
2414
  try { searchRef.current.focus({ preventScroll: true }); }
2400
2415
  catch (e) { searchRef.current.focus(); }
2401
2416
  }, [searchOpen]);
2417
+ // ---- the curtain's search column: its own box, and the motion that moves it --------
2418
+ // `sheetColW` is how much of the curtain's row the search column claims, and it is
2419
+ // computed HERE (not where the column is built) so the flip effect below keeps a stable
2420
+ // hook list on an empty conversation too — the panel bails out further down, and a hook
2421
+ // added after that point would change the hook count between renders (React #310).
2422
+ var searchColRef = react.useRef(null);
2423
+ var searchGeomRef = react.useRef(null);
2424
+ var sheetColW = (searchOpen || searchAnim === "out") ? SHEET_SEARCH_W : 0;
2425
+ // The column used to animate its own WIDTH, and because the outline is the flex sibling
2426
+ // that grows into the rest of the row, every frame of that re-laid out every rendered row
2427
+ // (measured on a 34-row list: a width step per frame, ~18ms frames and a 76ms first hitch;
2428
+ // the cost grows with the list, which is what made opening search on a long session feel
2429
+ // stuck). The layout now flips in ONE commit — that single reflow is unavoidable, the text
2430
+ // has to re-wrap — and the movement rides on transforms, which the compositor owns:
2431
+ // * opening: the column starts parked off the right edge and slides home, while the list
2432
+ // is flipped back to where it sat before the flip and released, so the outline still
2433
+ // travels left into place instead of jumping there;
2434
+ // * closing: the column keeps its box for the exit and slides off first (the box only
2435
+ // collapses when the exit state ends), then the list is flipped back to full width the
2436
+ // same way.
2437
+ // Both elements are measured, never derived: the flip is the previous box minus the new one.
2438
+ react.useLayoutEffect(function () {
2439
+ var list = listRef.current;
2440
+ var col = searchColRef.current;
2441
+ if (!sheetOpen || !list || !col) { searchGeomRef.current = null; return undefined; }
2442
+ // `offsetLeft` is the box's own position: layout, never a transform. A rect here would
2443
+ // read back the very transform this effect applies, and the next commit would "flip"
2444
+ // from the parked position it just created (measured: the list swung 110px past its
2445
+ // resting spot and eased back).
2446
+ var layoutLeft = list.offsetLeft;
2447
+ var colLayout = sheetColW > 0; // the layout currently on screen
2448
+ var wantOpen = !!searchOpen && searchAnim !== "out"; // "out" is the exit in flight
2449
+ var colWidth = col.getBoundingClientRect().width;
2450
+ var prev = searchGeomRef.current;
2451
+ if (prev && prev.colLayout !== colLayout) {
2452
+ // the layout flipped under the reader: put the list back where it was, then release it
2453
+ var dx = prev.layoutLeft - layoutLeft;
2454
+ if (dx !== 0) {
2455
+ list.style.transition = "none";
2456
+ list.style.transform = "translateX(" + dx + "px)";
2457
+ requestAnimationFrame(function () {
2458
+ list.style.transition = "transform 0.24s " + EASE;
2459
+ list.style.transform = "";
2460
+ setTimeout(function () { list.style.transition = ""; }, 320);
2461
+ });
2462
+ }
2463
+ }
2464
+ if (prev && prev.wantOpen !== wantOpen) {
2465
+ if (wantOpen) {
2466
+ // parked just off the row's right edge, then slid home by the transition
2467
+ col.style.transition = "none";
2468
+ col.style.transform = "translateX(" + (colWidth || SHEET_SEARCH_W) + "px)";
2469
+ requestAnimationFrame(function () {
2470
+ col.style.transition = "transform 0.24s " + EASE;
2471
+ col.style.transform = "";
2472
+ });
2473
+ } else {
2474
+ // out it goes while its box is still there; the box collapses when the exit ends
2475
+ col.style.transition = "transform 0.24s " + EASE;
2476
+ col.style.transform = "translateX(" + (colWidth || SHEET_SEARCH_W) + "px)";
2477
+ }
2478
+ }
2479
+ if (colWidth === 0) {
2480
+ col.style.transition = "none";
2481
+ col.style.transform = "";
2482
+ }
2483
+ searchGeomRef.current = { colLayout: colLayout, layoutLeft: layoutLeft, wantOpen: wantOpen };
2484
+ return undefined;
2485
+ }, [sheetOpen, sheetColW, searchOpen, searchAnim]);
2402
2486
  var onQueryChange = function (v) {
2403
2487
  setQuery(v);
2404
2488
  setMatchIdx(0);
@@ -5521,12 +5605,11 @@ window.__ModuleLoader__.load({
5521
5605
  });
5522
5606
  // ---- curtain search column: pushed in from the right edge -----------------
5523
5607
  // The panel's own search box and its two buttons, then the hits below them.
5524
- // Only the column's WIDTH animates (0 <-> 360) so the outline is pushed left
5525
- // instead of jumped; an inner box keeps the full width so the content never
5526
- // squashes while the column narrows, and the column is always mounted while the
5527
- // curtain is up so both directions animate.
5528
- var sheetColW = (searchOpen || searchAnim === "out") ? SHEET_SEARCH_W : 0;
5608
+ // The column's WIDTH is what makes room (see `sheetColW` above), an inner box keeps the
5609
+ // full width so the content never squashes while the column narrows, and the column is
5610
+ // always mounted while the curtain is up so both directions animate.
5529
5611
  var sheetSearchColEl = sheetOpen ? react_jsx_runtime.jsx("div", {
5612
+ ref: searchColRef,
5530
5613
  className: "dqt-sheet-search",
5531
5614
  "data-sheet-search": sheetColW > 0 ? "on" : undefined,
5532
5615
  style: {
@@ -5534,8 +5617,7 @@ window.__ModuleLoader__.load({
5534
5617
  width: sheetColW,
5535
5618
  overflow: "hidden",
5536
5619
  minHeight: 0,
5537
- borderLeft: sheetColW > 0 ? "1px solid " + C.panelBorder : "none",
5538
- transition: "width 0.24s " + EASE + ", border-color 0.24s " + EASE
5620
+ borderLeft: sheetColW > 0 ? "1px solid " + C.panelBorder : "none"
5539
5621
  },
5540
5622
  children: react_jsx_runtime.jsx("div", {
5541
5623
  style: { width: SHEET_SEARCH_W, height: "100%", display: "flex", flexDirection: "column", minHeight: 0 },
@@ -6963,13 +7045,17 @@ window.__ModuleLoader__.load({
6963
7045
  // live in both directions. The chrome mirrors the section's own card (radii,
6964
7046
  // borders, spacing, tokens) because a plugin bundle cannot import the section's
6965
7047
  // CSS module.
6966
- function TocSettingsCard() {
7048
+ function TocSettingsCard(props) {
6967
7049
  var prefs = usePrefs();
6968
7050
  setLanguage(prefs.lang, hostLanguage());
6969
7051
  var snap = prefsStore.host();
6970
7052
  var writable = !!snap && snap.writable === true;
6971
7053
  var user = snap && snap.user && typeof snap.user === "object" ? snap.user : {};
6972
- var openState = react.useState(false);
7054
+ // Start open where the seat already opened the section: the 0.1.7 plugin pages hand the
7055
+ // card a `view` (the reader clicked into the entry or the bundle page to get here), so
7056
+ // making them click the card's own header after that was a second expansion for nothing.
7057
+ // The rc line's configuration cell passes no props and keeps the collapsed list shape.
7058
+ var openState = react.useState(!!(props && (props.defaultOpen === true || props.view === "page")));
6973
7059
  var open = openState[0];
6974
7060
  var setOpen = openState[1];
6975
7061
  var commit = function (patch) { prefsStore.set(patch); };
@@ -7184,6 +7270,16 @@ window.__ModuleLoader__.load({
7184
7270
  });
7185
7271
  }
7186
7272
 
7273
+ // The 0.1.7 Plugins page's own entry for this plugin (`plugins.item`). That seat asks one
7274
+ // component for two views: a one-line `summary` for the entry in its Official group, and
7275
+ // the `page` body once the entry is opened. The summary answers with the card's own
7276
+ // description and every other view is the card itself, already open — the click that
7277
+ // opened the entry IS the expansion gesture.
7278
+ function TocCardPage(props) {
7279
+ if (props && props.view === "summary") return T("settings.desc");
7280
+ return react_jsx_runtime.jsx(TocSettingsCard, { defaultOpen: true });
7281
+ }
7282
+
7187
7283
  // Declared client services only. `slots` and `locale` exist on every supported line;
7188
7284
  // the settings service is looked up with `ctx.get` instead (see `pickSettingsScope`),
7189
7285
  // because declaring a name a host does not provide parks the whole package.
@@ -7396,6 +7492,21 @@ window.__ModuleLoader__.load({
7396
7492
  locale: "dsh-quick-toc"
7397
7493
  }, TocSettingsCard);
7398
7494
  });
7495
+ // 0.1.7's Plugins page lists one entry per plugin in its Official group
7496
+ // (`plugins.item`), which is where the reader looks for our settings there — the
7497
+ // earlier line's keyed configuration cell (`settings.plugin.item`) no longer exists
7498
+ // on that host, so without this seat the card had nowhere to appear. `id` names the
7499
+ // entry, `label` is its line in the group, and `order` places it after the host's
7500
+ // own pages (shell 10 / agent loop 20 / subagent 30 / web search 40).
7501
+ ctx.slots.inject("plugins.item", function () {
7502
+ return ctx.slots.register({
7503
+ name: "plugins.item",
7504
+ id: "quick-toc",
7505
+ order: 50,
7506
+ label: function () { return T("panel.title"); },
7507
+ locale: "dsh-quick-toc"
7508
+ }, TocCardPage);
7509
+ });
7399
7510
  ctx.slots.inject("settings.plugin.item", function () {
7400
7511
  return ctx.slots.register({
7401
7512
  name: "settings.plugin.item",
@@ -7444,13 +7555,14 @@ window.__ModuleLoader__.load({
7444
7555
  // The plugin-configuration card, in whichever seat the running host offers:
7445
7556
  // * up to rc.2 it is a keyed `settings.plugin.item` cell under
7446
7557
  // Settings → Plugins → Plugin configuration, keyed by the settings namespace;
7447
- // * on the alpha the plugin's own page in the sidebar's Plugins list carries it,
7448
- // as the bundle's configuration keyed by the npm package name
7449
- // (`plugins.bundle.config`). A row-level cell (`plugins.row.config`, keyed
7450
- // `<package>#<row id>`) exists too, but a registration there did not reach that
7451
- // page in 0.1.7-alpha.1 while the bundle cell did — and these preferences are the
7452
- // plugin's, not one row's, so the bundle page is the right seat anyway.
7453
- // Registering into a slot a given host never declares is inert, so both are
7558
+ // * on 0.1.7 the Plugins page's Official group lists one entry per plugin
7559
+ // (`plugins.item`) and the bundle page carries the bundle's own configuration
7560
+ // (`plugins.bundle.config`, keyed by the npm package name). A row-level cell
7561
+ // (`plugins.row.config`, keyed `<package>#<row id>`) exists too, but a
7562
+ // registration there did not reach that page in 0.1.7-alpha.1 while the bundle
7563
+ // cell did — and these preferences are the plugin's, not one row's, so the
7564
+ // bundle page is the right seat for them.
7565
+ // Registering into a slot a given host never declares is inert, so all three are
7454
7566
  // offered together, once the host layer bound (or the fallback timer fired).
7455
7567
  }
7456
7568
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quick-toc",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Conversation TOC for DeepSeek Harness (DSH): a whole-session, turn-grouped Markdown outline with hover previews, a level filter, a full-width curtain, jump-to-end buttons, title/full-text/cross-session search and a Chinese/English interface.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -38,7 +38,8 @@
38
38
  "0.1.5-rc.3": "compatible",
39
39
  "0.1.7-alpha.1": "compatible",
40
40
  "0.1.7-alpha.2": "compatible",
41
- "0.1.7-rc.1": "compatible"
41
+ "0.1.7-rc.1": "compatible",
42
+ "0.1.7-rc.2": "compatible"
42
43
  }
43
44
  }
44
45
  },