| author | |
| committer | |
| log | d4bd8923ad8cab346a854967015b06d9e5cd77f7 |
| tree | 5cb0e638ef084cc4c810ad081b1e2245584397d6 |
| parent | b5f4e8137b3cbd657fbe2b698f9db4e321acbc3d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Code <noreply@anthropic.com>7 files changed, 70 insertions(+), 0 deletions(-)
apps/server/src/keybindings.test.ts+1| ... | ... | @@ -207,6 +207,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 207 | 207 | assert.equal(defaultsByCommand.get("sidebar.toggle"), "mod+b"); |
| 208 | 208 | assert.equal(defaultsByCommand.get("rightPanel.toggle"), "mod+alt+b"); |
| 209 | 209 | assert.isFalse(defaultsByCommand.has("rightPanel.toggleMaximized")); |
| 210 | assert.equal(defaultsByCommand.get("rightPanel.close"), "mod+w"); | |
| 210 | 211 | assert.equal(defaultsByCommand.get("terminal.splitVertical"), "mod+shift+d"); |
| 211 | 212 | assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1"); |
| 212 | 213 | assert.equal(defaultsByCommand.get("modelPicker.jump.9"), "mod+9"); |
apps/web/src/components/ChatView.tsx+11| ... | ... | @@ -5691,6 +5691,16 @@ function ChatViewContent(props: ChatViewProps) { |
| 5691 | 5691 | return; |
| 5692 | 5692 | } |
| 5693 | 5693 | |
| 5694 | if (command === "rightPanel.close") { | |
| 5695 | // Nothing open: leave the event alone so the shortcut keeps its | |
| 5696 | // native meaning (close window on desktop, close tab in a browser). | |
| 5697 | if (!activeRightPanelSurface) return; | |
| 5698 | event.preventDefault(); | |
| 5699 | event.stopPropagation(); | |
| 5700 | if (!event.repeat) closeRightPanelSurface(activeRightPanelSurface); | |
| 5701 | return; | |
| 5702 | } | |
| 5703 | ||
| 5694 | 5704 | if (command === "terminal.split") { |
| 5695 | 5705 | event.preventDefault(); |
| 5696 | 5706 | event.stopPropagation(); |
| ... | ... | @@ -5779,6 +5789,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5779 | 5789 | terminalUiState.terminalOpen, |
| 5780 | 5790 | terminalUiState.activeTerminalId, |
| 5781 | 5791 | activeThreadId, |
| 5792 | closeRightPanelSurface, | |
| 5782 | 5793 | requestCloseTerminal, |
| 5783 | 5794 | requestClosePanelTerminal, |
| 5784 | 5795 | createNewTerminal, |
apps/web/src/keybindings.test.ts+23| ... | ... | @@ -108,6 +108,11 @@ const DEFAULT_BINDINGS = compile([ |
| 108 | 108 | command: "terminal.close", |
| 109 | 109 | whenAst: whenIdentifier("terminalFocus"), |
| 110 | 110 | }, |
| 111 | { | |
| 112 | shortcut: modShortcut("w"), | |
| 113 | command: "rightPanel.close", | |
| 114 | whenAst: whenNot(whenIdentifier("terminalFocus")), | |
| 115 | }, | |
| 111 | 116 | { |
| 112 | 117 | shortcut: modShortcut("d"), |
| 113 | 118 | command: "diff.toggle", |
| ... | ... | @@ -751,6 +756,24 @@ describe("resolveShortcutCommand", () => { |
| 751 | 756 | ); |
| 752 | 757 | }); |
| 753 | 758 | |
| 759 | it("routes mod+w to the terminal while focused and to the right panel otherwise", () => { | |
| 760 | const closeEvent = event({ key: "w", metaKey: true }); | |
| 761 | assert.strictEqual( | |
| 762 | resolveShortcutCommand(closeEvent, DEFAULT_BINDINGS, { | |
| 763 | platform: "MacIntel", | |
| 764 | context: { terminalFocus: true }, | |
| 765 | }), | |
| 766 | "terminal.close", | |
| 767 | ); | |
| 768 | assert.strictEqual( | |
| 769 | resolveShortcutCommand(closeEvent, DEFAULT_BINDINGS, { | |
| 770 | platform: "MacIntel", | |
| 771 | context: { terminalFocus: false }, | |
| 772 | }), | |
| 773 | "rightPanel.close", | |
| 774 | ); | |
| 775 | }); | |
| 776 | ||
| 754 | 777 | it("resolves a custom right panel maximize binding", () => { |
| 755 | 778 | const keybindings = compile([ |
| 756 | 779 | { |
apps/web/src/routes/_chat.pull-requests.tsx+27| ... | ... | @@ -11,6 +11,7 @@ import type { |
| 11 | 11 | PullRequestListState, |
| 12 | 12 | SourceControlProviderKind, |
| 13 | 13 | } from "@t3tools/contracts"; |
| 14 | import { useAtomValue } from "@effect/atom-react"; | |
| 14 | 15 | import { createFileRoute, useNavigate } from "@tanstack/react-router"; |
| 15 | 16 | import { |
| 16 | 17 | ArrowDownUpIcon, |
| ... | ... | @@ -34,6 +35,7 @@ import { |
| 34 | 35 | import { |
| 35 | 36 | useCallback, |
| 36 | 37 | useEffect, |
| 38 | useEffectEvent, | |
| 37 | 39 | useMemo, |
| 38 | 40 | useRef, |
| 39 | 41 | useState, |
| ... | ... | @@ -107,7 +109,10 @@ import { |
| 107 | 109 | } from "../components/WorkspaceBreadcrumb"; |
| 108 | 110 | import { WorkspacePageContainer } from "../components/WorkspacePageContainer"; |
| 109 | 111 | import { WorkspacePageHeader } from "../components/WorkspacePageHeader"; |
| 112 | import { isCommandPaletteOpen } from "../commandPaletteBus"; | |
| 110 | 113 | import { isElectron } from "../env"; |
| 114 | import { resolveShortcutCommand } from "../keybindings"; | |
| 115 | import { isTerminalFocused } from "../lib/terminalFocus"; | |
| 111 | 116 | import { PanelLayoutControls } from "../components/chat/PanelLayoutControls"; |
| 112 | 117 | import { Button } from "../components/ui/button"; |
| 113 | 118 | import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../components/ui/menu"; |
| ... | ... | @@ -135,6 +140,7 @@ import { |
| 135 | 140 | } from "../state/pullRequests"; |
| 136 | 141 | import { useAtomCommand } from "../state/use-atom-command"; |
| 137 | 142 | import { cn } from "~/lib/utils"; |
| 143 | import { primaryServerKeybindingsAtom } from "~/state/server"; | |
| 138 | 144 | import { getSourceControlPresentationForKind } from "~/sourceControlPresentation"; |
| 139 | 145 | |
| 140 | 146 | export interface PullRequestsSearch extends PullRequestListPreferences { |
| ... | ... | @@ -285,6 +291,7 @@ function PullRequestsRouteView() { |
| 285 | 291 | const statsPolicy: PullRequestStatsPolicy = |
| 286 | 292 | sort === "ready" || sort === "largest" || sort === "smallest" ? "eager" : "visible"; |
| 287 | 293 | const navigate = useNavigate({ from: Route.fullPath }); |
| 294 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); | |
| 288 | 295 | const { environments } = useEnvironments(); |
| 289 | 296 | // Every connected environment that has said it can list pull requests. Sorted, so the query |
| 290 | 297 | // keys, the scope key and the stored snapshot all read the same whichever order the |
| ... | ... | @@ -1888,6 +1895,26 @@ function PullRequestsRouteView() { |
| 1888 | 1895 | selectSurfaceInUrl(null); |
| 1889 | 1896 | }; |
| 1890 | 1897 | |
| 1898 | // This page has no ChatView, so the shared panel handles `rightPanel.close` | |
| 1899 | // itself. With nothing open the event falls through to its native meaning. | |
| 1900 | const closeActiveSurfaceFromShortcut = useEffectEvent((event: KeyboardEvent) => { | |
| 1901 | if (activePullRequestSurface === null) return; | |
| 1902 | event.preventDefault(); | |
| 1903 | event.stopPropagation(); | |
| 1904 | if (!event.repeat) closeSurface(activePullRequestSurface); | |
| 1905 | }); | |
| 1906 | useEffect(() => { | |
| 1907 | const onKeyDown = (event: KeyboardEvent) => { | |
| 1908 | if (event.defaultPrevented || isCommandPaletteOpen()) return; | |
| 1909 | const command = resolveShortcutCommand(event, keybindings, { | |
| 1910 | context: { terminalFocus: isTerminalFocused() }, | |
| 1911 | }); | |
| 1912 | if (command === "rightPanel.close") closeActiveSurfaceFromShortcut(event); | |
| 1913 | }; | |
| 1914 | window.addEventListener("keydown", onKeyDown); | |
| 1915 | return () => window.removeEventListener("keydown", onKeyDown); | |
| 1916 | }, [keybindings]); | |
| 1917 | ||
| 1891 | 1918 | return ( |
| 1892 | 1919 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> |
| 1893 | 1920 | <div className="relative flex min-h-0 flex-1"> |
docs/user/keybindings.md+6| ... | ... | @@ -52,6 +52,12 @@ successful pick; its hover glow and badge preview the element and color family t |
| 52 | 52 | `rightPanel.toggleMaximized` maximizes or restores the open right panel. It has no default shortcut, |
| 53 | 53 | so add one in **Settings** → **Keybindings** if you want to use it. |
| 54 | 54 | |
| 55 | `rightPanel.close` closes the active right panel tab and defaults to `mod+w`. Press it again to close | |
| 56 | the next tab. With the terminal focused, `mod+w` closes the terminal instead, and with nothing left | |
| 57 | to close it closes the desktop window as before. Browsers reserve `mod+w` for closing their own tab | |
| 58 | and never pass it to the page, so in a browser rebind this command (and `terminal.close`) to a | |
| 59 | shortcut the browser leaves alone, such as `alt+w`. | |
| 60 | ||
| 55 | 61 | `thread.copyReference` copies the active thread's pull request link, or its thread ID when no pull |
| 56 | 62 | request is available. Its default shortcut is `mod+shift+c`, and it does not replace terminal copy |
| 57 | 63 | while the terminal has focus. |
packages/contracts/src/keybindings.ts+1| ... | ... | @@ -59,6 +59,7 @@ export const STATIC_KEYBINDING_COMMANDS = [ |
| 59 | 59 | "terminal.close", |
| 60 | 60 | "rightPanel.toggle", |
| 61 | 61 | "rightPanel.toggleMaximized", |
| 62 | "rightPanel.close", | |
| 62 | 63 | "diff.toggle", |
| 63 | 64 | "preview.toggle", |
| 64 | 65 | "preview.refresh", |
packages/shared/src/keybindings.ts+1| ... | ... | @@ -26,6 +26,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ |
| 26 | 26 | { key: "mod+shift+d", command: "terminal.splitVertical", when: "terminalFocus" }, |
| 27 | 27 | { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, |
| 28 | 28 | { key: "mod+w", command: "terminal.close", when: "terminalFocus" }, |
| 29 | { key: "mod+w", command: "rightPanel.close", when: "!terminalFocus" }, | |
| 29 | 30 | { key: "mod+d", command: "diff.toggle", when: "!terminalFocus" }, |
| 30 | 31 | { key: "mod+shift+j", command: "preview.toggle" }, |
| 31 | 32 | { key: "mod+r", command: "preview.refresh", when: "previewFocus" }, |