@saadnvd1/agent-os 0.3.794 → 0.3.807

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.
Files changed (167) hide show
  1. package/app/api/busy/route.ts +2 -2
  2. package/app/api/chat-views/[sessionId]/route.ts +19 -0
  3. package/app/api/chat-views/route.test.ts +79 -0
  4. package/app/api/chat-views/route.ts +8 -0
  5. package/app/api/tips/route.test.ts +56 -0
  6. package/app/api/tips/route.ts +45 -0
  7. package/app/api/updates/health/route.ts +22 -0
  8. package/app/api/updates/route.test.ts +53 -0
  9. package/app/api/updates/settings/route.ts +37 -10
  10. package/app/page.tsx +17 -14
  11. package/components/Chat/ChatPanel.tsx +74 -59
  12. package/components/Chat/NewDivider.tsx +15 -0
  13. package/components/Chat/needs-more-history.test.ts +28 -0
  14. package/components/Chat/needs-more-history.ts +14 -0
  15. package/components/Chat/session-link.ts +7 -4
  16. package/components/Chat/useFind.ts +50 -1
  17. package/components/Chat/usePosition.ts +363 -0
  18. package/components/CodeSearchDialog.tsx +90 -0
  19. package/components/CommandPalette/CommandsBody.tsx +12 -4
  20. package/components/CommandPalette/useAppCommands.ts +29 -11
  21. package/components/DevServers/ServerLogsModal.tsx +24 -21
  22. package/components/Nav/HistoryButtons.tsx +128 -0
  23. package/components/Pane/MobileTabBar.tsx +3 -0
  24. package/components/Pane/index.tsx +2 -2
  25. package/components/Recipes/TimerFields.tsx +2 -1
  26. package/components/Schedules/ScheduleForm.tsx +1 -1
  27. package/components/SessionList/useSessionSwitching.ts +3 -2
  28. package/components/Settings/SettingsDialog.tsx +1 -1
  29. package/components/Shortcuts/PendingKeys.tsx +19 -0
  30. package/components/Shortcuts/ShortcutsButton.tsx +22 -0
  31. package/components/Shortcuts/ShortcutsDialog.tsx +122 -0
  32. package/components/SidebarFooter.tsx +4 -0
  33. package/components/Terminal/hooks/useTerminalConnection.ts +4 -3
  34. package/components/Tips/TipButton.tsx +39 -0
  35. package/components/Tips/TipDialog.tsx +148 -0
  36. package/components/Tips/TipHost.tsx +14 -0
  37. package/components/Tips/TipMedia.tsx +71 -0
  38. package/components/Tips/index.ts +2 -0
  39. package/components/Updates/AutoUpdates.tsx +141 -0
  40. package/components/Updates/UpdateBanner.test.ts +39 -1
  41. package/components/Updates/UpdateBanner.tsx +31 -5
  42. package/components/Updates/UpdatesSection.tsx +24 -3
  43. package/components/views/DesktopView.tsx +7 -9
  44. package/components/views/MobileView.tsx +2 -8
  45. package/components/views/lazy.ts +2 -2
  46. package/content/tips.json +541 -0
  47. package/content/whats-new.json +56 -0
  48. package/data/chat-views/index.ts +2 -0
  49. package/data/chat-views/keys.ts +3 -0
  50. package/data/chat-views/queries.ts +34 -0
  51. package/data/sessions/pin.ts +0 -1
  52. package/data/tips/index.ts +49 -0
  53. package/data/updates/index.ts +5 -0
  54. package/hooks/useChatViewsSync.ts +12 -0
  55. package/hooks/useNavActions.ts +104 -0
  56. package/hooks/useNavKeys.ts +105 -0
  57. package/hooks/useReloadWhenUpdated.ts +5 -1
  58. package/hooks/useSessionUrl.ts +23 -5
  59. package/lib/bus/index.ts +4 -2
  60. package/lib/bus/role-alias.ts +4 -2
  61. package/lib/bus/targets.ts +2 -1
  62. package/lib/busy.ts +23 -1
  63. package/lib/chat/escape.ts +8 -4
  64. package/lib/chat-views/body.ts +41 -0
  65. package/lib/chat-views/store.ts +67 -0
  66. package/lib/chat-views/types.ts +20 -0
  67. package/lib/db/migrations/20261031T1500_chat_views.ts +30 -0
  68. package/lib/db/migrations.ts +21 -0
  69. package/lib/keybindings/focus.ts +17 -0
  70. package/lib/keybindings/keymap.ts +120 -0
  71. package/lib/keybindings/sequence.ts +64 -0
  72. package/lib/live/service.ts +1 -1
  73. package/lib/nav/history-mirror.ts +59 -0
  74. package/lib/nav/history-writer.ts +109 -0
  75. package/lib/nav/position.ts +67 -0
  76. package/lib/nav/recent.ts +34 -0
  77. package/lib/orchestrator/armed.ts +5 -0
  78. package/lib/palette/sections.ts +12 -6
  79. package/lib/recipes/timer.ts +3 -1
  80. package/lib/schedules/cron.ts +14 -5
  81. package/lib/schedules/store.ts +3 -2
  82. package/lib/security/demo.ts +2 -0
  83. package/lib/session-url.ts +11 -5
  84. package/lib/tasks/finish.ts +5 -5
  85. package/lib/tips/conditions.ts +38 -0
  86. package/lib/tips/index.ts +2 -0
  87. package/lib/tips/load.ts +26 -0
  88. package/lib/tips/media.ts +101 -0
  89. package/lib/tips/pick.ts +35 -0
  90. package/lib/tips/schema.ts +82 -0
  91. package/lib/tips/state.ts +26 -0
  92. package/lib/tips/view.ts +60 -0
  93. package/lib/updates/auto.ts +149 -0
  94. package/lib/updates/banner.ts +78 -3
  95. package/lib/updates/check.ts +64 -8
  96. package/lib/updates/history.ts +99 -0
  97. package/lib/updates/run.ts +334 -23
  98. package/lib/updates/settings.ts +66 -0
  99. package/lib/updates/view.ts +14 -3
  100. package/package.json +2 -2
  101. package/public/tips/.gitkeep +0 -0
  102. package/public/tips/chat-access-picker-dark.webp +0 -0
  103. package/public/tips/chat-access-picker.webp +0 -0
  104. package/public/tips/chat-attach-files-poster.webp +0 -0
  105. package/public/tips/chat-attach-files.mp4 +0 -0
  106. package/public/tips/chat-bookmark-dark.webp +0 -0
  107. package/public/tips/chat-bookmark.webp +0 -0
  108. package/public/tips/chat-plan-mode-dark.webp +0 -0
  109. package/public/tips/chat-plan-mode.webp +0 -0
  110. package/public/tips/chat-search-poster.webp +0 -0
  111. package/public/tips/chat-search.mp4 +0 -0
  112. package/public/tips/chat-slash-menu-poster.webp +0 -0
  113. package/public/tips/chat-slash-menu.mp4 +0 -0
  114. package/public/tips/chat-tab-suggestion-poster.webp +0 -0
  115. package/public/tips/chat-tab-suggestion.mp4 +0 -0
  116. package/public/tips/chat-terminal-handoff-poster.webp +0 -0
  117. package/public/tips/chat-terminal-handoff.mp4 +0 -0
  118. package/public/tips/chat-undo-dark.webp +0 -0
  119. package/public/tips/chat-undo.webp +0 -0
  120. package/public/tips/export-ai-dark.webp +0 -0
  121. package/public/tips/export-ai.webp +0 -0
  122. package/public/tips/net-messages-dark.webp +0 -0
  123. package/public/tips/net-messages.webp +0 -0
  124. package/public/tips/recipe-save-dark.webp +0 -0
  125. package/public/tips/recipe-save.webp +0 -0
  126. package/public/tips/schedule-first-dark.webp +0 -0
  127. package/public/tips/schedule-first.webp +0 -0
  128. package/public/tips/share-check-dark.webp +0 -0
  129. package/public/tips/share-check.webp +0 -0
  130. package/public/tips/sidebar-draft-poster.webp +0 -0
  131. package/public/tips/sidebar-draft.mp4 +0 -0
  132. package/public/tips/sidebar-groups-dark.webp +0 -0
  133. package/public/tips/sidebar-groups.webp +0 -0
  134. package/public/tips/sidebar-load-dark.webp +0 -0
  135. package/public/tips/sidebar-load.webp +0 -0
  136. package/public/tips/sidebar-snooze-dark.webp +0 -0
  137. package/public/tips/sidebar-snooze.webp +0 -0
  138. package/public/tips/tasks-plan-pieces-dark.webp +0 -0
  139. package/public/tips/tasks-plan-pieces.webp +0 -0
  140. package/public/tips/tasks-sign-off-dark.webp +0 -0
  141. package/public/tips/tasks-sign-off.webp +0 -0
  142. package/public/tips/tasks-stacks-dark.webp +0 -0
  143. package/public/tips/tasks-stacks.webp +0 -0
  144. package/scripts/autodeploy +4 -0
  145. package/scripts/autodeploy.test.ts +12 -0
  146. package/scripts/check +2 -2
  147. package/scripts/docs-links.test.ts +1 -25
  148. package/scripts/lib/commands.sh +32 -1
  149. package/scripts/lib/deps-swap.sh +8 -6
  150. package/scripts/lib/heading-slugs.ts +26 -0
  151. package/scripts/lib/update-verify.mjs +90 -0
  152. package/scripts/redeploy +31 -2
  153. package/scripts/redeploy.test.ts +94 -3
  154. package/scripts/tips-lint.test.ts +77 -0
  155. package/scripts/tips-media.ts +31 -0
  156. package/scripts/update-e2e.test.ts +215 -0
  157. package/scripts/update-recover.mjs +130 -0
  158. package/scripts/update-recover.test.ts +144 -0
  159. package/scripts/update-run.mjs +182 -40
  160. package/scripts/update-run.test.ts +208 -2
  161. package/server.ts +3 -2
  162. package/stores/chatJump.ts +2 -6
  163. package/stores/chatPositions.ts +65 -0
  164. package/stores/navHistory.ts +98 -0
  165. package/stores/shortcutsUi.ts +20 -0
  166. package/stores/tips.ts +14 -0
  167. package/components/QuickSwitcher.tsx +0 -286
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { StepCounts } from "@/lib/chat/tool-outcome";
4
4
  import {
5
+ Fragment,
5
6
  memo,
6
7
  useCallback,
7
8
  useEffect,
@@ -52,7 +53,7 @@ import { RowState, RowStore } from "./rowState";
52
53
  import { useKeybindings } from "@/hooks/useKeybindings";
53
54
  import { Background, type BackgroundValue } from "./background";
54
55
  import { FindBar } from "./FindBar";
55
- import { FIND_EVENT, isFindKey, useFind } from "./useFind";
56
+ import { useFind, useFindShortcut } from "./useFind";
56
57
  import { McpServers } from "./McpServers";
57
58
  import { ChatSessionId } from "./session";
58
59
  import { ArtifactCard } from "./Artifact";
@@ -63,7 +64,10 @@ import { ContextMeter } from "./ContextMeter";
63
64
  import { SetupCard } from "./SetupCard";
64
65
  import { UserTurn } from "./EventRow";
65
66
  import { EventGroup } from "./EventTurns";
67
+ import { needsMoreHistory } from "./needs-more-history";
66
68
  import { useGoToItem } from "./useGoToItem";
69
+ import { NewDivider } from "./NewDivider";
70
+ import { usePosition } from "./usePosition";
67
71
  import { NeedsYouStrip } from "@/components/Orchestrator/NeedsYouStrip";
68
72
  import { useResolveQuestion } from "@/data/orchestrators";
69
73
  import { needsYouFocus, needsYouFocusActions } from "@/stores/needsYouFocus";
@@ -79,7 +83,6 @@ import {
79
83
  TurnStarts,
80
84
  type BookmarkChat,
81
85
  } from "@/components/Bookmarks/context";
82
- import { bookmarksUi, bookmarksUiActions } from "@/stores/bookmarksUi";
83
86
  import {
84
87
  AssistantMessage,
85
88
  CommandOutput,
@@ -235,18 +238,23 @@ const Block = memo(
235
238
  export function TimelineRows({
236
239
  blocks,
237
240
  actions,
241
+ newFrom,
238
242
  }: {
239
243
  blocks: TimelineBlock[];
240
244
  actions: ItemActions;
245
+ // The block the reader hasn't seen yet begins at, when they were away.
246
+ newFrom?: string | null;
241
247
  }) {
242
248
  return blocks.map((b) => (
243
- <div
244
- key={blockKey(b)}
245
- data-block={blockKey(b)}
246
- className="chat-row mx-auto w-full max-w-3xl px-4 pb-4"
247
- >
248
- <Block block={b} actions={actions} />
249
- </div>
249
+ <Fragment key={blockKey(b)}>
250
+ {newFrom === blockKey(b) && <NewDivider />}
251
+ <div
252
+ data-block={blockKey(b)}
253
+ className="chat-row mx-auto w-full max-w-3xl px-4 pb-4"
254
+ >
255
+ <Block block={b} actions={actions} />
256
+ </div>
257
+ </Fragment>
250
258
  ));
251
259
  }
252
260
 
@@ -367,9 +375,17 @@ export function ChatPanel({
367
375
  if ((e.target as Element).closest?.("[data-find-bar]")) return;
368
376
  const overlay = !!document.querySelector(OVERLAY_SELECTOR);
369
377
  const ghost = !!root.querySelector(GHOST_SELECTOR);
370
- const action = escapeAction(e, escape.current.running, overlay, ghost);
378
+ const composer = (e.target as Element).closest?.(".chat-composer");
379
+ const action = escapeAction(
380
+ e,
381
+ escape.current.running,
382
+ overlay,
383
+ ghost,
384
+ !!composer
385
+ );
371
386
  if (action === "interrupt") escape.current.interrupt();
372
387
  else if (action === "dismiss") setDismissed(escape.current.suggestion);
388
+ else if (action === "blur") (e.target as HTMLElement).blur?.();
373
389
  };
374
390
  window.addEventListener("keydown", onKey, true);
375
391
  return () => window.removeEventListener("keydown", onKey, true);
@@ -510,11 +526,7 @@ export function ChatPanel({
510
526
  copySelection(e, itemsByIdRef.current),
511
527
  []
512
528
  );
513
- // Starts at the end; older pages load as the reader nears the top.
514
- useLayoutEffect(() => {
515
- const el = listRef.current;
516
- if (el) el.scrollTop = el.scrollHeight;
517
- }, [sessionId]);
529
+ // Older pages load as the reader nears the top.
518
530
  const olderRef = useRef({ hasMore, loadingOlder, loadOlder });
519
531
  useEffect(() => {
520
532
  olderRef.current = { hasMore, loadingOlder, loadOlder };
@@ -525,6 +537,33 @@ export function ChatPanel({
525
537
  if (el && o.hasMore && !o.loadingOlder && el.scrollTop < el.clientHeight)
526
538
  o.loadOlder();
527
539
  }, []);
540
+ // Fill the screen on open: a page can fold into too few rows to scroll.
541
+ // One request per item count, so a page that fails or adds nothing stops it.
542
+ const filledAt = useRef({ sessionId, count: -1 });
543
+ useLayoutEffect(() => {
544
+ const el = listRef.current;
545
+ if (filledAt.current.sessionId !== sessionId)
546
+ filledAt.current = { sessionId, count: -1 };
547
+ if (!el || filledAt.current.count === items.length) return;
548
+ if (
549
+ needsMoreHistory({
550
+ hasMore,
551
+ loading: loadingOlder,
552
+ scrollHeight: el.scrollHeight,
553
+ clientHeight: el.clientHeight,
554
+ })
555
+ ) {
556
+ filledAt.current.count = items.length;
557
+ loadOlder();
558
+ }
559
+ }, [
560
+ sessionId,
561
+ items.length,
562
+ hasMore,
563
+ loadingOlder,
564
+ loadOlder,
565
+ blocks.length,
566
+ ]);
528
567
  const scrollToBlock = useCallback(async (key: string) => {
529
568
  // The row may only just have been added.
530
569
  await new Promise((r) => requestAnimationFrame(r));
@@ -562,17 +601,20 @@ export function ChatPanel({
562
601
  () => ({ sessionId, turnStarts: turnStartStore }),
563
602
  [sessionId, turnStartStore]
564
603
  );
565
- // A bookmark opened from a list: once this chat is the one showing.
566
- const { jump } = useSnapshot(bookmarksUi);
567
- useEffect(() => {
568
- // Waits for this chat's first page: until it lands, the items are
569
- // the last chat's, and every jump would miss.
570
- if (!jump || jump.sessionId !== sessionId || loadedFor !== sessionId)
571
- return;
572
- const itemId = bookmarksUiActions.takeJump(sessionId);
573
- if (itemId)
574
- goTo((all) => (all.some((i) => i.id === itemId) ? itemId : null));
575
- }, [jump, sessionId, goTo, loadedFor]);
604
+ const { newFrom } = usePosition({
605
+ sessionId,
606
+ active,
607
+ loadedFor,
608
+ items,
609
+ hasMore,
610
+ loadingOlder,
611
+ loadOlder,
612
+ listRef,
613
+ release,
614
+ toEnd,
615
+ foldEvents: !!orchestratorOf,
616
+ goTo,
617
+ });
576
618
  const resolveQuestion = useResolveQuestion(orchestratorOf ?? "");
577
619
  // The question the next message answers, resolved when it is sent.
578
620
  const [replyTo, setReplyTo] = useState<{ id: number; text: string } | null>(
@@ -598,39 +640,8 @@ export function ChatPanel({
598
640
  scrollToBlock,
599
641
  scroller: useCallback(() => listRef.current, []),
600
642
  });
601
- const activeRef = useRef(active);
602
- useEffect(() => {
603
- activeRef.current = active;
604
- });
605
- // ⌘F (Ctrl+F off a Mac) in this chat, or the phone header's search
606
- // button, opens the find bar; pressed again, the field takes the caret.
607
- const [findFocus, setFindFocus] = useState(0);
608
- const showFind = find.show;
643
+ const findFocus = useFindShortcut(sessionId, rootRef, active, find.show);
609
644
  const closeFind = find.close;
610
- useEffect(() => {
611
- const open = () => {
612
- showFind();
613
- setFindFocus((n) => n + 1);
614
- };
615
- const onKey = (e: KeyboardEvent) => {
616
- const root = rootRef.current;
617
- const here =
618
- root?.contains(e.target as Node) ||
619
- (activeRef.current && e.target === document.body);
620
- if (!here || !isFindKey(e)) return;
621
- e.preventDefault();
622
- open();
623
- };
624
- const onFind = (e: Event) => {
625
- if ((e as CustomEvent<string>).detail === sessionId) open();
626
- };
627
- window.addEventListener("keydown", onKey, true);
628
- window.addEventListener(FIND_EVENT, onFind);
629
- return () => {
630
- window.removeEventListener("keydown", onKey, true);
631
- window.removeEventListener(FIND_EVENT, onFind);
632
- };
633
- }, [sessionId, showFind]);
634
645
  // Another conversation starts without the last one's find.
635
646
  useEffect(() => closeFind, [sessionId, closeFind]);
636
647
  const idle = !running && connected;
@@ -700,7 +711,11 @@ export function ChatPanel({
700
711
  <ChatSessionId.Provider value={sessionId}>
701
712
  <ChatIdle.Provider value={idle}>
702
713
  <RetryTurn.Provider value={retryTurn}>
703
- <TimelineRows blocks={blocks} actions={actions} />
714
+ <TimelineRows
715
+ blocks={blocks}
716
+ actions={actions}
717
+ newFrom={newFrom}
718
+ />
704
719
  </RetryTurn.Provider>
705
720
  </ChatIdle.Provider>
706
721
  </ChatSessionId.Provider>
@@ -0,0 +1,15 @@
1
+ // "New" above the first thing that arrived while the reader was away.
2
+ export function NewDivider() {
3
+ return (
4
+ <div
5
+ data-new-divider
6
+ role="separator"
7
+ aria-label="New messages"
8
+ className="text-primary mx-auto flex w-full max-w-3xl items-center gap-3 px-4 pb-3 text-xs font-medium"
9
+ >
10
+ <span className="bg-primary/40 h-px flex-1" />
11
+ New
12
+ <span className="bg-primary/40 h-px flex-1" />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { needsMoreHistory } from "./needs-more-history";
3
+
4
+ const base = {
5
+ hasMore: true,
6
+ loading: false,
7
+ scrollHeight: 300,
8
+ clientHeight: 600,
9
+ };
10
+
11
+ describe("needsMoreHistory", () => {
12
+ it("asks for more when the list is shorter than the screen", () => {
13
+ expect(needsMoreHistory(base)).toBe(true);
14
+ });
15
+ it("asks while less than one extra screen overflows", () => {
16
+ expect(needsMoreHistory({ ...base, scrollHeight: 1200 })).toBe(true);
17
+ });
18
+ it("stops once there is more than a screen of overflow", () => {
19
+ expect(needsMoreHistory({ ...base, scrollHeight: 1201 })).toBe(false);
20
+ });
21
+ it("does nothing without more history or while a page is loading", () => {
22
+ expect(needsMoreHistory({ ...base, hasMore: false })).toBe(false);
23
+ expect(needsMoreHistory({ ...base, loading: true })).toBe(false);
24
+ });
25
+ it("ignores a list that has no size yet", () => {
26
+ expect(needsMoreHistory({ ...base, clientHeight: 0 })).toBe(false);
27
+ });
28
+ });
@@ -0,0 +1,14 @@
1
+ export interface HistoryFill {
2
+ hasMore: boolean;
3
+ loading: boolean;
4
+ scrollHeight: number;
5
+ clientHeight: number;
6
+ }
7
+
8
+ // An older page can fold into a few collapsed rows, leaving a list too short
9
+ // to scroll, so no scroll event would ever ask for the next page. Keep about
10
+ // one extra screen of content above the fold.
11
+ export function needsMoreHistory(f: HistoryFill): boolean {
12
+ if (!f.hasMore || f.loading || f.clientHeight <= 0) return false;
13
+ return f.scrollHeight <= f.clientHeight * 2;
14
+ }
@@ -1,4 +1,5 @@
1
1
  import type { MouseEvent } from "react";
2
+ import { navHistoryActions } from "@/stores/navHistory";
2
3
  import {
3
4
  hrefWithRef,
4
5
  hrefWithSession,
@@ -8,7 +9,7 @@ import {
8
9
  // Opens a session through its address, the way a shared link does. The
9
10
  // address it lands on is rewritten to the session's readable one.
10
11
  export function openSessionById(id: string): void {
11
- go(hrefWithSession(location.href, { id }));
12
+ go(hrefWithSession(location.href, { id }), id);
12
13
  }
13
14
 
14
15
  export function openSessionLink(
@@ -18,10 +19,12 @@ export function openSessionLink(
18
19
  if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
19
20
  e.preventDefault();
20
21
  if (typeof target === "string") openSessionById(target);
21
- else go(hrefWithRef(location.href, target));
22
+ else
23
+ go(hrefWithRef(location.href, target), "id" in target ? target.id : null);
22
24
  }
23
25
 
24
- function go(href: string): void {
25
- window.history.pushState(null, "", href);
26
+ // A link is a chat switch: it gets a history entry, then opens as an address.
27
+ export function go(href: string, sessionId: string | null): void {
28
+ navHistoryActions.push(href, sessionId, "Chat");
26
29
  window.dispatchEvent(new PopStateEvent("popstate"));
27
30
  }
@@ -1,6 +1,13 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import {
4
+ type RefObject,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
4
11
  import type { ChatItem } from "@/lib/chat/events";
5
12
  import {
6
13
  flattenHits,
@@ -253,3 +260,45 @@ export function useFind(deps: FindDeps) {
253
260
  previous: useCallback(() => go(-1), [go]),
254
261
  };
255
262
  }
263
+
264
+ /**
265
+ * ⌘F (Ctrl+F off a Mac) in this chat, or the phone header's search button,
266
+ * opens the find bar; pressed again, the field takes the caret. Returns a
267
+ * counter that changes each time, for the bar to focus on.
268
+ */
269
+ export function useFindShortcut(
270
+ sessionId: string,
271
+ rootRef: RefObject<HTMLElement | null>,
272
+ active: boolean,
273
+ show: () => void
274
+ ): number {
275
+ const [focus, setFocus] = useState(0);
276
+ const activeRef = useRef(active);
277
+ useEffect(() => {
278
+ activeRef.current = active;
279
+ });
280
+ useEffect(() => {
281
+ const open = () => {
282
+ show();
283
+ setFocus((n) => n + 1);
284
+ };
285
+ const onKey = (e: KeyboardEvent) => {
286
+ const here =
287
+ rootRef.current?.contains(e.target as Node) ||
288
+ (activeRef.current && e.target === document.body);
289
+ if (!here || !isFindKey(e)) return;
290
+ e.preventDefault();
291
+ open();
292
+ };
293
+ const onFind = (e: Event) => {
294
+ if ((e as CustomEvent<string>).detail === sessionId) open();
295
+ };
296
+ window.addEventListener("keydown", onKey, true);
297
+ window.addEventListener(FIND_EVENT, onFind);
298
+ return () => {
299
+ window.removeEventListener("keydown", onKey, true);
300
+ window.removeEventListener(FIND_EVENT, onFind);
301
+ };
302
+ }, [sessionId, show, rootRef]);
303
+ return focus;
304
+ }