From 72d14325e60ecd0bcf801cde60bc101ffd401010 Mon Sep 17 00:00:00 2001 From: Hannah Date: Tue, 6 Oct 2026 15:05:31 +0800 Subject: [PATCH] fix(app): preserve button focus during deferred terminal autofocus --- CHANGELOG.md | 2 +- .../terminal-retained-tab-stream.spec.ts | 19 +++++++++++++++++++ .../app/e2e/support/helpers/terminal-dsl.ts | 16 ++++++++++++++++ packages/app/src/components/terminal-pane.tsx | 3 +++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2191a65e1..78d9717887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed retained terminals not regaining keyboard focus after switching back to their workspace, including when an overlay or another input delays focus until it is released. +- Fixed retained terminals not regaining keyboard focus after switching back to their workspace, including when an overlay or another input delays focus until it is released, without taking focus from a newly focused button. ## 0.11.0-beta.5 - 2026-10-06 diff --git a/packages/app/e2e/browser/terminal-retained-tab-stream.spec.ts b/packages/app/e2e/browser/terminal-retained-tab-stream.spec.ts index 8a46d9b1b4..63425dfef3 100644 --- a/packages/app/e2e/browser/terminal-retained-tab-stream.spec.ts +++ b/packages/app/e2e/browser/terminal-retained-tab-stream.spec.ts @@ -102,4 +102,23 @@ test.describe("retained terminal tab streams", () => { output: "OVERLAY_FOCUS_OK", }); }); + + test("deferred terminal focus does not steal focus from a button", async ({ page }) => { + const first = await harness.createTerminal({ name: "button-first" }); + const other = await harness.createOtherWorkspace(); + const second = await other.createTerminal({ name: "button-second" }); + await harness.openTerminal(page, { terminalId: first.id }); + await other.openTerminal(page, { terminalId: second.id }); + await harness.switchToWorkspaceByShortcut(page); + await harness.expectTerminalFocused(page); + await harness.rememberWorkspaceForHistoryReturn(page); + await other.switchToWorkspaceByShortcut(page); + const overlay = await openCommandCenter(page); + await harness.returnToWorkspaceThroughHistory(page); + await expect(overlay.getByTestId("command-center-input")).toBeFocused(); + await harness.closeOverlayAndFocusButtonBeforeRetry(page); + await expect(page.getByTestId("sidebar-search")).toBeFocused(); + await page.keyboard.press("Enter"); + await expect(overlay).toBeVisible(); + }); }); diff --git a/packages/app/e2e/support/helpers/terminal-dsl.ts b/packages/app/e2e/support/helpers/terminal-dsl.ts index c9698a14c3..eb1dc76d59 100644 --- a/packages/app/e2e/support/helpers/terminal-dsl.ts +++ b/packages/app/e2e/support/helpers/terminal-dsl.ts @@ -164,6 +164,22 @@ export class TerminalE2EHarness { await expect(page).toHaveURL(new RegExp(`/workspace/${this.workspaceId}`)); } + async closeOverlayAndFocusButtonBeforeRetry(page: Page): Promise { + await page.clock.install(); + await page.clock.pauseAt(new Date()); + await page.keyboard.press("Escape"); + await expect(page.getByTestId("command-center-panel")).toBeHidden(); + await page.getByTestId("sidebar-search").focus(); + await page.clock.runFor(100); + await page.clock.resume(); + await page.evaluate( + () => + new Promise((resolve) => + requestAnimationFrame(() => requestAnimationFrame(() => resolve())), + ), + ); + } + async expectTerminalFocused(page: Page): Promise { await expect( this.terminalSurface(page).filter({ visible: true }).locator(".xterm-helper-textarea"), diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index 6c31fe07c1..61f1e4024d 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -408,6 +408,7 @@ export function TerminalPane({ let retryFrame: number | null = null; function stopRetrying(): void { document.removeEventListener("focusout", scheduleRetry); + document.removeEventListener("focusin", stopRetrying); unsubscribeOverlayChanges(); if (retryFrame !== null) window.cancelAnimationFrame(retryFrame); } @@ -421,6 +422,8 @@ export function TerminalPane({ } const unsubscribeOverlayChanges = subscribeWebOverlayChanges(scheduleRetry); document.addEventListener("focusout", scheduleRetry); + // A new focus owner wins over a deferred workspace autofocus request. + document.addEventListener("focusin", stopRetrying); return stopRetrying; }, [ isMobile,