diff --git a/packages/app/e2e/browser/agent-message-rewind.spec.ts b/packages/app/e2e/browser/agent-message-rewind.spec.ts index 78752a353..2cb01f89e 100644 --- a/packages/app/e2e/browser/agent-message-rewind.spec.ts +++ b/packages/app/e2e/browser/agent-message-rewind.spec.ts @@ -28,7 +28,7 @@ async function completeSubmittedTurn( async function rewindConversation(page: Page, userMessage: Locator, prompt: string): Promise { await userMessage.getByText(prompt, { exact: true }).hover(); await userMessage.getByRole("button", { name: "Rewind to this message", exact: true }).click(); - await page.getByRole("button", { name: "Rewind conversation", exact: true }).click(); + await page.getByRole("menuitem", { name: "Rewind conversation", exact: true }).click(); } async function expectTurnCompletesNormally( diff --git a/packages/app/e2e/browser/appearance-theme-picker.spec.ts b/packages/app/e2e/browser/appearance-theme-picker.spec.ts index b23dd2af5..60d688699 100644 --- a/packages/app/e2e/browser/appearance-theme-picker.spec.ts +++ b/packages/app/e2e/browser/appearance-theme-picker.spec.ts @@ -45,9 +45,9 @@ test("keeps the selected workspace visible in Pure black", async ({ page }, test } }); -test("applies the interface font size to settings text", async ({ page }) => { +test("applies the base font size to settings text", async ({ page }) => { await page.addInitScript(() => { - localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiFontSize: 24 })); + localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiBaseFontSize: 21 })); }); await page.goto("/settings"); await expect(page.getByTestId("settings-sidebar")).toBeVisible(); @@ -56,11 +56,11 @@ test("applies the interface font size to settings text", async ({ page }) => { const sectionTitle = page.getByText("Theme", { exact: true }).first(); await expect(sectionTitle).toHaveCSS("font-size", "18px"); - const fontSizeInput = page.getByLabel("Interface font size"); - await expect(fontSizeInput).toHaveValue("24"); + const fontSizeInput = page.getByLabel("Base font size"); + await expect(fontSizeInput).toHaveValue("21"); await fontSizeInput.fill("12"); await fontSizeInput.press("Tab"); await expect(fontSizeInput).toHaveValue("12"); - await expect(sectionTitle).toHaveCSS("font-size", "9px"); + await expect(sectionTitle).toHaveCSS("font-size", "10px"); }); diff --git a/packages/app/e2e/browser/settings-i18n.spec.ts b/packages/app/e2e/browser/settings-i18n.spec.ts index 48d20d7cb..ab368c3d9 100644 --- a/packages/app/e2e/browser/settings-i18n.spec.ts +++ b/packages/app/e2e/browser/settings-i18n.spec.ts @@ -12,12 +12,12 @@ test("Settings language selector switches General labels", async ({ page }) => { await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible(); await page.getByRole("button", { name: "System", exact: true }).click(); - await page.getByRole("button", { name: "简体中文 - Simplified Chinese", exact: true }).click(); + await page.getByRole("menuitem", { name: "简体中文 - Simplified Chinese", exact: true }).click(); await expect(page.getByText("默认发送", { exact: true }).first()).toBeVisible(); await page.getByRole("button", { name: "简体中文", exact: true }).click(); - await page.getByRole("button", { name: "English - 英语", exact: true }).click(); + await page.getByRole("menuitem", { name: "English - 英语", exact: true }).click(); await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible(); }); @@ -30,12 +30,12 @@ test("Settings language selector switches to Korean", async ({ page }) => { await openSettingsSection(page, "general"); await page.getByRole("button", { name: "System", exact: true }).click(); - await page.getByRole("button", { name: "한국어 - Korean", exact: true }).click(); + await page.getByRole("menuitem", { name: "한국어 - Korean", exact: true }).click(); await expect(page.getByText("기본 전송", { exact: true }).first()).toBeVisible(); await page.getByRole("button", { name: "한국어", exact: true }).click(); - await page.getByRole("button", { name: "English - 영어", exact: true }).click(); + await page.getByRole("menuitem", { name: "English - 영어", exact: true }).click(); await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible(); }); diff --git a/packages/app/e2e/browser/terminal-split-resize.spec.ts b/packages/app/e2e/browser/terminal-split-resize.spec.ts index 809cb8831..ab4830550 100644 --- a/packages/app/e2e/browser/terminal-split-resize.spec.ts +++ b/packages/app/e2e/browser/terminal-split-resize.spec.ts @@ -2,6 +2,7 @@ import type { Page } from "@playwright/test"; import { test, expect } from "../support/fixtures"; import { TerminalE2EHarness, withTerminalInApp } from "../support/helpers/terminal-dsl"; import { waitForTerminalContent } from "../support/helpers/terminal-perf"; +import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions"; interface TerminalSize { rows: number | null; @@ -82,7 +83,7 @@ async function verifySplitDownResizesPty(page: Page, harness: TerminalE2EHarness // Split the pane downward. This focuses the new empty pane, so the terminal // pane is unfocused at the exact moment its container shrinks. - await page.getByRole("button", { name: "Split pane down" }).first().click(); + await runWorkspaceActionFromCommandCenter(page, "Split pane down"); // The local xterm renderer shrinks immediately on split - the part of the // behaviour that already works and that makes the bug look like nothing changed. diff --git a/packages/app/e2e/browser/viewed-agent-timelines.spec.ts b/packages/app/e2e/browser/viewed-agent-timelines.spec.ts index 3ea9ef08c..5dc98dc82 100644 --- a/packages/app/e2e/browser/viewed-agent-timelines.spec.ts +++ b/packages/app/e2e/browser/viewed-agent-timelines.spec.ts @@ -6,6 +6,7 @@ import { getServerId } from "../support/helpers/server-id"; import { observeTimelineSubscriptions } from "../support/helpers/timeline-delivery"; import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; +import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions"; import { expectAgentIdle, expectInlineWorkingIndicator, @@ -150,7 +151,7 @@ test.describe("Viewed agent timelines", () => { try { await enableMoveTabShortcut(page); await openAgent(page, scenario, scenario.firstAgentId); - await page.getByRole("button", { name: "Split pane right" }).click(); + await runWorkspaceActionFromCommandCenter(page, "Split pane right"); await selectAgent(page, "Second viewed chat"); await moveActiveTabRight(page); await expect( diff --git a/packages/app/e2e/browser/workspace-focus-mode.spec.ts b/packages/app/e2e/browser/workspace-focus-mode.spec.ts index 819a14fad..e152b4def 100644 --- a/packages/app/e2e/browser/workspace-focus-mode.spec.ts +++ b/packages/app/e2e/browser/workspace-focus-mode.spec.ts @@ -43,7 +43,9 @@ test("sidebar shortcuts exit focus mode and execute their action", async ({ await pressRightSidebarShortcut(page); await expect(exitFocusModeButton(page)).toHaveCount(0); - await expect(page.getByText("Changes", { exact: true })).toBeVisible(); + await expect( + page.getByText("Changes", { exact: true }).filter({ visible: true }).first(), + ).toBeVisible(); }); test("sidebar shortcuts outside a workspace preserve its focus mode", async ({ diff --git a/packages/app/e2e/browser/workspace-pane-remount.spec.ts b/packages/app/e2e/browser/workspace-pane-remount.spec.ts index ac5ad25cf..ee403cfce 100644 --- a/packages/app/e2e/browser/workspace-pane-remount.spec.ts +++ b/packages/app/e2e/browser/workspace-pane-remount.spec.ts @@ -9,6 +9,7 @@ import { seedWorkspace } from "../support/helpers/seed-client"; import { getServerId } from "../support/helpers/server-id"; import { clickSettingsBackToWorkspace, openCompactSettings } from "../support/helpers/settings"; import { openSettings } from "../support/helpers/app"; +import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions"; import { clickFirstTerminalTab, waitForWorkspaceTabsVisible, @@ -65,7 +66,7 @@ test.describe("Workspace pane mounting", () => { .elementHandle(); expect(originalComposer).not.toBeNull(); - await page.getByRole("button", { name: "Split pane right" }).first().click(); + await runWorkspaceActionFromCommandCenter(page, "Split pane right"); await expect(page.getByTestId("message-input-root").filter({ visible: true })).toHaveCount( 2, { timeout: 30_000 }, diff --git a/packages/app/e2e/support/helpers/assistant-fork.ts b/packages/app/e2e/support/helpers/assistant-fork.ts index ad377fd1c..7ee4cdc4f 100644 --- a/packages/app/e2e/support/helpers/assistant-fork.ts +++ b/packages/app/e2e/support/helpers/assistant-fork.ts @@ -58,7 +58,7 @@ function inFlightTurn(page: Page) { async function openForkMenu(page: Page, trigger: ReturnType): Promise { await trigger.click(); - await expect(page.getByRole("button", { name: "Fork in a new tab" })).toBeVisible({ + await expect(page.getByRole("menuitem", { name: "Fork in a new tab" })).toBeVisible({ timeout: 10_000, }); } @@ -80,7 +80,7 @@ export async function openMostRecentAssistantForkMenu(page: Page): Promise export async function forkMostRecentAssistantTurnToNewTab(page: Page): Promise { await openMostRecentAssistantForkMenu(page); - await page.getByRole("button", { name: "Fork in a new tab" }).click(); + await page.getByRole("menuitem", { name: "Fork in a new tab" }).click(); } export async function expectInFlightForkAvailable(page: Page): Promise { @@ -92,12 +92,12 @@ export async function expectInFlightForkAvailable(page: Page): Promise { export async function forkInFlightTurnToNewTab(page: Page): Promise { const trigger = inFlightTurn(page).getByRole("button", { name: "Fork chat" }); await openForkMenu(page, trigger); - await page.getByRole("button", { name: "Fork in a new tab" }).click(); + await page.getByRole("menuitem", { name: "Fork in a new tab" }).click(); } export async function forkMostRecentAssistantTurnToNewWorkspace(page: Page): Promise { await openMostRecentAssistantForkMenu(page); - await page.getByRole("button", { name: "Fork in a new workspace" }).click(); + await page.getByRole("menuitem", { name: "Fork in a new workspace" }).click(); } export async function expectChatHistoryAttachment(page: Page): Promise { diff --git a/packages/app/e2e/support/helpers/chat-outline.ts b/packages/app/e2e/support/helpers/chat-outline.ts index e3f488f19..a32499291 100644 --- a/packages/app/e2e/support/helpers/chat-outline.ts +++ b/packages/app/e2e/support/helpers/chat-outline.ts @@ -1,6 +1,7 @@ import { expect, type Locator, type Page } from "@playwright/test"; import { openSettings } from "./app"; import { openSettingsSection } from "./settings"; +import { runWorkspaceActionFromCommandCenter } from "./command-center-workspace-actions"; export function chatOutlineRail(page: Page): Locator { return page.getByTestId("chat-outline-rail"); @@ -41,7 +42,7 @@ export async function clickChatOutlineRowEdge(page: Page, position: number): Pro } export async function splitCurrentPanelRight(page: Page): Promise { - await page.getByRole("button", { name: "Split pane right" }).first().click(); + await runWorkspaceActionFromCommandCenter(page, "Split pane right"); } export async function disableChatOutlineFromAppearance(page: Page): Promise { diff --git a/packages/app/e2e/support/helpers/command-center-workspace-actions.ts b/packages/app/e2e/support/helpers/command-center-workspace-actions.ts index c6d7bd4d2..5aba36208 100644 --- a/packages/app/e2e/support/helpers/command-center-workspace-actions.ts +++ b/packages/app/e2e/support/helpers/command-center-workspace-actions.ts @@ -64,3 +64,12 @@ export async function createTerminalFromCommandCenter(page: Page, panel: Locator await page.keyboard.press("Enter"); await expectTerminalTabOpen(page); } + +export async function runWorkspaceActionFromCommandCenter( + page: Page, + title: string, +): Promise { + const panel = await openCommandCenter(page); + await panel.getByTestId("command-center-input").fill(title); + await action(panel, title).click(); +} diff --git a/packages/app/e2e/support/helpers/composer.ts b/packages/app/e2e/support/helpers/composer.ts index c989ce6fd..eb8760b91 100644 --- a/packages/app/e2e/support/helpers/composer.ts +++ b/packages/app/e2e/support/helpers/composer.ts @@ -82,7 +82,7 @@ export async function openAttachmentMenu(page: Page): Promise { export async function expectAttachmentSheetRowsOnTitleRail(page: Page): Promise { const title = page.getByText("Add attachment", { exact: true }); const firstItemGlyph = page - .getByRole("button", { name: "Add image", exact: true }) + .getByRole("menuitem", { name: "Add image", exact: true }) .locator("svg") .first(); await waitForSettledPosition(title); diff --git a/packages/app/e2e/support/helpers/hosts.ts b/packages/app/e2e/support/helpers/hosts.ts index ef077f8ce..95b1d17d5 100644 --- a/packages/app/e2e/support/helpers/hosts.ts +++ b/packages/app/e2e/support/helpers/hosts.ts @@ -177,7 +177,7 @@ export async function renameHostFromSettings(page: Page, name: string): Promise< export async function chooseHostColor(page: Page, colorName: string): Promise { await page.getByRole("button", { name: /^Color, / }).click(); - await page.getByRole("button", { name: colorName, exact: true }).click(); + await page.getByRole("menuitem", { name: colorName, exact: true }).click(); await expect(page.getByRole("button", { name: `Color, ${colorName}` })).toBeVisible(); } @@ -186,7 +186,7 @@ export async function chooseHostBadgeDisplay( option: "Name" | "Icon only" | "Hidden", ): Promise { await page.getByTestId("host-appearance-badge-display").click(); - await page.getByRole("button", { name: option, exact: true }).click(); + await page.getByRole("menuitem", { name: option, exact: true }).click(); await expect(page.getByRole("button", { name: `Sidebar badge, ${option}` })).toBeVisible(); } diff --git a/packages/app/e2e/support/helpers/project-settings.ts b/packages/app/e2e/support/helpers/project-settings.ts index 8b3b23faa..cbfe787c2 100644 --- a/packages/app/e2e/support/helpers/project-settings.ts +++ b/packages/app/e2e/support/helpers/project-settings.ts @@ -239,7 +239,7 @@ export async function removeProjectScript(page: Page, scriptName: string): Promi const id = (await row.getAttribute("data-testid"))!.replace("script-row-", ""); await page.getByTestId(`script-row-menu-${id}`).click(); page.once("dialog", (dialog) => void dialog.accept()); - await page.getByRole("button", { name: "Remove" }).click(); + await page.getByRole("menuitem", { name: "Remove" }).click(); } // --- File manipulation --- diff --git a/packages/app/e2e/support/helpers/sidebar.ts b/packages/app/e2e/support/helpers/sidebar.ts index f0f2ce4b6..e0a83e0da 100644 --- a/packages/app/e2e/support/helpers/sidebar.ts +++ b/packages/app/e2e/support/helpers/sidebar.ts @@ -77,9 +77,9 @@ export async function expectWorkspaceContextMenuActions( ): Promise { const workspaceKey = `${getServerId()}:${workspaceId}`; const menu = page.getByTestId(`sidebar-workspace-context-menu-${workspaceKey}`); - await expect(menu.getByRole("button", { name: "Copy path" })).toBeVisible(); - await expect(menu.getByRole("button", { name: "Rename workspace" })).toBeVisible(); - await expect(menu.getByRole("button", { name: "Archive" })).toBeVisible(); + await expect(menu.getByRole("menuitem", { name: "Copy path" })).toBeVisible(); + await expect(menu.getByRole("menuitem", { name: "Rename workspace" })).toBeVisible(); + await expect(menu.getByRole("menuitem", { name: "Archive" })).toBeVisible(); } export async function openProjectContextMenu(page: Page, projectViewKey: string): Promise { @@ -101,8 +101,8 @@ export async function expectProjectContextMenuActions( projectViewKey: string, ): Promise { const menu = page.getByTestId(`sidebar-project-context-menu-${projectViewKey}`); - await expect(menu.getByRole("button", { name: "Open project settings" })).toBeVisible(); - await expect(menu.getByRole("button", { name: "Remove project" })).toBeVisible(); + await expect(menu.getByRole("menuitem", { name: "Open project settings" })).toBeVisible(); + await expect(menu.getByRole("menuitem", { name: "Remove project" })).toBeVisible(); } export async function selectWorkspaceInSidebar(page: Page, workspaceId: string): Promise { diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 28ab67cc6..f823a1aaf 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -1109,7 +1109,11 @@ function SettingsSidebar({ testID="settings-back-to-workspace" /> - + {sidebarBody} 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 dde7085e6..5e463ef90 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -1167,7 +1167,9 @@ function ResolvedWorkspaceDesktopTabsRow({ const tabScrollOffset = useSharedValue(0); const tabScrollViewportWidth = useSharedValue(0); const tabScrollContentWidth = useSharedValue(0); - const labelMeasurementsRef = useRef(new Map()); + const [labelMeasurements, setLabelMeasurements] = useState( + () => new Map(), + ); const [trackSnapshot, setTrackSnapshot] = useState(null); const workspaceRoot = useWorkspaceDirectory(normalizedServerId, normalizedWorkspaceId) ?? ""; const checkoutStatus = useCheckoutStatusQuery({ @@ -1298,24 +1300,29 @@ function ResolvedWorkspaceDesktopTabsRow({ [fallbackTabLabels, tabs], ); const tabLabelSignature = useMemo(() => workspaceTabLabelSignature(tabLabels), [tabLabels]); + const currentTabLabelKeys = useMemo(() => new Set(tabLabels.map(({ key }) => key)), [tabLabels]); + useEffect(() => { + setLabelMeasurements((current) => { + const removedKeys = [...current.keys()].filter((key) => !currentTabLabelKeys.has(key)); + if (removedKeys.length === 0) { + return current; + } + const next = new Map(current); + for (const key of removedKeys) { + next.delete(key); + } + return next; + }); + }, [currentTabLabelKeys]); const publishMeasuredTrack = useCallback(() => { if (tabsContainerWidth <= 0) { return; } - const labelWidths = completeWorkspaceTabLabelWidths(tabLabels, labelMeasurementsRef.current); + const labelWidths = completeWorkspaceTabLabelWidths(tabLabels, labelMeasurements); if (!labelWidths) { return; } - const retainedMeasurements = new Map(); - for (const { key } of tabLabels) { - const measurement = labelMeasurementsRef.current.get(key); - if (measurement) { - retainedMeasurements.set(key, measurement); - } - } - labelMeasurementsRef.current = retainedMeasurements; - setTrackSnapshot((current) => { if ( current?.signature === tabLabelSignature && @@ -1330,7 +1337,7 @@ function ResolvedWorkspaceDesktopTabsRow({ labelWidths, }; }); - }, [tabLabelSignature, tabLabels, tabs, tabsContainerWidth]); + }, [labelMeasurements, tabLabelSignature, tabLabels, tabs, tabsContainerWidth]); useLayoutEffect(() => { publishMeasuredTrack(); @@ -1342,14 +1349,17 @@ function ResolvedWorkspaceDesktopTabsRow({ if (width <= 0) { return; } - const current = labelMeasurementsRef.current.get(key); - if (current?.label === label && current.width === width) { - return; - } - labelMeasurementsRef.current.set(key, { label, width }); - publishMeasuredTrack(); + setLabelMeasurements((current) => { + const measurement = current.get(key); + if (measurement?.label === label && measurement.width === width) { + return current; + } + const next = new Map(current); + next.set(key, { label, width }); + return next; + }); }, - [publishMeasuredTrack], + [], ); const displayedTabs = useMemo(() => { diff --git a/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts b/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts index 0744c780d..768514022 100644 --- a/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts +++ b/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts @@ -156,13 +156,13 @@ test("a rebound shortcut lists its new keys in the shortcuts cheat sheet", async await page.keyboard.press("Alt+Shift+K"); await page.getByText("Done", { exact: true }).click(); - const reboundKeys = page.getByText("⌥+Shift+K", { exact: true }); + const reboundKeys = page.getByText("⌥⇧K", { exact: true }); await expect(reboundKeys).toBeVisible(); await expect(defaultKeys).toHaveCount(0); const dialog = await openCheatSheet(page); const row = dialog.getByTestId(`shortcut-help-row-${SHORTCUTS_ROW}`); - await expect(row.getByText("⌥+Shift+K", { exact: true })).toBeVisible(); + await expect(row.getByText("⌥⇧K", { exact: true })).toBeVisible(); // The whole point: the cheat sheet stops advertising the shipped default. await expect(row.getByText("?", { exact: true })).toHaveCount(0); }); diff --git a/packages/desktop/e2e/settings-sidebar-scroll.spec.ts b/packages/desktop/e2e/settings-sidebar-scroll.spec.ts index 942fcdb42..fdc585f4b 100644 --- a/packages/desktop/e2e/settings-sidebar-scroll.spec.ts +++ b/packages/desktop/e2e/settings-sidebar-scroll.spec.ts @@ -25,19 +25,17 @@ test.describe("Settings sidebar scrolling", () => { await openSettings(page); const sidebar = page.getByTestId("settings-sidebar"); + const scrollBody = page.getByTestId("settings-sidebar-scroll-body"); await expect(sidebar).toBeVisible(); + await expect(scrollBody).toBeVisible(); const geometry = await sidebar.evaluate((node) => { - let scroller: HTMLElement | null = null; - for (const element of node.querySelectorAll("*")) { - if (element.scrollHeight > element.clientHeight) { - scroller = element; - break; - } - } - if (!scroller) return null; + const scrollBodyElement = node.querySelector( + '[data-testid="settings-sidebar-scroll-body"]', + ); + if (!scrollBodyElement) return null; - const scrollerRect = scroller.getBoundingClientRect(); + const scrollerRect = scrollBodyElement.getBoundingClientRect(); const dragRegions = []; for (const element of node.querySelectorAll("*")) { if (getComputedStyle(element).getPropertyValue("-webkit-app-region") === "drag") {