@ryuu-64/dsh-find-all 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/NOTICE.md +43 -0
- package/README.fork-origin.md +48 -0
- package/README.md +75 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +660 -0
- package/lib/index.js +12 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 版权与致谢 / Copyright and credits
|
|
2
|
+
|
|
3
|
+
## 上游项目 / Upstream
|
|
4
|
+
|
|
5
|
+
本仓库是 [secyborg/dsh-find-bar](https://github.com/secyborg/dsh-find-bar) 的 fork。
|
|
6
|
+
|
|
7
|
+
| 项 | 值 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| 上游项目 | `dsh-find-bar` |
|
|
10
|
+
| 上游作者 | secyborg |
|
|
11
|
+
| 上游仓库 | https://github.com/secyborg/dsh-find-bar |
|
|
12
|
+
| Fork 依据的快照 | commit `ce7eb75efe94d768127c5977c649ecd950a9356c`("initial release: 0.1.0",2026-08-25) |
|
|
13
|
+
| 上游许可证 | MIT,`Copyright (c) 2026 secyborg` |
|
|
14
|
+
|
|
15
|
+
上游的 MIT 许可证要求"上述版权声明和许可声明应包含在本软件的所有副本或实质性部分中"。本仓库据此**完整保留**了:
|
|
16
|
+
|
|
17
|
+
- `LICENSE` —— 上游许可证原文,**未作任何修改**(包括版权行);
|
|
18
|
+
- `README.fork-origin.md` —— 上游 README 原文,未作修改;
|
|
19
|
+
- `lib/index.js`、`lib/client.js`、`test/find-logic.test.mjs` 的文件头注释均注明来源为上游 fork。
|
|
20
|
+
|
|
21
|
+
## 本 fork 的修改 / Modifications in this fork
|
|
22
|
+
|
|
23
|
+
相对上游快照 `ce7eb75`,本仓库改动如下(原上游功能一律保留):
|
|
24
|
+
|
|
25
|
+
1. 插件的名称与 cordis 行 id:`dsh-find-bar` → `dsh-find-all`(npm 包名:`@ryuu-64/dsh-find-all`);
|
|
26
|
+
2. **新增"整段会话"搜索**:通过会话服务 `ctx.sessions`(`binding(id).session.loadOlder()` + 快照里的 `hasMore`)把尚未加载的历史分页拉进页面,每页之后重跑匹配,使命中计数覆盖整段会话;宿主读不到 `hasMore` 时退化为"翻到页面不再变化为止";
|
|
27
|
+
3. 新增范围切换(`整段` / `本页`)、进度提示(含点击停止)、加载页数上限与会话切换时的状态重置;
|
|
28
|
+
4. `test/find-logic.test.mjs` 由上游测试改写为 `node:test` 并扩充:新增匹配器、键盘路由与分页状态机的 19 项用例;
|
|
29
|
+
5. 新增 README、`NOTICE.md`(本文件)与仓库元数据。
|
|
30
|
+
|
|
31
|
+
上游原有的查找条 UI、CSS Custom Highlight 高亮、`n/N` 计数、回车/`⌘G`/`F3` 导航、`window.find()` 退化路径**均未改动逻辑**。
|
|
32
|
+
|
|
33
|
+
## 本 fork 的版权 / Copyright of this fork
|
|
34
|
+
|
|
35
|
+
`LICENSE` 是标准的 MIT 双版权署名,两行都写明了:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Copyright (c) 2026 secyborg ← 上游 dsh-find-bar
|
|
39
|
+
Copyright (c) 2026 Ryuu-64 ← 本 fork dsh-find-all 的修改部分
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- 上游部分的版权归 **secyborg**,本仓库不对上游代码主张任何额外权利;
|
|
43
|
+
- 本仓库新增/修改的部分版权归 **Ryuu-64**,同样以 MIT 发布。任何人使用本仓库时,这两行版权声明都要一并保留。
|
|
@@ -0,0 +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
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# dsh-find-all
|
|
2
|
+
|
|
3
|
+
DSH web 插件:给桌面 App(Electron 壳)补上 **⌘F / Ctrl+F 页内查找**,并且**搜的是整段会话,不只是屏幕上已经渲染出来的那一截**。
|
|
4
|
+
|
|
5
|
+
> **Fork 自 [secyborg/dsh-find-bar](https://github.com/secyborg/dsh-find-bar)**(作者 secyborg,MIT)。
|
|
6
|
+
> Fork 依据的快照:commit `ce7eb75efe94d768127c5977c649ecd950a9356c`(0.1.0,2026-08-25)。
|
|
7
|
+
> 版权与许可是 MIT 双署名:`LICENSE` 里**同时**保留上游 `Copyright (c) 2026 secyborg` 与本 fork `Copyright (c) 2026 Ryuu-64`;上游 README 原样留作 `README.fork-origin.md`;改动清单与致谢见 `NOTICE.md`。
|
|
8
|
+
>
|
|
9
|
+
> 本仓库只做一件上游做不到的事:**把没加载进来的历史翻进来再搜**。
|
|
10
|
+
|
|
11
|
+
## 为什么需要它
|
|
12
|
+
|
|
13
|
+
DSH 桌面窗口是 Electron,本身没有浏览器那种查找栏;而会话视图**一次只保留最近 50 条消息**在页面里(点一次「加载更早」再多 50 条)。所以用普通查找条搜一个存在于更早历史里的词,结果永远是 0——哪怕这段会话里明明有。
|
|
14
|
+
|
|
15
|
+
本插件在你输入关键词后,通过会话服务把更早的历史一页页加载进来,每加载一页就重搜一次,直到整段会话都在页面里。于是命中计数和高亮覆盖的是**整个会话**。
|
|
16
|
+
|
|
17
|
+
## 功能
|
|
18
|
+
|
|
19
|
+
- **⌘F / Ctrl+F** 唤起查找条(自动带入当前选中的文字)
|
|
20
|
+
- **范围切换**:`整段`(默认)↔ `本页`。点一下那个胶囊按钮切换
|
|
21
|
+
- **整段模式**:自动分页加载历史,边加载边更新高亮与计数
|
|
22
|
+
- **进度可见**:`正在加载更早的历史… 已 3 页`,**点一下这段文字即可停止**
|
|
23
|
+
- **安全上限**:最多加载 400 页(约 2 万条消息);历史不再增长时会自行停止(宿主没有 `hasMore` 也能收敛)
|
|
24
|
+
- **导航**:Enter / Shift+Enter、⌘G / F3(+Shift)、Esc 关闭并清除高亮
|
|
25
|
+
- **高亮**:CSS Custom Highlight API 绘制,不改 DOM,与 React 渲染零冲突;不支持该 API 的环境退回 `window.find()`
|
|
26
|
+
- 切换会话时自动重置并重新开始搜索
|
|
27
|
+
|
|
28
|
+
## 它是怎么搜到"还没显示出来"的内容的
|
|
29
|
+
|
|
30
|
+
插件先问会话服务"现在是哪个会话",然后一页一页地请它把更早的消息加载出来(一页 50 条)。每加载完一页就重新搜一遍,所以计数和高亮最后覆盖的是整段会话。
|
|
31
|
+
"还有没有更早的"由会话服务自己报;报不出来的时候,它就一直翻到页面不再变化为止。
|
|
32
|
+
|
|
33
|
+
(给开发者:用的是客户端 `sessions` 服务——`ctx.sessions.list.getSnapshot().current` 取当前会话、`ctx.sessions.binding(id).session` 取会话面,翻页调 `loadOlder()`,判断用快照里的 `hasMore`。)
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
dsh plugin --profile desktop add @ryuu-64/dsh-find-all
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
装完**完整重启一次 DSH Desktop**(插件清单只在启动时装载)。
|
|
42
|
+
|
|
43
|
+
不想走 npm 就装本地源码(改完不用重新打包,重启即可):
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
dsh plugin --profile desktop add link:/path/to/dsh-find-all
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> 注意:它与 `dsh-find-bar` **都会抢 Ctrl+F**,请只保留一个:
|
|
50
|
+
> ```sh
|
|
51
|
+
> dsh plugin --profile desktop remove dsh-find-bar
|
|
52
|
+
> ```
|
|
53
|
+
|
|
54
|
+
## 卸载 / 回退
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
dsh plugin --profile desktop remove @ryuu-64/dsh-find-all # 卸载
|
|
58
|
+
dsh plugin --profile desktop add dsh-find-bar # 退回上游那个插件
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 已知限制
|
|
62
|
+
|
|
63
|
+
- 大小写不敏感,没有"区分大小写"开关
|
|
64
|
+
- 单次搜索最多记 5000 个命中
|
|
65
|
+
- 折叠起来的工具调用/思考块,其正文不在 DOM 里,搜不到(需要先展开)
|
|
66
|
+
- 第一次搜一个很长的会话要等几秒:那是真的在读历史(856 条 ≈ 17 页)
|
|
67
|
+
|
|
68
|
+
## 开发
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm test # 匹配逻辑、键盘路由、分页循环的单元测试
|
|
72
|
+
npm run check
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`lib/client.js` 就是构建产物(与生态里其它插件一致:直接由 `window.__ModuleLoader__` 装载),没有打包步骤。
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
// dsh-find-all — client half (v0.1).
|
|
2
|
+
//
|
|
3
|
+
// Forked from secyborg/dsh-find-bar (MIT, v0.1.0,
|
|
4
|
+
// https://github.com/secyborg/dsh-find-bar, snapshot commit
|
|
5
|
+
// ce7eb75efe94d768127c5977c649ecd950a9356c): the same find bar, plus the
|
|
6
|
+
// one thing it could not do — search the WHOLE conversation instead of only the
|
|
7
|
+
// part of it that happens to be rendered. Upstream copyright and licence are
|
|
8
|
+
// retained verbatim in LICENSE; see NOTICE.md for the modification list.
|
|
9
|
+
//
|
|
10
|
+
// Why a plain find bar is not enough in DSH Desktop:
|
|
11
|
+
// * The desktop window is an Electron shell, and Electron ships no
|
|
12
|
+
// find-in-page bar at all (the browser's Cmd+F is Chrome UI, not Blink).
|
|
13
|
+
// * The chat view keeps only a window of the session in the page: the tail
|
|
14
|
+
// page (50 messages) plus 50 more per "load earlier". Everything older is
|
|
15
|
+
// still on disk and invisible to any DOM search.
|
|
16
|
+
// So typing a word that lives in the unloaded part of the conversation finds
|
|
17
|
+
// nothing, even though the conversation demonstrably contains it.
|
|
18
|
+
//
|
|
19
|
+
// What this plugin adds:
|
|
20
|
+
// * A scope toggle in the bar: 「整段 / Whole」 (default) and 「本页 / Page」.
|
|
21
|
+
// * In whole-conversation mode a search pages the older history in through
|
|
22
|
+
// the session service (ctx.sessions.binding(id).session.loadOlder()),
|
|
23
|
+
// re-running the match pass after every page — so the count and the
|
|
24
|
+
// highlight set end up covering the entire session.
|
|
25
|
+
// * Progress is visible ("正在加载更早的历史… 已 3 页"), clickable to stop,
|
|
26
|
+
// capped at MAX_PAGES, and it stops by itself when the history stops
|
|
27
|
+
// growing (which also covers hosts that expose no `hasMore`).
|
|
28
|
+
//
|
|
29
|
+
// How it works without fighting React (unchanged from the fork):
|
|
30
|
+
// * Matches are collected as DOM Range objects (start/end offsets inside
|
|
31
|
+
// text nodes) via a TreeWalker that also descends into shadow roots.
|
|
32
|
+
// * Highlights use the CSS Custom Highlight API (CSS.highlights + Highlight)
|
|
33
|
+
// which paints ranges WITHOUT mutating the DOM — React keeps full
|
|
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).
|
|
37
|
+
//
|
|
38
|
+
// Keyboard:
|
|
39
|
+
// Cmd/Ctrl+F open (prefilled with the current selection)
|
|
40
|
+
// Enter next match Shift+Enter previous match
|
|
41
|
+
// Cmd/Ctrl+G / F3 next +Shift previous
|
|
42
|
+
// Esc close and clear highlights
|
|
43
|
+
|
|
44
|
+
window.__ModuleLoader__.load({
|
|
45
|
+
id: "dsh-find-all",
|
|
46
|
+
factory: () => {
|
|
47
|
+
var module = { exports: {} };
|
|
48
|
+
var exports = module.exports;
|
|
49
|
+
|
|
50
|
+
var BAR_ID = "dsh-find-all-root";
|
|
51
|
+
var HL_ALL = "dsh-find-all-hit";
|
|
52
|
+
var HL_CUR = "dsh-find-all-cur";
|
|
53
|
+
var MAX_MATCHES = 5000;
|
|
54
|
+
/** Safety cap on paged-in history: 400 pages x 50 messages. */
|
|
55
|
+
var MAX_PAGES = 400;
|
|
56
|
+
/** How long one page may take before the pager gives up on it. */
|
|
57
|
+
var PAGE_SETTLE_MS = 5000;
|
|
58
|
+
var PAGE_POLL_MS = 150;
|
|
59
|
+
/** How often the open bar notices that the user switched session. */
|
|
60
|
+
var SESSION_POLL_MS = 600;
|
|
61
|
+
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1 };
|
|
62
|
+
|
|
63
|
+
var ZH = typeof navigator !== "undefined" && /^zh/i.test(navigator.language || "");
|
|
64
|
+
var L = ZH ? {
|
|
65
|
+
placeholder: "查找整段会话…",
|
|
66
|
+
prev: "上一个匹配",
|
|
67
|
+
next: "下一个匹配",
|
|
68
|
+
close: "关闭查找",
|
|
69
|
+
cap: "已达匹配上限,仅显示前 {n} 条",
|
|
70
|
+
scopeWhole: "整段",
|
|
71
|
+
scopePage: "本页",
|
|
72
|
+
scopeToPage: "搜索范围:整段会话。点击改为只搜当前页面",
|
|
73
|
+
scopeToWhole: "搜索范围:仅当前页面。点击改为搜索整段会话",
|
|
74
|
+
loading: "正在加载更早的历史… 已 {n} 页",
|
|
75
|
+
allLoaded: "整段会话已就绪",
|
|
76
|
+
capped: "已到加载上限({n} 页),停止加载",
|
|
77
|
+
stalled: "没有更早的历史了",
|
|
78
|
+
stopTip: "点击停止加载",
|
|
79
|
+
noService: "取不到会话接口,只能搜当前页面"
|
|
80
|
+
} : {
|
|
81
|
+
placeholder: "Find in conversation…",
|
|
82
|
+
prev: "Previous match",
|
|
83
|
+
next: "Next match",
|
|
84
|
+
close: "Close find",
|
|
85
|
+
cap: "Match limit reached, showing first {n}",
|
|
86
|
+
scopeWhole: "Whole",
|
|
87
|
+
scopePage: "Page",
|
|
88
|
+
scopeToPage: "Scope: the whole conversation. Click to search only the rendered page",
|
|
89
|
+
scopeToWhole: "Scope: the rendered page only. Click to search the whole conversation",
|
|
90
|
+
loading: "Loading earlier history… {n} pages",
|
|
91
|
+
allLoaded: "Whole conversation loaded",
|
|
92
|
+
capped: "Page cap reached ({n}), stopped loading",
|
|
93
|
+
stalled: "No earlier history",
|
|
94
|
+
stopTip: "Click to stop loading",
|
|
95
|
+
noService: "Session service unavailable — rendered page only"
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
//#region pure helpers (exported for tests)
|
|
99
|
+
|
|
100
|
+
// All non-overlapping case-insensitive start indices of `query` in
|
|
101
|
+
// `text`; empty array for an empty query or no hits.
|
|
102
|
+
function findIndices(text, query) {
|
|
103
|
+
if (!query) return [];
|
|
104
|
+
var haystack = String(text).toLowerCase();
|
|
105
|
+
var needle = String(query).toLowerCase();
|
|
106
|
+
var out = [];
|
|
107
|
+
var i = haystack.indexOf(needle);
|
|
108
|
+
while (i !== -1) {
|
|
109
|
+
out.push(i);
|
|
110
|
+
if (out.length >= MAX_MATCHES) break;
|
|
111
|
+
i = haystack.indexOf(needle, i + needle.length);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Keyboard routing, isolated so tests can drive it with spies.
|
|
117
|
+
function createHandlers(ops) {
|
|
118
|
+
return function onKeydown(event) {
|
|
119
|
+
var meta = event.metaKey || event.ctrlKey;
|
|
120
|
+
var key = event.key;
|
|
121
|
+
if (meta && (key === "f" || key === "F")) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
ops.open();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!ops.isOpen()) return;
|
|
128
|
+
if (key === "Escape") {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
ops.close();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
var jumpKey = (meta && (key === "g" || key === "G")) || key === "F3";
|
|
134
|
+
if (jumpKey) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
ops.goTo(event.shiftKey ? -1 : 1);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (key === "Enter" && !event.isComposing && ops.isBarInput(event.target)) {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
ops.goTo(event.shiftKey ? -1 : 1);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Page older history in until the conversation stops growing.
|
|
148
|
+
*
|
|
149
|
+
* Every dependency is injected, so the loop is testable without a DOM or
|
|
150
|
+
* a host: the caller supplies how to read `hasMore`, how to load a page,
|
|
151
|
+
* how to measure the rendered history, and how to wait for that measure
|
|
152
|
+
* to change.
|
|
153
|
+
*
|
|
154
|
+
* @param opts - { maxPages, isCancelled, hasMore, loadOlder, signature,
|
|
155
|
+
* waitChange, onProgress }.
|
|
156
|
+
* `hasMore` returns true/false/null (null = the host cannot say, so the
|
|
157
|
+
* pager keeps going until a page changes nothing).
|
|
158
|
+
* @returns { pages, reason } where reason is one of done | cancelled |
|
|
159
|
+
* capped | stalled | error.
|
|
160
|
+
*/
|
|
161
|
+
async function runPageIn(opts) {
|
|
162
|
+
var pages = 0;
|
|
163
|
+
var reason = "done";
|
|
164
|
+
for (;;) {
|
|
165
|
+
if (opts.isCancelled()) { reason = "cancelled"; break; }
|
|
166
|
+
if (opts.hasMore() === false) { reason = "done"; break; }
|
|
167
|
+
if (pages >= opts.maxPages) { reason = "capped"; break; }
|
|
168
|
+
var before = opts.signature();
|
|
169
|
+
try {
|
|
170
|
+
await opts.loadOlder();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
reason = "error";
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
if (opts.isCancelled()) { reason = "cancelled"; break; }
|
|
176
|
+
if (!(await opts.waitChange(before))) { reason = "stalled"; break; }
|
|
177
|
+
pages += 1;
|
|
178
|
+
if (opts.onProgress) opts.onProgress(pages);
|
|
179
|
+
}
|
|
180
|
+
return { pages: pages, reason: reason };
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
|
|
184
|
+
function cssText() {
|
|
185
|
+
return "" +
|
|
186
|
+
"#" + BAR_ID + "{position:fixed;top:46px;right:16px;z-index:2147483000;align-items:center;gap:6px;padding:6px 8px;display:flex;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:var(--dsw-shadow-lv2)}" +
|
|
187
|
+
"#" + BAR_ID + " input{width:210px;height:28px;color:var(--dsw-alias-label-primary);font:13px/1 var(--dsw-font-family);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;outline:none;padding:0 8px}" +
|
|
188
|
+
"#" + BAR_ID + " input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}" +
|
|
189
|
+
"#" + BAR_ID + " .count{min-width:52px;color:var(--dsw-alias-label-tertiary);font:12px/1 var(--dsw-font-family);font-variant-numeric:tabular-nums;text-align:center}" +
|
|
190
|
+
"#" + BAR_ID + " .status{max-width:260px;color:var(--dsw-alias-label-tertiary);font:12px/1.2 var(--dsw-font-family);text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
|
|
191
|
+
"#" + BAR_ID + " .status[data-busy]{cursor:pointer;color:var(--dsw-alias-state-business-primary)}" +
|
|
192
|
+
"#" + BAR_ID + " .scope{width:auto;padding:0 9px;color:var(--dsw-alias-label-secondary);font:12px/1 var(--dsw-font-family);background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px}" +
|
|
193
|
+
"#" + BAR_ID + " .scope[data-whole]{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-state-business-primary)}" +
|
|
194
|
+
"#" + BAR_ID + " button{height:26px;min-width:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;align-items:center;justify-content:center;display:inline-flex}" +
|
|
195
|
+
"#" + BAR_ID + " button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}" +
|
|
196
|
+
"#" + BAR_ID + " svg{pointer-events:none}" +
|
|
197
|
+
"::highlight(" + HL_ALL + "){background-color:rgba(245,197,24,.42)}" +
|
|
198
|
+
"::highlight(" + HL_CUR + "){background-color:#f5a623;color:#141414}";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function injectCss() {
|
|
202
|
+
if (typeof document === "undefined") return;
|
|
203
|
+
var tagId = "dsh-find-all/bar.css";
|
|
204
|
+
if (document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
205
|
+
var tag = document.createElement("style");
|
|
206
|
+
tag.setAttribute("data-plugin-css", tagId);
|
|
207
|
+
tag.textContent = cssText();
|
|
208
|
+
document.head.appendChild(tag);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function highlightsSupported() {
|
|
213
|
+
return typeof CSS !== "undefined" && CSS.highlights && typeof Highlight === "function";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Collect text nodes under `root`, descending into shadow roots and
|
|
217
|
+
// skipping the find bar itself plus invisible metadata tags.
|
|
218
|
+
function collectTextNodes(root, into) {
|
|
219
|
+
if (!root) return into;
|
|
220
|
+
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
221
|
+
acceptNode: function (node) {
|
|
222
|
+
var parent = node.parentElement;
|
|
223
|
+
if (parent === null) return NodeFilter.FILTER_REJECT;
|
|
224
|
+
if (SKIP_TAGS[parent.tagName]) return NodeFilter.FILTER_REJECT;
|
|
225
|
+
if (parent.closest && parent.closest("#" + BAR_ID)) return NodeFilter.FILTER_REJECT;
|
|
226
|
+
return node.data.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
var node;
|
|
230
|
+
while ((node = walker.nextNode()) !== null) {
|
|
231
|
+
into.push(node);
|
|
232
|
+
// descend shadow roots hosted by element siblings of this text
|
|
233
|
+
}
|
|
234
|
+
// shadow roots are not reached by TreeWalker — walk elements separately
|
|
235
|
+
var elems = root.querySelectorAll ? root.querySelectorAll("*") : [];
|
|
236
|
+
for (var i = 0; i < elems.length; i++) {
|
|
237
|
+
if (elems[i].shadowRoot) collectTextNodes(elems[i].shadowRoot, into);
|
|
238
|
+
}
|
|
239
|
+
return into;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function collectRanges(query) {
|
|
243
|
+
var ranges = [];
|
|
244
|
+
if (!query || typeof document === "undefined" || document.body === null) return ranges;
|
|
245
|
+
var nodes = collectTextNodes(document.body, []);
|
|
246
|
+
for (var i = 0; i < nodes.length && ranges.length < MAX_MATCHES; i++) {
|
|
247
|
+
var node = nodes[i];
|
|
248
|
+
var indices = findIndices(node.data, query);
|
|
249
|
+
for (var j = 0; j < indices.length && ranges.length < MAX_MATCHES; j++) {
|
|
250
|
+
var range = document.createRange();
|
|
251
|
+
range.setStart(node, indices[j]);
|
|
252
|
+
range.setEnd(node, indices[j] + query.length);
|
|
253
|
+
ranges.push(range);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return ranges;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function makeHighlight(ranges) {
|
|
260
|
+
if (ranges.length === 0) return new Highlight();
|
|
261
|
+
var highlight = new Highlight(ranges[0]);
|
|
262
|
+
for (var i = 1; i < ranges.length; i++) highlight.add(ranges[i]);
|
|
263
|
+
return highlight;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function scrollToRange(range) {
|
|
267
|
+
var el = range.startContainer.parentElement;
|
|
268
|
+
if (el && el.scrollIntoView) {
|
|
269
|
+
try { el.scrollIntoView({ block: "center", behavior: "smooth" }); } catch (error) {}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
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;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Resolve once the rendered history differs from `before`, or the page times out. */
|
|
280
|
+
function waitForChange(before) {
|
|
281
|
+
return new Promise(function (resolve) {
|
|
282
|
+
var started = Date.now();
|
|
283
|
+
(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);
|
|
287
|
+
})();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
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
|
+
function faceHasMore(face) {
|
|
316
|
+
try {
|
|
317
|
+
if (typeof face.getSnapshot !== "function") return null;
|
|
318
|
+
var snapshot = face.getSnapshot();
|
|
319
|
+
return snapshot && typeof snapshot.hasMore === "boolean" ? snapshot.hasMore : null;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
|
|
326
|
+
//#region bar UI + state
|
|
327
|
+
|
|
328
|
+
var state = {
|
|
329
|
+
ctx: null,
|
|
330
|
+
bar: null,
|
|
331
|
+
input: null,
|
|
332
|
+
count: null,
|
|
333
|
+
status: null,
|
|
334
|
+
scopeBtn: null,
|
|
335
|
+
query: "",
|
|
336
|
+
ranges: [],
|
|
337
|
+
index: -1,
|
|
338
|
+
capped: false,
|
|
339
|
+
observer: null,
|
|
340
|
+
debounce: 0,
|
|
341
|
+
scope: "whole",
|
|
342
|
+
paging: false,
|
|
343
|
+
pageTimer: 0,
|
|
344
|
+
pageToken: 0,
|
|
345
|
+
pages: 0,
|
|
346
|
+
sessionId: null,
|
|
347
|
+
sessionTimer: 0
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
function paint() {
|
|
351
|
+
if (!highlightsSupported()) return;
|
|
352
|
+
CSS.highlights.delete(HL_ALL);
|
|
353
|
+
CSS.highlights.delete(HL_CUR);
|
|
354
|
+
if (state.ranges.length > 0) {
|
|
355
|
+
CSS.highlights.set(HL_ALL, makeHighlight(state.ranges));
|
|
356
|
+
if (state.index >= 0 && state.index < state.ranges.length) {
|
|
357
|
+
CSS.highlights.set(HL_CUR, makeHighlight([state.ranges[state.index]]));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function updateCount() {
|
|
363
|
+
if (!state.count) return;
|
|
364
|
+
var n = state.ranges.length;
|
|
365
|
+
if (!state.query) state.count.textContent = "";
|
|
366
|
+
else if (n === 0) state.count.textContent = state.paging ? "…" : "0/0";
|
|
367
|
+
else state.count.textContent = (state.index + 1) + "/" + n + (state.capped ? "+" : "");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function setStatus(text, busy) {
|
|
371
|
+
if (!state.status) return;
|
|
372
|
+
state.status.textContent = text || "";
|
|
373
|
+
if (busy) state.status.setAttribute("data-busy", "1");
|
|
374
|
+
else state.status.removeAttribute("data-busy");
|
|
375
|
+
updateCount();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function updateScopeButton() {
|
|
379
|
+
if (!state.scopeBtn) return;
|
|
380
|
+
var whole = state.scope === "whole";
|
|
381
|
+
state.scopeBtn.textContent = whole ? L.scopeWhole : L.scopePage;
|
|
382
|
+
state.scopeBtn.title = whole ? L.scopeToPage : L.scopeToWhole;
|
|
383
|
+
if (whole) state.scopeBtn.setAttribute("data-whole", "1");
|
|
384
|
+
else state.scopeBtn.removeAttribute("data-whole");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function runSearch(keepIndex) {
|
|
388
|
+
state.ranges = collectRanges(state.query);
|
|
389
|
+
state.capped = state.ranges.length >= MAX_MATCHES;
|
|
390
|
+
if (!keepIndex || state.index >= state.ranges.length) state.index = state.ranges.length > 0 ? 0 : -1;
|
|
391
|
+
paint();
|
|
392
|
+
updateCount();
|
|
393
|
+
if (state.index >= 0 && !highlightsSupported()) legacyFind(state.query, false);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function goTo(delta) {
|
|
397
|
+
var n = state.ranges.length;
|
|
398
|
+
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
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function scheduleRescan() {
|
|
416
|
+
// While a page-in is running the pager rescans itself after every page;
|
|
417
|
+
// rescanning on each mutation as well would walk the whole DOM per row.
|
|
418
|
+
if (state.paging) return;
|
|
419
|
+
clearTimeout(state.debounce);
|
|
420
|
+
state.debounce = setTimeout(function () {
|
|
421
|
+
if (state.bar) runSearch(true);
|
|
422
|
+
}, 250);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function startObserver() {
|
|
426
|
+
if (state.observer || typeof MutationObserver === "undefined") return;
|
|
427
|
+
state.observer = new MutationObserver(scheduleRescan);
|
|
428
|
+
state.observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function stopObserver() {
|
|
432
|
+
if (state.observer) { state.observer.disconnect(); state.observer = null; }
|
|
433
|
+
clearTimeout(state.debounce);
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
|
|
437
|
+
//#region paging the older history in
|
|
438
|
+
|
|
439
|
+
function cancelPageIn() {
|
|
440
|
+
state.pageToken += 1;
|
|
441
|
+
clearTimeout(state.pageTimer);
|
|
442
|
+
state.pageTimer = 0;
|
|
443
|
+
state.paging = false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function schedulePageIn() {
|
|
447
|
+
clearTimeout(state.pageTimer);
|
|
448
|
+
state.pageTimer = setTimeout(function () {
|
|
449
|
+
state.pageTimer = 0;
|
|
450
|
+
startPageIn();
|
|
451
|
+
}, 250);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function startPageIn() {
|
|
455
|
+
if (state.scope !== "whole" || !state.query || !isOpen()) return;
|
|
456
|
+
var token = ++state.pageToken;
|
|
457
|
+
var resolved = resolveFace(state.ctx);
|
|
458
|
+
if (!resolved) {
|
|
459
|
+
setStatus(L.noService, false);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
state.sessionId = resolved.id;
|
|
463
|
+
if (faceHasMore(resolved.face) === false) {
|
|
464
|
+
setStatus(L.allLoaded, false);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
state.paging = true;
|
|
468
|
+
state.pages = 0;
|
|
469
|
+
setStatus(L.loading.replace("{n}", "0"), true);
|
|
470
|
+
runPageIn({
|
|
471
|
+
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,
|
|
477
|
+
onProgress: function (pages) {
|
|
478
|
+
if (token !== state.pageToken) return;
|
|
479
|
+
state.pages = pages;
|
|
480
|
+
setStatus(L.loading.replace("{n}", String(pages)), true);
|
|
481
|
+
runSearch(true);
|
|
482
|
+
}
|
|
483
|
+
}).then(function (result) {
|
|
484
|
+
if (token !== state.pageToken) return;
|
|
485
|
+
state.paging = false;
|
|
486
|
+
state.pages = result.pages;
|
|
487
|
+
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);
|
|
489
|
+
else setStatus(result.pages > 0 ? L.allLoaded : "", false);
|
|
490
|
+
runSearch(true);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Drop everything paged for the previous session when the user switches. */
|
|
495
|
+
function watchSession() {
|
|
496
|
+
clearInterval(state.sessionTimer);
|
|
497
|
+
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();
|
|
507
|
+
}, SESSION_POLL_MS);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function stopWatchingSession() {
|
|
511
|
+
clearInterval(state.sessionTimer);
|
|
512
|
+
state.sessionTimer = 0;
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
515
|
+
|
|
516
|
+
function icon(d, size) {
|
|
517
|
+
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
518
|
+
svg.setAttribute("width", size); svg.setAttribute("height", size);
|
|
519
|
+
svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("fill", "none");
|
|
520
|
+
svg.setAttribute("stroke", "currentColor"); svg.setAttribute("stroke-width", "2");
|
|
521
|
+
svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round");
|
|
522
|
+
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
523
|
+
path.setAttribute("d", d);
|
|
524
|
+
svg.appendChild(path);
|
|
525
|
+
return svg;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function button(label, d, onClick) {
|
|
529
|
+
var btn = document.createElement("button");
|
|
530
|
+
btn.type = "button";
|
|
531
|
+
btn.setAttribute("aria-label", label);
|
|
532
|
+
btn.title = label;
|
|
533
|
+
btn.appendChild(icon(d, 14));
|
|
534
|
+
btn.addEventListener("click", onClick);
|
|
535
|
+
return btn;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function onQueryInput(input) {
|
|
539
|
+
state.query = input.value;
|
|
540
|
+
cancelPageIn();
|
|
541
|
+
setStatus("", false);
|
|
542
|
+
runSearch(false);
|
|
543
|
+
if (state.ranges.length > 0 && highlightsSupported()) scrollToRange(state.ranges[state.index]);
|
|
544
|
+
if (state.scope === "whole" && state.query) schedulePageIn();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function toggleScope() {
|
|
548
|
+
state.scope = state.scope === "whole" ? "page" : "whole";
|
|
549
|
+
updateScopeButton();
|
|
550
|
+
if (state.scope === "page") {
|
|
551
|
+
cancelPageIn();
|
|
552
|
+
setStatus("", false);
|
|
553
|
+
runSearch(true);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (state.query) schedulePageIn();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function buildBar() {
|
|
560
|
+
var bar = document.createElement("div");
|
|
561
|
+
bar.id = BAR_ID;
|
|
562
|
+
bar.setAttribute("role", "search");
|
|
563
|
+
|
|
564
|
+
var input = document.createElement("input");
|
|
565
|
+
input.type = "text";
|
|
566
|
+
input.placeholder = L.placeholder;
|
|
567
|
+
input.setAttribute("aria-label", L.placeholder);
|
|
568
|
+
input.addEventListener("input", function () { onQueryInput(input); });
|
|
569
|
+
|
|
570
|
+
var scopeBtn = document.createElement("button");
|
|
571
|
+
scopeBtn.type = "button";
|
|
572
|
+
scopeBtn.className = "scope";
|
|
573
|
+
scopeBtn.addEventListener("click", toggleScope);
|
|
574
|
+
|
|
575
|
+
var count = document.createElement("span");
|
|
576
|
+
count.className = "count";
|
|
577
|
+
count.setAttribute("aria-live", "polite");
|
|
578
|
+
|
|
579
|
+
var status = document.createElement("span");
|
|
580
|
+
status.className = "status";
|
|
581
|
+
status.title = L.stopTip;
|
|
582
|
+
status.addEventListener("click", function () {
|
|
583
|
+
if (!state.paging) return;
|
|
584
|
+
cancelPageIn();
|
|
585
|
+
setStatus(L.stalled, false);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
bar.appendChild(input);
|
|
589
|
+
bar.appendChild(scopeBtn);
|
|
590
|
+
bar.appendChild(count);
|
|
591
|
+
bar.appendChild(button(L.prev, "m18 15-6-6-6 6", function () { goTo(-1); }));
|
|
592
|
+
bar.appendChild(button(L.next, "m6 9 6 6 6-6", function () { goTo(1); }));
|
|
593
|
+
bar.appendChild(status);
|
|
594
|
+
bar.appendChild(button(L.close, "M18 6 6 18M6 6l12 12", close));
|
|
595
|
+
|
|
596
|
+
state.bar = bar;
|
|
597
|
+
state.input = input;
|
|
598
|
+
state.count = count;
|
|
599
|
+
state.status = status;
|
|
600
|
+
state.scopeBtn = scopeBtn;
|
|
601
|
+
updateScopeButton();
|
|
602
|
+
document.body.appendChild(bar);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function open() {
|
|
606
|
+
if (typeof document === "undefined" || document.body === null) return;
|
|
607
|
+
if (!state.bar) buildBar();
|
|
608
|
+
state.bar.style.display = "flex";
|
|
609
|
+
var selection = "";
|
|
610
|
+
try { selection = String(window.getSelection()).trim(); } catch (error) {}
|
|
611
|
+
if (selection && selection.length <= 200 && selection !== state.query) {
|
|
612
|
+
state.input.value = selection;
|
|
613
|
+
state.query = selection;
|
|
614
|
+
}
|
|
615
|
+
state.input.focus();
|
|
616
|
+
state.input.select();
|
|
617
|
+
runSearch(false);
|
|
618
|
+
startObserver();
|
|
619
|
+
watchSession();
|
|
620
|
+
if (state.scope === "whole" && state.query) schedulePageIn();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function close() {
|
|
624
|
+
cancelPageIn();
|
|
625
|
+
stopWatchingSession();
|
|
626
|
+
if (state.bar) state.bar.style.display = "none";
|
|
627
|
+
state.query = "";
|
|
628
|
+
state.ranges = [];
|
|
629
|
+
state.index = -1;
|
|
630
|
+
setStatus("", false);
|
|
631
|
+
paint(); // clears registered highlights
|
|
632
|
+
updateCount();
|
|
633
|
+
stopObserver();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function isOpen() {
|
|
637
|
+
return !!state.bar && state.bar.style.display !== "none";
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function apply(ctx) {
|
|
641
|
+
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);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
exports.apply = apply;
|
|
654
|
+
exports.inject = ["sessions"];
|
|
655
|
+
exports.findIndices = findIndices; // for tests
|
|
656
|
+
exports.createHandlers = createHandlers; // for tests
|
|
657
|
+
exports.runPageIn = runPageIn; // for tests
|
|
658
|
+
return module.exports;
|
|
659
|
+
}
|
|
660
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// dsh-find-all — host half.
|
|
2
|
+
//
|
|
3
|
+
// Deliberately a no-op: the entire feature lives in the client bundle
|
|
4
|
+
// (lib/client.js). This file exists only so the Cordis loader has a valid
|
|
5
|
+
// plugin to mount when the bundle patch inserts the row.
|
|
6
|
+
|
|
7
|
+
export const name = "dsh-find-all";
|
|
8
|
+
export const inject = [];
|
|
9
|
+
|
|
10
|
+
export function apply() {
|
|
11
|
+
// nothing to do on the host side
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ryuu-64/dsh-find-all",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"NOTICE.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"author": "Ryuu-64",
|
|
21
|
+
"contributors": [
|
|
22
|
+
"secyborg (upstream dsh-find-bar, MIT)"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Ryuu-64/dsh-find-all.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/Ryuu-64/dsh-find-all#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/Ryuu-64/dsh-find-all/issues"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@deepseek-ai/dsh-api-session-controller": ">=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-0 <0.2.0-0",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-chat": ">=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-0 <0.2.0-0",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-0 <0.2.0-0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
43
|
+
"check": "node --check lib/index.js && node --check lib/client.js && node --test \"test/**/*.test.mjs\""
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"dsh",
|
|
47
|
+
"dsh-plugin",
|
|
48
|
+
"deepseek-harness",
|
|
49
|
+
"find",
|
|
50
|
+
"search",
|
|
51
|
+
"conversation"
|
|
52
|
+
],
|
|
53
|
+
"dsh": {
|
|
54
|
+
"bundle": {
|
|
55
|
+
"patch": "./cordis.patch.yml"
|
|
56
|
+
},
|
|
57
|
+
"client": {
|
|
58
|
+
"inject": [
|
|
59
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-chat"
|
|
62
|
+
],
|
|
63
|
+
"platform": "web"
|
|
64
|
+
},
|
|
65
|
+
"compatibility": {
|
|
66
|
+
"dshReleases": {
|
|
67
|
+
"0.1.5-rc.2": "compatible"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|