clankerbend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3536 @@
1
+ (function installCodexVimNavigator() {
2
+ const VIM_NAV_APP_ID = "onewill.vim-nav";
3
+ const BRIDGE_VERSION = 112;
4
+ const RUNTIME_KEY = "__oneWhackRuntime";
5
+ const STYLE_ID = "codex-vim-nav-style";
6
+ const ANNOTATION_CLASS = "codex-vim-nav-annotation";
7
+ const ANCHOR_CLASS = "codex-vim-nav-anchor";
8
+ const CURRENT_CLASS = "codex-vim-nav-current";
9
+ const HIGHLIGHT_CLASS = "codex-vim-nav-highlight";
10
+ const STATUS_CHROME_CLASS = "codex-vim-nav-status-chrome";
11
+ const MODE_BADGE_ID = "codex-vim-nav-mode-badge";
12
+ const HOST_UI_CLASS = "onewhack-host-ui";
13
+ const SELECTION_MENU_ID = "onewhack-selection-menu";
14
+ const OVERLAY_ID = "onewhack-anchored-overlay";
15
+ const COMPOSER_CHIPS_ID = "onewhack-composer-chips";
16
+ const SELECTOR = [
17
+ "[data-content-search-unit-key]",
18
+ "[data-turn-key]",
19
+ "[data-content-search-turn-key]",
20
+ "[data-thread-user-message-navigation-item-id]"
21
+ ].join(",");
22
+
23
+ const existingBridge = oneWhackRuntime().getBridge(VIM_NAV_APP_ID);
24
+ if (existingBridge?.name === "vim-nav" && existingBridge?.version === BRIDGE_VERSION) {
25
+ return existingBridge.snapshot();
26
+ }
27
+ if (typeof window.__codexVimNavCleanup === "function") {
28
+ window.__codexVimNavCleanup();
29
+ }
30
+
31
+ const state = {
32
+ version: BRIDGE_VERSION,
33
+ name: "vim-nav",
34
+ currentAnchorId: null,
35
+ currentIndexValue: 0,
36
+ vimMode: false,
37
+ helpOpen: false,
38
+ badgeHidden: readBadgeHidden(),
39
+ ctrlDown: false,
40
+ altDown: false,
41
+ ctrlAltToggleDown: false,
42
+ pendingKeys: "",
43
+ countPrefix: "",
44
+ anchorOrder: [],
45
+ anchorIdByIndex: Object.create(null),
46
+ anchorOrderFrozen: false,
47
+ transcriptOrderSource: null,
48
+ indexingPromise: null,
49
+ indexingStartedAt: 0,
50
+ indexingGeneration: 0,
51
+ panelOpenPromise: null,
52
+ lastPanelResult: null,
53
+ selection: null,
54
+ keyEventCount: 0,
55
+ lastKey: null,
56
+ lastDigit: null,
57
+ lastCommandResult: null,
58
+ lastRelativeDebug: null,
59
+ lastMountSearch: null,
60
+ lastBottomJump: null,
61
+ lastOrderFallback: null,
62
+ commandChain: Promise.resolve(),
63
+ hostState: null,
64
+ hostEvents: [],
65
+ activeTextSelection: null,
66
+ lastSelectionMenuAt: 0,
67
+ renderedOverlaySignature: null,
68
+ pendingComposerSubmission: null,
69
+ submittedComposerSubmissions: []
70
+ };
71
+ let lastScrollContainer = null;
72
+
73
+ function ensureStyle() {
74
+ let style = document.getElementById(STYLE_ID);
75
+ if (!style) {
76
+ style = document.createElement("style");
77
+ style.id = STYLE_ID;
78
+ document.documentElement.appendChild(style);
79
+ }
80
+ style.textContent = `
81
+ .${ANCHOR_CLASS} {
82
+ position: relative !important;
83
+ }
84
+ .${ANNOTATION_CLASS} {
85
+ display: inline-grid !important;
86
+ place-items: center !important;
87
+ min-width: 28px !important;
88
+ height: 22px !important;
89
+ border: 1px solid rgba(143, 199, 212, .48) !important;
90
+ border-radius: 999px !important;
91
+ background: rgba(10, 16, 20, .86) !important;
92
+ color: #cfe6ec !important;
93
+ font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace !important;
94
+ letter-spacing: 0 !important;
95
+ user-select: none !important;
96
+ cursor: pointer !important;
97
+ }
98
+ .${ANNOTATION_CLASS}:hover {
99
+ border-color: rgba(143, 199, 212, .9) !important;
100
+ background: rgba(18, 34, 41, .96) !important;
101
+ }
102
+ .${CURRENT_CLASS} .${ANNOTATION_CLASS} {
103
+ border-color: #8fc7d4 !important;
104
+ box-shadow: 0 0 0 3px rgba(143, 199, 212, .22) !important;
105
+ color: #ffffff !important;
106
+ }
107
+ .${HIGHLIGHT_CLASS} {
108
+ outline: 3px solid #8fc7d4 !important;
109
+ outline-offset: 3px !important;
110
+ border-radius: 8px !important;
111
+ box-shadow: 0 0 0 7px rgba(143, 199, 212, .18) !important;
112
+ }
113
+ .${STATUS_CHROME_CLASS} {
114
+ display: none !important;
115
+ }
116
+ .${HOST_UI_CLASS} {
117
+ font: 13px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
118
+ letter-spacing: 0 !important;
119
+ color: #241f12 !important;
120
+ }
121
+ #${OVERLAY_ID} button,
122
+ #${COMPOSER_CHIPS_ID} button {
123
+ appearance: none !important;
124
+ border: 0 !important;
125
+ border-radius: 6px !important;
126
+ background: transparent !important;
127
+ color: inherit !important;
128
+ cursor: pointer !important;
129
+ font: 650 12px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
130
+ padding: 7px 8px !important;
131
+ }
132
+ #${OVERLAY_ID} {
133
+ position: fixed !important;
134
+ z-index: 2147483004 !important;
135
+ display: none !important;
136
+ width: min(340px, calc(100vw - 32px)) !important;
137
+ border: 1px solid rgba(205, 176, 64, .72) !important;
138
+ border-radius: 8px !important;
139
+ background: #ffd84f !important;
140
+ box-shadow: 0 16px 36px rgba(0, 0, 0, .22) !important;
141
+ padding: 12px !important;
142
+ }
143
+ #${OVERLAY_ID}.is-visible {
144
+ display: grid !important;
145
+ gap: 9px !important;
146
+ }
147
+ #${OVERLAY_ID} strong {
148
+ font: 700 13px/1.25 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
149
+ }
150
+ #${OVERLAY_ID} textarea,
151
+ #${OVERLAY_ID} input {
152
+ width: 100% !important;
153
+ min-height: 92px !important;
154
+ resize: vertical !important;
155
+ border: 1px solid rgba(150, 120, 24, .34) !important;
156
+ border-radius: 6px !important;
157
+ background: #fff3bd !important;
158
+ color: #241f12 !important;
159
+ font: 13px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
160
+ padding: 9px !important;
161
+ }
162
+ #${OVERLAY_ID} .onewhack-overlay-actions {
163
+ display: flex !important;
164
+ justify-content: flex-end !important;
165
+ gap: 6px !important;
166
+ }
167
+ #${OVERLAY_ID} button {
168
+ background: rgba(80, 65, 20, .78) !important;
169
+ color: #fff7d7 !important;
170
+ }
171
+ #${COMPOSER_CHIPS_ID} {
172
+ position: absolute !important;
173
+ z-index: 2147483002 !important;
174
+ display: none !important;
175
+ flex-wrap: wrap !important;
176
+ gap: 7px !important;
177
+ max-width: calc(100vw - 24px) !important;
178
+ pointer-events: none !important;
179
+ }
180
+ #${COMPOSER_CHIPS_ID}.is-visible {
181
+ display: flex !important;
182
+ }
183
+ #${COMPOSER_CHIPS_ID} .onewhack-context-chip {
184
+ display: inline-flex !important;
185
+ align-items: center !important;
186
+ gap: 5px !important;
187
+ max-width: min(280px, calc(100vw - 84px)) !important;
188
+ border: 1px solid rgba(217, 185, 68, .72) !important;
189
+ border-radius: 999px !important;
190
+ background: #ffef9a !important;
191
+ box-shadow: 0 4px 12px rgba(0, 0, 0, .12) !important;
192
+ color: #3a2f0c !important;
193
+ font: 600 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
194
+ padding: 5px 8px !important;
195
+ pointer-events: auto !important;
196
+ }
197
+ .onewhack-submitted-context-row .onewhack-context-chip {
198
+ display: inline-flex !important;
199
+ align-items: center !important;
200
+ gap: 5px !important;
201
+ max-width: min(280px, calc(100vw - 84px)) !important;
202
+ border: 1px solid rgba(217, 185, 68, .72) !important;
203
+ border-radius: 999px !important;
204
+ background: #ffef9a !important;
205
+ color: #3a2f0c !important;
206
+ font: 600 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
207
+ padding: 5px 8px !important;
208
+ }
209
+ .onewhack-submitted-context-row .onewhack-context-chip::before {
210
+ content: "□" !important;
211
+ color: #8a7327 !important;
212
+ font-size: 10px !important;
213
+ line-height: 1 !important;
214
+ }
215
+ #${COMPOSER_CHIPS_ID} .onewhack-context-chip::before {
216
+ content: "□" !important;
217
+ color: #8a7327 !important;
218
+ font-size: 10px !important;
219
+ line-height: 1 !important;
220
+ }
221
+ #${COMPOSER_CHIPS_ID} .onewhack-context-chip span {
222
+ overflow: hidden !important;
223
+ text-overflow: ellipsis !important;
224
+ white-space: nowrap !important;
225
+ }
226
+ #${COMPOSER_CHIPS_ID} .onewhack-context-chip button {
227
+ padding: 0 2px !important;
228
+ color: #725f20 !important;
229
+ }
230
+ html,
231
+ body,
232
+ main,
233
+ [role="main"],
234
+ [data-testid="thread-scroll-container"],
235
+ [data-thread-scroll-container],
236
+ [data-scroll-container] {
237
+ scroll-behavior: auto !important;
238
+ }
239
+ #${MODE_BADGE_ID} {
240
+ position: fixed !important;
241
+ right: 18px !important;
242
+ bottom: 18px !important;
243
+ z-index: 2147483002 !important;
244
+ display: none !important;
245
+ flex-direction: column !important;
246
+ align-items: flex-start !important;
247
+ gap: 5px !important;
248
+ max-width: min(360px, calc(100vw - 36px)) !important;
249
+ border: 1px solid rgba(143, 199, 212, .55) !important;
250
+ border-radius: 8px !important;
251
+ background: rgba(9, 14, 18, .94) !important;
252
+ color: #e8f6fa !important;
253
+ box-shadow: 0 12px 30px rgba(0, 0, 0, .36) !important;
254
+ padding: 8px 10px !important;
255
+ font: 600 12px/1.25 ui-monospace, SFMono-Regular, Menlo, monospace !important;
256
+ letter-spacing: 0 !important;
257
+ user-select: none !important;
258
+ }
259
+ #${MODE_BADGE_ID}.is-visible {
260
+ display: flex !important;
261
+ }
262
+ #${MODE_BADGE_ID}.is-active {
263
+ border-color: rgba(143, 199, 212, .82) !important;
264
+ background: rgba(9, 14, 18, .97) !important;
265
+ }
266
+ #${MODE_BADGE_ID}:not(.is-active) {
267
+ opacity: .76 !important;
268
+ }
269
+ #${MODE_BADGE_ID} strong {
270
+ color: #8fc7d4 !important;
271
+ }
272
+ #${MODE_BADGE_ID} .codex-vim-nav-title-row {
273
+ display: flex !important;
274
+ align-items: baseline !important;
275
+ gap: 7px !important;
276
+ white-space: nowrap !important;
277
+ }
278
+ #${MODE_BADGE_ID} .codex-vim-nav-dismiss {
279
+ margin-left: auto !important;
280
+ border: 0 !important;
281
+ border-radius: 4px !important;
282
+ background: transparent !important;
283
+ color: #7f9299 !important;
284
+ padding: 0 3px !important;
285
+ font: 700 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace !important;
286
+ cursor: pointer !important;
287
+ }
288
+ #${MODE_BADGE_ID} .codex-vim-nav-dismiss:hover {
289
+ color: #e8f6fa !important;
290
+ background: rgba(143, 199, 212, .12) !important;
291
+ }
292
+ #${MODE_BADGE_ID} .codex-vim-nav-state {
293
+ color: #e8f6fa !important;
294
+ }
295
+ #${MODE_BADGE_ID} .codex-vim-nav-help-toggle {
296
+ border: 1px solid rgba(143, 199, 212, .34) !important;
297
+ border-radius: 999px !important;
298
+ background: rgba(143, 199, 212, .08) !important;
299
+ color: #cfe6ec !important;
300
+ padding: 0 5px !important;
301
+ font: 700 11px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace !important;
302
+ cursor: pointer !important;
303
+ }
304
+ #${MODE_BADGE_ID} .codex-vim-nav-help-toggle:hover {
305
+ border-color: rgba(143, 199, 212, .75) !important;
306
+ background: rgba(143, 199, 212, .16) !important;
307
+ }
308
+ #${MODE_BADGE_ID} span {
309
+ color: #aab8be !important;
310
+ font-weight: 500 !important;
311
+ }
312
+ #${MODE_BADGE_ID} .codex-vim-nav-title-row span {
313
+ font-size: 11px !important;
314
+ }
315
+ #${MODE_BADGE_ID} .codex-vim-nav-help-row {
316
+ display: block !important;
317
+ white-space: nowrap !important;
318
+ }
319
+ #${MODE_BADGE_ID} .codex-vim-nav-help-menu {
320
+ display: grid !important;
321
+ grid-template-columns: auto 1fr !important;
322
+ column-gap: 8px !important;
323
+ row-gap: 3px !important;
324
+ border-top: 1px solid rgba(143, 199, 212, .22) !important;
325
+ margin-top: 3px !important;
326
+ padding-top: 6px !important;
327
+ }
328
+ #${MODE_BADGE_ID} .codex-vim-nav-help-menu kbd {
329
+ color: #e8f6fa !important;
330
+ font: 700 11px/1.25 ui-monospace, SFMono-Regular, Menlo, monospace !important;
331
+ }
332
+ #${MODE_BADGE_ID} .codex-vim-nav-help-menu span {
333
+ white-space: nowrap !important;
334
+ }
335
+ #${MODE_BADGE_ID} .codex-vim-nav-key-buffer {
336
+ display: flex !important;
337
+ align-items: center !important;
338
+ gap: 6px !important;
339
+ }
340
+ #${MODE_BADGE_ID} code {
341
+ border: 1px solid rgba(143, 199, 212, .34) !important;
342
+ border-radius: 5px !important;
343
+ background: rgba(143, 199, 212, .12) !important;
344
+ color: #e8f6fa !important;
345
+ padding: 2px 5px !important;
346
+ font: 700 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace !important;
347
+ }
348
+ @media (max-width: 900px) {
349
+ .${ANNOTATION_CLASS} {
350
+ display: inline-grid !important;
351
+ }
352
+ }
353
+ `;
354
+ }
355
+
356
+ function ensureModeBadge() {
357
+ ensureStyle();
358
+ let badge = document.getElementById(MODE_BADGE_ID);
359
+ if (!badge) {
360
+ badge = document.createElement("div");
361
+ badge.id = MODE_BADGE_ID;
362
+ document.documentElement.appendChild(badge);
363
+ }
364
+ renderModeBadge(badge);
365
+ badge.classList.toggle("is-visible", shouldShowModeBadge());
366
+ badge.classList.toggle("is-active", state.vimMode);
367
+ badge.classList.toggle("is-help-open", state.helpOpen);
368
+ }
369
+
370
+ function currentKeyBufferLabel() {
371
+ if (state.countPrefix) return state.countPrefix;
372
+ if (state.pendingKeys) return state.pendingKeys;
373
+ return "";
374
+ }
375
+
376
+ function renderModeBadge(badge = document.getElementById(MODE_BADGE_ID)) {
377
+ if (!badge) return;
378
+ const buffer = currentKeyBufferLabel();
379
+ badge.replaceChildren();
380
+ const titleRow = document.createElement("div");
381
+ titleRow.className = "codex-vim-nav-title-row";
382
+ const title = document.createElement("strong");
383
+ title.textContent = "OneWill VimNav";
384
+ const mode = document.createElement("span");
385
+ mode.className = "codex-vim-nav-state";
386
+ mode.textContent = `[${state.vimMode ? "ON" : "OFF"}]`;
387
+ const dismiss = document.createElement("button");
388
+ dismiss.type = "button";
389
+ dismiss.className = "codex-vim-nav-dismiss";
390
+ dismiss.textContent = "x";
391
+ dismiss.title = "Hide VimNav badge until Ctrl-Alt";
392
+ dismiss.addEventListener("click", (event) => {
393
+ event.preventDefault();
394
+ event.stopPropagation();
395
+ hideModeBadge();
396
+ });
397
+ titleRow.append(title, mode, dismiss);
398
+ badge.appendChild(titleRow);
399
+ const toggleRow = document.createElement("span");
400
+ toggleRow.className = "codex-vim-nav-help-row";
401
+ const helpButton = document.createElement("button");
402
+ helpButton.type = "button";
403
+ helpButton.className = "codex-vim-nav-help-toggle";
404
+ helpButton.textContent = "?";
405
+ helpButton.title = "Toggle VimNav help";
406
+ helpButton.addEventListener("click", (event) => {
407
+ event.preventDefault();
408
+ event.stopPropagation();
409
+ toggleHelp();
410
+ });
411
+ toggleRow.append("(Ctrl-Alt toggles, ", helpButton, " for help)");
412
+ badge.appendChild(toggleRow);
413
+ if (buffer) {
414
+ const row = document.createElement("div");
415
+ row.className = "codex-vim-nav-key-buffer";
416
+ const keyState = document.createElement("span");
417
+ keyState.textContent = "keys";
418
+ const code = document.createElement("code");
419
+ code.textContent = buffer;
420
+ row.append(keyState, code);
421
+ badge.appendChild(row);
422
+ }
423
+ if (state.helpOpen) {
424
+ const help = document.createElement("div");
425
+ help.className = "codex-vim-nav-help-menu";
426
+ for (const [keys, description] of [
427
+ ["j / k", "previous or next transcript item"],
428
+ ["gg / G", "first or last transcript item"],
429
+ ["67G", "jump to transcript item 67"],
430
+ ["{ / }", "previous or next user message"],
431
+ ["[ / ]", "align current item top or bottom"],
432
+ ["Backspace", "edit buffered keys"],
433
+ ["i / Esc", "exit VimNav"]
434
+ ]) {
435
+ const keyEl = document.createElement("kbd");
436
+ keyEl.textContent = keys;
437
+ const descEl = document.createElement("span");
438
+ descEl.textContent = description;
439
+ help.append(keyEl, descEl);
440
+ }
441
+ badge.appendChild(help);
442
+ }
443
+ }
444
+
445
+ function toggleHelp() {
446
+ state.badgeHidden = false;
447
+ writeBadgeHidden();
448
+ state.helpOpen = !state.helpOpen;
449
+ ensureModeBadge();
450
+ }
451
+
452
+ function shouldShowModeBadge() {
453
+ if (state.vimMode || state.helpOpen || currentKeyBufferLabel()) return true;
454
+ if (state.badgeHidden) return false;
455
+ return anchorElements().length > 0;
456
+ }
457
+
458
+ function hideModeBadge() {
459
+ state.vimMode = false;
460
+ state.helpOpen = false;
461
+ state.pendingKeys = "";
462
+ state.countPrefix = "";
463
+ state.badgeHidden = true;
464
+ writeBadgeHidden();
465
+ ensureModeBadge();
466
+ }
467
+
468
+ function setVimMode(enabled) {
469
+ state.vimMode = Boolean(enabled);
470
+ state.pendingKeys = "";
471
+ state.countPrefix = "";
472
+ if (state.vimMode) {
473
+ state.badgeHidden = false;
474
+ writeBadgeHidden();
475
+ }
476
+ if (state.vimMode) {
477
+ setCurrentToBottomVisibleAnchor();
478
+ focusTranscriptForVimMode();
479
+ requestAnimationFrame(() => focusTranscriptForVimMode());
480
+ setTimeout(() => focusTranscriptForVimMode(), 120);
481
+ primeAnchorOrder().finally(() => {
482
+ ensureAnnotations();
483
+ scheduleAnnotationRefresh();
484
+ });
485
+ }
486
+ ensureModeBadge();
487
+ }
488
+
489
+ function setCurrentToBottomVisibleAnchor() {
490
+ const mounted = mountedAnchorsByViewport();
491
+ const visible = mounted
492
+ .filter((item) => item.viewportBottom >= topVisibleInset() && item.viewportTop <= viewportSafeBottom())
493
+ .sort((a, b) => b.viewportBottom - a.viewportBottom);
494
+ const target = visible[0] ||
495
+ mounted
496
+ .map((item) => ({ ...item, distance: Math.abs(item.viewportBottom - viewportSafeBottom()) }))
497
+ .sort((a, b) => a.distance - b.distance)[0];
498
+ if (target) setCurrent(target.anchorId, "adapter", target.order - 1);
499
+ }
500
+
501
+ function focusTranscriptForVimMode() {
502
+ if (!state.vimMode) return false;
503
+ const active = document.activeElement;
504
+ if (active instanceof HTMLElement) active.blur?.();
505
+ const current = state.currentAnchorId ? findAnchor(state.currentAnchorId) : null;
506
+ const visible = viewportVisibleAnchors().sort((a, b) => b.viewportBottom - a.viewportBottom)[0];
507
+ const target = current || (visible?.anchorId ? findAnchor(visible.anchorId) : null) || findScrollContainer() || document.body;
508
+ if (!(target instanceof HTMLElement)) return false;
509
+ if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1");
510
+ target.focus({ preventScroll: true });
511
+ state.lastFocusTarget = {
512
+ tag: target.tagName,
513
+ anchorId: anchorId(target) || target.querySelector?.(SELECTOR) && anchorId(target.querySelector(SELECTOR)) || null,
514
+ at: Date.now()
515
+ };
516
+ return document.activeElement === target;
517
+ }
518
+
519
+ function readBadgeHidden() {
520
+ try {
521
+ return localStorage.getItem("codex-vim-nav:badge-hidden") === "true";
522
+ } catch {
523
+ return false;
524
+ }
525
+ }
526
+
527
+ function writeBadgeHidden() {
528
+ try {
529
+ if (state.badgeHidden) localStorage.setItem("codex-vim-nav:badge-hidden", "true");
530
+ else localStorage.removeItem("codex-vim-nav:badge-hidden");
531
+ } catch {
532
+ // Ignore storage failures in injected renderer contexts.
533
+ }
534
+ }
535
+
536
+ function cssEscape(value) {
537
+ if (window.CSS?.escape) return window.CSS.escape(String(value));
538
+ return String(value).replace(/["\\]/g, "\\$&");
539
+ }
540
+
541
+ function anchorId(el) {
542
+ return el?.dataset?.contentSearchUnitKey ||
543
+ el?.dataset?.turnKey ||
544
+ el?.dataset?.contentSearchTurnKey ||
545
+ el?.dataset?.threadUserMessageNavigationItemId ||
546
+ null;
547
+ }
548
+
549
+ function anchorKind(el) {
550
+ if (el?.dataset?.contentSearchUnitKey) return "content-search-unit";
551
+ if (el?.dataset?.turnKey) return "turn";
552
+ if (el?.dataset?.contentSearchTurnKey) return "content-search-turn";
553
+ if (el?.dataset?.threadUserMessageNavigationItemId) return "navigation-item";
554
+ return "unknown";
555
+ }
556
+
557
+ function roleForAnchor(el) {
558
+ const id = anchorId(el) || "";
559
+ const text = previewText(el).toLowerCase();
560
+ if (id.includes(":user")) return "user";
561
+ if (id.includes(":assistant")) return "assistant";
562
+ if (/approval|tool|command/.test(text)) return "tool";
563
+ return undefined;
564
+ }
565
+
566
+ function previewText(el) {
567
+ return (el?.innerText || el?.textContent || "").replace(/\s+/g, " ").trim().slice(0, 220);
568
+ }
569
+
570
+ function isContentUnitId(id) {
571
+ return /:(user|assistant)$/.test(String(id || ""));
572
+ }
573
+
574
+ function contentUnitSortKey(id) {
575
+ const match = String(id || "").match(/^(.*):(\d+):(user|assistant)$/);
576
+ if (!match) return null;
577
+ const unitIndex = Number(match[2]);
578
+ if (!Number.isSafeInteger(unitIndex) || unitIndex < 0) return null;
579
+ return {
580
+ turnSearchKey: match[1],
581
+ unitIndex,
582
+ roleRank: match[3] === "user" ? 0 : 1
583
+ };
584
+ }
585
+
586
+ function compareAnchorIds(a, b) {
587
+ const aKey = contentUnitSortKey(a);
588
+ const bKey = contentUnitSortKey(b);
589
+ if (aKey && bKey) {
590
+ return aKey.turnSearchKey.localeCompare(bKey.turnSearchKey) ||
591
+ aKey.unitIndex - bKey.unitIndex ||
592
+ aKey.roleRank - bKey.roleRank ||
593
+ String(a).localeCompare(String(b));
594
+ }
595
+ if (aKey) return -1;
596
+ if (bKey) return 1;
597
+ return 0;
598
+ }
599
+
600
+ function isStatusAnchor(el) {
601
+ const text = previewText(el);
602
+ if (/^worked for\b/i.test(text) && text.length < 80) return true;
603
+ if (/^(compact(ed|ion)?|rollback point|context summary)\b/i.test(text) && text.length < 140) return true;
604
+ return false;
605
+ }
606
+
607
+ function hasUserContentUnit(el) {
608
+ return Boolean(el.querySelector?.("[data-content-search-unit-key$=':0:user']"));
609
+ }
610
+
611
+ function hasContentUnit(el) {
612
+ return Boolean(el.querySelector?.("[data-content-search-unit-key$=':user'],[data-content-search-unit-key$=':assistant']"));
613
+ }
614
+
615
+ function hideStatusChrome() {
616
+ for (const el of document.querySelectorAll(`.${STATUS_CHROME_CLASS}`)) {
617
+ if (!isStatusAnchor(el) || hasContentUnit(el)) el.classList.remove(STATUS_CHROME_CLASS);
618
+ }
619
+ const candidates = Array.from(document.querySelectorAll("button,[role='button'],div,span"))
620
+ .filter((el) => el instanceof HTMLElement && isStatusAnchor(el) && !hasContentUnit(el));
621
+ for (const el of candidates) {
622
+ const container = el.closest("button,[role='button']") || el;
623
+ if (container instanceof HTMLElement && !hasContentUnit(container)) {
624
+ container.classList.add(STATUS_CHROME_CLASS);
625
+ }
626
+ }
627
+ }
628
+
629
+ function anchorElements() {
630
+ const seen = new Set();
631
+ const all = Array.from(document.querySelectorAll(SELECTOR))
632
+ .filter((el) => el instanceof HTMLElement);
633
+ const turnAnchors = all.filter((el) =>
634
+ (el.dataset.turnKey || el.dataset.contentSearchTurnKey || el.dataset.threadUserMessageNavigationItemId) &&
635
+ hasUserContentUnit(el) &&
636
+ !isStatusAnchor(el)
637
+ );
638
+ const contentUnitAnchors = all.filter((el) =>
639
+ el.dataset.contentSearchUnitKey &&
640
+ isContentUnitId(el.dataset.contentSearchUnitKey) &&
641
+ !isStatusAnchor(el)
642
+ );
643
+ const source = contentUnitAnchors.length ? contentUnitAnchors : turnAnchors.length ? turnAnchors : all;
644
+ const candidates = all
645
+ .filter((el) => source.includes(el))
646
+ .filter((el) => {
647
+ const id = anchorId(el);
648
+ if (!id || seen.has(id)) return false;
649
+ seen.add(id);
650
+ return true;
651
+ });
652
+ if (!contentUnitAnchors.length && turnAnchors.length) {
653
+ return candidates.filter((el) =>
654
+ !candidates.some((other) => other !== el && other.contains(el))
655
+ );
656
+ }
657
+ return candidates.filter((el) =>
658
+ !candidates.some((other) => other !== el && el.contains(other))
659
+ );
660
+ }
661
+
662
+ function rememberAnchorOrder(elements) {
663
+ const observedIds = elements.map(anchorId).filter(Boolean);
664
+ if (state.transcriptOrderSource === "app-server") {
665
+ state.anchorOrderFrozen = true;
666
+ return;
667
+ }
668
+ const merged = mergeAnchorOrder(observedIds);
669
+ if (merged.usedSortKey) {
670
+ state.anchorOrderFrozen = true;
671
+ return;
672
+ }
673
+ state.anchorOrder = state.anchorOrder.filter((id, index, list) =>
674
+ id && list.indexOf(id) === index
675
+ );
676
+ observedIds.forEach((id, observedIndex) => {
677
+ if (state.anchorOrder.includes(id)) return;
678
+ if (state.anchorOrderFrozen) {
679
+ mergeAnchorOrder([id]);
680
+ return;
681
+ }
682
+ for (let i = observedIndex + 1; i < observedIds.length; i += 1) {
683
+ const nextKnownIndex = state.anchorOrder.indexOf(observedIds[i]);
684
+ if (nextKnownIndex >= 0) {
685
+ state.anchorOrder.splice(nextKnownIndex, 0, id);
686
+ return;
687
+ }
688
+ }
689
+ for (let i = observedIndex - 1; i >= 0; i -= 1) {
690
+ const previousKnownIndex = state.anchorOrder.indexOf(observedIds[i]);
691
+ if (previousKnownIndex >= 0) {
692
+ state.anchorOrder.splice(previousKnownIndex + 1, 0, id);
693
+ return;
694
+ }
695
+ }
696
+ state.anchorOrder.push(id);
697
+ });
698
+ }
699
+
700
+ function mergeAnchorOrder(ids) {
701
+ const dedupedKnownIds = [...state.anchorOrder, ...ids]
702
+ .filter(Boolean)
703
+ .filter((id, index, list) => list.indexOf(id) === index);
704
+ const sortableIds = dedupedKnownIds.filter((id) => contentUnitSortKey(id));
705
+ const fallbackIds = dedupedKnownIds.filter((id) => !contentUnitSortKey(id));
706
+ if (sortableIds.length) {
707
+ state.anchorOrder = [...sortableIds.sort(compareAnchorIds), ...fallbackIds];
708
+ } else {
709
+ state.anchorOrder = dedupedKnownIds;
710
+ }
711
+ rebuildAnchorIndex();
712
+ return { usedSortKey: Boolean(sortableIds.length), count: state.anchorOrder.length };
713
+ }
714
+
715
+ function appendUnknownAnchors(ids) {
716
+ const seen = new Set(state.anchorOrder);
717
+ for (const id of ids) {
718
+ if (!id || seen.has(id)) continue;
719
+ state.anchorOrder.push(id);
720
+ seen.add(id);
721
+ }
722
+ rebuildAnchorIndex();
723
+ }
724
+
725
+ function mountedUnknownContentAnchorIds() {
726
+ if (state.transcriptOrderSource !== "app-server") return [];
727
+ const known = new Set(state.anchorOrder);
728
+ return collectAnchorIds().filter((id) =>
729
+ isContentUnitId(id) && !known.has(id)
730
+ );
731
+ }
732
+
733
+ function rebuildAnchorIndex() {
734
+ state.anchorIdByIndex = Object.create(null);
735
+ state.anchorOrder.forEach((id, index) => {
736
+ state.anchorIdByIndex[index] = id;
737
+ });
738
+ }
739
+
740
+ function mountedAnchorIds(elements) {
741
+ const seen = new Set();
742
+ return elements.map(anchorId).filter((id) => {
743
+ if (!id || seen.has(id)) return false;
744
+ seen.add(id);
745
+ return true;
746
+ });
747
+ }
748
+
749
+ function mountedOrderSource(ids) {
750
+ return ids.some((id) => contentUnitSortKey(id)) ? "content-search-unit-sort-key" : "mounted-dom-order";
751
+ }
752
+
753
+ function replaceStaleFrozenOrder(elements) {
754
+ if (!state.anchorOrderFrozen || state.transcriptOrderSource !== "app-server" || !state.anchorOrder.length) return false;
755
+ const ids = mountedAnchorIds(elements);
756
+ if (!ids.length || ids.some((id) => state.anchorOrder.includes(id))) return false;
757
+ const source = mountedOrderSource(ids);
758
+ state.anchorOrder = source === "content-search-unit-sort-key" ? [...ids].sort(compareAnchorIds) : ids;
759
+ rebuildAnchorIndex();
760
+ state.anchorOrderFrozen = true;
761
+ state.transcriptOrderSource = source;
762
+ if (state.currentAnchorId && !state.anchorOrder.includes(state.currentAnchorId)) state.currentAnchorId = null;
763
+ state.lastOrderFallback = {
764
+ reason: "stale-app-server-order",
765
+ mountedCount: ids.length,
766
+ replacedCount: state.anchorOrder.length,
767
+ source,
768
+ at: Date.now()
769
+ };
770
+ return true;
771
+ }
772
+
773
+ function anchorNumber(id) {
774
+ const index = state.anchorOrder.indexOf(id);
775
+ return index >= 0 ? index + 1 : state.anchorOrder.length + 1;
776
+ }
777
+
778
+ function hasStableIndex() {
779
+ return state.anchorOrderFrozen && ["app-server", "content-search-unit-sort-key", "mounted-dom-order"].includes(state.transcriptOrderSource);
780
+ }
781
+
782
+ function markerGlyph(item) {
783
+ return hasStableIndex() ? String(item.order) : "?";
784
+ }
785
+
786
+ function findScrollContainer() {
787
+ const anchors = anchorElements();
788
+ const explicit = [
789
+ "[data-testid='thread-scroll-container']",
790
+ "[data-thread-scroll-container]",
791
+ "[data-scroll-container]",
792
+ "main [class*='overflow-y-auto']",
793
+ "main [class*='overflow-auto']"
794
+ ];
795
+ for (const selector of explicit) {
796
+ const el = document.querySelector(selector);
797
+ if (isScrollable(el)) return rememberScrollContainer(el);
798
+ }
799
+ const candidates = Array.from(document.querySelectorAll("main, main *, [role='main'], [role='main'] *, body *"))
800
+ .filter(isScrollable)
801
+ .map((el) => {
802
+ const rect = el.getBoundingClientRect();
803
+ return {
804
+ el,
805
+ count: anchors.filter((anchor) => el.contains(anchor)).length,
806
+ overflow: el.scrollHeight - el.clientHeight,
807
+ area: Math.max(0, rect.width) * Math.max(0, rect.height)
808
+ };
809
+ })
810
+ .sort((a, b) => b.count - a.count || b.overflow - a.overflow || b.area - a.area);
811
+ if (candidates[0]?.el) return rememberScrollContainer(candidates[0].el);
812
+ if (lastScrollContainer?.isConnected) return lastScrollContainer;
813
+ return document.scrollingElement || document.documentElement;
814
+ }
815
+
816
+ function rememberScrollContainer(root) {
817
+ if (root) lastScrollContainer = root;
818
+ return root;
819
+ }
820
+
821
+ function isScrollable(el) {
822
+ if (!(el instanceof HTMLElement)) return false;
823
+ if (el.scrollHeight <= el.clientHeight + 4) return false;
824
+ const style = window.getComputedStyle(el);
825
+ return /(auto|scroll|overlay)/.test(`${style.overflowY} ${style.overflow}`);
826
+ }
827
+
828
+ function selectorForAnchor(id) {
829
+ const escaped = cssEscape(id);
830
+ return [
831
+ `[data-content-search-unit-key="${escaped}"]`,
832
+ `[data-turn-key="${escaped}"]`,
833
+ `[data-content-search-turn-key="${escaped}"]`,
834
+ `[data-thread-user-message-navigation-item-id="${escaped}"]`
835
+ ].join(",");
836
+ }
837
+
838
+ function findAnchor(id) {
839
+ return document.querySelector(selectorForAnchor(id));
840
+ }
841
+
842
+ function markerForAnchor(el, id) {
843
+ return el?.querySelector?.(`.${ANNOTATION_CLASS}[data-anchor-id="${cssEscape(id)}"]`) || el;
844
+ }
845
+
846
+ function scrollTargetForAnchor(el, id, options = {}) {
847
+ return el;
848
+ }
849
+
850
+ function isDocumentScrollRoot(root) {
851
+ return root === document.scrollingElement || root === document.documentElement || root === document.body;
852
+ }
853
+
854
+ function nearestScrollContainer(el) {
855
+ for (let node = el?.parentElement; node; node = node.parentElement) {
856
+ if (isScrollable(node)) return node;
857
+ }
858
+ return findScrollContainer();
859
+ }
860
+
861
+ function isHostUiElement(el) {
862
+ for (let node = el; node; node = node.parentElement) {
863
+ if (node.classList?.contains?.(HOST_UI_CLASS)) return true;
864
+ if (node.id === SELECTION_MENU_ID || node.id === OVERLAY_ID || node.id === COMPOSER_CHIPS_ID) return true;
865
+ }
866
+ return false;
867
+ }
868
+
869
+ function scrollTopOf(root) {
870
+ return isDocumentScrollRoot(root) ? window.scrollY : root.scrollTop;
871
+ }
872
+
873
+ function withInstantScroll(root, fn) {
874
+ const nodes = [
875
+ document.documentElement,
876
+ document.body,
877
+ root instanceof HTMLElement ? root : null
878
+ ].filter(Boolean);
879
+ const previous = nodes.map((node) => [node, node.style.scrollBehavior]);
880
+ for (const node of nodes) node.style.scrollBehavior = "auto";
881
+ try {
882
+ return fn();
883
+ } finally {
884
+ for (const [node, value] of previous) node.style.scrollBehavior = value;
885
+ }
886
+ }
887
+
888
+ function scrollRootRange(root) {
889
+ if (isDocumentScrollRoot(root)) {
890
+ return {
891
+ min: 0,
892
+ max: Math.max(
893
+ 0,
894
+ Math.max(document.documentElement.scrollHeight, document.body.scrollHeight) - window.innerHeight
895
+ )
896
+ };
897
+ }
898
+ const max = Math.max(0, (root.scrollHeight || 0) - (root.clientHeight || 0));
899
+ const style = getComputedStyle(root);
900
+ if (style.flexDirection === "column-reverse") {
901
+ return { min: -max, max: 0 };
902
+ }
903
+ return { min: 0, max };
904
+ }
905
+
906
+ function scrollTopForViewportDelta(root, delta) {
907
+ const currentTop = scrollTopOf(root);
908
+ if (!isDocumentScrollRoot(root) && getComputedStyle(root).flexDirection === "column-reverse") {
909
+ return currentTop - delta;
910
+ }
911
+ return currentTop + delta;
912
+ }
913
+
914
+ function scrollRootTo(root, top) {
915
+ const range = scrollRootRange(root);
916
+ const clampedTop = Math.max(range.min, Math.min(range.max, Number.isFinite(top) ? top : 0));
917
+ withInstantScroll(root, () => {
918
+ if (isDocumentScrollRoot(root)) {
919
+ window.scrollTo(0, clampedTop);
920
+ document.documentElement.scrollTop = clampedTop;
921
+ document.body.scrollTop = clampedTop;
922
+ return;
923
+ }
924
+ root.scrollTop = clampedTop;
925
+ });
926
+ }
927
+
928
+ function scrollableAncestors(el) {
929
+ const roots = [];
930
+ for (let node = el?.parentElement; node; node = node.parentElement) {
931
+ if (isScrollable(node)) roots.push(node);
932
+ }
933
+ const found = findScrollContainer();
934
+ if (found && !roots.includes(found)) roots.push(found);
935
+ const doc = document.scrollingElement || document.documentElement;
936
+ if (doc && !roots.includes(doc)) roots.push(doc);
937
+ return roots;
938
+ }
939
+
940
+ function bottomScrollRoots() {
941
+ const roots = [];
942
+ const add = (root) => {
943
+ if (root && !roots.includes(root)) roots.push(root);
944
+ };
945
+ add(lastScrollContainer);
946
+ add(findScrollContainer());
947
+ add(nearestScrollContainer(findAnchor(state.currentAnchorId) || anchorElements()[0] || document.body));
948
+ add(document.scrollingElement || document.documentElement);
949
+ Array.from(document.querySelectorAll("main, main *, [role='main'], [role='main'] *, body *"))
950
+ .filter(isScrollable)
951
+ .map((el) => ({
952
+ el,
953
+ overflow: Math.max(0, el.scrollHeight - el.clientHeight),
954
+ area: Math.max(0, el.getBoundingClientRect().width) * Math.max(0, el.getBoundingClientRect().height)
955
+ }))
956
+ .sort((a, b) => b.overflow - a.overflow || b.area - a.area)
957
+ .slice(0, 6)
958
+ .forEach((candidate) => add(candidate.el));
959
+ return roots;
960
+ }
961
+
962
+ function topVisibleInset() {
963
+ const blockers = Array.from(document.querySelectorAll("body *"))
964
+ .map((el) => {
965
+ if (!(el instanceof HTMLElement)) return null;
966
+ if (isHostUiElement(el)) return null;
967
+ const style = getComputedStyle(el);
968
+ if (!/(fixed|sticky)/.test(style.position)) return null;
969
+ const rect = el.getBoundingClientRect();
970
+ if (rect.width < window.innerWidth * 0.25 || rect.height < 20 || rect.height > 140) return null;
971
+ if (rect.top > 4 || rect.bottom < 24) return null;
972
+ if (rect.right < window.innerWidth * 0.25 || rect.left > window.innerWidth * 0.75) return null;
973
+ return rect.bottom;
974
+ })
975
+ .filter((bottom) => Number.isFinite(bottom));
976
+ const headerBottom = blockers.length ? Math.max(...blockers) : 0;
977
+ return Math.min(Math.max(headerBottom, 0), Math.min(140, window.innerHeight * 0.2));
978
+ }
979
+
980
+ function bottomVisibleInset() {
981
+ const blockers = Array.from(document.querySelectorAll("body *"))
982
+ .map((el) => {
983
+ if (!(el instanceof HTMLElement)) return null;
984
+ if (isHostUiElement(el)) return null;
985
+ const style = getComputedStyle(el);
986
+ const rect = el.getBoundingClientRect();
987
+ if (rect.width < window.innerWidth * 0.25 || rect.height < 24 || rect.height > window.innerHeight * 0.45) return null;
988
+ if (rect.bottom < window.innerHeight - 6 || rect.top > window.innerHeight - 24) return null;
989
+ if (rect.right < window.innerWidth * 0.25 || rect.left > window.innerWidth * 0.75) return null;
990
+ const isAnchored = /(fixed|sticky)/.test(style.position);
991
+ const isComposer = Boolean(el.matches("form, textarea, input, [contenteditable='true'], [role='textbox']") ||
992
+ el.querySelector?.("textarea, input, [contenteditable='true'], [role='textbox']"));
993
+ if (!isAnchored && !isComposer) return null;
994
+ return window.innerHeight - rect.top;
995
+ })
996
+ .filter((height) => Number.isFinite(height));
997
+ const blockerHeight = blockers.length ? Math.max(...blockers) : 0;
998
+ return Math.min(Math.max(blockerHeight, 0), Math.min(320, window.innerHeight * 0.45));
999
+ }
1000
+
1001
+ function composerVisibleInset() {
1002
+ const anchor = composerAnchorElement();
1003
+ const rect = anchor?.getBoundingClientRect?.();
1004
+ if (!rect || rect.bottom < window.innerHeight * 0.5 || rect.top > window.innerHeight) return 0;
1005
+ return Math.min(Math.max(0, window.innerHeight - rect.top), Math.min(320, window.innerHeight * 0.45));
1006
+ }
1007
+
1008
+ function viewportSafeBottom() {
1009
+ return window.innerHeight - Math.max(bottomVisibleInset(), composerVisibleInset());
1010
+ }
1011
+
1012
+ function viewportTargetTop(target, options = {}) {
1013
+ const padding = 8;
1014
+ const rect = target.getBoundingClientRect();
1015
+ const minTop = topVisibleInset() + padding;
1016
+ const maxTop = Math.max(minTop, viewportSafeBottom() - rect.height - padding);
1017
+ if (options.block === "end") return maxTop;
1018
+ if (options.block === "center") {
1019
+ return Math.max(minTop, Math.min(maxTop, (window.innerHeight / 2) - (rect.height / 2)));
1020
+ }
1021
+ return minTop;
1022
+ }
1023
+
1024
+ function viewportAlignmentDistance(target, options = {}) {
1025
+ const padding = 8;
1026
+ const rect = target.getBoundingClientRect();
1027
+ if (options.block === "end") return rect.bottom - viewportSafeBottom() + padding;
1028
+ return rect.top - viewportTargetTop(target, options);
1029
+ }
1030
+
1031
+ function tryViewportCorrection(target, options = {}) {
1032
+ if (!(target instanceof HTMLElement)) return false;
1033
+ const distance = () => viewportAlignmentDistance(target, options);
1034
+ let delta = distance();
1035
+ if (Math.abs(delta) <= 4) return true;
1036
+ for (let pass = 0; pass < 6; pass += 1) {
1037
+ let improved = false;
1038
+ for (const root of scrollableAncestors(target)) {
1039
+ const beforeDistance = Math.abs(distance());
1040
+ const originalTop = scrollTopOf(root);
1041
+ const candidates = [
1042
+ scrollTopForViewportDelta(root, delta),
1043
+ scrollTopForViewportDelta(root, -delta),
1044
+ originalTop + delta,
1045
+ originalTop - delta
1046
+ ];
1047
+ let bestTop = originalTop;
1048
+ let bestDistance = beforeDistance;
1049
+ for (const top of candidates) {
1050
+ scrollRootTo(root, top);
1051
+ const candidateDistance = Math.abs(distance());
1052
+ if (candidateDistance < bestDistance) {
1053
+ bestDistance = candidateDistance;
1054
+ bestTop = scrollTopOf(root);
1055
+ }
1056
+ scrollRootTo(root, originalTop);
1057
+ }
1058
+ if (bestDistance + 2 < beforeDistance) {
1059
+ scrollRootTo(root, bestTop);
1060
+ if (bestDistance <= 8) return true;
1061
+ delta = distance();
1062
+ improved = true;
1063
+ break;
1064
+ }
1065
+ }
1066
+ if (!improved) break;
1067
+ }
1068
+ return Math.abs(distance()) <= 8;
1069
+ }
1070
+
1071
+ function nativeScrollThenCorrect(target, options = {}) {
1072
+ if (!(target instanceof HTMLElement)) return false;
1073
+ try {
1074
+ target.scrollIntoView({
1075
+ block: options.block === "end" ? "end" : options.block === "center" ? "center" : "start",
1076
+ inline: "nearest",
1077
+ behavior: "instant"
1078
+ });
1079
+ } catch {
1080
+ target.scrollIntoView(options.block === "end" ? false : true);
1081
+ }
1082
+ return tryViewportCorrection(target, options);
1083
+ }
1084
+
1085
+ function scrollAnchorIntoView(el, id, options = {}) {
1086
+ const target = scrollTargetForAnchor(el, id, options);
1087
+ const root = rememberScrollContainer(nearestScrollContainer(target));
1088
+ const deltaFor = () => {
1089
+ const rootRect = root.getBoundingClientRect();
1090
+ const targetRect = target.getBoundingClientRect();
1091
+ const padding = 8;
1092
+ if (options.block === "start") return targetRect.top - Math.max(rootRect.top, topVisibleInset()) - padding;
1093
+ if (options.block === "end") return targetRect.bottom - Math.min(rootRect.bottom, viewportSafeBottom()) + padding;
1094
+ return targetRect.top + (targetRect.height / 2) - (rootRect.top + rootRect.height / 2);
1095
+ };
1096
+ const delta = deltaFor();
1097
+ const before = scrollTopOf(root);
1098
+ const nextTop = isDocumentScrollRoot(root) ? Math.max(0, before + delta) : scrollTopForViewportDelta(root, delta);
1099
+ scrollRootTo(root, nextTop);
1100
+ if (Math.abs(delta) > 2 && Math.abs(scrollTopOf(root) - before) < 1) {
1101
+ tryViewportCorrection(target, options);
1102
+ }
1103
+ if (!tryViewportCorrection(target, options)) {
1104
+ nativeScrollThenCorrect(target, options);
1105
+ }
1106
+ const correctFreshAlignment = () => {
1107
+ const freshEl = findAnchor(id);
1108
+ if (!(freshEl instanceof HTMLElement)) return;
1109
+ const freshTarget = scrollTargetForAnchor(freshEl, id, options);
1110
+ if (!freshTarget.isConnected) return;
1111
+ if (tryViewportCorrection(freshTarget, options) || nativeScrollThenCorrect(freshTarget, options)) return;
1112
+ const freshRoot = nearestScrollContainer(freshTarget);
1113
+ const rootRect = freshRoot.getBoundingClientRect();
1114
+ const targetRect = freshTarget.getBoundingClientRect();
1115
+ const padding = 8;
1116
+ let correction = 0;
1117
+ if (options.block === "start") {
1118
+ correction = targetRect.top - Math.max(rootRect.top, topVisibleInset()) - padding;
1119
+ } else if (options.block === "end") {
1120
+ correction = targetRect.bottom - Math.min(rootRect.bottom, viewportSafeBottom()) + padding;
1121
+ } else {
1122
+ correction = targetRect.top + (targetRect.height / 2) - (rootRect.top + rootRect.height / 2);
1123
+ }
1124
+ if (Math.abs(correction) > 4) {
1125
+ const correctedTop = isDocumentScrollRoot(freshRoot)
1126
+ ? Math.max(0, scrollTopOf(freshRoot) + correction)
1127
+ : scrollTopForViewportDelta(freshRoot, correction);
1128
+ scrollRootTo(freshRoot, correctedTop);
1129
+ }
1130
+ };
1131
+ correctFreshAlignment();
1132
+ }
1133
+
1134
+ function anchors() {
1135
+ const root = findScrollContainer();
1136
+ const rootRect = root.getBoundingClientRect();
1137
+ const elements = anchorElements();
1138
+ replaceStaleFrozenOrder(elements);
1139
+ rememberAnchorOrder(elements);
1140
+ return elements.filter((el) =>
1141
+ !state.anchorOrderFrozen || state.anchorOrder.includes(anchorId(el))
1142
+ ).map((el) => {
1143
+ const id = anchorId(el);
1144
+ const rect = el.getBoundingClientRect();
1145
+ const visible = rect.bottom >= rootRect.top &&
1146
+ rect.top <= rootRect.bottom &&
1147
+ rect.right >= rootRect.left &&
1148
+ rect.left <= rootRect.right;
1149
+ return {
1150
+ anchorId: id,
1151
+ kind: anchorKind(el),
1152
+ visible,
1153
+ top: Math.round(rect.top - rootRect.top),
1154
+ height: Math.round(rect.height),
1155
+ textPreview: previewText(el),
1156
+ order: anchorNumber(id),
1157
+ indexed: hasStableIndex(),
1158
+ inferredRole: roleForAnchor(el)
1159
+ };
1160
+ }).sort((a, b) => a.order - b.order);
1161
+ }
1162
+
1163
+ function viewportVisibleAnchors(items = anchors()) {
1164
+ return items.map((item) => {
1165
+ const el = findAnchor(item.anchorId);
1166
+ const marker = markerForAnchor(el, item.anchorId);
1167
+ const rect = marker?.getBoundingClientRect?.() || el?.getBoundingClientRect?.();
1168
+ return { ...item, viewportTop: rect?.top ?? item.top, viewportBottom: rect?.bottom ?? (item.top + item.height) };
1169
+ }).filter((item) =>
1170
+ item.viewportBottom >= topVisibleInset() &&
1171
+ item.viewportTop <= viewportSafeBottom()
1172
+ );
1173
+ }
1174
+
1175
+ function collectAnchorIds() {
1176
+ const seen = new Set();
1177
+ return anchorElements()
1178
+ .map(anchorId)
1179
+ .filter((id) => {
1180
+ if (!id || seen.has(id)) return false;
1181
+ seen.add(id);
1182
+ return true;
1183
+ });
1184
+ }
1185
+
1186
+ function hasAppServerOrder() {
1187
+ return state.transcriptOrderSource === "app-server" && state.anchorOrder.length > 0;
1188
+ }
1189
+
1190
+ function orderPendingResult() {
1191
+ ensureStyle();
1192
+ ensureModeBadge();
1193
+ return {
1194
+ ok: false,
1195
+ pending: true,
1196
+ error: "app-server transcript order pending"
1197
+ };
1198
+ }
1199
+
1200
+ async function runPrimeAnchorOrder(generation) {
1201
+ ensureStyle();
1202
+ ensureModeBadge();
1203
+ hideStatusChrome();
1204
+ if (generation !== state.indexingGeneration) return { ok: false, error: "stale index generation" };
1205
+ if (!hasAppServerOrder()) return orderPendingResult();
1206
+ const currentId = state.currentAnchorId;
1207
+ anchors();
1208
+ state.anchorOrderFrozen = true;
1209
+ if (currentId && findAnchor(currentId)) setCurrent(currentId, "adapter");
1210
+ ensureAnnotations();
1211
+ return {
1212
+ ok: true,
1213
+ count: state.anchorOrder.length,
1214
+ source: state.transcriptOrderSource ||
1215
+ (Object.keys(state.anchorIdByIndex).length ? "content-search-unit-sort-key" : "mounted-dom-order")
1216
+ };
1217
+ }
1218
+
1219
+ function primeAnchorOrder() {
1220
+ if (!hasAppServerOrder()) return Promise.resolve(orderPendingResult());
1221
+ if (state.anchorOrderFrozen) {
1222
+ anchors();
1223
+ ensureAnnotations();
1224
+ return Promise.resolve({
1225
+ ok: true,
1226
+ count: state.anchorOrder.length,
1227
+ cached: true,
1228
+ source: state.transcriptOrderSource ||
1229
+ (Object.keys(state.anchorIdByIndex).length ? "content-search-unit-sort-key" : "mounted-dom-order")
1230
+ });
1231
+ }
1232
+ if (state.indexingPromise && Date.now() - state.indexingStartedAt < 15000) return state.indexingPromise;
1233
+ const generation = state.indexingGeneration + 1;
1234
+ state.indexingGeneration = generation;
1235
+ state.indexingStartedAt = Date.now();
1236
+ state.indexingPromise = runPrimeAnchorOrder(generation).finally(() => {
1237
+ state.indexingPromise = null;
1238
+ state.indexingStartedAt = 0;
1239
+ });
1240
+ return state.indexingPromise;
1241
+ }
1242
+
1243
+ function freezeCurrentDomOrder(options = {}) {
1244
+ const selectCurrent = options.selectCurrent !== false;
1245
+ const annotate = options.annotate !== false;
1246
+ const ids = collectAnchorIds();
1247
+ if (!ids.length) return { ok: false, error: "no transcript anchors" };
1248
+ state.anchorIdByIndex = Object.create(null);
1249
+ state.anchorOrder = ids.some((id) => contentUnitSortKey(id)) ? [...ids].sort(compareAnchorIds) : ids;
1250
+ rebuildAnchorIndex();
1251
+ state.anchorOrderFrozen = true;
1252
+ state.transcriptOrderSource = mountedOrderSource(ids);
1253
+ if (!selectCurrent) {
1254
+ if (state.currentAnchorId && !state.anchorOrder.includes(state.currentAnchorId)) state.currentAnchorId = null;
1255
+ } else if (!state.currentAnchorId || !state.anchorOrder.includes(state.currentAnchorId)) {
1256
+ setCurrentToBottomVisibleAnchor();
1257
+ } else {
1258
+ setCurrent(state.currentAnchorId, "adapter");
1259
+ }
1260
+ if (annotate) ensureAnnotations();
1261
+ return { ok: true, count: state.anchorOrder.length };
1262
+ }
1263
+
1264
+ function withStableMountedIndex(command) {
1265
+ if (!hasAppServerOrder()) return orderPendingResult();
1266
+ if (!state.anchorOrderFrozen) state.anchorOrderFrozen = true;
1267
+ ensureAnnotations();
1268
+ return command();
1269
+ }
1270
+
1271
+ function withVisibleMountedIndex(command) {
1272
+ if (!hasAppServerOrder()) {
1273
+ ensureAnnotations();
1274
+ return command();
1275
+ }
1276
+ return withStableMountedIndex(command);
1277
+ }
1278
+
1279
+ function afterIndexing(command) {
1280
+ const run = () => {
1281
+ if (!hasAppServerOrder()) return orderPendingResult();
1282
+ ensureAnnotations();
1283
+ return command();
1284
+ };
1285
+ return run();
1286
+ }
1287
+
1288
+ function ensureAnnotations() {
1289
+ ensureStyle();
1290
+ ensureModeBadge();
1291
+ hideStatusChrome();
1292
+ const ids = new Set();
1293
+ for (const item of anchors()) {
1294
+ if (!item.indexed) continue;
1295
+ ids.add(item.anchorId);
1296
+ const el = findAnchor(item.anchorId);
1297
+ if (!(el instanceof HTMLElement)) continue;
1298
+ el.classList.add(ANCHOR_CLASS);
1299
+ el.classList.toggle(CURRENT_CLASS, item.anchorId === state.currentAnchorId);
1300
+ let label = el.querySelector(`.${ANNOTATION_CLASS}[data-anchor-id="${cssEscape(item.anchorId)}"][data-onewhack-app-id="${VIM_NAV_APP_ID}"]`);
1301
+ if (!label) {
1302
+ label = document.createElement("button");
1303
+ label.type = "button";
1304
+ label.className = ANNOTATION_CLASS;
1305
+ label.dataset.anchorId = item.anchorId;
1306
+ label.dataset.onewhackAppId = VIM_NAV_APP_ID;
1307
+ label.addEventListener("click", (event) => {
1308
+ event.preventDefault();
1309
+ event.stopImmediatePropagation();
1310
+ event.stopPropagation();
1311
+ const anchorId = event.currentTarget?.dataset?.anchorId || item.anchorId;
1312
+ setCurrent(anchorId, "transcript");
1313
+ highlightAnchor(anchorId, { durationMs: 600 });
1314
+ setTimeout(() => {
1315
+ openPanel().catch(() => {});
1316
+ }, 0);
1317
+ });
1318
+ }
1319
+ label.textContent = markerGlyph(item);
1320
+ label.title = item.indexed
1321
+ ? `Vim Nav ${item.order}: ${item.inferredRole || item.kind}`
1322
+ : `Vim Nav index pending: ${item.inferredRole || item.kind}`;
1323
+ label.setAttribute("aria-label", label.title);
1324
+ oneWhackRuntime().placeAnnotation?.(el, {
1325
+ appId: VIM_NAV_APP_ID,
1326
+ anchorId: item.anchorId,
1327
+ markerId: `vim-nav:${item.anchorId}`,
1328
+ priority: 30,
1329
+ element: label
1330
+ }) || el.insertAdjacentElement("afterbegin", label);
1331
+ }
1332
+ oneWhackRuntime().removeAnnotations?.(VIM_NAV_APP_ID, ids);
1333
+ document.querySelectorAll(`.${ANNOTATION_CLASS}[data-onewhack-app-id="${VIM_NAV_APP_ID}"]`).forEach((node) => {
1334
+ if (!ids.has(node.dataset.anchorId) && !node.closest(".onewhack-transcript-annotation-slot")) node.remove();
1335
+ });
1336
+ }
1337
+
1338
+ function setCurrent(id, source = "adapter", indexHint = null) {
1339
+ if (!id) return;
1340
+ state.currentAnchorId = id;
1341
+ if (Number.isInteger(indexHint)) {
1342
+ state.currentIndexValue = indexHint;
1343
+ } else {
1344
+ const index = state.anchorOrder.indexOf(id);
1345
+ if (index >= 0) state.currentIndexValue = index;
1346
+ }
1347
+ for (const el of document.querySelectorAll(`.${CURRENT_CLASS}`)) el.classList.remove(CURRENT_CLASS);
1348
+ findAnchor(id)?.classList.add(CURRENT_CLASS);
1349
+ state.selection = {
1350
+ selectionId: `sel_${Date.now()}`,
1351
+ source,
1352
+ appId: VIM_NAV_APP_ID,
1353
+ anchorId: id,
1354
+ entryId: `nav:${id}`,
1355
+ selectedAt: new Date().toISOString()
1356
+ };
1357
+ }
1358
+
1359
+ function currentIndex() {
1360
+ const list = anchors();
1361
+ const index = state.anchorOrder.indexOf(state.currentAnchorId);
1362
+ if (index >= 0) {
1363
+ state.currentIndexValue = index;
1364
+ return index;
1365
+ }
1366
+ if (Number.isInteger(state.currentIndexValue) &&
1367
+ state.currentIndexValue >= 0 &&
1368
+ state.currentIndexValue < Math.max(state.anchorOrder.length, 1)) {
1369
+ return state.currentIndexValue;
1370
+ }
1371
+ const firstVisible = list.find((item) => item.visible) || list[0];
1372
+ state.currentIndexValue = firstVisible ? firstVisible.order - 1 : 0;
1373
+ return state.currentIndexValue;
1374
+ }
1375
+
1376
+ function mountedTargetForIndex(index, direction = 0) {
1377
+ const list = anchors();
1378
+ if (!list.length) return null;
1379
+ const exact = list.find((item) => item.order - 1 === index);
1380
+ if (exact) return exact;
1381
+ if (direction < 0) {
1382
+ return [...list].reverse().find((item) => item.order - 1 <= index) || list[0];
1383
+ }
1384
+ if (direction > 0) {
1385
+ return list.find((item) => item.order - 1 >= index) || list[list.length - 1];
1386
+ }
1387
+ return list.reduce((nearest, item) =>
1388
+ Math.abs((item.order - 1) - index) < Math.abs((nearest.order - 1) - index) ? item : nearest
1389
+ , list[0]);
1390
+ }
1391
+
1392
+ function mountedAnchorsByViewport() {
1393
+ return anchors()
1394
+ .map((item) => {
1395
+ const el = findAnchor(item.anchorId);
1396
+ const marker = markerForAnchor(el, item.anchorId);
1397
+ const rect = marker?.getBoundingClientRect?.() || el?.getBoundingClientRect?.();
1398
+ return {
1399
+ ...item,
1400
+ viewportTop: rect?.top ?? item.top,
1401
+ viewportBottom: rect?.bottom ?? (item.top + item.height)
1402
+ };
1403
+ })
1404
+ .sort((a, b) => a.viewportTop - b.viewportTop || a.viewportBottom - b.viewportBottom);
1405
+ }
1406
+
1407
+ function jumpRelativeMounted(delta) {
1408
+ const list = mountedAnchorsByViewport();
1409
+ if (!list.length) return { ok: false, error: "no mounted transcript anchors" };
1410
+ const visible = list.filter((item) =>
1411
+ item.viewportBottom >= topVisibleInset() &&
1412
+ item.viewportTop <= viewportSafeBottom()
1413
+ );
1414
+ let baseIndex = list.findIndex((item) => item.anchorId === state.currentAnchorId);
1415
+ if (baseIndex < 0) {
1416
+ const fallback = delta < 0
1417
+ ? (visible[0] || list[0])
1418
+ : (visible[visible.length - 1] || list[list.length - 1]);
1419
+ baseIndex = Math.max(0, list.findIndex((item) => item.anchorId === fallback.anchorId));
1420
+ }
1421
+ const targetIndex = Math.max(0, Math.min(list.length - 1, baseIndex + delta));
1422
+ const target = list[targetIndex];
1423
+ if (!target) return { ok: false, error: "target transcript anchor not mounted" };
1424
+ setCurrent(target.anchorId, "adapter", target.order - 1);
1425
+ return scrollToAnchor(target.anchorId, {
1426
+ behavior: "auto",
1427
+ block: blockForAnchorVisibility(target.anchorId, "start"),
1428
+ indexHint: target.order - 1
1429
+ });
1430
+ }
1431
+
1432
+ async function jumpRelativeMountedAcrossGap(delta, currentOrder, beforeAnchorId) {
1433
+ const targetIndex = currentOrder - 1 + delta;
1434
+ const indexedTarget = await mountTargetForIndex(targetIndex, delta, {
1435
+ approximate: true,
1436
+ preferExact: true
1437
+ });
1438
+ if (indexedTarget && indexedTarget.anchorId !== beforeAnchorId) {
1439
+ setCurrent(indexedTarget.anchorId, "adapter", indexedTarget.order - 1);
1440
+ return scrollToAnchor(indexedTarget.anchorId, {
1441
+ behavior: "auto",
1442
+ block: "start",
1443
+ indexHint: indexedTarget.order - 1
1444
+ });
1445
+ }
1446
+
1447
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1448
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1449
+ const adjacent = delta < 0
1450
+ ? [...mounted].reverse().find((item) => item.order < currentOrder && item.anchorId !== beforeAnchorId)
1451
+ : mounted.find((item) => item.order > currentOrder && item.anchorId !== beforeAnchorId);
1452
+ if (adjacent) {
1453
+ setCurrent(adjacent.anchorId, "adapter", adjacent.order - 1);
1454
+ return scrollToAnchor(adjacent.anchorId, {
1455
+ behavior: "auto",
1456
+ block: blockForAnchorVisibility(adjacent.anchorId, "start"),
1457
+ indexHint: adjacent.order - 1
1458
+ });
1459
+ }
1460
+ await delay(90);
1461
+ }
1462
+
1463
+ const searchRoot = () => findScrollContainer() || nearestScrollContainer(anchorElements()[0] || findAnchor(state.currentAnchorId) || document.body);
1464
+ let root = searchRoot();
1465
+ const viewport = Math.min(root.clientHeight || window.innerHeight || 720, 1200);
1466
+ const step = delta < 0
1467
+ ? Math.max(520, viewport * 1.45)
1468
+ : Math.max(520, Math.min(viewport, 900) * 1.35);
1469
+ const attempts = [];
1470
+ for (let attempt = 0; attempt < 18; attempt += 1) {
1471
+ root = searchRoot();
1472
+ const beforeTop = scrollTopOf(root);
1473
+ scrollRootTo(root, scrollTopForViewportDelta(root, delta < 0 ? -step : step));
1474
+ await delay(80);
1475
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1476
+ const target = delta < 0
1477
+ ? [...mounted].reverse().find((item) =>
1478
+ item.order < currentOrder && item.anchorId !== beforeAnchorId
1479
+ )
1480
+ : mounted.find((item) =>
1481
+ item.order > currentOrder && item.anchorId !== beforeAnchorId
1482
+ );
1483
+ attempts.push({
1484
+ attempt,
1485
+ beforeTop: Math.round(beforeTop),
1486
+ afterTop: Math.round(scrollTopOf(root)),
1487
+ mounted: mounted.map((item) => item.order)
1488
+ });
1489
+ state.lastMountSearch = { mode: "relative-mounted-gap", direction: delta, currentOrder, attempts: attempts.slice(-5) };
1490
+ if (target) {
1491
+ setCurrent(target.anchorId, "adapter", target.order - 1);
1492
+ return scrollToAnchor(target.anchorId, {
1493
+ behavior: "auto",
1494
+ block: "start",
1495
+ indexHint: target.order - 1
1496
+ });
1497
+ }
1498
+ if (Math.abs(scrollTopOf(root) - beforeTop) <= 1) break;
1499
+ }
1500
+ if (delta > 0) {
1501
+ root = searchRoot();
1502
+ scrollRootTo(root, scrollRootRange(root).max);
1503
+ await delay(120);
1504
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1505
+ const target = mounted.find((item) =>
1506
+ item.order > currentOrder && item.anchorId !== beforeAnchorId
1507
+ ) || mounted[mounted.length - 1];
1508
+ if (target && target.anchorId !== beforeAnchorId && target.order > currentOrder) {
1509
+ setCurrent(target.anchorId, "adapter", target.order - 1);
1510
+ return scrollToAnchor(target.anchorId, {
1511
+ behavior: "auto",
1512
+ block: "start",
1513
+ indexHint: target.order - 1
1514
+ });
1515
+ }
1516
+ }
1517
+ if (delta > 0 && currentOrder < lastKnownIndex() + 1) {
1518
+ return jumpToBottom();
1519
+ }
1520
+ if (delta < 0 && currentOrder > 1) {
1521
+ return jumpToIndex(0, { block: "start", direction: -1, behavior: "auto" });
1522
+ }
1523
+ return jumpRelativeMounted(delta);
1524
+ }
1525
+
1526
+ function renderedCurrentOrder() {
1527
+ const desiredTop = topVisibleInset() + 8;
1528
+ const visibleMarker = Array.from(document.querySelectorAll(`.${ANNOTATION_CLASS}`))
1529
+ .map((marker) => {
1530
+ const rect = marker.getBoundingClientRect();
1531
+ return {
1532
+ order: Number(marker.textContent.trim()),
1533
+ top: rect.top,
1534
+ bottom: rect.bottom
1535
+ };
1536
+ })
1537
+ .filter((item) =>
1538
+ Number.isSafeInteger(item.order) &&
1539
+ item.bottom >= topVisibleInset() &&
1540
+ item.top <= viewportSafeBottom()
1541
+ )
1542
+ .sort((a, b) => Math.abs(a.top - desiredTop) - Math.abs(b.top - desiredTop))[0];
1543
+ if (visibleMarker) return visibleMarker.order;
1544
+ const currentMarker = document.querySelector(`.${CURRENT_CLASS} .${ANNOTATION_CLASS}`);
1545
+ const currentValue = Number(currentMarker?.textContent?.trim());
1546
+ return Number.isSafeInteger(currentValue) ? currentValue : null;
1547
+ }
1548
+
1549
+ function mountedFloorTargetForIndex(index) {
1550
+ const visible = viewportVisibleAnchors(anchors()).sort((a, b) => a.order - b.order);
1551
+ const visibleExact = visible.find((item) => item.order - 1 === index);
1552
+ if (visibleExact) return visibleExact;
1553
+
1554
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1555
+ const mountedExact = mounted.find((item) => item.order - 1 === index);
1556
+ if (mountedExact) return mountedExact;
1557
+ const visibleFloor = [...visible].reverse().find((item) => item.order - 1 <= index);
1558
+ if (visibleFloor) return visibleFloor;
1559
+ return [...mounted].reverse().find((item) => item.order - 1 <= index) || mounted[0] || null;
1560
+ }
1561
+
1562
+ function mountedBottomFloorTargetForIndex(index) {
1563
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1564
+ const mountedExact = mounted.find((item) => item.order - 1 === index);
1565
+ if (mountedExact) return mountedExact;
1566
+ return [...mounted].reverse().find((item) => item.order - 1 <= index) || mounted[mounted.length - 1] || null;
1567
+ }
1568
+
1569
+ function mountedResolutionForIndex(index) {
1570
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1571
+ if (!mounted.length) return null;
1572
+ const exact = mounted.find((item) => item.order - 1 === index) || null;
1573
+ const floor = [...mounted].reverse().find((item) => item.order - 1 <= index) || null;
1574
+ const ceiling = mounted.find((item) => item.order - 1 >= index) || null;
1575
+ return {
1576
+ mounted,
1577
+ first: mounted[0].order - 1,
1578
+ last: mounted[mounted.length - 1].order - 1,
1579
+ exact,
1580
+ floor,
1581
+ ceiling
1582
+ };
1583
+ }
1584
+
1585
+ function mountedIndexRange() {
1586
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1587
+ if (!mounted.length) return null;
1588
+ return {
1589
+ first: mounted[0].order - 1,
1590
+ last: mounted[mounted.length - 1].order - 1,
1591
+ mounted
1592
+ };
1593
+ }
1594
+
1595
+ function estimatedScrollTopForIndex(root, index) {
1596
+ const maxIndex = Math.max(lastKnownIndex(), 1);
1597
+ const range = scrollRootRange(root);
1598
+ const ratio = Math.max(0, Math.min(1, index / maxIndex));
1599
+ return range.min + ((range.max - range.min) * ratio);
1600
+ }
1601
+
1602
+ async function mountTargetForIndex(index, direction = 0, options = {}) {
1603
+ const knownId = state.anchorIdByIndex[index] || (Object.keys(state.anchorIdByIndex).length ? null : state.anchorOrder[index]);
1604
+ const shouldResolveFloor = options.resolveMissing === "floor-on-overshoot";
1605
+ const preferExact = options.preferExact === true && Boolean(knownId);
1606
+ const isExactKnownTarget = (candidate) =>
1607
+ candidate && (!knownId || candidate.anchorId === knownId || candidate.order - 1 === index);
1608
+ const searchRoot = () => findScrollContainer() || nearestScrollContainer(anchorElements()[0] || findAnchor(state.currentAnchorId) || document.body);
1609
+ let root = searchRoot();
1610
+ const resolveMounted = () => {
1611
+ const resolution = mountedResolutionForIndex(index);
1612
+ if (!resolution) return null;
1613
+ if (isExactKnownTarget(resolution.exact)) return { target: resolution.exact, done: true };
1614
+ if (resolution.first <= index && index <= resolution.last) {
1615
+ if (preferExact) return { target: null, done: false, resolution, contained: true };
1616
+ const missingTarget = shouldResolveFloor
1617
+ ? resolution.floor
1618
+ : direction > 0
1619
+ ? resolution.ceiling || resolution.floor
1620
+ : direction < 0
1621
+ ? resolution.floor || resolution.ceiling
1622
+ : resolution.floor || resolution.ceiling;
1623
+ return { target: missingTarget, done: true, resolution };
1624
+ }
1625
+ return { target: null, done: false, resolution };
1626
+ };
1627
+
1628
+ let resolved = resolveMounted();
1629
+ let target = resolved?.target || null;
1630
+ if (resolved?.done && target) return target;
1631
+ if (index <= 0) {
1632
+ scrollRootTo(root, scrollRootRange(root).min);
1633
+ await delay(450);
1634
+ anchors();
1635
+ target = mountedTargetForIndex(0, 1);
1636
+ return target && target.order - 1 === 0 ? target : target || null;
1637
+ }
1638
+ if (index >= lastKnownIndex()) {
1639
+ for (let attempt = 0; attempt < 6; attempt += 1) {
1640
+ root = searchRoot();
1641
+ scrollRootTo(root, scrollRootRange(root).max);
1642
+ await delay(180);
1643
+ anchors();
1644
+ target = mountedTargetForIndex(lastKnownIndex(), -1);
1645
+ if (target && target.order - 1 >= lastKnownIndex()) return target;
1646
+ }
1647
+ if (options.resolveMissing === "bottom-floor-on-end") return mountedBottomFloorTargetForIndex(index);
1648
+ return null;
1649
+ }
1650
+ if (options.approximate === true && state.anchorOrder.length > 1 && resolved?.resolution) {
1651
+ root = searchRoot();
1652
+ scrollRootTo(root, estimatedScrollTopForIndex(root, index));
1653
+ await delay(90);
1654
+ anchors();
1655
+ resolved = resolveMounted();
1656
+ target = resolved?.target || null;
1657
+ if (resolved?.done && target) return target;
1658
+ }
1659
+ {
1660
+ let travelDirection = direction || (resolved?.resolution
1661
+ ? (index < resolved.resolution.first ? -1 : index > resolved.resolution.last ? 1 : 0)
1662
+ : 0);
1663
+ if (travelDirection) {
1664
+ let previousFirst = resolved?.resolution?.first;
1665
+ let previousLast = resolved?.resolution?.last;
1666
+ const attempts = [];
1667
+ for (let attempt = 0; attempt < 36; attempt += 1) {
1668
+ root = searchRoot();
1669
+ const beforeResolution = resolved?.resolution || mountedResolutionForIndex(index);
1670
+ const viewport = Math.min(root.clientHeight || window.innerHeight || 720, 1100);
1671
+ const sparseRange = beforeResolution
1672
+ ? Math.max(0, beforeResolution.last - beforeResolution.first + 1 - beforeResolution.mounted.length)
1673
+ : 0;
1674
+ const upwardMultiplier = sparseRange > 6 ? 1.75 : 1.35;
1675
+ const step = travelDirection < 0
1676
+ ? Math.max(520, viewport * upwardMultiplier)
1677
+ : Math.max(240, Math.min(viewport, 900) * 0.82);
1678
+ const beforeTop = scrollTopOf(root);
1679
+ const wantedSign = travelDirection > 0 ? 1 : -1;
1680
+ const tryMove = async (sign) => {
1681
+ scrollRootTo(root, scrollTopForViewportDelta(root, sign * step));
1682
+ await delay(70);
1683
+ anchors();
1684
+ const movedResolution = mountedResolutionForIndex(index);
1685
+ const movedTop = scrollTopOf(root);
1686
+ const progress = !beforeResolution || !movedResolution ? 0 : travelDirection > 0
1687
+ ? Math.max(0, movedResolution.last - beforeResolution.last)
1688
+ : Math.max(0, beforeResolution.first - movedResolution.first);
1689
+ return { sign, movedTop, movedResolution, progress };
1690
+ };
1691
+ let move = await tryMove(wantedSign);
1692
+ if (!move.progress) {
1693
+ scrollRootTo(root, beforeTop);
1694
+ await delay(20);
1695
+ move = await tryMove(-wantedSign);
1696
+ }
1697
+ await delay(70);
1698
+ anchors();
1699
+ resolved = resolveMounted();
1700
+ target = resolved?.target || null;
1701
+ travelDirection = direction || (resolved?.resolution
1702
+ ? (index < resolved.resolution.first ? -1 : index > resolved.resolution.last ? 1 : 0)
1703
+ : travelDirection);
1704
+ attempts.push({
1705
+ attempt,
1706
+ wantedSign,
1707
+ usedSign: move.sign,
1708
+ beforeTop: Math.round(beforeTop),
1709
+ afterTop: Math.round(scrollTopOf(root)),
1710
+ beforeRange: beforeResolution ? [beforeResolution.first + 1, beforeResolution.last + 1] : null,
1711
+ afterRange: resolved?.resolution ? [resolved.resolution.first + 1, resolved.resolution.last + 1] : null,
1712
+ progress: move.progress
1713
+ });
1714
+ state.lastMountSearch = { index: index + 1, direction: travelDirection, attempts: attempts.slice(-6) };
1715
+ if (resolved?.done && target) return target;
1716
+ const nextFirst = resolved?.resolution?.first;
1717
+ const nextLast = resolved?.resolution?.last;
1718
+ const changedScroll = Math.abs(scrollTopOf(root) - beforeTop) > 1;
1719
+ const changedRange = nextFirst !== previousFirst || nextLast !== previousLast;
1720
+ const range = scrollRootRange(root);
1721
+ const atSearchEdge = travelDirection < 0
1722
+ ? scrollTopOf(root) <= range.min + 2
1723
+ : scrollTopOf(root) >= range.max - 2;
1724
+ if (!changedRange && atSearchEdge) {
1725
+ await delay(650);
1726
+ anchors();
1727
+ resolved = resolveMounted();
1728
+ target = resolved?.target || null;
1729
+ if (resolved?.done && target) return target;
1730
+ const delayedFirst = resolved?.resolution?.first;
1731
+ const delayedLast = resolved?.resolution?.last;
1732
+ if (delayedFirst !== nextFirst || delayedLast !== nextLast) {
1733
+ previousFirst = delayedFirst;
1734
+ previousLast = delayedLast;
1735
+ continue;
1736
+ }
1737
+ }
1738
+ previousFirst = nextFirst;
1739
+ previousLast = nextLast;
1740
+ if (!changedScroll && !changedRange) break;
1741
+ if (resolved?.resolution) {
1742
+ if (travelDirection > 0 && resolved.resolution.first > index) {
1743
+ if (preferExact) break;
1744
+ break;
1745
+ }
1746
+ if (travelDirection < 0 && resolved.resolution.last < index) {
1747
+ if (preferExact) break;
1748
+ break;
1749
+ }
1750
+ }
1751
+ }
1752
+ }
1753
+ }
1754
+ if (shouldResolveFloor) {
1755
+ const floorTarget = mountedFloorTargetForIndex(index);
1756
+ if (floorTarget && floorTarget.order - 1 <= index) return floorTarget;
1757
+ const resolution = mountedResolutionForIndex(index);
1758
+ if (resolution?.ceiling) return resolution.ceiling;
1759
+ return floorTarget || null;
1760
+ }
1761
+ target = mountedTargetForIndex(index, direction);
1762
+ return isExactKnownTarget(target) ? target : null;
1763
+ }
1764
+
1765
+ function lastKnownIndex() {
1766
+ const list = anchors();
1767
+ if (!list.length) return 0;
1768
+ const indexedKeys = Object.keys(state.anchorIdByIndex).map(Number).filter((index) => Number.isSafeInteger(index));
1769
+ return Math.max(...list.map((item) => item.order - 1), ...indexedKeys, state.anchorOrder.length - 1, 0);
1770
+ }
1771
+
1772
+ async function jumpToIndex(index, options = {}) {
1773
+ const list = anchors();
1774
+ if (!list.length) return { ok: false, error: "no transcript anchors" };
1775
+ const indexedKeys = Object.keys(state.anchorIdByIndex).map(Number).filter((key) => Number.isSafeInteger(key));
1776
+ const maxIndex = Math.max(...list.map((item) => item.order - 1), ...indexedKeys, state.anchorOrder.length - 1, 0);
1777
+ const clamped = Math.max(0, Math.min(maxIndex, index));
1778
+ const target = options.resolveHidden === "bottom-floor-mounted"
1779
+ ? mountedBottomFloorTargetForIndex(clamped)
1780
+ : options.resolveHidden === "floor-mounted"
1781
+ ? mountedFloorTargetForIndex(clamped)
1782
+ : await mountTargetForIndex(clamped, options.direction || 0, options);
1783
+ if (!target) return { ok: false, error: "target transcript anchor not mounted" };
1784
+ const targetIndex = target.order - 1;
1785
+ setCurrent(target.anchorId, options.source || "adapter", clamped);
1786
+ return scrollToAnchor(target.anchorId, {
1787
+ behavior: options.behavior || "auto",
1788
+ block: options.block || "center",
1789
+ indexHint: targetIndex,
1790
+ skipScrollIfVisible: options.skipScrollIfResolvedFloor && targetIndex !== clamped
1791
+ });
1792
+ }
1793
+
1794
+ async function jumpRelative(delta) {
1795
+ const visible = viewportVisibleAnchors(anchors());
1796
+ const currentVisible = state.currentAnchorId ? isAnchorViewportVisible(state.currentAnchorId) : false;
1797
+ const mounted = anchors().sort((a, b) => a.order - b.order);
1798
+ const selectedIsMounted = Boolean(state.currentAnchorId && mounted.some((item) => item.anchorId === state.currentAnchorId));
1799
+ if (!visible.length && !currentVisible) {
1800
+ return jumpRelativeMounted(delta);
1801
+ }
1802
+ if (!currentVisible && visible.length && !selectedIsMounted) {
1803
+ const target = delta < 0
1804
+ ? visible.sort((a, b) => a.viewportTop - b.viewportTop)[0]
1805
+ : visible.sort((a, b) => b.viewportBottom - a.viewportBottom)[0];
1806
+ if (target) setCurrent(target.anchorId, "adapter", target.order - 1);
1807
+ }
1808
+ const renderedOrder = renderedCurrentOrder();
1809
+ const selectedOrder = state.currentAnchorId ? anchorNumber(state.currentAnchorId) : null;
1810
+ const currentAnchorOrder = selectedIsMounted ? selectedOrder : renderedOrder || selectedOrder;
1811
+ const currentOrder = currentAnchorOrder != null && currentAnchorOrder <= state.anchorOrder.length
1812
+ ? currentAnchorOrder
1813
+ : Number.isInteger(state.currentIndexValue) ? state.currentIndexValue + 1 : null;
1814
+ const adjacentMounted = delta < 0
1815
+ ? [...mounted].reverse().find((item) => item.order < currentOrder && item.anchorId !== state.currentAnchorId)
1816
+ : mounted.find((item) => item.order > currentOrder && item.anchorId !== state.currentAnchorId);
1817
+ state.lastRelativeDebug = {
1818
+ delta,
1819
+ renderedOrder,
1820
+ currentAnchorOrder,
1821
+ currentOrder,
1822
+ currentAnchorId: state.currentAnchorId,
1823
+ selectedIsMounted,
1824
+ adjacent: adjacentMounted ? { order: adjacentMounted.order, anchorId: adjacentMounted.anchorId } : null,
1825
+ mounted: mounted.map((item) => item.order)
1826
+ };
1827
+ if (adjacentMounted) {
1828
+ setCurrent(adjacentMounted.anchorId, "adapter", adjacentMounted.order - 1);
1829
+ return scrollToAnchor(adjacentMounted.anchorId, {
1830
+ behavior: "auto",
1831
+ block: blockForAnchorVisibility(adjacentMounted.anchorId, "start"),
1832
+ indexHint: adjacentMounted.order - 1
1833
+ });
1834
+ }
1835
+ const beforeAnchorId = state.currentAnchorId;
1836
+ if (delta < 0) {
1837
+ return jumpRelativeMountedAcrossGap(delta, currentOrder, beforeAnchorId);
1838
+ }
1839
+ const beforeIndex = currentIndex();
1840
+ const result = await jumpToIndex(beforeIndex + delta, { block: "start", direction: delta, behavior: "auto" });
1841
+ const afterIndex = state.anchorOrder.indexOf(state.currentAnchorId);
1842
+ const moved = result?.ok &&
1843
+ state.currentAnchorId !== beforeAnchorId &&
1844
+ (delta < 0 ? afterIndex < beforeIndex : afterIndex > beforeIndex);
1845
+ if (moved) return result;
1846
+ return jumpRelativeMountedAcrossGap(delta, currentOrder, beforeAnchorId);
1847
+ }
1848
+
1849
+ function jumpToNumberIndex(requested) {
1850
+ const mountedRange = mountedIndexRange();
1851
+ const knownTarget = Boolean(state.anchorIdByIndex[requested] || state.anchorOrder[requested]);
1852
+ if (mountedRange && requested >= mountedRange.first && requested <= mountedRange.last) {
1853
+ const exact = mountedRange.mounted.find((item) => item.order - 1 === requested);
1854
+ if (!exact && knownTarget) {
1855
+ return jumpToIndex(requested, {
1856
+ block: "start",
1857
+ behavior: "auto",
1858
+ direction: requested < currentIndex() ? -1 : 1,
1859
+ approximate: true,
1860
+ preferExact: true,
1861
+ resolveMissing: "floor-on-overshoot",
1862
+ skipScrollIfResolvedFloor: true
1863
+ });
1864
+ }
1865
+ const target = mountedFloorTargetForIndex(requested);
1866
+ if (!target) return { ok: false, error: "target transcript anchor not mounted" };
1867
+ setCurrent(target.anchorId, "adapter", target.order - 1);
1868
+ return scrollToAnchor(target.anchorId, {
1869
+ block: "start",
1870
+ behavior: "auto",
1871
+ indexHint: target.order - 1,
1872
+ skipScrollIfVisible: !exact
1873
+ });
1874
+ }
1875
+ if (mountedRange) {
1876
+ return jumpToIndex(requested, {
1877
+ block: "start",
1878
+ behavior: "auto",
1879
+ direction: requested < mountedRange.first ? -1 : 1,
1880
+ approximate: true,
1881
+ preferExact: knownTarget,
1882
+ resolveMissing: "floor-on-overshoot",
1883
+ skipScrollIfResolvedFloor: true
1884
+ });
1885
+ }
1886
+ return jumpToIndex(requested, {
1887
+ block: "start",
1888
+ behavior: "auto",
1889
+ approximate: true,
1890
+ preferExact: knownTarget,
1891
+ resolveMissing: "floor-on-overshoot",
1892
+ skipScrollIfResolvedFloor: true
1893
+ });
1894
+ }
1895
+
1896
+ function jumpToCountOrLast(prefix = state.countPrefix) {
1897
+ if (prefix) {
1898
+ const displayNumber = Number(prefix);
1899
+ const requested = displayNumber - 1;
1900
+ if (requested > lastKnownIndex()) {
1901
+ return jumpToBottom();
1902
+ }
1903
+ return jumpToNumberIndex(requested);
1904
+ }
1905
+ return jumpToBottom();
1906
+ }
1907
+
1908
+ async function jumpToBottom() {
1909
+ const targetIndex = lastKnownIndex();
1910
+ let best = mountedBottomFloorTargetForIndex(targetIndex);
1911
+ const debug = {
1912
+ targetIndex,
1913
+ initialBest: best ? { order: best.order, anchorId: best.anchorId } : null,
1914
+ roots: []
1915
+ };
1916
+ state.lastBottomJump = debug;
1917
+ if (best && best.order - 1 >= targetIndex - 3) {
1918
+ const result = jumpToMountedBottomTarget(best);
1919
+ await delay(160);
1920
+ anchors();
1921
+ debug.initialBestVisible = isAnchorViewportVisible(best.anchorId);
1922
+ if (debug.initialBestVisible) {
1923
+ debug.result = "initial-best";
1924
+ return result;
1925
+ }
1926
+ }
1927
+ for (const root of bottomScrollRoots()) {
1928
+ const range = scrollRootRange(root);
1929
+ const before = scrollTopOf(root);
1930
+ for (const top of [...new Set([range.max, range.min])]) {
1931
+ scrollRootTo(root, top);
1932
+ await delay(140);
1933
+ anchors();
1934
+ const candidate = mountedBottomFloorTargetForIndex(targetIndex);
1935
+ const visible = candidate ? isAnchorViewportVisible(candidate.anchorId) : false;
1936
+ debug.roots.push({
1937
+ root: describeScrollRoot(root),
1938
+ before,
1939
+ requestedTop: top,
1940
+ after: scrollTopOf(root),
1941
+ range,
1942
+ mounted: anchors().map((item) => item.order),
1943
+ candidate: candidate ? { order: candidate.order, anchorId: candidate.anchorId, visible } : null
1944
+ });
1945
+ if (candidate && (!best || candidate.order > best.order)) best = candidate;
1946
+ if (candidate && candidate.order - 1 >= targetIndex - 8) {
1947
+ const result = jumpToMountedBottomTarget(candidate);
1948
+ await delay(160);
1949
+ if (isAnchorViewportVisible(candidate.anchorId)) {
1950
+ debug.result = "root-candidate";
1951
+ return result;
1952
+ }
1953
+ }
1954
+ }
1955
+ }
1956
+ const result = await jumpToIndex(targetIndex, {
1957
+ block: "start",
1958
+ direction: 1,
1959
+ behavior: "auto",
1960
+ resolveMissing: "bottom-floor-on-end"
1961
+ });
1962
+ if (result?.ok || !best || best.order - 1 < targetIndex - 8) {
1963
+ debug.result = result?.ok ? "jump-index" : "failed";
1964
+ debug.error = result?.error || null;
1965
+ return result;
1966
+ }
1967
+ if (!findAnchor(best.anchorId)) {
1968
+ debug.result = "best-unmounted";
1969
+ debug.error = result?.error || null;
1970
+ return result;
1971
+ }
1972
+ debug.result = "best-fallback";
1973
+ return jumpToMountedBottomTarget(best);
1974
+ }
1975
+
1976
+ function describeScrollRoot(root) {
1977
+ if (!root) return null;
1978
+ if (isDocumentScrollRoot(root)) return "document";
1979
+ const el = root;
1980
+ return [
1981
+ el.tagName?.toLowerCase() || "node",
1982
+ el.id ? `#${el.id}` : "",
1983
+ el.className && typeof el.className === "string" ? `.${el.className.split(/\s+/).slice(0, 3).join(".")}` : ""
1984
+ ].join("");
1985
+ }
1986
+
1987
+ function jumpToMountedBottomTarget(target) {
1988
+ setCurrent(target.anchorId, "adapter", target.order - 1);
1989
+ return scrollToAnchor(target.anchorId, {
1990
+ behavior: "auto",
1991
+ block: "end",
1992
+ indexHint: target.order - 1
1993
+ });
1994
+ }
1995
+
1996
+ function alignCurrent(block) {
1997
+ const list = anchors();
1998
+ if (!list.length) return { ok: false, error: "no transcript anchors" };
1999
+ const index = currentIndex();
2000
+ const target = mountedTargetForIndex(index);
2001
+ if (!target) return { ok: false, error: "target transcript anchor not mounted" };
2002
+ return scrollToAnchor(target.anchorId, {
2003
+ behavior: "auto",
2004
+ block,
2005
+ indexHint: target.order - 1
2006
+ });
2007
+ }
2008
+
2009
+ function jumpRole(role, direction) {
2010
+ const list = anchors();
2011
+ const start = currentIndex();
2012
+ const candidates = direction < 0 ? [...list].reverse() : list;
2013
+ for (const item of candidates) {
2014
+ const index = item.order - 1;
2015
+ if ((direction < 0 ? index < start : index > start) && item.inferredRole === role) {
2016
+ return jumpToIndex(index, { direction, block: "start", behavior: "auto" });
2017
+ }
2018
+ }
2019
+ return { ok: false, error: `${role} anchor not found` };
2020
+ }
2021
+
2022
+ function scrollToAnchor(id, options = {}) {
2023
+ ensureAnnotations();
2024
+ const el = findAnchor(id);
2025
+ if (!el) return { ok: false, error: "anchor not found" };
2026
+ setCurrent(id, "adapter", options.indexHint);
2027
+ if (options.skipScrollIfVisible && isAnchorViewportVisible(id)) {
2028
+ highlightAnchor(id, { durationMs: 900 });
2029
+ return { ok: true, anchorId: id, skippedScroll: true };
2030
+ }
2031
+ scrollAnchorIntoView(el, id, options);
2032
+ ensureAnnotations();
2033
+ setTimeout(() => ensureAnnotations(), 120);
2034
+ highlightAnchor(id, { durationMs: 900 });
2035
+ return { ok: true, anchorId: id };
2036
+ }
2037
+
2038
+ function isAnchorViewportVisible(id) {
2039
+ const el = findAnchor(id);
2040
+ if (!(el instanceof HTMLElement)) return false;
2041
+ const target = markerForAnchor(el, id);
2042
+ const rect = target?.getBoundingClientRect?.() || el.getBoundingClientRect();
2043
+ return rect.bottom >= topVisibleInset() &&
2044
+ rect.top <= viewportSafeBottom() &&
2045
+ rect.right >= 0 &&
2046
+ rect.left <= window.innerWidth;
2047
+ }
2048
+
2049
+ function blockForAnchorVisibility(id, fallback = "start") {
2050
+ const el = findAnchor(id);
2051
+ if (!(el instanceof HTMLElement)) return fallback;
2052
+ const target = markerForAnchor(el, id);
2053
+ const rect = target?.getBoundingClientRect?.() || el.getBoundingClientRect();
2054
+ if (rect.bottom > viewportSafeBottom()) return "end";
2055
+ if (rect.top < topVisibleInset()) return "start";
2056
+ return fallback;
2057
+ }
2058
+
2059
+ function highlightAnchor(id, options = {}) {
2060
+ ensureStyle();
2061
+ for (const el of document.querySelectorAll(`.${HIGHLIGHT_CLASS}`)) el.classList.remove(HIGHLIGHT_CLASS);
2062
+ const el = findAnchor(id);
2063
+ if (!el) return { ok: false, error: "anchor not found" };
2064
+ el.classList.add(HIGHLIGHT_CLASS);
2065
+ setTimeout(() => el.classList.remove(HIGHLIGHT_CLASS), options.durationMs || 1500);
2066
+ return { ok: true, anchorId: id };
2067
+ }
2068
+
2069
+ function setAnnotations(annotations = []) {
2070
+ ensureAnnotations();
2071
+ let applied = 0;
2072
+ for (const annotation of annotations) {
2073
+ const el = findAnchor(annotation.anchorId);
2074
+ const marker = annotation.markers?.[0];
2075
+ const label = el?.querySelector?.(`.${ANNOTATION_CLASS}[data-anchor-id="${cssEscape(annotation.anchorId)}"]`);
2076
+ if (label && marker) {
2077
+ label.textContent = marker.glyph || label.textContent;
2078
+ label.title = marker.label || label.title;
2079
+ applied += 1;
2080
+ }
2081
+ }
2082
+ return { ok: true, applied };
2083
+ }
2084
+
2085
+ function setTranscriptOrder(anchorIds = [], metadata = {}) {
2086
+ const ids = Array.from(new Set(anchorIds.filter(Boolean)));
2087
+ if (!ids.length) return { ok: false, error: "empty transcript order" };
2088
+ state.anchorOrder = ids;
2089
+ rebuildAnchorIndex();
2090
+ state.anchorOrderFrozen = true;
2091
+ state.transcriptOrderSource = metadata.source || "external";
2092
+ ensureAnnotations();
2093
+ return { ok: true, count: ids.length, source: state.transcriptOrderSource };
2094
+ }
2095
+
2096
+ let annotationRefreshTimer = null;
2097
+
2098
+ function scheduleAnnotationRefresh() {
2099
+ if (annotationRefreshTimer) return;
2100
+ annotationRefreshTimer = setTimeout(() => {
2101
+ annotationRefreshTimer = null;
2102
+ if (!state.anchorOrderFrozen) {
2103
+ if (!state.indexingPromise) primeAnchorOrder().catch(() => {});
2104
+ return;
2105
+ }
2106
+ ensureAnnotations();
2107
+ }, 80);
2108
+ }
2109
+
2110
+ function visibleInteractiveElements() {
2111
+ return Array.from(document.querySelectorAll("button, [role='button'], input, textarea, a"))
2112
+ .filter((el) => {
2113
+ const rect = el.getBoundingClientRect();
2114
+ return rect.width > 0 && rect.height > 0 &&
2115
+ rect.bottom > 0 && rect.top < window.innerHeight &&
2116
+ rect.right > 0 && rect.left < window.innerWidth;
2117
+ });
2118
+ }
2119
+
2120
+ function textOf(el) {
2121
+ return (el?.innerText || el?.textContent || el?.getAttribute?.("aria-label") || "")
2122
+ .replace(/\s+/g, " ")
2123
+ .trim();
2124
+ }
2125
+
2126
+ function browserUrlInput() {
2127
+ return Array.from(document.querySelectorAll("input")).find((input) => /url|address/i.test(input.placeholder || ""));
2128
+ }
2129
+
2130
+ function nativeBrowserButton() {
2131
+ return visibleInteractiveElements()
2132
+ .map((el) => ({ el, rect: el.getBoundingClientRect(), text: textOf(el) }))
2133
+ .filter(({ rect }) => rect.left > window.innerWidth * 0.45)
2134
+ .find(({ text }) => /⌘T/.test(text) || /^Brow/i.test(text))?.el || null;
2135
+ }
2136
+
2137
+ function toggleSidePanelButton() {
2138
+ return visibleInteractiveElements()
2139
+ .filter((el) => /toggle side panel/i.test(el.getAttribute("aria-label") || el.getAttribute("title") || ""))
2140
+ .sort((a, b) => b.getBoundingClientRect().left - a.getBoundingClientRect().left)[0] || null;
2141
+ }
2142
+
2143
+ function localServerButton() {
2144
+ return visibleInteractiveElements()
2145
+ .find((el) => /vim nav|codex vim|localhost|127\.0\.0\.1/i.test(el.getAttribute("aria-label") || textOf(el)));
2146
+ }
2147
+
2148
+ function panelDebug(extra = {}) {
2149
+ const rightControls = visibleInteractiveElements()
2150
+ .filter((el) => el.getBoundingClientRect().left > window.innerWidth * 0.45)
2151
+ .map((el) => ({
2152
+ tag: el.tagName,
2153
+ role: el.getAttribute("role") || null,
2154
+ text: textOf(el).slice(0, 80),
2155
+ aria: el.getAttribute("aria-label") || null,
2156
+ title: el.getAttribute("title") || null,
2157
+ placeholder: el.getAttribute("placeholder") || null,
2158
+ left: Math.round(el.getBoundingClientRect().left),
2159
+ top: Math.round(el.getBoundingClientRect().top)
2160
+ }))
2161
+ .slice(-24);
2162
+ return {
2163
+ hasUrlInput: Boolean(browserUrlInput()),
2164
+ hasBrowserButton: Boolean(nativeBrowserButton()),
2165
+ hasSideToggle: Boolean(toggleSidePanelButton()),
2166
+ frameSrcs: Array.from(document.querySelectorAll("webview, iframe")).map((el) => el.getAttribute("src") || ""),
2167
+ rightControls,
2168
+ ...extra
2169
+ };
2170
+ }
2171
+
2172
+ function delay(ms) {
2173
+ return new Promise((resolve) => setTimeout(resolve, ms));
2174
+ }
2175
+
2176
+ function oneWhackRuntime() {
2177
+ if (window[RUNTIME_KEY]?.registerApp) return window[RUNTIME_KEY];
2178
+ const runtime = {
2179
+ protocolVersion: null,
2180
+ hostUrl: null,
2181
+ apps: {},
2182
+ registerApp(app) {
2183
+ if (!app?.appId) throw new Error("OneWhack appId is required");
2184
+ const current = this.apps[app.appId] || {};
2185
+ const currentVersion = Number(current.bridge?.version);
2186
+ const nextVersion = Number(app.bridge?.version);
2187
+ if (
2188
+ current.bridge &&
2189
+ app.bridge &&
2190
+ Number.isFinite(currentVersion) &&
2191
+ Number.isFinite(nextVersion) &&
2192
+ nextVersion < currentVersion
2193
+ ) {
2194
+ throw new Error("OneWhack app bridge version regressed for " + app.appId);
2195
+ }
2196
+ const slot = {
2197
+ ...current,
2198
+ ...app,
2199
+ appId: app.appId,
2200
+ entryUrl: app.entryUrl || current.entryUrl || null,
2201
+ capabilities: app.capabilities || current.capabilities || {},
2202
+ injectedAt: app.injectedAt || current.injectedAt || new Date().toISOString()
2203
+ };
2204
+ this.apps[app.appId] = slot;
2205
+ return slot;
2206
+ },
2207
+ getApp(appId) {
2208
+ return this.apps[appId] || null;
2209
+ },
2210
+ getBridge(appId) {
2211
+ return this.getApp(appId)?.bridge || null;
2212
+ },
2213
+ getEntryUrl(appId) {
2214
+ return this.getApp(appId)?.entryUrl || null;
2215
+ }
2216
+ };
2217
+ window[RUNTIME_KEY] = runtime;
2218
+ return runtime;
2219
+ }
2220
+
2221
+ function enqueueHostEvent(event) {
2222
+ state.hostEvents.push({
2223
+ eventId: `evt_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2224
+ requestedAt: new Date().toISOString(),
2225
+ ...event
2226
+ });
2227
+ }
2228
+
2229
+ function drainHostEvents() {
2230
+ const events = state.hostEvents;
2231
+ state.hostEvents = [];
2232
+ return events;
2233
+ }
2234
+
2235
+ function applyHostState(hostState) {
2236
+ state.hostState = hostState || null;
2237
+ renderSelectionMenu();
2238
+ renderOverlay();
2239
+ renderComposerContextChips();
2240
+ maskSubmittedComposerContexts();
2241
+ installComposerSubmitInterceptor();
2242
+ return { ok: true };
2243
+ }
2244
+
2245
+ function hostSelectionActions() {
2246
+ return (state.hostState?.selectionActions || [])
2247
+ .filter((action) => action?.enabled !== false && action.appliesTo === "text-selection");
2248
+ }
2249
+
2250
+ function ensureHostElement(id) {
2251
+ ensureStyle();
2252
+ let el = document.getElementById(id);
2253
+ if (!el) {
2254
+ el = document.createElement("div");
2255
+ el.id = id;
2256
+ el.className = HOST_UI_CLASS;
2257
+ document.documentElement.appendChild(el);
2258
+ }
2259
+ return el;
2260
+ }
2261
+
2262
+ function transcriptAnchorForNode(node) {
2263
+ for (let el = node instanceof Element ? node : node?.parentElement; el; el = el.parentElement) {
2264
+ if (el.matches?.(SELECTOR)) return el;
2265
+ const nested = el.querySelector?.(SELECTOR);
2266
+ if (nested?.contains?.(node)) return nested;
2267
+ }
2268
+ return null;
2269
+ }
2270
+
2271
+ function selectionRangeFromDom() {
2272
+ const selection = window.getSelection?.();
2273
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;
2274
+ const range = selection.getRangeAt(0);
2275
+ const text = String(selection.toString() || "").replace(/\s+/g, " ").trim();
2276
+ if (!text) return null;
2277
+ const anchorEl = transcriptAnchorForNode(range.commonAncestorContainer);
2278
+ const anchorIdValue = anchorId(anchorEl);
2279
+ if (!anchorEl || !anchorIdValue) return null;
2280
+ const preview = previewText(anchorEl);
2281
+ const index = preview.indexOf(text);
2282
+ return {
2283
+ selectionId: `sel_${Date.now()}`,
2284
+ source: "transcript",
2285
+ appId: state.hostState?.panel?.activeAppId || VIM_NAV_APP_ID,
2286
+ anchorId: anchorIdValue,
2287
+ quote: text,
2288
+ range: {
2289
+ anchorId: anchorIdValue,
2290
+ text,
2291
+ quote: text,
2292
+ prefix: index >= 0 ? preview.slice(Math.max(0, index - 80), index) : "",
2293
+ suffix: index >= 0 ? preview.slice(index + text.length, index + text.length + 80) : "",
2294
+ startOffset: index >= 0 ? index : undefined,
2295
+ endOffset: index >= 0 ? index + text.length : undefined,
2296
+ fingerprint: `${anchorIdValue}:${text}`
2297
+ },
2298
+ selectedAt: new Date().toISOString(),
2299
+ rect: rectFromRange(range)
2300
+ };
2301
+ }
2302
+
2303
+ function rectFromRange(range) {
2304
+ const rect = range.getBoundingClientRect();
2305
+ if (rect.width || rect.height) return rect;
2306
+ const fallback = range.startContainer?.parentElement?.getBoundingClientRect?.();
2307
+ return fallback || { left: window.innerWidth / 2, top: window.innerHeight / 2, right: window.innerWidth / 2, bottom: window.innerHeight / 2, width: 0, height: 0 };
2308
+ }
2309
+
2310
+ function updateTextSelectionFromDom() {
2311
+ const next = selectionRangeFromDom();
2312
+ if (!next) return false;
2313
+ state.activeTextSelection = next;
2314
+ state.selection = {
2315
+ selectionId: next.selectionId,
2316
+ source: "transcript",
2317
+ appId: next.appId,
2318
+ anchorId: next.anchorId,
2319
+ quote: next.quote,
2320
+ range: next.range,
2321
+ selectedAt: next.selectedAt
2322
+ };
2323
+ setCurrent(next.anchorId, "transcript");
2324
+ enqueueHostEvent({
2325
+ kind: "selection",
2326
+ selection: state.selection
2327
+ });
2328
+ renderSelectionMenu();
2329
+ return true;
2330
+ }
2331
+
2332
+ function hideSelectionMenu() {
2333
+ const menu = document.getElementById(SELECTION_MENU_ID);
2334
+ menu?.remove();
2335
+ document.querySelectorAll("[data-onewhack-native-selection-action='true']").forEach((node) => node.remove());
2336
+ }
2337
+
2338
+ function selectionPayloadForAction(action, selection) {
2339
+ return {
2340
+ ...(action.payload || {}),
2341
+ selection: {
2342
+ selectionId: selection.selectionId,
2343
+ source: "transcript",
2344
+ appId: action.appId,
2345
+ anchorId: selection.anchorId,
2346
+ quote: selection.quote,
2347
+ range: selection.range,
2348
+ rect: plainRect(selection.rect),
2349
+ selectedAt: selection.selectedAt
2350
+ }
2351
+ };
2352
+ }
2353
+
2354
+ function plainRect(rect) {
2355
+ if (!rect) return null;
2356
+ return {
2357
+ left: Number(rect.left || 0),
2358
+ top: Number(rect.top || 0),
2359
+ right: Number(rect.right || 0),
2360
+ bottom: Number(rect.bottom || 0),
2361
+ width: Number(rect.width || 0),
2362
+ height: Number(rect.height || 0)
2363
+ };
2364
+ }
2365
+
2366
+ function triggerSelectionAction(action, selection) {
2367
+ enqueueHostEvent({
2368
+ kind: "appAction",
2369
+ appId: action.appId,
2370
+ type: action.type,
2371
+ payload: selectionPayloadForAction(action, selection)
2372
+ });
2373
+ hideSelectionMenu();
2374
+ }
2375
+
2376
+ function nativeSelectionToolbar() {
2377
+ const candidates = Array.from(document.querySelectorAll("div,section,nav,menu,[role='toolbar'],[role='menu']"))
2378
+ .filter((el) => el instanceof HTMLElement && !isHostUiElement(el))
2379
+ .map((el) => {
2380
+ const text = (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim();
2381
+ const rect = el.getBoundingClientRect();
2382
+ const buttons = Array.from(el.querySelectorAll("button,[role='button']"));
2383
+ const hasNativeSelectionAction = buttons.some((button) => /\bAdd to chat\b/i.test(button.textContent || "")) ||
2384
+ buttons.some((button) => /\bAsk in side chat\b/i.test(button.textContent || "")) ||
2385
+ /\bAdd to chat\b/i.test(text) ||
2386
+ /\bAsk in side chat\b/i.test(text);
2387
+ const score = (/\bAdd to chat\b/i.test(text) ? 10 : 0) +
2388
+ (/\bAsk in side chat\b/i.test(text) ? 10 : 0) +
2389
+ (buttons.length >= 2 ? 2 : 0) +
2390
+ (rect.width > 160 && rect.height > 28 ? 2 : 0);
2391
+ return { el, text, rect, score, hasNativeSelectionAction };
2392
+ })
2393
+ .filter((candidate) =>
2394
+ candidate.hasNativeSelectionAction &&
2395
+ candidate.score >= 10 &&
2396
+ candidate.rect.width > 0 &&
2397
+ candidate.rect.height > 0 &&
2398
+ candidate.rect.width <= 640 &&
2399
+ candidate.rect.height <= 120 &&
2400
+ candidate.rect.top >= 0 &&
2401
+ candidate.rect.bottom <= window.innerHeight
2402
+ )
2403
+ .sort((a, b) => b.score - a.score || a.rect.top - b.rect.top);
2404
+ return candidates[0]?.el || null;
2405
+ }
2406
+
2407
+ function nativeSelectionToolbarButtons(toolbar) {
2408
+ return Array.from(toolbar?.querySelectorAll?.("button,[role='button']") || [])
2409
+ .filter((button) => /\bAdd to chat\b/i.test(button.textContent || "") || /\bAsk in side chat\b/i.test(button.textContent || ""));
2410
+ }
2411
+
2412
+ function nativeSelectionInsertionTarget(toolbar) {
2413
+ const buttons = nativeSelectionToolbarButtons(toolbar);
2414
+ const lastButton = buttons[buttons.length - 1];
2415
+ const parent = lastButton?.parentElement;
2416
+ if (parent instanceof HTMLElement && toolbar.contains(parent)) return parent;
2417
+ return toolbar;
2418
+ }
2419
+
2420
+ function extendNativeSelectionToolbar(toolbar, target) {
2421
+ if (!(toolbar instanceof HTMLElement)) return;
2422
+ toolbar.dataset.onewhackNativeSelectionToolbar = "true";
2423
+ toolbar.style.setProperty("display", "inline-flex", "important");
2424
+ toolbar.style.setProperty("align-items", "center", "important");
2425
+ toolbar.style.setProperty("flex-wrap", "nowrap", "important");
2426
+ toolbar.style.setProperty("width", "max-content", "important");
2427
+ toolbar.style.setProperty("max-width", "calc(100vw - 24px)", "important");
2428
+ toolbar.style.setProperty("height", "auto", "important");
2429
+ toolbar.style.setProperty("overflow", "visible", "important");
2430
+ if (target instanceof HTMLElement) {
2431
+ target.style.setProperty("display", "inline-flex", "important");
2432
+ target.style.setProperty("align-items", "center", "important");
2433
+ target.style.setProperty("flex-wrap", "nowrap", "important");
2434
+ target.style.setProperty("width", "max-content", "important");
2435
+ target.style.setProperty("max-width", "calc(100vw - 24px)", "important");
2436
+ }
2437
+ }
2438
+
2439
+ function nativeSelectionReferenceButton(toolbar) {
2440
+ const buttons = nativeSelectionToolbarButtons(toolbar);
2441
+ return buttons[buttons.length - 1] || null;
2442
+ }
2443
+
2444
+ function nativeSelectionButtonLabel(action) {
2445
+ const label = action.label || action.type;
2446
+ if (normalizedActionLabel(label) === "add note") return "📝 Add note";
2447
+ return label;
2448
+ }
2449
+
2450
+ function stylePropertyValue(style, property) {
2451
+ const direct = style?.getPropertyValue?.(property);
2452
+ if (direct) return direct;
2453
+ const camel = property.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
2454
+ return style?.[property] || style?.[camel] || "";
2455
+ }
2456
+
2457
+ function createNativeSelectionButton(action, selection, referenceButton = null) {
2458
+ const button = document.createElement("button");
2459
+ let activated = false;
2460
+ const activate = (event) => {
2461
+ event.preventDefault();
2462
+ event.stopPropagation();
2463
+ if (activated) return;
2464
+ activated = true;
2465
+ triggerSelectionAction(action, selection);
2466
+ };
2467
+ button.type = "button";
2468
+ button.dataset.onewhackNativeSelectionAction = "true";
2469
+ button.textContent = nativeSelectionButtonLabel(action);
2470
+ button.title = action.label || action.type;
2471
+ button.style.cssText = [
2472
+ "display:inline-flex",
2473
+ "align-items:center",
2474
+ "gap:6px",
2475
+ "min-height:28px",
2476
+ "appearance:none",
2477
+ "border:0",
2478
+ "background:transparent",
2479
+ "color:inherit",
2480
+ "font:inherit",
2481
+ "padding:0 2px",
2482
+ "white-space:nowrap",
2483
+ "pointer-events:auto",
2484
+ "cursor:pointer"
2485
+ ].join(";");
2486
+ if (referenceButton instanceof HTMLElement) {
2487
+ const referenceStyle = getComputedStyle(referenceButton);
2488
+ for (const property of ["font-family", "font-size", "font-weight", "font-style", "line-height", "letter-spacing"]) {
2489
+ const value = stylePropertyValue(referenceStyle, property);
2490
+ if (value) button.style.setProperty(property, value, "important");
2491
+ }
2492
+ }
2493
+ button.addEventListener("pointerdown", activate);
2494
+ button.addEventListener("mousedown", activate);
2495
+ button.addEventListener("click", activate);
2496
+ return button;
2497
+ }
2498
+
2499
+ function normalizedActionLabel(value) {
2500
+ return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
2501
+ }
2502
+
2503
+ function injectNativeSelectionActions(selection, actions) {
2504
+ const toolbar = nativeSelectionToolbar();
2505
+ if (!toolbar) return false;
2506
+ document.querySelectorAll("[data-onewhack-native-selection-action='true']").forEach((node) => node.remove());
2507
+ const target = nativeSelectionInsertionTarget(toolbar);
2508
+ extendNativeSelectionToolbar(toolbar, target);
2509
+ const referenceButton = nativeSelectionReferenceButton(toolbar);
2510
+ const nativeLabels = new Set(
2511
+ Array.from(toolbar.querySelectorAll("button,[role='button']"))
2512
+ .map((button) => normalizedActionLabel(button.innerText || button.textContent))
2513
+ .filter(Boolean)
2514
+ );
2515
+ for (const action of actions) {
2516
+ if (nativeLabels.has(normalizedActionLabel(action.label || action.type))) continue;
2517
+ target.appendChild(createNativeSelectionButton(action, selection, referenceButton));
2518
+ }
2519
+ return true;
2520
+ }
2521
+
2522
+ function renderSelectionMenu() {
2523
+ const menu = document.getElementById(SELECTION_MENU_ID);
2524
+ const selection = state.activeTextSelection;
2525
+ const actions = hostSelectionActions();
2526
+ if (!selection || !actions.length) {
2527
+ menu?.remove();
2528
+ document.querySelectorAll("[data-onewhack-native-selection-action='true']").forEach((node) => node.remove());
2529
+ return;
2530
+ }
2531
+ if (injectNativeSelectionActions(selection, actions)) {
2532
+ menu?.remove();
2533
+ return;
2534
+ }
2535
+ menu?.remove();
2536
+ }
2537
+
2538
+ function overlayAnchorRect(overlay) {
2539
+ if (overlay?.anchorRect) return overlay.anchorRect;
2540
+ if (state.activeTextSelection?.anchorId === overlay?.anchorId && state.activeTextSelection?.rect) return state.activeTextSelection.rect;
2541
+ const anchor = overlay?.anchorId ? findAnchor(overlay.anchorId) : null;
2542
+ return anchor?.getBoundingClientRect?.() || { left: window.innerWidth / 2 - 160, top: window.innerHeight / 2 - 80, right: window.innerWidth / 2 + 160, bottom: window.innerHeight / 2 + 80 };
2543
+ }
2544
+
2545
+ function overlayRenderSignature(overlay) {
2546
+ if (!overlay) return "";
2547
+ return JSON.stringify({
2548
+ overlayId: overlay.overlayId || "",
2549
+ appId: overlay.appId || "",
2550
+ kind: overlay.kind || "form",
2551
+ title: overlay.title || "",
2552
+ anchorId: overlay.anchorId || "",
2553
+ fields: (overlay.fields || []).map((field) => ({
2554
+ fieldId: field.fieldId || "",
2555
+ kind: field.kind || "input",
2556
+ label: field.label || ""
2557
+ })),
2558
+ actions: (overlay.actions || []).map((action) => ({
2559
+ type: action.type || "",
2560
+ label: action.label || ""
2561
+ }))
2562
+ });
2563
+ }
2564
+
2565
+ function overlayFieldValues(el) {
2566
+ return Object.fromEntries([...el.querySelectorAll("[data-onewhack-field-id]")]
2567
+ .map((input) => [input.dataset.onewhackFieldId, input.value || ""]));
2568
+ }
2569
+
2570
+ function positionOverlay(el, overlay) {
2571
+ const rect = overlayAnchorRect(overlay);
2572
+ const margin = 12;
2573
+ const gap = 10;
2574
+ const overlayRect = el.getBoundingClientRect?.() || {};
2575
+ const overlayWidth = overlayRect.width || 340;
2576
+ const overlayHeight = overlayRect.height || 168;
2577
+ const safeTop = topVisibleInset() + margin;
2578
+ const safeBottom = viewportSafeBottom() - margin;
2579
+ const maxTop = Math.max(safeTop, safeBottom - overlayHeight);
2580
+ const belowTop = rect.bottom + gap;
2581
+ const aboveTop = rect.top - overlayHeight - gap;
2582
+ const top = belowTop <= maxTop
2583
+ ? belowTop
2584
+ : aboveTop >= safeTop
2585
+ ? aboveTop
2586
+ : Math.min(maxTop, Math.max(safeTop, belowTop));
2587
+ const left = Math.min(window.innerWidth - overlayWidth - margin, Math.max(margin, rect.left));
2588
+ el.style.top = `${top}px`;
2589
+ el.style.left = `${left}px`;
2590
+ }
2591
+
2592
+ function focusOverlayField(field) {
2593
+ if (!field?.focus) return;
2594
+ try {
2595
+ field.focus({ preventScroll: true });
2596
+ } catch {
2597
+ field.focus();
2598
+ }
2599
+ }
2600
+
2601
+ function renderOverlay() {
2602
+ const el = ensureHostElement(OVERLAY_ID);
2603
+ const overlay = state.hostState?.overlay;
2604
+ if (!overlay) {
2605
+ el.classList.remove("is-visible");
2606
+ el.replaceChildren();
2607
+ state.renderedOverlaySignature = null;
2608
+ return;
2609
+ }
2610
+ const signature = overlayRenderSignature(overlay);
2611
+ if (state.renderedOverlaySignature === signature && el.classList.contains("is-visible")) {
2612
+ positionOverlay(el, overlay);
2613
+ return;
2614
+ }
2615
+ const currentValues = overlayFieldValues(el);
2616
+ const activeFieldId = document.activeElement?.dataset?.onewhackFieldId || null;
2617
+ const fields = (overlay.fields || []).map((field) => {
2618
+ const input = field.kind === "textarea" ? document.createElement("textarea") : document.createElement("input");
2619
+ input.name = field.fieldId;
2620
+ input.placeholder = field.label || field.fieldId;
2621
+ input.value = currentValues[field.fieldId] ?? field.value ?? "";
2622
+ input.dataset.onewhackFieldId = field.fieldId;
2623
+ return input;
2624
+ });
2625
+ const titleText = String(overlay.title || "").trim();
2626
+ const firstFieldLabel = String(overlay.fields?.[0]?.label || overlay.fields?.[0]?.fieldId || "").trim();
2627
+ const title = titleText && titleText !== firstFieldLabel ? document.createElement("strong") : null;
2628
+ if (title) title.textContent = titleText;
2629
+ const actions = document.createElement("div");
2630
+ actions.className = "onewhack-overlay-actions";
2631
+ for (const action of overlay.actions || []) {
2632
+ const button = document.createElement("button");
2633
+ button.type = "button";
2634
+ button.textContent = action.label || "Apply";
2635
+ button.addEventListener("click", () => {
2636
+ const values = Object.fromEntries([...el.querySelectorAll("[data-onewhack-field-id]")]
2637
+ .map((input) => [input.dataset.onewhackFieldId, input.value || ""]));
2638
+ enqueueHostEvent({
2639
+ kind: "appAction",
2640
+ appId: overlay.appId,
2641
+ type: action.type,
2642
+ payload: {
2643
+ ...(action.payload || {}),
2644
+ ...values,
2645
+ overlayId: overlay.overlayId
2646
+ }
2647
+ });
2648
+ });
2649
+ actions.appendChild(button);
2650
+ }
2651
+ el.replaceChildren(...(title ? [title] : []), ...fields, actions);
2652
+ state.renderedOverlaySignature = signature;
2653
+ el.classList.add("is-visible");
2654
+ positionOverlay(el, overlay);
2655
+ const activeField = activeFieldId
2656
+ ? [...el.querySelectorAll("[data-onewhack-field-id]")].find((field) => field.dataset.onewhackFieldId === activeFieldId)
2657
+ : null;
2658
+ const firstField = fields.find((field) => field instanceof HTMLTextAreaElement || field instanceof HTMLInputElement);
2659
+ setTimeout(() => focusOverlayField(activeField || firstField), 0);
2660
+ }
2661
+
2662
+ function composerCandidates() {
2663
+ const selectors = "textarea, input, [contenteditable='true'], [role='textbox']";
2664
+ return Array.from(document.querySelectorAll(selectors))
2665
+ .filter((el) => el instanceof HTMLElement && !isHostUiElement(el))
2666
+ .map((input) => {
2667
+ const inputRect = input.getBoundingClientRect();
2668
+ if (inputRect.width < 180 || inputRect.height < 16) return null;
2669
+ if (inputRect.bottom < window.innerHeight * 0.45) return null;
2670
+ if (inputRect.top > window.innerHeight || inputRect.bottom < 0) return null;
2671
+ if (inputRect.right < window.innerWidth * 0.35) return null;
2672
+ let anchor = input.parentElement instanceof HTMLElement ? input.parentElement : input;
2673
+ for (let node = input.parentElement; node && node !== document.body && node !== document.documentElement; node = node.parentElement) {
2674
+ const rect = node.getBoundingClientRect();
2675
+ if (rect.width < inputRect.width || rect.height < inputRect.height) continue;
2676
+ if (rect.bottom < inputRect.bottom - 4 || rect.top > inputRect.top + 4) continue;
2677
+ if (rect.height > Math.max(260, inputRect.height + 180)) break;
2678
+ if (rect.top < inputRect.top - 180) break;
2679
+ anchor = node;
2680
+ if (node.matches?.("form")) break;
2681
+ }
2682
+ const rect = anchor.getBoundingClientRect();
2683
+ return { input, anchor, rect, inputRect };
2684
+ })
2685
+ .filter(Boolean)
2686
+ .sort((a, b) =>
2687
+ b.inputRect.bottom - a.inputRect.bottom ||
2688
+ b.inputRect.width - a.inputRect.width ||
2689
+ (b.rect.width * b.rect.height) - (a.rect.width * a.rect.height)
2690
+ );
2691
+ }
2692
+
2693
+ function composerAnchorElement() {
2694
+ const candidates = composerCandidates();
2695
+ return candidates[0]?.anchor || null;
2696
+ }
2697
+
2698
+ function composerInputElement() {
2699
+ const candidates = composerCandidates();
2700
+ return candidates[0]?.input || null;
2701
+ }
2702
+
2703
+ function positionComposerContextChips(el) {
2704
+ const margin = 12;
2705
+ const anchor = composerAnchorElement();
2706
+ const chipRect = el.getBoundingClientRect?.() || {};
2707
+ const chipHeight = chipRect.height || 32;
2708
+ if (!anchor) {
2709
+ if (el.parentElement !== document.documentElement) document.documentElement.appendChild(el);
2710
+ el.style.setProperty("position", "fixed", "important");
2711
+ el.style.left = "42px";
2712
+ el.style.right = "42px";
2713
+ el.style.top = "auto";
2714
+ el.style.bottom = "78px";
2715
+ el.style.maxWidth = "calc(100vw - 84px)";
2716
+ return;
2717
+ }
2718
+ if (el.parentElement !== anchor) anchor.appendChild(el);
2719
+ if (getComputedStyle(anchor).position === "static") anchor.style.position = "relative";
2720
+ const rect = anchor.getBoundingClientRect();
2721
+ const maxWidth = Math.max(160, Math.min(rect.width - 16, window.innerWidth - (margin * 2)));
2722
+ const top = rect.height >= chipHeight + 44 ? 8 : Math.max(-chipHeight - 8, margin - rect.top);
2723
+ el.style.setProperty("position", "absolute", "important");
2724
+ el.style.left = "8px";
2725
+ el.style.right = "auto";
2726
+ el.style.top = `${top}px`;
2727
+ el.style.bottom = "auto";
2728
+ el.style.maxWidth = `${maxWidth}px`;
2729
+ }
2730
+
2731
+ function renderComposerContextChips() {
2732
+ const el = ensureHostElement(COMPOSER_CHIPS_ID);
2733
+ const items = activeComposerSubmitItems();
2734
+ if (!items.length) {
2735
+ el.classList.remove("is-visible");
2736
+ el.replaceChildren();
2737
+ return;
2738
+ }
2739
+ el.replaceChildren(...items.map((item) => {
2740
+ const chip = document.createElement("div");
2741
+ chip.className = "onewhack-context-chip";
2742
+ const label = document.createElement("span");
2743
+ label.textContent = item.label || item.body || item.itemId;
2744
+ chip.title = item.body || item.label || item.itemId;
2745
+ chip.addEventListener("click", () => {
2746
+ if (item.kind === "runtime-file") return;
2747
+ enqueueHostEvent({
2748
+ kind: item.range ? "highlightRange" : "highlightAnchor",
2749
+ anchorId: item.anchorId,
2750
+ range: item.range
2751
+ });
2752
+ });
2753
+ const remove = document.createElement("button");
2754
+ remove.type = "button";
2755
+ remove.textContent = "x";
2756
+ remove.title = "Remove context";
2757
+ remove.addEventListener("click", (event) => {
2758
+ event.stopPropagation();
2759
+ if (item.kind === "runtime-file") {
2760
+ enqueueHostEvent({ kind: "composerAttachmentRemove", path: item.path });
2761
+ } else {
2762
+ enqueueHostEvent({ kind: "composerContextRemove", itemId: item.itemId });
2763
+ }
2764
+ });
2765
+ chip.append(label, remove);
2766
+ return chip;
2767
+ }));
2768
+ el.classList.add("is-visible");
2769
+ positionComposerContextChips(el);
2770
+ }
2771
+
2772
+ function activeComposerContextItems() {
2773
+ return (state.hostState?.composer?.contextItems || []).filter((item) => item?.status !== "resolved" && item?.status !== "sent");
2774
+ }
2775
+
2776
+ function activeComposerAttachmentItems() {
2777
+ return (state.hostState?.composer?.attachments || [])
2778
+ .filter((item) => item?.status !== "sent" && item?.body)
2779
+ .map((item) => ({
2780
+ ...item,
2781
+ itemId: item.fileId || item.path,
2782
+ label: item.name || item.relativePath || item.path,
2783
+ body: item.body,
2784
+ kind: "runtime-file"
2785
+ }));
2786
+ }
2787
+
2788
+ function activeComposerSubmitItems() {
2789
+ return [
2790
+ ...activeComposerContextItems().map((item) => ({ ...item, kind: item.kind || "context-item" })),
2791
+ ...activeComposerAttachmentItems()
2792
+ ];
2793
+ }
2794
+
2795
+ function composerText(el) {
2796
+ if (!el) return "";
2797
+ return "value" in el ? String(el.value || "") : String(el.innerText || el.textContent || "");
2798
+ }
2799
+
2800
+ function setComposerText(el, value) {
2801
+ if (!el) return false;
2802
+ try {
2803
+ el.focus({ preventScroll: true });
2804
+ } catch {
2805
+ el.focus?.();
2806
+ }
2807
+ if ("value" in el) {
2808
+ const setter = Object.getOwnPropertyDescriptor(el.constructor?.prototype || HTMLTextAreaElement.prototype, "value")?.set;
2809
+ if (setter) setter.call(el, value);
2810
+ else el.value = value;
2811
+ el.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
2812
+ el.dispatchEvent(new Event("change", { bubbles: true }));
2813
+ return true;
2814
+ }
2815
+ el.textContent = value;
2816
+ el.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
2817
+ return true;
2818
+ }
2819
+
2820
+ function oneWhackSubmitPrompt(submission) {
2821
+ const lines = [
2822
+ `[[ONEWHACK_CONTEXT:${submission.submissionId}]]`,
2823
+ "Use these OneWhack sticky notes as context. Do not mention the marker."
2824
+ ];
2825
+ for (const [index, item] of submission.items.entries()) {
2826
+ const selected = item.range?.quote || item.range?.text || item.label || "";
2827
+ if (item.kind === "runtime-file") {
2828
+ lines.push(
2829
+ "",
2830
+ `${index + 1}. Attached file: ${selected}`,
2831
+ item.path ? `Path: ${item.path}` : "",
2832
+ "",
2833
+ item.body || selected
2834
+ );
2835
+ } else {
2836
+ lines.push(
2837
+ "",
2838
+ `${index + 1}. Highlighted: ${selected}`,
2839
+ `Note: ${item.body || selected}`
2840
+ );
2841
+ }
2842
+ }
2843
+ lines.push(
2844
+ `[[/ONEWHACK_CONTEXT:${submission.submissionId}]]`,
2845
+ "",
2846
+ submission.userText || "Use the OneWhack context above."
2847
+ );
2848
+ return lines.join("\n");
2849
+ }
2850
+
2851
+ function submittedChipRow(submission) {
2852
+ const row = document.createElement("div");
2853
+ row.className = "onewhack-submitted-context-row";
2854
+ row.style.cssText = [
2855
+ "display:flex",
2856
+ "flex-wrap:wrap",
2857
+ "gap:7px",
2858
+ "margin:0 0 7px 0"
2859
+ ].join(";");
2860
+ for (const item of submission.items) {
2861
+ const chip = document.createElement("span");
2862
+ chip.className = "onewhack-context-chip";
2863
+ chip.textContent = item.label || item.body || item.itemId;
2864
+ chip.title = item.body || item.label || item.itemId;
2865
+ row.appendChild(chip);
2866
+ }
2867
+ return row;
2868
+ }
2869
+
2870
+ function maskSubmittedComposerContexts() {
2871
+ if (!state.submittedComposerSubmissions.length) return;
2872
+ for (const submission of state.submittedComposerSubmissions) {
2873
+ const marker = `[[ONEWHACK_CONTEXT:${submission.submissionId}]]`;
2874
+ const candidates = Array.from(document.querySelectorAll("main *, [role='main'] *, body *"))
2875
+ .filter((el) => el instanceof HTMLElement && !isHostUiElement(el))
2876
+ .filter((el) => (el.innerText || el.textContent || "").includes(marker))
2877
+ .filter((el) => !Array.from(el.children || []).some((child) => (child.innerText || child.textContent || "").includes(marker)))
2878
+ .sort((a, b) => (a.textContent || "").length - (b.textContent || "").length);
2879
+ const target = candidates[0];
2880
+ if (!target || target.dataset.onewhackMaskedSubmission === submission.submissionId) continue;
2881
+ target.dataset.onewhackMaskedSubmission = submission.submissionId;
2882
+ const prompt = document.createElement("span");
2883
+ prompt.textContent = submission.userText || "";
2884
+ target.replaceChildren(submittedChipRow(submission), prompt);
2885
+ }
2886
+ }
2887
+
2888
+ function scheduleSubmittedContextMask() {
2889
+ for (const delayMs of [50, 150, 350, 800, 1600, 3000]) {
2890
+ const timer = setTimeout(maskSubmittedComposerContexts, delayMs);
2891
+ timer?.unref?.();
2892
+ }
2893
+ }
2894
+
2895
+ function prepareComposerContextForSubmit() {
2896
+ const items = activeComposerSubmitItems();
2897
+ if (!items.length) return false;
2898
+ const input = composerInputElement();
2899
+ if (!input) return false;
2900
+ const userText = composerText(input).trim();
2901
+ const submission = {
2902
+ submissionId: `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2903
+ userText,
2904
+ items: items.map((item) => ({ ...item })),
2905
+ submittedAt: Date.now()
2906
+ };
2907
+ const nextText = oneWhackSubmitPrompt(submission);
2908
+ if (!setComposerText(input, nextText)) return false;
2909
+ state.pendingComposerSubmission = submission;
2910
+ state.submittedComposerSubmissions = [...state.submittedComposerSubmissions, submission].slice(-8);
2911
+ enqueueHostEvent({
2912
+ kind: "composerContextSubmitted",
2913
+ itemIds: items.filter((item) => item.kind !== "runtime-file").map((item) => item.itemId)
2914
+ });
2915
+ enqueueHostEvent({
2916
+ kind: "composerAttachmentsSubmitted",
2917
+ paths: items.filter((item) => item.kind === "runtime-file").map((item) => item.path).filter(Boolean)
2918
+ });
2919
+ scheduleSubmittedContextMask();
2920
+ return true;
2921
+ }
2922
+
2923
+ function isComposerSubmitButton(target) {
2924
+ const button = target?.closest?.("button,[role='button']");
2925
+ if (!(button instanceof HTMLElement) || button.disabled) return false;
2926
+ if (isHostUiElement(button)) return false;
2927
+ const label = [
2928
+ button.getAttribute?.("aria-label") || "",
2929
+ button.getAttribute?.("title") || "",
2930
+ button.innerText || button.textContent || ""
2931
+ ].join(" ");
2932
+ if (/send|submit|arrow/i.test(label)) return true;
2933
+ const composer = composerAnchorElement();
2934
+ const buttonRect = button.getBoundingClientRect?.();
2935
+ const composerRect = composer?.getBoundingClientRect?.();
2936
+ return Boolean(buttonRect && composerRect &&
2937
+ buttonRect.left >= composerRect.left &&
2938
+ buttonRect.right <= composerRect.right + 8 &&
2939
+ buttonRect.top >= composerRect.top - 8 &&
2940
+ buttonRect.bottom <= composerRect.bottom + 8);
2941
+ }
2942
+
2943
+ function installComposerSubmitInterceptor() {
2944
+ if (window.__oneWhackComposerSubmitInterceptorInstalled) return;
2945
+ window.__oneWhackComposerSubmitInterceptorInstalled = true;
2946
+ window.__oneWhackComposerSubmitClickHandler = (event) => {
2947
+ if (isComposerSubmitButton(event.target)) prepareComposerContextForSubmit();
2948
+ };
2949
+ window.__oneWhackComposerSubmitKeyHandler = (event) => {
2950
+ if (event.key !== "Enter" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
2951
+ const input = composerInputElement();
2952
+ if (input && (event.target === input || input.contains?.(event.target))) prepareComposerContextForSubmit();
2953
+ };
2954
+ document.addEventListener("click", window.__oneWhackComposerSubmitClickHandler, true);
2955
+ document.addEventListener("keydown", window.__oneWhackComposerSubmitKeyHandler, true);
2956
+ }
2957
+
2958
+ function oneWhackAppUrl() {
2959
+ return String(oneWhackRuntime().getEntryUrl(VIM_NAV_APP_ID) || "").trim();
2960
+ }
2961
+
2962
+ async function ensureNativeBrowserPanel() {
2963
+ if (browserUrlInput()) return true;
2964
+ const waitForBrowserButton = async () => {
2965
+ const deadline = Date.now() + 2000;
2966
+ while (Date.now() < deadline) {
2967
+ const button = nativeBrowserButton();
2968
+ if (button) return button;
2969
+ await delay(100);
2970
+ }
2971
+ return nativeBrowserButton();
2972
+ };
2973
+ const waitForBrowserControls = async () => {
2974
+ const deadline = Date.now() + 2000;
2975
+ while (Date.now() < deadline) {
2976
+ if (browserUrlInput() || localServerButton()) return true;
2977
+ await delay(100);
2978
+ }
2979
+ return Boolean(browserUrlInput() || localServerButton());
2980
+ };
2981
+ let browserButton = nativeBrowserButton();
2982
+ if (browserButton) {
2983
+ browserButton.click();
2984
+ return waitForBrowserControls();
2985
+ }
2986
+ const toggle = toggleSidePanelButton();
2987
+ if (toggle) {
2988
+ toggle.click();
2989
+ const revealed = await waitForBrowserButton();
2990
+ if (revealed) {
2991
+ revealed.click();
2992
+ return waitForBrowserControls();
2993
+ }
2994
+ }
2995
+ return Boolean(browserUrlInput() || localServerButton());
2996
+ }
2997
+
2998
+ async function openPanel() {
2999
+ if (state.panelOpenPromise) return state.panelOpenPromise;
3000
+ state.panelOpenPromise = doOpenPanel().then((result) => {
3001
+ state.lastPanelResult = result;
3002
+ return result;
3003
+ }, (err) => {
3004
+ state.lastPanelResult = { ok: false, error: err?.message || String(err) };
3005
+ throw err;
3006
+ }).finally(() => {
3007
+ state.panelOpenPromise = null;
3008
+ });
3009
+ return state.panelOpenPromise;
3010
+ }
3011
+
3012
+ async function doOpenPanel() {
3013
+ const serverUrl = oneWhackAppUrl().replace(/\/$/, "");
3014
+ if (!serverUrl) return { ok: false, error: "OneWhack host URL missing" };
3015
+ const panelUrl = `${serverUrl}/`;
3016
+ const webviews = () => Array.from(document.querySelectorAll("webview, iframe"));
3017
+ const normalizedSrc = (el) => String(el.getAttribute("src") || "")
3018
+ .replace("localhost", "127.0.0.1")
3019
+ .replace(/\/$/, "");
3020
+ const isPanelUrl = (src) =>
3021
+ src === serverUrl || src.startsWith(`${serverUrl}/`) || src.startsWith(`${serverUrl}#`) || src.startsWith(`${serverUrl}?`);
3022
+ const isVisiblePanel = (el) => {
3023
+ if (!(el instanceof HTMLElement)) return false;
3024
+ const rect = el.getBoundingClientRect();
3025
+ const style = getComputedStyle(el);
3026
+ return rect.width > 160 &&
3027
+ rect.height > 160 &&
3028
+ rect.right > 0 &&
3029
+ rect.bottom > 0 &&
3030
+ rect.left < window.innerWidth &&
3031
+ rect.top < window.innerHeight &&
3032
+ style.display !== "none" &&
3033
+ style.visibility !== "hidden" &&
3034
+ style.opacity !== "0";
3035
+ };
3036
+ const matchingPanel = () => webviews().find((el) => isPanelUrl(normalizedSrc(el)) && isVisiblePanel(el));
3037
+ const waitForPanel = async (timeoutMs = 1200) => {
3038
+ const deadline = Date.now() + timeoutMs;
3039
+ while (Date.now() < deadline) {
3040
+ if (matchingPanel()) return true;
3041
+ await delay(100);
3042
+ }
3043
+ return Boolean(matchingPanel());
3044
+ };
3045
+ const setInputValue = (input, value) => {
3046
+ const setter = Object.getOwnPropertyDescriptor(input.constructor?.prototype || HTMLInputElement.prototype, "value")?.set;
3047
+ if (setter) setter.call(input, value);
3048
+ else input.value = value;
3049
+ input.dispatchEvent(new Event("input", { bubbles: true }));
3050
+ input.dispatchEvent(new Event("change", { bubbles: true }));
3051
+ };
3052
+ const submitUrlInput = async (mode) => {
3053
+ const input = browserUrlInput();
3054
+ if (!input) return null;
3055
+ input.focus();
3056
+ setInputValue(input, panelUrl);
3057
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));
3058
+ input.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", bubbles: true }));
3059
+ input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", bubbles: true }));
3060
+ await delay(900);
3061
+ if (await waitForPanel(900)) return { ok: true, mode };
3062
+ return null;
3063
+ };
3064
+ if (matchingPanel()) return { ok: true, mode: "already-open" };
3065
+ const navigateBlankWebview = async () => {
3066
+ const target = webviews().find((el) => {
3067
+ const src = normalizedSrc(el);
3068
+ return !src || src === "about:blank";
3069
+ });
3070
+ if (!target) return null;
3071
+ target.setAttribute("src", panelUrl);
3072
+ target.src = panelUrl;
3073
+ await delay(900);
3074
+ return matchingPanel()
3075
+ ? { ok: true, mode: "navigated-blank-webview" }
3076
+ : null;
3077
+ };
3078
+ const installPanelIframe = async () => {
3079
+ const existing = document.getElementById("onewhack-vim-nav-side-panel-frame");
3080
+ if (existing) {
3081
+ existing.setAttribute("src", panelUrl);
3082
+ await delay(300);
3083
+ return matchingPanel() ? { ok: true, mode: "injected-panel-iframe-existing" } : null;
3084
+ }
3085
+ const input = browserUrlInput();
3086
+ if (!input && !localServerButton()) return null;
3087
+ const inputRect = input?.getBoundingClientRect?.();
3088
+ const candidates = Array.from(document.querySelectorAll("[role='tabpanel'], aside, section, main, div"))
3089
+ .map((el) => ({ el, rect: el.getBoundingClientRect(), text: textOf(el) }))
3090
+ .filter(({ rect }) =>
3091
+ rect.width > 240 &&
3092
+ rect.height > 240 &&
3093
+ rect.left > window.innerWidth * 0.45 &&
3094
+ rect.top < Math.max(120, (inputRect?.top || 0) + 80) &&
3095
+ rect.bottom > window.innerHeight * 0.5
3096
+ )
3097
+ .sort((a, b) => {
3098
+ const aInput = input && a.el.contains(input) ? 1 : 0;
3099
+ const bInput = input && b.el.contains(input) ? 1 : 0;
3100
+ if (aInput !== bInput) return bInput - aInput;
3101
+ const aBrowser = /new tab|start browsing|enter a url|browser/i.test(a.text) ? 1 : 0;
3102
+ const bBrowser = /new tab|start browsing|enter a url|browser/i.test(b.text) ? 1 : 0;
3103
+ if (aBrowser !== bBrowser) return bBrowser - aBrowser;
3104
+ return (b.rect.width * b.rect.height) - (a.rect.width * a.rect.height);
3105
+ });
3106
+ const panel = candidates[0]?.el;
3107
+ if (!(panel instanceof HTMLElement)) return null;
3108
+ const panelText = textOf(panel);
3109
+ if (!input && !/new tab|start browsing|enter a url|browser/i.test(panelText)) return null;
3110
+ if (getComputedStyle(panel).position === "static") panel.style.position = "relative";
3111
+ const frame = document.createElement("iframe");
3112
+ frame.id = "onewhack-vim-nav-side-panel-frame";
3113
+ frame.src = panelUrl;
3114
+ frame.title = "Vim Nav";
3115
+ frame.setAttribute("data-onewhack-panel-frame", VIM_NAV_APP_ID);
3116
+ frame.style.cssText = [
3117
+ "position:absolute",
3118
+ "left:0",
3119
+ "right:0",
3120
+ "bottom:0",
3121
+ "top:44px",
3122
+ "width:100%",
3123
+ "height:calc(100% - 44px)",
3124
+ "border:0",
3125
+ "background:#05090c",
3126
+ "z-index:2147482500"
3127
+ ].join(";");
3128
+ panel.appendChild(frame);
3129
+ await delay(500);
3130
+ return matchingPanel() ? { ok: true, mode: "injected-panel-iframe" } : null;
3131
+ };
3132
+ await ensureNativeBrowserPanel();
3133
+ if (await waitForPanel(300)) return { ok: true, mode: "already-open" };
3134
+ const blankResult = await navigateBlankWebview();
3135
+ if (blankResult) return blankResult;
3136
+ const inputBeforeServerButton = await submitUrlInput("opened-url-input");
3137
+ if (inputBeforeServerButton) return inputBeforeServerButton;
3138
+ const serverButton = localServerButton();
3139
+ if (serverButton) {
3140
+ serverButton.click();
3141
+ await delay(900);
3142
+ if (await waitForPanel(900)) return { ok: true, mode: "opened-local-server" };
3143
+ const serverButtonResult = await navigateBlankWebview();
3144
+ if (serverButtonResult) return serverButtonResult;
3145
+ const inputAfterServerButton = await submitUrlInput("opened-url-input-after-local-server");
3146
+ if (inputAfterServerButton) return inputAfterServerButton;
3147
+ }
3148
+ const finalInputResult = await submitUrlInput("opened-url-input-final");
3149
+ if (finalInputResult) return finalInputResult;
3150
+ const finalBlankResult = await navigateBlankWebview();
3151
+ if (finalBlankResult) return finalBlankResult;
3152
+ const iframeResult = await installPanelIframe();
3153
+ if (iframeResult) return iframeResult;
3154
+ if (!browserUrlInput() && !localServerButton()) {
3155
+ return { ok: false, error: "native Browser panel controls not found", debug: panelDebug({ stage: "no-controls" }) };
3156
+ }
3157
+ return { ok: false, error: "native Browser panel did not navigate to Vim Nav", debug: panelDebug({ stage: "not-navigated" }) };
3158
+ }
3159
+
3160
+ function shouldIgnoreKey(event) {
3161
+ if (event.metaKey) return true;
3162
+ if (effectiveCtrlAlt(event)) return false;
3163
+ if (state.vimMode) return false;
3164
+ if (event.defaultPrevented) return true;
3165
+ if (event.ctrlKey || event.altKey) return true;
3166
+ const el = document.activeElement;
3167
+ if (!el) return false;
3168
+ const tag = el.tagName?.toLowerCase();
3169
+ return tag === "input" || tag === "textarea" || el.isContentEditable;
3170
+ }
3171
+
3172
+ function isModifierOnlyKey(event) {
3173
+ return event.key === "Control" ||
3174
+ event.key === "Alt" ||
3175
+ event.code === "ControlLeft" ||
3176
+ event.code === "ControlRight" ||
3177
+ event.code === "AltLeft" ||
3178
+ event.code === "AltRight";
3179
+ }
3180
+
3181
+ function updateModifierState(event, isDown) {
3182
+ if (event.key === "Control" || event.code === "ControlLeft" || event.code === "ControlRight") {
3183
+ state.ctrlDown = Boolean(isDown);
3184
+ }
3185
+ if (event.key === "Alt" || event.code === "AltLeft" || event.code === "AltRight") {
3186
+ state.altDown = Boolean(isDown);
3187
+ }
3188
+ }
3189
+
3190
+ function effectiveCtrlAlt(event) {
3191
+ return Boolean((event.ctrlKey && event.altKey) || (state.ctrlDown && state.altDown));
3192
+ }
3193
+
3194
+ function digitFromKeyEvent(event) {
3195
+ if (/^\d$/.test(event.key)) return event.key;
3196
+ const digitMatch = /^Digit(\d)$/.exec(event.code || "");
3197
+ if (digitMatch) return digitMatch[1];
3198
+ const numpadMatch = /^Numpad(\d)$/.exec(event.code || "");
3199
+ return numpadMatch ? numpadMatch[1] : null;
3200
+ }
3201
+
3202
+ function handleKey(event) {
3203
+ updateModifierState(event, true);
3204
+ state.keyEventCount += 1;
3205
+ state.lastKey = {
3206
+ key: event.key,
3207
+ code: event.code,
3208
+ ctrlKey: event.ctrlKey,
3209
+ altKey: event.altKey,
3210
+ vimMode: state.vimMode,
3211
+ at: Date.now()
3212
+ };
3213
+ if (shouldIgnoreKey(event)) return;
3214
+ const key = event.key;
3215
+ const code = event.code;
3216
+ const runCommand = (name, command) => {
3217
+ const prefixBefore = state.countPrefix;
3218
+ state.commandChain = state.commandChain
3219
+ .catch(() => {})
3220
+ .then(() => Promise.resolve().then(command))
3221
+ .then((result) => {
3222
+ state.lastCommandResult = { name, prefixBefore, result };
3223
+ ensureModeBadge();
3224
+ return result;
3225
+ }, (err) => {
3226
+ state.lastCommandResult = { name, prefixBefore, ok: false, error: err?.message || String(err) };
3227
+ ensureModeBadge();
3228
+ return null;
3229
+ });
3230
+ };
3231
+ if (effectiveCtrlAlt(event)) {
3232
+ if (isModifierOnlyKey(event)) {
3233
+ event.preventDefault();
3234
+ event.stopImmediatePropagation();
3235
+ if (!event.repeat && !state.ctrlAltToggleDown) {
3236
+ state.ctrlAltToggleDown = true;
3237
+ setVimMode(!state.vimMode);
3238
+ }
3239
+ return;
3240
+ }
3241
+ const isRelativeMoveKey = code === "KeyJ" || key === "j" || key === "ArrowDown" ||
3242
+ code === "KeyK" || key === "k" || key === "ArrowUp";
3243
+ if (!hasAppServerOrder() && key !== "?" && !isRelativeMoveKey) {
3244
+ event.preventDefault();
3245
+ event.stopImmediatePropagation();
3246
+ orderPendingResult();
3247
+ return;
3248
+ }
3249
+ if (code === "KeyJ" || key === "j" || key === "ArrowDown") {
3250
+ event.preventDefault();
3251
+ event.stopImmediatePropagation();
3252
+ runCommand("ctrl-alt-j", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(1) : jumpRelativeMounted(1)));
3253
+ } else if (code === "KeyK" || key === "k" || key === "ArrowUp") {
3254
+ event.preventDefault();
3255
+ event.stopImmediatePropagation();
3256
+ runCommand("ctrl-alt-k", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(-1) : jumpRelativeMounted(-1)));
3257
+ } else if ((code === "KeyG" && event.shiftKey) || key === "G" || key === "End") {
3258
+ event.preventDefault();
3259
+ event.stopImmediatePropagation();
3260
+ runCommand("ctrl-alt-G", () => afterIndexing(() => jumpToBottom()));
3261
+ } else if (code === "KeyG" || key === "g" || key === "Home") {
3262
+ event.preventDefault();
3263
+ event.stopImmediatePropagation();
3264
+ runCommand("ctrl-alt-g", () => afterIndexing(() => jumpToIndex(0, { block: "start", behavior: "auto" })));
3265
+ } else if (key === "[" || (code === "BracketLeft" && !event.shiftKey)) {
3266
+ event.preventDefault();
3267
+ event.stopImmediatePropagation();
3268
+ runCommand("ctrl-alt-[", () => alignCurrent("start"));
3269
+ } else if (key === "]" || (code === "BracketRight" && !event.shiftKey)) {
3270
+ event.preventDefault();
3271
+ event.stopImmediatePropagation();
3272
+ runCommand("ctrl-alt-]", () => alignCurrent("end"));
3273
+ } else if (key === "{" || (code === "BracketLeft" && event.shiftKey) || key === "PageUp") {
3274
+ event.preventDefault();
3275
+ event.stopImmediatePropagation();
3276
+ runCommand("ctrl-alt-{", () => jumpRole("user", -1));
3277
+ } else if (key === "}" || (code === "BracketRight" && event.shiftKey) || key === "PageDown") {
3278
+ event.preventDefault();
3279
+ event.stopImmediatePropagation();
3280
+ runCommand("ctrl-alt-}", () => jumpRole("user", 1));
3281
+ } else if (key === "?" || (code === "Slash" && event.shiftKey)) {
3282
+ event.preventDefault();
3283
+ event.stopImmediatePropagation();
3284
+ toggleHelp();
3285
+ }
3286
+ return;
3287
+ }
3288
+
3289
+ if (state.vimMode) {
3290
+ if (key === "Escape" || key === "i") {
3291
+ event.preventDefault();
3292
+ event.stopImmediatePropagation();
3293
+ setVimMode(false);
3294
+ return;
3295
+ }
3296
+ if (key === "?" || (code === "Slash" && event.shiftKey)) {
3297
+ event.preventDefault();
3298
+ event.stopImmediatePropagation();
3299
+ toggleHelp();
3300
+ return;
3301
+ }
3302
+ event.preventDefault();
3303
+ event.stopImmediatePropagation();
3304
+ const isRelativeMoveKey = code === "KeyJ" || key === "j" || code === "KeyK" || key === "k";
3305
+ if (!hasAppServerOrder() && !isRelativeMoveKey) {
3306
+ orderPendingResult();
3307
+ return;
3308
+ }
3309
+ }
3310
+
3311
+ if (key === "Backspace" || code === "Backspace") {
3312
+ event.preventDefault();
3313
+ event.stopImmediatePropagation();
3314
+ if (state.countPrefix) {
3315
+ state.countPrefix = state.countPrefix.slice(0, -1);
3316
+ } else if (state.pendingKeys) {
3317
+ state.pendingKeys = "";
3318
+ }
3319
+ ensureModeBadge();
3320
+ return;
3321
+ }
3322
+
3323
+ if (state.pendingKeys === "g") {
3324
+ state.pendingKeys = "";
3325
+ state.countPrefix = "";
3326
+ ensureModeBadge();
3327
+ if ((code === "KeyG" && !event.shiftKey) || key === "g") {
3328
+ event.preventDefault();
3329
+ runCommand("gg", () => afterIndexing(() => jumpToIndex(0, { block: "start", behavior: "auto" })));
3330
+ }
3331
+ return;
3332
+ }
3333
+ const digit = digitFromKeyEvent(event);
3334
+ if (digit != null) {
3335
+ event.preventDefault();
3336
+ state.countPrefix = `${state.countPrefix}${digit}`.replace(/^0+(?=\d)/, "");
3337
+ state.lastDigit = {
3338
+ digit,
3339
+ key,
3340
+ code,
3341
+ prefix: state.countPrefix,
3342
+ at: Date.now()
3343
+ };
3344
+ ensureModeBadge();
3345
+ return;
3346
+ }
3347
+
3348
+ if (code === "KeyJ" || key === "j") {
3349
+ event.preventDefault();
3350
+ state.countPrefix = "";
3351
+ ensureModeBadge();
3352
+ runCommand("j", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(1) : jumpRelativeMounted(1)));
3353
+ } else if (code === "KeyK" || key === "k") {
3354
+ event.preventDefault();
3355
+ state.countPrefix = "";
3356
+ ensureModeBadge();
3357
+ runCommand("k", () => withVisibleMountedIndex(() => hasAppServerOrder() ? jumpRelative(-1) : jumpRelativeMounted(-1)));
3358
+ } else if ((code === "KeyG" && event.shiftKey) || key === "G") {
3359
+ event.preventDefault();
3360
+ const prefix = state.countPrefix;
3361
+ state.countPrefix = "";
3362
+ ensureModeBadge();
3363
+ runCommand("G", () => afterIndexing(() => jumpToCountOrLast(prefix)));
3364
+ } else if ((code === "KeyG" && !event.shiftKey) || key === "g") {
3365
+ event.preventDefault();
3366
+ state.countPrefix = "";
3367
+ state.pendingKeys = "g";
3368
+ ensureModeBadge();
3369
+ } else if (key === "[" || (code === "BracketLeft" && !event.shiftKey)) {
3370
+ event.preventDefault();
3371
+ state.countPrefix = "";
3372
+ ensureModeBadge();
3373
+ runCommand("[", () => alignCurrent("start"));
3374
+ } else if (key === "]" || (code === "BracketRight" && !event.shiftKey)) {
3375
+ event.preventDefault();
3376
+ state.countPrefix = "";
3377
+ ensureModeBadge();
3378
+ runCommand("]", () => alignCurrent("end"));
3379
+ } else if (key === "{" || (code === "BracketLeft" && event.shiftKey)) {
3380
+ event.preventDefault();
3381
+ state.countPrefix = "";
3382
+ ensureModeBadge();
3383
+ runCommand("{", () => jumpRole("user", -1));
3384
+ } else if (key === "}" || (code === "BracketRight" && event.shiftKey)) {
3385
+ event.preventDefault();
3386
+ state.countPrefix = "";
3387
+ ensureModeBadge();
3388
+ runCommand("}", () => jumpRole("user", 1));
3389
+ }
3390
+ }
3391
+
3392
+ function handleKeyUp(event) {
3393
+ updateModifierState(event, false);
3394
+ if ((event.key === "Control" || event.key === "Alt" || !event.ctrlKey || !event.altKey) &&
3395
+ state.ctrlAltToggleDown &&
3396
+ !effectiveCtrlAlt(event)) {
3397
+ state.ctrlAltToggleDown = false;
3398
+ }
3399
+ }
3400
+
3401
+ function handlePossibleTextSelection() {
3402
+ setTimeout(() => {
3403
+ const changed = updateTextSelectionFromDom();
3404
+ if (!changed && !window.getSelection?.()?.toString?.().trim()) {
3405
+ state.activeTextSelection = null;
3406
+ hideSelectionMenu();
3407
+ }
3408
+ }, 0);
3409
+ }
3410
+
3411
+ function resetModifierState() {
3412
+ state.ctrlDown = false;
3413
+ state.altDown = false;
3414
+ state.ctrlAltToggleDown = false;
3415
+ }
3416
+
3417
+ function snapshot() {
3418
+ ensureAnnotations();
3419
+ const root = findScrollContainer();
3420
+ const items = anchors();
3421
+ if (!state.currentAnchorId && items.length) {
3422
+ const firstVisible = items.find((item) => item.visible) || items[0];
3423
+ setCurrent(firstVisible.anchorId, "adapter");
3424
+ } else if (state.currentAnchorId && state.selection?.anchorId !== state.currentAnchorId) {
3425
+ setCurrent(state.currentAnchorId, "adapter");
3426
+ }
3427
+ return {
3428
+ href: location.href,
3429
+ title: document.title,
3430
+ version: state.version,
3431
+ scroll: {
3432
+ top: Math.round(root.scrollTop),
3433
+ height: Math.round(root.scrollHeight),
3434
+ clientHeight: Math.round(root.clientHeight)
3435
+ },
3436
+ anchors: items,
3437
+ visibleCount: items.filter((item) => item.visible).length,
3438
+ annotationCount: document.querySelectorAll(`.${ANNOTATION_CLASS}`).length,
3439
+ selection: state.selection,
3440
+ vimMode: state.vimMode,
3441
+ transcriptOrderSource: state.transcriptOrderSource,
3442
+ mountedContentAnchorIds: collectAnchorIds().filter(isContentUnitId),
3443
+ unknownMountedAnchorCount: mountedUnknownContentAnchorIds().length,
3444
+ mountedTurnSearchKeys: Array.from(new Set(collectAnchorIds()
3445
+ .map((id) => contentUnitSortKey(id)?.turnSearchKey)
3446
+ .filter(Boolean))),
3447
+ panel: state.lastPanelResult,
3448
+ debug: {
3449
+ keyEventCount: state.keyEventCount,
3450
+ lastKey: state.lastKey,
3451
+ countPrefix: state.countPrefix,
3452
+ lastDigit: state.lastDigit,
3453
+ lastCommandResult: state.lastCommandResult,
3454
+ lastRelativeDebug: state.lastRelativeDebug,
3455
+ lastMountSearch: state.lastMountSearch,
3456
+ lastBottomJump: state.lastBottomJump,
3457
+ lastOrderFallback: state.lastOrderFallback,
3458
+ lastFocusTarget: state.lastFocusTarget || null
3459
+ }
3460
+ };
3461
+ }
3462
+
3463
+ window.removeEventListener("keydown", window.__codexVimNavKeyHandler, true);
3464
+ document.removeEventListener("keydown", window.__codexVimNavKeyHandler, true);
3465
+ window.removeEventListener("keyup", window.__codexVimNavKeyUpHandler, true);
3466
+ document.removeEventListener("keyup", window.__codexVimNavKeyUpHandler, true);
3467
+ document.removeEventListener("mouseup", window.__oneWhackTextSelectionHandler, true);
3468
+ window.__codexVimNavKeyHandler = handleKey;
3469
+ window.__codexVimNavKeyUpHandler = handleKeyUp;
3470
+ window.__oneWhackTextSelectionHandler = handlePossibleTextSelection;
3471
+ window.addEventListener("keydown", handleKey, true);
3472
+ document.addEventListener("keydown", handleKey, true);
3473
+ window.addEventListener("keyup", handleKeyUp, true);
3474
+ document.addEventListener("keyup", handleKeyUp, true);
3475
+ document.addEventListener("mouseup", handlePossibleTextSelection, true);
3476
+ const resizeHandler = () => { state.version += 1; scheduleAnnotationRefresh(); };
3477
+ const blurHandler = () => resetModifierState();
3478
+ const mutationObserver = new MutationObserver(() => {
3479
+ state.version += 1;
3480
+ scheduleAnnotationRefresh();
3481
+ maskSubmittedComposerContexts();
3482
+ });
3483
+ window.addEventListener("resize", resizeHandler);
3484
+ window.addEventListener("blur", blurHandler);
3485
+ mutationObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
3486
+ window.__codexVimNavCleanup = () => {
3487
+ window.removeEventListener("keydown", handleKey, true);
3488
+ document.removeEventListener("keydown", handleKey, true);
3489
+ window.removeEventListener("keyup", handleKeyUp, true);
3490
+ document.removeEventListener("keyup", handleKeyUp, true);
3491
+ document.removeEventListener("mouseup", handlePossibleTextSelection, true);
3492
+ document.removeEventListener("click", window.__oneWhackComposerSubmitClickHandler, true);
3493
+ document.removeEventListener("keydown", window.__oneWhackComposerSubmitKeyHandler, true);
3494
+ window.removeEventListener("resize", resizeHandler);
3495
+ window.removeEventListener("blur", blurHandler);
3496
+ mutationObserver.disconnect();
3497
+ if (annotationRefreshTimer) clearTimeout(annotationRefreshTimer);
3498
+ annotationRefreshTimer = null;
3499
+ window.__oneWhackComposerSubmitInterceptorInstalled = false;
3500
+ };
3501
+
3502
+ const bridge = {
3503
+ name: "vim-nav",
3504
+ version: BRIDGE_VERSION,
3505
+ snapshot,
3506
+ applyHostState,
3507
+ drainHostEvents,
3508
+ openPanel,
3509
+ scrollToAnchor,
3510
+ highlightAnchor,
3511
+ highlightRange: (range, options) => oneWhackRuntime().highlightRange
3512
+ ? oneWhackRuntime().highlightRange(range, options)
3513
+ : highlightAnchor(range?.anchorId, options),
3514
+ setAnnotations,
3515
+ setTranscriptOrder,
3516
+ primeAnchorOrder,
3517
+ jumpToIndex,
3518
+ jumpRelative,
3519
+ __testUpdateTextSelectionFromDom: updateTextSelectionFromDom
3520
+ };
3521
+
3522
+ oneWhackRuntime().registerApp({
3523
+ appId: VIM_NAV_APP_ID,
3524
+ capabilities: {
3525
+ transcriptRead: true,
3526
+ transcriptAnnotate: true,
3527
+ transcriptNavigate: true,
3528
+ commands: true
3529
+ },
3530
+ bridge
3531
+ });
3532
+ return primeAnchorOrder().then(() => snapshot()).catch(() => {
3533
+ freezeCurrentDomOrder({ selectCurrent: false, annotate: true });
3534
+ return snapshot();
3535
+ });
3536
+ })();