web: keyboard shortcuts

This commit is contained in:
2026-05-02 22:45:15 -07:00
parent 4b0cc3fbf7
commit ca6b5e0807
2 changed files with 64 additions and 0 deletions

View File

@@ -40,6 +40,10 @@ Default dev URL: `http://localhost:5173`
- Composer adapts to the active item: - Composer adapts to the active item:
- Chat sends `POST /v1/chat-completions/stream` (SSE). - Chat sends `POST /v1/chat-completions/stream` (SSE).
- Search sends `POST /v1/searches/:searchId/run/stream` (SSE). - Search sends `POST /v1/searches/:searchId/run/stream` (SSE).
- Keyboard shortcuts:
- `Cmd/Ctrl+J`: start a new chat.
- `Shift+Cmd/Ctrl+J`: start a new search.
- `Cmd/Ctrl+Up/Down`: move through the sidebar list.
Client API contract docs: Client API contract docs:
- `../docs/api/rest.md` - `../docs/api/rest.md`

View File

@@ -1008,6 +1008,10 @@ export default function App() {
if (selectedSearchSummary) return `${getSearchTitle(selectedSearchSummary)} — Sybil`; if (selectedSearchSummary) return `${getSearchTitle(selectedSearchSummary)} — Sybil`;
return "Sybil"; return "Sybil";
}, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]); }, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]);
const primaryShortcutModifier = useMemo(() => {
if (typeof navigator === "undefined") return "Ctrl";
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform) ? "Cmd" : "Ctrl";
}, []);
useEffect(() => { useEffect(() => {
document.title = pageTitle; document.title = pageTitle;
@@ -1035,6 +1039,56 @@ export default function App() {
setIsMobileSidebarOpen(false); setIsMobileSidebarOpen(false);
}; };
const selectAdjacentSidebarItem = (direction: -1 | 1) => {
if (!filteredSidebarItems.length) return;
setError(null);
setContextMenu(null);
setDraftKind(null);
setIsMobileSidebarOpen(false);
setSelectedItem((current) => {
const currentIndex = current
? filteredSidebarItems.findIndex((item) => item.kind === current.kind && item.id === current.id)
: -1;
const fallbackIndex = direction > 0 ? 0 : filteredSidebarItems.length - 1;
const nextIndex =
currentIndex < 0
? fallbackIndex
: Math.min(filteredSidebarItems.length - 1, Math.max(0, currentIndex + direction));
const nextItem = filteredSidebarItems[nextIndex];
return { kind: nextItem.kind, id: nextItem.id };
});
};
useEffect(() => {
if (!isAuthenticated) return;
const handleKeyDown = (event: KeyboardEvent) => {
const hasPrimaryModifier = event.metaKey || event.ctrlKey;
if (!hasPrimaryModifier || event.altKey) return;
const key = event.key.toLowerCase();
if (key === "j") {
event.preventDefault();
if (event.shiftKey) {
handleCreateSearch();
} else {
handleCreateChat();
}
focusComposer();
return;
}
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
event.preventDefault();
selectAdjacentSidebarItem(event.key === "ArrowUp" ? -1 : 1);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [filteredSidebarItems, isAuthenticated]);
const openContextMenu = (event: MouseEvent, item: SidebarSelection) => { const openContextMenu = (event: MouseEvent, item: SidebarSelection) => {
event.preventDefault(); event.preventDefault();
const menuWidth = 160; const menuWidth = 160;
@@ -1632,10 +1686,16 @@ export default function App() {
<Button className="h-11 w-full justify-start gap-3 text-[15px]" onClick={handleCreateChat}> <Button className="h-11 w-full justify-start gap-3 text-[15px]" onClick={handleCreateChat}>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
New chat New chat
<span className="ml-auto rounded-md border border-violet-100/12 bg-white/5 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/52">
{primaryShortcutModifier} J
</span>
</Button> </Button>
<Button className="h-10 w-full justify-start gap-3" variant="secondary" onClick={handleCreateSearch}> <Button className="h-10 w-full justify-start gap-3" variant="secondary" onClick={handleCreateSearch}>
<Search className="h-4 w-4" /> <Search className="h-4 w-4" />
New search New search
<span className="ml-auto rounded-md border border-violet-100/10 bg-white/[0.035] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/44">
Shift {primaryShortcutModifier} J
</span>
</Button> </Button>
<div className="relative"> <div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-200/58" /> <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-200/58" />