Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/app/src/context/command.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { upsertCommandRegistration } from "./command"
import { resolveKeybindOption, upsertCommandRegistration } from "./command"

describe("upsertCommandRegistration", () => {
test("replaces keyed registrations", () => {
Expand All @@ -23,3 +23,19 @@ describe("upsertCommandRegistration", () => {
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)
})
})
21 changes: 16 additions & 5 deletions packages/app/src/context/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,15 @@ export interface CommandOption {
suggested?: boolean
disabled?: boolean
hidden?: boolean
when?: (event: KeyboardEvent) => boolean
onSelect?: (source?: "palette" | "keybind" | "slash") => 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"

export type CommandCatalogItem = {
Expand Down Expand Up @@ -334,7 +339,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
})

const keymap = createMemo(() => {
const map = new Map<string, CommandOption>()
const map = new Map<string, CommandOption[]>()
for (const option of options()) {
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
if (option.disabled) continue
Expand All @@ -344,8 +349,12 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
for (const kb of keybinds) {
if (!kb.key) continue
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
if (map.has(sig)) continue
map.set(sig, option)
const existing = map.get(sig)
if (existing) {
existing.push(option)
continue
}
map.set(sig, [option])
}
}
return map
Expand Down Expand Up @@ -374,7 +383,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex

const sig = signatureFromEvent(event)
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 isTab = event.key === "Tab"

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

if (isPalette) {
event.preventDefault()
event.stopPropagation()
showPalette()
return
}

if (!option) return
event.preventDefault()
event.stopPropagation()
option.onSelect?.("keybind")
}

onMount(() => {
makeEventListener(document, "keydown", handleKeyDown)
makeEventListener(document, "keydown", handleKeyDown, { capture: true })
})

function register(cb: () => CommandOption[]): void
Expand Down
17 changes: 17 additions & 0 deletions packages/app/src/pages/session/use-session-commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
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 = () => {
void openDialog(
() => import("@/components/dialog-select-mcp"),
Expand Down Expand Up @@ -518,6 +526,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
]

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({
id: "terminal.new",
title: language.t("command.terminal.new"),
Expand Down
Loading