diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index ea04c2fed..ced7f5ef6 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -17,6 +17,7 @@ import { isTerminalSplitShortcut, isTerminalToggleShortcut, resolveShortcutCommand, + panelNavigationShortcutData, shortcutLabelForCommand, shortcutLabelsForCommand, terminalNavigationShortcutData, @@ -513,6 +514,34 @@ describe("terminalNavigationShortcutData", () => { }); }); +describe("panelNavigationShortcutData", () => { + it("maps Ctrl+Left to the left sidebar", () => { + assert.strictEqual( + panelNavigationShortcutData(event({ key: "ArrowLeft", ctrlKey: true })), + "sidebar", + ); + }); + + it("maps Ctrl+Right to the right panel", () => { + assert.strictEqual( + panelNavigationShortcutData(event({ key: "ArrowRight", ctrlKey: true })), + "right-panel", + ); + }); + + it("ignores unsupported modifier combinations", () => { + assert.isNull(panelNavigationShortcutData(event({ key: "ArrowLeft", metaKey: true }))); + assert.isNull(panelNavigationShortcutData(event({ key: "ArrowRight", altKey: true }))); + assert.isNull(panelNavigationShortcutData(event({ key: "ArrowLeft", shiftKey: true }))); + }); + + it("ignores non-keydown events", () => { + assert.isNull( + panelNavigationShortcutData(event({ type: "keyup", key: "ArrowLeft", ctrlKey: true })), + ); + }); +}); + describe("plus key parsing", () => { it("matches the plus key shortcut", () => { const plusBindings = compile([{ shortcut: modShortcut("+"), command: "terminal.toggle" }]); diff --git a/apps/web/src/keybindings.ts b/apps/web/src/keybindings.ts index d7bf84228..4a0dd47e9 100644 --- a/apps/web/src/keybindings.ts +++ b/apps/web/src/keybindings.ts @@ -318,3 +318,26 @@ export function terminalNavigationShortcutData( return null; } + +export type PanelNavigationShortcutTarget = "sidebar" | "right-panel"; + +export function panelNavigationShortcutData( + event: ShortcutEventLike, +): PanelNavigationShortcutTarget | null { + if (event.type !== undefined && event.type !== "keydown") { + return null; + } + + if (event.metaKey || event.altKey || event.shiftKey || !event.ctrlKey) { + return null; + } + + const key = normalizeEventKey(event.key); + if (key === "arrowleft") { + return "sidebar"; + } + if (key === "arrowright") { + return "right-panel"; + } + return null; +} diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx index 882be464c..be34afc18 100644 --- a/apps/web/src/routes/_chat.$threadId.tsx +++ b/apps/web/src/routes/_chat.$threadId.tsx @@ -13,7 +13,13 @@ import { } from "react"; import { RightPanelHeader } from "~/components/RightPanelHeader"; import { WorkspacePanel } from "~/components/WorkspacePanel"; -import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; +import { + Sidebar, + SidebarInset, + SidebarProvider, + SidebarRail, + useSidebar, +} from "~/components/ui/sidebar"; import { WorkspaceFileTree } from "~/components/WorkspaceFileTree"; import { useChatWidgetStore } from "../chatWidgetStore"; import { useCodeViewerStore } from "../codeViewerStore"; @@ -24,6 +30,8 @@ import { useDiffViewerStore } from "../diffViewerStore"; import { isMobileShell } from "../env"; import { useClientMode } from "../hooks/useClientMode"; import { useTheme } from "../hooks/useTheme"; +import { isTerminalFocused } from "../lib/terminalFocus"; +import { panelNavigationShortcutData } from "../keybindings"; import { useRightPanelStore } from "../rightPanelStore"; import { useSimulationViewerStore } from "../simulationViewerStore"; import { useStore } from "../store"; @@ -169,6 +177,7 @@ const RightPanelSheet = (props: { open: boolean; onClose: () => void; children: function ChatThreadRouteView() { const threadsHydrated = useStore((store) => store.threadsHydrated); const navigate = useNavigate(); + const { toggleSidebar } = useSidebar(); const threadId = Route.useParams({ select: (params) => ThreadId.makeUnsafe(params.threadId), }); @@ -228,6 +237,45 @@ function ChatThreadRouteView() { closeSimulationStore(); }, [closeSimulationStore]); + useEffect(() => { + const onWindowKeyDown = (event: KeyboardEvent) => { + if (event.defaultPrevented) return; + if (event.repeat) return; + if (isTerminalFocused()) return; + + const target = event.target; + if (target instanceof HTMLElement) { + if ( + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.isContentEditable + ) { + return; + } + } + + const shortcutTarget = panelNavigationShortcutData(event); + if (shortcutTarget === null) return; + + event.preventDefault(); + event.stopPropagation(); + + if (shortcutTarget === "sidebar") { + toggleSidebar(); + return; + } + + if (rightPanelOpen) { + closeRightPanel(); + } else { + openRightPanel(); + } + }; + + window.addEventListener("keydown", onWindowKeyDown); + return () => window.removeEventListener("keydown", onWindowKeyDown); + }, [closeRightPanel, openRightPanel, rightPanelOpen, toggleSidebar]); + // ── Sync sub-panel opens → right panel tab ──────────────────────── // When code viewer opens (or a new file is activated), switch to workspace tab. useEffect(() => {