authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-02 23:15:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 23:15:23-07:00
logd4bd8923ad8cab346a854967015b06d9e5cd77f7
tree5cb0e638ef084cc4c810ad081b1e2245584397d6
parentb5f4e8137b3cbd657fbe2b698f9db4e321acbc3d
signature Signed by PGP key B5690EEEBB952194

feat(web): mod+w closes the active right panel tab before the window (#9363)

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) => {
207207 assert.equal(defaultsByCommand.get("sidebar.toggle"), "mod+b");
208208 assert.equal(defaultsByCommand.get("rightPanel.toggle"), "mod+alt+b");
209209 assert.isFalse(defaultsByCommand.has("rightPanel.toggleMaximized"));
210 assert.equal(defaultsByCommand.get("rightPanel.close"), "mod+w");
210211 assert.equal(defaultsByCommand.get("terminal.splitVertical"), "mod+shift+d");
211212 assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1");
212213 assert.equal(defaultsByCommand.get("modelPicker.jump.9"), "mod+9");
apps/web/src/components/ChatView.tsx+11
......@@ -5691,6 +5691,16 @@ function ChatViewContent(props: ChatViewProps) {
56915691 return;
56925692 }
56935693
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
56945704 if (command === "terminal.split") {
56955705 event.preventDefault();
56965706 event.stopPropagation();
......@@ -5779,6 +5789,7 @@ function ChatViewContent(props: ChatViewProps) {
57795789 terminalUiState.terminalOpen,
57805790 terminalUiState.activeTerminalId,
57815791 activeThreadId,
5792 closeRightPanelSurface,
57825793 requestCloseTerminal,
57835794 requestClosePanelTerminal,
57845795 createNewTerminal,
apps/web/src/keybindings.test.ts+23
......@@ -108,6 +108,11 @@ const DEFAULT_BINDINGS = compile([
108108 command: "terminal.close",
109109 whenAst: whenIdentifier("terminalFocus"),
110110 },
111 {
112 shortcut: modShortcut("w"),
113 command: "rightPanel.close",
114 whenAst: whenNot(whenIdentifier("terminalFocus")),
115 },
111116 {
112117 shortcut: modShortcut("d"),
113118 command: "diff.toggle",
......@@ -751,6 +756,24 @@ describe("resolveShortcutCommand", () => {
751756 );
752757 });
753758
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
754777 it("resolves a custom right panel maximize binding", () => {
755778 const keybindings = compile([
756779 {
apps/web/src/routes/_chat.pull-requests.tsx+27
......@@ -11,6 +11,7 @@ import type {
1111 PullRequestListState,
1212 SourceControlProviderKind,
1313} from "@t3tools/contracts";
14import { useAtomValue } from "@effect/atom-react";
1415import { createFileRoute, useNavigate } from "@tanstack/react-router";
1516import {
1617 ArrowDownUpIcon,
......@@ -34,6 +35,7 @@ import {
3435import {
3536 useCallback,
3637 useEffect,
38 useEffectEvent,
3739 useMemo,
3840 useRef,
3941 useState,
......@@ -107,7 +109,10 @@ import {
107109} from "../components/WorkspaceBreadcrumb";
108110import { WorkspacePageContainer } from "../components/WorkspacePageContainer";
109111import { WorkspacePageHeader } from "../components/WorkspacePageHeader";
112import { isCommandPaletteOpen } from "../commandPaletteBus";
110113import { isElectron } from "../env";
114import { resolveShortcutCommand } from "../keybindings";
115import { isTerminalFocused } from "../lib/terminalFocus";
111116import { PanelLayoutControls } from "../components/chat/PanelLayoutControls";
112117import { Button } from "../components/ui/button";
113118import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "../components/ui/menu";
......@@ -135,6 +140,7 @@ import {
135140} from "../state/pullRequests";
136141import { useAtomCommand } from "../state/use-atom-command";
137142import { cn } from "~/lib/utils";
143import { primaryServerKeybindingsAtom } from "~/state/server";
138144import { getSourceControlPresentationForKind } from "~/sourceControlPresentation";
139145
140146export interface PullRequestsSearch extends PullRequestListPreferences {
......@@ -285,6 +291,7 @@ function PullRequestsRouteView() {
285291 const statsPolicy: PullRequestStatsPolicy =
286292 sort === "ready" || sort === "largest" || sort === "smallest" ? "eager" : "visible";
287293 const navigate = useNavigate({ from: Route.fullPath });
294 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
288295 const { environments } = useEnvironments();
289296 // Every connected environment that has said it can list pull requests. Sorted, so the query
290297 // keys, the scope key and the stored snapshot all read the same whichever order the
......@@ -1888,6 +1895,26 @@ function PullRequestsRouteView() {
18881895 selectSurfaceInUrl(null);
18891896 };
18901897
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
18911918 return (
18921919 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
18931920 <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
5252`rightPanel.toggleMaximized` maximizes or restores the open right panel. It has no default shortcut,
5353so add one in **Settings** → **Keybindings** if you want to use it.
5454
55`rightPanel.close` closes the active right panel tab and defaults to `mod+w`. Press it again to close
56the next tab. With the terminal focused, `mod+w` closes the terminal instead, and with nothing left
57to close it closes the desktop window as before. Browsers reserve `mod+w` for closing their own tab
58and never pass it to the page, so in a browser rebind this command (and `terminal.close`) to a
59shortcut the browser leaves alone, such as `alt+w`.
60
5561`thread.copyReference` copies the active thread's pull request link, or its thread ID when no pull
5662request is available. Its default shortcut is `mod+shift+c`, and it does not replace terminal copy
5763while the terminal has focus.
packages/contracts/src/keybindings.ts+1
......@@ -59,6 +59,7 @@ export const STATIC_KEYBINDING_COMMANDS = [
5959 "terminal.close",
6060 "rightPanel.toggle",
6161 "rightPanel.toggleMaximized",
62 "rightPanel.close",
6263 "diff.toggle",
6364 "preview.toggle",
6465 "preview.refresh",
packages/shared/src/keybindings.ts+1
......@@ -26,6 +26,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
2626 { key: "mod+shift+d", command: "terminal.splitVertical", when: "terminalFocus" },
2727 { key: "mod+n", command: "terminal.new", when: "terminalFocus" },
2828 { key: "mod+w", command: "terminal.close", when: "terminalFocus" },
29 { key: "mod+w", command: "rightPanel.close", when: "!terminalFocus" },
2930 { key: "mod+d", command: "diff.toggle", when: "!terminalFocus" },
3031 { key: "mod+shift+j", command: "preview.toggle" },
3132 { key: "mod+r", command: "preview.refresh", when: "previewFocus" },