| author | |
| committer | |
| log | ef4ce2e1ec3a6e08ccc392b802462519cbb706bc |
| tree | 5ee31f7b71e8c315d30b1c0fdae7b25f0c28d9bc |
| parent | 61e9ba4b19fc4ed58bb59e23a2aaa995ee3197bd |
- add `chat.new` and `editor.openFavorite` to keybinding contracts and server defaults
- use shared keybinding matching in sidebar/chat view instead of hardcoded Cmd/Ctrl combos
- document new commands/defaults and extend server/web keybinding tests8 files changed, 122 insertions(+), 41 deletions(-)
KEYBINDINGS.md+7-1| ... | ... | @@ -19,7 +19,9 @@ The file must be a JSON array of rules: |
| 19 | 19 | [ |
| 20 | 20 | { "key": "mod+j", "command": "terminal.toggle" }, |
| 21 | 21 | { "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" }, |
| 22 | { "key": "mod+shift+d", "command": "terminal.new", "when": "terminalFocus" } | |
| 22 | { "key": "mod+shift+d", "command": "terminal.new", "when": "terminalFocus" }, | |
| 23 | { "key": "mod+shift+o", "command": "chat.new" }, | |
| 24 | { "key": "mod+o", "command": "editor.openFavorite" } | |
| 23 | 25 | ] |
| 24 | 26 | ``` |
| 25 | 27 | |
| ... | ... | @@ -38,6 +40,8 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged |
| 38 | 40 | - `terminal.toggle`: open/close terminal drawer |
| 39 | 41 | - `terminal.split`: split terminal (in focused terminal context by default) |
| 40 | 42 | - `terminal.new`: create new terminal (in focused terminal context by default) |
| 43 | - `chat.new`: create a new chat thread for the active project | |
| 44 | - `editor.openFavorite`: open current project/worktree in the last-used editor | |
| 41 | 45 | |
| 42 | 46 | ## Key Syntax |
| 43 | 47 | |
| ... | ... | @@ -85,6 +89,8 @@ Built-in defaults are resolved on the server and sent to the web client via `ser |
| 85 | 89 | - `terminal.toggle`: `mod+j` |
| 86 | 90 | - `terminal.split`: `mod+d` when `terminalFocus` |
| 87 | 91 | - `terminal.new`: `mod+shift+d` when `terminalFocus` |
| 92 | - `chat.new`: `mod+shift+o` | |
| 93 | - `editor.openFavorite`: `mod+o` | |
| 88 | 94 | |
| 89 | 95 | If you define any rules for a command, the default rule for that command is removed on the server. |
| 90 | 96 |
apps/server/src/wsServer.test.ts+10| ... | ... | @@ -313,6 +313,8 @@ describe("WebSocket Server", () => { |
| 313 | 313 | { key: "mod+j", command: "terminal.toggle" }, |
| 314 | 314 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 315 | 315 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, |
| 316 | { key: "mod+shift+o", command: "chat.new" }, | |
| 317 | { key: "mod+o", command: "editor.openFavorite" }, | |
| 316 | 318 | ], |
| 317 | 319 | }); |
| 318 | 320 | }); |
| ... | ... | @@ -347,6 +349,8 @@ describe("WebSocket Server", () => { |
| 347 | 349 | expect(response.result).toEqual({ |
| 348 | 350 | cwd: "/my/workspace", |
| 349 | 351 | keybindings: [ |
| 352 | { key: "mod+shift+o", command: "chat.new" }, | |
| 353 | { key: "mod+o", command: "editor.openFavorite" }, | |
| 350 | 354 | { key: "cmd+j", command: "terminal.toggle" }, |
| 351 | 355 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 352 | 356 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, |
| ... | ... | @@ -386,6 +390,8 @@ describe("WebSocket Server", () => { |
| 386 | 390 | keybindings: [ |
| 387 | 391 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 388 | 392 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, |
| 393 | { key: "mod+shift+o", command: "chat.new" }, | |
| 394 | { key: "mod+o", command: "editor.openFavorite" }, | |
| 389 | 395 | { key: "mod+j", command: "terminal.toggle" }, |
| 390 | 396 | ], |
| 391 | 397 | }); |
| ... | ... | @@ -426,6 +432,8 @@ describe("WebSocket Server", () => { |
| 426 | 432 | { key: "mod+j", command: "terminal.toggle" }, |
| 427 | 433 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 428 | 434 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, |
| 435 | { key: "mod+shift+o", command: "chat.new" }, | |
| 436 | { key: "mod+o", command: "editor.openFavorite" }, | |
| 429 | 437 | ], |
| 430 | 438 | }); |
| 431 | 439 | expect( |
| ... | ... | @@ -461,6 +469,8 @@ describe("WebSocket Server", () => { |
| 461 | 469 | { key: "mod+j", command: "terminal.toggle" }, |
| 462 | 470 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 463 | 471 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, |
| 472 | { key: "mod+shift+o", command: "chat.new" }, | |
| 473 | { key: "mod+o", command: "editor.openFavorite" }, | |
| 464 | 474 | ], |
| 465 | 475 | }); |
| 466 | 476 | expect( |
apps/server/src/wsServer.ts+2| ... | ... | @@ -74,6 +74,8 @@ const DEFAULT_KEYBINDINGS: KeybindingsConfig = [ |
| 74 | 74 | { key: "mod+j", command: "terminal.toggle" }, |
| 75 | 75 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 76 | 76 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, |
| 77 | { key: "mod+shift+o", command: "chat.new" }, | |
| 78 | { key: "mod+o", command: "editor.openFavorite" }, | |
| 77 | 79 | ]; |
| 78 | 80 | |
| 79 | 81 | function mergeWithDefaultKeybindings(custom: KeybindingsConfig): KeybindingsConfig { |
apps/web/src/components/ChatView.tsx+27-25| ... | ... | @@ -54,6 +54,7 @@ import type { ChatImageAttachment } from "../types"; |
| 54 | 54 | import BranchToolbar from "./BranchToolbar"; |
| 55 | 55 | import GitActionsControl from "./GitActionsControl"; |
| 56 | 56 | import { |
| 57 | isOpenFavoriteEditorShortcut, | |
| 57 | 58 | isTerminalNewShortcut, |
| 58 | 59 | isTerminalSplitShortcut, |
| 59 | 60 | isTerminalToggleShortcut, |
| ... | ... | @@ -263,20 +264,20 @@ export default function ChatView() { |
| 263 | 264 | } as const); |
| 264 | 265 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; |
| 265 | 266 | const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd)); |
| 266 | const terminalKeybindingsQuery = useQuery({ | |
| 267 | const keybindingsQuery = useQuery({ | |
| 267 | 268 | ...serverConfigQueryOptions(api), |
| 268 | 269 | select: (config) => config.keybindings, |
| 269 | 270 | }); |
| 270 | const terminalKeybindings = terminalKeybindingsQuery.data ?? EMPTY_KEYBINDINGS; | |
| 271 | const keybindings = keybindingsQuery.data ?? EMPTY_KEYBINDINGS; | |
| 271 | 272 | // Default true while loading to avoid toolbar flicker. |
| 272 | 273 | const isGitRepo = branchesQuery.data?.isRepo ?? true; |
| 273 | 274 | const splitTerminalShortcutLabel = useMemo( |
| 274 | () => shortcutLabelForCommand(terminalKeybindings, "terminal.split"), | |
| 275 | [terminalKeybindings], | |
| 275 | () => shortcutLabelForCommand(keybindings, "terminal.split"), | |
| 276 | [keybindings], | |
| 276 | 277 | ); |
| 277 | 278 | const newTerminalShortcutLabel = useMemo( |
| 278 | () => shortcutLabelForCommand(terminalKeybindings, "terminal.new"), | |
| 279 | [terminalKeybindings], | |
| 279 | () => shortcutLabelForCommand(keybindings, "terminal.new"), | |
| 280 | [keybindings], | |
| 280 | 281 | ); |
| 281 | 282 | |
| 282 | 283 | const envLocked = Boolean( |
| ... | ... | @@ -527,14 +528,14 @@ export default function ChatView() { |
| 527 | 528 | terminalOpen: Boolean(activeThread?.terminalOpen), |
| 528 | 529 | }; |
| 529 | 530 | |
| 530 | if (isTerminalToggleShortcut(event, terminalKeybindings, { context: shortcutContext })) { | |
| 531 | if (isTerminalToggleShortcut(event, keybindings, { context: shortcutContext })) { | |
| 531 | 532 | event.preventDefault(); |
| 532 | 533 | event.stopPropagation(); |
| 533 | 534 | toggleTerminalVisibility(); |
| 534 | 535 | return; |
| 535 | 536 | } |
| 536 | 537 | |
| 537 | if (isTerminalSplitShortcut(event, terminalKeybindings, { context: shortcutContext })) { | |
| 538 | if (isTerminalSplitShortcut(event, keybindings, { context: shortcutContext })) { | |
| 538 | 539 | event.preventDefault(); |
| 539 | 540 | event.stopPropagation(); |
| 540 | 541 | if (!activeThread?.terminalOpen) { |
| ... | ... | @@ -548,7 +549,7 @@ export default function ChatView() { |
| 548 | 549 | return; |
| 549 | 550 | } |
| 550 | 551 | |
| 551 | if (!isTerminalNewShortcut(event, terminalKeybindings, { context: shortcutContext })) return; | |
| 552 | if (!isTerminalNewShortcut(event, keybindings, { context: shortcutContext })) return; | |
| 552 | 553 | event.preventDefault(); |
| 553 | 554 | event.stopPropagation(); |
| 554 | 555 | if (!activeThread?.terminalOpen) { |
| ... | ... | @@ -568,7 +569,7 @@ export default function ChatView() { |
| 568 | 569 | createNewTerminal, |
| 569 | 570 | dispatch, |
| 570 | 571 | splitTerminal, |
| 571 | terminalKeybindings, | |
| 572 | keybindings, | |
| 572 | 573 | toggleTerminalVisibility, |
| 573 | 574 | ]); |
| 574 | 575 | |
| ... | ... | @@ -937,7 +938,7 @@ export default function ChatView() { |
| 937 | 938 | </div> |
| 938 | 939 | <div className="flex items-center gap-3"> |
| 939 | 940 | {/* Open in editor */} |
| 940 | {activeProject && <OpenInPicker />} | |
| 941 | {activeProject && <OpenInPicker keybindings={keybindings} />} | |
| 941 | 942 | {/* Git actions */} |
| 942 | 943 | {activeProject && <GitActionsControl api={api} gitCwd={gitCwd} />} |
| 943 | 944 | |
| ... | ... | @@ -1519,7 +1520,7 @@ function ReasoningEffortPicker(props: { |
| 1519 | 1520 | ); |
| 1520 | 1521 | } |
| 1521 | 1522 | |
| 1522 | function OpenInPicker() { | |
| 1523 | function OpenInPicker({ keybindings }: { keybindings: KeybindingsConfig }) { | |
| 1523 | 1524 | const [lastEditor, setLastEditor] = useState<EditorId>(() => { |
| 1524 | 1525 | const stored = localStorage.getItem(LAST_EDITOR_KEY); |
| 1525 | 1526 | return EDITORS.some((e) => e.id === stored) ? (stored as EditorId) : EDITORS[0].id; |
| ... | ... | @@ -1560,20 +1561,23 @@ function OpenInPicker() { |
| 1560 | 1561 | [api, activeProject, activeThread, lastEditor, setLastEditor], |
| 1561 | 1562 | ); |
| 1562 | 1563 | |
| 1563 | // Cmd+O / Ctrl+O to open in last-used editor | |
| 1564 | const openFavoriteEditorShortcutLabel = useMemo( | |
| 1565 | () => shortcutLabelForCommand(keybindings, "editor.openFavorite"), | |
| 1566 | [keybindings], | |
| 1567 | ); | |
| 1568 | ||
| 1564 | 1569 | useEffect(() => { |
| 1565 | 1570 | const handler = (e: globalThis.KeyboardEvent) => { |
| 1566 | if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { | |
| 1567 | if (api && activeProject) { | |
| 1568 | e.preventDefault(); | |
| 1569 | const cwd = activeThread?.worktreePath ?? activeProject.cwd; | |
| 1570 | void api.shell.openInEditor(cwd, lastEditor); | |
| 1571 | } | |
| 1572 | } | |
| 1571 | if (!isOpenFavoriteEditorShortcut(e, keybindings)) return; | |
| 1572 | if (!api || !activeProject) return; | |
| 1573 | ||
| 1574 | e.preventDefault(); | |
| 1575 | const cwd = activeThread?.worktreePath ?? activeProject.cwd; | |
| 1576 | void api.shell.openInEditor(cwd, lastEditor); | |
| 1573 | 1577 | }; |
| 1574 | 1578 | window.addEventListener("keydown", handler); |
| 1575 | 1579 | return () => window.removeEventListener("keydown", handler); |
| 1576 | }, [api, activeProject, activeThread, lastEditor]); | |
| 1580 | }, [api, activeProject, activeThread, keybindings, lastEditor]); | |
| 1577 | 1581 | |
| 1578 | 1582 | return ( |
| 1579 | 1583 | <Group aria-label="Subscription actions"> |
| ... | ... | @@ -1591,10 +1595,8 @@ function OpenInPicker() { |
| 1591 | 1595 | <MenuItem key={value} onClick={() => openInEditor(value)}> |
| 1592 | 1596 | <Icon aria-hidden="true" className="text-muted-foreground" /> |
| 1593 | 1597 | {label} |
| 1594 | {value === lastEditor && ( | |
| 1595 | <MenuShortcut> | |
| 1596 | {navigator.platform.includes("Mac") ? "\u2318O" : "Ctrl+O"} | |
| 1597 | </MenuShortcut> | |
| 1598 | {value === lastEditor && openFavoriteEditorShortcutLabel && ( | |
| 1599 | <MenuShortcut>{openFavoriteEditorShortcutLabel}</MenuShortcut> | |
| 1598 | 1600 | )} |
| 1599 | 1601 | </MenuItem> |
| 1600 | 1602 | ))} |
apps/web/src/components/Sidebar.tsx+13-9| ... | ... | @@ -1,11 +1,13 @@ |
| 1 | 1 | import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; |
| 2 | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | import { useMutation, useQueryClient } from "@tanstack/react-query"; | |
| 3 | import type { KeybindingsConfig } from "@t3tools/contracts"; | |
| 4 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | |
| 4 | 5 | import { isElectron } from "../env"; |
| 5 | 6 | import { useTheme } from "../hooks/useTheme"; |
| 6 | 7 | import { DEFAULT_MODEL } from "../model-logic"; |
| 7 | 8 | import { derivePendingApprovals } from "../session-logic"; |
| 8 | 9 | import { useStore } from "../store"; |
| 10 | import { isChatNewShortcut } from "../keybindings"; | |
| 9 | 11 | import { |
| 10 | 12 | DEFAULT_THREAD_TERMINAL_HEIGHT, |
| 11 | 13 | DEFAULT_THREAD_TERMINAL_ID, |
| ... | ... | @@ -14,9 +16,12 @@ import { |
| 14 | 16 | } from "../types"; |
| 15 | 17 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 16 | 18 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; |
| 19 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; | |
| 17 | 20 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 18 | 21 | |
| 19 | 22 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; |
| 23 | const EMPTY_KEYBINDINGS: KeybindingsConfig = []; | |
| 24 | ||
| 20 | 25 | function formatRelativeTime(iso: string): string { |
| 21 | 26 | const diff = Date.now() - new Date(iso).getTime(); |
| 22 | 27 | const minutes = Math.floor(diff / 60_000); |
| ... | ... | @@ -112,6 +117,11 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null |
| 112 | 117 | export default function Sidebar() { |
| 113 | 118 | const { state, dispatch } = useStore(); |
| 114 | 119 | const api = useNativeApi(); |
| 120 | const keybindingsQuery = useQuery({ | |
| 121 | ...serverConfigQueryOptions(api), | |
| 122 | select: (config) => config.keybindings, | |
| 123 | }); | |
| 124 | const keybindings = keybindingsQuery.data ?? EMPTY_KEYBINDINGS; | |
| 115 | 125 | const queryClient = useQueryClient(); |
| 116 | 126 | const removeWorktreeMutation = useMutation(gitRemoveWorktreeMutationOptions({ api, queryClient })); |
| 117 | 127 | const { theme, setTheme } = useTheme(); |
| ... | ... | @@ -324,13 +334,7 @@ export default function Sidebar() { |
| 324 | 334 | |
| 325 | 335 | useEffect(() => { |
| 326 | 336 | const onWindowKeyDown = (event: KeyboardEvent) => { |
| 327 | const isNewThreadShortcut = | |
| 328 | event.metaKey && | |
| 329 | event.shiftKey && | |
| 330 | !event.ctrlKey && | |
| 331 | !event.altKey && | |
| 332 | event.key.toLowerCase() === "o"; | |
| 333 | if (!isNewThreadShortcut) return; | |
| 337 | if (!isChatNewShortcut(event, keybindings)) return; | |
| 334 | 338 | |
| 335 | 339 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); |
| 336 | 340 | const projectId = activeThread?.projectId ?? state.projects[0]?.id; |
| ... | ... | @@ -344,7 +348,7 @@ export default function Sidebar() { |
| 344 | 348 | return () => { |
| 345 | 349 | window.removeEventListener("keydown", onWindowKeyDown); |
| 346 | 350 | }; |
| 347 | }, [handleNewThread, state.activeThreadId, state.projects, state.threads]); | |
| 351 | }, [handleNewThread, keybindings, state.activeThreadId, state.projects, state.threads]); | |
| 348 | 352 | |
| 349 | 353 | return ( |
| 350 | 354 | <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card"> |
apps/web/src/keybindings.test.ts+39| ... | ... | @@ -3,6 +3,8 @@ import { describe, expect, it } from "vitest"; |
| 3 | 3 | import type { KeybindingsConfig } from "@t3tools/contracts"; |
| 4 | 4 | import { |
| 5 | 5 | formatShortcutLabel, |
| 6 | isChatNewShortcut, | |
| 7 | isOpenFavoriteEditorShortcut, | |
| 6 | 8 | isTerminalClearShortcut, |
| 7 | 9 | isTerminalNewShortcut, |
| 8 | 10 | isTerminalSplitShortcut, |
| ... | ... | @@ -26,6 +28,8 @@ const DEFAULT_BINDINGS: KeybindingsConfig = [ |
| 26 | 28 | { key: "mod+j", command: "terminal.toggle" }, |
| 27 | 29 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 28 | 30 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, |
| 31 | { key: "mod+shift+o", command: "chat.new" }, | |
| 32 | { key: "mod+o", command: "editor.openFavorite" }, | |
| 29 | 33 | ]; |
| 30 | 34 | |
| 31 | 35 | describe("isTerminalToggleShortcut", () => { |
| ... | ... | @@ -112,6 +116,41 @@ describe("shortcutLabelForCommand", () => { |
| 112 | 116 | ]; |
| 113 | 117 | expect(shortcutLabelForCommand(bindings, "terminal.split", "Linux")).toBe("Ctrl+Shift+\\"); |
| 114 | 118 | }); |
| 119 | ||
| 120 | it("returns labels for non-terminal commands", () => { | |
| 121 | expect(shortcutLabelForCommand(DEFAULT_BINDINGS, "chat.new", "MacIntel")).toBe("⇧⌘O"); | |
| 122 | expect(shortcutLabelForCommand(DEFAULT_BINDINGS, "editor.openFavorite", "Linux")).toBe( | |
| 123 | "Ctrl+O", | |
| 124 | ); | |
| 125 | }); | |
| 126 | }); | |
| 127 | ||
| 128 | describe("chat/editor shortcuts", () => { | |
| 129 | it("matches chat.new shortcut", () => { | |
| 130 | expect( | |
| 131 | isChatNewShortcut(event({ key: "o", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, { | |
| 132 | platform: "MacIntel", | |
| 133 | }), | |
| 134 | ).toBe(true); | |
| 135 | expect( | |
| 136 | isChatNewShortcut(event({ key: "o", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, { | |
| 137 | platform: "Linux", | |
| 138 | }), | |
| 139 | ).toBe(true); | |
| 140 | }); | |
| 141 | ||
| 142 | it("matches editor.openFavorite shortcut", () => { | |
| 143 | expect( | |
| 144 | isOpenFavoriteEditorShortcut(event({ key: "o", metaKey: true }), DEFAULT_BINDINGS, { | |
| 145 | platform: "MacIntel", | |
| 146 | }), | |
| 147 | ).toBe(true); | |
| 148 | expect( | |
| 149 | isOpenFavoriteEditorShortcut(event({ key: "o", ctrlKey: true }), DEFAULT_BINDINGS, { | |
| 150 | platform: "Linux", | |
| 151 | }), | |
| 152 | ).toBe(true); | |
| 153 | }); | |
| 115 | 154 | }); |
| 116 | 155 | |
| 117 | 156 | describe("formatShortcutLabel", () => { |
apps/web/src/keybindings.ts+22-6| ... | ... | @@ -14,7 +14,7 @@ export interface ShortcutMatchContext { |
| 14 | 14 | [key: string]: boolean; |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | export type ResolvedTerminalKeybindings = KeybindingsConfig; | |
| 17 | export type ResolvedKeybindings = KeybindingsConfig; | |
| 18 | 18 | |
| 19 | 19 | interface ParsedShortcut { |
| 20 | 20 | key: string; |
| ... | ... | @@ -301,7 +301,7 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc |
| 301 | 301 | |
| 302 | 302 | function matchesCommandShortcut( |
| 303 | 303 | event: ShortcutEventLike, |
| 304 | keybindings: ResolvedTerminalKeybindings, | |
| 304 | keybindings: ResolvedKeybindings, | |
| 305 | 305 | command: KeybindingCommand, |
| 306 | 306 | options?: ShortcutMatchOptions, |
| 307 | 307 | ): boolean { |
| ... | ... | @@ -356,7 +356,7 @@ export function formatShortcutLabel(shortcutValue: string, platform = navigator. |
| 356 | 356 | } |
| 357 | 357 | |
| 358 | 358 | export function shortcutLabelForCommand( |
| 359 | keybindings: ResolvedTerminalKeybindings, | |
| 359 | keybindings: ResolvedKeybindings, | |
| 360 | 360 | command: KeybindingCommand, |
| 361 | 361 | platform = navigator.platform, |
| 362 | 362 | ): string | null { |
| ... | ... | @@ -370,7 +370,7 @@ export function shortcutLabelForCommand( |
| 370 | 370 | |
| 371 | 371 | export function isTerminalToggleShortcut( |
| 372 | 372 | event: ShortcutEventLike, |
| 373 | keybindings: ResolvedTerminalKeybindings, | |
| 373 | keybindings: ResolvedKeybindings, | |
| 374 | 374 | options?: ShortcutMatchOptions, |
| 375 | 375 | ): boolean { |
| 376 | 376 | return matchesCommandShortcut(event, keybindings, "terminal.toggle", options); |
| ... | ... | @@ -378,7 +378,7 @@ export function isTerminalToggleShortcut( |
| 378 | 378 | |
| 379 | 379 | export function isTerminalSplitShortcut( |
| 380 | 380 | event: ShortcutEventLike, |
| 381 | keybindings: ResolvedTerminalKeybindings, | |
| 381 | keybindings: ResolvedKeybindings, | |
| 382 | 382 | options?: ShortcutMatchOptions, |
| 383 | 383 | ): boolean { |
| 384 | 384 | return matchesCommandShortcut(event, keybindings, "terminal.split", options); |
| ... | ... | @@ -386,12 +386,28 @@ export function isTerminalSplitShortcut( |
| 386 | 386 | |
| 387 | 387 | export function isTerminalNewShortcut( |
| 388 | 388 | event: ShortcutEventLike, |
| 389 | keybindings: ResolvedTerminalKeybindings, | |
| 389 | keybindings: ResolvedKeybindings, | |
| 390 | 390 | options?: ShortcutMatchOptions, |
| 391 | 391 | ): boolean { |
| 392 | 392 | return matchesCommandShortcut(event, keybindings, "terminal.new", options); |
| 393 | 393 | } |
| 394 | 394 | |
| 395 | export function isChatNewShortcut( | |
| 396 | event: ShortcutEventLike, | |
| 397 | keybindings: ResolvedKeybindings, | |
| 398 | options?: ShortcutMatchOptions, | |
| 399 | ): boolean { | |
| 400 | return matchesCommandShortcut(event, keybindings, "chat.new", options); | |
| 401 | } | |
| 402 | ||
| 403 | export function isOpenFavoriteEditorShortcut( | |
| 404 | event: ShortcutEventLike, | |
| 405 | keybindings: ResolvedKeybindings, | |
| 406 | options?: ShortcutMatchOptions, | |
| 407 | ): boolean { | |
| 408 | return matchesCommandShortcut(event, keybindings, "editor.openFavorite", options); | |
| 409 | } | |
| 410 | ||
| 395 | 411 | export function isTerminalClearShortcut( |
| 396 | 412 | event: ShortcutEventLike, |
| 397 | 413 | platform = navigator.platform, |
packages/contracts/src/keybindings.ts+2| ... | ... | @@ -4,6 +4,8 @@ export const keybindingCommandSchema = z.enum([ |
| 4 | 4 | "terminal.toggle", |
| 5 | 5 | "terminal.split", |
| 6 | 6 | "terminal.new", |
| 7 | "chat.new", | |
| 8 | "editor.openFavorite", | |
| 7 | 9 | ]); |
| 8 | 10 | |
| 9 | 11 | const keybindingValueSchema = z.string().trim().min(1).max(64); |