diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index ea35af060b..1d5fc5be3e 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -117,6 +117,7 @@ import { restoreGlobalNavigationEntry } from "@/lib/navigation/globalNavigationE import { isBrowserReloadShortcut, isBrowserTaskManagerShortcut, + isCloseAllTabsShortcut, isCloseOtherTabsShortcut, isCloseTabShortcut, isCloseWindowShortcut, @@ -4360,6 +4361,12 @@ async function handleKeydown(e: KeyboardEvent) { appTabBarRef.value?.closeOtherActiveTabs(); return; } + if (isCloseAllTabsShortcut(e, shortcuts)) { + e.preventDefault(); + e.stopPropagation(); + appTabBarRef.value?.closeAllTabs(); + return; + } if (isDisconnectAllActiveConnectionsShortcut(e, shortcuts)) { e.preventDefault(); e.stopPropagation(); diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index 9d03dbcf6e..d076f7d517 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -177,7 +177,12 @@ function closeOtherActiveTabs() { queryStore.closeTab(activeTabId); } -defineExpose({ closeOtherActiveTabs }); +function closeAllTabs() { + closeSpecialRegularSurfaces(); + queryStore.closeAllTabs(); +} + +defineExpose({ closeOtherActiveTabs, closeAllTabs }); function handleSaveAndClose() { const id = queryStore.saveAndClosePendingTab(); diff --git a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts index 1d7ef204a8..4a6af32707 100644 --- a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { isCloseAllTabsShortcut } from "@/lib/editor/keyboardShortcuts"; import { closeOtherTabsDefaultShortcut, countShortcutConflictPairs, @@ -291,6 +292,25 @@ describe("shortcutRegistry editor actions", () => { expect(normalizeShortcutSettings({ closeWindow: "" }).closeWindow).toBe(""); }); + it("registers closeAllTabs as an unbound global shortcut that users can assign", () => { + const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "closeAllTabs"); + expect(definition).toMatchObject({ + id: "closeAllTabs", + labelKey: "contextMenu.closeAllTabs", + scope: "global", + defaultShortcut: "", + }); + // 无默认键位:保持未绑定状态,由用户在设置里自行分配 + expect(DEFAULT_SHORTCUT_SETTINGS.closeAllTabs).toBe(""); + expect(normalizeShortcutSettings({}).closeAllTabs).toBe(""); + expect(normalizeShortcutSettings({ closeAllTabs: "Shift+Mod+Alt+W" }).closeAllTabs).toBe("Shift+Mod+Alt+W"); + expect(normalizeShortcutSettings({ closeAllTabs: "" }).closeAllTabs).toBe(""); + const event = { key: "w", shiftKey: true, metaKey: true, altKey: true }; + // 显式分配后生效;默认未绑定时永不触发 + expect(isCloseAllTabsShortcut(event, { closeAllTabs: "Shift+Mod+Alt+W" }, "MacIntel")).toBe(true); + expect(isCloseAllTabsShortcut(event, {}, "MacIntel")).toBe(false); + }); + it("normalizes custom, cleared, and invalid modifier-only shortcuts", () => { expect(normalizeShortcutSettings({ openDataInNewTab: "Shift" }).openDataInNewTab).toBe("Shift"); expect(normalizeShortcutSettings({ openDataInNewTab: "" }).openDataInNewTab).toBe(""); diff --git a/apps/desktop/src/lib/editor/keyboardShortcuts.ts b/apps/desktop/src/lib/editor/keyboardShortcuts.ts index 6a6f5b0224..121d3c32c3 100644 --- a/apps/desktop/src/lib/editor/keyboardShortcuts.ts +++ b/apps/desktop/src/lib/editor/keyboardShortcuts.ts @@ -188,6 +188,10 @@ export function isCloseOtherTabsShortcut(event: ShortcutLikeEvent, shortcuts?: P return matchesShortcut(event, actionShortcut("closeOtherTabs", shortcuts), platform); } +export function isCloseAllTabsShortcut(event: ShortcutLikeEvent, shortcuts?: Partial, platform = globalThis.navigator?.platform || ""): boolean { + return matchesShortcut(event, actionShortcut("closeAllTabs", shortcuts), platform); +} + export function isCloseWindowShortcut(event: ShortcutLikeEvent, shortcuts?: Partial): boolean { return matchesShortcut(event, actionShortcut("closeWindow", shortcuts)); } diff --git a/apps/desktop/src/lib/editor/shortcutRegistry.ts b/apps/desktop/src/lib/editor/shortcutRegistry.ts index 9a44287ca8..352b8bb793 100644 --- a/apps/desktop/src/lib/editor/shortcutRegistry.ts +++ b/apps/desktop/src/lib/editor/shortcutRegistry.ts @@ -52,6 +52,7 @@ export type ShortcutActionId = | "openSettings" | "closeTab" | "closeOtherTabs" + | "closeAllTabs" | "closeWindow" | "focusSearch" | "quickOpen" @@ -477,6 +478,14 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ scope: "global", defaultShortcut: closeOtherTabsDefaultShortcut(), }, + { + // VS Code's close-all-tabs chord (Cmd/Ctrl+K W) needs chord support the + // registry doesn't have, so this action ships unbound and stays optional. + id: "closeAllTabs", + labelKey: "contextMenu.closeAllTabs", + scope: "global", + defaultShortcut: "", + }, { id: "closeWindow", labelKey: "settings.shortcutCloseWindow",