fix(app): prioritize shortcuts in terminal (#35668)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { upsertCommandRegistration } from "./command"
|
import { resolveKeybindOption, upsertCommandRegistration } from "./command"
|
||||||
|
|
||||||
describe("upsertCommandRegistration", () => {
|
describe("upsertCommandRegistration", () => {
|
||||||
test("replaces keyed registrations", () => {
|
test("replaces keyed registrations", () => {
|
||||||
@@ -23,3 +23,19 @@ describe("upsertCommandRegistration", () => {
|
|||||||
expect(next[1]?.options).toBe(one)
|
expect(next[1]?.options).toBe(one)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("resolveKeybindOption", () => {
|
||||||
|
test("prefers a matching contextual command over the global fallback", () => {
|
||||||
|
const fallback = { id: "tab.close", title: "Close tab" }
|
||||||
|
const contextual = { id: "terminal.close", title: "Close terminal", when: () => true }
|
||||||
|
|
||||||
|
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(contextual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the global fallback outside the command context", () => {
|
||||||
|
const fallback = { id: "tab.close", title: "Close tab" }
|
||||||
|
const contextual = { id: "terminal.close", title: "Close terminal", when: () => false }
|
||||||
|
|
||||||
|
expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(fallback)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -82,10 +82,15 @@ export interface CommandOption {
|
|||||||
suggested?: boolean
|
suggested?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
|
when?: (event: KeyboardEvent) => boolean
|
||||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||||
onHighlight?: () => (() => void) | void
|
onHighlight?: () => (() => void) | void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
|
||||||
|
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
||||||
|
}
|
||||||
|
|
||||||
type CommandSource = "palette" | "keybind" | "slash"
|
type CommandSource = "palette" | "keybind" | "slash"
|
||||||
|
|
||||||
export type CommandCatalogItem = {
|
export type CommandCatalogItem = {
|
||||||
@@ -334,7 +339,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
})
|
})
|
||||||
|
|
||||||
const keymap = createMemo(() => {
|
const keymap = createMemo(() => {
|
||||||
const map = new Map<string, CommandOption>()
|
const map = new Map<string, CommandOption[]>()
|
||||||
for (const option of options()) {
|
for (const option of options()) {
|
||||||
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
|
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
|
||||||
if (option.disabled) continue
|
if (option.disabled) continue
|
||||||
@@ -344,8 +349,12 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
for (const kb of keybinds) {
|
for (const kb of keybinds) {
|
||||||
if (!kb.key) continue
|
if (!kb.key) continue
|
||||||
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
|
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
|
||||||
if (map.has(sig)) continue
|
const existing = map.get(sig)
|
||||||
map.set(sig, option)
|
if (existing) {
|
||||||
|
existing.push(option)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
map.set(sig, [option])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
@@ -374,7 +383,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
|
|
||||||
const sig = signatureFromEvent(event)
|
const sig = signatureFromEvent(event)
|
||||||
const isPalette = palette().has(sig)
|
const isPalette = palette().has(sig)
|
||||||
const option = keymap().get(sig)
|
const option = resolveKeybindOption(keymap().get(sig), event)
|
||||||
const modified = event.ctrlKey || event.metaKey || event.altKey
|
const modified = event.ctrlKey || event.metaKey || event.altKey
|
||||||
const isTab = event.key === "Tab"
|
const isTab = event.key === "Tab"
|
||||||
|
|
||||||
@@ -383,17 +392,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
|
|
||||||
if (isPalette) {
|
if (isPalette) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
showPalette()
|
showPalette()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!option) return
|
if (!option) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
option.onSelect?.("keybind")
|
option.onSelect?.("keybind")
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
makeEventListener(document, "keydown", handleKeyDown)
|
makeEventListener(document, "keydown", handleKeyDown, { capture: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
function register(cb: () => CommandOption[]): void
|
function register(cb: () => CommandOption[]): void
|
||||||
|
|||||||
@@ -269,6 +269,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
view().terminal.open()
|
view().terminal.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeTerminal = () => {
|
||||||
|
const id = terminal.active()
|
||||||
|
if (!id) return
|
||||||
|
const last = terminal.all().length === 1
|
||||||
|
void terminal.close(id)
|
||||||
|
if (last) view().terminal.close()
|
||||||
|
}
|
||||||
|
|
||||||
const chooseMcp = () => {
|
const chooseMcp = () => {
|
||||||
void openDialog(
|
void openDialog(
|
||||||
() => import("@/components/dialog-select-mcp"),
|
() => import("@/components/dialog-select-mcp"),
|
||||||
@@ -518,6 +526,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const terminalCmds = () => [
|
const terminalCmds = () => [
|
||||||
|
terminalCommand({
|
||||||
|
id: "terminal.close",
|
||||||
|
title: language.t("terminal.close"),
|
||||||
|
keybind: "mod+w",
|
||||||
|
hidden: true,
|
||||||
|
when: (event) =>
|
||||||
|
event.target instanceof Element && !!event.target.closest('[data-component="terminal"]'),
|
||||||
|
onSelect: closeTerminal,
|
||||||
|
}),
|
||||||
terminalCommand({
|
terminalCommand({
|
||||||
id: "terminal.new",
|
id: "terminal.new",
|
||||||
title: language.t("command.terminal.new"),
|
title: language.t("command.terminal.new"),
|
||||||
|
|||||||
Reference in New Issue
Block a user