From 6177d85e9959e851621fbb8828222717beca2a1c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 1 Aug 2026 11:16:48 +0200 Subject: [PATCH] Make sidebar shortcuts leave focus mode (#2717) * fix(app): exit focus mode for sidebar shortcuts Focus mode kept sidebar actions hidden after their handlers ran. Exit only after a sidebar action is handled so the requested action still takes effect. * fix(app): order focus exit before sidebar action Leave focus mode before dispatching the sidebar command so its handler runs against the visible panel state. * fix(app): preserve focus mode outside workspaces Only exit active workspace focus mode after a sidebar shortcut is handled. Keep persisted focus state unchanged when the shortcut has no foreground workspace target. --- packages/app/e2e/workspace-focus-mode.spec.ts | 62 +++++++++++++++++-- packages/app/src/app/_layout.tsx | 3 + .../app/src/hooks/use-keyboard-shortcuts.ts | 12 +++- packages/app/src/stores/panel-store/index.ts | 8 +++ 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/packages/app/e2e/workspace-focus-mode.spec.ts b/packages/app/e2e/workspace-focus-mode.spec.ts index 5aa030ea2..fc1dc7701 100644 --- a/packages/app/e2e/workspace-focus-mode.spec.ts +++ b/packages/app/e2e/workspace-focus-mode.spec.ts @@ -1,23 +1,73 @@ -import { expect, test } from "./fixtures"; +import { expect, test, type Page } from "./fixtures"; const modifier = process.platform === "darwin" ? "Meta" : "Control"; -async function pressFocusModeShortcut(page: import("@playwright/test").Page) { +async function pressFocusModeShortcut(page: Page) { await page.keyboard.press(`${modifier}+Shift+F`); } -async function pressSettingsShortcut(page: import("@playwright/test").Page) { +async function pressSettingsShortcut(page: Page) { await page.keyboard.press(`${modifier}+Comma`); } -async function blurActiveElement(page: import("@playwright/test").Page) { +async function pressRightSidebarShortcut(page: Page) { + await page.keyboard.press(`${modifier}+E`); +} + +async function returnToWorkspace(page: Page, workspaceUrl: string) { + await page.goto(workspaceUrl); +} + +async function blurActiveElement(page: Page) { await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); } +function exitFocusModeButton(page: Page) { + return page.getByRole("button", { name: "Exit focus mode" }); +} + +async function enterFocusMode(page: Page) { + await blurActiveElement(page); + await pressFocusModeShortcut(page); + await expect(exitFocusModeButton(page)).toBeVisible(); +} + +test("sidebar shortcuts exit focus mode and execute their action", async ({ + page, + withWorkspace, +}) => { + const workspace = await withWorkspace({ prefix: "focus-mode-sidebar-shortcut-" }); + await workspace.navigateTo(); + await enterFocusMode(page); + + await pressRightSidebarShortcut(page); + + await expect(exitFocusModeButton(page)).toHaveCount(0); + await expect(page.getByText("Changes", { exact: true })).toBeVisible(); +}); + +test("sidebar shortcuts outside a workspace preserve its focus mode", async ({ + page, + withWorkspace, +}) => { + const workspace = await withWorkspace({ prefix: "focus-mode-sidebar-boundary-" }); + await workspace.navigateTo(); + await enterFocusMode(page); + const workspaceUrl = page.url(); + + await pressSettingsShortcut(page); + await expect(page.getByRole("navigation", { name: "Settings" })).toBeVisible(); + + await pressRightSidebarShortcut(page); + await returnToWorkspace(page, workspaceUrl); + + await expect(exitFocusModeButton(page)).toBeVisible(); +}); + test("focus mode only applies to the active workspace screen", async ({ page, withWorkspace }) => { const workspace = await withWorkspace({ prefix: "focus-mode-boundary-" }); await workspace.navigateTo(); - const exitFocusMode = page.getByRole("button", { name: "Exit focus mode" }); + const exitFocusMode = exitFocusModeButton(page); const settingsButton = page.getByRole("button", { name: "Settings", exact: true }); const settingsSidebar = page.getByRole("navigation", { name: "Settings" }); @@ -41,7 +91,7 @@ test("focus mode only applies to the active workspace screen", async ({ page, wi await expect(exitFocusMode).toHaveCount(0); await pressFocusModeShortcut(page); - await page.goto(workspaceUrl); + await returnToWorkspace(page, workspaceUrl); await expect(exitFocusMode).toBeVisible(); await exitFocusMode.click(); diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 5b26b0d15..5429c82e8 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -428,6 +428,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon const openDesktopAgentList = usePanelStore((state) => state.openDesktopAgentList); const closeDesktopAgentList = usePanelStore((state) => state.closeDesktopAgentList); const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer); + const exitFocusMode = usePanelStore((state) => state.exitFocusMode); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); const isDesktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const isDesktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); @@ -472,8 +473,10 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon useKeyboardShortcuts({ enabled: keyboardShortcutsEnabled, isMobile: isCompactLayout, + isWorkspaceFocusModeEnabled, toggleAgentList, toggleBothSidebars: toggleDesktopSidebars, + exitFocusMode, cycleTheme, }); diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index ce6155a5a..b6f7f03c9 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -40,14 +40,18 @@ import { export function useKeyboardShortcuts({ enabled, isMobile, + isWorkspaceFocusModeEnabled, toggleAgentList, toggleBothSidebars, + exitFocusMode, cycleTheme, }: { enabled: boolean; isMobile: boolean; + isWorkspaceFocusModeEnabled: boolean; toggleAgentList: () => void; toggleBothSidebars?: () => void; + exitFocusMode: () => void; cycleTheme?: () => void; }) { const pathname = usePathname(); @@ -207,11 +211,15 @@ export function useKeyboardShortcuts({ shortcutsDialogOpen: store.shortcutsDialogOpen, }, ); - return performShortcutAction( + const handled = performShortcutAction( shortcutAction, input.domEvent, input.browserFocusRestoreElement, ); + if (handled && isWorkspaceFocusModeEnabled && input.action.startsWith("sidebar.")) { + exitFocusMode(); + } + return handled; }; const resolveAndPerformShortcut = (input: { @@ -371,10 +379,12 @@ export function useKeyboardShortcuts({ bindings, cycleTheme, enabled, + exitFocusMode, activeWorkspaceSelection, isDesktopApp, isMac, isMobile, + isWorkspaceFocusModeEnabled, openProjectPickerAction, pathname, publishBrowserShortcutPolicy, diff --git a/packages/app/src/stores/panel-store/index.ts b/packages/app/src/stores/panel-store/index.ts index 52d2aa0cc..baac870f4 100644 --- a/packages/app/src/stores/panel-store/index.ts +++ b/packages/app/src/stores/panel-store/index.ts @@ -90,6 +90,7 @@ export interface PanelState { // Actions toggleFocusMode: () => void; + exitFocusMode: () => void; showMobileAgent: () => void; showMobileAgentList: () => void; toggleMobileAgentList: () => void; @@ -157,6 +158,13 @@ export const usePanelStore = create()( desktop: { ...state.desktop, focusModeEnabled: !state.desktop.focusModeEnabled }, })), + exitFocusMode: () => + set((state) => + state.desktop.focusModeEnabled + ? { desktop: { ...state.desktop, focusModeEnabled: false } } + : state, + ), + showMobileAgent: () => set((state) => setMobilePanelTargetPatch(state, "agent")), showMobileAgentList: () => set((state) => setMobilePanelTargetPatch(state, "agent-list")),