@ryuu-64/dsh-find-all 0.1.3 → 0.1.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.md CHANGED
@@ -1,92 +1,130 @@
1
- # Changelog
2
-
3
- ## 0.1.3
4
-
5
- Documentation only: no code, no bundle, no behavior changed. Published so the README on the npm
6
- package page is the user-facing one — npm serves the README carried by the latest release.
7
-
8
- ### Changed
9
-
10
- - **`README.md` is now written for users.** It had been carrying maintainer material: internal
11
- session-service calls, the tarball install for when publishing is blocked, the 0.1.0
12
- registration post-mortem, the derived-literal warning for future forkers, and the test
13
- commands. A user opening the page had to work out which parts applied to them. The README now
14
- answers what the plugin does, how to install it, the keyboard reference, known limitations, and
15
- how to uninstall.
16
-
17
- ### Added
18
-
19
- - **`CONTRIBUTING.md`** takes that material rather than dropping it, plus a note on why release
20
- coordinates cannot be reused (the reason 0.1.1 became 0.1.2). It is not in `package.json`'s
21
- `files` whitelist, so npm users do not receive it.
22
-
23
- ### Fixed
24
-
25
- - `scripts/install-local.mjs` told the operator to move to the registry "once 0.1.1 is published",
26
- a version that can never exist. It now refers to the installed version generically.
27
-
28
- ## 0.1.2
29
-
30
- Nothing about the code changed — this release exists only because **0.1.1 can never be
31
- published**. The publish that produced `+ @ryuu-64/dsh-find-all@0.1.1` left the version in npm's
32
- staging area rather than on the registry, and the registry refuses to reuse the coordinate:
33
-
34
- ```text
35
- 409 Cannot publish over previously staged version "0.1.1"
36
- 409 Cannot stage previously published version "0.1.1"
37
- ```
38
-
39
- That is expected, not a bug: registry data is immutable, and *"if you've ever published a package
40
- called bob at version 1.1.0, no other package can ever be published with that name at that
41
- version. This is true even if that package is unpublished"* ([npm Unpublish
42
- Policy](https://docs.npmjs.com/policies/unpublish)). The 0.1.1 tarball never became reachable
43
- (`/0.1.1` → 404, its tarball → 404), so the fix is republished unchanged as 0.1.2.
44
-
45
- The 0.1.1 section below still describes this release's content: same bundle, same fix, byte for
46
- byte.
47
-
48
- ## 0.1.1
49
-
50
- ### Fixed
51
-
52
- - **The client bundle now registers under the package name.** `lib/client.js` called
53
- `window.__ModuleLoader__.load({ id: "dsh-find-all", ... })` — the unscoped name — while the
54
- package is `@ryuu-64/dsh-find-all`. The host composes the boot-graph row id from the package
55
- name (`ClientModuleRegistry.reconcilePackage`), and `ClientModuleSystem.arrive()` rejects the
56
- row when the executed bundle did not register a factory under exactly that key, so the plugin
57
- loaded and then failed with:
58
-
59
- ```text
60
- failed to import loader entry <id>: client-modules: bundle /plugins/??...&rev=...
61
- loaded without registering "@ryuu-64/dsh-find-all" via __ModuleLoader__.load
62
- ```
63
-
64
- The GUI surfaced this as "Failed to load plugins / 部分插件加载失败", which reads like a DSH
65
- version incompatibility; it was not.
66
-
67
- Introduced by the 0.1.0 fork, which renamed every other `dsh-find-bar` identifier to
68
- `dsh-find-all` but left the registration id unscoped. `id` is now `"@ryuu-64/dsh-find-all"`.
69
-
70
- ### Added
71
-
72
- - `scripts/check-client-registration.mjs` (wired into `npm run check`): reads the expected id from
73
- `package.json` and fails with a `file:line` diagnostic when the bundle registers anything else,
74
- so the drift cannot come back unnoticed. It takes an optional package root, so it can inspect a
75
- registry tarball or an installed profile instead of only this checkout.
76
- - `npm run verify:release`: packs the artifact `npm publish` would upload and checks the id inside
77
- that tarball. This is the check that would have caught 0.1.0 before it reached the registry.
78
- - `npm run deploy:profile`: installs a published release into a DSH profile and refuses to finish
79
- until the installed bundle is byte-identical to the registry artifact and registers correctly.
80
- Rolls the profile manifests back on any failure.
81
- - Tests for the registration check itself (`test/client-registration-check.test.mjs`): they run it
82
- against a mutated copy, which is how the check was caught silently falling back to the checkout
83
- and reporting the defective registry 0.1.0 as clean.
84
- - `test/bundle-registration.test.mjs`: the invariant as a test, with the expected value read
85
- from `package.json` rather than hardcoded.
86
- - README section on the "Failed to load plugins" symptom, why it is not a DSH version problem, and
87
- the fork re-scope hazard behind it.
88
-
89
- ## 0.1.0
90
-
91
- Initial release: a Ctrl+F find bar that searches the whole conversation, forked from
92
- [`secyborg/dsh-find-bar`](https://github.com/secyborg/dsh-find-bar) (MIT).
1
+ # Changelog
2
+
3
+ ## 0.1.5
4
+
5
+ Fix compatibility with newer DSH hosts while preserving the existing find controls and matching behavior.
6
+
7
+ - Use the official session header slot and session identity instead of the removed global current-session field. Ambiguous, unsupported or stale conversation targets fail closed; highlights, paging and listeners stay scoped and are cleaned up with the host lifecycle.
8
+ - Retain all previously admitted published runtimes and include the two tested 0.2 release candidates through an exact ten-version peer list. No version exemption or future-version guarantee is added.
9
+ - Document the legacy Desktop plugin-manager UI installation route, alongside the current bundled CLI route.
10
+
11
+ [Acceptance run 36998700597](https://github.com/Ryuu-64/dsh-find-all/actions/runs/36998700597) completed the plugin business checks on ten Web runtimes and three official Windows Desktop builds. The unavailable historical Desktop builds remain unverified. Host startup and shutdown errors also occurred without the plugin and remain recorded as an overall CI failure; this release does not claim to fix those host errors. See the README for the accepted version and installation boundaries.
12
+
13
+
14
+ ## 0.1.4
15
+
16
+ Documentation only: no code, no bundle, no behavior changed — `lib/` is untouched, so 0.1.4
17
+ behaves exactly like 0.1.3.
18
+
19
+ 0.1.3 fixed the README but sent it with a link to `CONTRIBUTING.md`, a file that same release had
20
+ just added. That file has now been deleted (see below), which would have left the published README
21
+ pointing at nothing. npm serves the README carried by the latest release, so correcting the link
22
+ on the package page means publishing again — that is the only reason this version exists.
23
+
24
+ ### Removed
25
+
26
+ - **`CONTRIBUTING.md` deleted, in the repository and in the published package.** It was written as
27
+ a chronological account of what changed rather than a document with a point of view: the
28
+ registration-id section worked through the 0.1.0 post-mortem before saying what to do about it,
29
+ and the structure, commands, and version-number sections restated the commit history. Nothing in
30
+ it was needed to use the plugin.
31
+ - One item in it was not recoverable from anywhere else and is gone with the file: that a
32
+ `failed to import loader entry <id> … loaded without registering` banner means the client
33
+ bundle registered under the wrong id, and is not a DSH version incompatibility. It stays
34
+ reachable in git history, and `scripts/check-client-registration.mjs` still fails the build on
35
+ the underlying defect.
36
+
37
+ ### Fixed
38
+
39
+ - `README.md` no longer links to the deleted file.
40
+
41
+ ## 0.1.3
42
+
43
+ Documentation only: no code, no bundle, no behavior changed. Published so the README on the npm
44
+ package page is the user-facing one — npm serves the README carried by the latest release.
45
+
46
+ ### Changed
47
+
48
+ - **`README.md` is now written for users.** It had been carrying maintainer material: internal
49
+ session-service calls, the tarball install for when publishing is blocked, the 0.1.0
50
+ registration post-mortem, the derived-literal warning for future forkers, and the test
51
+ commands. A user opening the page had to work out which parts applied to them. The README now
52
+ answers what the plugin does, how to install it, the keyboard reference, known limitations, and
53
+ how to uninstall.
54
+
55
+ ### Added
56
+
57
+ - **`CONTRIBUTING.md`** takes that material rather than dropping it, plus a note on why release
58
+ coordinates cannot be reused (the reason 0.1.1 became 0.1.2). It is not in `package.json`'s
59
+ `files` whitelist, so npm users do not receive it.
60
+
61
+ ### Fixed
62
+
63
+ - `scripts/install-local.mjs` told the operator to move to the registry "once 0.1.1 is published",
64
+ a version that can never exist. It now refers to the installed version generically.
65
+
66
+ ## 0.1.2
67
+
68
+ Nothing about the code changed — this release exists only because **0.1.1 can never be
69
+ published**. The publish that produced `+ @ryuu-64/dsh-find-all@0.1.1` left the version in npm's
70
+ staging area rather than on the registry, and the registry refuses to reuse the coordinate:
71
+
72
+ ```text
73
+ 409 Cannot publish over previously staged version "0.1.1"
74
+ 409 Cannot stage previously published version "0.1.1"
75
+ ```
76
+
77
+ That is expected, not a bug: registry data is immutable, and *"if you've ever published a package
78
+ called bob at version 1.1.0, no other package can ever be published with that name at that
79
+ version. This is true even if that package is unpublished"* ([npm Unpublish
80
+ Policy](https://docs.npmjs.com/policies/unpublish)). The 0.1.1 tarball never became reachable
81
+ (`/0.1.1` → 404, its tarball → 404), so the fix is republished unchanged as 0.1.2.
82
+
83
+ The 0.1.1 section below still describes this release's content: same bundle, same fix, byte for
84
+ byte.
85
+
86
+ ## 0.1.1
87
+
88
+ ### Fixed
89
+
90
+ - **The client bundle now registers under the package name.** `lib/client.js` called
91
+ `window.__ModuleLoader__.load({ id: "dsh-find-all", ... })` — the unscoped name — while the
92
+ package is `@ryuu-64/dsh-find-all`. The host composes the boot-graph row id from the package
93
+ name (`ClientModuleRegistry.reconcilePackage`), and `ClientModuleSystem.arrive()` rejects the
94
+ row when the executed bundle did not register a factory under exactly that key, so the plugin
95
+ loaded and then failed with:
96
+
97
+ ```text
98
+ failed to import loader entry <id>: client-modules: bundle /plugins/??...&rev=...
99
+ loaded without registering "@ryuu-64/dsh-find-all" via __ModuleLoader__.load
100
+ ```
101
+
102
+ The GUI surfaced this as "Failed to load plugins / 部分插件加载失败", which reads like a DSH
103
+ version incompatibility; it was not.
104
+
105
+ Introduced by the 0.1.0 fork, which renamed every other `dsh-find-bar` identifier to
106
+ `dsh-find-all` but left the registration id unscoped. `id` is now `"@ryuu-64/dsh-find-all"`.
107
+
108
+ ### Added
109
+
110
+ - `scripts/check-client-registration.mjs` (wired into `npm run check`): reads the expected id from
111
+ `package.json` and fails with a `file:line` diagnostic when the bundle registers anything else,
112
+ so the drift cannot come back unnoticed. It takes an optional package root, so it can inspect a
113
+ registry tarball or an installed profile instead of only this checkout.
114
+ - `npm run verify:release`: packs the artifact `npm publish` would upload and checks the id inside
115
+ that tarball. This is the check that would have caught 0.1.0 before it reached the registry.
116
+ - `npm run deploy:profile`: installs a published release into a DSH profile and refuses to finish
117
+ until the installed bundle is byte-identical to the registry artifact and registers correctly.
118
+ Rolls the profile manifests back on any failure.
119
+ - Tests for the registration check itself (`test/client-registration-check.test.mjs`): they run it
120
+ against a mutated copy, which is how the check was caught silently falling back to the checkout
121
+ and reporting the defective registry 0.1.0 as clean.
122
+ - `test/bundle-registration.test.mjs`: the invariant as a test, with the expected value read
123
+ from `package.json` rather than hardcoded.
124
+ - README section on the "Failed to load plugins" symptom, why it is not a DSH version problem, and
125
+ the fork re-scope hazard behind it.
126
+
127
+ ## 0.1.0
128
+
129
+ Initial release: a Ctrl+F find bar that searches the whole conversation, forked from
130
+ [`secyborg/dsh-find-bar`](https://github.com/secyborg/dsh-find-bar) (MIT).
package/LICENSE CHANGED
@@ -1,22 +1,22 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 secyborg
4
- Copyright (c) 2026 Ryuu-64
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 secyborg
4
+ Copyright (c) 2026 Ryuu-64
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -1,48 +1,48 @@
1
- # dsh-find-bar
2
-
3
- DSH web 插件:为桌面 App(Electron 壳)补上 **⌘F / Ctrl+F 页内查找**,
4
- 体验对齐浏览器原生查找条——支持全文高亮、匹配计数、回车跳转。
5
-
6
- 搜索范围:当前页面渲染的全部文本——对话内容、侧边栏、设置页。
7
-
8
- ## 原理
9
-
10
- Electron 没有浏览器内置查找栏。本插件在客户端实现:
11
-
12
- - 监听 ⌘F/Ctrl+F(capture 阶段)弹出自绘查找条(跟随主题变量)
13
- - 用 TreeWalker 收集文本节点(含 shadow root 内),构建 DOM Range 匹配
14
- - 用 **CSS Custom Highlight API**(`CSS.highlights` + `::highlight()`)高亮——
15
- 只绘制不修改 DOM,与 React 渲染零冲突
16
- - 不支持该 API 的环境自动退回 `window.find()`
17
-
18
- ## 快捷键
19
-
20
- | 键 | 功能 |
21
- |---|---|
22
- | ⌘F / Ctrl+F | 打开查找条(自动填入当前选中文本) |
23
- | Enter / Shift+Enter | 下一个 / 上一个匹配 |
24
- | ⌘G / F3(+Shift) | 下一个 / 上一个 |
25
- | Esc | 关闭并清除高亮 |
26
-
27
- 输入即搜(防抖跟随内容流式更新),计数显示 `当前/总数`。
28
-
29
- ## 安装
30
-
31
- ```sh
32
- npm pack
33
- dsh plugin --profile desktop add ./dsh-find-bar-0.1.0.tgz
34
- ```
35
-
36
- 安装后重启一次应用(插件清单启动时装载)。
37
-
38
- ## 已知限制
39
-
40
- - 只搜索**已渲染**在 DOM 里的内容;未展开的会话/极长列表若被界面折叠,需先展开
41
- - 匹配上限 5000 条(超限显示 `+`)
42
- - 大小写不敏感,暂无"区分大小写"开关
43
-
44
- ## 卸载
45
-
46
- ```sh
47
- dsh plugin --profile desktop remove dsh-find-bar
48
- ```
1
+ # dsh-find-bar
2
+
3
+ DSH web 插件:为桌面 App(Electron 壳)补上 **⌘F / Ctrl+F 页内查找**,
4
+ 体验对齐浏览器原生查找条——支持全文高亮、匹配计数、回车跳转。
5
+
6
+ 搜索范围:当前页面渲染的全部文本——对话内容、侧边栏、设置页。
7
+
8
+ ## 原理
9
+
10
+ Electron 没有浏览器内置查找栏。本插件在客户端实现:
11
+
12
+ - 监听 ⌘F/Ctrl+F(capture 阶段)弹出自绘查找条(跟随主题变量)
13
+ - 用 TreeWalker 收集文本节点(含 shadow root 内),构建 DOM Range 匹配
14
+ - 用 **CSS Custom Highlight API**(`CSS.highlights` + `::highlight()`)高亮——
15
+ 只绘制不修改 DOM,与 React 渲染零冲突
16
+ - 不支持该 API 的环境自动退回 `window.find()`
17
+
18
+ ## 快捷键
19
+
20
+ | 键 | 功能 |
21
+ |---|---|
22
+ | ⌘F / Ctrl+F | 打开查找条(自动填入当前选中文本) |
23
+ | Enter / Shift+Enter | 下一个 / 上一个匹配 |
24
+ | ⌘G / F3(+Shift) | 下一个 / 上一个 |
25
+ | Esc | 关闭并清除高亮 |
26
+
27
+ 输入即搜(防抖跟随内容流式更新),计数显示 `当前/总数`。
28
+
29
+ ## 安装
30
+
31
+ ```sh
32
+ npm pack
33
+ dsh plugin --profile desktop add ./dsh-find-bar-0.1.0.tgz
34
+ ```
35
+
36
+ 安装后重启一次应用(插件清单启动时装载)。
37
+
38
+ ## 已知限制
39
+
40
+ - 只搜索**已渲染**在 DOM 里的内容;未展开的会话/极长列表若被界面折叠,需先展开
41
+ - 匹配上限 5000 条(超限显示 `+`)
42
+ - 大小写不敏感,暂无"区分大小写"开关
43
+
44
+ ## 卸载
45
+
46
+ ```sh
47
+ dsh plugin --profile desktop remove dsh-find-bar
48
+ ```
package/README.md CHANGED
@@ -22,24 +22,30 @@ DSH 桌面窗口是 Electron 壳,本身没有浏览器那种查找栏。而会
22
22
 
23
23
  ## 安装
24
24
 
25
- 用 DSH 自带的插件命令装:
25
+ 先按 **DSH 运行时版本**选择安装入口;它不一定等于 Desktop 产品版本。下面的包名用于安装 npm 已发布版本;验收特定候选包时,请替换为对应 `.tgz` 的绝对路径。
26
+
27
+ **运行时 0.2.0-rc.2**:用 Desktop 自带的插件命令安装:
26
28
 
27
29
  ```sh
28
30
  dsh plugin --profile desktop add @ryuu-64/dsh-find-all
29
31
  ```
30
32
 
31
- 装完请**完整重启一次 DSH Desktop**——插件清单只在启动的时候装载,不重启不会生效。
33
+ **运行时 0.2.0-rc.1 / 0.1.7-rc.2**:这两个官方安装包没有上述新版 CLI 入口,通用 `dsh plugin --profile desktop` 也会拒绝操作。请在 Desktop 中打开「插件 / Plugins」→「添加插件 / Add plugin」,填入 `@ryuu-64/dsh-find-all`(验收候选时填 `.tgz` 绝对路径),点击「安装 / Install」,完成后点击「立即启用 / Enable now」。不要用版本豁免绕过安装错误。
34
+
35
+ 安装后**完整重启一次 DSH Desktop**,再检查插件是否正常加载。已验证的三个桌面版本支持原生禁用/重新启用;旧宿主的刷新要求见下方兼容表说明。
32
36
 
33
37
  装完按 **⌘F / Ctrl+F**,右上角出现查找栏就算装好了。
34
38
 
35
39
  ### 和 dsh-find-bar 只能留一个
36
40
 
37
- `dsh-find-bar` 和本插件**都会抢 Ctrl+F**,两个同时装会互相顶掉。如果你之前装过它,先卸掉:
41
+ `dsh-find-bar` 和本插件**都会抢 Ctrl+F**,两个同时装会互相顶掉。如果你之前装过它,先卸掉。运行时 0.2.0-rc.2 可用:
38
42
 
39
43
  ```sh
40
44
  dsh plugin --profile desktop remove dsh-find-bar
41
45
  ```
42
46
 
47
+ 运行时 0.2.0-rc.1 / 0.1.7-rc.2 请在「插件 / Plugins」中找到 `dsh-find-bar` 并选择「卸载 / Uninstall」。
48
+
43
49
  ## 使用
44
50
 
45
51
  | 操作 | 按键 |
@@ -62,15 +68,35 @@ dsh plugin --profile desktop remove dsh-find-bar
62
68
 
63
69
  ## 卸载 / 回退
64
70
 
71
+ 运行时 0.2.0-rc.2 可用:
72
+
65
73
  ```sh
66
74
  dsh plugin --profile desktop remove @ryuu-64/dsh-find-all # 卸载
67
75
  dsh plugin --profile desktop add dsh-find-bar # 想换回上游那个插件
68
76
  ```
69
77
 
78
+ 运行时 0.2.0-rc.1 / 0.1.7-rc.2 请在「插件 / Plugins」中卸载本插件;需要回退时,再通过「添加插件 / Add plugin」安装 `dsh-find-bar`。
79
+
70
80
  ## 关于这个项目
71
81
 
72
82
  本插件 fork 自 [secyborg/dsh-find-bar](https://github.com/secyborg/dsh-find-bar)(作者 secyborg,MIT),在上游的基础上加了「整段会话」搜索。上游原有的查找条界面、高亮、`n/N` 计数和键盘导航都保留了下来。
73
83
 
74
84
  版权与许可为 MIT 双署名:`Copyright (c) 2026 secyborg` 与 `Copyright (c) 2026 Ryuu-64`。上游 README 原样保存在 `README.fork-origin.md`,改动清单与致谢见 `NOTICE.md`。
75
85
 
76
- 参与开发、打包发布、以及「Failed to load plugins」报错的排查,见 `CONTRIBUTING.md`。
86
+ ## 兼容性开发说明
87
+
88
+ 当前候选改动由会话标题栏中的查找按钮取得明确的会话身份;首次快捷键在只有一个可见会话面板时可直接查找;多会话时先点击或聚焦目标会话。已明确拒绝或失效的目标不会自动改搜其它会话。无法识别可靠的会话正文范围时会提示先选择会话,不退回整页搜索。历史加载失败或中断时保留已显示的部分结果,并说明尚未搜完。
89
+
90
+ 右侧嵌入会话暂不支持查找,会提示范围不可用,不会自动改搜主栏;明确选择主会话后仍可查找。
91
+
92
+ [2026-10-02 验收记录(代码版本 `69c477c`)](https://github.com/Ryuu-64/dsh-find-all/actions/runs/36998700597)使用同一候选包完成十版 Web 和三个官方 Windows Desktop 的插件功能验收:首次快捷键、80 条结果的分页、会话切换、结果导航及宿主支持的启停行为。当前 rc2 还验证了实际右侧嵌入会话的范围隔离。
93
+
94
+ | DSH 运行时版本 | 实际 Web | 官方 Windows Desktop |
95
+ | --- | --- | --- |
96
+ | 0.1.5-rc.2、0.1.5-rc.3、0.1.6-alpha.2 | 功能验收通过 | 已核官方路径未取得安装包,未验证 |
97
+ | 0.1.6-alpha.1、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1 | 功能验收通过 | 本轮未验证 |
98
+ | 0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 | 功能验收通过 | 插件功能验收通过;宿主错误仍保留 |
99
+
100
+ **该次 CI 整体仍为失败**:三个桌面版本均记录到宿主启动时的 `startupData/preloadScripts` 错误及退出时的连接错误,未装插件的基线也存在同类错误。没有忽略这些错误,也不把插件功能通过写成宿主整体无错误。Web 通过不代表对应 Desktop 已验证,未知未来版本不作承诺。详见 [兼容性验收方法与边界](docs/compatibility.md);最终交付状态以 [PR #9](https://github.com/Ryuu-64/dsh-find-all/pull/9) 的独立审核为准。
101
+
102
+ 旧宿主 0.1.5-rc.2 / rc.3 / 0.1.6-alpha.1 本身不支持插件图变更的热卸载:禁用或重新启用后须刷新页面或重启应用。其余七个已验 Web 运行时和上述三个已验 Desktop 均通过了两次原生禁用/重新启用测试;未验证的 Desktop 不据此推定通过。
@@ -0,0 +1,68 @@
1
+ # Issue #1: compatibility acceptance
2
+
3
+ This implementation follows the approved [Issue #1 plan](https://github.com/Ryuu-64/dsh-find-all/issues/1#issuecomment-5933617888), with the accepted release scope and unverified Desktop limits recorded in [PR #9](https://github.com/Ryuu-64/dsh-find-all/pull/9). This does not promise compatibility with untested or future hosts. Issues #2–#8 are outside this change.
4
+
5
+ ## Contract research
6
+
7
+ The adapter uses a `conversation.session.header.utilities` list entry. Its `sessionId` comes from the session standard props; it never consults the old global `sessions.list.current` field. A mounted button identifies one view instance, not just one session. Keyboard focus or a click inside a visible view selects that instance. An ambiguous multi-view selection fails closed with a prompt to select a conversation. Focus in the find bar preserves the selected instance. On the first shortcut, neutral body focus may discover one registered view only when all visible registered instances and known conversation content/scroll/flow containers identify that same single instance. Visible unsupported or embedded views also make discovery ambiguous; the main panel and its own content are deduplicated. Explicit rejection, ambiguity, or loss of an earlier selection blocks that discovery; passive checks never select a replacement.
8
+
9
+ Official release-tag sources inspected:
10
+
11
+ - [`0.1.5-rc.2` slot contract](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.5-rc.2/packages/client/ui-conversation/src/client/contract/slots.ts), [session standard props](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.5-rc.2/packages/client/ui-session/src/client/index.ts), and [header utility registration / `ctx.effect` example](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.5-rc.2/packages/session-query/session-log-export/src/client/index.ts)
12
+ - [`0.1.5-rc.2` root](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.5-rc.2/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx) and [`0.1.5-rc.3` root](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.5-rc.3/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx): the header and one conversation scrollport share the `data-phase` root
13
+ - [`0.1.6-alpha.2` main panel](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.6-alpha.2/packages/client/ui-conversation/src/client/skeleton/ConversationMainPanel.tsx) and [content](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.6-alpha.2/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx): split components retain that root/scrollport structure
14
+ - [`0.1.7-rc.2` content](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.7-rc.2/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx), [`0.2.0-rc.1` content](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.1/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx), and [`0.2.0-rc.2` content](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx): explicit content session identity is additionally checked
15
+ - [`0.2.0-rc.2` slot registry](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/client/ui-renderer/src/client/registry.ts): slot injection and registration are context-owned effects
16
+
17
+ The official [startup fixture](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/apps/web/tests/startup-auto-selection.e2e.ts) explicitly distinguishes the resident conversation root from the composer's unrelated `data-phase`. Routing skips that editor attribute. Initial focus in an unbound Hero/settling composer before any visible registered session exists keeps the never-selected state; a rejected active conversation or lost selection still cannot trigger discovery of another view.
18
+
19
+ DOM attributes are implementation details, not a promised future host API. `resolveScope` is their single adapter boundary. It requires an active, connected, visible panel, exactly one registered session anchor, one scrollport, and one visible `data-chat-flow`. If explicit host session metadata exists, it must match. Search and mutation observation stay inside that flow. A missing/ambiguous/hidden/replaced flow yields no results or paging. There is no `document.body` or `window.find` fallback.
20
+
21
+ `ctx.effect` owns global key/focus/pointer listeners and teardown. React effect cleanup removes each session anchor. Close/dispose cancels debounce, paging-start, paging-settle, session polling, observation, and highlights. An in-flight host request cannot be aborted through this API; its generation token prevents stale results from being painted or another page being requested.
22
+
23
+ Paging errors/stalls and manual stop retain rendered matches and explicitly label them incomplete. Existing matching and query behavior have not been redesigned.
24
+
25
+ ## Acceptance layers (do not conflate)
26
+
27
+ 1. **Logic and contract fixtures**: `npm run check` runs pure logic, registration, and React 18 + jsdom synthetic session tests. These assert old/new structural adapters, multiple views (including the same session twice), ambiguity, hidden/mismatched roots, scoped results, paging failure, in-flight switching, timer/highlight teardown, and no browser-wide fallback. Geometry is mocked in jsdom; this is not a real host/browser acceptance result.
28
+ 2. **Actual Web runtime + synthetic sessions**: installation of the same packed artifact, real module loader/slot rendering, paging, multiple view transitions and the host-supported disable/re-enable lifecycle must be recorded for each target release. A browser fixture alone cannot pass this layer.
29
+ 3. **Target Electron Desktop smoke**: the same artifact must be checked on the target Desktop build/OS. Web or jsdom success does not pass this layer.
30
+
31
+ Candidate releases: `0.1.5-rc.2`, `0.1.5-rc.3`, `0.1.6-alpha.1`, `0.1.6-alpha.2`, `0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2`.
32
+
33
+ The 2026-10-02 registry audit compared every published `@deepseek-ai/dsh` version with main's three peer requirements (`>=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-0 <0.2.0-0`) using the official prerequisite rule. The original six-target sample accidentally excluded four releases that the old gate admitted: `0.1.6-alpha.1`, `0.1.7-alpha.1`, `0.1.7-alpha.2`, and `0.1.7-rc.1`. The ten-target candidate includes all currently published versions admitted by that old gate plus both current 0.2 release candidates. The user's Desktop 2.0.14 report identifies its bundled runtime as `0.1.7-rc.1`; Desktop marketing/version labels are not inferred to equal DSH runtime versions.
34
+
35
+ The four added targets' official tagged slot, session props, conversation skeleton and HMR sources were inspected, and their vendor package pins are recorded in `scripts/compat/vendor-versions.json`. Links: [0.1.6-alpha.1](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.1.6-alpha.1/packages/client), [0.1.7-alpha.1](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.1.7-alpha.1/packages/client), [0.1.7-alpha.2](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.1.7-alpha.2/packages/client), [0.1.7-rc.1](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.1.7-rc.1/packages/client).
36
+
37
+ Every candidate must pass the same new tgz in real Web acceptance, including first Ctrl+F with body focus and no preparatory anchor click. Previous artifact results cannot validate changed production bytes. Desktop acceptance is separately recorded, including a Windows Web run for platform-specific harness behavior. The exact peer list enables verification without exemptions; it is not a passed support matrix. The historical `dshReleases` record is unchanged. Before release, failures must be fixed or clearly documented; removing a previously accepted target requires an explicit reviewed compatibility decision, not silent narrowing.
38
+
39
+ The official [compatibility evaluator](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/boot/app-boot/src/plugin-compatibility.ts) uses `semver.satisfies(..., { includePrerelease: true })`. Do not infer its results from ordinary npm semver behavior. Do not remove peer requirements or grant an exemption to make the matrix appear green.
40
+
41
+ ## Reproduction
42
+
43
+ ```sh
44
+ npm ci --ignore-scripts --legacy-peer-deps
45
+ npm run check
46
+ npm_config_cache=/tmp/find-all-npm-cache npm run verify:release
47
+ npm pack --ignore-scripts --pack-destination /tmp/find-all-candidate
48
+ # Extract this one tgz, retain its SHA-256, then test those bytes:
49
+ FIND_ALL_PACKAGE_ROOT=/tmp/find-all-candidate/package node --test test/session-adapter.test.mjs
50
+ ```
51
+
52
+ `--legacy-peer-deps` here is only for isolated development test dependencies; it is not a host installation result or exemption. Test sessions must be synthetic, with no user data or model credentials. Do not change the artifact between version runs. Unknown future host versions need fresh regression evidence before changing support metadata.
53
+
54
+ ## Host lifecycle boundary
55
+
56
+ The official HMR client in [0.1.5-rc.2](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.5-rc.2/packages/client/hmr/src/client/index.ts) and [0.1.5-rc.3](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.5-rc.3/packages/client/hmr/src/client/index.ts) deliberately ignores graph changes after initial load. These hosts and [0.1.6-alpha.1](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.6-alpha.1/packages/client/hmr/src/client/index.ts) require a page refresh or application restart after enabling/disabling a plugin. We do not patch the host or claim hot unload works there. Their acceptance checks wait for the actual server graph change, then verify disabled/restored behavior after an explicit refresh.
57
+
58
+ The inspected HMR clients in [0.1.6-alpha.2](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.6-alpha.2/packages/client/hmr/src/client/index.ts), [0.1.7-rc.2](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.7-rc.2/packages/client/hmr/src/client/index.ts), [0.2.0-rc.1](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.1/packages/client/hmr/src/client/index.ts), and [0.2.0-rc.2](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/client/hmr/src/client/index.ts) synchronize graph entries. The newly inspected 0.1.7-alpha.1, alpha.2 and rc.1 use the same `entries.sync` mechanism. Their acceptance checks require two real hot disable/re-enable cycles with zero page navigations. Every target must also pass actual search, history paging, and A/B/A session switching using the same candidate tgz. See the PR's linked CI evidence for run-specific results and artifact hashes; pending matrix entries are not passes. All Electron Desktop targets remain unverified until separately recorded.
59
+
60
+ ## Windows validation isolation
61
+
62
+ The runner preserves its real `USERPROFILE`; DSH state and Electron `--user-data-dir` use owned temporary directories. A bounded no-inspector comparison changes only `USERPROFILE` (with separate evidence log filenames) before the UI smoke. It records crash/survival, not a UI pass. No real user profile, credentials, sandbox or security policy is changed. Windows Web validation invokes the npm `.cmd` shim through PowerShell with literal arguments and imports resolved modules through `pathToFileURL`.
63
+
64
+ ## Simultaneous embedded sidebar boundary
65
+
66
+ The official rc2 [SidebarChatTab](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/client/ui-subagent/src/client/sidebar-chat/index.tsx) renders an embedded ConversationContent with `data-conversation-content`, `data-conversation-session` and `data-content-phase`, without the main panel's `data-phase` or a session header utility. This unsupported view must block neutral fallback to another conversation; explicit interaction with it remains rejected after it disappears. Fixtures cover visible registered Hero/settling/unknown phases, hidden utilities and headerless sidebars.
67
+
68
+ The current rc2 real-host case authors a completed one-shot child with the official persisted header/descriptor/catalog pattern from [subagent-conversation.e2e.ts](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/apps/web/tests/subagent-conversation.e2e.ts), opens the host's actual right-sidebar control, and natively re-enables the plugin with both views visible. It requires neutral Ctrl+F to reject ambiguity, an explicit main choice to remain scoped, and unsupported sidebar interaction to reject main-session search. No model is called and no second DOM pane is fabricated. The CI result for each artifact determines whether this case passed.
package/lib/client.js CHANGED
@@ -32,8 +32,8 @@
32
32
  // * Highlights use the CSS Custom Highlight API (CSS.highlights + Highlight)
33
33
  // which paints ranges WITHOUT mutating the DOM — React keeps full
34
34
  // ownership of the tree. Chromium/Electron support this since 105.
35
- // * When the API is unavailable, the bar degrades to the legacy
36
- // window.find() calls (no count, still functional).
35
+ // * Without CSS Highlights, navigation still scrolls only scoped ranges.
36
+ // Browser-wide window.find() is deliberately never used.
37
37
  //
38
38
  // Keyboard:
39
39
  // Cmd/Ctrl+F open (prefilled with the current selection)
@@ -43,7 +43,7 @@
43
43
 
44
44
  window.__ModuleLoader__.load({
45
45
  id: "@ryuu-64/dsh-find-all",
46
- factory: () => {
46
+ factory: (require) => {
47
47
  var module = { exports: {} };
48
48
  var exports = module.exports;
49
49
 
@@ -76,7 +76,11 @@ window.__ModuleLoader__.load({
76
76
  capped: "已到加载上限({n} 页),停止加载",
77
77
  stalled: "没有更早的历史了",
78
78
  stopTip: "点击停止加载",
79
- noService: "取不到会话接口,只能搜当前页面"
79
+ noService: "会话接口不可用,仅保留已显示的部分结果",
80
+ noTarget: "请先选择可见的会话,再打开查找",
81
+ noRoot: "当前会话的搜索范围不可用",
82
+ incomplete: "历史尚未搜完,保留部分结果",
83
+ find: "查找会话"
80
84
  } : {
81
85
  placeholder: "Find in conversation…",
82
86
  prev: "Previous match",
@@ -92,7 +96,11 @@ window.__ModuleLoader__.load({
92
96
  capped: "Page cap reached ({n}), stopped loading",
93
97
  stalled: "No earlier history",
94
98
  stopTip: "Click to stop loading",
95
- noService: "Session service unavailable — rendered page only"
99
+ noService: "Session service unavailable; rendered results only",
100
+ noTarget: "Select a visible conversation before opening find",
101
+ noRoot: "Search scope unavailable for this conversation",
102
+ incomplete: "History search incomplete; partial results retained",
103
+ find: "Find in conversation"
96
104
  };
97
105
 
98
106
  //#region pure helpers (exported for tests)
@@ -121,7 +129,7 @@ window.__ModuleLoader__.load({
121
129
  if (meta && (key === "f" || key === "F")) {
122
130
  event.preventDefault();
123
131
  event.stopPropagation();
124
- ops.open();
132
+ ops.open(event.target);
125
133
  return;
126
134
  }
127
135
  if (!ops.isOpen()) return;
@@ -239,10 +247,10 @@ window.__ModuleLoader__.load({
239
247
  return into;
240
248
  }
241
249
 
242
- function collectRanges(query) {
250
+ function collectRanges(query, root) {
243
251
  var ranges = [];
244
- if (!query || typeof document === "undefined" || document.body === null) return ranges;
245
- var nodes = collectTextNodes(document.body, []);
252
+ if (!query || !root || typeof document === "undefined") return ranges;
253
+ var nodes = collectTextNodes(root, []);
246
254
  for (var i = 0; i < nodes.length && ranges.length < MAX_MATCHES; i++) {
247
255
  var node = nodes[i];
248
256
  var indices = findIndices(node.data, query);
@@ -270,48 +278,195 @@ window.__ModuleLoader__.load({
270
278
  }
271
279
  }
272
280
 
273
- /** Cheap proxy for "how much history is rendered" — used to notice a page landing. */
274
- function domSignature() {
275
- if (typeof document === "undefined" || document.body === null) return 0;
276
- return collectTextNodes(document.body, []).length;
281
+ //#region host adapter
282
+ // Official session-scoped header utility props supply identity. DOM selectors
283
+ // are intentionally isolated here: see docs/compatibility.md for tag sources.
284
+ // No global current-session state and no page-wide search fallback.
285
+ function visible(element) {
286
+ if (!element || !element.isConnected || !element.getClientRects().length) return false;
287
+ for (var node = element; node; node = node.parentElement) {
288
+ if (node.hidden || node.getAttribute("aria-hidden") === "true") return false;
289
+ var css = window.getComputedStyle(node);
290
+ if (css.display === "none" || css.visibility === "hidden" || css.visibility === "collapse" || css.opacity === "0") return false;
291
+ }
292
+ return true;
293
+ }
294
+
295
+ function conversationPanel(element) {
296
+ // The host composer has its own unrelated data-phase. A conversation
297
+ // root additionally owns the scrollport (official startup fixture).
298
+ for (var node = element; node && node !== document.body; node = node.parentElement) {
299
+ if (!node.hasAttribute || !node.hasAttribute("data-phase")) continue;
300
+ var phase = node.getAttribute("data-phase");
301
+ // Preserve even a broken declared panel boundary; never walk through
302
+ // it to a different ancestor conversation because its scroll is absent.
303
+ if (phase === "active" || phase === "hero" || phase === "settling" || node.querySelector('[data-conversation-scroll]')) return node;
304
+ }
305
+ return null;
306
+ }
307
+
308
+ function resolveScope(anchor, sessionId) {
309
+ if (!anchor || typeof sessionId !== "string" || !sessionId || !visible(anchor)) return null;
310
+ var panel = conversationPanel(anchor);
311
+ if (!panel || panel.getAttribute("data-phase") !== "active" || panel === document.documentElement) return null;
312
+ var anchors = panel.querySelectorAll('[data-find-all-session]');
313
+ if (anchors.length !== 1 || anchors[0] !== anchor) return null;
314
+ var scrolls = panel.querySelectorAll('[data-conversation-scroll]');
315
+ if (scrolls.length !== 1) return null;
316
+ var content = scrolls[0].closest('[data-conversation-content]');
317
+ // 0.1.6-alpha.2 has the content wrapper but no identity attribute.
318
+ // Identity still comes from the unique session-scoped header anchor.
319
+ if (content && content.hasAttribute('data-conversation-session') && content.getAttribute('data-conversation-session') !== sessionId) return null;
320
+ var flows = scrolls[0].querySelectorAll('[data-chat-flow]');
321
+ if (flows.length !== 1 || !visible(flows[0])) return null;
322
+ return { panel: panel, root: flows[0] };
323
+ }
324
+
325
+ function createHostAdapter(ctx, changed) {
326
+ var entries = new Map();
327
+ var scopeUnavailable = false;
328
+ // Only a never-selected adapter may discover an initial neutral context.
329
+ // Explicit rejection or loss of a selection must never select a substitute.
330
+ var selectionState = "pristine";
331
+ function candidates() {
332
+ var out = [];
333
+ entries.forEach(function (id, anchor) {
334
+ // Keep unsupported/invisible instances in the identity roster. Dropping
335
+ // them before routing could silently choose a different valid session.
336
+ out.push({ id: id, anchor: anchor, panel: conversationPanel(anchor), scope: resolveScope(anchor, id) });
337
+ });
338
+ return out;
339
+ }
340
+ function choose(entry) {
341
+ if (!entry) return null;
342
+ if (!entry.scope) { selectionState = "rejected"; scopeUnavailable = true; return null; }
343
+ selectionState = "selected";
344
+ return { id: entry.id, anchor: entry.anchor, panel: entry.scope.panel, root: entry.scope.root };
345
+ }
346
+
347
+ function visibleEntry(entry) {
348
+ var container = entry.panel || entry.anchor.closest('[data-phase], [data-conversation-content]');
349
+ return visible(entry.anchor) || visible(container);
350
+ }
351
+
352
+ function discoverInitial(all) {
353
+ var shown = all.filter(visibleEntry);
354
+ var nodes = Array.prototype.filter.call(document.querySelectorAll('[data-phase], [data-conversation-content], [data-conversation-scroll], [data-chat-flow]'), visible);
355
+ scopeUnavailable = false;
356
+ if (shown.length === 0) {
357
+ // An empty resident Hero is not a session choice. A known active
358
+ // or identified but unregistered view is unavailable, not pristine.
359
+ if (nodes.some(function (node) { return node.getAttribute('data-phase') === 'active' || node.getAttribute('data-content-phase') === 'active' || node.hasAttribute('data-conversation-session') || node.hasAttribute('data-chat-flow'); })) {
360
+ selectionState = "rejected"; scopeUnavailable = true;
361
+ }
362
+ return null;
363
+ }
364
+ if (shown.length !== 1) { selectionState = "rejected"; return null; }
365
+ var entry = shown[0], scope = entry.scope;
366
+ if (!scope) return choose(entry);
367
+ var content = scope.root.closest('[data-conversation-content]');
368
+ var scroll = scope.root.closest('[data-conversation-scroll]');
369
+ // One main panel and its own content/scroll/flow count as one view.
370
+ // Any other visible known container, including a headerless embedded
371
+ // sidebar, makes neutral discovery ambiguous even if unsupported.
372
+ var other = nodes.some(function (node) {
373
+ if (node.hasAttribute('data-conversation-content') && node !== content) return true;
374
+ if (node.hasAttribute('data-conversation-scroll') && node !== scroll) return true;
375
+ if (node.hasAttribute('data-chat-flow') && node !== scope.root) return true;
376
+ return node.hasAttribute('data-phase') && conversationPanel(node) === node && node !== scope.panel;
377
+ });
378
+ if (other) { selectionState = "rejected"; return null; }
379
+ return choose(entry);
380
+ }
381
+
382
+ function refresh(entry) {
383
+ if (!entry) return null;
384
+ // Passive checks and repeated shortcuts may only retain this exact
385
+ // registered instance and identity. They never discover a substitute.
386
+ if (entries.get(entry.anchor) !== entry.id) { selectionState = "rejected"; scopeUnavailable = true; return null; }
387
+ scopeUnavailable = false;
388
+ return choose({ id: entry.id, anchor: entry.anchor, scope: resolveScope(entry.anchor, entry.id) });
389
+ }
390
+
391
+ return {
392
+ mount: function (anchor, id) {
393
+ entries.set(anchor, id);
394
+ changed();
395
+ return function () { entries.delete(anchor); changed(); };
396
+ },
397
+ refresh: refresh,
398
+ select: function (target, previous, retainNeutral) {
399
+ var all = candidates();
400
+ if (!target || !target.nodeType) target = document.activeElement;
401
+ if (target && state.bar && state.bar.contains(target)) return refresh(previous);
402
+ if (retainNeutral && (target === document.body || target === document.documentElement)) {
403
+ if (previous || selectionState !== "pristine") return refresh(previous);
404
+ return discoverInitial(all);
405
+ }
406
+ scopeUnavailable = false;
407
+ var direct = all.filter(function (entry) { return target && entry.anchor.contains(target); });
408
+ if (direct.length === 1) return choose(direct[0]);
409
+ var content = target && target.closest ? target.closest('[data-conversation-content]') : null;
410
+ var panel = conversationPanel(target) || content;
411
+ var hit = all.filter(function (entry) { return panel && entry.panel === panel; });
412
+ if (hit.length === 1) {
413
+ if (content && hit[0].scope && hit[0].scope.root.closest('[data-conversation-content]') !== content) {
414
+ selectionState = "rejected"; scopeUnavailable = true; return null;
415
+ }
416
+ return choose(hit[0]);
417
+ }
418
+ if (panel) {
419
+ // Before any session is selected, focus in the resident unbound
420
+ // Hero/settling composer is not a rejected conversation choice.
421
+ var phase = panel.getAttribute('data-phase') || panel.getAttribute('data-content-phase');
422
+ var unbound = !panel.hasAttribute('data-conversation-session') && !panel.querySelector('[data-conversation-session]');
423
+ if (selectionState === "pristine" && !previous && unbound && !all.some(visibleEntry) && (phase === "hero" || phase === "settling")) return null;
424
+ selectionState = "rejected"; scopeUnavailable = true; return null;
425
+ }
426
+ if (previous) selectionState = "rejected";
427
+ return null;
428
+ },
429
+ scopeUnavailable: function () { return scopeUnavailable; },
430
+ valid: function (entry) {
431
+ if (!entry || entries.get(entry.anchor) !== entry.id) return false;
432
+ var scope = resolveScope(entry.anchor, entry.id);
433
+ return !!scope && scope.root === entry.root;
434
+ },
435
+ face: function (entry) {
436
+ try {
437
+ if (!this.valid(entry) || !ctx.sessions || typeof ctx.sessions.binding !== "function") return null;
438
+ var binding = ctx.sessions.binding(entry.id);
439
+ var face = binding && binding.session;
440
+ return face && typeof face.loadOlder === "function" ? face : null;
441
+ } catch (error) { return null; }
442
+ },
443
+ clear: function () { entries.clear(); }
444
+ };
277
445
  }
278
446
 
279
- /** Resolve once the rendered history differs from `before`, or the page times out. */
280
- function waitForChange(before) {
447
+ function domSignature(root) {
448
+ return root ? collectTextNodes(root, []).length : 0;
449
+ }
450
+
451
+ function waitForChange(before, root, token) {
281
452
  return new Promise(function (resolve) {
282
453
  var started = Date.now();
454
+ function finish(value) {
455
+ clearTimeout(state.settleTimer);
456
+ state.settleTimer = 0;
457
+ state.settleCancel = null;
458
+ resolve(value);
459
+ }
460
+ state.settleCancel = function () { finish(false); };
283
461
  (function poll() {
284
- if (domSignature() !== before) { resolve(true); return; }
285
- if (Date.now() - started >= PAGE_SETTLE_MS) { resolve(false); return; }
286
- setTimeout(poll, PAGE_POLL_MS);
462
+ if (token !== state.pageToken || !state.adapter.valid(state.target)) { finish(false); return; }
463
+ if (domSignature(root) !== before) { finish(true); return; }
464
+ if (Date.now() - started >= PAGE_SETTLE_MS) { finish(false); return; }
465
+ state.settleTimer = setTimeout(poll, PAGE_POLL_MS);
287
466
  })();
288
467
  });
289
468
  }
290
469
 
291
- //#region session bridge
292
- //
293
- // The `sessions` client service is provided by
294
- // @deepseek-ai/dsh-api-session-controller; the per-session face it binds
295
- // exposes loadOlder() plus a snapshot carrying hasMore/loadingOlder.
296
- // Everything here is defensive: a host that changes shape must degrade to
297
- // page-only search, never break the bar.
298
-
299
- function resolveFace(ctx) {
300
- try {
301
- var sessions = ctx && ctx.sessions;
302
- if (!sessions || !sessions.list || typeof sessions.list.getSnapshot !== "function") return null;
303
- var id = sessions.list.getSnapshot().current;
304
- if (!id) return null;
305
- if (typeof sessions.binding !== "function") return null;
306
- var binding = sessions.binding(id);
307
- var face = binding && binding.session ? binding.session : null;
308
- if (!face || typeof face.loadOlder !== "function") return null;
309
- return { id: id, face: face };
310
- } catch (error) {
311
- return null;
312
- }
313
- }
314
-
315
470
  function faceHasMore(face) {
316
471
  try {
317
472
  if (typeof face.getSnapshot !== "function") return null;
@@ -327,6 +482,10 @@ window.__ModuleLoader__.load({
327
482
 
328
483
  var state = {
329
484
  ctx: null,
485
+ adapter: null,
486
+ target: null,
487
+ settleTimer: 0,
488
+ settleCancel: null,
330
489
  bar: null,
331
490
  input: null,
332
491
  count: null,
@@ -384,32 +543,29 @@ window.__ModuleLoader__.load({
384
543
  else state.scopeBtn.removeAttribute("data-whole");
385
544
  }
386
545
 
546
+ function unavailableStatus() {
547
+ return state.target || (state.adapter && state.adapter.scopeUnavailable()) ? L.noRoot : L.noTarget;
548
+ }
549
+
387
550
  function runSearch(keepIndex) {
388
- state.ranges = collectRanges(state.query);
551
+ var valid = state.adapter && state.adapter.valid(state.target);
552
+ state.ranges = valid ? collectRanges(state.query, state.target.root) : [];
553
+ if (!valid) setStatus(unavailableStatus(), false);
389
554
  state.capped = state.ranges.length >= MAX_MATCHES;
390
555
  if (!keepIndex || state.index >= state.ranges.length) state.index = state.ranges.length > 0 ? 0 : -1;
391
556
  paint();
392
557
  updateCount();
393
- if (state.index >= 0 && !highlightsSupported()) legacyFind(state.query, false);
558
+
394
559
  }
395
560
 
396
561
  function goTo(delta) {
562
+ if (!state.adapter || !state.adapter.valid(state.target)) { runSearch(false); return; }
397
563
  var n = state.ranges.length;
398
564
  if (n === 0) return;
399
- if (highlightsSupported()) {
400
- state.index = ((state.index + delta) % n + n) % n;
401
- paint();
402
- updateCount();
403
- scrollToRange(state.ranges[state.index]);
404
- } else {
405
- legacyFind(state.query, delta < 0);
406
- }
407
- }
408
-
409
- function legacyFind(query, backwards) {
410
- if (typeof window.find === "function") {
411
- try { window.find(query, false, backwards, true); } catch (error) {}
412
- }
565
+ state.index = ((state.index + delta) % n + n) % n;
566
+ paint();
567
+ updateCount();
568
+ scrollToRange(state.ranges[state.index]);
413
569
  }
414
570
 
415
571
  function scheduleRescan() {
@@ -418,14 +574,14 @@ window.__ModuleLoader__.load({
418
574
  if (state.paging) return;
419
575
  clearTimeout(state.debounce);
420
576
  state.debounce = setTimeout(function () {
421
- if (state.bar) runSearch(true);
577
+ if (isOpen()) runSearch(true);
422
578
  }, 250);
423
579
  }
424
580
 
425
581
  function startObserver() {
426
- if (state.observer || typeof MutationObserver === "undefined") return;
582
+ if (state.observer || !state.target || typeof MutationObserver === "undefined") return;
427
583
  state.observer = new MutationObserver(scheduleRescan);
428
- state.observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
584
+ state.observer.observe(state.target.root, { childList: true, subtree: true, characterData: true });
429
585
  }
430
586
 
431
587
  function stopObserver() {
@@ -438,6 +594,8 @@ window.__ModuleLoader__.load({
438
594
 
439
595
  function cancelPageIn() {
440
596
  state.pageToken += 1;
597
+ if (state.settleCancel) state.settleCancel();
598
+ clearTimeout(state.settleTimer);
441
599
  clearTimeout(state.pageTimer);
442
600
  state.pageTimer = 0;
443
601
  state.paging = false;
@@ -454,13 +612,18 @@ window.__ModuleLoader__.load({
454
612
  function startPageIn() {
455
613
  if (state.scope !== "whole" || !state.query || !isOpen()) return;
456
614
  var token = ++state.pageToken;
457
- var resolved = resolveFace(state.ctx);
458
- if (!resolved) {
615
+ var target = state.target;
616
+ if (!state.adapter || !state.adapter.valid(target)) {
617
+ setStatus(unavailableStatus(), false);
618
+ return;
619
+ }
620
+ var face = state.adapter.face(target);
621
+ if (!face) {
459
622
  setStatus(L.noService, false);
460
623
  return;
461
624
  }
462
- state.sessionId = resolved.id;
463
- if (faceHasMore(resolved.face) === false) {
625
+ state.sessionId = target.id;
626
+ if (faceHasMore(face) === false) {
464
627
  setStatus(L.allLoaded, false);
465
628
  return;
466
629
  }
@@ -469,11 +632,11 @@ window.__ModuleLoader__.load({
469
632
  setStatus(L.loading.replace("{n}", "0"), true);
470
633
  runPageIn({
471
634
  maxPages: MAX_PAGES,
472
- isCancelled: function () { return token !== state.pageToken || !isOpen(); },
473
- hasMore: function () { return faceHasMore(resolved.face); },
474
- loadOlder: function () { return Promise.resolve(resolved.face.loadOlder()); },
475
- signature: domSignature,
476
- waitChange: waitForChange,
635
+ isCancelled: function () { return token !== state.pageToken || !isOpen() || !state.adapter.valid(target); },
636
+ hasMore: function () { return faceHasMore(face); },
637
+ loadOlder: function () { return Promise.resolve(face.loadOlder()); },
638
+ signature: function () { return domSignature(target.root); },
639
+ waitChange: function (before) { return waitForChange(before, target.root, token); },
477
640
  onProgress: function (pages) {
478
641
  if (token !== state.pageToken) return;
479
642
  state.pages = pages;
@@ -485,25 +648,35 @@ window.__ModuleLoader__.load({
485
648
  state.paging = false;
486
649
  state.pages = result.pages;
487
650
  if (result.reason === "capped") setStatus(L.capped.replace("{n}", String(result.pages)), false);
488
- else if (result.reason === "stalled" || result.reason === "error") setStatus(L.stalled, false);
651
+ else if (result.reason !== "done") setStatus(L.incomplete, false);
489
652
  else setStatus(result.pages > 0 ? L.allLoaded : "", false);
490
653
  runSearch(true);
491
654
  });
492
655
  }
493
656
 
494
- /** Drop everything paged for the previous session when the user switches. */
657
+ function syncTarget(target, passive, retainNeutral) {
658
+ if (!state.adapter) return;
659
+ var selected = passive ? state.adapter.refresh(state.target) : state.adapter.select(target || document.activeElement, state.target, retainNeutral);
660
+ if (selected && state.target && selected.id === state.target.id && selected.anchor === state.target.anchor && selected.root === state.target.root) return;
661
+ if (!selected && !state.target) {
662
+ if (isOpen()) runSearch(false);
663
+ return;
664
+ }
665
+ cancelPageIn();
666
+ stopObserver();
667
+ state.target = selected;
668
+ state.sessionId = selected ? selected.id : null;
669
+ if (!isOpen()) return;
670
+ setStatus(selected ? "" : unavailableStatus(), false);
671
+ runSearch(false);
672
+ startObserver();
673
+ if (selected && state.scope === "whole" && state.query) schedulePageIn();
674
+ }
675
+
495
676
  function watchSession() {
496
677
  clearInterval(state.sessionTimer);
497
678
  state.sessionTimer = setInterval(function () {
498
- if (!isOpen()) return;
499
- var resolved = resolveFace(state.ctx);
500
- var id = resolved ? resolved.id : null;
501
- if (id === state.sessionId) return;
502
- state.sessionId = id;
503
- cancelPageIn();
504
- setStatus("", false);
505
- runSearch(false);
506
- if (state.scope === "whole" && state.query) schedulePageIn();
679
+ if (isOpen()) syncTarget(null, true);
507
680
  }, SESSION_POLL_MS);
508
681
  }
509
682
 
@@ -582,7 +755,7 @@ window.__ModuleLoader__.load({
582
755
  status.addEventListener("click", function () {
583
756
  if (!state.paging) return;
584
757
  cancelPageIn();
585
- setStatus(L.stalled, false);
758
+ setStatus(L.incomplete, false);
586
759
  });
587
760
 
588
761
  bar.appendChild(input);
@@ -602,13 +775,14 @@ window.__ModuleLoader__.load({
602
775
  document.body.appendChild(bar);
603
776
  }
604
777
 
605
- function open() {
778
+ function open(target) {
606
779
  if (typeof document === "undefined" || document.body === null) return;
780
+ syncTarget(target || document.activeElement, false, true);
607
781
  if (!state.bar) buildBar();
608
782
  state.bar.style.display = "flex";
609
783
  var selection = "";
610
784
  try { selection = String(window.getSelection()).trim(); } catch (error) {}
611
- if (selection && selection.length <= 200 && selection !== state.query) {
785
+ if (state.target && selection && selection.length <= 200 && selection !== state.query) {
612
786
  state.input.value = selection;
613
787
  state.query = selection;
614
788
  }
@@ -625,6 +799,7 @@ window.__ModuleLoader__.load({
625
799
  stopWatchingSession();
626
800
  if (state.bar) state.bar.style.display = "none";
627
801
  state.query = "";
802
+ if (state.input) state.input.value = "";
628
803
  state.ranges = [];
629
804
  state.index = -1;
630
805
  setStatus("", false);
@@ -638,20 +813,57 @@ window.__ModuleLoader__.load({
638
813
  }
639
814
 
640
815
  function apply(ctx) {
816
+ var react = require("react");
641
817
  state.ctx = ctx;
642
- injectCss();
643
- var ops = {
644
- open: open,
645
- close: close,
646
- goTo: goTo,
647
- isOpen: isOpen,
648
- isBarInput: function (target) { return target === state.input; }
649
- };
650
- window.addEventListener("keydown", createHandlers(ops), true);
818
+ state.adapter = createHostAdapter(ctx, function () {
819
+ syncTarget(null, true);
820
+ });
821
+ function SessionFind(props) {
822
+ var anchor = react.useRef(null);
823
+ react.useEffect(function () {
824
+ if (!anchor.current || !props.sessionId) return;
825
+ return state.adapter.mount(anchor.current, props.sessionId);
826
+ }, [props.sessionId]);
827
+ return react.createElement("button", {
828
+ ref: anchor, type: "button", title: L.find, "aria-label": L.find,
829
+ "data-find-all-session": props.sessionId,
830
+ onClick: function (event) { open(event.currentTarget); }
831
+ }, "⌕");
832
+ }
833
+ ctx.slots.inject("conversation.session.header.utilities", function () {
834
+ return ctx.slots.register({ name: "conversation.session.header.utilities", id: "dsh-find-all" }, SessionFind);
835
+ });
836
+ ctx.effect(function () {
837
+ injectCss();
838
+ var handler = createHandlers({
839
+ open: open, close: close, goTo: goTo, isOpen: isOpen,
840
+ isBarInput: function (target) { return target === state.input; }
841
+ });
842
+ function focus(event) {
843
+ if (!state.bar || !state.bar.contains(event.target)) syncTarget(event.target);
844
+ }
845
+ window.addEventListener("keydown", handler, true);
846
+ window.addEventListener("focusin", focus, true);
847
+ window.addEventListener("pointerdown", focus, true);
848
+ return function () {
849
+ window.removeEventListener("keydown", handler, true);
850
+ window.removeEventListener("focusin", focus, true);
851
+ window.removeEventListener("pointerdown", focus, true);
852
+ close();
853
+ if (state.bar) state.bar.remove();
854
+ var style = document.querySelector('style[data-plugin-css="dsh-find-all/bar.css"]');
855
+ if (style) style.remove();
856
+ state.bar = state.input = state.count = state.status = state.scopeBtn = null;
857
+ state.adapter.clear();
858
+ state.target = state.ctx = null;
859
+ };
860
+ }, "find-all: browser lifecycle");
651
861
  }
652
862
 
653
863
  exports.apply = apply;
654
- exports.inject = ["sessions"];
864
+ exports.inject = ["sessions", "slots"];
865
+ exports.resolveScope = resolveScope;
866
+ exports.createHostAdapter = createHostAdapter;
655
867
  exports.findIndices = findIndices; // for tests
656
868
  exports.createHandlers = createHandlers; // for tests
657
869
  exports.runPageIn = runPageIn; // for tests
package/package.json CHANGED
@@ -1,76 +1,83 @@
1
- {
2
- "name": "@ryuu-64/dsh-find-all",
3
- "version": "0.1.3",
4
- "description": "Ctrl+F find bar for the DSH desktop app: it searches the whole conversation, not just the messages already on screen.",
5
- "type": "module",
6
- "license": "MIT",
7
- "main": "lib/index.js",
8
- "exports": {
9
- ".": "./lib/index.js",
10
- "./client": "./lib/client.js",
11
- "./package.json": "./package.json"
12
- },
13
- "files": [
14
- "lib",
15
- "cordis.patch.yml",
16
- "README.md",
17
- "CHANGELOG.md",
18
- "NOTICE.md",
19
- "LICENSE"
20
- ],
21
- "author": "Ryuu-64",
22
- "contributors": [
23
- "secyborg (upstream dsh-find-bar, MIT)"
24
- ],
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/Ryuu-64/dsh-find-all.git"
28
- },
29
- "homepage": "https://github.com/Ryuu-64/dsh-find-all#readme",
30
- "bugs": {
31
- "url": "https://github.com/Ryuu-64/dsh-find-all/issues"
32
- },
33
- "publishConfig": {
34
- "access": "public",
35
- "registry": "https://registry.npmjs.org/"
36
- },
37
- "peerDependencies": {
38
- "@deepseek-ai/dsh-api-session-controller": ">=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-0 <0.2.0-0",
39
- "@deepseek-ai/dsh-client-ui-chat": ">=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-0 <0.2.0-0",
40
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-0 <0.2.0-0"
41
- },
42
- "scripts": {
43
- "test": "node --test \"test/**/*.test.mjs\"",
44
- "check:registration": "node scripts/check-client-registration.mjs",
45
- "check": "node --check lib/index.js && node --check lib/client.js && npm run check:registration && node --test \"test/**/*.test.mjs\"",
46
- "verify:release": "node scripts/verify-release.mjs",
47
- "install:local": "node scripts/install-local.mjs",
48
- "deploy:profile": "node scripts/deploy-profile.mjs"
49
- },
50
- "keywords": [
51
- "dsh",
52
- "dsh-plugin",
53
- "deepseek-harness",
54
- "find",
55
- "search",
56
- "conversation"
57
- ],
58
- "dsh": {
59
- "bundle": {
60
- "patch": "./cordis.patch.yml"
61
- },
62
- "client": {
63
- "inject": [
64
- "@deepseek-ai/dsh-api-session-controller",
65
- "@deepseek-ai/dsh-client-ui-conversation",
66
- "@deepseek-ai/dsh-client-ui-chat"
67
- ],
68
- "platform": "web"
69
- },
70
- "compatibility": {
71
- "dshReleases": {
72
- "0.1.5-rc.2": "compatible"
73
- }
74
- }
75
- }
76
- }
1
+ {
2
+ "name": "@ryuu-64/dsh-find-all",
3
+ "version": "0.1.5",
4
+ "description": "Ctrl+F find bar for the DSH desktop app: it searches the whole conversation, not just the messages already on screen.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "lib",
15
+ "cordis.patch.yml",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "NOTICE.md",
19
+ "docs/compatibility.md",
20
+ "LICENSE"
21
+ ],
22
+ "author": "Ryuu-64",
23
+ "contributors": [
24
+ "secyborg (upstream dsh-find-bar, MIT)"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Ryuu-64/dsh-find-all.git"
29
+ },
30
+ "homepage": "https://github.com/Ryuu-64/dsh-find-all#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/Ryuu-64/dsh-find-all/issues"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "peerDependencies": {
39
+ "@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
40
+ "@deepseek-ai/dsh-client-ui-chat": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
41
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
42
+ },
43
+ "scripts": {
44
+ "test": "node --test \"test/**/*.test.mjs\"",
45
+ "check:registration": "node scripts/check-client-registration.mjs",
46
+ "check": "node --check lib/index.js && node --check lib/client.js && npm run check:registration && node --test \"test/**/*.test.mjs\"",
47
+ "verify:release": "node scripts/verify-release.mjs",
48
+ "install:local": "node scripts/install-local.mjs",
49
+ "deploy:profile": "node scripts/deploy-profile.mjs"
50
+ },
51
+ "keywords": [
52
+ "dsh",
53
+ "dsh-plugin",
54
+ "deepseek-harness",
55
+ "find",
56
+ "search",
57
+ "conversation"
58
+ ],
59
+ "dsh": {
60
+ "bundle": {
61
+ "patch": "./cordis.patch.yml"
62
+ },
63
+ "client": {
64
+ "inject": [
65
+ "@deepseek-ai/dsh-api-session-controller",
66
+ "@deepseek-ai/dsh-client-ui-conversation",
67
+ "@deepseek-ai/dsh-client-ui-chat"
68
+ ],
69
+ "platform": "web"
70
+ },
71
+ "compatibility": {
72
+ "dshReleases": {
73
+ "0.1.5-rc.2": "compatible"
74
+ }
75
+ }
76
+ },
77
+ "devDependencies": {
78
+ "jsdom": "27.4.0",
79
+ "playwright": "1.56.1",
80
+ "react": "18.3.1",
81
+ "react-dom": "18.3.1"
82
+ }
83
+ }