History

Rename local chat shortcut and surface terminal close hints

- Replace `chat.newSameGitState` with `chat.newLocal` across contracts, server/web defaults, docs, and tests - Make `chat.new` preserve branch/worktree state while `chat.newLocal` creates a local thread - Show terminal close shortcut labels in drawer action tooltips and tab close controls

Julius Marminge committed 9 months ago

Commit bd99acd1 parent fb4afca10 files · +76 −57Browse files

Unified · Split

10 files
  1. KEYBINDINGS.md +3 −3
  2. apps/server/src/keybindings.test.ts +2 −2
  3. apps/server/src/keybindings.ts +1 −1
  4. apps/web/src/components/ChatView.tsx +5 −0
  5. apps/web/src/components/Sidebar.tsx +7 −7
  6. apps/web/src/components/ThreadTerminalDrawer.tsx +44 −36
  7. apps/web/src/keybindings.test.ts +5 −5
  8. apps/web/src/keybindings.ts +2 −2
  9. packages/contracts/src/keybindings.test.ts +6 −0
  10. packages/contracts/src/keybindings.ts +1 −1
KEYBINDINGS.md+3−3
@@ -24,7 +24,7 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`]( · 23 unmodified lines
2424 { "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" },
2525 { "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" },
2626 { "key": "mod+shift+o", "command": "chat.new" },
27− { "key": "mod+shift+n", "command": "chat.newSameGitState" },
27+ { "key": "mod+shift+n", "command": "chat.newLocal" },
2828 { "key": "mod+o", "command": "editor.openFavorite" }
2929 ]
3030 ```
@@ -49,8 +49,8 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged · 18 unmodified lines
4949 - `terminal.split`: split terminal (in focused terminal context by default)
5050 - `terminal.new`: create new terminal (in focused terminal context by default)
5151 - `terminal.close`: close/kill the focused terminal (in focused terminal context by default)
52−- `chat.new`: create a new chat thread for the active project
53−- `chat.newSameGitState`: create a new chat thread preserving the active thread's branch/worktree state
52+- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
53+- `chat.newLocal`: create a new local chat thread for the active project (no worktree context)
5454 - `editor.openFavorite`: open current project/worktree in the last-used editor
5555
5656 ### Key Syntax
apps/server/src/keybindings.test.ts+2−2
@@ -71,7 +71,7 @@ describe("server keybindings", () => { · 70 unmodified lines
7171 );
7272 });
7373
74− it("defines defaults for terminal.close and chat.newSameGitState", () => {
74+ it("defines defaults for terminal.close and chat.newLocal", () => {
7575 assert.deepInclude(DEFAULT_KEYBINDINGS, {
7676 key: "mod+w",
7777 command: "terminal.close",
@@ -79,7 +79,7 @@ describe("server keybindings", () => { · 1 unmodified line
7979 });
8080 assert.deepInclude(DEFAULT_KEYBINDINGS, {
8181 key: "mod+shift+n",
82− command: "chat.newSameGitState",
82+ command: "chat.newLocal",
8383 });
8484 });
8585 });
apps/server/src/keybindings.ts+1−1
@@ -30,7 +30,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ · 29 unmodified lines
3030 { key: "mod+n", command: "terminal.new", when: "terminalFocus" },
3131 { key: "mod+w", command: "terminal.close", when: "terminalFocus" },
3232 { key: "mod+shift+o", command: "chat.new" },
33− { key: "mod+shift+n", command: "chat.newSameGitState" },
33+ { key: "mod+shift+n", command: "chat.newLocal" },
3434 { key: "mod+o", command: "editor.openFavorite" },
3535 ];
3636
apps/web/src/components/ChatView.tsx+5−0
@@ -279,6 +279,10 @@ export default function ChatView() { · 278 unmodified lines
279279 () => shortcutLabelForCommand(keybindings, "terminal.new"),
280280 [keybindings],
281281 );
282+ const closeTerminalShortcutLabel = useMemo(
283+ () => shortcutLabelForCommand(keybindings, "terminal.close"),
284+ [keybindings],
285+ );
282286
283287 const envLocked = Boolean(
284288 activeThread &&
@@ -1464,6 +1468,7 @@ export default function ChatView() { · 1179 unmodified lines
14641468 onNewTerminal={createNewTerminal}
14651469 splitShortcutLabel={splitTerminalShortcutLabel ?? undefined}
14661470 newShortcutLabel={newTerminalShortcutLabel ?? undefined}
1471+ closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
14671472 onActiveTerminalChange={activateTerminal}
14681473 onCloseTerminal={closeTerminal}
14691474 onHeightChange={(height) =>
apps/web/src/components/Sidebar.tsx+7−7
@@ -7,7 +7,7 @@ import { useTheme } from "../hooks/useTheme"; · 6 unmodified lines
77 import { DEFAULT_MODEL } from "../model-logic";
88 import { derivePendingApprovals } from "../session-logic";
99 import { useStore } from "../store";
10−import { isChatNewSameGitStateShortcut, isChatNewShortcut } from "../keybindings";
10+import { isChatNewLocalShortcut, isChatNewShortcut } from "../keybindings";
1111 import {
1212 DEFAULT_THREAD_TERMINAL_HEIGHT,
1313 DEFAULT_THREAD_TERMINAL_ID,
@@ -388,14 +388,11 @@ export default function Sidebar() { · 374 unmodified lines
388388 useEffect(() => {
389389 const onWindowKeyDown = (event: KeyboardEvent) => {
390390 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
391− if (isChatNewSameGitStateShortcut(event, keybindings)) {
391+ if (isChatNewLocalShortcut(event, keybindings)) {
392392 const projectId = activeThread?.projectId ?? state.projects[0]?.id;
393393 if (!projectId) return;
394394 event.preventDefault();
395− handleNewThread(projectId, {
396− branch: activeThread?.branch ?? null,
397− worktreePath: activeThread?.worktreePath ?? null,
398− });
395+ handleNewThread(projectId);
399396 return;
400397 }
401398
@@ -403,7 +400,10 @@ export default function Sidebar() { · 1 unmodified line
403400 const projectId = activeThread?.projectId ?? state.projects[0]?.id;
404401 if (!projectId) return;
405402 event.preventDefault();
406− handleNewThread(projectId);
403+ handleNewThread(projectId, {
404+ branch: activeThread?.branch ?? null,
405+ worktreePath: activeThread?.worktreePath ?? null,
406+ });
407407 };
408408
409409 window.addEventListener("keydown", onWindowKeyDown);
apps/web/src/components/ThreadTerminalDrawer.tsx+44−36
@@ -1,5 +1,5 @@
11 import { FitAddon } from "@xterm/addon-fit";
2−import { Plus, SquareSplitHorizontal, TerminalSquare, Trash2 } from "lucide-react";
2+import { Plus, SquareSplitHorizontal, TerminalSquare, Trash2, XIcon } from "lucide-react";
33 import { type NativeApi } from "@t3tools/contracts";
44 import { Terminal, type ITheme } from "@xterm/xterm";
55 import {
@@ -18,10 +18,7 @@ import { · 12 unmodified lines
1818 preferredTerminalEditor,
1919 resolvePathLinkTarget,
2020 } from "../terminal-links";
21−import {
22− isTerminalClearShortcut,
23− terminalNavigationShortcutData,
24−} from "../keybindings";
21+import { isTerminalClearShortcut, terminalNavigationShortcutData } from "../keybindings";
2522 import {
2623 DEFAULT_THREAD_TERMINAL_HEIGHT,
2724 DEFAULT_THREAD_TERMINAL_ID,
@@ -161,10 +158,7 @@ function TerminalViewport({ · 133 unmodified lines
161158 try {
162159 await api.terminal.write({ threadId, terminalId, data });
163160 } catch (error) {
164− writeSystemMessage(
165− activeTerminal,
166− error instanceof Error ? error.message : fallbackError,
167− );
161+ writeSystemMessage(activeTerminal, error instanceof Error ? error.message : fallbackError);
168162 }
169163 };
170164
@@ -429,6 +423,7 @@ interface ThreadTerminalDrawerProps { · 258 unmodified lines
429423 onNewTerminal: () => void;
430424 splitShortcutLabel?: string | undefined;
431425 newShortcutLabel?: string | undefined;
426+ closeShortcutLabel?: string | undefined;
432427 onActiveTerminalChange: (terminalId: string) => void;
433428 onCloseTerminal: (terminalId: string) => void;
434429 onHeightChange: (height: number) => void;
@@ -444,15 +439,9 @@ interface TerminalActionButtonProps { · 9 unmodified lines
444439 function TerminalActionButton({ label, className, onClick, children }: TerminalActionButtonProps) {
445440 return (
446441 <Popover>
447− <PopoverTrigger openOnHover
448− render={
449− <button
450− type="button"
451− className={className}
452− onClick={onClick}
453− aria-label={label}
454− />
455− }
442+ <PopoverTrigger
443+ openOnHover
444+ render={<button type="button" className={className} onClick={onClick} aria-label={label} />}
456445 >
457446 {children}
458447 </PopoverTrigger>
@@ -483,6 +472,7 @@ export default function ThreadTerminalDrawer({ · 24 unmodified lines
483472 onNewTerminal,
484473 splitShortcutLabel,
485474 newShortcutLabel,
475+ closeShortcutLabel,
486476 onActiveTerminalChange,
487477 onCloseTerminal,
488478 onHeightChange,
@@ -598,6 +588,9 @@ export default function ThreadTerminalDrawer({ · 109 unmodified lines
598588 ),
599589 [normalizedTerminalIds],
600590 );
591+ const closeTerminalActionLabel = closeShortcutLabel
592+ ? `Close Terminal (${closeShortcutLabel})`
593+ : "Close Terminal";
601594
602595 useEffect(() => {
603596 onHeightChangeRef.current = onHeightChange;
@@ -709,7 +702,9 @@ export default function ThreadTerminalDrawer({ · 105 unmodified lines
709702 <TerminalActionButton
710703 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
711704 onClick={onSplitTerminal}
712− label={splitShortcutLabel ? `Split Terminal (${splitShortcutLabel})` : "Split Terminal"}
705+ label={
706+ splitShortcutLabel ? `Split Terminal (${splitShortcutLabel})` : "Split Terminal"
707+ }
713708 >
714709 <SquareSplitHorizontal className="size-3.25" />
715710 </TerminalActionButton>
@@ -725,7 +720,7 @@ export default function ThreadTerminalDrawer({ · 9 unmodified lines
725720 <TerminalActionButton
726721 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
727722 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}
728− label="Close Terminal"
723+ label={closeTerminalActionLabel}
729724 >
730725 <Trash2 className="size-3.25" />
731726 </TerminalActionButton>
@@ -805,16 +800,14 @@ export default function ThreadTerminalDrawer({ · 73 unmodified lines
805800 <TerminalActionButton
806801 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"
807802 onClick={onNewTerminal}
808− label={
809− newShortcutLabel ? `New Terminal (${newShortcutLabel})` : "New Terminal"
810− }
803+ label={newShortcutLabel ? `New Terminal (${newShortcutLabel})` : "New Terminal"}
811804 >
812805 <Plus className="size-3.25" />
813806 </TerminalActionButton>
814807 <TerminalActionButton
815808 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"
816809 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}
817− label="Close Terminal"
810+ label={closeTerminalActionLabel}
818811 >
819812 <Trash2 className="size-3.25" />
820813 </TerminalActionButton>
@@ -852,6 +845,9 @@ export default function ThreadTerminalDrawer({ · 31 unmodified lines
852845 >
853846 {terminalGroup.terminalIds.map((terminalId) => {
854847 const isActive = terminalId === resolvedActiveTerminalId;
848+ const closeTerminalLabel = `Close ${
849+ terminalLabelById.get(terminalId) ?? "terminal"
850+ }${isActive && closeShortcutLabel ? ` (${closeShortcutLabel})` : ""}`;
855851 return (
856852 <div
857853 key={terminalId}
@@ -875,18 +871,30 @@ export default function ThreadTerminalDrawer({ · 17 unmodified lines
875871 </span>
876872 </button>
877873 {normalizedTerminalIds.length > 1 && (
878− <button
879− type="button"
880− className="rounded px-1 text-xs font-medium leading-none text-muted-foreground opacity-0 transition hover:bg-accent hover:text-foreground group-hover:opacity-100"
881− onClick={(event) => {
882− event.stopPropagation();
883− onCloseTerminal(terminalId);
884− }}
885− aria-label={`Close ${terminalLabelById.get(terminalId) ?? "terminal"}`}
886− title={`Close ${terminalLabelById.get(terminalId) ?? "terminal"}`}
887− >
888− ×
889− </button>
874+ <Popover>
875+ <PopoverTrigger
876+ openOnHover
877+ render={
878+ <button
879+ type="button"
880+ className="inline-flex size-3.5 items-center justify-center rounded text-xs font-medium leading-none text-muted-foreground opacity-0 transition hover:bg-accent hover:text-foreground group-hover:opacity-100"
881+ onClick={() => onCloseTerminal(terminalId)}
882+ aria-label={closeTerminalLabel}
883+ />
884+ }
885+ >
886+ <XIcon className="size-2.5" />
887+ </PopoverTrigger>
888+ <PopoverPopup
889+ tooltipStyle
890+ side="bottom"
891+ sideOffset={6}
892+ align="center"
893+ className="pointer-events-none select-none"
894+ >
895+ {closeTerminalLabel}
896+ </PopoverPopup>
897+ </Popover>
890898 )}
891899 </div>
892900 );
apps/web/src/keybindings.test.ts+5−5
@@ -7,9 +7,9 @@ import { · 6 unmodified lines
77 type ResolvedKeybindingsConfig,
88 } from "@t3tools/contracts";
99 import {
10− isChatNewSameGitStateShortcut,
1110 formatShortcutLabel,
1211 isChatNewShortcut,
12+ isChatNewLocalShortcut,
1313 isOpenFavoriteEditorShortcut,
1414 isTerminalClearShortcut,
1515 isTerminalCloseShortcut,
@@ -91,7 +91,7 @@ const DEFAULT_BINDINGS = compile([ · 75 unmodified lines
9191 whenAst: whenIdentifier("terminalFocus"),
9292 },
9393 { shortcut: modShortcut("o", { shiftKey: true }), command: "chat.new" },
94− { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newSameGitState" },
94+ { shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newLocal" },
9595 { shortcut: modShortcut("o"), command: "editor.openFavorite" },
9696 ]);
9797
@@ -245,9 +245,9 @@ describe("chat/editor shortcuts", () => { · 147 unmodified lines
245245 );
246246 });
247247
248− it("matches chat.newSameGitState shortcut", () => {
248+ it("matches chat.newLocal shortcut", () => {
249249 assert.isTrue(
250− isChatNewSameGitStateShortcut(
250+ isChatNewLocalShortcut(
251251 event({ key: "n", metaKey: true, shiftKey: true }),
252252 DEFAULT_BINDINGS,
253253 {
@@ -256,7 +256,7 @@ describe("chat/editor shortcuts", () => { · 2 unmodified lines
256256 ),
257257 );
258258 assert.isTrue(
259− isChatNewSameGitStateShortcut(
259+ isChatNewLocalShortcut(
260260 event({ key: "n", ctrlKey: true, shiftKey: true }),
261261 DEFAULT_BINDINGS,
262262 {
apps/web/src/keybindings.ts+2−2
@@ -206,12 +206,12 @@ export function isChatNewShortcut( · 205 unmodified lines
206206 return matchesCommandShortcut(event, keybindings, "chat.new", options);
207207 }
208208
209−export function isChatNewSameGitStateShortcut(
209+export function isChatNewLocalShortcut(
210210 event: ShortcutEventLike,
211211 keybindings: ResolvedKeybindingsConfig,
212212 options?: ShortcutMatchOptions,
213213 ): boolean {
214− return matchesCommandShortcut(event, keybindings, "chat.newSameGitState", options);
214+ return matchesCommandShortcut(event, keybindings, "chat.newLocal", options);
215215 }
216216
217217 export function isOpenFavoriteEditorShortcut(
packages/contracts/src/keybindings.test.ts+6−0
@@ -20,6 +20,12 @@ describe("keybindings contracts", () => { · 19 unmodified lines
2020 command: "terminal.close",
2121 });
2222 assert.strictEqual(parsedClose.command, "terminal.close");
23+
24+ const parsedLocal = keybindingRuleSchema.parse({
25+ key: "mod+shift+n",
26+ command: "chat.newLocal",
27+ });
28+ assert.strictEqual(parsedLocal.command, "chat.newLocal");
2329 });
2430
2531 it("rejects invalid command values", () => {
packages/contracts/src/keybindings.ts+1−1
@@ -6,7 +6,7 @@ export const keybindingCommandSchema = z.enum([ · 5 unmodified lines
66 "terminal.new",
77 "terminal.close",
88 "chat.new",
9− "chat.newSameGitState",
9+ "chat.newLocal",
1010 "editor.openFavorite",
1111 ]);
1212

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t