diff --git a/site/src/modules/terminal/WorkspaceTerminal.tsx b/site/src/modules/terminal/WorkspaceTerminal.tsx index bf46d5321b1..87a5ea13ce6 100644 --- a/site/src/modules/terminal/WorkspaceTerminal.tsx +++ b/site/src/modules/terminal/WorkspaceTerminal.tsx @@ -276,14 +276,25 @@ export const WorkspaceTerminal = ({ }, [isVisible, refit]); useEffect(() => { - if (!terminal || !hasBeenVisible) { + if (!terminal || !isVisible || !autoFocus) { return; } - terminal.clear(); - if (autoFocus) { + const frame = requestAnimationFrame(() => { terminal.focus(); + }); + + return () => { + cancelAnimationFrame(frame); + }; + }, [terminal, isVisible, autoFocus]); + + useEffect(() => { + if (!terminal || !hasBeenVisible) { + return; } + + terminal.clear(); terminal.options.disableStdin = true; if (loading) { @@ -454,7 +465,6 @@ export const WorkspaceTerminal = ({ }, [ hasBeenVisible, agentId, - autoFocus, baseUrl, containerName, containerUser, diff --git a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx index d5f63899b1a..7e26dfbf6c9 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx @@ -14,6 +14,7 @@ import { withAuthProvider, withDashboardProvider, withProxyProvider, + withWebSocket, } from "#/testHelpers/storybook"; import { AgentChatPageLoadingView, @@ -1208,3 +1209,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: () => ( + + ), + 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 }, + ); + }, +}; diff --git a/site/src/pages/AgentsPage/components/TerminalPanel.tsx b/site/src/pages/AgentsPage/components/TerminalPanel.tsx index 9d0cb8fe51a..f07423ebb50 100644 --- a/site/src/pages/AgentsPage/components/TerminalPanel.tsx +++ b/site/src/pages/AgentsPage/components/TerminalPanel.tsx @@ -94,7 +94,6 @@ export const TerminalPanel: FC = ({ ref={terminalRef} agentId={workspaceAgent.id} operatingSystem={workspaceAgent.operating_system} - autoFocus={false} isVisible={isVisible} onStatusChange={setConnectionStatus} onError={handleTerminalError}