diff --git a/packages/app/e2e/browser/terminal-tab-title-loading.spec.ts b/packages/app/e2e/browser/terminal-tab-title-loading.spec.ts new file mode 100644 index 0000000000..93e9e72aed --- /dev/null +++ b/packages/app/e2e/browser/terminal-tab-title-loading.spec.ts @@ -0,0 +1,86 @@ +import { test, expect, type Page } from "../support/fixtures"; +import { clickNewTerminal, gotoWorkspace } from "../support/helpers/launcher"; +import { renameModalInput, renameModalSubmit } from "../support/helpers/rename"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { selectWorkspaceInSidebar } from "../support/helpers/sidebar"; + +function terminalTab(page: Page, terminalId: string) { + // Terminal tab chips have no tab role; their persistent identity survives title loading. + return page.getByTestId(`workspace-tab-terminal_${terminalId}`).filter({ visible: true }).first(); +} + +async function createNamedTerminal(page: Page, workspace: SeededWorkspace, title: string) { + await gotoWorkspace(page, workspace.workspaceId); + await clickNewTerminal(page); + const list = () => + workspace.client.listTerminals(workspace.repoPath, undefined, { + workspaceId: workspace.workspaceId, + }); + await expect.poll(async () => (await list()).terminals.length).toBe(1); + const terminalId = (await list()).terminals[0]!.id; + await terminalTab(page, terminalId).click({ button: "right" }); + await page.getByRole("menuitem", { name: "Rename", exact: true }).click(); + const modal = `workspace-tab-rename-modal-terminal-${terminalId}`; + await renameModalInput(page, modal).fill(title); + await renameModalSubmit(page, modal).click(); + await expect(renameModalInput(page, modal)).toHaveCount(0); + await expect(terminalTab(page, terminalId)).toHaveText(title); + return terminalId; +} + +async function recordRestoredTabText(page: Page, terminalId: string) { + // Install before the next document loads: a final-state assertion would miss the flash. + await page.addInitScript((id) => { + const samples: string[] = []; + Object.assign(window, { terminalTitleSamples: samples }); + new MutationObserver(() => { + const tab = document.querySelector(`[data-testid="workspace-tab-terminal_${id}"]`); + if (!tab) return; + const text = (tab.textContent ?? "").trim(); + if (samples.at(-1) !== text) samples.push(text); + }).observe(document, { subtree: true, childList: true, characterData: true }); + }, terminalId); +} + +async function expectRestoredTitle(page: Page, terminalId: string, title: string, stage: string) { + await expect(terminalTab(page, terminalId)).toHaveText(title, { timeout: 30_000 }); + const samples = await page.evaluate( + () => (window as unknown as { terminalTitleSamples: string[] }).terminalTitleSamples, + ); + await test.info().attach(`${stage}-tab-text`, { + body: JSON.stringify(samples), + contentType: "application/json", + }); + expect(samples).toContain(title); + expect(samples).not.toContain("Terminal"); +} + +test("restored terminal titles do not flash a default name while loading", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "terminal-title-loading-", git: false }); + const other = await seedWorkspace({ repoPrefix: "terminal-title-other-", git: false }); + try { + const title = "My named terminal"; + const terminalId = await createNamedTerminal(page, workspace, title); + await recordRestoredTabText(page, terminalId); + + await test.step("reload with a terminal tab restored", async () => { + await page.reload(); + await expectRestoredTitle(page, terminalId, title, "reload"); + }); + + await test.step("first workspace visit in a fresh page session", async () => { + await gotoWorkspace(page, other.workspaceId); + await selectWorkspaceInSidebar(page, workspace.workspaceId); + await expectRestoredTitle(page, terminalId, title, "first-visit"); + }); + + await test.step("return to the workspace with its titles already loaded", async () => { + await selectWorkspaceInSidebar(page, other.workspaceId); + await selectWorkspaceInSidebar(page, workspace.workspaceId); + await expectRestoredTitle(page, terminalId, title, "return-visit"); + }); + } finally { + await workspace.cleanup(); + await other.cleanup(); + } +}); diff --git a/packages/app/src/panels/terminal-panel.tsx b/packages/app/src/panels/terminal-panel.tsx index 32dd1f188d..8082bd36e5 100644 --- a/packages/app/src/panels/terminal-panel.tsx +++ b/packages/app/src/panels/terminal-panel.tsx @@ -69,7 +69,7 @@ function useTerminalPanelDescriptor( label, subtitle: t("workspace.tabs.fallback.terminal"), tooltip: label, - titleState: "ready", + titleState: terminalsQuery.isPending ? "loading" : "ready", icon: Terminal, statusBucket: deriveTerminalActivityStatusBucket(terminal?.activity), }; diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index b462cf0cd5..5c183dc94c 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -1487,9 +1487,7 @@ function ResolvedDesktopTabChip({ ); const rawTooltipLabel = - presentation.titleState === "loading" - ? t("workspace.tabs.loadingAgentTitle") - : presentation.tooltip; + presentation.titleState === "loading" ? t("common.states.loading") : presentation.tooltip; const accessibilityLabel = item.tab.target.kind === "agent" ? normalizeAgentTooltipTitle(rawTooltipLabel)