@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.
- package/app/api/busy/route.ts +2 -2
- package/app/api/chat-views/[sessionId]/route.ts +19 -0
- package/app/api/chat-views/route.test.ts +79 -0
- package/app/api/chat-views/route.ts +8 -0
- package/app/api/tips/route.test.ts +56 -0
- package/app/api/tips/route.ts +45 -0
- package/app/api/updates/health/route.ts +22 -0
- package/app/api/updates/route.test.ts +53 -0
- package/app/api/updates/settings/route.ts +37 -10
- package/app/page.tsx +17 -14
- package/components/Chat/ChatPanel.tsx +74 -59
- package/components/Chat/NewDivider.tsx +15 -0
- package/components/Chat/needs-more-history.test.ts +28 -0
- package/components/Chat/needs-more-history.ts +14 -0
- package/components/Chat/session-link.ts +7 -4
- package/components/Chat/useFind.ts +50 -1
- package/components/Chat/usePosition.ts +363 -0
- package/components/CodeSearchDialog.tsx +90 -0
- package/components/CommandPalette/CommandsBody.tsx +12 -4
- package/components/CommandPalette/useAppCommands.ts +29 -11
- package/components/DevServers/ServerLogsModal.tsx +24 -21
- package/components/Nav/HistoryButtons.tsx +128 -0
- package/components/Pane/MobileTabBar.tsx +3 -0
- package/components/Pane/index.tsx +2 -2
- package/components/Recipes/TimerFields.tsx +2 -1
- package/components/Schedules/ScheduleForm.tsx +1 -1
- package/components/SessionList/useSessionSwitching.ts +3 -2
- package/components/Settings/SettingsDialog.tsx +1 -1
- package/components/Shortcuts/PendingKeys.tsx +19 -0
- package/components/Shortcuts/ShortcutsButton.tsx +22 -0
- package/components/Shortcuts/ShortcutsDialog.tsx +122 -0
- package/components/SidebarFooter.tsx +4 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +4 -3
- package/components/Tips/TipButton.tsx +39 -0
- package/components/Tips/TipDialog.tsx +148 -0
- package/components/Tips/TipHost.tsx +14 -0
- package/components/Tips/TipMedia.tsx +71 -0
- package/components/Tips/index.ts +2 -0
- package/components/Updates/AutoUpdates.tsx +141 -0
- package/components/Updates/UpdateBanner.test.ts +39 -1
- package/components/Updates/UpdateBanner.tsx +31 -5
- package/components/Updates/UpdatesSection.tsx +24 -3
- package/components/views/DesktopView.tsx +7 -9
- package/components/views/MobileView.tsx +2 -8
- package/components/views/lazy.ts +2 -2
- package/content/tips.json +541 -0
- package/content/whats-new.json +56 -0
- package/data/chat-views/index.ts +2 -0
- package/data/chat-views/keys.ts +3 -0
- package/data/chat-views/queries.ts +34 -0
- package/data/sessions/pin.ts +0 -1
- package/data/tips/index.ts +49 -0
- package/data/updates/index.ts +5 -0
- package/hooks/useChatViewsSync.ts +12 -0
- package/hooks/useNavActions.ts +104 -0
- package/hooks/useNavKeys.ts +105 -0
- package/hooks/useReloadWhenUpdated.ts +5 -1
- package/hooks/useSessionUrl.ts +23 -5
- package/lib/bus/index.ts +4 -2
- package/lib/bus/role-alias.ts +4 -2
- package/lib/bus/targets.ts +2 -1
- package/lib/busy.ts +23 -1
- package/lib/chat/escape.ts +8 -4
- package/lib/chat-views/body.ts +41 -0
- package/lib/chat-views/store.ts +67 -0
- package/lib/chat-views/types.ts +20 -0
- package/lib/db/migrations/20261031T1500_chat_views.ts +30 -0
- package/lib/db/migrations.ts +21 -0
- package/lib/keybindings/focus.ts +17 -0
- package/lib/keybindings/keymap.ts +120 -0
- package/lib/keybindings/sequence.ts +64 -0
- package/lib/live/service.ts +1 -1
- package/lib/nav/history-mirror.ts +59 -0
- package/lib/nav/history-writer.ts +109 -0
- package/lib/nav/position.ts +67 -0
- package/lib/nav/recent.ts +34 -0
- package/lib/orchestrator/armed.ts +5 -0
- package/lib/palette/sections.ts +12 -6
- package/lib/recipes/timer.ts +3 -1
- package/lib/schedules/cron.ts +14 -5
- package/lib/schedules/store.ts +3 -2
- package/lib/security/demo.ts +2 -0
- package/lib/session-url.ts +11 -5
- package/lib/tasks/finish.ts +5 -5
- package/lib/tips/conditions.ts +38 -0
- package/lib/tips/index.ts +2 -0
- package/lib/tips/load.ts +26 -0
- package/lib/tips/media.ts +101 -0
- package/lib/tips/pick.ts +35 -0
- package/lib/tips/schema.ts +82 -0
- package/lib/tips/state.ts +26 -0
- package/lib/tips/view.ts +60 -0
- package/lib/updates/auto.ts +149 -0
- package/lib/updates/banner.ts +78 -3
- package/lib/updates/check.ts +64 -8
- package/lib/updates/history.ts +99 -0
- package/lib/updates/run.ts +334 -23
- package/lib/updates/settings.ts +66 -0
- package/lib/updates/view.ts +14 -3
- package/package.json +2 -2
- package/public/tips/.gitkeep +0 -0
- package/public/tips/chat-access-picker-dark.webp +0 -0
- package/public/tips/chat-access-picker.webp +0 -0
- package/public/tips/chat-attach-files-poster.webp +0 -0
- package/public/tips/chat-attach-files.mp4 +0 -0
- package/public/tips/chat-bookmark-dark.webp +0 -0
- package/public/tips/chat-bookmark.webp +0 -0
- package/public/tips/chat-plan-mode-dark.webp +0 -0
- package/public/tips/chat-plan-mode.webp +0 -0
- package/public/tips/chat-search-poster.webp +0 -0
- package/public/tips/chat-search.mp4 +0 -0
- package/public/tips/chat-slash-menu-poster.webp +0 -0
- package/public/tips/chat-slash-menu.mp4 +0 -0
- package/public/tips/chat-tab-suggestion-poster.webp +0 -0
- package/public/tips/chat-tab-suggestion.mp4 +0 -0
- package/public/tips/chat-terminal-handoff-poster.webp +0 -0
- package/public/tips/chat-terminal-handoff.mp4 +0 -0
- package/public/tips/chat-undo-dark.webp +0 -0
- package/public/tips/chat-undo.webp +0 -0
- package/public/tips/export-ai-dark.webp +0 -0
- package/public/tips/export-ai.webp +0 -0
- package/public/tips/net-messages-dark.webp +0 -0
- package/public/tips/net-messages.webp +0 -0
- package/public/tips/recipe-save-dark.webp +0 -0
- package/public/tips/recipe-save.webp +0 -0
- package/public/tips/schedule-first-dark.webp +0 -0
- package/public/tips/schedule-first.webp +0 -0
- package/public/tips/share-check-dark.webp +0 -0
- package/public/tips/share-check.webp +0 -0
- package/public/tips/sidebar-draft-poster.webp +0 -0
- package/public/tips/sidebar-draft.mp4 +0 -0
- package/public/tips/sidebar-groups-dark.webp +0 -0
- package/public/tips/sidebar-groups.webp +0 -0
- package/public/tips/sidebar-load-dark.webp +0 -0
- package/public/tips/sidebar-load.webp +0 -0
- package/public/tips/sidebar-snooze-dark.webp +0 -0
- package/public/tips/sidebar-snooze.webp +0 -0
- package/public/tips/tasks-plan-pieces-dark.webp +0 -0
- package/public/tips/tasks-plan-pieces.webp +0 -0
- package/public/tips/tasks-sign-off-dark.webp +0 -0
- package/public/tips/tasks-sign-off.webp +0 -0
- package/public/tips/tasks-stacks-dark.webp +0 -0
- package/public/tips/tasks-stacks.webp +0 -0
- package/scripts/autodeploy +4 -0
- package/scripts/autodeploy.test.ts +12 -0
- package/scripts/check +2 -2
- package/scripts/docs-links.test.ts +1 -25
- package/scripts/lib/commands.sh +32 -1
- package/scripts/lib/deps-swap.sh +8 -6
- package/scripts/lib/heading-slugs.ts +26 -0
- package/scripts/lib/update-verify.mjs +90 -0
- package/scripts/redeploy +31 -2
- package/scripts/redeploy.test.ts +94 -3
- package/scripts/tips-lint.test.ts +77 -0
- package/scripts/tips-media.ts +31 -0
- package/scripts/update-e2e.test.ts +215 -0
- package/scripts/update-recover.mjs +130 -0
- package/scripts/update-recover.test.ts +144 -0
- package/scripts/update-run.mjs +182 -40
- package/scripts/update-run.test.ts +208 -2
- package/server.ts +3 -2
- package/stores/chatJump.ts +2 -6
- package/stores/chatPositions.ts +65 -0
- package/stores/navHistory.ts +98 -0
- package/stores/shortcutsUi.ts +20 -0
- package/stores/tips.ts +14 -0
- 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 {
|
|
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
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
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
|
|
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
|
|
22
|
+
else
|
|
23
|
+
go(hrefWithRef(location.href, target), "id" in target ? target.id : null);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
|
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
|
+
}
|