From 4b4bcc2e604c69b97da4cd96fac8202937b7d505 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:12:33 -0700 Subject: [PATCH] Revert "Size tabs to titles and preserve floating panel drag space (#34025)" (#34506) This reverts commit 316b1887fd287dd59606e5fbfe20638f0e14b71c. --- .../FloatingTerminalPanelSurface.tsx | 6 - .../src/components/tab-bar/BrowserTab.tsx | 4 +- .../tab-bar/ClientHostedBrowserTab.tsx | 4 +- .../src/components/tab-bar/EditorFileTab.tsx | 4 +- .../src/components/tab-bar/SortableTab.tsx | 4 +- .../tab-bar/TabBar.context-menu.test.ts | 5 +- .../src/components/tab-bar/TabBar.fixture.tsx | 167 --------------- .../src/components/tab-bar/TabHoverCard.tsx | 2 +- .../tab-bar/TerminalTabLeadingIcon.tsx | 14 +- .../tab-bar/tab-title-tooltip.test.tsx | 8 +- .../src/components/tab-bar/tab-width-rules.ts | 6 +- .../floating-workspace-pane-height.spec.ts | 190 +++--------------- tests/e2e/tab-strip-active-dock.spec.ts | 166 --------------- 13 files changed, 54 insertions(+), 526 deletions(-) delete mode 100644 src/renderer/src/components/tab-bar/TabBar.fixture.tsx diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanelSurface.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanelSurface.tsx index 5a3f2b34a51..0b7563f55f5 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanelSurface.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanelSurface.tsx @@ -181,12 +181,6 @@ export function renderFloatingTerminalPanelSurface({ tabStripChrome="floating-panel" /> - {/* Keep a grab area even when the tab strip overflows. */} -
{ onTabPointerDown( e, diff --git a/src/renderer/src/components/tab-bar/ClientHostedBrowserTab.tsx b/src/renderer/src/components/tab-bar/ClientHostedBrowserTab.tsx index c968f9a4069..b3e83d037a0 100644 --- a/src/renderer/src/components/tab-bar/ClientHostedBrowserTab.tsx +++ b/src/renderer/src/components/tab-bar/ClientHostedBrowserTab.tsx @@ -12,7 +12,7 @@ import { getClientHostedBrowserRowLabel } from './client-hosted-browser-row-label' import { preventMiddleButtonDefault } from './middle-button-default-guard' -import { TAB_LABEL_WIDTH_CLASSES, TAB_ROOT_WIDTH_CLASSES } from './tab-width-rules' +import { TAB_LABEL_WIDTH_CLASSES } from './tab-width-rules' import { useTabStripSlotProps } from './use-tab-strip-slot-props' /** Paired-client pages have no unified tab: expose presence and close without persisted ordering, pinning or drag actions. */ @@ -47,7 +47,7 @@ export default function ClientHostedBrowserTab({ >
{ if (event.button === 1) { diff --git a/src/renderer/src/components/tab-bar/EditorFileTab.tsx b/src/renderer/src/components/tab-bar/EditorFileTab.tsx index 80f7df546c7..2375c75e61a 100644 --- a/src/renderer/src/components/tab-bar/EditorFileTab.tsx +++ b/src/renderer/src/components/tab-bar/EditorFileTab.tsx @@ -28,7 +28,7 @@ import { import { canOpenMarkdownPreview } from '@/components/editor/markdown-preview-controls' import { EditorFileTabContextMenu } from './EditorFileTabContextMenu' import { translate } from '@/i18n/i18n' -import { TAB_LABEL_WIDTH_CLASSES, TAB_ROOT_WIDTH_CLASSES } from './tab-width-rules' +import { TAB_LABEL_WIDTH_CLASSES } from './tab-width-rules' import { useTabStripSlotProps } from './use-tab-strip-slot-props' import { EditorFileTabCloseButton } from './EditorFileTabCloseButton' import { useTabStripPointerActivation } from './tab-strip-pointer-activation' @@ -229,7 +229,7 @@ export default function EditorFileTab({ data-pinned={isPinned ? 'true' : 'false'} {...attributes} {...dragListeners} - className={`${TAB_ROOT_WIDTH_CLASSES} group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none outline-none focus:outline-none focus-visible:outline-none ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`} + className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none outline-none focus:outline-none focus-visible:outline-none ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`} onPointerDown={(e) => { onTabPointerDown( e, diff --git a/src/renderer/src/components/tab-bar/SortableTab.tsx b/src/renderer/src/components/tab-bar/SortableTab.tsx index e58ba574244..df8bca0ec35 100644 --- a/src/renderer/src/components/tab-bar/SortableTab.tsx +++ b/src/renderer/src/components/tab-bar/SortableTab.tsx @@ -23,7 +23,7 @@ import { useTabRenameReturnFocus } from './use-tab-rename-return-focus' import { SortableTabContextMenu } from './SortableTabContextMenu' import { useSortableTabContextMenu } from './use-sortable-tab-context-menu' import { translate } from '@/i18n/i18n' -import { TAB_LABEL_WIDTH_CLASSES, TAB_ROOT_WIDTH_CLASSES } from './tab-width-rules' +import { TAB_LABEL_WIDTH_CLASSES } from './tab-width-rules' import { useTabStripSlotProps } from './use-tab-strip-slot-props' import { useTabStripPointerActivation } from './tab-strip-pointer-activation' import { TerminalTabLeadingIcon } from './TerminalTabLeadingIcon' @@ -209,7 +209,7 @@ export default function SortableTab({ data-agent-activity-status={activityStatus} {...attributes} {...dragListeners} - className={`${TAB_ROOT_WIDTH_CLASSES} group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none outline-none focus:outline-none focus-visible:outline-none ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`} + className={`group relative flex items-center h-full px-1.5 text-xs cursor-pointer select-none outline-none focus:outline-none focus-visible:outline-none ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getDropIndicatorClasses(dropIndicator ?? null)} ${getTabRootStateClasses(isActive)}`} onDoubleClick={(e) => { if (isEditing) { return diff --git a/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts b/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts index 9e94c698b91..80fa9cc1632 100644 --- a/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts +++ b/src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts @@ -353,7 +353,7 @@ describe('TabBar context menu wiring', () => { expect(sortable[0].props.tabCount).toBe(2) }) - it('keeps the tab strip content-sized while allowing horizontal scrolling', async () => { + it('keeps the tab strip content-sized until horizontal scrolling is needed', async () => { const element = await renderTabBar({ tabs: [TERMINAL_TAB], editorFiles: [EDITOR_FILE], @@ -362,7 +362,7 @@ describe('TabBar context menu wiring', () => { }) const divs = findChildrenByType(element, 'div') const stripWrapper = divs.find((candidate) => - String(candidate.props.className ?? '').includes('group/tab-strip') + String(candidate.props.className ?? '').includes('flex-[0_1_auto]') ) const strip = divs.find((candidate) => String(candidate.props.className ?? '').includes('terminal-tab-strip') @@ -371,7 +371,6 @@ describe('TabBar context menu wiring', () => { expect(stripWrapper).toBeTruthy() expect(stripWrapper?.props.className).toContain('min-w-0') expect(stripWrapper?.props.className).toContain('max-w-full') - expect(stripWrapper?.props.className).toContain('flex-[0_1_auto]') expect(strip).toBeTruthy() expect(strip?.props.className).toContain('min-w-0') expect(strip?.props.className).toContain('flex-1') diff --git a/src/renderer/src/components/tab-bar/TabBar.fixture.tsx b/src/renderer/src/components/tab-bar/TabBar.fixture.tsx deleted file mode 100644 index b4d7516155d..00000000000 --- a/src/renderer/src/components/tab-bar/TabBar.fixture.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useState } from 'react' -import { DndContext } from '@dnd-kit/core' -import { defineFixture } from '../../component-fixtures/component-fixture' -import type { TerminalTab } from '../../../../shared/terminal-tab-types' -import TabBar from './TabBar' -import type { TabBarProps } from './tab-bar-props' - -const WORKSPACE_ID = 'tab-width-fixture' -const TITLES = [ - 'Should we regenerate the conversation title?', - 'Native chat tab title regeneration', - 'Codex Chat', - "You're the president agent coordinating this review" -] -const noop = (): void => {} - -function TabWidthFixture({ - count, - titles = TITLES, - browserTabs, - editorFiles, - tabBarOrder -}: { - count: number - titles?: string[] -} & Pick): React.JSX.Element { - const [activeTabId, setActiveTabId] = useState('tab-0') - const tabs: TerminalTab[] = Array.from({ length: count }, (_, index) => ({ - id: `tab-${index}`, - worktreeId: WORKSPACE_ID, - ptyId: null, - title: titles[index % titles.length], - customTitle: null, - color: null, - sortOrder: index, - createdAt: 0, - launchAgent: 'codex' - })) - - return ( - -
- -
-
- ) -} - -export const spacious = defineFixture({ - width: 1280, - render: () => -}) - -export const single = defineFixture({ - width: 1280, - render: () => -}) - -export const long = defineFixture({ - width: 1280, - render: () => -}) - -export const contentFit = defineFixture({ - width: 1280, - render: () => ( - - ) -}) - -export const narrow = defineFixture({ - width: 420, - render: () => -}) - -export const mixed = defineFixture({ - width: 620, - render: () => ( - - ) -}) - -export const overflowing = defineFixture({ - width: 420, - render: () => -}) - -export const mixedTypes = defineFixture({ - width: 620, - render: () => ( - - ) -}) - -export const competingTitles = defineFixture({ - width: 620, - render: () => ( - - ) -}) diff --git a/src/renderer/src/components/tab-bar/TabHoverCard.tsx b/src/renderer/src/components/tab-bar/TabHoverCard.tsx index a84c307afe6..0068112123d 100644 --- a/src/renderer/src/components/tab-bar/TabHoverCard.tsx +++ b/src/renderer/src/components/tab-bar/TabHoverCard.tsx @@ -56,7 +56,7 @@ export function TabHoverCard({ className="flex items-center gap-2 text-muted-foreground" data-tab-hover-card-program > - + {icon} {programName} diff --git a/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx b/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx index a86be6e78fc..677297e17ca 100644 --- a/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx +++ b/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx @@ -51,8 +51,6 @@ export function TerminalTabLeadingIcon({ isActive, progress }: TerminalTabLeadingIconProps): React.JSX.Element { - // Reserve activity and identity slots before agent detection as well as status changes. - const iconClassName = 'mr-1 inline-flex w-7 shrink-0 items-center gap-1' if (showUnreadActivity) { return ( {agent ? : null} @@ -76,7 +74,7 @@ export function TerminalTabLeadingIcon({ {/* Why: the state glyph is self-describing and the tab title already has a tooltip; hover here stays quiet. */} @@ -88,17 +86,13 @@ export function TerminalTabLeadingIcon({ if (agent) { return ( - + ) } return ( diff --git a/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx b/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx index 2e1d5c88e5e..41996d9dc17 100644 --- a/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx +++ b/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx @@ -199,10 +199,12 @@ function textSpanHtml(markup: string, text: string): string { function expectTabContainerWidth(markup: string, root: string): void { const container = firstOpeningTag(markup) - const widthClasses = 'min-w-25 max-w-max flex-[1_1_0px]' + // Assert literal widths so changing the shared constant cannot silently allow live-title tab resizing. + const widthClasses = 'w-[180px] min-w-[72px] min-[1280px]:w-[220px]' expect(container).toContain(widthClasses) - expect(root).not.toContain('min-w-25') - expect(root).toContain('w-full') + expect(root).not.toContain('w-[180px]') + expect(root).not.toContain('min-w-[72px]') + expect(root).not.toContain('min-[1280px]:w-[220px]') } function expectTooltipContent(markup: string, text: string): void { diff --git a/src/renderer/src/components/tab-bar/tab-width-rules.ts b/src/renderer/src/components/tab-bar/tab-width-rules.ts index be09c2f1d6d..47a7b68f7a0 100644 --- a/src/renderer/src/components/tab-bar/tab-width-rules.ts +++ b/src/renderer/src/components/tab-bar/tab-width-rules.ts @@ -1,6 +1,4 @@ -// Share crowded strips equally, releasing unused space from tabs that fit their titles. -export const TAB_CONTAINER_WIDTH_CLASSES = 'min-w-25 max-w-max flex-[1_1_0px]' - -export const TAB_ROOT_WIDTH_CLASSES = 'w-full' +// Definite widths prevent live titles from resizing the strip; flex-shrink still allows narrowing to the minimum. +export const TAB_CONTAINER_WIDTH_CLASSES = 'w-[180px] min-w-[72px] min-[1280px]:w-[220px]' export const TAB_LABEL_WIDTH_CLASSES = 'min-w-0 flex-1 truncate' diff --git a/tests/e2e/floating-workspace-pane-height.spec.ts b/tests/e2e/floating-workspace-pane-height.spec.ts index 8b6bd504b7a..6adfd316538 100644 --- a/tests/e2e/floating-workspace-pane-height.spec.ts +++ b/tests/e2e/floating-workspace-pane-height.spec.ts @@ -42,43 +42,40 @@ type E2EWindow = typeof window & { __store?: E2EStore } -async function seedFloatingSimulatorTabs(page: Page, count = 2): Promise { - const tabs = await page.evaluate( - ({ worktreeId, count }) => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `__store` is the dev/e2e store handle Orca injects on window; the cast only names it. - const store = (window as E2EWindow).__store - if (!store) { - throw new Error('Store unavailable') +async function seedFloatingSimulatorTabs(page: Page): Promise { + const tabs = await page.evaluate((worktreeId) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `__store` is the dev/e2e store handle Orca injects on window; the cast only names it. + const store = (window as E2EWindow).__store + if (!store) { + throw new Error('Store unavailable') + } + const state = store.getState() + store.setState({ + settings: { + ...state.settings, + floatingTerminalEnabled: true, + mobileEmulatorEnabled: true } - const state = store.getState() - store.setState({ - settings: { - ...state.settings, - floatingTerminalEnabled: true, - mobileEmulatorEnabled: true - } - }) - const refreshed = store.getState() - const existing = (refreshed.unifiedTabsByWorktree[worktreeId] ?? []).filter( - (tab) => tab.contentType === 'simulator' + }) + const refreshed = store.getState() + const existing = (refreshed.unifiedTabsByWorktree[worktreeId] ?? []).filter( + (tab) => tab.contentType === 'simulator' + ) + while (existing.length < 2) { + existing.push( + refreshed.createUnifiedTab(worktreeId, 'simulator', { + label: `Mobile Emulator ${existing.length + 1}`, + recordInteraction: false + }) ) - while (existing.length < count) { - existing.push( - refreshed.createUnifiedTab(worktreeId, 'simulator', { - label: `Mobile Emulator ${existing.length + 1}`, - recordInteraction: false - }) - ) - } - refreshed.activateTab(existing[0].id) - return existing.map((tab) => ({ - id: tab.id, - contentType: tab.contentType, - groupId: tab.groupId - })) - }, - { worktreeId: FLOATING_WORKTREE_ID, count } - ) + } + refreshed.activateTab(existing[0].id) + return existing.map((tab) => ({ + id: tab.id, + contentType: tab.contentType, + groupId: tab.groupId + })) + }, FLOATING_WORKTREE_ID) await page.waitForFunction( (panelSelector) => Boolean(document.querySelector(panelSelector)), PANEL_SELECTOR, @@ -147,126 +144,3 @@ test('floating panel group bodies and panes own real pixels, single group and sp await orcaPage.mouse.click(firstBodyBox!.x + 20, firstBodyBox!.y + 20) await expect(openPanel.locator(`[data-tab-id="${first.id}"]`)).toBeVisible() }) - -test('tabs use spare room for titles, share crowded space, and scroll at the minimum', async ({ - orcaPage -}) => { - const titles = [ - 'A very long conversation title '.repeat(12), - 'Go', - 'Another long conversation title '.repeat(3) - ] - const seedFloatingTerminalTabs = (count: number): Promise => - orcaPage.evaluate( - ({ count, titles, worktreeId }) => { - const store = window.__store! - store.setState((state) => ({ - settings: { ...state.settings!, floatingTerminalEnabled: true } - })) - const tabs = [...(store.getState().tabsByWorktree[worktreeId] ?? [])] - while (tabs.length < count) { - const tab = store - .getState() - .createTab(worktreeId, undefined, undefined, { activate: false }) - store - .getState() - .setTabCustomTitle(tab.id, titles[tabs.length] ?? `Tab ${tabs.length + 1}`) - tabs.push(tab) - } - store.getState().setActiveTabForWorktree(worktreeId, tabs[0].id) - store.getState().activateTab(tabs[0].id) - return tabs.map((tab) => tab.id) - }, - { count, titles, worktreeId: FLOATING_WORKTREE_ID } - ) - const [first] = await seedFloatingTerminalTabs(1) - await openFloatingPanel(orcaPage) - const panel = orcaPage.locator(OPEN_PANEL_SELECTOR).first() - await panel.evaluate((element) => { - element.style.maxWidth = '720px' - }) - const firstSlot = panel.locator(`[data-tab-strip-slot="${first}"]`) - const width = (): Promise => - firstSlot.evaluate((element) => element.getBoundingClientRect().width) - await expect.poll(width).toBeGreaterThan(384) - const singleWidth = await width() - const strip = panel.locator('.terminal-tab-strip').first() - - const [, second] = await seedFloatingTerminalTabs(2) - await expect(panel.locator(`[data-tab-strip-slot="${second}"]`)).toContainText('Go') - await expect.poll(width).toBeCloseTo(singleWidth - 100, 0) - - const [, , third] = await seedFloatingTerminalTabs(3) - await expect(panel.locator(`[data-tab-strip-slot="${third}"]`)).toContainText( - 'Another long conversation title' - ) - await expect - .poll(() => strip.evaluate((element) => element.scrollWidth > element.clientWidth)) - .toBe(false) - const widths = (): Promise => - panel - .locator('[data-tab-strip-slot]') - .evaluateAll((slots) => slots.map((element) => element.getBoundingClientRect().width)) - const crowdedWidths = await widths() - expect(crowdedWidths.every((value) => value >= 100)).toBe(true) - expect(crowdedWidths[0]).toBeCloseTo(crowdedWidths[2], 0) - expect(crowdedWidths[1]).toBe(100) - - await seedFloatingTerminalTabs(6) - await expect(panel.locator('[data-tab-strip-slot]')).toHaveCount(6) - await expect - .poll(() => strip.evaluate((element) => element.scrollWidth > element.clientWidth)) - .toBe(true) - expect(await widths()).toEqual([100, 100, 100, 100, 100, 100]) -}) - -// Real pointer capture requires the isolated visible-window project. -test('@headful floating header keeps a drag and maximize target with expanded and overflowing tabs', async ({ - orcaPage -}) => { - for (const count of [1, 16]) { - await seedFloatingSimulatorTabs(orcaPage, count) - await openFloatingPanel(orcaPage) - - const panel = orcaPage.locator(OPEN_PANEL_SELECTOR).first() - const handle = panel.locator('[data-floating-terminal-drag-handle]') - const strip = panel.locator('.terminal-tab-strip').first() - await expect - .poll(() => strip.evaluate((element) => element.scrollWidth > element.clientWidth)) - .toBe(count > 1) - - const before = await panel.boundingBox() - const grab = await handle.boundingBox() - expect(before).not.toBeNull() - expect(grab).not.toBeNull() - expect(grab!.width).toBeGreaterThanOrEqual(48) - expect(grab!.height).toBeGreaterThanOrEqual(28) - - const x = grab!.x + grab!.width / 2 - const y = grab!.y + grab!.height / 2 - await orcaPage.mouse.move(x, y) - await orcaPage.mouse.down() - await orcaPage.mouse.move(x - 40, y + 24, { steps: 4 }) - await orcaPage.mouse.up() - await expect - .poll(() => panel.evaluate((element) => element.getBoundingClientRect().x)) - .toBeCloseTo(before!.x - 40, 0) - await expect - .poll(() => panel.evaluate((element) => element.getBoundingClientRect().y)) - .toBeCloseTo(before!.y + 24, 0) - - await handle.dblclick() - await expect(panel.getByRole('button', { name: 'Restore floating workspace' })).toHaveAttribute( - 'aria-pressed', - 'true' - ) - await handle.dblclick() - await expect( - panel.getByRole('button', { name: 'Maximize floating workspace' }) - ).toHaveAttribute('aria-pressed', 'false') - expect(await orcaPage.evaluate(() => window.getSelection()?.isCollapsed ?? true)).toBe(true) - await expect - .poll(() => panel.evaluate((element) => element.getBoundingClientRect().x)) - .toBeCloseTo(before!.x - 40, 0) - } -}) diff --git a/tests/e2e/tab-strip-active-dock.spec.ts b/tests/e2e/tab-strip-active-dock.spec.ts index 624a84cbb65..c4c521cac7d 100644 --- a/tests/e2e/tab-strip-active-dock.spec.ts +++ b/tests/e2e/tab-strip-active-dock.spec.ts @@ -74,172 +74,6 @@ test.describe('Tab strip active-tab dock', () => { await ensureTerminalVisible(orcaPage) }) - test('fits titles without resizing neighbors when tabs open and close in spare space', async ({ - orcaPage - }) => { - const worktreeId = await waitForActiveWorktree(orcaPage) - const tabIds = await orcaPage.evaluate((worktreeId) => { - const state = window.__store!.getState() - const short = state.createTab(worktreeId, undefined, undefined, { activate: false }) - const long = state.createTab(worktreeId, undefined, undefined, { activate: false }) - state.setTabCustomTitle(short.id, 'Terminal 1') - state.setTabCustomTitle( - long.id, - 'Measure the effect of the new tab layout on conversation navigation' - ) - return { short: short.id, long: long.id } - }, worktreeId) - const short = orcaPage.locator(slotSelector(tabIds.short)) - const long = orcaPage.locator(slotSelector(tabIds.long)) - const width = (tab: typeof short): Promise => - tab.evaluate((element) => element.getBoundingClientRect().width) - - await expect(short).toContainText('Terminal 1') - await expect(long).toContainText( - 'Measure the effect of the new tab layout on conversation navigation' - ) - expect(await width(short)).toBeLessThan(140) - expect(await width(long)).toBeGreaterThan(384) - const label = long.locator('span.truncate') - expect(await label.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true) - const geometry = (): Promise => - orcaPage.evaluate( - (ids) => - ids.map((id) => { - const slot = document.querySelector(`[data-tab-strip-slot="${id}"]`)! - const tab = slot.getBoundingClientRect() - const close = slot.querySelector('[data-tab-close-button]')!.getBoundingClientRect() - return [tab.x, close.x] - }), - [tabIds.short, tabIds.long] - ) - const before = await geometry() - const added = await orcaPage.evaluate((worktreeId) => { - const state = window.__store!.getState() - const tab = state.createTab(worktreeId, undefined, undefined, { activate: false }) - state.setTabCustomTitle(tab.id, 'Go') - return tab.id - }, worktreeId) - await expect(orcaPage.locator(slotSelector(added))).toContainText('Go') - for (let frame = 0; frame < 4; frame++) { - await nextFrames(orcaPage, 1) - expect(await geometry()).toEqual(before) - } - await long.locator('[data-testid="sortable-tab"]').click() - await expect(long.locator('[data-active="true"]')).toHaveCount(1) - expect(await geometry()).toEqual(before) - await orcaPage.locator(`${slotSelector(added)} [data-tab-close-button]`).click() - await expect(orcaPage.locator(slotSelector(added))).toHaveCount(0) - for (let frame = 0; frame < 4; frame++) { - await nextFrames(orcaPage, 1) - expect(await geometry()).toEqual(before) - } - - await orcaPage.evaluate( - ({ id }) => { - window.__store!.getState().setTabCustomTitle(id, 'Chat') - }, - { id: tabIds.long } - ) - await expect(long).toContainText('Chat') - await expect.poll(() => width(long)).toBeLessThanOrEqual(100) - - await orcaPage.evaluate( - ({ id }) => { - window - .__store!.getState() - .setTabCustomTitle(id, 'A very long conversation title '.repeat(20)) - }, - { id: tabIds.long } - ) - await expect(long).toContainText('A very long conversation title') - await expect.poll(() => width(long)).toBeGreaterThan(384) - const strip = orcaPage.locator(STRIP).first() - expect(await strip.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true) - const stripBox = await strip.boundingBox() - const buttonBox = await orcaPage - .getByRole('button', { name: 'New tab', exact: true }) - .first() - .boundingBox() - expect(stripBox).not.toBeNull() - expect(buttonBox).not.toBeNull() - expect(buttonBox!.x - stripBox!.x - stripBox!.width).toBeGreaterThanOrEqual(0) - expect(buttonBox!.x - stripBox!.x - stripBox!.width).toBeLessThanOrEqual(12) - }) - - test('keeps tab and close-button positions stable through agent detection and activity changes', async ({ - orcaPage - }) => { - const worktreeId = await waitForActiveWorktree(orcaPage) - const ids = await orcaPage.evaluate((worktreeId) => { - const store = window.__store! - const agent = store - .getState() - .createTab(worktreeId, undefined, undefined, { activate: false }) - const next = store.getState().createTab(worktreeId, undefined, undefined, { activate: false }) - store.getState().setTabCustomTitle(agent.id, 'Agent conversation') - store.getState().setTabCustomTitle(next.id, 'Neighbor') - return [agent.id, next.id] - }, worktreeId) - const agent = orcaPage.locator(slotSelector(ids[0])) - await expect(agent.locator('[data-shell-icon]')).toBeVisible() - await expect(orcaPage.locator(slotSelector(ids[1]))).toContainText('Neighbor') - const geometry = (): Promise => - orcaPage.evaluate( - (ids) => - ids.map((id) => { - const slot = document.querySelector(`[data-tab-strip-slot="${id}"]`)! - const tab = slot.getBoundingClientRect() - const close = slot.querySelector('[data-tab-close-button]')!.getBoundingClientRect() - return [tab.x, tab.width, close.x] - }), - ids - ) - const before = await geometry() - - for (const stage of ['working', 'unread', 'idle'] as const) { - await orcaPage.evaluate( - ({ tabId, worktreeId, stage }) => { - const store = window.__store! - const paneKey = `${tabId}:00000000-0000-4000-8000-000000000001` - store.setState((state) => { - const agentStatusByPaneKey = { ...state.agentStatusByPaneKey } - const unreadTerminalTabs = { ...state.unreadTerminalTabs } - delete agentStatusByPaneKey[paneKey] - delete unreadTerminalTabs[tabId] - if (stage === 'working') { - agentStatusByPaneKey[paneKey] = { - paneKey, - worktreeId, - agentType: 'codex', - state: 'working', - prompt: '', - updatedAt: Date.now(), - stateStartedAt: Date.now(), - stateHistory: [] - } - } else if (stage === 'unread') { - unreadTerminalTabs[tabId] = true - } - return { agentStatusByPaneKey, unreadTerminalTabs } - }) - }, - { tabId: ids[0], worktreeId, stage } - ) - await expect(agent.locator('[data-testid="tab-agent-activity-indicator"]')).toHaveCount( - stage === 'working' ? 1 : 0 - ) - await expect(agent.locator('[data-testid="tab-activity-bell"]')).toHaveCount( - stage === 'unread' ? 1 : 0 - ) - await expect(agent.locator('[data-agent-icon="codex"]')).toHaveCount( - stage === 'working' ? 1 : 0 - ) - await expect(agent).toContainText('Agent conversation') - await expect.poll(geometry).toEqual(before) - } - }) - test('docks an offscreen active tab and reveals an arriving tab beside it without flicker', async ({ orcaPage }) => {