@aiti-ai/aiti-msg-nav 0.0.0-stage → 0.1.8

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,841 @@
1
+ let _ctx = null;
2
+ function __setCtx(ctx) {
3
+ _ctx = ctx;
4
+ }
5
+ function c(name) {
6
+ return _ctx?.[name] ?? (() => void 0);
7
+ }
8
+ function createElement(...args) {
9
+ return c("h").call(null, ...args);
10
+ }
11
+ function mergeKey(args) {
12
+ const [type, props, key, ...rest] = args;
13
+ if (props && typeof props === "object") {
14
+ const merged = { ...props };
15
+ if (key !== void 0 && key !== null) merged.key = key;
16
+ return [type, merged, ...rest];
17
+ }
18
+ return args;
19
+ }
20
+ const jsx = (...args) => createElement(...mergeKey(args));
21
+ const jsxs = (...args) => {
22
+ const props = args[1];
23
+ const children = props?.children;
24
+ if (Array.isArray(children)) {
25
+ for (const c2 of children) {
26
+ if (c2 && typeof c2 === "object" && c2._store) {
27
+ c2._store.validated = 1;
28
+ }
29
+ }
30
+ }
31
+ return createElement(...mergeKey(args));
32
+ };
33
+ const useState = (init) => c("useState")(init);
34
+ const useEffect = (fn, deps) => c("useEffect")(fn, deps);
35
+ const useRef = (v) => c("useRef")(v);
36
+ const useCallback = (fn, deps) => c("useCallback")(fn, deps);
37
+ function t(key, params) {
38
+ const fn = _ctx?.t;
39
+ return typeof fn === "function" ? fn(key, params) : key;
40
+ }
41
+ function getFeatures() {
42
+ const fn = _ctx?.getFeatures;
43
+ return typeof fn === "function" ? fn() : {};
44
+ }
45
+ function onFeatureChange(cb) {
46
+ const fn = _ctx?.onFeatureChange;
47
+ return typeof fn === "function" ? fn(cb) : () => {
48
+ };
49
+ }
50
+ const SCROLL_ANCHOR_THRESHOLD = 80;
51
+ const CHAT_MINIMAP_WIDTH = 4;
52
+ const TOOLBAR_W = 44;
53
+ const TOOLBAR_MIN_H = 206;
54
+ function getMsgEls(container) {
55
+ return Array.from(container.querySelectorAll("[data-chat-msg-role]"));
56
+ }
57
+ function getUserEls(container) {
58
+ return getMsgEls(container).filter((el) => el.getAttribute("data-chat-msg-role") === "user");
59
+ }
60
+ function elTopInContainer(el, c2) {
61
+ if (!el || !el.isConnected) return -1;
62
+ return el.getBoundingClientRect().top - c2.getBoundingClientRect().top + c2.scrollTop;
63
+ }
64
+ function useWakeLock(active) {
65
+ const [supported, setSupported] = useState(false);
66
+ useEffect(() => {
67
+ if (typeof navigator !== "undefined" && "wakeLock" in navigator) setSupported(true);
68
+ }, []);
69
+ const activeRef = useRef(active);
70
+ activeRef.current = active;
71
+ const sentinelRef = useRef(null);
72
+ useEffect(() => {
73
+ if (!active) return;
74
+ if (typeof navigator === "undefined" || !("wakeLock" in navigator)) return;
75
+ let disposed = false;
76
+ const acquire = async () => {
77
+ if (disposed || !activeRef.current || sentinelRef.current) return;
78
+ try {
79
+ const sentinel = await navigator.wakeLock.request("screen");
80
+ if (disposed || !activeRef.current) {
81
+ sentinel.release().catch(() => {
82
+ });
83
+ return;
84
+ }
85
+ sentinelRef.current = sentinel;
86
+ sentinel.addEventListener("release", () => {
87
+ if (sentinelRef.current === sentinel) sentinelRef.current = null;
88
+ if (activeRef.current && !disposed && document.visibilityState === "visible") acquire();
89
+ });
90
+ } catch {
91
+ }
92
+ };
93
+ acquire();
94
+ const onVisibility = () => {
95
+ if (document.visibilityState === "visible") acquire();
96
+ };
97
+ document.addEventListener("visibilitychange", onVisibility);
98
+ return () => {
99
+ disposed = true;
100
+ document.removeEventListener("visibilitychange", onVisibility);
101
+ const s = sentinelRef.current;
102
+ sentinelRef.current = null;
103
+ if (s) s.release().catch(() => {
104
+ });
105
+ };
106
+ }, [active]);
107
+ return supported;
108
+ }
109
+ const DEFAULT_POS = { align: "right", y: 12 };
110
+ function ArrowUp() {
111
+ return /* @__PURE__ */ jsx("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "18 15 12 9 6 15" }) });
112
+ }
113
+ function ArrowDown() {
114
+ return /* @__PURE__ */ jsx("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "6 9 12 15 18 9" }) });
115
+ }
116
+ function PrevUserIcon() {
117
+ return /* @__PURE__ */ jsxs("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", children: [
118
+ /* @__PURE__ */ jsx("circle", { cx: 12, cy: 5, r: 1.6 }),
119
+ /* @__PURE__ */ jsx("path", { d: "M12 8v6" }),
120
+ /* @__PURE__ */ jsx("path", { d: "M8 13h8" })
121
+ ] });
122
+ }
123
+ function NextUserIcon() {
124
+ return /* @__PURE__ */ jsxs("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", children: [
125
+ /* @__PURE__ */ jsx("circle", { cx: 12, cy: 19, r: 1.6 }),
126
+ /* @__PURE__ */ jsx("path", { d: "M12 16v-6" }),
127
+ /* @__PURE__ */ jsx("path", { d: "M8 11h8" })
128
+ ] });
129
+ }
130
+ const btnBase = {
131
+ pointerEvents: "auto",
132
+ display: "flex",
133
+ alignItems: "center",
134
+ justifyContent: "center",
135
+ width: TOOLBAR_W,
136
+ height: TOOLBAR_W,
137
+ borderRadius: "50%",
138
+ border: "1px solid var(--border)",
139
+ background: "color-mix(in srgb, var(--bg-panel) 92%, transparent)",
140
+ color: "var(--text-muted)",
141
+ cursor: "pointer",
142
+ boxShadow: "0 2px 8px rgba(15,23,42,0.18)",
143
+ transition: "color 0.12s, background 0.12s"
144
+ };
145
+ const tooltipTop = {
146
+ earliest: 17,
147
+ prevUser: 71,
148
+ nextUser: 125,
149
+ latest: 179
150
+ };
151
+ function ScrollNavToolbar(props) {
152
+ const agentRunning = props.agentRunning === true;
153
+ const isMobile = typeof props.isMobile === "boolean" ? props.isMobile : typeof window !== "undefined" && window.innerWidth < 768;
154
+ const revealHistoryRef = useRef(null);
155
+ revealHistoryRef.current = props.revealHistory ?? null;
156
+ const [, force] = useState(0);
157
+ useEffect(() => onFeatureChange(() => force((v) => v + 1)), []);
158
+ const features = getFeatures();
159
+ const showToolbar = features["show.toolbar"] !== false;
160
+ const keepAwakeFeature = features["behavior.keepAwake"] !== false;
161
+ const [pos, setPos] = useState(() => {
162
+ try {
163
+ const raw = localStorage.getItem("pi-scroll-toolbar-pos");
164
+ if (raw) {
165
+ const parsed = JSON.parse(raw);
166
+ if (typeof parsed.y === "number" && (parsed.align === "left" || parsed.align === "right")) {
167
+ return { align: parsed.align, y: parsed.y };
168
+ }
169
+ }
170
+ } catch {
171
+ }
172
+ return DEFAULT_POS;
173
+ });
174
+ const posRef = useRef(pos);
175
+ const setPosAndRef = useCallback((p) => {
176
+ posRef.current = p;
177
+ setPos(p);
178
+ }, []);
179
+ const dragRef = useRef(null);
180
+ const toolbarRef = useRef(null);
181
+ const dragModeRef = useRef(false);
182
+ const tapNavigatedRef = useRef(false);
183
+ const lastNavTargetRef = useRef(null);
184
+ const [scrollAnchors, setScrollAnchors] = useState({ atTop: true, atBottom: true });
185
+ const [scrollActive, setScrollActive] = useState(false);
186
+ const [scrollBtnsHovered, setScrollBtnsHovered] = useState(false);
187
+ const [scrollTooltip, setScrollTooltip] = useState(null);
188
+ const tooltipTimerRef = useRef(null);
189
+ const showScrollTooltip = useCallback((kind) => {
190
+ if (tooltipTimerRef.current) clearTimeout(tooltipTimerRef.current);
191
+ setScrollTooltip(kind);
192
+ tooltipTimerRef.current = setTimeout(() => setScrollTooltip(null), 1500);
193
+ }, []);
194
+ const hideScrollTooltip = useCallback(() => {
195
+ if (tooltipTimerRef.current) clearTimeout(tooltipTimerRef.current);
196
+ tooltipTimerRef.current = null;
197
+ setScrollTooltip(null);
198
+ }, []);
199
+ const scrollIdleTimerRef = useRef(null);
200
+ const tailFollowUntilRef = useRef(0);
201
+ const earliestFollowUntilRef = useRef(0);
202
+ const navSeqRef = useRef(0);
203
+ const cancelTailFollow = useCallback(() => {
204
+ tailFollowUntilRef.current = 0;
205
+ }, []);
206
+ const cancelEarliestFollow = useCallback(() => {
207
+ earliestFollowUntilRef.current = 0;
208
+ }, []);
209
+ const trackedContainerRef = useRef(null);
210
+ const trackedContentRef = useRef(null);
211
+ const roRef = useRef(null);
212
+ const recomputeTimerRef = useRef(null);
213
+ const syncFrameRef = useRef(null);
214
+ const mousePosRef = useRef({ x: -9999, y: -9999 });
215
+ const toolbarRectRef = useRef(null);
216
+ useEffect(() => {
217
+ const onMouseMove = (e) => {
218
+ mousePosRef.current = { x: e.clientX, y: e.clientY };
219
+ const r = toolbarRectRef.current;
220
+ if (!r) return;
221
+ const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
222
+ setScrollBtnsHovered(inside);
223
+ };
224
+ window.addEventListener("mousemove", onMouseMove);
225
+ return () => window.removeEventListener("mousemove", onMouseMove);
226
+ }, []);
227
+ const [keepAwakeToast, setKeepAwakeToast] = useState(false);
228
+ const keepAwakeToastTimerRef = useRef(null);
229
+ const tooltipShowCountRef = useRef(0);
230
+ const keepAwakeToastCountRef = useRef(0);
231
+ const [keepAwake, setKeepAwake] = useState(false);
232
+ const wakeLockSupported = useWakeLock(keepAwake && agentRunning);
233
+ const longPressTimerRef = useRef(null);
234
+ const didLongPressRef = useRef(false);
235
+ const clearLongPress = useCallback(() => {
236
+ if (longPressTimerRef.current) {
237
+ clearTimeout(longPressTimerRef.current);
238
+ longPressTimerRef.current = null;
239
+ }
240
+ }, []);
241
+ const onToolbarPointerDown = useCallback((e) => {
242
+ if (e.button !== 0) return;
243
+ dragRef.current = { startX: e.clientX, startY: e.clientY, startY0: pos.y, moved: false, pointerId: e.pointerId };
244
+ dragModeRef.current = true;
245
+ const onMove = (ev) => {
246
+ const drag = dragRef.current;
247
+ if (!drag || ev.pointerId !== drag.pointerId) return;
248
+ const dx = ev.clientX - drag.startX;
249
+ const dy = ev.clientY - drag.startY;
250
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 8) return;
251
+ drag.moved = true;
252
+ try {
253
+ ev.preventDefault();
254
+ } catch {
255
+ }
256
+ clearLongPress();
257
+ const el = toolbarRef.current;
258
+ if (!el) return;
259
+ const maxY = Math.max(8, window.innerHeight - el.offsetHeight - 8);
260
+ const ny = Math.min(Math.max(8, drag.startY0 + dy), maxY);
261
+ const nx = ev.clientX < window.innerWidth / 2 ? "left" : "right";
262
+ setPosAndRef({ align: nx, y: ny });
263
+ };
264
+ const onUp = () => {
265
+ const drag = dragRef.current;
266
+ if (!drag) return;
267
+ const wasDrag = drag.moved;
268
+ dragRef.current = null;
269
+ dragModeRef.current = false;
270
+ if (wasDrag) {
271
+ try {
272
+ localStorage.setItem("pi-scroll-toolbar-pos", JSON.stringify({ align: posRef.current.align, y: posRef.current.y }));
273
+ } catch {
274
+ }
275
+ }
276
+ window.removeEventListener("pointermove", onMove);
277
+ window.removeEventListener("pointerup", onUp);
278
+ window.removeEventListener("pointercancel", onUp);
279
+ };
280
+ window.addEventListener("pointermove", onMove, { passive: false });
281
+ window.addEventListener("pointerup", onUp);
282
+ window.addEventListener("pointercancel", onUp);
283
+ }, [pos.y, clearLongPress, setPosAndRef]);
284
+ const recomputeAnchors = useCallback(() => {
285
+ const c2 = trackedContainerRef.current;
286
+ if (!c2 || !c2.isConnected) return;
287
+ const atTop = c2.scrollTop <= SCROLL_ANCHOR_THRESHOLD;
288
+ const atBottom = c2.scrollHeight - c2.scrollTop - c2.clientHeight <= SCROLL_ANCHOR_THRESHOLD;
289
+ setScrollAnchors((prev) => prev.atTop === atTop && prev.atBottom === atBottom ? prev : { atTop, atBottom });
290
+ if (Date.now() < tailFollowUntilRef.current) {
291
+ const dist = c2.scrollHeight - c2.scrollTop - c2.clientHeight;
292
+ if (dist > 4) c2.scrollTo({ top: c2.scrollHeight, behavior: "auto" });
293
+ }
294
+ if (Date.now() < earliestFollowUntilRef.current) {
295
+ if (c2.scrollTop > 0) c2.scrollTo({ top: 0, behavior: "auto" });
296
+ }
297
+ }, []);
298
+ const scheduleRecompute = useCallback(() => {
299
+ if (recomputeTimerRef.current) return;
300
+ recomputeTimerRef.current = setTimeout(() => {
301
+ recomputeTimerRef.current = null;
302
+ recomputeAnchors();
303
+ }, 120);
304
+ }, [recomputeAnchors]);
305
+ const observeContainer = useCallback((c2) => {
306
+ roRef.current?.disconnect();
307
+ tailFollowUntilRef.current = 0;
308
+ earliestFollowUntilRef.current = 0;
309
+ const ro = new ResizeObserver(scheduleRecompute);
310
+ ro.observe(c2);
311
+ trackedContainerRef.current = c2;
312
+ trackedContentRef.current = null;
313
+ const content = c2.firstElementChild;
314
+ if (content) {
315
+ ro.observe(content);
316
+ trackedContentRef.current = content;
317
+ }
318
+ roRef.current = ro;
319
+ recomputeAnchors();
320
+ }, [scheduleRecompute, recomputeAnchors]);
321
+ const syncContainer = useCallback(() => {
322
+ const next = document.querySelector("[data-chat-scroll]");
323
+ if (next && next === trackedContainerRef.current) {
324
+ if (!roRef.current) {
325
+ observeContainer(next);
326
+ return;
327
+ }
328
+ const content = next.firstElementChild;
329
+ if (content && content !== trackedContentRef.current) {
330
+ trackedContentRef.current = content;
331
+ roRef.current?.observe(content);
332
+ }
333
+ return;
334
+ }
335
+ if (!next && !trackedContainerRef.current) return;
336
+ if (next) {
337
+ observeContainer(next);
338
+ } else {
339
+ roRef.current?.disconnect();
340
+ roRef.current = null;
341
+ trackedContainerRef.current = null;
342
+ trackedContentRef.current = null;
343
+ tailFollowUntilRef.current = 0;
344
+ earliestFollowUntilRef.current = 0;
345
+ setScrollAnchors((prev) => prev.atTop && prev.atBottom ? prev : { atTop: true, atBottom: true });
346
+ }
347
+ }, [observeContainer]);
348
+ const getContainer = useCallback(() => {
349
+ if (trackedContainerRef.current?.isConnected) return trackedContainerRef.current;
350
+ const next = document.querySelector("[data-chat-scroll]");
351
+ if (next && next !== trackedContainerRef.current) observeContainer(next);
352
+ return trackedContainerRef.current;
353
+ }, [observeContainer]);
354
+ const handleScrollEvent = useCallback((e) => {
355
+ if (e.target !== trackedContainerRef.current) syncContainer();
356
+ recomputeAnchors();
357
+ if (e.target === trackedContainerRef.current) {
358
+ setScrollActive(true);
359
+ if (scrollIdleTimerRef.current) clearTimeout(scrollIdleTimerRef.current);
360
+ scrollIdleTimerRef.current = setTimeout(() => setScrollActive(false), 2e3);
361
+ }
362
+ }, [recomputeAnchors, syncContainer]);
363
+ useEffect(() => {
364
+ syncContainer();
365
+ const onScroll = (e) => handleScrollEvent(e);
366
+ document.addEventListener("scroll", onScroll, { capture: true, passive: true });
367
+ const mo = new MutationObserver(() => {
368
+ if (syncFrameRef.current !== null) return;
369
+ syncFrameRef.current = requestAnimationFrame(() => {
370
+ syncFrameRef.current = null;
371
+ syncContainer();
372
+ });
373
+ });
374
+ mo.observe(document.body, { childList: true, subtree: true });
375
+ return () => {
376
+ document.removeEventListener("scroll", onScroll, { capture: true });
377
+ mo.disconnect();
378
+ roRef.current?.disconnect();
379
+ roRef.current = null;
380
+ trackedContainerRef.current = null;
381
+ trackedContentRef.current = null;
382
+ if (recomputeTimerRef.current) {
383
+ clearTimeout(recomputeTimerRef.current);
384
+ recomputeTimerRef.current = null;
385
+ }
386
+ if (syncFrameRef.current !== null) {
387
+ cancelAnimationFrame(syncFrameRef.current);
388
+ syncFrameRef.current = null;
389
+ }
390
+ };
391
+ }, [handleScrollEvent, syncContainer]);
392
+ const sessionId = props.sessionId;
393
+ const prevSessionIdRef = useRef(sessionId);
394
+ useEffect(() => {
395
+ if (prevSessionIdRef.current === sessionId) return;
396
+ prevSessionIdRef.current = sessionId;
397
+ tailFollowUntilRef.current = 0;
398
+ earliestFollowUntilRef.current = 0;
399
+ syncFrameRef.current !== null && (cancelAnimationFrame(syncFrameRef.current), syncFrameRef.current = null);
400
+ syncContainer();
401
+ }, [sessionId, syncContainer]);
402
+ useEffect(() => {
403
+ const onWheel = (e) => {
404
+ if (e.deltaY < 0) cancelTailFollow();
405
+ else if (e.deltaY > 0) cancelEarliestFollow();
406
+ };
407
+ const onTouchStart = () => {
408
+ cancelTailFollow();
409
+ cancelEarliestFollow();
410
+ };
411
+ const onKey = (e) => {
412
+ if (e.key === "ArrowUp" || e.key === "PageUp" || e.key === "Home") cancelTailFollow();
413
+ if (e.key === "ArrowDown" || e.key === "PageDown" || e.key === "End") cancelEarliestFollow();
414
+ };
415
+ window.addEventListener("wheel", onWheel, { passive: true, capture: true });
416
+ window.addEventListener("touchstart", onTouchStart, { passive: true, capture: true });
417
+ window.addEventListener("keydown", onKey, { capture: true });
418
+ return () => {
419
+ window.removeEventListener("wheel", onWheel, { capture: true });
420
+ window.removeEventListener("touchstart", onTouchStart, { capture: true });
421
+ window.removeEventListener("keydown", onKey, { capture: true });
422
+ };
423
+ }, [cancelTailFollow, cancelEarliestFollow]);
424
+ useEffect(() => () => {
425
+ if (scrollIdleTimerRef.current) clearTimeout(scrollIdleTimerRef.current);
426
+ if (tooltipTimerRef.current) clearTimeout(tooltipTimerRef.current);
427
+ if (keepAwakeToastTimerRef.current) clearTimeout(keepAwakeToastTimerRef.current);
428
+ if (longPressTimerRef.current) clearTimeout(longPressTimerRef.current);
429
+ }, []);
430
+ const showScrollButtons = (!scrollAnchors.atTop || !scrollAnchors.atBottom) && (scrollActive || !isMobile && scrollBtnsHovered);
431
+ useEffect(() => {
432
+ const el = toolbarRef.current;
433
+ if (!el) return;
434
+ const read = () => {
435
+ const r = el.getBoundingClientRect();
436
+ toolbarRectRef.current = { left: r.left, right: r.right, top: r.top, bottom: r.bottom };
437
+ const m = mousePosRef.current;
438
+ if (m.x >= r.left && m.x <= r.right && m.y >= r.top && m.y <= r.bottom) setScrollBtnsHovered(true);
439
+ };
440
+ read();
441
+ const onResize = () => {
442
+ const el2 = toolbarRef.current;
443
+ if (!el2) return;
444
+ const rr = el2.getBoundingClientRect();
445
+ toolbarRectRef.current = { left: rr.left, right: rr.right, top: rr.top, bottom: rr.bottom };
446
+ };
447
+ window.addEventListener("resize", onResize);
448
+ return () => window.removeEventListener("resize", onResize);
449
+ }, [pos.align, pos.y, isMobile, showScrollButtons]);
450
+ const scrollToEarliest = useCallback(() => {
451
+ const c2 = getContainer();
452
+ if (!c2) return;
453
+ cancelTailFollow();
454
+ navSeqRef.current += 1;
455
+ if (revealHistoryRef.current) {
456
+ earliestFollowUntilRef.current = Date.now() + 8e3;
457
+ revealHistoryRef.current();
458
+ c2.scrollTo({ top: 0, behavior: "auto" });
459
+ recomputeAnchors();
460
+ } else {
461
+ c2.scrollTo({ top: 0, behavior: "smooth" });
462
+ }
463
+ }, [getContainer, cancelTailFollow, recomputeAnchors]);
464
+ const scrollToLatest = useCallback(() => {
465
+ const c2 = getContainer();
466
+ if (!c2) return;
467
+ cancelEarliestFollow();
468
+ navSeqRef.current += 1;
469
+ tailFollowUntilRef.current = Date.now() + 2500;
470
+ c2.scrollTo({ top: c2.scrollHeight, behavior: "smooth" });
471
+ recomputeAnchors();
472
+ }, [getContainer, recomputeAnchors, cancelEarliestFollow]);
473
+ const revealActionMenu = useCallback((el) => {
474
+ try {
475
+ const menuTarget = el.firstElementChild ?? el;
476
+ if (lastNavTargetRef.current && lastNavTargetRef.current !== menuTarget) {
477
+ lastNavTargetRef.current.dispatchEvent(new MouseEvent("mouseout", { bubbles: true }));
478
+ }
479
+ menuTarget.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
480
+ lastNavTargetRef.current = menuTarget;
481
+ } catch {
482
+ }
483
+ }, []);
484
+ const scrollToUserMessageRef = useRef(null);
485
+ const prevRevealUsedRef = useRef(false);
486
+ const scrollToUserMessage = useCallback((dir) => {
487
+ const seq = ++navSeqRef.current;
488
+ const c2 = getContainer();
489
+ if (!c2) return;
490
+ cancelTailFollow();
491
+ cancelEarliestFollow();
492
+ const allEls = getMsgEls(c2);
493
+ if (allEls.length === 0) return;
494
+ const userEls = getUserEls(c2);
495
+ if (dir === -1) {
496
+ let anchor = -1;
497
+ const limit = c2.scrollTop + 8;
498
+ for (let i = 0; i < userEls.length; i++) {
499
+ const t0 = elTopInContainer(userEls[i], c2);
500
+ if (t0 < 0) continue;
501
+ if (t0 <= limit + 4) {
502
+ anchor = i;
503
+ continue;
504
+ }
505
+ if (anchor === -1) anchor = i;
506
+ break;
507
+ }
508
+ if (anchor === -1) anchor = userEls.length - 1;
509
+ const anchorEl = userEls[anchor];
510
+ const anchorTop = elTopInContainer(anchorEl, c2);
511
+ const anchorVisible = anchorTop >= c2.scrollTop;
512
+ const start = anchorVisible ? anchor - 1 : anchor;
513
+ for (let i = start; i >= 0; i--) {
514
+ const el = userEls[i];
515
+ if (!el || !el.isConnected) continue;
516
+ const t0 = elTopInContainer(el, c2);
517
+ if (t0 < 0) continue;
518
+ c2.scrollTo({ top: Math.max(0, t0 - 24), behavior: "smooth" });
519
+ revealActionMenu(el);
520
+ prevRevealUsedRef.current = false;
521
+ return;
522
+ }
523
+ } else {
524
+ let anchor = -1;
525
+ for (let i = 0; i < allEls.length; i++) {
526
+ const el = allEls[i];
527
+ if (!el || !el.isConnected) continue;
528
+ const t0 = elTopInContainer(el, c2);
529
+ if (t0 < 0) continue;
530
+ if (t0 >= c2.scrollTop) {
531
+ anchor = i;
532
+ break;
533
+ }
534
+ }
535
+ if (anchor === -1) anchor = allEls.length - 1;
536
+ for (let i = anchor + 1; i < allEls.length; i++) {
537
+ const el = allEls[i];
538
+ if (!el || !el.isConnected || el.getAttribute("data-chat-msg-role") !== "user") continue;
539
+ const t0 = elTopInContainer(el, c2);
540
+ if (t0 < 0) continue;
541
+ c2.scrollTo({ top: Math.max(0, t0 - 24), behavior: "smooth" });
542
+ revealActionMenu(el);
543
+ return;
544
+ }
545
+ const distEnd = c2.scrollHeight - c2.scrollTop - c2.clientHeight;
546
+ if (distEnd > 4) {
547
+ cancelEarliestFollow();
548
+ tailFollowUntilRef.current = Date.now() + 1500;
549
+ c2.scrollTo({ top: c2.scrollHeight, behavior: "smooth" });
550
+ recomputeAnchors();
551
+ }
552
+ }
553
+ if (dir === -1) {
554
+ if (revealHistoryRef.current && !prevRevealUsedRef.current) {
555
+ prevRevealUsedRef.current = true;
556
+ earliestFollowUntilRef.current = Date.now() + 8e3;
557
+ revealHistoryRef.current();
558
+ requestAnimationFrame(() => requestAnimationFrame(() => {
559
+ setTimeout(() => {
560
+ if (navSeqRef.current === seq) scrollToUserMessageRef.current?.(-1);
561
+ }, 60);
562
+ }));
563
+ return;
564
+ }
565
+ let attempts = 0;
566
+ const retry = async () => {
567
+ attempts += 1;
568
+ if (attempts > 3) return;
569
+ if (navSeqRef.current !== seq) return;
570
+ cancelEarliestFollow();
571
+ const c22 = getContainer();
572
+ if (!c22) return;
573
+ const h0 = c22.scrollHeight;
574
+ c22.scrollTo({ top: Math.min(c22.scrollHeight, 300), behavior: "auto" });
575
+ await new Promise((r) => setTimeout(r, 90));
576
+ c22.scrollTo({ top: 0, behavior: "auto" });
577
+ for (let i = 0; i < 20; i++) {
578
+ await new Promise((r) => setTimeout(r, 100));
579
+ if (navSeqRef.current !== seq) return;
580
+ if (i >= 2 && c22.scrollHeight !== h0) break;
581
+ }
582
+ if (getContainer()?.scrollHeight === h0) return;
583
+ setTimeout(() => {
584
+ if (navSeqRef.current === seq) scrollToUserMessageRef.current?.(-1);
585
+ }, 120);
586
+ };
587
+ retry();
588
+ }
589
+ }, [revealActionMenu, getContainer, cancelTailFollow, cancelEarliestFollow, recomputeAnchors]);
590
+ scrollToUserMessageRef.current = scrollToUserMessage;
591
+ const onHover = useCallback((kind) => (e) => {
592
+ if (tooltipShowCountRef.current < 3) {
593
+ tooltipShowCountRef.current += 1;
594
+ showScrollTooltip(kind);
595
+ }
596
+ e.currentTarget.style.background = "var(--bg-hover)";
597
+ e.currentTarget.style.color = "var(--text)";
598
+ }, [showScrollTooltip]);
599
+ const onHoverOut = useCallback((e) => {
600
+ hideScrollTooltip();
601
+ e.currentTarget.style.background = "color-mix(in srgb, var(--bg-panel) 92%, transparent)";
602
+ e.currentTarget.style.color = "var(--text-muted)";
603
+ }, [hideScrollTooltip]);
604
+ if (!showToolbar) return null;
605
+ const latestBtnStyle = {
606
+ ...btnBase,
607
+ border: keepAwake ? "1px solid color-mix(in srgb, var(--accent) 55%, var(--border))" : "1px solid var(--border)",
608
+ background: keepAwake ? "color-mix(in srgb, var(--accent) 18%, var(--bg-panel))" : "color-mix(in srgb, var(--bg-panel) 92%, transparent)",
609
+ color: keepAwake ? "var(--accent)" : "var(--text-muted)",
610
+ boxShadow: "0 2px 8px rgba(15,23,42,0.22)"
611
+ };
612
+ return /* @__PURE__ */ jsxs(
613
+ "div",
614
+ {
615
+ ref: toolbarRef,
616
+ style: {
617
+ position: "absolute",
618
+ left: pos.align === "left" ? 12 : void 0,
619
+ right: pos.align === "right" ? isMobile ? 12 : `max(${CHAT_MINIMAP_WIDTH + 12}px, calc((100% - 820px) / 2 - 12px))` : void 0,
620
+ top: pos.y,
621
+ width: TOOLBAR_W,
622
+ // hover 感知层:常驻(按钮隐藏后仍可把指针停在原位唤出);pointerEvents:none 纯传感
623
+ minHeight: TOOLBAR_MIN_H,
624
+ zIndex: 30,
625
+ pointerEvents: "none",
626
+ touchAction: "none"
627
+ },
628
+ children: [
629
+ showScrollButtons && /* @__PURE__ */ jsxs(
630
+ "div",
631
+ {
632
+ onPointerDown: onToolbarPointerDown,
633
+ style: {
634
+ display: "flex",
635
+ flexDirection: "column",
636
+ gap: 10,
637
+ position: "relative",
638
+ zIndex: 30,
639
+ pointerEvents: "auto",
640
+ cursor: dragModeRef.current ? "grabbing" : "grab",
641
+ touchAction: "none"
642
+ },
643
+ children: [
644
+ /* @__PURE__ */ jsx(
645
+ "button",
646
+ {
647
+ "aria-label": "scrollToEarliest",
648
+ onClick: scrollToEarliest,
649
+ onMouseEnter: onHover("earliest"),
650
+ onMouseLeave: onHoverOut,
651
+ style: btnBase,
652
+ children: /* @__PURE__ */ jsx(ArrowUp, {})
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsx(
656
+ "button",
657
+ {
658
+ "aria-label": "scrollToPrevUser",
659
+ onClick: () => {
660
+ if (tapNavigatedRef.current) {
661
+ tapNavigatedRef.current = false;
662
+ return;
663
+ }
664
+ scrollToUserMessage(-1);
665
+ },
666
+ onPointerUp: () => {
667
+ const drag = dragRef.current;
668
+ if (drag && !drag.moved) {
669
+ tapNavigatedRef.current = true;
670
+ scrollToUserMessage(-1);
671
+ }
672
+ },
673
+ onMouseEnter: onHover("prevUser"),
674
+ onMouseLeave: onHoverOut,
675
+ style: btnBase,
676
+ children: /* @__PURE__ */ jsx(PrevUserIcon, {})
677
+ }
678
+ ),
679
+ /* @__PURE__ */ jsx(
680
+ "button",
681
+ {
682
+ "aria-label": "scrollToNextUser",
683
+ onClick: () => {
684
+ if (tapNavigatedRef.current) {
685
+ tapNavigatedRef.current = false;
686
+ return;
687
+ }
688
+ scrollToUserMessage(1);
689
+ },
690
+ onPointerUp: () => {
691
+ const drag = dragRef.current;
692
+ if (drag && !drag.moved) {
693
+ tapNavigatedRef.current = true;
694
+ scrollToUserMessage(1);
695
+ }
696
+ },
697
+ onMouseEnter: onHover("nextUser"),
698
+ onMouseLeave: onHoverOut,
699
+ style: btnBase,
700
+ children: /* @__PURE__ */ jsx(NextUserIcon, {})
701
+ }
702
+ ),
703
+ /* @__PURE__ */ jsx(
704
+ "div",
705
+ {
706
+ style: {
707
+ pointerEvents: "auto",
708
+ display: "flex",
709
+ alignItems: "center",
710
+ justifyContent: "center",
711
+ width: 60,
712
+ height: 60,
713
+ margin: -8,
714
+ userSelect: "none",
715
+ WebkitUserSelect: "none"
716
+ },
717
+ onPointerDown: (e) => {
718
+ e.stopPropagation();
719
+ clearLongPress();
720
+ if (!keepAwakeFeature || !wakeLockSupported) return;
721
+ longPressTimerRef.current = setTimeout(() => {
722
+ didLongPressRef.current = true;
723
+ const nowOn = !keepAwake;
724
+ setKeepAwake(nowOn);
725
+ document.getSelection()?.removeAllRanges();
726
+ const prev = document.body.style.userSelect;
727
+ document.body.style.userSelect = "none";
728
+ if (nowOn && keepAwakeToastCountRef.current < 3) {
729
+ keepAwakeToastCountRef.current += 1;
730
+ setKeepAwakeToast(true);
731
+ }
732
+ if (keepAwakeToastTimerRef.current) clearTimeout(keepAwakeToastTimerRef.current);
733
+ keepAwakeToastTimerRef.current = setTimeout(() => {
734
+ setKeepAwakeToast(false);
735
+ document.body.style.userSelect = prev;
736
+ }, 2e3);
737
+ }, 600);
738
+ },
739
+ onPointerUp: clearLongPress,
740
+ onPointerCancel: clearLongPress,
741
+ onPointerLeave: clearLongPress,
742
+ onContextMenu: (e) => e.preventDefault(),
743
+ children: /* @__PURE__ */ jsx(
744
+ "button",
745
+ {
746
+ "aria-label": "scrollToLatest",
747
+ onClick: () => {
748
+ if (didLongPressRef.current) {
749
+ didLongPressRef.current = false;
750
+ return;
751
+ }
752
+ scrollToLatest();
753
+ },
754
+ onMouseEnter: (e) => {
755
+ if (tooltipShowCountRef.current < 3) {
756
+ tooltipShowCountRef.current += 1;
757
+ showScrollTooltip("latest");
758
+ }
759
+ e.currentTarget.style.background = keepAwake ? "color-mix(in srgb, var(--accent) 26%, var(--bg-panel))" : "var(--bg-hover)";
760
+ e.currentTarget.style.color = keepAwake ? "var(--accent)" : "var(--text)";
761
+ },
762
+ onMouseLeave: (e) => {
763
+ hideScrollTooltip();
764
+ e.currentTarget.style.background = keepAwake ? "color-mix(in srgb, var(--accent) 18%, var(--bg-panel))" : "color-mix(in srgb, var(--bg-panel) 92%, transparent)";
765
+ e.currentTarget.style.color = keepAwake ? "var(--accent)" : "var(--text-muted)";
766
+ },
767
+ style: latestBtnStyle,
768
+ children: /* @__PURE__ */ jsx(ArrowDown, {})
769
+ }
770
+ )
771
+ }
772
+ )
773
+ ]
774
+ }
775
+ ),
776
+ scrollTooltip && /* @__PURE__ */ jsx(
777
+ "div",
778
+ {
779
+ style: {
780
+ position: "absolute",
781
+ right: "calc(100% + 10px)",
782
+ top: tooltipTop[scrollTooltip],
783
+ whiteSpace: "nowrap",
784
+ fontSize: 12,
785
+ color: "var(--text)",
786
+ background: "color-mix(in srgb, var(--bg-panel) 96%, transparent)",
787
+ border: "1px solid var(--border)",
788
+ borderRadius: 6,
789
+ padding: "4px 8px",
790
+ boxShadow: "0 2px 10px rgba(15,23,42,0.25)",
791
+ pointerEvents: "none",
792
+ zIndex: 31
793
+ },
794
+ children: t(scrollTooltip === "latest" ? "nav.latestHint" : `nav.${scrollTooltip}`)
795
+ }
796
+ ),
797
+ keepAwakeToast && /* @__PURE__ */ jsx(
798
+ "div",
799
+ {
800
+ style: {
801
+ position: "absolute",
802
+ bottom: 12,
803
+ left: "50%",
804
+ transform: "translateX(-50%)",
805
+ zIndex: 40,
806
+ fontSize: 13,
807
+ color: "var(--text)",
808
+ background: "color-mix(in srgb, var(--bg-panel) 96%, transparent)",
809
+ border: "1px solid color-mix(in srgb, var(--accent) 50%, var(--border))",
810
+ borderRadius: 8,
811
+ padding: "6px 14px",
812
+ boxShadow: "0 2px 12px rgba(15,23,42,0.25)",
813
+ pointerEvents: "none",
814
+ textAlign: "center",
815
+ maxWidth: "min(80vw, 480px)"
816
+ },
817
+ children: t("nav.keepAwakeOn")
818
+ }
819
+ )
820
+ ]
821
+ }
822
+ );
823
+ }
824
+ function register(ctx) {
825
+ __setCtx(ctx);
826
+ ctx.registerItems?.("chatOverlay", [{
827
+ id: "msg-nav-toolbar",
828
+ render: (props) => /* @__PURE__ */ jsx(
829
+ ScrollNavToolbar,
830
+ {
831
+ agentRunning: props.agentRunning === true,
832
+ isMobile: props.isMobile === true,
833
+ revealHistory: typeof props.revealHistory === "function" ? props.revealHistory : void 0,
834
+ sessionId: props.currentSession?.id
835
+ }
836
+ )
837
+ }]);
838
+ }
839
+ export {
840
+ register as default
841
+ };