From 8531cfc5d081a6b5c9c679cfad69d8c1cd3106d1 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Wed, 21 Oct 2026 06:21:05 -0700 Subject: [PATCH 1/2] fix(web): pass usage route context to global shortcut handlers Squashed from #21457. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/AppSidebarLayout.tsx | 4 +++- apps/web/src/components/CommandPalette.tsx | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 311efa7e5f0c..dce6a923efd2 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -220,6 +220,7 @@ function routePanelsOpen(routeThreadRef: ReturnType) { } function NavigationHistoryShortcuts() { + const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" }); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const routeThreadRef = useThreadRouteRef(); @@ -239,6 +240,7 @@ function NavigationHistoryShortcuts() { ...routePanelsOpen(routeThreadRef), editableFocus: isEditableFocused(event.target), modelPickerOpen: isModelPickerOpen(), + usagePageOpen, }, }); if (command !== "navigation.back" && command !== "navigation.forward") return; @@ -251,7 +253,7 @@ function NavigationHistoryShortcuts() { window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [keybindings, routeThreadRef]); + }, [keybindings, routeThreadRef, usagePageOpen]); return null; } diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 5ee7676de332..0d2b480e89c9 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -237,6 +237,7 @@ const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; export function CommandPalette({ children }: { children: ReactNode }) { const navigate = useNavigate(); + const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" }); const [state, dispatch] = useReducer(reduceCommandPaletteUiState, { open: false, mode: "command", @@ -287,6 +288,7 @@ export function CommandPalette({ children }: { children: ReactNode }) { previewFocus: isPreviewFocused(), previewOpen, modelPickerOpen: composerHandleRef.current?.isModelPickerOpen() ?? false, + usagePageOpen, }, }); if (command === "appearance.cycle") { @@ -352,6 +354,7 @@ export function CommandPalette({ children }: { children: ReactNode }) { theme, themeHalves, toggleMode, + usagePageOpen, ]); useEffect( From 1f4296e28c6a7e6bdc4f6c100153959e0d901818 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 06:24:12 -0700 Subject: [PATCH 2/2] test(web): cover the usage toggle shortcut on the Usage page Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/AppSidebarLayout.tsx | 2 +- .../UsageToggleShortcut.browser.test.tsx | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/UsageToggleShortcut.browser.test.tsx diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index dce6a923efd2..526ac86a6c9a 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -219,7 +219,7 @@ function routePanelsOpen(routeThreadRef: ReturnType) { }; } -function NavigationHistoryShortcuts() { +export function NavigationHistoryShortcuts() { const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" }); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const routeThreadRef = useThreadRouteRef(); diff --git a/apps/web/src/components/UsageToggleShortcut.browser.test.tsx b/apps/web/src/components/UsageToggleShortcut.browser.test.tsx new file mode 100644 index 000000000000..1efaa2837b00 --- /dev/null +++ b/apps/web/src/components/UsageToggleShortcut.browser.test.tsx @@ -0,0 +1,52 @@ +import { compileResolvedKeybindingsConfig } from "@t3tools/shared/keybindings"; +import { describe, expect, it, vi } from "vite-plus/test"; +import { userEvent } from "vite-plus/test/browser"; + +import { render } from "../../test/render"; + +const state = vi.hoisted(() => ({ navigate: vi.fn() })); + +// One key toggles Usage: it opens Usage elsewhere and goes back from it. +const keybindings = compileResolvedKeybindingsConfig([ + { key: "mod+u", command: "navigation.back", when: "usagePageOpen" }, + { key: "mod+u", command: "usage.open", when: "!usagePageOpen" }, +]); + +vi.mock(import("@effect/atom-react"), async (original) => ({ + ...(await original()), + useAtomValue: (() => keybindings) as never, +})); +vi.mock(import("@tanstack/react-router"), async (original) => ({ + ...(await original()), + useNavigate: (() => state.navigate) as never, +})); + +import { NavigationHistoryShortcuts } from "./AppSidebarLayout"; +import { CommandPalette } from "./CommandPalette"; + +// regression: #21357 +describe("usage toggle shortcut on Usage", () => { + it("goes back", async () => { + vi.clearAllMocks(); + using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64"); + await using _screen = await render(, { router: "/usage" }); + + using back = vi.spyOn(window.history, "back").mockImplementation(() => undefined); + await userEvent.keyboard("{Control>}u{/Control}"); + + expect(back).toHaveBeenCalledOnce(); + }); + + // Both listeners sit on window, so the palette must not claim the key whatever their order. + it("does not reopen Usage from the command palette", async () => { + vi.clearAllMocks(); + using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64"); + await using _screen = await render({null}, { + router: "/usage", + }); + + await userEvent.keyboard("{Control>}u{/Control}"); + + expect(state.navigate).not.toHaveBeenCalled(); + }); +});