@ryuu-64/dsh-find-all 0.1.7 → 0.1.9

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/lib/client.js CHANGED
@@ -77,6 +77,7 @@ window.__ModuleLoader__.load({
77
77
  next: "下一个匹配",
78
78
  close: "关闭查找",
79
79
  cap: "仅显示前 {n} 处匹配",
80
+ resultCount: "{n} 个结果",
80
81
  scopeWhole: "包含更早内容",
81
82
  scopePage: "已加载内容",
82
83
  scopeToPage: "搜索当前会话中已加载的内容,并继续加载更早内容。点击改为仅搜索已加载内容",
@@ -86,16 +87,27 @@ window.__ModuleLoader__.load({
86
87
  capped: "搜索未完成:已到更早内容的加载上限({n} 页),已保留找到的结果",
87
88
  stopTip: "点击停止加载",
88
89
  noService: "搜索未完成:会话接口不可用,已保留找到的结果",
89
- noTarget: "请先选择可见的会话,再打开查找",
90
+ noTarget: "当前主会话没有可搜索的聊天正文",
90
91
  noRoot: "当前会话的搜索范围不可用",
91
92
  incomplete: "搜索未完成,已保留找到的结果",
92
- find: "查找会话"
93
+ find: "查找会话",
94
+ returnPosition: "返回搜索前位置", returning: "正在返回…", returned: "已返回搜索前位置",
95
+ returnAvailable: "已跳到匹配;可返回搜索前位置",
96
+ matchRevealFailed: "已找到匹配,但宿主未能展开并显示它。可手动展开后重试。",
97
+ jumpHint: "找到 {n} 个结果;按 Enter 或向下跳到第一个,按向上跳到最后一个。",
98
+ originUnavailable: "未能记录当前阅读位置;仍可查找,但无法返回",
99
+ returnCancelled: "已取消返回", returnFailed: "未能返回原位置。",
100
+ return_removed: "原内容已不再可用。", return_render: "原内容已加载,但宿主未能显示原段落。可展开原内容后重试。",
101
+ return_unavailable: "宿主无法提供所需内容或定位能力。", return_capped: "已达到历史加载上限,可重试。",
102
+ return_stalled: "等待内容时没有进展,可重试。", return_error: "读取原内容失败,可重试。",
103
+ return_position: "宿主未保持原段落可见。可先在正文内滚动,再重试。"
93
104
  } : {
94
105
  placeholder: "Find in this conversation…",
95
106
  prev: "Previous match",
96
107
  next: "Next match",
97
108
  close: "Close find",
98
109
  cap: "Showing only the first {n} matches",
110
+ resultCount: "{n} results",
99
111
  scopeWhole: "Include earlier content",
100
112
  scopePage: "Loaded content",
101
113
  scopeToPage: "Search loaded content in this conversation and load earlier content. Click to search loaded content only",
@@ -105,10 +117,20 @@ window.__ModuleLoader__.load({
105
117
  capped: "History search incomplete: loading limit reached ({n} pages); partial results retained",
106
118
  stopTip: "Click to stop loading",
107
119
  noService: "History search incomplete: session service unavailable; partial results retained",
108
- noTarget: "Select a visible conversation before opening find",
120
+ noTarget: "No searchable chat content in the main conversation",
109
121
  noRoot: "Search scope unavailable for this conversation",
110
122
  incomplete: "History search incomplete; partial results retained",
111
- find: "Find in conversation"
123
+ find: "Find in conversation",
124
+ returnPosition: "Return to reading position", returning: "Returning…", returned: "Returned to reading position",
125
+ returnAvailable: "Moved to a match; return to the reading position is available",
126
+ matchRevealFailed: "A match was found, but the host could not reveal and display it. Expand the section and retry.",
127
+ jumpHint: "{n} matches found; Enter or Next goes to the first, Previous goes to the last.",
128
+ originUnavailable: "The current reading position could not be saved; search remains available, but return is unavailable",
129
+ returnCancelled: "Return cancelled", returnFailed: "Could not return to the reading position.",
130
+ return_removed: "The original content is no longer available.", return_render: "The content is loaded, but the host could not display the original passage. Expand it and retry.",
131
+ return_unavailable: "The host cannot provide the required content or positioning capability.", return_capped: "History page limit reached. You can retry.",
132
+ return_stalled: "No progress while waiting for content. You can retry.", return_error: "Loading the original content failed. You can retry.",
133
+ return_position: "The host did not keep the original passage visible. Try scrolling in the conversation, then retrying."
112
134
  };
113
135
 
114
136
  //#region pure helpers (exported for tests)
@@ -140,6 +162,8 @@ window.__ModuleLoader__.load({
140
162
  // Keyboard routing, isolated so tests can drive it with spies.
141
163
  function createHandlers(ops) {
142
164
  return function onKeydown(event) {
165
+ // Let actual modals and editors with their own find UI own their keys.
166
+ if (event.defaultPrevented || event.isComposing || (ops.ownsKeys && !ops.ownsKeys(event))) return;
143
167
  var meta = event.metaKey || event.ctrlKey;
144
168
  var key = event.key;
145
169
  if (meta && (key === "f" || key === "F")) {
@@ -210,16 +234,23 @@ window.__ModuleLoader__.load({
210
234
 
211
235
  function cssText() {
212
236
  return "" +
213
- "#" + 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)}" +
214
- "#" + 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}" +
237
+ "#" + BAR_ID + "{--dsh-find-all-anchor-gap:4px;--dsh-find-all-panel-inset:16px;--dsh-find-all-max-width:760px;position:fixed;top:calc(var(--dsh-find-all-header-bottom) + var(--dsh-find-all-anchor-gap));right:calc(var(--dsh-find-all-panel-right-space) + var(--dsh-find-all-panel-inset));z-index:2147483000;visibility:hidden;isolation:isolate;box-sizing:border-box;align-items:stretch;gap:4px;padding:6px 8px;display:flex;flex-direction:column;width:min(var(--dsh-find-all-max-width),calc(var(--dsh-find-all-panel-width) - var(--dsh-find-all-panel-inset) - var(--dsh-find-all-panel-inset)));max-width:calc(100vw - var(--dsh-find-all-panel-inset) - var(--dsh-find-all-panel-inset));background:transparent;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);border:0;border-radius:var(--dsw-radius-md,8px);box-shadow:var(--dsw-elevation-panel,var(--dsw-shadow-lv2))}" +
238
+ "#" + BAR_ID + "[data-positioned]{visibility:visible}" +
239
+ "#" + BAR_ID + ":before{content:\"\";position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));-webkit-backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%))}" +
240
+ "#" + BAR_ID + " .controls{display:flex;flex-wrap:wrap;align-items:center;gap:6px;min-width:0}" +
241
+ "#" + BAR_ID + " input{flex:1 1 210px;min-width:120px;width:auto;height:28px;color:var(--dsw-alias-label-primary);font:13px/1 var(--dsw-font-family);background:var(--dsw-alias-bg-base);border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm,6px);outline:none;padding:0 8px}" +
215
242
  "#" + BAR_ID + " input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}" +
216
243
  "#" + 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}" +
217
- "#" + 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}" +
244
+ "#" + BAR_ID + " .status{width:100%;min-height:15px;color:var(--dsw-alias-label-tertiary);font:12px/1.25 var(--dsw-font-family);text-align:left;white-space:normal;overflow-wrap:anywhere}" +
218
245
  "#" + BAR_ID + " .status[data-busy]{cursor:pointer;color:var(--dsw-alias-state-business-primary)}" +
219
246
  "#" + 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}" +
220
247
  "#" + BAR_ID + " .scope[data-whole]{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-state-business-primary)}" +
221
248
  "#" + 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}" +
222
249
  "#" + BAR_ID + " button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}" +
250
+ "#" + BAR_ID + " button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}" +
251
+ "#" + BAR_ID + " button:disabled{opacity:.5;cursor:default}" +
252
+ "#" + BAR_ID + " [data-find-all-return][hidden]{display:none}" +
253
+ "#" + BAR_ID + " [data-find-all-return]{padding:0 8px;white-space:nowrap}" +
223
254
  "#" + BAR_ID + " svg{pointer-events:none}" +
224
255
  "::highlight(" + HL_ALL + "){background-color:rgba(245,197,24,.42)}" +
225
256
  "::highlight(" + HL_CUR + "){background-color:#f5a623;color:#141414}";
@@ -266,11 +297,68 @@ window.__ModuleLoader__.load({
266
297
  return into;
267
298
  }
268
299
 
300
+ // MessageIconActions is host chrome: copy/branch controls, Turn usage and
301
+ // the local clock. It must never change the conversation result set merely
302
+ // because hover/focus changes its opacity. Current hosts expose data-clock
303
+ // on that row. The structural fallback is limited to the four admitted
304
+ // pre-data-clock hosts, whose official MessageItem/TurnTailNodeView sources
305
+ // render the same icon-only copy action in the verified positions below.
306
+ function legacyMessageActionRows(root) {
307
+ var rows = new Set();
308
+ if (!root) return rows;
309
+ function includeRoot(selector, into) {
310
+ if (root.nodeType === 1 && root.matches && root.matches(selector)) into.push(root);
311
+ var descendants = root.querySelectorAll ? root.querySelectorAll(selector) : [];
312
+ for (var i = 0; i < descendants.length; i++) into.push(descendants[i]);
313
+ return into;
314
+ }
315
+ var markedSelector = '[data-clock="start"],[data-clock="end"]';
316
+ if ((root.nodeType === 1 && root.matches && root.matches(markedSelector)) ||
317
+ (root.querySelector && root.querySelector(markedSelector))) return rows;
318
+
319
+ function containsIconOnlyAction(element) {
320
+ if (!element || !element.querySelectorAll) return false;
321
+ var buttons = element.querySelectorAll('button[type="button"][aria-label]');
322
+ for (var j = 0; j < buttons.length; j++) {
323
+ if (buttons[j].querySelector("svg") && !(buttons[j].textContent || "").trim()) return true;
324
+ }
325
+ return false;
326
+ }
327
+
328
+ var tails = includeRoot("[data-turn-tail]", []);
329
+ for (var i = 0; i < tails.length; i++) {
330
+ var tailAction = tails[i].lastElementChild;
331
+ if (containsIconOnlyAction(tailAction)) rows.add(tailAction);
332
+ }
333
+
334
+ var flows = includeRoot('[data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]', []);
335
+ for (i = 0; i < flows.length; i++) {
336
+ var buttons = flows[i].querySelectorAll('button[type="button"][aria-label]');
337
+ for (var j = 0; j < buttons.length; j++) {
338
+ if (!buttons[j].querySelector("svg") || (buttons[j].textContent || "").trim()) continue;
339
+ var candidate = null;
340
+ for (var node = buttons[j].parentElement; node && node !== flows[i]; node = node.parentElement) {
341
+ var parent = node.parentElement;
342
+ if (parent && parent !== flows[i] && parent.children.length === 2 &&
343
+ node === parent.lastElementChild && node.previousElementSibling) candidate = node;
344
+ }
345
+ if (candidate) { rows.add(candidate); break; }
346
+ }
347
+ }
348
+ return rows;
349
+ }
350
+
351
+ function messageActionRow(node, legacyRows) {
352
+ var clock = node.getAttribute && node.getAttribute("data-clock");
353
+ return clock === "start" || clock === "end" || legacyRows.has(node);
354
+ }
355
+
269
356
  // Join inline Text nodes only within one structural block and DOM root.
270
357
  // These snapshots live for one scan. Preserve every original code unit,
271
358
  // including whitespace-only nodes and code newlines; never trim or pad.
272
359
  function collectTextBlocks(root) {
273
360
  var blocks = [];
361
+ var legacyActions = legacyMessageActionRows(root);
274
362
  var text = "";
275
363
  var segments = [];
276
364
  function flush() {
@@ -287,7 +375,7 @@ window.__ModuleLoader__.load({
287
375
  return;
288
376
  }
289
377
  if (node.nodeType !== 1 && node.nodeType !== 11) return;
290
- if (SKIP_TAGS[node.tagName] || node.id === BAR_ID || node.tagName === "BR") {
378
+ if (SKIP_TAGS[node.tagName] || node.id === BAR_ID || messageActionRow(node, legacyActions) || node.tagName === "BR") {
291
379
  // Excluded subtrees and br are hard boundaries, not empty text.
292
380
  flush();
293
381
  return;
@@ -336,9 +424,11 @@ window.__ModuleLoader__.load({
336
424
  }
337
425
 
338
426
  function scrollToRange(range) {
427
+ var viewport = readingViewport(state.target);
428
+ if (viewport && rangeRect(range, viewport)) return;
339
429
  var el = range.startContainer.parentElement;
340
430
  if (el && el.scrollIntoView) {
341
- try { el.scrollIntoView({ block: "center", behavior: "smooth" }); } catch (error) {}
431
+ try { el.scrollIntoView({ block: "center", behavior: "instant" }); } catch (error) {}
342
432
  }
343
433
  }
344
434
 
@@ -369,6 +459,11 @@ window.__ModuleLoader__.load({
369
459
  return null;
370
460
  }
371
461
 
462
+ function nestedProcessFlow(flow, root) {
463
+ return flow.hasAttribute('data-step-process-content') && flow.closest('[data-chat-group-key]') &&
464
+ flow.parentElement.closest('[data-chat-flow]') && (!root || root.contains(flow));
465
+ }
466
+
372
467
  function resolveScope(anchor, sessionId) {
373
468
  if (!anchor || typeof sessionId !== "string" || !sessionId || !visible(anchor)) return null;
374
469
  var panel = conversationPanel(anchor);
@@ -381,120 +476,76 @@ window.__ModuleLoader__.load({
381
476
  // 0.1.6-alpha.2 has the content wrapper but no identity attribute.
382
477
  // Identity still comes from the unique session-scoped header anchor.
383
478
  if (content && content.hasAttribute('data-conversation-session') && content.getAttribute('data-conversation-session') !== sessionId) return null;
384
- var flows = scrolls[0].querySelectorAll('[data-chat-flow]');
479
+ var flows = Array.prototype.filter.call(scrolls[0].querySelectorAll('[data-chat-flow]'), function (flow) {
480
+ return !nestedProcessFlow(flow);
481
+ });
385
482
  if (flows.length !== 1 || !visible(flows[0])) return null;
386
483
  return { panel: panel, root: flows[0] };
387
484
  }
388
485
 
389
486
  function createHostAdapter(ctx, changed) {
390
487
  var entries = new Map();
488
+ var mainFrame = null;
391
489
  var scopeUnavailable = false;
392
- // Only a never-selected adapter may discover an initial neutral context.
393
- // Explicit rejection or loss of a selection must never select a substitute.
394
- var selectionState = "pristine";
395
- function candidates() {
396
- var out = [];
397
- entries.forEach(function (id, anchor) {
398
- // Keep unsupported/invisible instances in the identity roster. Dropping
399
- // them before routing could silently choose a different valid session.
400
- out.push({ id: id, anchor: anchor, panel: conversationPanel(anchor), scope: resolveScope(anchor, id) });
401
- });
402
- return out;
490
+ function shown(entry) {
491
+ return visible(entry.anchor) || visible(conversationPanel(entry.anchor) || entry.anchor.closest('[data-phase], [data-conversation-content]'));
403
492
  }
404
493
  function choose(entry) {
405
- if (!entry) return null;
406
- if (!entry.scope) { selectionState = "rejected"; scopeUnavailable = true; return null; }
407
- selectionState = "selected";
408
- return { id: entry.id, anchor: entry.anchor, panel: entry.scope.panel, root: entry.scope.root };
494
+ if (entry) mainFrame = { anchor: entry.anchor, panel: conversationPanel(entry.anchor) };
495
+ var scope = entry && resolveScope(entry.anchor, entry.id);
496
+ scopeUnavailable = !!entry && !scope;
497
+ return scope ? { id: entry.id, anchor: entry.anchor, panel: scope.panel,
498
+ root: scope.root, registration: entry } : null;
409
499
  }
410
-
411
- function visibleEntry(entry) {
412
- var container = entry.panel || entry.anchor.closest('[data-phase], [data-conversation-content]');
413
- return visible(entry.anchor) || visible(container);
414
- }
415
-
416
- function discoverInitial(all) {
417
- var shown = all.filter(visibleEntry);
418
- var nodes = Array.prototype.filter.call(document.querySelectorAll('[data-phase], [data-conversation-content], [data-conversation-scroll], [data-chat-flow]'), visible);
419
- scopeUnavailable = false;
420
- if (shown.length === 0) {
421
- // An empty resident Hero is not a session choice. A known active
422
- // or identified but unregistered view is unavailable, not pristine.
423
- 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'); })) {
424
- selectionState = "rejected"; scopeUnavailable = true;
425
- }
426
- return null;
500
+ function current(previous) {
501
+ // Header utilities are mounted by the main conversation frame. Embedded
502
+ // sidebars have no such utility: focus and page-wide container counts
503
+ // cannot turn them into a search target or poison the next shortcut.
504
+ previous = previous || mainFrame;
505
+ var same = previous && entries.get(previous.anchor);
506
+ if (!same && previous && visible(previous.panel)) {
507
+ var replacements = Array.from(entries.values()).filter(function (entry) {
508
+ return conversationPanel(entry.anchor) === previous.panel && shown(entry);
509
+ });
510
+ if (replacements.length === 1) same = replacements[0];
427
511
  }
428
- if (shown.length !== 1) { selectionState = "rejected"; return null; }
429
- var entry = shown[0], scope = entry.scope;
430
- if (!scope) return choose(entry);
431
- var content = scope.root.closest('[data-conversation-content]');
432
- var scroll = scope.root.closest('[data-conversation-scroll]');
433
- // One main panel and its own content/scroll/flow count as one view.
434
- // Any other visible known container, including a headerless embedded
435
- // sidebar, makes neutral discovery ambiguous even if unsupported.
436
- var other = nodes.some(function (node) {
437
- if (node.hasAttribute('data-conversation-content') && node !== content) return true;
438
- if (node.hasAttribute('data-conversation-scroll') && node !== scroll) return true;
439
- if (node.hasAttribute('data-chat-flow') && node !== scope.root) return true;
440
- return node.hasAttribute('data-phase') && conversationPanel(node) === node && node !== scope.panel;
441
- });
442
- if (other) { selectionState = "rejected"; return null; }
443
- return choose(entry);
512
+ if (same && shown(same)) return choose(same);
513
+ // A surviving main frame with a missing utility is unavailable, not an
514
+ // invitation to select another frame. A real navigation may replace it.
515
+ if (previous && visible(previous.panel)) { scopeUnavailable = true; return null; }
516
+ var all = Array.from(entries.values()).filter(shown);
517
+ scopeUnavailable = all.length > 0;
518
+ return all.length === 1 ? choose(all[0]) : null;
444
519
  }
445
-
446
- function refresh(entry) {
447
- if (!entry) return null;
448
- // Passive checks and repeated shortcuts may only retain this exact
449
- // registered instance and identity. They never discover a substitute.
450
- if (entries.get(entry.anchor) !== entry.id) { selectionState = "rejected"; scopeUnavailable = true; return null; }
451
- scopeUnavailable = false;
452
- return choose({ id: entry.id, anchor: entry.anchor, scope: resolveScope(entry.anchor, entry.id) });
453
- }
454
-
455
520
  return {
456
- mount: function (anchor, id) {
457
- entries.set(anchor, id);
521
+ mount: function (anchor, id, nodes) {
522
+ // A new effect/store generation invalidates old async work even when
523
+ // the session id, DOM button and flow happen to be reused.
524
+ var entry = { anchor: anchor, id: id, nodes: nodes || null };
525
+ entries.set(anchor, entry);
458
526
  changed();
459
- return function () { entries.delete(anchor); changed(); };
527
+ return function () {
528
+ if (entries.get(anchor) === entry) entries.delete(anchor);
529
+ changed();
530
+ };
460
531
  },
461
- refresh: refresh,
462
- select: function (target, previous, retainNeutral) {
463
- var all = candidates();
464
- if (!target || !target.nodeType) target = document.activeElement;
465
- if (target && state.bar && state.bar.contains(target)) return refresh(previous);
466
- if (retainNeutral && (target === document.body || target === document.documentElement)) {
467
- if (previous || selectionState !== "pristine") return refresh(previous);
468
- return discoverInitial(all);
469
- }
470
- scopeUnavailable = false;
471
- var direct = all.filter(function (entry) { return target && entry.anchor.contains(target); });
472
- if (direct.length === 1) return choose(direct[0]);
473
- var content = target && target.closest ? target.closest('[data-conversation-content]') : null;
474
- var panel = conversationPanel(target) || content;
475
- var hit = all.filter(function (entry) { return panel && entry.panel === panel; });
476
- if (hit.length === 1) {
477
- if (content && hit[0].scope && hit[0].scope.root.closest('[data-conversation-content]') !== content) {
478
- selectionState = "rejected"; scopeUnavailable = true; return null;
479
- }
480
- return choose(hit[0]);
481
- }
482
- if (panel) {
483
- // Before any session is selected, focus in the resident unbound
484
- // Hero/settling composer is not a rejected conversation choice.
485
- var phase = panel.getAttribute('data-phase') || panel.getAttribute('data-content-phase');
486
- var unbound = !panel.hasAttribute('data-conversation-session') && !panel.querySelector('[data-conversation-session]');
487
- if (selectionState === "pristine" && !previous && unbound && !all.some(visibleEntry) && (phase === "hero" || phase === "settling")) return null;
488
- selectionState = "rejected"; scopeUnavailable = true; return null;
489
- }
490
- if (previous) selectionState = "rejected";
491
- return null;
532
+ nodes: function (entry) { return entry && entry.registration.nodes; },
533
+ refresh: current,
534
+ select: function (target, previous) {
535
+ // Explicit registered main-frame interaction disambiguates future
536
+ // hosts with multiple main utilities. Unregistered embedded content
537
+ // and unrelated UI never replace the current main frame.
538
+ var direct = Array.from(entries.values()).filter(function (entry) {
539
+ var panel = conversationPanel(entry.anchor);
540
+ return target && (entry.anchor.contains(target) || (panel && conversationPanel(target) === panel));
541
+ });
542
+ return direct.length === 1 ? choose(direct[0]) : current(previous);
492
543
  },
493
544
  scopeUnavailable: function () { return scopeUnavailable; },
494
545
  valid: function (entry) {
495
- if (!entry || entries.get(entry.anchor) !== entry.id) return false;
546
+ if (!entry || entries.get(entry.anchor) !== entry.registration) return false;
496
547
  var scope = resolveScope(entry.anchor, entry.id);
497
- return !!scope && scope.root === entry.root;
548
+ return !!scope && scope.panel === entry.panel && scope.root === entry.root;
498
549
  },
499
550
  face: function (entry) {
500
551
  try {
@@ -504,10 +555,50 @@ window.__ModuleLoader__.load({
504
555
  return face && typeof face.loadOlder === "function" ? face : null;
505
556
  } catch (error) { return null; }
506
557
  },
507
- clear: function () { entries.clear(); }
558
+ clear: function () { entries.clear(); mainFrame = null; }
508
559
  };
509
560
  }
510
561
 
562
+ // RC2's rendered Turn controls call its navigation owner, including
563
+ // cancelling a retained paging anchor. Use that actual loaded UI action;
564
+ // never synthesize wheel/beforematch or import a private Chat controller.
565
+ function hostNavigationReady(target, face) {
566
+ var status = face && facePageState(face);
567
+ if (face && (!status || status.reason || pendingLoads.has(face))) return false;
568
+ // The snapshot can settle before React commits Chat's loadingOlder
569
+ // input. Its direct history control must also stop being disabled.
570
+ return !target.root.querySelector(':scope > div:not([data-chat-anchor-key]):not([data-chat-group-key]) > button:disabled');
571
+ }
572
+
573
+ function activateLoadedTurn(target, store, preferredTurn) {
574
+ if (!store) return false;
575
+ var list = target.root.parentElement, chat = list && list.parentElement, frame = chat && chat.parentElement;
576
+ if (!frame || !target.panel.contains(frame)) return false;
577
+ var navs = Array.prototype.filter.call(frame.querySelectorAll('nav'), function (nav) {
578
+ var label = nav.getAttribute('aria-label');
579
+ return !target.root.contains(nav) && (label === 'Turn navigation' || label === '轮次导航');
580
+ });
581
+ if (navs.length !== 1 || !visible(navs[0])) return false;
582
+ var loaded = new Set();
583
+ Array.prototype.forEach.call(target.root.querySelectorAll('[data-chat-anchor-key][data-chat-turn]'), function (row) {
584
+ var key = row.getAttribute('data-chat-node-key') || row.getAttribute('data-chat-anchor-key');
585
+ if (store.get(key)) loaded.add(row.getAttribute('data-chat-turn'));
586
+ });
587
+ var candidates = [];
588
+ Array.prototype.forEach.call(navs[0].querySelectorAll('button[data-index]'), function (button) {
589
+ // These exact loaded labels are locale-owned by the inspected host.
590
+ // The distinct "Load and jump" labels must never be activated here.
591
+ var match = /^(?:Jump to turn (\d+)|跳转到第 (\d+) 轮)$/.exec(button.getAttribute('aria-label') || '');
592
+ var turn = match && (match[1] || match[2]);
593
+ if (!turn || !loaded.has(turn) || button.disabled || button.getAttribute('aria-busy') === 'true' || !visible(button)) return;
594
+ candidates.push({ button: button, rank: turn === preferredTurn ? 0 : button.getAttribute('aria-current') === 'true' ? 1 : 2 });
595
+ });
596
+ candidates.sort(function (a, b) { return a.rank - b.rank; });
597
+ if (!candidates.length) return false;
598
+ candidates[0].button.click();
599
+ return true;
600
+ }
601
+
511
602
  function domSignature(root) {
512
603
  return root ? collectTextNodes(root, []).length : 0;
513
604
  }
@@ -571,6 +662,427 @@ window.__ModuleLoader__.load({
571
662
  }
572
663
  //#endregion
573
664
 
665
+ //#region one find session's reading origin
666
+ function readingViewport(target) {
667
+ var scroll = target && target.root.closest('[data-conversation-scroll]');
668
+ if (!scroll) return null;
669
+ var rect = scroll.getBoundingClientRect();
670
+ var bottom = Math.min(rect.bottom, window.innerHeight);
671
+ var composer = scroll.querySelector('[data-composer-seat]');
672
+ if (composer) {
673
+ var composerTop = composer.getBoundingClientRect().top;
674
+ if (composerTop > rect.top) bottom = Math.min(bottom, composerTop);
675
+ }
676
+ var top = Math.max(0, rect.top);
677
+ return bottom > top && rect.width > 0 ? { scroll: scroll, top: top, bottom: bottom, left: Math.max(0, rect.left), right: Math.min(window.innerWidth, rect.right) } : null;
678
+ }
679
+
680
+ function readingText(row) {
681
+ var text = '', segments = [];
682
+ var walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT), node;
683
+ while ((node = walker.nextNode()) !== null) {
684
+ var parent = node.parentElement;
685
+ if (!parent || parent.closest('script, style, noscript, button, input, textarea, select, [data-code-block-banner]')) continue;
686
+ if (parent.closest('[data-chat-anchor-key]') !== row) continue;
687
+ segments.push({ node: node, start: text.length, end: text.length + node.data.length });
688
+ text += node.data;
689
+ }
690
+ return { text: text, segments: segments };
691
+ }
692
+
693
+ function readingRange(snapshot, offset) {
694
+ for (var i = 0; i < snapshot.segments.length; i++) {
695
+ var segment = snapshot.segments[i];
696
+ if (offset < segment.start || offset >= segment.end) continue;
697
+ var start = offset - segment.start;
698
+ var range = document.createRange();
699
+ range.setStart(segment.node, start);
700
+ range.setEnd(segment.node, Math.min(segment.node.data.length, start + (segment.node.data.codePointAt(start) > 65535 ? 2 : 1)));
701
+ return range;
702
+ }
703
+ return null;
704
+ }
705
+
706
+ function clippedViewport(element, viewport) {
707
+ var top = viewport.top, bottom = viewport.bottom, left = viewport.left, right = viewport.right;
708
+ for (var parent = element; parent && parent !== viewport.scroll; parent = parent.parentElement) {
709
+ var style = window.getComputedStyle(parent);
710
+ var clipsY = /^(auto|scroll|hidden|clip)$/.test(style.overflowY), clipsX = /^(auto|scroll|hidden|clip)$/.test(style.overflowX);
711
+ if (!clipsX && !clipsY) continue;
712
+ var box = parent.getBoundingClientRect();
713
+ if (clipsY) { top = Math.max(top, box.top); bottom = Math.min(bottom, box.bottom); }
714
+ if (clipsX) { left = Math.max(left, box.left); right = Math.min(right, box.right); }
715
+ }
716
+ return { top: top, bottom: bottom, left: left, right: right };
717
+ }
718
+
719
+ function rangeRect(range, viewport) {
720
+ if (!range || !range.startContainer.isConnected || typeof range.getClientRects !== 'function') return null;
721
+ var clip = viewport && clippedViewport(range.startContainer.parentElement, viewport);
722
+ var rects = range.getClientRects();
723
+ for (var i = 0; i < rects.length; i++) {
724
+ var rect = rects[i];
725
+ if (rect.height > 0 && rect.width > 0 && (!viewport || (clip.bottom > clip.top && clip.right > clip.left && rect.bottom > clip.top && rect.top < clip.bottom && rect.right > clip.left && rect.left < clip.right))) return rect;
726
+ }
727
+ return null;
728
+ }
729
+
730
+ function hiddenUntilFoundAncestor(node, root) {
731
+ var element = node && (node.nodeType === 1 ? node : node.parentElement);
732
+ for (; element && root && root.contains(element); element = element.parentElement) {
733
+ var hidden = element.getAttribute && element.getAttribute('hidden');
734
+ if (typeof hidden === 'string' && hidden.toLowerCase() === 'until-found') return element;
735
+ if (element === root) break;
736
+ }
737
+ return null;
738
+ }
739
+
740
+ function captureReadingOrigin() {
741
+ var target = state.target, viewport = readingViewport(target);
742
+ var store = state.adapter && state.adapter.nodes(target);
743
+ if (!viewport || !store) return null;
744
+ var rows = target.root.querySelectorAll('[data-chat-anchor-key]');
745
+ var best = null;
746
+ for (var i = 0; i < rows.length; i++) {
747
+ var row = rows[i];
748
+ if (!visible(row) || row.hasAttribute('data-chat-group-key')) continue;
749
+ var anchorKey = row.getAttribute('data-chat-anchor-key');
750
+ var nodeKey = row.getAttribute('data-chat-node-key') || anchorKey;
751
+ var node = store.get(nodeKey);
752
+ if (!node || !Number.isFinite(node.anchorSeq)) continue;
753
+ var snapshot = readingText(row);
754
+ for (var j = 0; j < snapshot.segments.length; j++) {
755
+ var segment = snapshot.segments[j];
756
+ var whole = document.createRange(); whole.selectNodeContents(segment.node);
757
+ if (!rangeRect(whole, viewport)) continue;
758
+ // Text order follows the rendered line order inside one text node.
759
+ // The final geometry check rejects unsupported or non-visible runs.
760
+ var clipTop = clippedViewport(segment.node.parentElement, viewport).top;
761
+ var lo = segment.start, hi = segment.end - 1;
762
+ while (lo < hi) {
763
+ var mid = Math.floor((lo + hi) / 2);
764
+ var probe = rangeRect(readingRange(snapshot, mid));
765
+ if (probe && probe.bottom <= clipTop + 1) lo = mid + 1;
766
+ else hi = mid;
767
+ }
768
+ var rect = rangeRect(readingRange(snapshot, lo), viewport);
769
+ if (!rect) continue;
770
+ var distance = Math.abs(rect.top - viewport.top);
771
+ if (best && distance >= best.distance) continue;
772
+ var body = row.closest('[data-step-process-body]');
773
+ var group = body && body.closest('[data-chat-group-key]');
774
+ best = {
775
+ inner: group ? { key: group.getAttribute('data-chat-group-key'), top: rect.top - body.getBoundingClientRect().top } : null,
776
+ target: target, store: store, face: state.adapter.face(target), nodeKey: nodeKey, anchorKey: anchorKey,
777
+ seq: node.anchorSeq, text: snapshot.text, offset: lo, top: rect.top - viewport.top,
778
+ focus: document.activeElement, distance: distance, navigated: false
779
+ };
780
+ }
781
+ }
782
+ return best;
783
+ }
784
+
785
+ function originValid(origin) {
786
+ return !!origin && isOpen() && state.origin === origin && state.target === origin.target &&
787
+ state.adapter && state.adapter.valid(origin.target) && state.adapter.nodes(origin.target) === origin.store &&
788
+ state.adapter.face(origin.target) === origin.face;
789
+ }
790
+
791
+ function readingRow(origin) {
792
+ var rows = origin.target.root.querySelectorAll('[data-chat-anchor-key]'), found = null;
793
+ for (var i = 0; i < rows.length; i++) {
794
+ if (rows[i].getAttribute('data-chat-anchor-key') !== origin.anchorKey ||
795
+ (rows[i].getAttribute('data-chat-node-key') || origin.anchorKey) !== origin.nodeKey) continue;
796
+ if (found) return null;
797
+ found = rows[i];
798
+ }
799
+ return found;
800
+ }
801
+
802
+ function locateReading(origin, row) {
803
+ if (!row || !visible(row)) return null;
804
+ var snapshot = readingText(row), offset = origin.offset;
805
+ if (snapshot.text !== origin.text && !snapshot.text.startsWith(origin.text)) {
806
+ var from = Math.max(0, origin.offset - 64);
807
+ var quote = origin.text.slice(from, origin.offset + 65);
808
+ if (origin.text.indexOf(quote) !== from || origin.text.indexOf(quote, from + 1) >= 0) return null;
809
+ var index = snapshot.text.indexOf(quote);
810
+ if (index < 0 || snapshot.text.indexOf(quote, index + 1) >= 0) return null;
811
+ offset = index + origin.offset - from;
812
+ }
813
+ return readingRange(snapshot, offset);
814
+ }
815
+
816
+ function updateReturnButton() {
817
+ if (!state.returnBtn) return;
818
+ var available = originValid(state.origin) && state.origin.navigated;
819
+ state.returnBtn.hidden = !state.restoring && !available;
820
+ state.returnBtn.disabled = !!state.restoring;
821
+ state.returnBtn.textContent = state.restoring ? L.returning : L.returnPosition;
822
+ state.returnBtn.setAttribute('aria-label', state.returnBtn.textContent);
823
+ state.returnBtn.title = state.returnBtn.textContent;
824
+ }
825
+
826
+ function cancelNavigation() {
827
+ clearTimeout(state.navigationTimer);
828
+ state.navigationTimer = 0;
829
+ state.navigationToken += 1;
830
+ }
831
+
832
+ function cancelReturn(message) {
833
+ if (!state.restoring) return;
834
+ state.restoring = null;
835
+ if (state.settleCancel) state.settleCancel();
836
+ if (message) setStatus(message, false);
837
+ updateReturnButton();
838
+ }
839
+
840
+ function discardOrigin() {
841
+ cancelReturn();
842
+ cancelNavigation();
843
+ state.origin = null;
844
+ updateReturnButton();
845
+ }
846
+
847
+ function navigateToRange(range) {
848
+ cancelReturn(L.returnCancelled);
849
+ cancelNavigation();
850
+ var target = state.target, face = state.adapter && state.adapter.face(target);
851
+ if (target && state.adapter.valid(target) && hostNavigationReady(target, face)) {
852
+ var row = range.startContainer.parentElement.closest('[data-chat-turn]');
853
+ activateLoadedTurn(target, state.adapter.nodes(target), row && row.getAttribute('data-chat-turn'));
854
+ if (state.target !== target || !state.adapter.valid(target) || state.adapter.face(target) !== face) return;
855
+ }
856
+ var origin = state.origin, token = state.navigationToken, started = Date.now();
857
+ var activated = new Set(), scrolledRange = null;
858
+ function check() {
859
+ state.navigationTimer = 0;
860
+ if (token !== state.navigationToken || !originValid(origin)) return;
861
+ var viewport = readingViewport(origin.target);
862
+ var current = state.ranges[state.index];
863
+ var attached = current && origin.target.root.contains(current.startContainer);
864
+ // hidden=until-found uses content-visibility:hidden, so Chromium may
865
+ // report layout geometry for descendants that are not painted. Reveal
866
+ // the host-owned disclosure before trusting Range geometry.
867
+ var concealed = attached && hiddenUntilFoundAncestor(current.startContainer, origin.target.root);
868
+ var geometry = attached && !concealed && rangeRect(current);
869
+ if (current && (!geometry || concealed)) revealMatchRange(origin.target, current, activated);
870
+ if (geometry && scrolledRange !== current) {
871
+ scrollToRange(current);
872
+ scrolledRange = current;
873
+ }
874
+ if (viewport && attached && !concealed && rangeRect(current, viewport)) {
875
+ var firstNavigation = !origin.navigated;
876
+ origin.navigated = true;
877
+ updateReturnButton();
878
+ if (!firstNavigation && state.status && state.status.textContent === L.matchRevealFailed) setStatus("", false);
879
+ if (firstNavigation) {
880
+ if (state.jumpStatus !== null) {
881
+ var priorStatus = state.jumpStatus;
882
+ setStatus(priorStatus, false);
883
+ } else setStatus(L.returnAvailable, false);
884
+ if (state.announcement) state.announcement.textContent = L.returnAvailable;
885
+ }
886
+ return;
887
+ }
888
+ if (Date.now() - started < 1500) {
889
+ state.navigationTimer = setTimeout(check, 100);
890
+ return;
891
+ }
892
+ setStatus(L.matchRevealFailed, false);
893
+ if (state.announcement) state.announcement.textContent = L.matchRevealFailed;
894
+ }
895
+ check();
896
+ }
897
+
898
+ function focusWithoutScroll(element) {
899
+ if (!element || !element.isConnected || typeof element.focus !== "function") return;
900
+ try { element.focus({ preventScroll: true }); } catch (error) { element.focus(); }
901
+ }
902
+
903
+ function activateDisclosure(button, activated) {
904
+ if (!button || button.disabled || !visible(button) || activated.has(button)) return false;
905
+ activated.add(button);
906
+ // DSH's disclosure handlers focus their button. Pre-focus without
907
+ // scrolling, then restore the find control that initiated navigation.
908
+ var previous = document.activeElement;
909
+ focusWithoutScroll(button);
910
+ button.click();
911
+ if (previous !== button) focusWithoutScroll(previous);
912
+ return true;
913
+ }
914
+
915
+ function revealMatchRange(target, range, activated) {
916
+ if (!target || !range || !target.root.contains(range.startContainer)) return false;
917
+ var element = range.startContainer.nodeType === 1 ? range.startContainer : range.startContainer.parentElement;
918
+ if (!element) return false;
919
+ var changed = false;
920
+ // Native details participates in the platform ancestor-revealing
921
+ // algorithm. Opening it does not bypass application-owned state.
922
+ for (var node = element; node && target.root.contains(node); node = node.parentElement) {
923
+ if (node.tagName === "DETAILS" && !node.open) {
924
+ node.open = true;
925
+ changed = true;
926
+ }
927
+ }
928
+ var ancestors = [];
929
+ for (node = element; node && target.root.contains(node); node = node.parentElement) {
930
+ ancestors.push(node);
931
+ }
932
+
933
+ // DSH retains folded process content with hidden=until-found. Use its
934
+ // rendered disclosure controls so React and the navigation owner remain
935
+ // synchronized; never edit the hidden attribute or synthesize beforematch.
936
+ var row = element.closest('[data-chat-turn]');
937
+ var turn = row && row.getAttribute('data-chat-turn');
938
+ var buttons = target.root.querySelectorAll('button[data-turn-process][aria-expanded="false"]');
939
+ for (var i = 0; i < buttons.length; i++) {
940
+ var owner = buttons[i].closest('[data-chat-turn]');
941
+ if (turn && owner && owner.getAttribute('data-chat-turn') === turn && activateDisclosure(buttons[i], activated)) return true;
942
+ }
943
+ // Prefer the disclosure's semantic aria-controls relationship for
944
+ // nested groups. Walk outermost first because an inner control can itself
945
+ // still be inside a folded outer Turn.
946
+ buttons = target.root.querySelectorAll('button[aria-controls][aria-expanded="false"]');
947
+ for (var h = ancestors.length - 1; h >= 0; h--) {
948
+ if (!ancestors[h].id) continue;
949
+ for (i = 0; i < buttons.length; i++) {
950
+ if (buttons[i].getAttribute('aria-controls') === ancestors[h].id && activateDisclosure(buttons[i], activated)) return true;
951
+ }
952
+ }
953
+ // Older supported hosts expose the same group disclosure without
954
+ // aria-controls, but keep its stable group/activity markers.
955
+ var group = row && row.closest('[data-chat-group-key]');
956
+ var button = group && group.querySelector('button[data-process-activity][aria-expanded="false"]');
957
+ if (activateDisclosure(button, activated)) return true;
958
+ return changed;
959
+ }
960
+
961
+ function returnFailure(reason) {
962
+ return L.returnFailed + ' ' + (L['return_' + reason] || L.return_unavailable);
963
+ }
964
+
965
+ async function returnToOrigin() {
966
+ var origin = state.origin;
967
+ if (state.restoring || !originValid(origin) || !origin.navigated) return;
968
+ cancelPageIn(); cancelNavigation();
969
+ var task = { origin: origin };
970
+ state.restoring = task;
971
+ setStatus(L.returning, false); updateReturnButton();
972
+ function cancelled() { return state.restoring !== task || !originValid(origin); }
973
+ function finishFailure(reason) {
974
+ if (cancelled()) return;
975
+ state.restoring = null;
976
+ if (reason === 'removed') state.origin = null;
977
+ setStatus(returnFailure(reason), false); updateReturnButton();
978
+ }
979
+ function nodeState() {
980
+ if (origin.face && origin.face.getSnapshot().removed) return 'removed';
981
+ if (origin.store.get(origin.nodeKey)) return 'loaded';
982
+ var nodes = origin.store.values();
983
+ var first = nodes.reduce(function (min, node) { return Math.min(min, node.anchorSeq); }, Infinity);
984
+ // Materialized Chat nodes do not expose the event window's lower bound.
985
+ // Earlier missing content permits a bounded load attempt, not a deletion claim.
986
+ return first > origin.seq ? 'missing' : 'unavailable';
987
+ }
988
+ try {
989
+ var pages = 0;
990
+ while (!cancelled() && nodeState() === 'missing') {
991
+ if (!origin.face) { finishFailure('unavailable'); return; }
992
+ if (pages >= MAX_PAGES) { finishFailure('capped'); return; }
993
+ var ready = await waitForPage(function () {
994
+ if (pendingLoads.has(origin.face)) return null;
995
+ return facePageState(origin.face);
996
+ }, cancelled);
997
+ if (cancelled()) return;
998
+ if (ready.reason) { finishFailure(ready.reason); return; }
999
+ if (nodeState() !== 'missing') break;
1000
+ if (ready.hasMore !== true) { finishFailure('unavailable'); return; }
1001
+ var before = origin.store.values().map(function (node) { return node.key + ':' + node.anchorSeq; }).join('|');
1002
+ var loaded = await loadPage(origin.face, cancelled);
1003
+ if (cancelled()) return;
1004
+ if (loaded.reason) { finishFailure(loaded.reason); return; }
1005
+ var progress = await waitForPage(function () {
1006
+ if (nodeState() !== 'missing') return {};
1007
+ var after = origin.store.values().map(function (node) { return node.key + ':' + node.anchorSeq; }).join('|');
1008
+ return before !== after ? {} : null;
1009
+ }, cancelled);
1010
+ if (cancelled()) return;
1011
+ if (progress.reason) { finishFailure(progress.reason); return; }
1012
+ pages += 1;
1013
+ }
1014
+ if (cancelled()) return;
1015
+ if (nodeState() !== 'loaded') { finishFailure(nodeState()); return; }
1016
+ var navigationReady = await waitForPage(function () {
1017
+ var status = origin.face && facePageState(origin.face);
1018
+ if (status && status.reason) return status;
1019
+ return hostNavigationReady(origin.target, origin.face) ? {} : null;
1020
+ }, cancelled);
1021
+ if (cancelled()) return;
1022
+ if (navigationReady.reason) { finishFailure(navigationReady.reason); return; }
1023
+ var originalRow = readingRow(origin);
1024
+ activateLoadedTurn(origin.target, origin.store, originalRow && originalRow.getAttribute('data-chat-turn'));
1025
+ if (cancelled()) return;
1026
+ var activated = new Set();
1027
+ var rendered = await waitForPage(function () {
1028
+ var row = readingRow(origin);
1029
+ var range = locateReading(origin, row);
1030
+ if (range && !rangeRect(range)) revealMatchRange(origin.target, range, activated);
1031
+ return range && rangeRect(range) ? { row: row, range: range } : null;
1032
+ }, cancelled);
1033
+ if (cancelled()) return;
1034
+ if (rendered.reason) { finishFailure('render'); return; }
1035
+ var inner = rendered.row.closest('[data-step-process-body]');
1036
+ if (inner && inner.scrollHeight > inner.clientHeight) {
1037
+ var group = inner.closest('[data-chat-group-key]');
1038
+ if (!group) { finishFailure('render'); return; }
1039
+ var innerRect = rangeRect(rendered.range), innerBox = inner.getBoundingClientRect();
1040
+ var sameGroup = origin.inner && group.getAttribute('data-chat-group-key') === origin.inner.key;
1041
+ var innerOffset = Math.max(1 - innerRect.height, Math.min(sameGroup ? origin.inner.top : 0, inner.clientHeight - 1));
1042
+ if (cancelled()) return;
1043
+ inner.scrollTop += innerRect.top - innerBox.top - innerOffset;
1044
+ }
1045
+ var viewport = readingViewport(origin.target), rect = rangeRect(rendered.range);
1046
+ if (!viewport || !rect || cancelled()) { finishFailure('unavailable'); return; }
1047
+ var offset = Math.max(1 - rect.height, Math.min(origin.top, viewport.bottom - viewport.top - 1));
1048
+ var desired = viewport.scroll.scrollTop + rect.top - viewport.top - offset;
1049
+ var top = Math.max(0, Math.min(viewport.scroll.scrollHeight - viewport.scroll.clientHeight, desired));
1050
+ var expectedOffset = offset + desired - top;
1051
+ var initialHeight = viewport.scroll.scrollHeight, initialWidth = viewport.scroll.getBoundingClientRect().width;
1052
+ // One instant landing, then observation. Never fight the host's own
1053
+ // retained paging anchor with repeated scroll writes.
1054
+ viewport.scroll.scrollTo({ top: top, behavior: 'instant' });
1055
+ var started = Date.now(), stable = null;
1056
+ var settled = await waitForPage(function () {
1057
+ var live = locateReading(origin, readingRow(origin));
1058
+ var bounds = readingViewport(origin.target), box = bounds && rangeRect(live, bounds);
1059
+ if (!box) { stable = null; return Date.now() - started >= 1000 ? { reason: 'position' } : null; }
1060
+ var current = box.top - bounds.top;
1061
+ var layoutChanged = bounds.scroll.scrollHeight !== initialHeight || bounds.scroll.getBoundingClientRect().width !== initialWidth;
1062
+ if (!layoutChanged && Math.abs(current - expectedOffset) > 3) return Date.now() - started >= 1000 ? { reason: 'position' } : null;
1063
+ if (!stable || Math.abs(stable.top - current) > 2) stable = { top: current, since: Date.now() };
1064
+ if (Date.now() - stable.since < 600) return null;
1065
+ return { row: readingRow(origin), range: live };
1066
+ }, cancelled);
1067
+ if (cancelled()) return;
1068
+ if (settled.reason) { finishFailure(settled.reason); return; }
1069
+ if (!rangeRect(settled.range, readingViewport(origin.target))) { finishFailure('position'); return; }
1070
+ state.restoring = null;
1071
+ origin.navigated = false;
1072
+ if (state.input) state.input.focus({ preventScroll: true });
1073
+ setStatus(L.returned, false);
1074
+ updateReturnButton();
1075
+ if (state.announcement) state.announcement.textContent = L.returned;
1076
+ } catch (error) { finishFailure('error'); }
1077
+ finally {
1078
+ if (state.restoring === task && !originValid(origin)) {
1079
+ state.restoring = null; state.origin = null; updateReturnButton();
1080
+ if (isOpen()) setStatus(returnFailure('unavailable'), false);
1081
+ }
1082
+ }
1083
+ }
1084
+ //#endregion
1085
+
574
1086
  //#region bar UI + state
575
1087
 
576
1088
  var state = {
@@ -597,9 +1109,71 @@ window.__ModuleLoader__.load({
597
1109
  pageToken: 0,
598
1110
  pages: 0,
599
1111
  sessionId: null,
600
- sessionTimer: 0
1112
+ sessionTimer: 0,
1113
+ origin: null, restoring: null, returnBtn: null, announcement: null, navigationTimer: 0, navigationToken: 0,
1114
+ jumpStatus: null,
1115
+ positionObserver: null,
1116
+ positionFrame: 0
601
1117
  };
602
1118
 
1119
+ function barAnchor(target) {
1120
+ if (!target || !target.anchor || !target.panel || !target.anchor.closest) return null;
1121
+ var header = target.anchor.closest('[data-conversation-header], header');
1122
+ if (header && target.panel.contains(header)) return header;
1123
+ // Older supported hosts may not expose the header data attribute. The
1124
+ // registered utility still lives in the header's direct child of the panel.
1125
+ var child = target.anchor;
1126
+ while (child && child.parentElement && child.parentElement !== target.panel) child = child.parentElement;
1127
+ return child && child.parentElement === target.panel ? child : null;
1128
+ }
1129
+
1130
+ function positionBar() {
1131
+ if (!isOpen() || !state.bar) return false;
1132
+ if (!state.target) { state.bar.removeAttribute("data-positioned"); return false; }
1133
+ var header = barAnchor(state.target);
1134
+ if (!header || !header.isConnected || !state.target.panel.isConnected) {
1135
+ state.bar.removeAttribute("data-positioned");
1136
+ return false;
1137
+ }
1138
+ var headerRect = header.getBoundingClientRect();
1139
+ var panelRect = state.target.panel.getBoundingClientRect();
1140
+ var viewportWidth = document.documentElement.clientWidth || window.innerWidth;
1141
+ var panelLeft = Math.max(0, panelRect.left);
1142
+ var panelRight = Math.min(viewportWidth, panelRect.right);
1143
+ var dpr = window.devicePixelRatio || 1;
1144
+ function cssPixel(value) { return Math.round(value * dpr) / dpr + "px"; }
1145
+ state.bar.style.setProperty("--dsh-find-all-header-bottom", cssPixel(headerRect.bottom));
1146
+ state.bar.style.setProperty("--dsh-find-all-panel-right-space", cssPixel(Math.max(0, viewportWidth - panelRight)));
1147
+ state.bar.style.setProperty("--dsh-find-all-panel-width", cssPixel(Math.max(0, panelRight - panelLeft)));
1148
+ state.bar.setAttribute("data-positioned", "");
1149
+ return true;
1150
+ }
1151
+
1152
+ function scheduleBarPosition() {
1153
+ if (!isOpen() || state.positionFrame) return;
1154
+ if (typeof requestAnimationFrame !== "function") { positionBar(); return; }
1155
+ state.positionFrame = requestAnimationFrame(function () {
1156
+ state.positionFrame = 0;
1157
+ positionBar();
1158
+ });
1159
+ }
1160
+
1161
+ function stopBarPositioning() {
1162
+ if (state.positionObserver) { state.positionObserver.disconnect(); state.positionObserver = null; }
1163
+ if (state.positionFrame && typeof cancelAnimationFrame === "function") cancelAnimationFrame(state.positionFrame);
1164
+ state.positionFrame = 0;
1165
+ }
1166
+
1167
+ function startBarPositioning() {
1168
+ stopBarPositioning();
1169
+ positionBar();
1170
+ var header = barAnchor(state.target);
1171
+ if (!header || typeof ResizeObserver === "undefined") return;
1172
+ state.positionObserver = new ResizeObserver(scheduleBarPosition);
1173
+ state.positionObserver.observe(header);
1174
+ if (state.target.panel !== header) state.positionObserver.observe(state.target.panel);
1175
+ }
1176
+
603
1177
  function paint() {
604
1178
  if (!highlightsSupported()) return;
605
1179
  CSS.highlights.delete(HL_ALL);
@@ -617,6 +1191,7 @@ window.__ModuleLoader__.load({
617
1191
  var n = state.ranges.length;
618
1192
  if (!state.query) state.count.textContent = "";
619
1193
  else if (n === 0) state.count.textContent = state.paging ? "…" : "0/0";
1194
+ else if (state.index < 0) state.count.textContent = L.resultCount.replace("{n}", String(n) + (state.capped ? "+" : ""));
620
1195
  else state.count.textContent = (state.index + 1) + "/" + n + (state.capped ? "+" : "");
621
1196
  var cap = state.query && n > 0 && state.capped ? L.cap.replace("{n}", String(MAX_MATCHES)) : "";
622
1197
  state.count.title = cap;
@@ -626,6 +1201,7 @@ window.__ModuleLoader__.load({
626
1201
 
627
1202
  function setStatus(text, busy) {
628
1203
  if (!state.status) return;
1204
+ state.jumpStatus = null;
629
1205
  state.status.textContent = text || "";
630
1206
  state.status.title = busy ? L.stopTip : (text || "");
631
1207
  if (busy) state.status.setAttribute("data-busy", "1");
@@ -633,6 +1209,11 @@ window.__ModuleLoader__.load({
633
1209
  updateCount();
634
1210
  }
635
1211
 
1212
+ function setJumpStatus(base) {
1213
+ setStatus(L.jumpHint.replace("{n}", String(state.ranges.length)) + (base ? " " + base : ""), false);
1214
+ state.jumpStatus = base || "";
1215
+ }
1216
+
636
1217
  function updateScopeButton() {
637
1218
  if (!state.scopeBtn) return;
638
1219
  var whole = state.scope === "whole";
@@ -742,6 +1323,7 @@ window.__ModuleLoader__.load({
742
1323
  var retained = keepIndex && valid ? retainedIndex(state.selection) : -1;
743
1324
  if (!state.ranges.length) state.index = -1;
744
1325
  else if (!keepIndex) state.index = 0;
1326
+ else if (state.index < 0) state.index = -1;
745
1327
  else state.index = retained >= 0 ? retained : Math.max(0, Math.min(state.index, state.ranges.length - 1));
746
1328
  rememberSelection();
747
1329
  paint();
@@ -750,14 +1332,15 @@ window.__ModuleLoader__.load({
750
1332
  }
751
1333
 
752
1334
  function goTo(delta) {
1335
+ cancelReturn(L.returnCancelled);
753
1336
  if (!state.adapter || !state.adapter.valid(state.target)) { runSearch(false); return; }
754
1337
  var n = state.ranges.length;
755
1338
  if (n === 0) return;
756
- state.index = ((state.index + delta) % n + n) % n;
1339
+ state.index = state.index < 0 ? (delta < 0 ? n - 1 : 0) : ((state.index + delta) % n + n) % n;
757
1340
  rememberSelection();
758
1341
  paint();
759
1342
  updateCount();
760
- scrollToRange(state.ranges[state.index]);
1343
+ navigateToRange(state.ranges[state.index]);
761
1344
  }
762
1345
 
763
1346
  function scheduleRescan() {
@@ -858,26 +1441,34 @@ window.__ModuleLoader__.load({
858
1441
  if (result.reason === "done" && (!terminal || terminal.reason || terminal.hasMore !== false || pendingLoads.has(face))) result.reason = "stalled";
859
1442
  state.paging = false;
860
1443
  state.pages = result.pages;
861
- if (result.reason === "capped") setStatus(L.capped.replace("{n}", String(result.pages)), false);
862
- else if (result.reason !== "done") setStatus(L.incomplete, false);
863
- else setStatus(L.available, false);
864
1444
  runSearch(true);
1445
+ if (!state.adapter || !state.adapter.valid(target)) return;
1446
+ var message = result.reason === "capped" ? L.capped.replace("{n}", String(result.pages)) :
1447
+ result.reason !== "done" ? L.incomplete : L.available;
1448
+ if (state.ranges.length > 0 && originValid(state.origin) && !state.origin.navigated) setJumpStatus(message);
1449
+ else setStatus(message, false);
865
1450
  });
866
1451
  }
867
1452
 
868
- function syncTarget(target, passive, retainNeutral) {
1453
+ function syncTarget(target, passive) {
869
1454
  if (!state.adapter) return;
870
- var selected = passive ? state.adapter.refresh(state.target) : state.adapter.select(target || document.activeElement, state.target, retainNeutral);
871
- if (selected && state.target && selected.id === state.target.id && selected.anchor === state.target.anchor && selected.root === state.target.root) return;
1455
+ var selected = passive ? state.adapter.refresh(state.target) : state.adapter.select(target || document.activeElement, state.target);
1456
+ if (selected && state.target && selected.id === state.target.id && selected.anchor === state.target.anchor && selected.root === state.target.root && selected.panel === state.target.panel && selected.registration === state.target.registration) {
1457
+ if (state.origin && !originValid(state.origin)) { discardOrigin(); setStatus(returnFailure('unavailable'), false); }
1458
+ scheduleBarPosition();
1459
+ return;
1460
+ }
872
1461
  if (!selected && !state.target) {
873
1462
  if (isOpen()) runSearch(false);
874
1463
  return;
875
1464
  }
1465
+ discardOrigin();
876
1466
  cancelPageIn();
877
1467
  stopObserver();
878
1468
  state.target = selected;
879
1469
  state.sessionId = selected ? selected.id : null;
880
1470
  if (!isOpen()) return;
1471
+ startBarPositioning();
881
1472
  setStatus(selected ? "" : unavailableStatus(), false);
882
1473
  runSearch(false);
883
1474
  startObserver();
@@ -920,15 +1511,17 @@ window.__ModuleLoader__.load({
920
1511
  }
921
1512
 
922
1513
  function onQueryInput(input) {
1514
+ cancelReturn(L.returnCancelled); cancelNavigation();
923
1515
  state.query = input.value;
924
1516
  cancelPageIn();
925
1517
  setStatus("", false);
926
1518
  runSearch(false);
927
- if (state.ranges.length > 0 && highlightsSupported()) scrollToRange(state.ranges[state.index]);
1519
+ if (state.ranges.length > 0 && highlightsSupported()) navigateToRange(state.ranges[state.index]);
928
1520
  if (state.scope === "whole" && state.query) schedulePageIn();
929
1521
  }
930
1522
 
931
1523
  function toggleScope() {
1524
+ cancelReturn(L.returnCancelled);
932
1525
  state.scope = state.scope === "whole" ? "page" : "whole";
933
1526
  updateScopeButton();
934
1527
  if (state.scope === "page") {
@@ -944,6 +1537,8 @@ window.__ModuleLoader__.load({
944
1537
  var bar = document.createElement("div");
945
1538
  bar.id = BAR_ID;
946
1539
  bar.setAttribute("role", "search");
1540
+ var controls = document.createElement("div");
1541
+ controls.className = "controls";
947
1542
 
948
1543
  var input = document.createElement("input");
949
1544
  input.type = "text";
@@ -960,9 +1555,13 @@ window.__ModuleLoader__.load({
960
1555
  count.className = "count";
961
1556
  count.setAttribute("role", "status");
962
1557
  count.setAttribute("aria-live", "polite");
1558
+ count.setAttribute("aria-atomic", "true");
963
1559
 
964
1560
  var status = document.createElement("span");
965
1561
  status.className = "status";
1562
+ status.setAttribute("role", "status");
1563
+ status.setAttribute("aria-live", "polite");
1564
+ status.setAttribute("aria-atomic", "true");
966
1565
  status.title = L.stopTip;
967
1566
  status.addEventListener("click", function () {
968
1567
  if (!state.paging) return;
@@ -970,13 +1569,20 @@ window.__ModuleLoader__.load({
970
1569
  setStatus(L.incomplete, false);
971
1570
  });
972
1571
 
973
- bar.appendChild(input);
974
- bar.appendChild(scopeBtn);
975
- bar.appendChild(count);
976
- bar.appendChild(button(L.prev, "m18 15-6-6-6 6", function () { goTo(-1); }));
977
- bar.appendChild(button(L.next, "m6 9 6 6 6-6", function () { goTo(1); }));
1572
+ controls.appendChild(input);
1573
+ controls.appendChild(scopeBtn);
1574
+ controls.appendChild(count);
1575
+ controls.appendChild(button(L.prev, "m18 15-6-6-6 6", function () { goTo(-1); }));
1576
+ controls.appendChild(button(L.next, "m6 9 6 6 6-6", function () { goTo(1); }));
1577
+ var returnBtn = document.createElement('button');
1578
+ returnBtn.type = 'button'; returnBtn.setAttribute('data-find-all-return', '');
1579
+ returnBtn.hidden = true;
1580
+ returnBtn.addEventListener('click', returnToOrigin);
1581
+ state.returnBtn = returnBtn;
1582
+ controls.appendChild(returnBtn);
1583
+ controls.appendChild(button(L.close, "M18 6 6 18M6 6l12 12", close));
1584
+ bar.appendChild(controls);
978
1585
  bar.appendChild(status);
979
- bar.appendChild(button(L.close, "M18 6 6 18M6 6l12 12", close));
980
1586
 
981
1587
  state.bar = bar;
982
1588
  state.input = input;
@@ -985,13 +1591,24 @@ window.__ModuleLoader__.load({
985
1591
  state.scopeBtn = scopeBtn;
986
1592
  updateScopeButton();
987
1593
  document.body.appendChild(bar);
1594
+ var announcement = document.createElement('span');
1595
+ announcement.setAttribute('role', 'status'); announcement.setAttribute('aria-live', 'polite');
1596
+ announcement.setAttribute('aria-atomic', 'true');
1597
+ announcement.style.cssText = 'position:fixed;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)';
1598
+ state.announcement = announcement;
1599
+ document.body.appendChild(announcement);
988
1600
  }
989
1601
 
990
1602
  function open(target) {
991
1603
  if (typeof document === "undefined" || document.body === null) return;
992
- syncTarget(target || document.activeElement, false, true);
1604
+ var wasOpen = isOpen();
1605
+ syncTarget(target || document.activeElement, false);
1606
+ if (!wasOpen) state.origin = captureReadingOrigin();
993
1607
  if (!state.bar) buildBar();
994
1608
  state.bar.style.display = "flex";
1609
+ startBarPositioning();
1610
+ updateReturnButton();
1611
+ if (state.announcement) state.announcement.textContent = "";
995
1612
  var selection = "";
996
1613
  try { selection = String(window.getSelection()).trim(); } catch (error) {}
997
1614
  if (state.target && selection && selection.length <= 200 && selection !== state.query) {
@@ -1001,15 +1618,20 @@ window.__ModuleLoader__.load({
1001
1618
  state.input.focus();
1002
1619
  state.input.select();
1003
1620
  runSearch(false);
1621
+ if (!state.query && state.target && state.adapter && state.adapter.valid(state.target) && !state.origin) {
1622
+ setStatus(L.originUnavailable, false);
1623
+ }
1004
1624
  startObserver();
1005
1625
  watchSession();
1006
1626
  if (state.scope === "whole" && state.query) schedulePageIn();
1007
1627
  }
1008
1628
 
1009
1629
  function close() {
1630
+ discardOrigin();
1010
1631
  cancelPageIn();
1011
1632
  stopWatchingSession();
1012
- if (state.bar) state.bar.style.display = "none";
1633
+ stopBarPositioning();
1634
+ if (state.bar) { state.bar.style.display = "none"; state.bar.removeAttribute("data-positioned"); }
1013
1635
  state.ranges = [];
1014
1636
  state.index = -1;
1015
1637
  state.selection = null;
@@ -1029,17 +1651,18 @@ window.__ModuleLoader__.load({
1029
1651
  var SearchIcon = ui.IconSearchOutlineRegular || ui.IconSearchOutline16;
1030
1652
  state.ctx = ctx;
1031
1653
  state.adapter = createHostAdapter(ctx, function () {
1032
- syncTarget(null, true);
1654
+ if (isOpen()) syncTarget(null, true);
1033
1655
  });
1034
1656
  function SessionFind(props) {
1035
1657
  var anchor = react.useRef(null);
1658
+ var nodes = typeof props.useChat === 'function' ? props.useChat(function (snapshot) { return snapshot.nodes; }) : null;
1036
1659
  react.useEffect(function () {
1037
1660
  if (!anchor.current || !props.sessionId) return;
1038
1661
  // Older supported Button exports do not forward refs. Always
1039
1662
  // register the real button rather than the sizing-neutral wrapper.
1040
1663
  var button = anchor.current.querySelector("button[data-find-all-session]");
1041
- if (button) return state.adapter.mount(button, props.sessionId);
1042
- }, [props.sessionId]);
1664
+ if (button) return state.adapter.mount(button, props.sessionId, nodes);
1665
+ }, [props.sessionId, nodes]);
1043
1666
  return react.createElement("span", { ref: anchor, style: { display: "inline-flex", flex: "none" } },
1044
1667
  react.createElement(ui.Button, {
1045
1668
  type: "button", size: "sm", variant: "ghost", title: L.find, "aria-label": L.find,
@@ -1055,21 +1678,50 @@ window.__ModuleLoader__.load({
1055
1678
  injectCss();
1056
1679
  var handler = createHandlers({
1057
1680
  open: open, close: close, goTo: goTo, isOpen: isOpen,
1681
+ ownsKeys: function (event) {
1682
+ var path = typeof event.composedPath === 'function' ? event.composedPath() : [event.target];
1683
+ return !path.some(function (node) {
1684
+ return node && node.closest && node.closest('dialog[open], [aria-modal="true"], .monaco-editor, .cm-editor, .CodeMirror');
1685
+ });
1686
+ },
1058
1687
  isBarInput: function (target) { return target === state.input; }
1059
1688
  });
1060
1689
  function focus(event) {
1061
1690
  if (!state.bar || !state.bar.contains(event.target)) syncTarget(event.target);
1062
1691
  }
1692
+ function readingFocus(event) {
1693
+ // Reader activity may refresh the registered main frame, but cannot
1694
+ // select an embedded sidebar or change the search scope.
1695
+ if (event.isTrusted && event.target && event.target.closest && event.target.closest('[data-conversation-scroll]')) focus(event);
1696
+ }
1697
+ function interruptReading(event) {
1698
+ if (!state.restoring || !state.target) return;
1699
+ var viewport = readingViewport(state.target);
1700
+ if (!viewport || !viewport.scroll.contains(event.target)) return;
1701
+ if (event.type === 'keydown' && !/^(ArrowUp|ArrowDown|PageUp|PageDown|Home|End| )$/.test(event.key)) return;
1702
+ if (event.target.closest('input, textarea, [contenteditable="true"]')) return;
1703
+ cancelReturn(L.returnCancelled);
1704
+ }
1705
+ ['wheel', 'touchstart', 'pointerdown', 'keydown'].forEach(function (type) { window.addEventListener(type, interruptReading, true); });
1063
1706
  window.addEventListener("keydown", handler, true);
1064
1707
  window.addEventListener("focusin", focus, true);
1065
1708
  window.addEventListener("pointerdown", focus, true);
1709
+ window.addEventListener("wheel", readingFocus, true);
1710
+ window.addEventListener("resize", scheduleBarPosition);
1711
+ window.addEventListener("scroll", scheduleBarPosition, true);
1066
1712
  return function () {
1713
+ ['wheel', 'touchstart', 'pointerdown', 'keydown'].forEach(function (type) { window.removeEventListener(type, interruptReading, true); });
1067
1714
  window.removeEventListener("keydown", handler, true);
1068
1715
  window.removeEventListener("focusin", focus, true);
1069
1716
  window.removeEventListener("pointerdown", focus, true);
1717
+ window.removeEventListener("wheel", readingFocus, true);
1718
+ window.removeEventListener("resize", scheduleBarPosition);
1719
+ window.removeEventListener("scroll", scheduleBarPosition, true);
1070
1720
  close();
1071
1721
  state.query = "";
1072
1722
  if (state.bar) state.bar.remove();
1723
+ if (state.announcement) state.announcement.remove();
1724
+ state.announcement = state.returnBtn = null;
1073
1725
  var style = document.querySelector('style[data-plugin-css="dsh-find-all/bar.css"]');
1074
1726
  if (style) style.remove();
1075
1727
  state.bar = state.input = state.count = state.status = state.scopeBtn = null;