diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 7fdd47f210d9dcec59d3972fd4b002eb9ea6f016..28fa757c2c9618b100af7b26f24e14dd9ec6a2d7 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -65,6 +65,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, { key: "mod+w", command: "terminal.close", when: "terminalFocus" }, + { key: "mod+d", command: "diff.toggle", when: "!terminalFocus" }, { key: "mod+n", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0adc025eba3cdd8b0abb6c8b4e708e55465ac947..bb4fac354cfce6efc728f9174b674ada10666d0c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -615,6 +615,20 @@ export default function ChatView({ threadId }: ChatViewProps) { () => shortcutLabelForCommand(keybindings, "terminal.close"), [keybindings], ); + const diffPanelShortcutLabel = useMemo( + () => shortcutLabelForCommand(keybindings, "diff.toggle"), + [keybindings], + ); + const onToggleDiff = useCallback(() => { + void navigate({ + to: "/$threadId", + params: { threadId }, + search: (previous) => { + const rest = stripDiffSearchParams(previous); + return diffOpen ? rest : { ...rest, diff: "1" }; + }, + }); + }, [diffOpen, navigate, threadId]); const envLocked = Boolean( activeThread && @@ -1147,6 +1161,13 @@ export default function ChatView({ threadId }: ChatViewProps) { return; } + if (command === "diff.toggle") { + event.preventDefault(); + event.stopPropagation(); + onToggleDiff(); + return; + } + const scriptId = projectScriptIdFromCommand(command); if (!scriptId || !activeProject) return; const script = activeProject.scripts.find((entry) => entry.id === scriptId); @@ -1168,6 +1189,7 @@ export default function ChatView({ threadId }: ChatViewProps) { runProjectScript, splitTerminal, keybindings, + onToggleDiff, toggleTerminalVisibility, ]); @@ -1612,16 +1634,6 @@ export default function ChatView({ threadId }: ChatViewProps) { const onExpandTimelineImage = useCallback((image: ExpandedImagePreview) => { setExpandedImage(image); }, []); - const onToggleDiff = useCallback(() => { - void navigate({ - to: "/$threadId", - params: { threadId }, - search: (previous) => { - const rest = stripDiffSearchParams(previous); - return diffOpen ? rest : { ...rest, diff: "1" }; - }, - }); - }, [diffOpen, navigate, threadId]); const onOpenTurnDiff = useCallback( (turnId: TurnId, filePath?: string) => { void navigate({ @@ -1692,6 +1704,7 @@ export default function ChatView({ threadId }: ChatViewProps) { activeProject ? (lastInvokedScriptByProjectId[activeProject.id] ?? null) : null } keybindings={keybindings} + diffToggleShortcutLabel={diffPanelShortcutLabel} gitCwd={gitCwd} diffOpen={diffOpen} onRunProjectScript={(script) => { @@ -2043,6 +2056,7 @@ interface ChatHeaderProps { activeProjectScripts: ProjectScript[] | undefined; preferredScriptId: string | null; keybindings: ResolvedKeybindingsConfig; + diffToggleShortcutLabel: string | null; gitCwd: string | null; diffOpen: boolean; onRunProjectScript: (script: ProjectScript) => void; @@ -2058,6 +2072,7 @@ const ChatHeader = memo(function ChatHeader({ activeProjectScripts, preferredScriptId, keybindings, + diffToggleShortcutLabel, gitCwd, diffOpen, onRunProjectScript, @@ -2101,6 +2116,9 @@ const ChatHeader = memo(function ChatHeader({ pressed={diffOpen} onPressedChange={onToggleDiff} aria-label="Toggle diff panel" + title={ + diffToggleShortcutLabel ? `Toggle diff panel (${diffToggleShortcutLabel})` : undefined + } variant="outline" size="xs" > diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 3061b7d877749d938054b7b1ba6197598a6a2bcd..ff313fb68e8fe15fb936256af0ca4da41bb31d8c 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -132,6 +132,7 @@ function SidebarProvider({ // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { + if (!isMobile) return; if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { event.preventDefault(); toggleSidebar(); @@ -140,7 +141,7 @@ function SidebarProvider({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); + }, [isMobile, toggleSidebar]); // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index fb010fed316c4914887737dc4b23b0f49f5336d2..0ecccf43f85efe455016b2aa5b2378b4d4c62869 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -10,6 +10,7 @@ import { formatShortcutLabel, isChatNewShortcut, isChatNewLocalShortcut, + isDiffToggleShortcut, isOpenFavoriteEditorShortcut, isTerminalClearShortcut, isTerminalCloseShortcut, @@ -91,6 +92,11 @@ const DEFAULT_BINDINGS = compile([ command: "terminal.close", whenAst: whenIdentifier("terminalFocus"), }, + { + shortcut: modShortcut("d"), + command: "diff.toggle", + whenAst: whenNot(whenIdentifier("terminalFocus")), + }, { shortcut: modShortcut("o", { shiftKey: true }), command: "chat.new" }, { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newLocal" }, { shortcut: modShortcut("o"), command: "editor.openFavorite" }, @@ -230,6 +236,7 @@ describe("shortcutLabelForCommand", () => { it("returns labels for non-terminal commands", () => { assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "chat.new", "MacIntel"), "⇧⌘O"); + assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "diff.toggle", "Linux"), "Ctrl+D"); assert.strictEqual( shortcutLabelForCommand(DEFAULT_BINDINGS, "editor.openFavorite", "Linux"), "Ctrl+O", @@ -276,6 +283,21 @@ describe("chat/editor shortcuts", () => { }), ); }); + + it("matches diff.toggle shortcut outside terminal focus", () => { + assert.isTrue( + isDiffToggleShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, { + platform: "MacIntel", + context: { terminalFocus: false }, + }), + ); + assert.isFalse( + isDiffToggleShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, { + platform: "MacIntel", + context: { terminalFocus: true }, + }), + ); + }); }); describe("cross-command precedence", () => { diff --git a/apps/web/src/keybindings.ts b/apps/web/src/keybindings.ts index e32ab01ed1d40259b1c9ba079d6ff2d1bd6a8a40..09d9308aade0c24f7e6e8dd7bde8359465c48ac6 100644 --- a/apps/web/src/keybindings.ts +++ b/apps/web/src/keybindings.ts @@ -198,6 +198,14 @@ export function isTerminalCloseShortcut( return matchesCommandShortcut(event, keybindings, "terminal.close", options); } +export function isDiffToggleShortcut( + event: ShortcutEventLike, + keybindings: ResolvedKeybindingsConfig, + options?: ShortcutMatchOptions, +): boolean { + return matchesCommandShortcut(event, keybindings, "diff.toggle", options); +} + export function isChatNewShortcut( event: ShortcutEventLike, keybindings: ResolvedKeybindingsConfig, diff --git a/packages/contracts/src/keybindings.test.ts b/packages/contracts/src/keybindings.test.ts index 28dcb628a3a2fd4362ece22ba95b7c2ee7695c72..1b99362c537a942e95a57f50dbf1089dc4a69b2e 100644 --- a/packages/contracts/src/keybindings.test.ts +++ b/packages/contracts/src/keybindings.test.ts @@ -35,6 +35,12 @@ it.effect("parses keybinding rules", () => }); assert.strictEqual(parsedClose.command, "terminal.close"); + const parsedDiffToggle = yield* decode(KeybindingRule, { + key: "mod+d", + command: "diff.toggle", + }); + assert.strictEqual(parsedDiffToggle.command, "diff.toggle"); + const parsedLocal = yield* decode(KeybindingRule, { key: "mod+shift+n", command: "chat.newLocal", diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index dcba2c7c5931641dd53d767c0e6ec24e790ed6c9..a6e7e9bdeb9f5523a2cc3d31c145f36ea2294de4 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -14,6 +14,7 @@ export const STATIC_KEYBINDING_COMMANDS = [ "terminal.split", "terminal.new", "terminal.close", + "diff.toggle", "chat.new", "chat.newLocal", "editor.openFavorite",