Skip to content

Commit 65fd2e5

Browse files
authored
fix(app): prioritize shortcuts in terminal (anomalyco#35668)
1 parent c5fe32f commit 65fd2e5

3 files changed

Lines changed: 50 additions & 6 deletions

File tree

packages/app/src/context/command.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test"
2-
import { upsertCommandRegistration } from "./command"
2+
import { resolveKeybindOption, upsertCommandRegistration } from "./command"
33

44
describe("upsertCommandRegistration", () => {
55
test("replaces keyed registrations", () => {
@@ -23,3 +23,19 @@ describe("upsertCommandRegistration", () => {
2323
expect(next[1]?.options).toBe(one)
2424
})
2525
})
26+
27+
describe("resolveKeybindOption", () => {
28+
test("prefers a matching contextual command over the global fallback", () => {
29+
const fallback = { id: "tab.close", title: "Close tab" }
30+
const contextual = { id: "terminal.close", title: "Close terminal", when: () => true }
31+
32+
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(contextual)
33+
})
34+
35+
test("uses the global fallback outside the command context", () => {
36+
const fallback = { id: "tab.close", title: "Close tab" }
37+
const contextual = { id: "terminal.close", title: "Close terminal", when: () => false }
38+
39+
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(fallback)
40+
})
41+
})

packages/app/src/context/command.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,15 @@ export interface CommandOption {
8282
suggested?: boolean
8383
disabled?: boolean
8484
hidden?: boolean
85+
when?: (event: KeyboardEvent) => boolean
8586
onSelect?: (source?: "palette" | "keybind" | "slash") => void
8687
onHighlight?: () => (() => void) | void
8788
}
8889

90+
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
91+
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
92+
}
93+
8994
type CommandSource = "palette" | "keybind" | "slash"
9095

9196
export type CommandCatalogItem = {
@@ -334,7 +339,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
334339
})
335340

336341
const keymap = createMemo(() => {
337-
const map = new Map<string, CommandOption>()
342+
const map = new Map<string, CommandOption[]>()
338343
for (const option of options()) {
339344
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
340345
if (option.disabled) continue
@@ -344,8 +349,12 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
344349
for (const kb of keybinds) {
345350
if (!kb.key) continue
346351
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
347-
if (map.has(sig)) continue
348-
map.set(sig, option)
352+
const existing = map.get(sig)
353+
if (existing) {
354+
existing.push(option)
355+
continue
356+
}
357+
map.set(sig, [option])
349358
}
350359
}
351360
return map
@@ -374,7 +383,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
374383

375384
const sig = signatureFromEvent(event)
376385
const isPalette = palette().has(sig)
377-
const option = keymap().get(sig)
386+
const option = resolveKeybindOption(keymap().get(sig), event)
378387
const modified = event.ctrlKey || event.metaKey || event.altKey
379388
const isTab = event.key === "Tab"
380389

@@ -383,17 +392,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
383392

384393
if (isPalette) {
385394
event.preventDefault()
395+
event.stopPropagation()
386396
showPalette()
387397
return
388398
}
389399

390400
if (!option) return
391401
event.preventDefault()
402+
event.stopPropagation()
392403
option.onSelect?.("keybind")
393404
}
394405

395406
onMount(() => {
396-
makeEventListener(document, "keydown", handleKeyDown)
407+
makeEventListener(document, "keydown", handleKeyDown, { capture: true })
397408
})
398409

399410
function register(cb: () => CommandOption[]): void

packages/app/src/pages/session/use-session-commands.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
269269
view().terminal.open()
270270
}
271271

272+
const closeTerminal = () => {
273+
const id = terminal.active()
274+
if (!id) return
275+
const last = terminal.all().length === 1
276+
void terminal.close(id)
277+
if (last) view().terminal.close()
278+
}
279+
272280
const chooseMcp = () => {
273281
void openDialog(
274282
() => import("@/components/dialog-select-mcp"),
@@ -518,6 +526,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
518526
]
519527

520528
const terminalCmds = () => [
529+
terminalCommand({
530+
id: "terminal.close",
531+
title: language.t("terminal.close"),
532+
keybind: "mod+w",
533+
hidden: true,
534+
when: (event) =>
535+
event.target instanceof Element && !!event.target.closest('[data-component="terminal"]'),
536+
onSelect: closeTerminal,
537+
}),
521538
terminalCommand({
522539
id: "terminal.new",
523540
title: language.t("command.terminal.new"),

0 commit comments

Comments
 (0)