Просмотр исходного кода

fix(app): prioritize shortcuts in terminal (#35668)

Brendan Allan 2 месяцев назад
Родитель
Сommit
65fd2e5c91

+ 17 - 1
packages/app/src/context/command.test.ts

@@ -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)
+  })
+})

+ 16 - 5
packages/app/src/context/command.tsx

@@ -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
-          map.set(sig, option)
+          const existing = map.get(sig)
+          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

+ 17 - 0
packages/app/src/pages/session/use-session-commands.tsx

@@ -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"),