dsh-find-in-session 0.1.4 → 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 +24 -0
- package/README.md +8 -7
- package/lib/client.js +232 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5 — 2026-11-15
|
|
4
|
+
|
|
5
|
+
- Fix: the “located the event, but its text is not rendered yet” message could
|
|
6
|
+
stay on screen forever and ended up covering the composer. It was a
|
|
7
|
+
`position:fixed` overlay appended to `document.body`, and `showToast()` created
|
|
8
|
+
a fresh `<div>` per message while keeping one shared `setTimeout` id — a second
|
|
9
|
+
message inside the 2.2 s window cancelled the *first* element's dismissal, and
|
|
10
|
+
nothing else (closing the find bar, unloading the plugin) ever removed it. No
|
|
11
|
+
bookkeeping and no cap meant every failed jump could leave one more overlay
|
|
12
|
+
behind, all at `bottom:96px`, i.e. right on the message input box.
|
|
13
|
+
- The message is now a single status line inside the find panel
|
|
14
|
+
(`role="status"`, `aria-live="polite"`), directly above the result list: one
|
|
15
|
+
element, one timer, cleared by a new query, by a scope change, by `Esc`/close
|
|
16
|
+
and by plugin teardown. It cannot overlap the composer because it is not an
|
|
17
|
+
overlay at all.
|
|
18
|
+
- The hit whose jump failed also keeps a `未渲染` / `not rendered` marker in its
|
|
19
|
+
own row, so the reason survives the notice fading.
|
|
20
|
+
- Jump locating no longer claims “not rendered” for text that is on screen but
|
|
21
|
+
split across inline nodes (`code` / `strong` / links): the snippet is matched
|
|
22
|
+
against the squeezed (whitespace-free, case-folded) text of each event block
|
|
23
|
+
(`[data-chat-node-key]`), longest prefix first, then that block is scrolled to
|
|
24
|
+
and flashed. Blocks are cached and invalidated by the transcript mutation
|
|
25
|
+
observer, so paging does not re-walk the transcript on every poll.
|
|
26
|
+
|
|
3
27
|
## 0.1.4 — 2026-11-01
|
|
4
28
|
|
|
5
29
|
- Replaced the single-glyph `当 / 全` scope button with a labelled segmented
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ curl http://127.0.0.1:<port>/dsh-find-in-session/ping
|
|
|
66
66
|
| `Ctrl+F`(macOS `Cmd+F`) | 打开/聚焦查找条(会话头部也有放大镜按钮) |
|
|
67
67
|
| 输入关键词 | 即时高亮当前视图全部命中,下方异步出现「全部历史命中」列表 |
|
|
68
68
|
| `Enter` / `Shift+Enter` | 下一处 / 上一处(无视图内命中时在全历史命中间跳) |
|
|
69
|
-
| 点击历史命中 |
|
|
69
|
+
| 点击历史命中 | 翻页加载到该事件、滚动定位并闪烁;这条命中在视图里确实没有正文时,该行标注「未渲染」,查找条里再出现一行提示(提示在面板内,不会浮到输入框上) |
|
|
70
70
|
| `范围 [仅当前视图] [含已替换]` | `仅当前视图` = 只搜转写里显示的正文(`surface: current`,默认);`含已替换` = 连被改写、压缩掉的旧事件一起搜。两段各带匹配的 tooltip |
|
|
71
71
|
| `Esc` | 关闭查找条并清除高亮 |
|
|
72
72
|
|
|
@@ -82,17 +82,18 @@ curl http://127.0.0.1:<port>/dsh-find-in-session/ping
|
|
|
82
82
|
|
|
83
83
|
- **不改宿主、不加依赖**:全历史扫描走 `filterEvents()`,它是 provider-independent 的字面量扫描(`text` 子句大小写不敏感、空白弹性),所以**不需要**挂 `dsh-session-query-sqlite` 的 FTS、不需要建索引库,也不与官方 `dsh-experimental-session-search` 抢服务。
|
|
84
84
|
- **高亮用 CSS Custom Highlight API**:不给文本节点套 `<mark>`,避免与 React 重渲染、流式增量、代码块复制打架;宿主不支持该 API 时退化为「只滚动不染色」。
|
|
85
|
-
- **定位靠文本而不是事件键**:DSH 的 Chat 没有公开「按事件跳转」的 API,但 DOM 上有 `[data-chat-flow]` / `[data-chat-node-key]
|
|
85
|
+
- **定位靠文本而不是事件键**:DSH 的 Chat 没有公开「按事件跳转」的 API,但 DOM 上有 `[data-chat-flow]` / `[data-chat-node-key]`。插件先在单个文本节点里匹配命中片段里最长的词,再把片段的**去空白**形式和每个事件块(`[data-chat-node-key]`)比对一次——转写是 markdown 渲染,同一句话会被行内 `code` / `strong` / 链接切成多个文本节点,只按单节点匹配会把屏幕上的文字误判成「未渲染」。块文本按 DOM 变更缓存,配合 `loadThrough(seq)` 翻页;找不到时再点转写自己的「加载更早」,最多 14 轮。
|
|
86
|
+
- **提示不浮在页面上**:定位失败只写面板里的一行状态栏(`role="status"`,6 秒自清,换词/换范围/`Esc`/卸载都会清掉),并在该条命中行留一个「未渲染」小标。0.1.4 及以前是往 `document.body` 上挂 `position:fixed` 的浮层——每个提示新建一个节点、共用一个定时器,2.2 秒内的第二条会把第一条的消失定时器清掉,于是第一条永久停在 `bottom:96px`,正好压住消息输入框。
|
|
86
87
|
- **只在本机可用**:宿主路由与其它 DSH 本机路由一致,做回环 socket + 回环 Host 头 + 同源三道校验,隧道/局域网客户端一律 403。
|
|
87
88
|
- **条栏里不放多字文本**:查找条只承载输入框、计数、`↑`/`↓`/`✕` 这类定尺寸控件;「范围」放在条下方独立一行,靠 flex-wrap 换行而不是挤压。
|
|
88
89
|
|
|
89
90
|
## 验证记录
|
|
90
91
|
|
|
91
|
-
在本机 `0.2.1-rc.5` 上实测(2026-11-01
|
|
92
|
+
在本机 `0.2.1-rc.5` 上实测(2026-11-01;2026-11-15 的提示浮层修复重测了 `version=0.1.5` 与单元测试数):
|
|
92
93
|
|
|
93
94
|
| 检查 | 结果 |
|
|
94
95
|
|---|---|
|
|
95
|
-
| 桌面端真机 | `activated=true`、`version=0.1.
|
|
96
|
+
| 桌面端真机 | `activated=true`、`version=0.1.5`、`highlight=true`、`opens=2 via=button` |
|
|
96
97
|
| 宿主路由存活 | `GET /dsh-find-in-session/ping` → `{"ok":true,"ready":true}` |
|
|
97
98
|
| 真实会话检索 | 本会话搜 `findInPage` → `total=8`,命中带 `seq / type / surface / snippet`(`surface` 能区分 `current` 与 `log-only`) |
|
|
98
99
|
| 浏览器半被收录 | 另起 `dsh web`(临时挂载后已卸载):`client.registered=true`、`client.bootListed=true` |
|
|
@@ -102,7 +103,7 @@ curl http://127.0.0.1:<port>/dsh-find-in-session/ping
|
|
|
102
103
|
| 高亮与逐条跳转 | 注入 2 处关键词的转写:计数 `1/2`、`CSS.highlights` 里 2 个 Range、上下切换 `1/2 -> 2/2 -> 1/2` 回绕 |
|
|
103
104
|
| 全历史命中 + 翻页定位 | 打开真实会话(转写 2,282 字符)搜 `chrome`:视图内 `1/9`、宿主回报 19 处;点第 1 条 → 转写涨到 8,927 字符(`loadThrough` 生效)、滚动 `0 -> 5847`;点最后一条 → 该行 `data-active=true` |
|
|
104
105
|
| 面板布局 | 504px 视口下:面板 442×106,两段按钮 `75×21` / `64×21`,`scrollWidth == clientWidth`,面板无横向溢出 |
|
|
105
|
-
| 单元测试 | `node --test --test-isolation=none "test/*.test.js"` →
|
|
106
|
+
| 单元测试 | `node --test --test-isolation=none "test/*.test.js"` → 24 pass |
|
|
106
107
|
|
|
107
108
|
浏览器侧验证可复现:起一个临时 `dsh web`(把本插件临时挂到某个 profile),用无头 Chrome 打开它,再跑:
|
|
108
109
|
|
|
@@ -130,11 +131,11 @@ pwsh -ExecutionPolicy Bypass -File .\scripts\ui-probe\run-jump-check.ps1 -Pag
|
|
|
130
131
|
## 测试
|
|
131
132
|
|
|
132
133
|
```powershell
|
|
133
|
-
node --test --test-isolation=none "test/*.test.js" #
|
|
134
|
+
node --test --test-isolation=none "test/*.test.js" # 24 pass(DSH 沙箱内必须带 --test-isolation=none)
|
|
134
135
|
node --test "test/*.test.js" # 普通环境
|
|
135
136
|
```
|
|
136
137
|
|
|
137
|
-
零依赖:覆盖字面量编译、片段窗口、上限与 surface
|
|
138
|
+
零依赖:覆盖字面量编译、片段窗口、上限与 surface 过滤,宿主路由的守卫/校验/错误码/正常返回,浏览器半在 stub DOM 下的 factory → exports → apply → 两处 slot 注册 + 激活探针,以及 `test/client-jump.test.js` 里用假 DOM 走完整条「Ctrl+F → 输入 → 全历史命中 → 点击」路径:定位失败只留面板内的一行状态(不再往 `document.body` 挂浮层)、该命中行带「未渲染」标、换词/`Esc`/卸载都会清掉,且跨行内节点的片段能被定位到事件块。
|
|
138
139
|
|
|
139
140
|
## 文件
|
|
140
141
|
|
package/lib/client.js
CHANGED
|
@@ -37,7 +37,7 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
const NS = 'session-search'
|
|
38
38
|
const SEARCH_ROUTE = '/dsh-find-in-session/search'
|
|
39
39
|
const HELLO_ROUTE = '/dsh-find-in-session/client-hello'
|
|
40
|
-
const VERSION = '0.1.
|
|
40
|
+
const VERSION = '0.1.5'
|
|
41
41
|
const HIT_HIGHLIGHT = 'dsh-ss-hit'
|
|
42
42
|
const ACTIVE_HIGHLIGHT = 'dsh-ss-active'
|
|
43
43
|
const STYLE_ID = 'dsh-find-in-session-style'
|
|
@@ -45,6 +45,16 @@ window.__ModuleLoader__.load({
|
|
|
45
45
|
const HOST_DEBOUNCE_MS = 320
|
|
46
46
|
const PAGE_ROUNDS = 14
|
|
47
47
|
const PAGE_WAIT_MS = 380
|
|
48
|
+
// Feedback for a jump that could not be completed is one reused status line
|
|
49
|
+
// inside the panel: it is disposed with the panel, so it can neither stack
|
|
50
|
+
// up nor float over the composer the way a body-level toast can.
|
|
51
|
+
const NOTICE_MS = 6000
|
|
52
|
+
// A Host snippet is one run of text, but the rendered transcript splits it
|
|
53
|
+
// at every inline code / strong / link, so a single-text-node match misses
|
|
54
|
+
// text that is plainly on screen. These prefix lengths are tried against
|
|
55
|
+
// the event block instead of one node.
|
|
56
|
+
const BLOCK_PROBES = [160, 48, 24]
|
|
57
|
+
const MIN_BLOCK_PROBE = 12
|
|
48
58
|
// The transcript renders the current model surface; replaced (shadowed) and
|
|
49
59
|
// log-only events are not displayed, so they are noise for a find bar and
|
|
50
60
|
// only join the scan when the user asks for them.
|
|
@@ -68,6 +78,9 @@ window.__ModuleLoader__.load({
|
|
|
68
78
|
activeSeq: null,
|
|
69
79
|
busy: false,
|
|
70
80
|
includeHidden: false,
|
|
81
|
+
// seqs whose jump found nothing on screen; the row keeps a marker until
|
|
82
|
+
// the query or the scope changes
|
|
83
|
+
unlocated: new Set(),
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
const messageOf = (error) => String(error && error.message ? error.message : error)
|
|
@@ -236,6 +249,74 @@ window.__ModuleLoader__.load({
|
|
|
236
249
|
return rangeOf(node, 0, node.nodeValue.length)
|
|
237
250
|
}
|
|
238
251
|
|
|
252
|
+
// --------------------------------------------------------- block locating
|
|
253
|
+
// A Host snippet is one continuous run of text, but the transcript renders
|
|
254
|
+
// markdown: inline `code`, **strong** and links split the same sentence into
|
|
255
|
+
// several text nodes, and the rendered whitespace never lines up exactly.
|
|
256
|
+
// Comparing the squeezed (whitespace-free, case-folded) text of an event
|
|
257
|
+
// block still finds the event, which is all a jump needs.
|
|
258
|
+
let blockCache = null
|
|
259
|
+
|
|
260
|
+
function markTranscriptDirty() {
|
|
261
|
+
blockCache = null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function squeeze(text) {
|
|
265
|
+
return String(text === undefined || text === null ? '' : text)
|
|
266
|
+
.replace(/\s+/gu, '')
|
|
267
|
+
.toLowerCase()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Event blocks of the rendered transcript, each with its squeezed text. */
|
|
271
|
+
function transcriptBlocks() {
|
|
272
|
+
if (blockCache !== null) return blockCache
|
|
273
|
+
const blocks = []
|
|
274
|
+
try {
|
|
275
|
+
const root = chatRoot()
|
|
276
|
+
if (root !== null) {
|
|
277
|
+
const keyed = typeof root.querySelectorAll === 'function' ? root.querySelectorAll('[data-chat-node-key]') : null
|
|
278
|
+
const elements = keyed !== null && keyed.length > 0 ? Array.from(keyed) : Array.from(root.children || [])
|
|
279
|
+
for (const element of elements) {
|
|
280
|
+
const text = squeeze(element.textContent)
|
|
281
|
+
if (text !== '') blocks.push({ element, text })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// An unexpected DOM shape may cost the fallback locator, never the jump.
|
|
286
|
+
}
|
|
287
|
+
blockCache = blocks
|
|
288
|
+
return blocks
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function blockProbes(wanted) {
|
|
292
|
+
const probes = []
|
|
293
|
+
for (const length of BLOCK_PROBES) {
|
|
294
|
+
const probe = wanted.slice(0, Math.min(length, wanted.length))
|
|
295
|
+
if (probe.length < MIN_BLOCK_PROBE || probes.indexOf(probe) !== -1) continue
|
|
296
|
+
probes.push(probe)
|
|
297
|
+
}
|
|
298
|
+
return probes
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The event block that renders a snippet the single-node pass missed.
|
|
303
|
+
* Longest prefix first: a shorter probe is only trusted when no block holds
|
|
304
|
+
* a longer one, so a click lands on the right event, not the first similar.
|
|
305
|
+
*/
|
|
306
|
+
function locateEventBlock(needle) {
|
|
307
|
+
const wanted = squeeze(needle)
|
|
308
|
+
if (wanted.length < MIN_BLOCK_PROBE) return null
|
|
309
|
+
const probes = blockProbes(wanted)
|
|
310
|
+
if (probes.length === 0) return null
|
|
311
|
+
const blocks = transcriptBlocks()
|
|
312
|
+
for (const probe of probes) {
|
|
313
|
+
for (const block of blocks) {
|
|
314
|
+
if (block.text.indexOf(probe) !== -1) return block.element
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
|
|
239
320
|
// --------------------------------------------------------------- find bar
|
|
240
321
|
function installStyleOnce() {
|
|
241
322
|
if (document.getElementById(STYLE_ID) !== null) return
|
|
@@ -291,6 +372,20 @@ window.__ModuleLoader__.load({
|
|
|
291
372
|
.dsh-ss-seg > button[data-active="true"] { background: rgba(77, 107, 254, 0.18); opacity: 1; font-weight: 600; }
|
|
292
373
|
.dsh-ss-host { border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.22)); max-height: 46vh; overflow: auto; display: none; }
|
|
293
374
|
.dsh-ss-host[data-show="true"] { display: block; }
|
|
375
|
+
/* Status line: in the panel's own flow, above the result list. It cannot
|
|
376
|
+
overlap the composer, and it cannot outlive the panel. */
|
|
377
|
+
.dsh-ss-notice {
|
|
378
|
+
display: none; margin: 0 12px 8px; padding: 6px 10px; border-radius: 8px;
|
|
379
|
+
font-size: 12px; line-height: 1.5; word-break: break-word;
|
|
380
|
+
background: rgba(255, 138, 0, 0.16);
|
|
381
|
+
border: 1px solid rgba(255, 138, 0, 0.4);
|
|
382
|
+
}
|
|
383
|
+
.dsh-ss-notice[data-show="true"] { display: block; }
|
|
384
|
+
.dsh-ss-row-unlocated {
|
|
385
|
+
flex: 0 0 auto; margin-left: auto; padding: 0 6px; border-radius: 999px;
|
|
386
|
+
background: rgba(255, 138, 0, 0.18); font-size: 10px; letter-spacing: 0.02em;
|
|
387
|
+
}
|
|
388
|
+
.dsh-ss-row[data-unlocated="true"] .dsh-ss-row-text { opacity: 0.72; }
|
|
294
389
|
.dsh-ss-host-head {
|
|
295
390
|
padding: 7px 12px; font-size: 11px; letter-spacing: 0.03em; opacity: 0.62;
|
|
296
391
|
display: flex; align-items: baseline; gap: 8px;
|
|
@@ -385,6 +480,9 @@ window.__ModuleLoader__.load({
|
|
|
385
480
|
const choose = (includeHidden) => {
|
|
386
481
|
if (runtime.includeHidden === includeHidden) return
|
|
387
482
|
runtime.includeHidden = includeHidden
|
|
483
|
+
clearNotice()
|
|
484
|
+
runtime.unlocated.clear()
|
|
485
|
+
markTranscriptDirty()
|
|
388
486
|
render()
|
|
389
487
|
if (runtime.query.trim() !== '') scheduleHostSearch(runtime.query)
|
|
390
488
|
}
|
|
@@ -393,14 +491,20 @@ window.__ModuleLoader__.load({
|
|
|
393
491
|
seg.append(segCurrent, segAll)
|
|
394
492
|
scopeRow.append(scopeLabel, seg)
|
|
395
493
|
|
|
494
|
+
const notice = document.createElement('div')
|
|
495
|
+
notice.className = 'dsh-ss-notice'
|
|
496
|
+
notice.dataset.show = 'false'
|
|
497
|
+
notice.setAttribute('role', 'status')
|
|
498
|
+
notice.setAttribute('aria-live', 'polite')
|
|
499
|
+
|
|
396
500
|
const host = document.createElement('div')
|
|
397
501
|
host.className = 'dsh-ss-host'
|
|
398
502
|
host.dataset.show = 'false'
|
|
399
503
|
|
|
400
|
-
panel.append(bar, scopeRow, host)
|
|
504
|
+
panel.append(bar, scopeRow, notice, host)
|
|
401
505
|
document.body.appendChild(panel)
|
|
402
506
|
|
|
403
|
-
const ui = { panel, bar, input, count, prev, next, close, host, segCurrent, segAll }
|
|
507
|
+
const ui = { panel, bar, input, count, prev, next, close, notice, host, segCurrent, segAll }
|
|
404
508
|
runtime.ui = ui
|
|
405
509
|
|
|
406
510
|
input.addEventListener('input', () => {
|
|
@@ -485,6 +589,8 @@ window.__ModuleLoader__.load({
|
|
|
485
589
|
row.type = 'button'
|
|
486
590
|
row.className = 'dsh-ss-row'
|
|
487
591
|
row.dataset.active = runtime.activeSeq === hit.seq ? 'true' : 'false'
|
|
592
|
+
const unlocated = runtime.unlocated.has(hit.seq)
|
|
593
|
+
if (unlocated) row.dataset.unlocated = 'true'
|
|
488
594
|
|
|
489
595
|
const meta = document.createElement('div')
|
|
490
596
|
meta.className = 'dsh-ss-row-meta'
|
|
@@ -495,6 +601,14 @@ window.__ModuleLoader__.load({
|
|
|
495
601
|
const time = document.createElement('span')
|
|
496
602
|
time.textContent = stamp
|
|
497
603
|
meta.append(kind, time)
|
|
604
|
+
if (unlocated) {
|
|
605
|
+
// The row keeps saying why the jump did nothing, long after the one-line
|
|
606
|
+
// notice above the list has faded.
|
|
607
|
+
const marker = document.createElement('span')
|
|
608
|
+
marker.className = 'dsh-ss-row-unlocated'
|
|
609
|
+
marker.textContent = runtime.t('row.unlocated')
|
|
610
|
+
meta.append(marker)
|
|
611
|
+
}
|
|
498
612
|
|
|
499
613
|
const text = document.createElement('div')
|
|
500
614
|
text.className = 'dsh-ss-row-text'
|
|
@@ -535,6 +649,9 @@ window.__ModuleLoader__.load({
|
|
|
535
649
|
runtime.sessionId = next
|
|
536
650
|
runtime.host = { status: 'idle', hits: [], total: 0, truncated: false, error: null, token: runtime.host.token + 1 }
|
|
537
651
|
runtime.activeSeq = null
|
|
652
|
+
clearNotice()
|
|
653
|
+
runtime.unlocated.clear()
|
|
654
|
+
markTranscriptDirty()
|
|
538
655
|
if (runtime.open && runtime.query.trim() !== '') scheduleHostSearch(runtime.query)
|
|
539
656
|
render()
|
|
540
657
|
}
|
|
@@ -571,6 +688,10 @@ window.__ModuleLoader__.load({
|
|
|
571
688
|
|
|
572
689
|
function setQuery(value) {
|
|
573
690
|
runtime.query = typeof value === 'string' ? value : ''
|
|
691
|
+
// A new query invalidates the status line and every "unrendered" marker.
|
|
692
|
+
clearNotice()
|
|
693
|
+
runtime.unlocated.clear()
|
|
694
|
+
markTranscriptDirty()
|
|
574
695
|
const matches = scanDom(runtime.query)
|
|
575
696
|
runtime.matches = matches
|
|
576
697
|
runtime.index = matches.length > 0 ? 0 : -1
|
|
@@ -653,23 +774,67 @@ window.__ModuleLoader__.load({
|
|
|
653
774
|
render()
|
|
654
775
|
}
|
|
655
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Resolve one Host hit to something on screen: the exact range of its text
|
|
779
|
+
* when the transcript keeps it in one text node, otherwise the event block
|
|
780
|
+
* that renders it. Only a hit with neither is reported as unrendered.
|
|
781
|
+
*/
|
|
782
|
+
function locateHit(needle) {
|
|
783
|
+
const range = locateNeedle(needle)
|
|
784
|
+
if (range !== null) return { range }
|
|
785
|
+
const block = locateEventBlock(needle)
|
|
786
|
+
if (block !== null) return { block }
|
|
787
|
+
return null
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function revealTarget(target) {
|
|
791
|
+
if (target.range !== undefined && target.range !== null) {
|
|
792
|
+
scrollToRange(target.range)
|
|
793
|
+
flashRange(target.range)
|
|
794
|
+
return
|
|
795
|
+
}
|
|
796
|
+
if (target.block !== undefined && target.block !== null) {
|
|
797
|
+
scrollElement(target.block)
|
|
798
|
+
flashElement(target.block)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function scrollElement(element) {
|
|
803
|
+
if (element === null || element === undefined) return false
|
|
804
|
+
try {
|
|
805
|
+
element.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' })
|
|
806
|
+
} catch {
|
|
807
|
+
try {
|
|
808
|
+
element.scrollIntoView()
|
|
809
|
+
} catch {
|
|
810
|
+
return false
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return true
|
|
814
|
+
}
|
|
815
|
+
|
|
656
816
|
async function revealHostHit(hit) {
|
|
657
817
|
if (runtime.busy) return
|
|
658
818
|
runtime.busy = true
|
|
659
819
|
runtime.activeSeq = hit.seq
|
|
820
|
+
clearNotice()
|
|
660
821
|
render()
|
|
661
822
|
try {
|
|
662
823
|
const needle = needleOf(hit.snippet)
|
|
663
|
-
if (needle
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
const paged = await pageThrough(hit.seq, needle)
|
|
671
|
-
if (!paged) showToast(runtime.t('toast.notRendered'))
|
|
824
|
+
if (needle === '') return
|
|
825
|
+
const target = locateHit(needle)
|
|
826
|
+
if (target !== null) {
|
|
827
|
+
revealTarget(target)
|
|
828
|
+
runtime.unlocated.delete(hit.seq)
|
|
829
|
+
return
|
|
672
830
|
}
|
|
831
|
+
const paged = await pageThrough(hit.seq, needle)
|
|
832
|
+
if (paged) {
|
|
833
|
+
runtime.unlocated.delete(hit.seq)
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
runtime.unlocated.add(hit.seq)
|
|
837
|
+
showNotice(runtime.t('notice.notRendered'))
|
|
673
838
|
} finally {
|
|
674
839
|
runtime.busy = false
|
|
675
840
|
render()
|
|
@@ -688,6 +853,7 @@ window.__ModuleLoader__.load({
|
|
|
688
853
|
|
|
689
854
|
async function pageThrough(seq, needle) {
|
|
690
855
|
await loadThrough(seq)
|
|
856
|
+
markTranscriptDirty()
|
|
691
857
|
if (await waitForNeedle(needle, 900)) return true
|
|
692
858
|
for (let round = 0; round < PAGE_ROUNDS; round += 1) {
|
|
693
859
|
if (!clickLoadEarlier()) return false
|
|
@@ -699,10 +865,9 @@ window.__ModuleLoader__.load({
|
|
|
699
865
|
async function waitForNeedle(needle, timeout) {
|
|
700
866
|
const deadline = Date.now() + timeout
|
|
701
867
|
for (;;) {
|
|
702
|
-
const
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
flashRange(found)
|
|
868
|
+
const target = locateHit(needle)
|
|
869
|
+
if (target !== null) {
|
|
870
|
+
revealTarget(target)
|
|
706
871
|
return true
|
|
707
872
|
}
|
|
708
873
|
if (Date.now() >= deadline) return false
|
|
@@ -721,11 +886,13 @@ window.__ModuleLoader__.load({
|
|
|
721
886
|
if (session && typeof session.loadThrough === 'function') {
|
|
722
887
|
await session.loadThrough(seq)
|
|
723
888
|
await sleep(120)
|
|
889
|
+
markTranscriptDirty()
|
|
724
890
|
return true
|
|
725
891
|
}
|
|
726
892
|
if (session && typeof session.loadOlder === 'function') {
|
|
727
893
|
await session.loadOlder()
|
|
728
894
|
await sleep(120)
|
|
895
|
+
markTranscriptDirty()
|
|
729
896
|
return true
|
|
730
897
|
}
|
|
731
898
|
} catch {
|
|
@@ -750,14 +917,18 @@ window.__ModuleLoader__.load({
|
|
|
750
917
|
return false
|
|
751
918
|
}
|
|
752
919
|
|
|
753
|
-
function
|
|
754
|
-
const element = elementOfRange(range)
|
|
920
|
+
function flashElement(element) {
|
|
755
921
|
if (element === null || element === undefined) return
|
|
922
|
+
if (element.classList === undefined || element.classList === null) return
|
|
756
923
|
element.classList.add('dsh-ss-flash')
|
|
757
924
|
setTimeout(() => element.classList.remove('dsh-ss-flash'), 1200)
|
|
758
925
|
}
|
|
759
926
|
|
|
760
|
-
|
|
927
|
+
function flashRange(range) {
|
|
928
|
+
flashElement(elementOfRange(range))
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
let noticeTimer = null
|
|
761
932
|
let observer = null
|
|
762
933
|
let observerTimer = null
|
|
763
934
|
|
|
@@ -769,6 +940,9 @@ window.__ModuleLoader__.load({
|
|
|
769
940
|
function startObserver() {
|
|
770
941
|
if (observer !== null || typeof MutationObserver !== 'function') return
|
|
771
942
|
observer = new MutationObserver((records) => {
|
|
943
|
+
// Any transcript mutation invalidates the cached block text, even when
|
|
944
|
+
// the panel is closed and no rescan of the query is needed.
|
|
945
|
+
markTranscriptDirty()
|
|
772
946
|
if (!runtime.open || runtime.query.trim() === '') return
|
|
773
947
|
for (const record of records) {
|
|
774
948
|
const target = record.target
|
|
@@ -798,18 +972,42 @@ window.__ModuleLoader__.load({
|
|
|
798
972
|
observer = null
|
|
799
973
|
}
|
|
800
974
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Status line for the find panel. One element, one timer: a second message
|
|
978
|
+
* replaces the first instead of orphaning it (the old toast kept a single
|
|
979
|
+
* module-level timeout for elements it created per call, so two messages in
|
|
980
|
+
* the same 2.2s window left the first one on screen forever), the line lives
|
|
981
|
+
* inside the panel, and it clears itself, on close, and on teardown.
|
|
982
|
+
*/
|
|
983
|
+
function showNotice(text) {
|
|
984
|
+
const ui = runtime.ui
|
|
985
|
+
if (ui === null) return
|
|
986
|
+
ui.notice.textContent = text
|
|
987
|
+
ui.notice.dataset.show = text === '' ? 'false' : 'true'
|
|
988
|
+
if (noticeTimer !== null) clearTimeout(noticeTimer)
|
|
989
|
+
noticeTimer = null
|
|
990
|
+
if (text === '') return
|
|
991
|
+
noticeTimer = setTimeout(() => {
|
|
992
|
+
noticeTimer = null
|
|
993
|
+
clearNotice()
|
|
994
|
+
}, NOTICE_MS)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function clearNotice() {
|
|
998
|
+
if (noticeTimer !== null) {
|
|
999
|
+
clearTimeout(noticeTimer)
|
|
1000
|
+
noticeTimer = null
|
|
1001
|
+
}
|
|
1002
|
+
const ui = runtime.ui
|
|
1003
|
+
if (ui === null) return
|
|
1004
|
+
ui.notice.textContent = ''
|
|
1005
|
+
ui.notice.dataset.show = 'false'
|
|
809
1006
|
}
|
|
810
1007
|
|
|
811
1008
|
function openPanel(via) {
|
|
812
1009
|
const wasOpen = runtime.open
|
|
1010
|
+
markTranscriptDirty()
|
|
813
1011
|
const ui = buildUi()
|
|
814
1012
|
runtime.open = true
|
|
815
1013
|
if (ui.input.value !== runtime.query) ui.input.value = runtime.query
|
|
@@ -827,6 +1025,7 @@ window.__ModuleLoader__.load({
|
|
|
827
1025
|
|
|
828
1026
|
function closePanel() {
|
|
829
1027
|
runtime.open = false
|
|
1028
|
+
clearNotice()
|
|
830
1029
|
if (runtime.hostTimer !== null) {
|
|
831
1030
|
clearTimeout(runtime.hostTimer)
|
|
832
1031
|
runtime.hostTimer = null
|
|
@@ -978,7 +1177,8 @@ window.__ModuleLoader__.load({
|
|
|
978
1177
|
'role.tool': '\u5de5\u5177',
|
|
979
1178
|
'role.event': '\u4e8b\u4ef6',
|
|
980
1179
|
'button.title': '\u5728\u4f1a\u8bdd\u4e2d\u67e5\u627e (Ctrl+F)',
|
|
981
|
-
'
|
|
1180
|
+
'notice.notRendered': '\u5df2\u5b9a\u4f4d\u5230\u8be5\u4e8b\u4ef6\uff0c\u4f46\u6b63\u6587\u8fd8\u672a\u6e32\u67d3\u5230\u89c6\u56fe\u91cc',
|
|
1181
|
+
'row.unlocated': '\u672a\u6e32\u67d3',
|
|
982
1182
|
},
|
|
983
1183
|
en: {
|
|
984
1184
|
'panel.title': 'Find in conversation',
|
|
@@ -1002,7 +1202,8 @@ window.__ModuleLoader__.load({
|
|
|
1002
1202
|
'role.tool': 'Tool',
|
|
1003
1203
|
'role.event': 'Event',
|
|
1004
1204
|
'button.title': 'Find in conversation (Ctrl+F)',
|
|
1005
|
-
'
|
|
1205
|
+
'notice.notRendered': 'Located the event, but its text is not rendered yet',
|
|
1206
|
+
'row.unlocated': 'not rendered',
|
|
1006
1207
|
},
|
|
1007
1208
|
}),
|
|
1008
1209
|
'session-search: locale dictionaries',
|
|
@@ -1021,10 +1222,12 @@ window.__ModuleLoader__.load({
|
|
|
1021
1222
|
document.removeEventListener('keydown', onKeyDown, true)
|
|
1022
1223
|
window.removeEventListener('resize', onResize)
|
|
1023
1224
|
stopObserver()
|
|
1225
|
+
clearNotice()
|
|
1024
1226
|
if (runtime.ui !== null) {
|
|
1025
1227
|
runtime.ui.panel.remove()
|
|
1026
1228
|
runtime.ui = null
|
|
1027
1229
|
}
|
|
1230
|
+
markTranscriptDirty()
|
|
1028
1231
|
clearHighlights()
|
|
1029
1232
|
},
|
|
1030
1233
|
'session-search: teardown',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-find-in-session",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "DeepSeek Harness plugin: search inside a conversation — Ctrl+F find bar over the rendered transcript plus full-history search through ctx.sessionQuery.filterEvents, with jump-to-message paging of older history.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|