dsh-quick-toc 0.7.3 → 0.7.5

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,6 +2,24 @@
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.5] - 2026-09-24
6
+
7
+ ### Changed
8
+ - **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.
9
+ - **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.
10
+ - **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.
11
+
12
+ ### Fixed
13
+ - **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.
14
+
15
+ ## [0.7.4] - 2026-09-23
16
+
17
+ ### Fixed
18
+ - **Opening the curtain had no animation on a long session**: opening the curtain made the outline's own auto-loader pull the whole session in at once (measured: DOM nodes 1365 to 46150, single long tasks of 275-897ms), so the curtain's 0.46s drop was over before a frame of it could paint. Automatic loading now requires a real reader gesture (a wheel tick on the list, a key on the panel, a press on the list); a geometric coincidence (a short list that happens to fit, a freshly opened curtain, a resized window) is no longer treated as a load request.
19
+ - **Switching the dock made the panel disappear on one side and slide in from the other**: the switch moved `left`, which is not in the transition list, so the box teleported while only the offset was tweened. The panel now always sits on one fixed anchor, and its open, collapsed, either-dock and curtain-handoff positions are all offsets on that anchor, so a dock switch is a pure translate animation.
20
+ - **The collapsed panel came to rest left of the position it opens from (8px on the left dock)**: the collapse clip was a fixed panel width (plus 8px on the left dock) while the box actually travelled a panel width plus 16px, so the visible edge drifted. The clip now matches the travel, which keeps the visible edge on the panel's own open edge line all the way out, the same on both docks.
21
+ - **A collapse occasionally came to rest in the wrong place**: the host re-measures the conversation area while the collapse is running, which moves the line the panel aims at (measured: the tuck-in landed at 1396 instead of 1526). The open-to-closed step now freezes the geometry that collapse started with; the next open reads the area afresh.
22
+
5
23
  ## [0.7.3] - 2026-09-23
6
24
 
7
25
  ### Changed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  **dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.7.5] - 2026-09-24
6
+
7
+ ### 变更
8
+ - **支持 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` 随之增加该版本。
9
+ - **声明的兼容下限收到 0.1.5-rc.3**:更早的 0.1.5-rc.1、0.1.5-rc.2 不再声明(`engines.dsh` 与两条 peer 区间同步收紧),README 兼容表跟上。
10
+ - **插件自带的 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 会让宿主直接启动失败)。
11
+
12
+ ### 修复
13
+ - **设置作用域的绑定兜底**:宿主设置文档在插件加载时可能仍在载入(0.1.7-rc.1 实测该表单的 `status` 为 `loading`),原先只认一次「就绪」快照;现在会退到第一个存在的候选表单,由订阅在文档到位后接续。
14
+
15
+ ## [0.7.4] - 2026-09-23
16
+
17
+ ### 修复
18
+ - **长会话里打开幕布没有展开动画**:打开幕布时,本插件自己的自动加载把整段会话一次性拉了进来(实测 DOM 节点 1365 → 46150、单帧长任务 275–897ms),0.46 秒的展开动画在第一帧画出来之前就播完了。自动加载现在必须由读者的真实操作触发(列表上的滚轮、面板上的按键、在列表上按下),几何上的巧合(短列表恰好装得下、刚打开幕布、窗口改变尺寸)不再被当成加载请求。
19
+ - **切换停靠边时面板先消失、再从另一边滑出来**:切换原本移动 `left`,而 `left` 不在过渡列表里——盒子瞬移,只有位移在补间。面板现在始终停在一个固定锚点上,展开、收起、两个停靠边、交给幕布这四个位置都是锚点上的位移,切换停靠边是一段纯位移动画。
20
+ - **收起后停的位置比展开位置偏左(左停靠实测 8px)**:收起的裁剪量原本是面板宽度(左停靠另加 8px),与盒子真正滑动的距离(左停靠为面板宽度 + 16px)不相等,可见边一路漂移。裁剪量改为与实际滑动距离一致后,可见边全程钉在面板自己那条开合线上,左右停靠对称。
21
+ - **收起时偶尔收偏**:宿主会在收起动画途中重新测量会话区,目标线随之改变(实测收进去的位置从 1526 跑到 1396)。收起开始的那一步现在冻结本次动画使用的几何,之后再展开才读取新几何。
22
+
5
23
  ## [0.7.3] - 2026-09-23
6
24
 
7
25
  ### 变更
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.3) | 0.1.5-rc.1, 0.1.5-rc.2, 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2 |
45
+ | **0.7.x** (latest, 0.7.5) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1 |
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 |
@@ -51,7 +51,7 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
51
51
 
52
52
  Each major line lists only its newest patch (the defects a new feature introduces are fixed in the patches that follow, so within one major line the newest patch is the one to use; older patches keep working — the plugin does not break existing interfaces).
53
53
 
54
- `engines.dsh` is declared as **`>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**, with the peer ranges shaped the same way (a prerelease must be given an explicit branch: node-semver admits one only when some comparator in the range sits on that version's exact `major.minor.patch` tuple and carries a prerelease tag of its own, so a plain `>=0.1.5-rc.1` never matches `0.1.7-alpha.1`). 0.7.3 was tested on **0.1.5-rc.3** and **0.1.7-alpha.2** (the panel and the curtain, row jumps and older-history loading, the title/full-text/cross-session searches, switching sessions from a cross-session hit, the configuration card and the host settings document's reads and writes were each exercised), and neither release needed a code change — checked offline as well: rc.3 differs from rc.2 only in each package's `package.json` version (plus two sidebar files), and alpha.2's changes sit in the session controller and the chat renderer while every interface and node shape this plugin uses is still there (`turnOutline`, `turn-error`, `useChat`, `data-chat-anchor-key`, `sessions.search`, `loadThrough`, `settings.*` and both card slots; the two extra `data-conversation-scroll` hits in alpha.2 are stylesheet text, not a second scroll container). 0.7.2 was tested on **0.1.5-rc.2** (the curtain scale was confirmed point by point: at three factors the curtain box keeps its width, height and position while only the content scales, both slider ends, the write-on-release behaviour and the default rendering were exercised, and the two scales were confirmed independent); 0.7.1 was tested on **0.1.5-rc.2** (the "back to where I left" floating button, the leave positions the panel and the curtain record separately, the row being read landing at the top when a surface opens, and the absence of the button after closing at the bottom were each confirmed); 0.7.0 was tested on the same release (the full-width curtain and its droplet handle, the jump-to-section-end buttons, cross-session search, keyboard navigation, the questions-only view and the remembered reading position were each exercised in a real browser; the panel scale, a dragged size reaching storage and coming back, and the collapsed handle's position had been measured before that); on **0.1.5-rc.1** the plugin's loading and client-module delivery were tested for real (the host boots without errors, `--dump-config` carries the quick-toc entry, the combined client bundle URL lists `dsh-quick-toc/client.js`, and the bytes it serves contain this release's curtain handle, jump-to-section-end and reading-position markers). The host interfaces it relies on — the `turnOutline` projection, the session-scoped `useProjection`, the client `sessions` service and its `loadThrough` jump loader (since 0.5.0), the `turn-error` node used for failed turns (since 0.5.1), and the host `settings` service (`installSection`), the client `settingsScope` and the `settings.plugin.item` card slot added in 0.6.0, and the client `sessions.search` that 0.7.0's cross-session search calls — were checked one by one against the installed 0.1.5-rc.1 and 0.1.5-rc.2 bundles, whose code is byte-identical between the two releases (`dsh-client-ui-chat` differs by a single CSS declaration unrelated to this plugin, and the package that carries `search`, `dsh-api-session-controller`, is byte-identical down to the same limits: at most 20 hits per search, 240 code points per snippet), so both releases are claimed. Older DSH versions are unverified and therefore not claimed; on an older DSH the newest usable plugin version is **0.3.2**. **0.1.7-alpha.1 passes too**, in a real instance inside an isolated `DSH_HOME`: the host boots without errors, and the panel and curtain, row jumps and older-history loading, the title/full-text/cross-session searches and switching sessions from a cross-session hit were each exercised in a real browser, with the settings document's reads and writes confirmed as well. To that end 0.7.2 speaks both host lines — on 0.1.5-rc.x it uses the host's `settings.installSection`, the client `settingsScope` and the `settings.plugin.item` card slot; on 0.1.7-alpha.x it uses `settings.configure`, the client `configForms` and the `plugins.bundle.config` card slot, and switches sessions through `uiWorkspace.openSession`. The plugin picks whichever exists at runtime, and **declares no settings service in `inject` at all** (declaring one a host does not provide parks the whole package, which on 0.1.7-alpha.1 stopped the entire web boot from finishing). On install or update, the market (dshmarket) reads `engines.dsh` (or `dsh.engines.dsh`) and the `@deepseek-ai/dsh-*` peer ranges present in the local install from the npm `latest` manifest, shows the host requirement a plugin declares on its card, and offers an optional filter that hides only entries confirmed to mismatch the running host; `dsh.compatibility.dshReleases` is this repository's own record, read by neither DSH nor the market.
54
+ `engines.dsh` is declared as **`>=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**, with the peer ranges shaped the same way (a prerelease must be given an explicit branch: node-semver admits one only when some comparator in the range sits on that version's exact `major.minor.patch` tuple and carries a prerelease tag of its own, so a plain `>=0.1.5-rc.1` never matches `0.1.7-alpha.1`). 0.7.3 was tested on **0.1.5-rc.3** and **0.1.7-alpha.2** (the panel and the curtain, row jumps and older-history loading, the title/full-text/cross-session searches, switching sessions from a cross-session hit, the configuration card and the host settings document's reads and writes were each exercised), and neither release needed a code change — checked offline as well: rc.3 differs from rc.2 only in each package's `package.json` version (plus two sidebar files), and alpha.2's changes sit in the session controller and the chat renderer while every interface and node shape this plugin uses is still there (`turnOutline`, `turn-error`, `useChat`, `data-chat-anchor-key`, `sessions.search`, `loadThrough`, `settings.*` and both card slots; the two extra `data-conversation-scroll` hits in alpha.2 are stylesheet text, not a second scroll container). 0.7.2 was tested on **0.1.5-rc.2** (the curtain scale was confirmed point by point: at three factors the curtain box keeps its width, height and position while only the content scales, both slider ends, the write-on-release behaviour and the default rendering were exercised, and the two scales were confirmed independent); 0.7.1 was tested on **0.1.5-rc.2** (the "back to where I left" floating button, the leave positions the panel and the curtain record separately, the row being read landing at the top when a surface opens, and the absence of the button after closing at the bottom were each confirmed); 0.7.0 was tested on the same release (the full-width curtain and its droplet handle, the jump-to-section-end buttons, cross-session search, keyboard navigation, the questions-only view and the remembered reading position were each exercised in a real browser; the panel scale, a dragged size reaching storage and coming back, and the collapsed handle's position had been measured before that); on **0.1.5-rc.1** the plugin's loading and client-module delivery were tested for real (the host boots without errors, `--dump-config` carries the quick-toc entry, the combined client bundle URL lists `dsh-quick-toc/client.js`, and the bytes it serves contain this release's curtain handle, jump-to-section-end and reading-position markers). The host interfaces it relies on — the `turnOutline` projection, the session-scoped `useProjection`, the client `sessions` service and its `loadThrough` jump loader (since 0.5.0), the `turn-error` node used for failed turns (since 0.5.1), and the host `settings` service (`installSection`), the client `settingsScope` and the `settings.plugin.item` card slot added in 0.6.0, and the client `sessions.search` that 0.7.0's cross-session search calls — were checked one by one against the installed 0.1.5-rc.1 and 0.1.5-rc.2 bundles, whose code is byte-identical between the two releases (`dsh-client-ui-chat` differs by a single CSS declaration unrelated to this plugin, and the package that carries `search`, `dsh-api-session-controller`, is byte-identical down to the same limits: at most 20 hits per search, 240 code points per snippet), so both releases are claimed. Older DSH versions are unverified and therefore not claimed; on an older DSH the newest usable plugin version is **0.3.2**. **0.1.7-alpha.1 passes too**, in a real instance inside an isolated `DSH_HOME`: the host boots without errors, and the panel and curtain, row jumps and older-history loading, the title/full-text/cross-session searches and switching sessions from a cross-session hit were each exercised in a real browser, with the settings document's reads and writes confirmed as well. To that end 0.7.2 speaks both host lines — on 0.1.5-rc.x it uses the host's `settings.installSection`, the client `settingsScope` and the `settings.plugin.item` card slot; on 0.1.7-alpha.x it uses `settings.configure`, the client `configForms` and the `plugins.bundle.config` card slot, and switches sessions through `uiWorkspace.openSession`. The plugin picks whichever exists at runtime, and **declares no settings service in `inject` at all** (declaring one a host does not provide parks the whole package, which on 0.1.7-alpha.1 stopped the entire web boot from finishing). On install or update, the market (dshmarket) reads `engines.dsh` (or `dsh.engines.dsh`) and the `@deepseek-ai/dsh-*` peer ranges present in the local install from the npm `latest` manifest, shows the host requirement a plugin declares on its card, and offers an optional filter that hides only entries confirmed to mismatch the running host; `dsh.compatibility.dshReleases` is this repository's own record, read by neither DSH nor the market. **0.1.7-rc.1 passes as well** (the 0.1.7 line on the rc channel: `settingsScope`, `settings.plugin.item`, `sessions.open` and `installSection` are all gone, and it carries the same `configForms` / `plugins.bundle.config` / `uiWorkspace` / `settings.configure` seams as 0.1.7-alpha.x — their interface definitions compare byte for byte): in an isolated `DSH_HOME` sandbox the host booted with no error, and on a real 13k-node session the panel, the curtain, Esc, the dock switch (left 288 to right 1238, transform only) and the collapse (parking at 1526, handle back in place) were all exercised with no browser console error. `engines.dsh`'s floor moved up to **0.1.5-rc.3** accordingly (the older rc.1/rc.2 are no longer declared).
55
55
 
56
56
  ## Install
57
57
 
@@ -113,9 +113,9 @@ A missing `turnOutline` means the host has no such projection, and the staged `j
113
113
  - The "unloaded turn" capability needs two host facilities: the `turnOutline` projection (the whole-session turn index, each entry carrying its `turn/start` seq) and the turn-jump loader (the client `sessions` service's `binding(sessionId).session.loadThrough(seq)`, reached through the client ctx's declaration-free `ctx.get("sessions")` lookup). Each degrades on its own: without the projection the outline lists loaded turns only, without the loader unloaded entries are shown but not jumped to, and nothing else in the panel is affected.
114
114
  - The failure row reads the host's `turn-error` conversation node (published when `turn/end` carries an `error` reason, with `message` and an optional `code`); on a host without that node the row is simply absent and nothing else changes.
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
- - 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.x it is `ctx.settingsScope.bind({ namespace: "dsh-quick-toc" })` (addressed by namespace), while on 0.1.7-alpha.x 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.
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.x 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 0.1.7-alpha.x 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 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.
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.3) | 0.1.5-rc.1、0.1.5-rc.2、0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2 |
45
+ | **0.7.x**(最新,0.7.5) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1 |
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 |
@@ -51,7 +51,7 @@
51
51
 
52
52
  每个大版本只列该系列最新的一个补丁版本(新功能引入的缺陷都在其后的补丁里修掉了,所以同一个大版本内直接用最新补丁即可;旧补丁仍可继续用,插件不破坏既有接口)。
53
53
 
54
- `engines.dsh` 声明为 **`>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**,peer 范围同形(预发布版本必须显式留分支:node-semver 只在一个范围的某个比较符与该版本落在同一个 `major.minor.patch` 元组、且自身带预发布标签时才放行它,所以写成 `>=0.1.5-rc.1` 是匹配不到 `0.1.7-alpha.1` 的)。0.7.3 在 **0.1.5-rc.3** 与 **0.1.7-alpha.2** 上实测通过(面板与幕布、点行跳转与翻历史、标题/全文/跨会话三档检索、跨会话命中切换会话、设置卡片与宿主设置文档的读写都逐项跑过),这两版不需要任何代码改动——离线核对:rc.3 相对 rc.2 只有各包 `package.json` 的版本号差异(外加侧边栏两个文件),alpha.2 的改动集中在会话控制器与聊天渲染,但插件依赖的接口与节点形状逐项核对后仍在(`turnOutline`、`turn-error`、`useChat`、`data-chat-anchor-key`、`sessions.search`、`loadThrough`、`settings.*` 与两张卡片槽位都在,`data-conversation-scroll` 在 alpha.2 里新增的两处命中是样式表文本、不是第二个滚动容器)。0.7.2 在 **0.1.5-rc.2** 上实测通过(幕布缩放逐项确认:三档因子下幕布盒子的宽高与位置不变、只有内容按比例缩放,50%–200% 两端、松手写入、以及默认值渲染都比对过;面板与幕布两套缩放各自独立);0.7.1 在 **0.1.5-rc.2** 上实测通过(「回到我离开的位置」浮动按钮、面板与幕布分别记录的离开位置、打开时落在正在阅读的那一行、以及在底部关闭再打开不出按钮都逐项确认);0.7.0 在同一版本上实测通过(全屏幕布与液滴把手、跳到本节末尾、跨会话检索、键盘导航、只看提问、记住阅读位置都在真实浏览器里逐项跑过;面板缩放、尺寸的落盘与读回、收起把手的位置等既有能力此前已实测);**0.1.5-rc.1** 上实测了插件加载与客户端模块下发(宿主启动无报错、`--dump-config` 里有 quick-toc 条目、客户端组合包 URL 里列出 `dsh-quick-toc/client.js`,取回的下发内容包含本版新增的幕布把手、跳到本节末尾与阅读位置等标记),它依赖的宿主接口——`turnOutline` 投影、会话级槽位的 `useProjection`、客户端 `sessions` 服务与 `loadThrough` 跳转加载器(0.5.0 起)、失败回合所用的 `turn-error` 节点(0.5.1 起)、以及 0.6.0 新增的宿主 `settings` 服务(`installSection`)、客户端 `settingsScope` 与 `settings.plugin.item` 配置卡片槽位、以及 0.7.0 跨会话检索所用的客户端 `sessions.search`——在 0.1.5-rc.1 与 0.1.5-rc.2 的安装包中逐个核对存在(这些宿主包的代码在两版之间逐字节相同,`dsh-client-ui-chat` 仅差一条与本插件无关的 CSS 声明;`search` 所在的 `dsh-api-session-controller` 两版逐字节相同,含「一次最多 20 条、片段上限 240 字符」的同一套上限),因此两版都声明兼容。更早的 DSH 版本未经验证,不作声明;在更早的 DSH 上可安装的最新插件版本是 **0.3.2**。**0.1.7-alpha.1 上也实测通过**(隔离的 `DSH_HOME` 里跑真实实例):宿主启动无报错、面板与幕布、点行跳转与翻历史、标题/全文/跨会话三档检索、跨会话命中切换会话,都在真实浏览器里逐项跑过,设置文档的读写也逐项确认。为此 0.7.2 同时支持两条宿主线——0.1.5-rc.x 上是宿主的 `settings.installSection` + 客户端 `settingsScope` + `settings.plugin.item` 卡片槽位,0.1.7-alpha.x 上换成 `settings.configure` + 客户端 `configForms` + `plugins.bundle.config` 卡片槽位、并用 `uiWorkspace.openSession` 切换会话;插件在运行时按服务是否存在二选一,且**不再把任一设置服务写进 `inject`**(声明一个宿主没有的服务会让整个包停在 pending,0.1.7-alpha.1 上这曾让整页 Web 起不来)。安装或更新时,市场(dshmarket)读取 npm 上 `latest` 版本的 `engines.dsh`(或 `dsh.engines.dsh`)与本机存在的 `@deepseek-ai/dsh-*` peer 范围,在卡片上显示该插件声明的宿主要求,并提供「只显示与当前宿主确认匹配的条目」这一可选过滤;`dsh.compatibility.dshReleases` 是本仓库自己的记录,DSH 与市场都不读取。
54
+ `engines.dsh` 声明为 **`>=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**,peer 范围同形(预发布版本必须显式留分支:node-semver 只在一个范围的某个比较符与该版本落在同一个 `major.minor.patch` 元组、且自身带预发布标签时才放行它,所以写成 `>=0.1.5-rc.1` 是匹配不到 `0.1.7-alpha.1` 的)。0.7.3 在 **0.1.5-rc.3** 与 **0.1.7-alpha.2** 上实测通过(面板与幕布、点行跳转与翻历史、标题/全文/跨会话三档检索、跨会话命中切换会话、设置卡片与宿主设置文档的读写都逐项跑过),这两版不需要任何代码改动——离线核对:rc.3 相对 rc.2 只有各包 `package.json` 的版本号差异(外加侧边栏两个文件),alpha.2 的改动集中在会话控制器与聊天渲染,但插件依赖的接口与节点形状逐项核对后仍在(`turnOutline`、`turn-error`、`useChat`、`data-chat-anchor-key`、`sessions.search`、`loadThrough`、`settings.*` 与两张卡片槽位都在,`data-conversation-scroll` 在 alpha.2 里新增的两处命中是样式表文本、不是第二个滚动容器)。0.7.2 在 **0.1.5-rc.2** 上实测通过(幕布缩放逐项确认:三档因子下幕布盒子的宽高与位置不变、只有内容按比例缩放,50%–200% 两端、松手写入、以及默认值渲染都比对过;面板与幕布两套缩放各自独立);0.7.1 在 **0.1.5-rc.2** 上实测通过(「回到我离开的位置」浮动按钮、面板与幕布分别记录的离开位置、打开时落在正在阅读的那一行、以及在底部关闭再打开不出按钮都逐项确认);0.7.0 在同一版本上实测通过(全屏幕布与液滴把手、跳到本节末尾、跨会话检索、键盘导航、只看提问、记住阅读位置都在真实浏览器里逐项跑过;面板缩放、尺寸的落盘与读回、收起把手的位置等既有能力此前已实测);**0.1.5-rc.1** 上实测了插件加载与客户端模块下发(宿主启动无报错、`--dump-config` 里有 quick-toc 条目、客户端组合包 URL 里列出 `dsh-quick-toc/client.js`,取回的下发内容包含本版新增的幕布把手、跳到本节末尾与阅读位置等标记),它依赖的宿主接口——`turnOutline` 投影、会话级槽位的 `useProjection`、客户端 `sessions` 服务与 `loadThrough` 跳转加载器(0.5.0 起)、失败回合所用的 `turn-error` 节点(0.5.1 起)、以及 0.6.0 新增的宿主 `settings` 服务(`installSection`)、客户端 `settingsScope` 与 `settings.plugin.item` 配置卡片槽位、以及 0.7.0 跨会话检索所用的客户端 `sessions.search`——在 0.1.5-rc.1 与 0.1.5-rc.2 的安装包中逐个核对存在(这些宿主包的代码在两版之间逐字节相同,`dsh-client-ui-chat` 仅差一条与本插件无关的 CSS 声明;`search` 所在的 `dsh-api-session-controller` 两版逐字节相同,含「一次最多 20 条、片段上限 240 字符」的同一套上限),因此两版都声明兼容。更早的 DSH 版本未经验证,不作声明;在更早的 DSH 上可安装的最新插件版本是 **0.3.2**。**0.1.7-alpha.1 上也实测通过**(隔离的 `DSH_HOME` 里跑真实实例):宿主启动无报错、面板与幕布、点行跳转与翻历史、标题/全文/跨会话三档检索、跨会话命中切换会话,都在真实浏览器里逐项跑过,设置文档的读写也逐项确认。**0.1.7-rc.1 上也实测通过**(rc 通道上的 0.1.7 线:`settingsScope` / `settings.plugin.item` / `sessions.open` / `installSection` 都已不存在,与 0.1.7-alpha.x 是同一套 `configForms` / `plugins.bundle.config` / `uiWorkspace` / `settings.configure`,接口定义逐字比对相同):隔离 `DSH_HOME` 沙盒里宿主启动无报错,在 1.3 万节点的真实会话上面板展开、幕布、Esc 收起、停靠切换(左 288 → 右 1238,纯位移)与收起(停在 1526、把手回到原位)逐项跑过,浏览器 console 无报错;`engines.dsh` 的最小版本随之收到 **0.1.5-rc.3**(更早的 rc.1/rc.2 不再声明)。为此 0.7.2 同时支持两条宿主线——0.1.5-rc.x 上是宿主的 `settings.installSection` + 客户端 `settingsScope` + `settings.plugin.item` 卡片槽位,0.1.7-alpha.x 上换成 `settings.configure` + 客户端 `configForms` + `plugins.bundle.config` 卡片槽位、并用 `uiWorkspace.openSession` 切换会话;插件在运行时按服务是否存在二选一,且**不再把任一设置服务写进 `inject`**(声明一个宿主没有的服务会让整个包停在 pending,0.1.7-alpha.1 上这曾让整页 Web 起不来)。安装或更新时,市场(dshmarket)读取 npm 上 `latest` 版本的 `engines.dsh`(或 `dsh.engines.dsh`)与本机存在的 `@deepseek-ai/dsh-*` peer 范围,在卡片上显示该插件声明的宿主要求,并提供「只显示与当前宿主确认匹配的条目」这一可选过滤;`dsh.compatibility.dshReleases` 是本仓库自己的记录,DSH 与市场都不读取。
55
55
 
56
56
  ## 安装
57
57
 
@@ -113,9 +113,9 @@ dsh plugin --profile web add <插件目录路径>
113
113
  - 「未加载回合」能力依赖宿主的两样东西:`turnOutline` 投影(整段会话的回合索引,每条含 `turn/start` 的 seq)与会话跳转加载器(客户端 `sessions` 服务的 `binding(sessionId).session.loadThrough(seq)`,通过客户端 ctx 的 `ctx.get("sessions")` 免声明查找取得)。两者各自独立降级:没有投影时只列已加载回合,没有加载器时未加载条目只展示、不跳转,面板其余功能不受影响。
114
114
  - 失败回合的报错行读的是宿主的 `turn-error` 会话节点(宿主在 `turn/end` 的原因为 `error` 时发布,含 `message` 与可选的 `code`);宿主不提供该节点时只是不显示这一行,其余功能不受影响。
115
115
  - 界面文字来自 `lib/client.js` 顶部的一张字符串表(`DICTS`,中英各一份),语言设置决定用哪一份:`跟随宿主` 时优先问宿主的翻译函数(`ctx.locale.bind("dsh-quick-toc")`,注册的表就来自 `DICTS`),拿不到才回退到内置表。中英两份表的键必须对齐(仅 `time.beforeYesterday`(前天)是中文独有——英文对更早的时间直接用日期;`time.yesterday` 两语言都有,英文作 `yesterday`)。
116
- - 偏好存储分两层,由一个模块级 store 统一读出:宿主设置文档(语言/停靠边/层级/缩放/把手位置/记住阅读位置/模糊/悬停/诊断九项,回环页面上是权威层)与 `localStorage`(镜像 + 顶边距/宽度/高度三项的正式存储——它们描述"这一块屏幕",按浏览器保存)。**宿主那一层按运行中的宿主二选一**:0.1.5-rc.x 上是 `ctx.settingsScope.bind({ namespace: "dsh-quick-toc" })`(按命名空间寻址),0.1.7-alpha.x 上改成 `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']` 这类本地键在回环页面上不再生效——诊断开关请用卡片里那一条。
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.x 上挂 `settings.plugin.item` 槽(`key` 为插件命名空间,该槽按宿主实际提供的命名空间派发,宿主半未加载时卡片自然不出现,面板不受影响);0.1.7-alpha.x 上改挂 `plugins.bundle.config` 槽(`key` 为 npm 包名,卡片出现在侧边栏「插件」页该 bundle 的页面上)。两条注册都只在设置作用域绑定成功后才做,卡片与面板共享上面那个 store,因此两边实时互通。
118
+ - 插件配置卡片也按宿主二选一注册:0.1.5-rc.3 上挂 `settings.plugin.item` 槽(`key` 为插件命名空间,该槽按宿主实际提供的命名空间派发,宿主半未加载时卡片自然不出现,面板不受影响);0.1.7 两条线(alpha 与 rc)上改挂 `plugins.bundle.config` 槽(`key` 为 npm 包名,卡片出现在侧边栏「插件」页该 bundle 的页面上)。两条注册都只在设置作用域绑定成功后才做,卡片与面板共享上面那个 store,因此两边实时互通。
119
119
  - 修改 `lib/client.js` 后刷新页面即可看到变化(客户端模块按内容哈希发版,DSH 的客户端 HMR 也会推送重载);改 `lib/index.js`(宿主半)需重启 DSH
120
120
 
121
121
  ## License
package/cordis.patch.yml CHANGED
@@ -1,4 +1,12 @@
1
1
  # dsh-quick-toc bundle patch: one loader row.
2
+ #
3
+ # The row id is the settings key from DSH 0.1.7-rc.1 on: that line imports the
4
+ # sections of the removed settings.yaml by matching each section against the
5
+ # entry of the same name, and this plugin's host half registered the namespace
6
+ # `dsh-quick-toc` (its package name). Keeping the id equal to that namespace is
7
+ # what carries an existing installation's saved preferences over; naming it
8
+ # `quick-toc` would leave them behind. On the 0.1.5-rc lines the id is only a
9
+ # loader key, so the same value is harmless there.
2
10
  - insert:
3
- - id: quick-toc
11
+ - id: dsh-quick-toc
4
12
  name: dsh-quick-toc
package/lib/client.js CHANGED
@@ -51,6 +51,10 @@ window.__ModuleLoader__.load({
51
51
  // panel's own collapse duration (0.28s) plus a hair, so the curtain's drop only
52
52
  // starts once the panel has actually finished travelling.
53
53
  var PANEL_TUCK_MS = 300;
54
+ // How long a collapse keeps the edge it started from (see the geometry block): the
55
+ // panel's collapse takes 0.28s, and the host can re-measure the conversation area at
56
+ // any point while it runs, so the freeze outlives the motion by a frame or two.
57
+ var COLLAPSE_HOLD_MS = 420;
54
58
  // The row-end control ("jump to the end of this section") lives in the last 24px
55
59
  // of a row (a 20px button inset 4px). Every box a row can paint — the hover
56
60
  // highlight, a group header's hover label, the right-aligned context text — stops
@@ -1719,8 +1723,16 @@ window.__ModuleLoader__.load({
1719
1723
  var autoWatchRef = react.useRef(null); // did the pull we started actually land? (fallback click)
1720
1724
  var autoProbeRef = react.useRef(0); // throttle for the on-scroll approach probe
1721
1725
  var autoStampRef = react.useRef(0);
1726
+ var autoGestureRef = react.useRef(0); // last REAL reader gesture on the outline list
1722
1727
  var autoLoadRef = react.useRef(true);
1723
1728
  var panelVisibleRef = react.useRef(true);
1729
+ // A collapse keeps the geometry it started with: the host re-measures the
1730
+ // conversation area while our panel is sliding away, and reading the new numbers
1731
+ // live moves the line the panel tucks into — it then settles somewhere other than
1732
+ // the line it slid out from. While a collapse runs, the edge it aims at is frozen.
1733
+ var geoOpenRef = react.useRef(null);
1734
+ var geoHoldRef = react.useRef(0);
1735
+ var geoSnapRef = react.useRef(null);
1724
1736
  // The reader's place in the outline: sampled by the scroll handler and right
1725
1737
  // before any change of our own, restored in a LAYOUT effect (see the paging
1726
1738
  // block). `anchorHintRef` remembers where the visible boundary was, so finding it
@@ -2586,6 +2598,15 @@ window.__ModuleLoader__.load({
2586
2598
  var AUTO_WAIT_MS = 40; // beat while a page is in flight: the host is the limit
2587
2599
  var AUTO_BURST_MAX = 60; // runaway guard only: a long gap must stay walkable
2588
2600
  var AUTO_BURST_IDLE_MS = 3000; // quiet this long and the budget is fresh again
2601
+ // Loading is something the READER asks for, so every automatic pull must be
2602
+ // answered by a real gesture (a wheel tick, a press, a key) within this window.
2603
+ // Purely geometric triggers — a freshly laid out list whose few rows all fit the
2604
+ // box, a panel/curtain opened, a window resized — look exactly like "an unloaded
2605
+ // turn is in view" without anyone having asked: opening the curtain once made this
2606
+ // loader call `loadThrough` on the first ghost it found, which materialised the
2607
+ // rest of the session (46k nodes, 300–900ms long tasks) before the curtain's own
2608
+ // 0.46s drop animation could paint. A gesture-gated loader cannot do that.
2609
+ var AUTO_GESTURE_MS = 1500;
2589
2610
  var ghostInView = function (el) {
2590
2611
  var ghosts = el.querySelectorAll("[data-ghost-turn]");
2591
2612
  if (ghosts.length === 0) return null;
@@ -2728,6 +2749,10 @@ window.__ModuleLoader__.load({
2728
2749
  return true;
2729
2750
  }
2730
2751
  if (historyExhausted(face)) return false;
2752
+ // Nothing the reader did — no wheel, no press, no key — since the window above:
2753
+ // a geometric coincidence (a fresh list that happens to fit, a newly opened
2754
+ // curtain, a resized window) is not a request to load history.
2755
+ if (Date.now() - autoGestureRef.current > AUTO_GESTURE_MS) return false;
2731
2756
  // A ghost the reader can actually see, or the gap coming up on them.
2732
2757
  if (ghostTurn !== null || frontierApproaching(el)) {
2733
2758
  // ARRIVED at an unloaded turn: cover it in one host call (`loadThrough`), which is
@@ -2815,6 +2840,7 @@ window.__ModuleLoader__.load({
2815
2840
  // always refreshes older turns, even without a visible scrollbar
2816
2841
  var onListWheel = function (e) {
2817
2842
  outlineTouchRef.current = Date.now();
2843
+ autoGestureRef.current = Date.now(); // a wheel tick is a reader gesture
2818
2844
  var el = listRef.current;
2819
2845
  if (!el) return;
2820
2846
  hoverEnd();
@@ -3964,9 +3990,23 @@ window.__ModuleLoader__.load({
3964
3990
  // ---- nothing to show without headings ----
3965
3991
  if (groups.length === 0) return null;
3966
3992
 
3967
- // ---- geometry: always position by `left` (px) so dock-switch & slide share one animated transition
3968
- // (The scale preference leaves all of this alone: it acts on the panel's inner
3969
- // box, so the panel itself keeps the width and height the reader dragged.)
3993
+ // ---- geometry: ONE parked anchor; the dock side lives in the transform --------------
3994
+ // The box is parked at a single spot — off-area to the LEFT of the conversation — and
3995
+ // every position the panel takes (open, collapsed on either dock, handed to the
3996
+ // curtain) is a `transform` offset from there. Two reasons:
3997
+ // * a dock switch then animates the TRANSFORM alone, so the panel travels across the
3998
+ // screen on the compositor. Docking used to move `left` (which is not in the
3999
+ // transition list): the box teleported to the other side while only the transform
4000
+ // animated, so the panel vanished on one side and slid in from the other.
4001
+ // * `left` must never be animated: it re-lays out the whole panel — header, toolbar
4002
+ // and every rendered row — on every frame, and with a long session loaded that
4003
+ // layout cannot keep up (the wipe ran while the slide stalled, and the panel
4004
+ // flickered). Adding `left` to the transition for a dock switch was measured as
4005
+ // "very laggy and flashing" for exactly this reason.
4006
+ // The COLLAPSED spot is per dock (left: the anchor itself; right: one panel width past
4007
+ // the open position), which is what makes the collapse fly out toward its own edge.
4008
+ // (The scale preference leaves all of this alone: it acts on the panel's inner box,
4009
+ // so the panel itself keeps the width and height the reader dragged.)
3970
4010
  var dockRight = dock === "right";
3971
4011
  // Keep the OPEN panel inside the window whatever the conversation area does.
3972
4012
  // The area is the host's to move: its transcript width grips rewrite the
@@ -3980,27 +4020,48 @@ window.__ModuleLoader__.load({
3980
4020
  return Math.max(8, Math.min(Math.max(8, window.innerWidth - panelW - 8), x));
3981
4021
  };
3982
4022
  // While the panel is being handed over to (or back from) the curtain it is
3983
- // rendered at its COLLAPSED geometry, so the same left/clip transition that
4023
+ // rendered at its COLLAPSED geometry, so the same transform/clip transition that
3984
4024
  // serves open/collapse also serves the handoff: no teleport, no flash.
3985
4025
  var geoOpen = open && !panelAside;
3986
- // The box is always PARKED at its collapsed spot and the open state is a transform
3987
- // offset on top of it (see panelTransition): same visual geometry as before, but the
3988
- // slide is compositable. `openLeft` is the clamped open position, `parkedLeft` the
3989
- // off-area spot the collapsed panel occupies.
3990
- var openLeft, parkedLeft;
4026
+ var anchorLeft = viewport ? viewport.left - panelW - 8 : -(panelW + 48);
4027
+ var openLeft;
3991
4028
  if (dockRight) {
3992
4029
  // RIGHT dock: keep the panel's VISIBLE right edge pinned at the SAME
3993
4030
  // line when collapsed as when expanded (the expand edge is correct).
3994
4031
  var openRight = window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW;
3995
4032
  openLeft = clampLeft(openRight);
3996
- parkedLeft = openRight + panelW;
3997
4033
  } else {
3998
4034
  // LEFT dock open (left dock is the confirmed-correct reference — untouched)
3999
4035
  openLeft = clampLeft(viewport ? viewport.left + 8 : 8);
4000
- parkedLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
4001
4036
  }
4002
- var panelLeft = parkedLeft;
4003
- var panelOffset = geoOpen ? (openLeft - parkedLeft) : 0;
4037
+ // A collapse finishes at the line it started from. The area we park against can be
4038
+ // re-measured while the panel is still sliding away — the host rewrites it when the
4039
+ // transcript lays out, and the window itself can change — and aiming at the new
4040
+ // numbers mid-flight lands the panel somewhere other than the edge it came out of
4041
+ // (measured: with the area re-measured during the collapse, the last visible strip
4042
+ // sat at 1396 instead of the 1526 line the panel had slid out from). So the
4043
+ // open -> closed step freezes the edge the collapse heads for, and the next open —
4044
+ // which reads the area afresh — picks up the new geometry. A dock switch cancels the
4045
+ // freeze: that is the reader asking for the other side, not a re-measure. The first
4046
+ // render arms nothing: a panel that was never open has no edge to hold, and pinning
4047
+ // the pre-measurement fallback would leave the closed box where no such panel goes.
4048
+ if (geoOpenRef.current === null) {
4049
+ geoOpenRef.current = geoOpen;
4050
+ } else if (geoOpenRef.current !== geoOpen) {
4051
+ geoOpenRef.current = geoOpen;
4052
+ geoHoldRef.current = geoOpen ? 0 : Date.now() + COLLAPSE_HOLD_MS;
4053
+ geoSnapRef.current = geoOpen ? null : { anchorLeft: anchorLeft, openLeft: openLeft, dock: dock };
4054
+ }
4055
+ var geoFrozen = geoSnapRef.current !== null && geoSnapRef.current.dock === dock
4056
+ && Date.now() < geoHoldRef.current;
4057
+ if (geoFrozen) {
4058
+ anchorLeft = geoSnapRef.current.anchorLeft;
4059
+ openLeft = geoSnapRef.current.openLeft;
4060
+ }
4061
+ var openOffset = openLeft - anchorLeft;
4062
+ var parkedOffset = dockRight ? (openLeft + panelW) - anchorLeft : 0;
4063
+ var panelLeft = anchorLeft;
4064
+ var panelOffset = geoOpen ? openOffset : parkedOffset;
4004
4065
  var panelTransform = panelOffset === 0 ? "none" : "translateX(" + zLen(panelOffset) + ")";
4005
4066
  var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
4006
4067
  // same reasoning vertically: the draggable offset is stored in localStorage
@@ -4653,6 +4714,8 @@ window.__ModuleLoader__.load({
4653
4714
  // the search box owns its own keys (it stops propagation for the ones it uses)
4654
4715
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (target && target.isContentEditable)) return;
4655
4716
  if (e.altKey || e.ctrlKey || e.metaKey) return;
4717
+ // a key on the panel is a reader gesture: the auto-loader may act on it
4718
+ autoGestureRef.current = Date.now();
4656
4719
  var key = e.key;
4657
4720
  if (key === "ArrowDown" || key === "ArrowUp") {
4658
4721
  swallow(e);
@@ -4752,13 +4815,22 @@ window.__ModuleLoader__.load({
4752
4815
  // right dock -> clipped from the right at the screen/sidebar line
4753
4816
  // LEFT dock keeps its original clip (collapse left). RIGHT dock clips the
4754
4817
  // LEFT side so the panel closes toward its right (dock) edge in place.
4818
+ //
4819
+ // The inset is the distance the box actually TRAVELS, not the panel's width. The
4820
+ // visible edge of the panel is the clip edge, and that edge is `box.left + inset`
4821
+ // (left dock) / `box.right - inset` (right dock) while the box slides; matching the
4822
+ // inset to the travel makes the two move at the same rate, so the edge stays pinned
4823
+ // on the panel's OWN open edge line the whole way out. With a fixed `panelW + 8`
4824
+ // against a `panelW + 16` travel the edge drifted 8px past that line, which the
4825
+ // reader saw as the panel tucking in slightly left of where it comes back out.
4826
+ var travelPx = Math.abs(openOffset - parkedOffset);
4755
4827
  var panelClip = "inset(0 0 0 0px)";
4756
4828
  if (!geoOpen) {
4757
4829
  if (dockRight) {
4758
4830
  // mirror of LEFT: clip the RIGHT side (panel flies right, shrinking from its right)
4759
- panelClip = "inset(0 " + zLen(panelW) + " 0 0)";
4831
+ panelClip = "inset(0 " + zLen(travelPx) + " 0 0)";
4760
4832
  } else {
4761
- panelClip = "inset(0 0 0 " + zLen(panelW + 8) + ")";
4833
+ panelClip = "inset(0 0 0 " + zLen(travelPx) + ")";
4762
4834
  }
4763
4835
  }
4764
4836
  // faded out entirely while another center-column view is active
@@ -5434,6 +5506,9 @@ window.__ModuleLoader__.load({
5434
5506
  },
5435
5507
  onScroll: onListScroll,
5436
5508
  onWheel: onListWheel,
5509
+ // a press on a row (or on the list) is a reader gesture too: click-to-load an
5510
+ // unloaded row and drag-scrolling must both keep working
5511
+ onPointerDown: function () { autoGestureRef.current = Date.now(); },
5437
5512
  children: react_jsx_runtime.jsx("div", {
5438
5513
  // the two-phase cross-fade of the questions-only switch rides on this
5439
5514
  // wrapper (classes, never inline animation: React rewrites inline styles
@@ -7266,6 +7341,17 @@ window.__ModuleLoader__.load({
7266
7341
  if (form && form.getSnapshot && form.getSnapshot().status === "ready") return candidates[j];
7267
7342
  } catch (e) { /* try the next candidate */ }
7268
7343
  }
7344
+ // The settings mirror can still be LOADING when a plugin applies, so the readiness
7345
+ // check above may not have matched yet (DSH 0.1.7-rc.1 answers `status: "loading"`
7346
+ // at this point, while 0.1.5-rc.x's `settingsScope` answered immediately). The
7347
+ // scope subscribes to the form and adopts its snapshot when the document arrives,
7348
+ // so any existing form is enough: take the first candidate that HAS one instead of
7349
+ // blindly naming the first candidate (which can be a name with no form at all).
7350
+ for (var k = 0; k < candidates.length; k++) {
7351
+ try {
7352
+ if (forms.get(candidates[k])) return candidates[k];
7353
+ } catch (e) { /* try the next candidate */ }
7354
+ }
7269
7355
  return candidates[0];
7270
7356
  };
7271
7357
  var pickSettingsScope = function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quick-toc",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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",
@@ -14,7 +14,7 @@
14
14
  "LICENSE"
15
15
  ],
16
16
  "engines": {
17
- "dsh": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
17
+ "dsh": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
18
18
  },
19
19
  "exports": {
20
20
  ".": "./lib/index.js",
@@ -35,19 +35,18 @@
35
35
  },
36
36
  "compatibility": {
37
37
  "dshReleases": {
38
- "0.1.5-rc.1": "compatible",
39
- "0.1.5-rc.2": "compatible",
40
38
  "0.1.5-rc.3": "compatible",
41
39
  "0.1.7-alpha.1": "compatible",
42
- "0.1.7-alpha.2": "compatible"
40
+ "0.1.7-alpha.2": "compatible",
41
+ "0.1.7-rc.1": "compatible"
43
42
  }
44
43
  }
45
44
  },
46
45
  "peerDependencies": {
47
46
  "react": "^18.2.0",
48
47
  "@deepseek-ai/schemastery": "^3.18.0",
49
- "@deepseek-ai/dsh-client-ui-chat": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0",
50
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
48
+ "@deepseek-ai/dsh-client-ui-chat": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0",
49
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.3 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
51
50
  },
52
51
  "peerDependenciesMeta": {
53
52
  "@deepseek-ai/schemastery": {