fix(site): focus agents terminal on tab switch (#24677)

Fixes
[CODAGT-221](https://linear.app/codercom/issue/CODAGT-221/terminal-selection-in-side-panel-does-not-focus-input).
Verified manually that the fix automatically focuses the terminal input
when the terminal tab is selected.
This commit is contained in:
Hugo Dutka
2026-04-23 18:36:46 +02:00
committed by GitHub
parent f8fe5d680b
commit c56061a09d
3 changed files with 75 additions and 5 deletions
@@ -275,15 +275,26 @@ export const WorkspaceTerminal = ({
refit();
}, [isVisible, refit]);
useEffect(() => {
if (!terminal || !isVisible || !autoFocus) {
return;
}
const frame = requestAnimationFrame(() => {
terminal.focus();
});
return () => {
cancelAnimationFrame(frame);
};
}, [terminal, isVisible, autoFocus]);
useEffect(() => {
if (!terminal || !hasBeenVisible) {
return;
}
terminal.clear();
if (autoFocus) {
terminal.focus();
}
terminal.options.disableStdin = true;
if (loading) {
@@ -454,7 +465,6 @@ export const WorkspaceTerminal = ({
}, [
hasBeenVisible,
agentId,
autoFocus,
baseUrl,
containerName,
containerUser,
@@ -14,6 +14,7 @@ import {
withAuthProvider,
withDashboardProvider,
withProxyProvider,
withWebSocket,
} from "#/testHelpers/storybook";
import {
AgentChatPageLoadingView,
@@ -1207,3 +1208,63 @@ export const ScrollStableAfterEditTruncation: Story = {
).toBeNull();
},
};
/**
* Selecting the Terminal tab in the sidebar must move keyboard focus into
* the terminal so typing goes there, not the chat input.
*/
export const TerminalFocusOnTabSwitch: Story = {
parameters: {
chromatic: { disableSnapshot: true },
webSocket: { "/api/v2/workspaceagents/": [{ event: "message", data: "" }] },
},
decorators: [withWebSocket],
render: () => (
<StoryAgentChatPageView
showSidebarPanel
workspace={MockWorkspace}
workspaceAgent={MockWorkspaceAgent}
/>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// The sidebar should open on the Git tab by default.
const terminalTab = await canvas.findByRole("tab", { name: "Terminal" });
// 1. Click the Terminal tab.
await userEvent.click(terminalTab);
// Wait for the terminal container to appear.
const terminalContainer = await waitFor(() => {
const el = canvas.getByTestId("agents-sidebar-terminal");
expect(el).toBeVisible();
return el;
});
// The xterm focus target is a textarea inside the terminal container.
await waitFor(
() => {
const textarea = terminalContainer.querySelector("textarea");
expect(textarea).not.toBeNull();
expect(document.activeElement).toBe(textarea);
},
{ timeout: 3000 },
);
// 2. Switch to Git, then back to Terminal.
const gitTab = canvas.getByRole("tab", { name: "Git" });
await userEvent.click(gitTab);
await userEvent.click(terminalTab);
// Focus should return to the terminal textarea.
await waitFor(
() => {
const textarea = terminalContainer.querySelector("textarea");
expect(textarea).not.toBeNull();
expect(document.activeElement).toBe(textarea);
},
{ timeout: 3000 },
);
},
};
@@ -94,7 +94,6 @@ export const TerminalPanel: FC<TerminalPanelProps> = ({
ref={terminalRef}
agentId={workspaceAgent.id}
operatingSystem={workspaceAgent.operating_system}
autoFocus={false}
isVisible={isVisible}
onStatusChange={setConnectionStatus}
onError={handleTerminalError}