From 291c8969a5cba03fb38e759a3b57f8e19ecf860b Mon Sep 17 00:00:00 2001 From: Matt Cowger Date: Mon, 3 Aug 2026 13:04:09 +0000 Subject: [PATCH] feat(app): close settings with Escape --- .../e2e/browser/settings-navigation.spec.ts | 7 +++++++ .../app/src/hooks/use-keyboard-shortcuts.ts | 16 +++++++++++++- .../app/src/keyboard/route-shortcut.test.ts | 21 +++++++++++++++++++ packages/app/src/keyboard/route-shortcut.ts | 7 +++++++ packages/app/src/lib/overlay-root.ts | 4 ++++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/browser/settings-navigation.spec.ts b/packages/app/e2e/browser/settings-navigation.spec.ts index 3657f22936..44c4248984 100644 --- a/packages/app/e2e/browser/settings-navigation.spec.ts +++ b/packages/app/e2e/browser/settings-navigation.spec.ts @@ -130,6 +130,13 @@ test.describe("Settings sidebar navigation", () => { await clickSettingsBackToWorkspace(page); await expect(page).not.toHaveURL(/\/settings(\/|$)/); }); + + test("pressing Escape closes settings", async ({ page }) => { + await gotoAppShell(page); + await openSettings(page); + await page.keyboard.press("Escape"); + await expect(page).not.toHaveURL(/\/settings(\/|$)/); + }); }); test.describe("Settings — compact master-detail", () => { diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index 243c366af7..5e2650847f 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -32,6 +32,8 @@ import { isNative } from "@/constants/platform"; import { keyboardShortcutsAvailable } from "@/keyboard/availability"; import { getDesktopHost, isElectronRuntime } from "@/desktop/host"; import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime"; +import { buildOpenProjectRoute } from "@/utils/host-routes"; +import { hasActiveWebOverlay } from "@/lib/overlay-root"; import { type ActiveWorkspaceSelection, navigateToLastWorkspace, @@ -160,7 +162,11 @@ export function useKeyboardShortcuts({ navigateToWorkspace({ serverId: action.serverId, workspaceId: action.workspaceId }); return true; case "navigate-last-workspace": - return navigateToLastWorkspace(); + if (navigateToLastWorkspace()) { + return true; + } + router.replace(buildOpenProjectRoute()); + return true; case "router-replace": router.replace(action.route as Parameters[0]); return true; @@ -306,6 +312,14 @@ export function useKeyboardShortcuts({ } const key = event.key ?? ""; + if ( + key === "Escape" && + pathname.startsWith("/settings") && + !isMobile && + hasActiveWebOverlay() + ) { + return; + } if (key === badgeModifierKey && !event.shiftKey) { setBadgeModifierDown(true); } diff --git a/packages/app/src/keyboard/route-shortcut.test.ts b/packages/app/src/keyboard/route-shortcut.test.ts index 5ace809b91..917935f3a3 100644 --- a/packages/app/src/keyboard/route-shortcut.test.ts +++ b/packages/app/src/keyboard/route-shortcut.test.ts @@ -55,6 +55,27 @@ describe("routeKeyboardShortcut — dispatch passthroughs", () => { action: expected, }); }); + + it("closes desktop settings when Escape routes through agent interrupt", () => { + expect( + routeKeyboardShortcut( + { action: "agent.interrupt", payload: null }, + makeCtx({ pathname: "/settings/general" }), + ), + ).toEqual({ kind: "navigate-last-workspace" }); + }); + + it("keeps agent interrupt behavior on compact settings layouts", () => { + expect( + routeKeyboardShortcut( + { action: "agent.interrupt", payload: null }, + makeCtx({ pathname: "/settings/general", isMobile: true }), + ), + ).toEqual({ + kind: "dispatch", + action: { id: "agent.interrupt", scope: "global" }, + }); + }); }); describe("routeKeyboardShortcut — workspace.tab.navigate", () => { diff --git a/packages/app/src/keyboard/route-shortcut.ts b/packages/app/src/keyboard/route-shortcut.ts index 71b4395cca..ecbfb76502 100644 --- a/packages/app/src/keyboard/route-shortcut.ts +++ b/packages/app/src/keyboard/route-shortcut.ts @@ -173,6 +173,13 @@ export function routeKeyboardShortcut( ): ShortcutAction { const passthrough = PASSTHROUGH_DISPATCH[input.action]; if (passthrough) { + if ( + input.action === "agent.interrupt" && + ctx.pathname.startsWith("/settings") && + !ctx.isMobile + ) { + return { kind: "navigate-last-workspace" }; + } return dispatch(passthrough); } diff --git a/packages/app/src/lib/overlay-root.ts b/packages/app/src/lib/overlay-root.ts index 9b19b88252..8b499ccdfd 100644 --- a/packages/app/src/lib/overlay-root.ts +++ b/packages/app/src/lib/overlay-root.ts @@ -97,6 +97,10 @@ function getTopWebOverlay(): WebOverlayEntry | undefined { }, undefined); } +export function hasActiveWebOverlay(): boolean { + return getTopWebOverlay() !== undefined; +} + function getFocusableElements(scope: HTMLElement): HTMLElement[] { const selector = [ "a[href]",