diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index ca74e3c1..791c25bd 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -40,6 +40,7 @@ import { useUiPreferencesContext } from "@/contexts/UiPreferencesContext"; import { defaultSizes, SplitView, type RowColSizes } from "@/shell/SplitView"; import { renderTabContent } from "@/shell/tabUtils"; import { TabBar } from "@/shell/TabBar"; +import { dispatchCtrlW, isShiftKey } from "@/lib/app-keyboard-shortcuts"; // Shell surfaces that are not needed for first paint. const CommandPalette = lazy(() => @@ -466,9 +467,10 @@ export function AppShell({ activeTabIdRef.current = activeTabId; }, [activeTabId]); useEffect(() => { - return window.electronAPI?.onCloseActiveTab?.(() => - closeActiveTabRef.current(), - ); + return window.electronAPI?.onCloseActiveTab?.(() => { + if (dispatchCtrlW(document.activeElement)) return; + closeActiveTabRef.current(); + }); }, []); const skipSplitSyncRef = useRef(false); useEffect(() => { @@ -575,7 +577,7 @@ export function AppShell({ // hard to discover. useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "ShiftLeft" && !e.repeat) { + if (isShiftKey(e) && !e.repeat) { const now = Date.now(); if (now - lastShiftTime.current < 300 && commandPaletteShortcutEnabled) setCommandPaletteOpen((prev) => !prev); diff --git a/src/ui/lib/app-keyboard-shortcuts.ts b/src/ui/lib/app-keyboard-shortcuts.ts new file mode 100644 index 00000000..ca421ca1 --- /dev/null +++ b/src/ui/lib/app-keyboard-shortcuts.ts @@ -0,0 +1,20 @@ +export function isShiftKey(event: Pick) { + return ( + event.key === "Shift" || + event.code === "ShiftLeft" || + event.code === "ShiftRight" + ); +} + +export function dispatchCtrlW(target: EventTarget | null) { + if (!target) return false; + + const event = new KeyboardEvent("keydown", { + key: "w", + code: "KeyW", + ctrlKey: true, + bubbles: true, + cancelable: true, + }); + return !target.dispatchEvent(event); +} diff --git a/src/ui/shell/CommandPalette.tsx b/src/ui/shell/CommandPalette.tsx index 201206f5..b73a5b7a 100644 --- a/src/ui/shell/CommandPalette.tsx +++ b/src/ui/shell/CommandPalette.tsx @@ -140,6 +140,7 @@ export function CommandPalette({ [], ); const [snippets, setSnippets] = useState([]); + const [selectedValue, setSelectedValue] = useState(""); const { runSnippet, dialog: runSnippetDialog } = useSnippetRunner(); useEffect(() => { @@ -207,6 +208,20 @@ export function CommandPalette({ }) : []; + useEffect(() => { + if (!isOpen) return; + const firstHost = filteredHosts[0]; + if (search.trim() && firstHost) { + setSelectedValue(`host-${firstHost.id}`); + return; + } + setSelectedValue( + window.electronAPI?.isElectron + ? "quick-action-local-terminal" + : "quick-action-add-host", + ); + }, [filteredHosts, isOpen, search]); + const activeTargetTab = terminalTabs.find((tab) => tab.id === activeTabId) ?? terminalTabs[0]; @@ -234,7 +249,13 @@ export function CommandPalette({ )} onClick={(e) => e.stopPropagation()} > - +
{ + it.each(["ShiftLeft", "ShiftRight"])( + "accepts %s for double Shift", + (code) => { + expect(isShiftKey({ key: "Shift", code })).toBe(true); + }, + ); + + it("does not classify other keys as Shift", () => { + expect(isShiftKey({ key: "w", code: "KeyW" })).toBe(false); + }); + + it("lets the focused control consume Electron Ctrl+W", () => { + const target = document.createElement("input"); + const listener = vi.fn((event: KeyboardEvent) => event.preventDefault()); + target.addEventListener("keydown", listener); + + expect(dispatchCtrlW(target)).toBe(true); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ key: "w", code: "KeyW", ctrlKey: true }), + ); + }); + + it("falls back to closing the active tab when Ctrl+W is not consumed", () => { + expect(dispatchCtrlW(document.createElement("div"))).toBe(false); + }); +});