Revert "Size tabs to titles and preserve floating panel drag space (#34025)" (#34506)

This reverts commit 316b1887fd.
This commit is contained in:
Jinjing
2026-10-27 12:12:33 -07:00
committed by GitHub
parent 55dffb479a
commit 4b4bcc2e60
13 changed files with 54 additions and 526 deletions
@@ -181,12 +181,6 @@ export function renderFloatingTerminalPanelSurface({
tabStripChrome="floating-panel"
/>
</div>
{/* Keep a grab area even when the tab strip overflows. */}
<div
className="h-full w-16 shrink-0 select-none"
data-floating-terminal-drag-handle
aria-hidden
/>
<FloatingTerminalWindowControls
maximized={maximized}
onToggleMaximized={toggleMaximized}
@@ -24,7 +24,7 @@ import {
} from './drop-indicator'
import { preventMiddleButtonDefault } from './middle-button-default-guard'
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 { TabWorkspaceLayoutMenuSection } from './TabWorkspaceLayoutMenuSection'
import { useTabStripPointerActivation } from './tab-strip-pointer-activation'
@@ -158,7 +158,7 @@ export default function BrowserTab({
data-pinned={isPinned ? 'true' : 'false'}
{...attributes}
{...listeners}
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,
@@ -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({
>
<div
data-client-hosted-browser-row-id={row.browserPageId}
className={`${TAB_ROOT_WIDTH_CLASSES} group relative flex h-full cursor-pointer select-none items-center px-1.5 text-xs outline-none focus:outline-none focus-visible:outline-none ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getTabRootStateClasses(isActive)}`}
className={`group relative flex h-full cursor-pointer select-none items-center px-1.5 text-xs outline-none focus:outline-none focus-visible:outline-none ${getTabStripBorderClasses(hasTabsToRight, { includeTopBorder: includeTopTabBorder })} ${getTabRootStateClasses(isActive)}`}
onPointerDown={onActivate}
onMouseDown={(event) => {
if (event.button === 1) {
@@ -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,
@@ -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
@@ -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')
@@ -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<TabBarProps, 'browserTabs' | 'editorFiles' | 'tabBarOrder'>): 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 (
<DndContext>
<div className="@container/tab-strip h-8 bg-card">
<TabBar
tabs={tabs}
activeTabId={activeTabId}
activeTabType="terminal"
worktreeId={WORKSPACE_ID}
groupId="tab-width-group"
browserTabs={browserTabs}
editorFiles={editorFiles}
tabBarOrder={tabBarOrder}
expandedPaneByTabId={{}}
showAgentLaunchItems={false}
onActivate={setActiveTabId}
onClose={noop}
onCloseOthers={noop}
onCloseToRight={noop}
onCloseToLeft={noop}
onNewTerminalTab={noop}
onNewBrowserTab={noop}
onSetCustomTitle={noop}
onSetTabColor={noop}
onTogglePaneExpand={noop}
/>
</div>
</DndContext>
)
}
export const spacious = defineFixture({
width: 1280,
render: () => <TabWidthFixture count={4} />
})
export const single = defineFixture({
width: 1280,
render: () => <TabWidthFixture count={1} titles={['Terminal 1']} />
})
export const long = defineFixture({
width: 1280,
render: () => <TabWidthFixture count={1} titles={['A very long conversation title '.repeat(8)]} />
})
export const contentFit = defineFixture({
width: 1280,
render: () => (
<TabWidthFixture
count={3}
titles={[
'Measure the effect of the new tab layout on conversation navigation',
'PR implementation approach',
'Review the implementation for layout and interaction regressions'
]}
/>
)
})
export const narrow = defineFixture({
width: 420,
render: () => <TabWidthFixture count={4} />
})
export const mixed = defineFixture({
width: 620,
render: () => (
<TabWidthFixture
count={3}
titles={['A very long conversation title '.repeat(8), 'Go', 'Notes']}
/>
)
})
export const overflowing = defineFixture({
width: 420,
render: () => <TabWidthFixture count={12} />
})
export const mixedTypes = defineFixture({
width: 620,
render: () => (
<TabWidthFixture
count={1}
titles={['A very long conversation title '.repeat(12)]}
tabBarOrder={['tab-0', 'browser-0', 'file-0']}
browserTabs={[
{
id: 'browser-0',
worktreeId: WORKSPACE_ID,
url: 'https://google.com',
title: 'Google',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 0
}
]}
editorFiles={[
{
id: 'file-0',
worktreeId: WORKSPACE_ID,
filePath: '/tmp/untitled.md',
relativePath: 'untitled.md',
language: 'markdown',
isDirty: true,
mode: 'edit'
}
]}
/>
)
})
export const competingTitles = defineFixture({
width: 620,
render: () => (
<TabWidthFixture
count={3}
titles={[
'A very long conversation title '.repeat(12),
'Go',
'Another long conversation title '.repeat(3)
]}
/>
)
})
@@ -56,7 +56,7 @@ export function TabHoverCard({
className="flex items-center gap-2 text-muted-foreground"
data-tab-hover-card-program
>
<span className="flex h-4 min-w-4 shrink-0 items-center justify-center" aria-hidden>
<span className="flex size-4 shrink-0 items-center justify-center" aria-hidden>
{icon}
</span>
<span className="min-w-0 break-all">{programName}</span>
@@ -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 (
<span
@@ -61,7 +59,7 @@ export function TerminalTabLeadingIcon({
'auto.components.tab.bar.TerminalTabLeadingIcon.7ab2964bea',
'Unread agent completion'
)}
className={iconClassName}
className="mr-1 inline-flex shrink-0 items-center gap-1"
>
<FilledBellIcon className="size-3 text-amber-500 drop-shadow-sm" />
{agent ? <TerminalTabAgentIdentityIcon agent={agent} isActive={isActive} /> : null}
@@ -76,7 +74,7 @@ export function TerminalTabLeadingIcon({
<span
data-testid="tab-agent-activity-indicator"
data-agent-activity-status={activityStatus}
className={iconClassName}
className="mr-1 inline-flex shrink-0 items-center gap-1"
>
{/* Why: the state glyph is self-describing and the tab title already has a tooltip; hover here stays quiet. */}
<AgentStateDot state={dotState} size="md" title={null} progress={progress} />
@@ -88,17 +86,13 @@ export function TerminalTabLeadingIcon({
if (agent) {
return (
<TerminalTabAgentIdentityIcon
agent={agent}
isActive={isActive}
className={cn(iconClassName, 'justify-end')}
/>
<TerminalTabAgentIdentityIcon agent={agent} isActive={isActive} className="mr-1 shrink-0" />
)
}
return (
<span
className={cn(iconClassName, !isActive && 'opacity-70')}
className={`mr-1 inline-flex shrink-0 ${isActive ? '' : 'opacity-70'}`}
data-shell-icon={shell ?? 'generic'}
aria-hidden
>
@@ -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 {
@@ -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'
+32 -158
View File
@@ -42,43 +42,40 @@ type E2EWindow = typeof window & {
__store?: E2EStore
}
async function seedFloatingSimulatorTabs(page: Page, count = 2): Promise<E2EUnifiedTab[]> {
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<E2EUnifiedTab[]> {
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<string[]> =>
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<number> =>
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<number[]> =>
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)
}
})
-166
View File
@@ -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<number> =>
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<number[][]> =>
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<number[][]> =>
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
}) => {