diff --git a/KEYBINDINGS.md b/KEYBINDINGS.md index 7734d4ebe90179f5babb1724f2180587d36aadb9..16fce1a4d9f9016558ac4c7666b4390e68a72c87 100644 --- a/KEYBINDINGS.md +++ b/KEYBINDINGS.md @@ -24,7 +24,7 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`]( { "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" }, { "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" }, { "key": "mod+shift+o", "command": "chat.new" }, - { "key": "mod+shift+n", "command": "chat.newSameGitState" }, + { "key": "mod+shift+n", "command": "chat.newLocal" }, { "key": "mod+o", "command": "editor.openFavorite" } ] ``` @@ -49,8 +49,8 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged - `terminal.split`: split terminal (in focused terminal context by default) - `terminal.new`: create new terminal (in focused terminal context by default) - `terminal.close`: close/kill the focused terminal (in focused terminal context by default) -- `chat.new`: create a new chat thread for the active project -- `chat.newSameGitState`: create a new chat thread preserving the active thread's branch/worktree state +- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state +- `chat.newLocal`: create a new local chat thread for the active project (no worktree context) - `editor.openFavorite`: open current project/worktree in the last-used editor ### Key Syntax diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index 18ecbabc03970e539c00a3e22c5652f89a6266bf..364ebc4862cdf48d63f3a048fe0ea8460b0a7122 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -71,7 +71,7 @@ describe("server keybindings", () => { ); }); - it("defines defaults for terminal.close and chat.newSameGitState", () => { + it("defines defaults for terminal.close and chat.newLocal", () => { assert.deepInclude(DEFAULT_KEYBINDINGS, { key: "mod+w", command: "terminal.close", @@ -79,7 +79,7 @@ describe("server keybindings", () => { }); assert.deepInclude(DEFAULT_KEYBINDINGS, { key: "mod+shift+n", - command: "chat.newSameGitState", + command: "chat.newLocal", }); }); }); diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 5750b4f4bea65f97d48c2468831def0d8476b368..a78e25b31b6a67d5d00e1c0b28e2064c3a67ac5a 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -30,7 +30,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, { key: "mod+w", command: "terminal.close", when: "terminalFocus" }, { key: "mod+shift+o", command: "chat.new" }, - { key: "mod+shift+n", command: "chat.newSameGitState" }, + { key: "mod+shift+n", command: "chat.newLocal" }, { key: "mod+o", command: "editor.openFavorite" }, ]; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d308ec63f5d21607cdfa2be7bee2b59c09e28303..ccbbd10d780745eb86650a560404ded6460a37c8 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -279,6 +279,10 @@ export default function ChatView() { () => shortcutLabelForCommand(keybindings, "terminal.new"), [keybindings], ); + const closeTerminalShortcutLabel = useMemo( + () => shortcutLabelForCommand(keybindings, "terminal.close"), + [keybindings], + ); const envLocked = Boolean( activeThread && @@ -1464,6 +1468,7 @@ export default function ChatView() { onNewTerminal={createNewTerminal} splitShortcutLabel={splitTerminalShortcutLabel ?? undefined} newShortcutLabel={newTerminalShortcutLabel ?? undefined} + closeShortcutLabel={closeTerminalShortcutLabel ?? undefined} onActiveTerminalChange={activateTerminal} onCloseTerminal={closeTerminal} onHeightChange={(height) => diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 2a3deef38b05c89bb07949449bb611b8140f776b..2c29a6da4fef1e01c1bc0e2696def26ef5e29a8a 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -7,7 +7,7 @@ import { useTheme } from "../hooks/useTheme"; import { DEFAULT_MODEL } from "../model-logic"; import { derivePendingApprovals } from "../session-logic"; import { useStore } from "../store"; -import { isChatNewSameGitStateShortcut, isChatNewShortcut } from "../keybindings"; +import { isChatNewLocalShortcut, isChatNewShortcut } from "../keybindings"; import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, @@ -388,14 +388,11 @@ export default function Sidebar() { useEffect(() => { const onWindowKeyDown = (event: KeyboardEvent) => { const activeThread = state.threads.find((t) => t.id === state.activeThreadId); - if (isChatNewSameGitStateShortcut(event, keybindings)) { + if (isChatNewLocalShortcut(event, keybindings)) { const projectId = activeThread?.projectId ?? state.projects[0]?.id; if (!projectId) return; event.preventDefault(); - handleNewThread(projectId, { - branch: activeThread?.branch ?? null, - worktreePath: activeThread?.worktreePath ?? null, - }); + handleNewThread(projectId); return; } @@ -403,7 +400,10 @@ export default function Sidebar() { const projectId = activeThread?.projectId ?? state.projects[0]?.id; if (!projectId) return; event.preventDefault(); - handleNewThread(projectId); + handleNewThread(projectId, { + branch: activeThread?.branch ?? null, + worktreePath: activeThread?.worktreePath ?? null, + }); }; window.addEventListener("keydown", onWindowKeyDown); diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index d74ba49a0800f221529cdfa1abe582bbeafefa40..1c35459a9b11fab296db05e6cf7485f008df48d1 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -1,5 +1,5 @@ import { FitAddon } from "@xterm/addon-fit"; -import { Plus, SquareSplitHorizontal, TerminalSquare, Trash2 } from "lucide-react"; +import { Plus, SquareSplitHorizontal, TerminalSquare, Trash2, XIcon } from "lucide-react"; import { type NativeApi } from "@t3tools/contracts"; import { Terminal, type ITheme } from "@xterm/xterm"; import { @@ -18,10 +18,7 @@ import { preferredTerminalEditor, resolvePathLinkTarget, } from "../terminal-links"; -import { - isTerminalClearShortcut, - terminalNavigationShortcutData, -} from "../keybindings"; +import { isTerminalClearShortcut, terminalNavigationShortcutData } from "../keybindings"; import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, @@ -161,10 +158,7 @@ function TerminalViewport({ try { await api.terminal.write({ threadId, terminalId, data }); } catch (error) { - writeSystemMessage( - activeTerminal, - error instanceof Error ? error.message : fallbackError, - ); + writeSystemMessage(activeTerminal, error instanceof Error ? error.message : fallbackError); } }; @@ -429,6 +423,7 @@ interface ThreadTerminalDrawerProps { onNewTerminal: () => void; splitShortcutLabel?: string | undefined; newShortcutLabel?: string | undefined; + closeShortcutLabel?: string | undefined; onActiveTerminalChange: (terminalId: string) => void; onCloseTerminal: (terminalId: string) => void; onHeightChange: (height: number) => void; @@ -444,15 +439,9 @@ interface TerminalActionButtonProps { function TerminalActionButton({ label, className, onClick, children }: TerminalActionButtonProps) { return ( - - } + } > {children} @@ -483,6 +472,7 @@ export default function ThreadTerminalDrawer({ onNewTerminal, splitShortcutLabel, newShortcutLabel, + closeShortcutLabel, onActiveTerminalChange, onCloseTerminal, onHeightChange, @@ -598,6 +588,9 @@ export default function ThreadTerminalDrawer({ ), [normalizedTerminalIds], ); + const closeTerminalActionLabel = closeShortcutLabel + ? `Close Terminal (${closeShortcutLabel})` + : "Close Terminal"; useEffect(() => { onHeightChangeRef.current = onHeightChange; @@ -709,7 +702,9 @@ export default function ThreadTerminalDrawer({ @@ -725,7 +720,7 @@ export default function ThreadTerminalDrawer({ onCloseTerminal(resolvedActiveTerminalId)} - label="Close Terminal" + label={closeTerminalActionLabel} > @@ -805,16 +800,14 @@ export default function ThreadTerminalDrawer({ onCloseTerminal(resolvedActiveTerminalId)} - label="Close Terminal" + label={closeTerminalActionLabel} > @@ -852,6 +845,9 @@ export default function ThreadTerminalDrawer({ > {terminalGroup.terminalIds.map((terminalId) => { const isActive = terminalId === resolvedActiveTerminalId; + const closeTerminalLabel = `Close ${ + terminalLabelById.get(terminalId) ?? "terminal" + }${isActive && closeShortcutLabel ? ` (${closeShortcutLabel})` : ""}`; return (
{normalizedTerminalIds.length > 1 && ( - + + onCloseTerminal(terminalId)} + aria-label={closeTerminalLabel} + /> + } + > + + + + {closeTerminalLabel} + + )}
); diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index 25df37f4316b54dd55753dcc57a91dade6c8d589..2a3b9c3244e4dbba13dc5252971c884c056e8324 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -7,9 +7,9 @@ import { type ResolvedKeybindingsConfig, } from "@t3tools/contracts"; import { - isChatNewSameGitStateShortcut, formatShortcutLabel, isChatNewShortcut, + isChatNewLocalShortcut, isOpenFavoriteEditorShortcut, isTerminalClearShortcut, isTerminalCloseShortcut, @@ -91,7 +91,7 @@ const DEFAULT_BINDINGS = compile([ whenAst: whenIdentifier("terminalFocus"), }, { shortcut: modShortcut("o", { shiftKey: true }), command: "chat.new" }, - { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newSameGitState" }, + { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newLocal" }, { shortcut: modShortcut("o"), command: "editor.openFavorite" }, ]); @@ -245,9 +245,9 @@ describe("chat/editor shortcuts", () => { ); }); - it("matches chat.newSameGitState shortcut", () => { + it("matches chat.newLocal shortcut", () => { assert.isTrue( - isChatNewSameGitStateShortcut( + isChatNewLocalShortcut( event({ key: "n", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, { @@ -256,7 +256,7 @@ describe("chat/editor shortcuts", () => { ), ); assert.isTrue( - isChatNewSameGitStateShortcut( + isChatNewLocalShortcut( event({ key: "n", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, { diff --git a/apps/web/src/keybindings.ts b/apps/web/src/keybindings.ts index 070dcae67b1585f3c4b2a09287423c26c0c33306..859602e97cb6f9afb8e5a65b555cd8ebb1003f5d 100644 --- a/apps/web/src/keybindings.ts +++ b/apps/web/src/keybindings.ts @@ -206,12 +206,12 @@ export function isChatNewShortcut( return matchesCommandShortcut(event, keybindings, "chat.new", options); } -export function isChatNewSameGitStateShortcut( +export function isChatNewLocalShortcut( event: ShortcutEventLike, keybindings: ResolvedKeybindingsConfig, options?: ShortcutMatchOptions, ): boolean { - return matchesCommandShortcut(event, keybindings, "chat.newSameGitState", options); + return matchesCommandShortcut(event, keybindings, "chat.newLocal", options); } export function isOpenFavoriteEditorShortcut( diff --git a/packages/contracts/src/keybindings.test.ts b/packages/contracts/src/keybindings.test.ts index 7e782a3dbd9f40176795b832045f6d250b4e3c9b..12726ea720b8f7e0e3cad46e9f50d49f53ea6baf 100644 --- a/packages/contracts/src/keybindings.test.ts +++ b/packages/contracts/src/keybindings.test.ts @@ -20,6 +20,12 @@ describe("keybindings contracts", () => { command: "terminal.close", }); assert.strictEqual(parsedClose.command, "terminal.close"); + + const parsedLocal = keybindingRuleSchema.parse({ + key: "mod+shift+n", + command: "chat.newLocal", + }); + assert.strictEqual(parsedLocal.command, "chat.newLocal"); }); it("rejects invalid command values", () => { diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 62dc84062959d5a8a0e4d461b3f66502121a4cbf..d9d8bfbf9eefd4228dfa9e8fdc1907d315c39f52 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -6,7 +6,7 @@ export const keybindingCommandSchema = z.enum([ "terminal.new", "terminal.close", "chat.new", - "chat.newSameGitState", + "chat.newLocal", "editor.openFavorite", ]);