From f22a37e613e965c8ebc02e1f5565e21fd72eaf2f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 11 Sep 2026 10:16:33 +0200 Subject: [PATCH] fix(app): let touch workspace titles use the available width (#4698) * fix(app): let touch workspace titles use the available width Omit trailing stats when the permanent touch menu replaces them. Keep concealed stats in layout only for temporary desktop shortcut hints, preserving hover and shortcut geometry. * test(app): express sidebar width checks as user journeys --- .../e2e/browser/sidebar-title-width.spec.ts | 153 ++++++++++++++++++ .../src/components/sidebar-workspace-list.tsx | 4 +- .../sidebar/sidebar-status-list.tsx | 11 +- .../sidebar/sidebar-workspace-row-content.tsx | 26 ++- .../sidebar/sidebar-workspace-row.tsx | 4 +- 5 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 packages/app/e2e/browser/sidebar-title-width.spec.ts diff --git a/packages/app/e2e/browser/sidebar-title-width.spec.ts b/packages/app/e2e/browser/sidebar-title-width.spec.ts new file mode 100644 index 0000000000..80f96dc68b --- /dev/null +++ b/packages/app/e2e/browser/sidebar-title-width.spec.ts @@ -0,0 +1,153 @@ +import { rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import type { Locator, Page, TestInfo } from "@playwright/test"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { + closeSidebarDisplayPreferences, + openMobileAgentSidebar, + openSidebarDisplayPage, + selectSidebarStatusGrouping, +} from "../support/helpers/sidebar"; + +const TITLE = "Linux desktop sandbox launch support"; + +async function seedChangedWorkspace() { + const workspace = await seedWorkspace({ + repoPrefix: "sidebar-title-", + title: TITLE, + repo: { withRemote: true }, + }); + try { + await rm(path.join(workspace.repoPath, "remote.git"), { recursive: true }); + await workspace.client.renameProject(workspace.projectId, "Paseo"); + await writeFile(path.join(workspace.repoPath, "README.md"), "Changed line\n".repeat(12345)); + await workspace.client.checkoutRefresh(workspace.repoPath); + await expect + .poll(async () => { + const { entries } = await workspace.client.fetchWorkspaces(); + return entries.find((entry) => entry.id === workspace.workspaceId)?.diffStat?.additions; + }) + .toBe(12345); + return workspace; + } catch (error) { + await workspace.cleanup(); + throw error; + } +} + +function workspaceRow(page: Page) { + return page.getByRole("button", { name: new RegExp(TITLE) }).filter({ visible: true }); +} + +async function titleWidth(row: Locator) { + return row + .getByText(TITLE, { exact: true }) + .evaluate((element) => element.getBoundingClientRect().width); +} + +async function toggleTrailing(page: Page, label: "Diff stats" | "Last activity", touch = false) { + await openSidebarDisplayPage(page, "sidebar-display-show"); + await page.getByRole("menuitem", { name: label, exact: true }).click(); + if (touch) { + await page + .getByRole("button", { name: "Bottom sheet backdrop", exact: true }) + .click({ position: { x: 5, y: 5 } }); + await expect( + page.getByRole("button", { name: "Bottom sheet backdrop", exact: true }), + ).toHaveCount(0); + } else { + await closeSidebarDisplayPreferences(page); + } + await page.mouse.move(0, 0); +} + +async function expectTouchTitleWidth(page: Page) { + const row = workspaceRow(page); + const withDiff = await titleWidth(row); + await toggleTrailing(page, "Diff stats", true); + const withoutStats = await titleWidth(row); + expect(withDiff).toBeCloseTo(withoutStats, 0); + await toggleTrailing(page, "Last activity", true); + expect(await titleWidth(row)).toBeCloseTo(withoutStats, 0); + await toggleTrailing(page, "Diff stats", true); +} + +let workspace: SeededWorkspace; +test.beforeEach(async () => { + workspace = await seedChangedWorkspace(); +}); +test.afterEach(async () => { + await workspace?.cleanup(); +}); + +async function openTouchWorkspaceList(page: Page, testInfo: TestInfo) { + await page.setViewportSize({ width: 390, height: 844 }); + await page.emulateMedia({ colorScheme: "dark" }); + await gotoAppShell(page); + await openMobileAgentSidebar(page); + await expect(workspaceRow(page)).toBeVisible(); + await page.mouse.move(0, 0); + await page.screenshot({ path: testInfo.outputPath("sidebar.png") }); +} + +async function pinWorkspaceForLayout(page: Page) { + await workspace.client.setWorkspacePinned(workspace.workspaceId, true); + await expect(page.getByTestId("sidebar-pinned-section")).toBeVisible(); +} + +async function moveWorkspaceToStatusGrouping(page: Page) { + await workspace.client.setWorkspacePinned(workspace.workspaceId, false); + await selectSidebarStatusGrouping(page); +} + +async function openDesktopWorkspaceList(page: Page) { + await gotoAppShell(page); + const row = workspaceRow(page); + await expect(row).toBeVisible(); + await page.mouse.move(0, 0); + await expect(row.getByText("+12.3k", { exact: true })).toBeVisible(); + return titleWidth(row); +} + +async function expectHoverKeepsTitleWidth(page: Page, width: number, testInfo: TestInfo) { + const row = workspaceRow(page); + await row.hover(); + await expect(row.getByLabel("Workspace actions", { exact: true })).toBeVisible(); + expect(await titleWidth(row)).toBeCloseTo(width, 0); + await page.screenshot({ path: testInfo.outputPath("desktop-hover.png") }); +} + +async function expectShortcutHintsKeepTitleWidth(page: Page, width: number, testInfo: TestInfo) { + const row = workspaceRow(page); + await page.keyboard.down("Alt"); + await expect(row.getByText("1", { exact: true })).toBeVisible(); + expect(await titleWidth(row)).toBeCloseTo(width, 0); + await page.screenshot({ path: testInfo.outputPath("desktop-shortcuts.png") }); + await page.keyboard.up("Alt"); + await expect(row.getByText("1", { exact: true })).toHaveCount(0); +} + +async function expectDisablingStatsExpandsTitle(page: Page, width: number) { + await toggleTrailing(page, "Diff stats"); + expect(await titleWidth(workspaceRow(page))).toBeGreaterThan(width); +} + +test("touch titles use the space occupied by hidden stats in every grouping", async ({ + page, +}, testInfo) => { + await openTouchWorkspaceList(page, testInfo); + await expectTouchTitleWidth(page); + await pinWorkspaceForLayout(page); + await expectTouchTitleWidth(page); + await moveWorkspaceToStatusGrouping(page); + await expectTouchTitleWidth(page); +}); + +test("desktop hover and shortcut hints preserve title width", async ({ page }, testInfo) => { + const width = await openDesktopWorkspaceList(page); + await expectHoverKeepsTitleWidth(page, width, testInfo); + await expectShortcutHintsKeepTitleWidth(page, width, testInfo); + await expectDisablingStatsExpandsTitle(page, width); +}); diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 23e4292aeb..4275099cfd 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -645,7 +645,7 @@ function WorkspaceRowRightGroup({ const trailing = useSidebarWorkspaceTrailing(); const showShortcut = showShortcutBadge && shortcutNumber !== null; const { - showTrailing, + trailingPresentation, showKebab: showKebabInSlot, showScrim, renderSlot, @@ -667,7 +667,7 @@ function WorkspaceRowRightGroup({ ) : null} {renderSlot ? ( - + { const showShortcut = showShortcutBadge && shortcutNumber !== null; const { - showTrailing, + trailingPresentation, showKebab: showKebabInSlot, showScrim, renderSlot, @@ -907,7 +908,7 @@ function StatusWorkspaceRowInnerContent({ workspace={workspace} backdrop={backdrop} trailing={trailing} - showBase={showTrailing} + trailingPresentation={trailingPresentation} showKebab={showKebabInSlot} showScrim={showScrim} reserveSlotWidth={reserveSlotWidth} @@ -938,7 +939,7 @@ function StatusWorkspaceActionSlot({ workspace, backdrop, trailing, - showBase, + trailingPresentation, showKebab, showScrim, reserveSlotWidth, @@ -958,7 +959,7 @@ function StatusWorkspaceActionSlot({ workspace: SidebarWorkspaceEntry; backdrop: SidebarSurfaceBackdrop; trailing: SidebarWorkspaceTrailing; - showBase: boolean; + trailingPresentation: SidebarWorkspaceTrailingPresentation; showKebab: boolean; showScrim: boolean; reserveSlotWidth: boolean; @@ -978,7 +979,7 @@ function StatusWorkspaceActionSlot({ const kebab = useOpenKebabMenuVisibility(showKebab); return ( - + {children}; + if (presentation === "absent") return null; + return ( + + {children} + + ); } export function SidebarWorkspaceTrailingActionOverlay({ diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx index f019158e97..f8dcdebaf5 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx @@ -403,7 +403,7 @@ function WorkspaceRowTrailingActions({ const { t } = useTranslation(); const showShortcut = showShortcutBadge && shortcutNumber !== null; const { - showTrailing, + trailingPresentation, showKebab: showKebabInSlot, showScrim, renderSlot, @@ -425,7 +425,7 @@ function WorkspaceRowTrailingActions({ ) : null} {renderSlot ? ( - +