fix(app): refocus retained terminals after workspace switches

This commit is contained in:
Hannah
2026-10-06 14:38:47 +08:00
parent 0255738ee7
commit 3a10500fcf
3 changed files with 72 additions and 4 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## Unreleased
### Fixed
- Fixed retained terminals not regaining keyboard focus after switching back to their workspace with a number shortcut.
## 0.11.0-beta.5 - 2026-10-06
### Added
@@ -59,4 +59,45 @@ test.describe("retained terminal tab streams", () => {
expect(await terminalAttachOverlayWasSeen(page)).toBe(false);
});
test("workspace number shortcuts refocus a retained terminal", async ({ page }) => {
const first = await harness.createTerminal({ name: "focus-first" });
const other = await TerminalE2EHarness.create({ tempPrefix: "terminal-workspace-focus-" });
try {
const second = await other.createTerminal({ name: "focus-second" });
await harness.openTerminal(page, { terminalId: first.id });
await other.openTerminal(page, { terminalId: second.id });
const rows = page
.locator('[data-testid^="sidebar-workspace-row-"]')
.filter({ visible: true });
const rowIds = await rows.evaluateAll((elements) => {
const ids: Array<string | null> = [];
for (const element of elements) ids.push(element.getAttribute("data-testid"));
return ids;
});
const firstIndex = rowIds.findIndex((id) => id?.endsWith(`:${harness.workspaceId}`)) + 1;
const secondIndex = rowIds.findIndex((id) => id?.endsWith(`:${other.workspaceId}`)) + 1;
expect(firstIndex).toBeGreaterThan(0);
expect(secondIndex).toBeGreaterThan(0);
const terminalInput = page
.getByTestId("terminal-surface")
.filter({ visible: true })
.locator("textarea");
await expect(terminalInput).toBeFocused();
// Browser Alt+Digit routes the same workspace action as desktop Cmd+Digit.
await page.keyboard.press(`Alt+${firstIndex}`);
await expect(page).toHaveURL(new RegExp(`/workspace/${harness.workspaceId}`));
await expect(terminalInput).toBeFocused();
await page.keyboard.press(`Alt+${secondIndex}`);
await expect(page).toHaveURL(new RegExp(`/workspace/${other.workspaceId}`));
await expect(terminalInput).toBeFocused();
await page.keyboard.type("printf 'WORKSPACE_FOCUS_OK\\n'");
await page.keyboard.press("Enter");
await expect
.poll(async () => (await other.client.captureTerminal(second.id)).lines)
.toContain("WORKSPACE_FOCUS_OK");
} finally {
await other.cleanup();
}
});
});
+25 -4
View File
@@ -2,6 +2,7 @@ import { TerminalFind, type TerminalPaneFindHandle } from "@/terminal/find";
import type { TerminalFindResult } from "@/terminal/runtime/terminal-emulator-runtime";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { hasActiveWebOverlay } from "@/lib/overlay-root";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import * as Clipboard from "expo-clipboard";
import {
@@ -68,7 +69,7 @@ import {
type TerminalResizeRequest,
} from "./terminal-resize-debouncer";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isNative } from "@/constants/platform";
import { isNative, isWeb } from "@/constants/platform";
import {
applyTerminalRendererReadyChange,
resolveTerminalStreamTarget,
@@ -377,19 +378,39 @@ export function TerminalPane({
);
useEffect(() => {
if (isMobile || !isPaneFocused || !terminalId) {
if (isMobile || !isPaneFocused || !isWorkspaceFocused || !terminalId) {
lastAutoFocusKeyRef.current = null;
return;
}
if (!isWorkspaceFocused) {
if (rendererReadyStreamKey !== terminalStreamKey) {
return;
}
const focusKey = `${scopeKey}:${terminalId}`;
if (lastAutoFocusKeyRef.current !== focusKey) {
lastAutoFocusKeyRef.current = focusKey;
if (isWeb) {
const active = document.activeElement;
const hasFocusedInput =
active instanceof HTMLElement &&
active.matches('input, textarea, [contenteditable="true"]') &&
!active.matches(".xterm-helper-textarea") &&
active.getClientRects().length > 0;
if (hasActiveWebOverlay() || hasFocusedInput) {
return;
}
}
requestTerminalFocus();
}
}, [isMobile, isPaneFocused, isWorkspaceFocused, requestTerminalFocus, scopeKey, terminalId]);
}, [
isMobile,
isPaneFocused,
isWorkspaceFocused,
rendererReadyStreamKey,
requestTerminalFocus,
scopeKey,
terminalId,
terminalStreamKey,
]);
useEffect(() => {
const canRequest = canRequestFocusClaim({