Fix Resource Manager tab sizing and sort PTYs by workspace activity (#33226)

* Keep PTY resource tab mounted and sort workspaces by activity

* Fix Resource Manager tab sizing and verify PTY list scaling
This commit is contained in:
Jinjing
2026-10-24 11:33:12 -07:00
committed by GitHub
parent ddfa8544a4
commit da180863a9
8 changed files with 369 additions and 62 deletions
@@ -6,6 +6,8 @@ import { ResourcePtyList } from './ResourcePtyList'
import type { useResourceSessionInventory } from './use-resource-session-inventory'
import type { useResourcePtyActions } from './use-resource-pty-actions'
const EMPTY_PTY_ROWS: ReturnType<typeof buildResourcePtyRows> = []
type Props = Omit<Parameters<typeof buildResourcePtyRows>[0], 'sessions'> & {
active: boolean
inventory: ReturnType<typeof useResourceSessionInventory>
@@ -37,14 +39,17 @@ export function ResourcePtyPanel({
runtimePaneTitlesByTabId,
sessions
})
: [],
: EMPTY_PTY_ROWS,
[active, sessions, bindings, worktrees, repos, projectGroups, runtimePaneTitlesByTabId]
)
const groups = useMemo(() => groupResourcePtyRows(rows), [rows])
const unbound = rows.filter((row) => row.binding === 'unbound').length
const unbound = useMemo(
() => rows.reduce((count, row) => count + Number(row.binding === 'unbound'), 0),
[rows]
)
return (
<div className="flex h-[420px] flex-col">
<div className="space-y-2 border-b border-border px-3 py-2">
<div className="flex h-full min-h-0 flex-col">
<div className="shrink-0 space-y-2 border-b border-border px-3 py-2">
<div className="text-xs text-muted-foreground">
{inventory.observedAt === null
? translate('resourceManager.pty.thisComputer', 'This computer')
@@ -75,7 +80,7 @@ export function ResourcePtyPanel({
)}
</div>
{inventory.sessionsError && (
<div role="alert" className="px-3 py-2 text-xs text-destructive">
<div role="alert" className="shrink-0 px-3 py-2 text-xs text-destructive">
{translate(
'resourceManager.pty.failed',
'Unverifiable: local terminals could not be refreshed. Last-observed terminals are shown.'
@@ -106,7 +106,7 @@ export function ResourceUsageStatusSegment({
align="end"
sideOffset={8}
{...STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS}
className="w-[26rem] max-w-[calc(100vw-2rem)] p-0"
className="flex h-[min(40rem,var(--radix-popover-content-available-height))] w-[26rem] max-w-[calc(100vw-2rem)] flex-col"
onOpenAutoFocus={(event) => event.preventDefault()}
// Why: activating a tab focuses xterm's DOM node; Radix would read that as focus-outside and close. Outside-click and Escape still close.
onFocusOutside={(event) => event.preventDefault()}
@@ -123,8 +123,12 @@ export function ResourceUsageStatusSegment({
? () => void controller.localPtyInventory.refreshSessions()
: controller.refreshResources
})}
<Tabs value={controller.selectedTab} onValueChange={controller.setSelectedTab}>
<div className="px-3 pt-1 pb-1">
<Tabs
value={controller.selectedTab}
onValueChange={controller.setSelectedTab}
className="min-h-0 flex-1"
>
<div className="shrink-0 px-3 pt-1 pb-1">
<TabsList variant="line" size="sm">
<TabsTrigger value="resources">
<Activity aria-hidden="true" className="size-3" />
@@ -141,7 +145,10 @@ export function ResourceUsageStatusSegment({
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="resources">
<TabsContent
value="resources"
className="flex min-h-0 flex-col data-[state=inactive]:hidden"
>
{controller.actionError && (
<p role="alert" className="px-3 py-2 text-xs text-destructive">
{controller.actionError}
@@ -159,7 +166,6 @@ export function ResourceUsageStatusSegment({
commitToneClass,
orphanCount
})}
{/* Why: fixed 420px height so the popover doesn't jump as worktrees expand/collapse or sessions change; inner tree owns its scroll. */}
{renderResourceUsagePopoverBody({
setPopoverBodyNode,
unifiedRepos,
@@ -188,7 +194,7 @@ export function ResourceUsageStatusSegment({
})}
<WorkspaceSpaceCompactPanel onOpenFullPage={openSpaceResults} />
</TabsContent>
<TabsContent value="ptys">
<TabsContent value="ptys" className="min-h-0 data-[state=inactive]:hidden">
<ResourcePtyPanel
active={controller.ptyViewActive}
inventory={controller.localPtyInventory}
@@ -34,7 +34,7 @@ export function WorkspaceSpaceCompactPanel({
}, [cancelWorkspaceSpaceScan])
return (
<div className="border-t border-border/50 bg-muted/15 px-3 py-2">
<div className="shrink-0 border-t border-border/50 bg-muted/15 px-3 py-2">
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<HardDrive className="size-3.5 shrink-0 text-muted-foreground" />
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { buildResourcePtyRows, groupResourcePtyRows } from './resource-pty-view-model'
import type { PtyListedSession } from '../../../../shared/pty-listed-session'
import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree'
@@ -220,16 +220,100 @@ describe('local PTY view model', () => {
expect(order(rows.filter((row) => row.identity !== '3'))).toEqual(expected)
})
it('joins and groups 1,000 sessions in memory', () => {
const start = performance.now()
it('sorts projects and workspaces by latest activity, including folder workspaces', () => {
const folder = folderWorkspaceToWorktree({ ...makeFolderWorkspace(), lastActivityAt: 300 })
const rows = buildResourcePtyRows({
...base(),
sessions: Array.from({ length: 1_000 }, (_, index) =>
session(`pty-${index}`, { title: `Terminal ${index}` })
)
worktrees: [
{ ...makeWorktree('older', 'Alpha'), lastActivityAt: 100 },
{ ...makeWorktree('recent', 'Zeta'), lastActivityAt: 200 },
folder
],
projectGroups: [makeProjectGroup()],
sessions: [
session('old', { worktreeId: 'older' }),
session('recent', { worktreeId: 'recent' }),
session('folder', { worktreeId: folder.id }),
session('unknown')
]
})
const groups = groupResourcePtyRows(rows)
expect(groups[0]?.workspaces[0]?.rows).toHaveLength(1_000)
console.info(`PTY model 1,000 rows: ${(performance.now() - start).toFixed(2)}ms`)
const order = (input: typeof rows) =>
groupResourcePtyRows(input).map((group) => ({
id: group.id,
workspaces: group.workspaces.map((workspace) => workspace.id)
}))
const expected = [
{ id: folder.repoId, workspaces: [folder.id] },
{ id: 'repo-1', workspaces: ['recent', 'older'] },
{ id: '', workspaces: [''] }
]
expect(order(rows)).toEqual(expected)
expect(order(rows.toReversed())).toEqual(expected)
expect(rows.map((row) => row.lastActivityAt)).toEqual([100, 200, 300, 0])
})
it('falls back to stable name ordering for missing or invalid activity timestamps', () => {
const inputs = base()
const rows = buildResourcePtyRows({
...inputs,
worktrees: [
{ ...makeWorktree('a', 'Alpha'), lastActivityAt: Number.NaN },
{ ...makeWorktree('z', 'Zeta'), lastActivityAt: Number.POSITIVE_INFINITY }
],
sessions: [session('z', { worktreeId: 'z' }), session('a', { worktreeId: 'a' })]
})
expect(rows.every((row) => row.lastActivityAt === 0)).toBe(true)
expect(groupResourcePtyRows(rows)[0]?.workspaces.map((workspace) => workspace.id)).toEqual([
'a',
'z'
])
})
it.each([1_000, 10_000])('joins and sorts %i sessions with bounded comparison work', (count) => {
const workspaceCount = count / 10
const inputs = {
...base(),
repos: Array.from({ length: 20 }, (_, index) => ({
...makeRepo(),
id: `repo-${index}`,
displayName: `Project ${index}`
})),
worktrees: Array.from({ length: workspaceCount }, (_, index) =>
makeWorktree(`workspace-${index}`, `Workspace ${index}`, {
repoId: `repo-${index % 20}`,
lastActivityAt: index % 7
})
),
sessions: Array.from({ length: count }, (_, index) =>
session(`pty-${(index * 7919) % count}`, {
worktreeId: `workspace-${index % workspaceCount}`
})
)
}
const start = performance.now()
const rows = buildResourcePtyRows(inputs)
const elapsedJoin = performance.now() - start
const comparisons = vi.spyOn(String.prototype, 'localeCompare')
const normalizations = vi.spyOn(String.prototype, 'toLocaleLowerCase')
try {
const groups = groupResourcePtyRows(rows)
const elapsedSort = performance.now() - start - elapsedJoin
expect(normalizations).toHaveBeenCalledTimes(workspaceCount + 20)
expect(comparisons.mock.calls.length).toBeLessThan(2 * count * Math.ceil(Math.log2(count)))
expect(
groups.flatMap((group) => group.workspaces.flatMap((workspace) => workspace.rows))
).toHaveLength(count)
for (const group of groups) {
expect(group.workspaces.map((workspace) => workspace.lastActivityAt)).toEqual(
group.workspaces.map((workspace) => workspace.lastActivityAt).toSorted((a, b) => b - a)
)
}
console.info(
`PTY ${count}: join ${elapsedJoin.toFixed(2)}ms, sort ${elapsedSort.toFixed(2)}ms`
)
} finally {
comparisons.mockRestore()
normalizations.mockRestore()
}
})
})
@@ -8,6 +8,7 @@ import {
} from '../../../../shared/execution-host'
import { projectGroupIdFromRepoId } from '../../../../shared/folder-workspace-worktree'
import { makePaneKey, isTerminalLeafId } from '../../../../shared/stable-pane-id'
import { recentStamp } from '../../../../shared/sidebar-list/stamp-order'
import {
buildResourceSessionBindingIndex,
type ResourceSessionBindingInputs
@@ -22,6 +23,7 @@ export type ResourcePtyRow = {
identity: string
label: string
sortName: string
lastActivityAt: number
workspace: string | null
projectId: string
project: string | null
@@ -34,7 +36,15 @@ export type ResourcePtyRow = {
export type ResourcePtyGroup = {
id: string
name: string | null
workspaces: { id: string; name: string | null; rows: ResourcePtyRow[] }[]
sortName: string
lastActivityAt: number
workspaces: {
id: string
name: string | null
sortName: string
lastActivityAt: number
rows: ResourcePtyRow[]
}[]
}
export function buildResourcePtyRows(args: {
@@ -152,6 +162,7 @@ export function buildResourcePtyRows(args: {
identity: ptySessionIdentity(session),
label,
sortName: tab?.customTitle?.trim() || tab?.defaultTitle?.trim() || session.id,
lastActivityAt: recentStamp(workspace ?? {}),
workspace: workspaceName,
workspaceId: workspace?.id ?? null,
projectId: project ? (workspace?.repoId ?? '') : '',
@@ -177,7 +188,13 @@ export function groupResourcePtyRows(rows: readonly ResourcePtyRow[]): ResourceP
let project = projects.get(row.projectId)
if (!project) {
project = {
group: { id: row.projectId, name: row.project, workspaces: [] },
group: {
id: row.projectId,
name: row.project,
sortName: row.project?.trim().toLocaleLowerCase() ?? '\uffff',
lastActivityAt: 0,
workspaces: []
},
workspaces: new Map()
}
projects.set(row.projectId, project)
@@ -185,19 +202,27 @@ export function groupResourcePtyRows(rows: readonly ResourcePtyRow[]): ResourceP
const workspaceId = row.workspaceId ?? ''
let workspace = project.workspaces.get(workspaceId)
if (!workspace) {
workspace = { id: workspaceId, name: row.workspace, rows: [] }
workspace = {
id: workspaceId,
name: row.workspace,
sortName: row.workspace?.trim().toLocaleLowerCase() ?? '\uffff',
lastActivityAt: 0,
rows: []
}
project.workspaces.set(workspaceId, workspace)
project.group.workspaces.push(workspace)
}
workspace.rows.push(row)
workspace.lastActivityAt = Math.max(workspace.lastActivityAt, row.lastActivityAt)
project.group.lastActivityAt = Math.max(project.group.lastActivityAt, row.lastActivityAt)
}
const groups = [...projects.values()].map(({ group }) => group)
for (const group of groups) {
group.workspaces.sort(
(a, b) =>
(a.name?.trim().toLocaleLowerCase() ?? '\uffff').localeCompare(
b.name?.trim().toLocaleLowerCase() ?? '\uffff'
) || a.id.localeCompare(b.id)
b.lastActivityAt - a.lastActivityAt ||
a.sortName.localeCompare(b.sortName) ||
a.id.localeCompare(b.id)
)
for (const workspace of group.workspaces) {
workspace.rows.sort(
@@ -207,8 +232,8 @@ export function groupResourcePtyRows(rows: readonly ResourcePtyRow[]): ResourceP
}
return groups.sort(
(a, b) =>
(a.name?.trim().toLocaleLowerCase() ?? '\uffff').localeCompare(
b.name?.trim().toLocaleLowerCase() ?? '\uffff'
) || a.id.localeCompare(b.id)
b.lastActivityAt - a.lastActivityAt ||
a.sortName.localeCompare(b.sortName) ||
a.id.localeCompare(b.id)
)
}
@@ -0,0 +1,155 @@
// @vitest-environment happy-dom
import type { ComponentProps } from 'react'
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { ResourcePtyPanel } from './ResourcePtyPanel'
import * as model from './resource-pty-view-model'
import { makeRepo, makeWorktree } from '../worktree-jump-palette-test-fixtures'
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string, values?: Record<string, string | number>) =>
Object.entries(values ?? {}).reduce(
(text, [key, value]) => text.replace(`{{${key}}}`, String(value)),
fallback
)
}))
const VIEWPORT_HEIGHT = 360
const ROW_HEIGHT = 40
beforeEach(() => {
vi.stubGlobal(
'ResizeObserver',
class {
observe() {}
unobserve() {}
disconnect() {}
}
)
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (
this: HTMLElement
) {
return this.hasAttribute('data-resource-pty-body') ? VIEWPORT_HEIGHT : ROW_HEIGHT
})
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (this: Element) {
return new DOMRect(
0,
0,
400,
this.hasAttribute('data-resource-pty-body') ? VIEWPORT_HEIGHT : ROW_HEIGHT
)
})
})
afterEach(() => {
cleanup()
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
function panelProps(count: number): ComponentProps<typeof ResourcePtyPanel> {
return {
active: true,
inventory: {
sessionInventory: {
sessions: Array.from({ length: count }, (_, index) => ({
id: `pty-${String(index).padStart(5, '0')}`,
incarnationId: 'A',
fencedTermination: true,
cwd: '/notes',
title: `Terminal ${index}`,
worktreeId: 'wt-1',
agentOwnership: 'absent' as const
})),
count
},
observedAt: 1,
sessionsError: false,
loading: false,
refreshSessions: async () => {},
clearSessionsError: () => {},
invalidate: () => {},
removeSession: () => {},
removeSessions: () => {}
},
actions: {
confirmation: null,
setConfirmation: () => {},
ending: new Set(),
errors: new Map(),
confirm: () => {},
endConfirmed: async () => {}
},
onNavigate: () => {},
bindings: { tabsByWorktree: {}, ptyIdsByTabId: {}, workspaceSessionReady: true },
worktrees: [makeWorktree('wt-1', 'Notes', { lastActivityAt: 100 })],
repos: [makeRepo()],
projectGroups: [],
runtimePaneTitlesByTabId: {}
}
}
it.each([1_000, 10_000])(
'bounds mounted PTY rows for %i sessions and reaches the last row',
(count) => {
const props = panelProps(count)
const view = render(
<TooltipProvider>
<ResourcePtyPanel {...props} />
</TooltipProvider>
)
const scroller = view.container.querySelector<HTMLElement>('[data-resource-pty-body]')
if (!scroller) {
throw new Error('PTY scroller missing')
}
const mountedRows = () => view.container.querySelectorAll('[data-resource-pty-row]')
expect(mountedRows().length).toBeGreaterThan(0)
expect(mountedRows().length).toBeLessThan(50)
expect(mountedRows()[0]?.getAttribute('data-resource-pty-row')).toContain('pty-00000')
expect(view.container.querySelector('[role="listitem"]')?.getAttribute('aria-setsize')).toBe(
String(count + 2)
)
act(() => {
scroller.scrollTop = (count + 2) * ROW_HEIGHT - VIEWPORT_HEIGHT
fireEvent.scroll(scroller)
})
expect(mountedRows().length).toBeGreaterThan(0)
expect(mountedRows().length).toBeLessThan(50)
expect(
mountedRows()[mountedRows().length - 1]?.getAttribute('data-resource-pty-row')
).toContain(`pty-${String(count - 1).padStart(5, '0')}`)
}
)
it('skips joins and sorting on scrolling or action updates, and does no model work when inactive', () => {
const join = vi.spyOn(model, 'buildResourcePtyRows')
const sort = vi.spyOn(model, 'groupResourcePtyRows')
const props = panelProps(1_000)
const panel = (value: typeof props) => (
<TooltipProvider>
<ResourcePtyPanel {...value} />
</TooltipProvider>
)
const view = render(panel(props))
const joined = join.mock.calls.length
const sorted = sort.mock.calls.length
const scroller = view.container.querySelector<HTMLElement>('[data-resource-pty-body]')
if (!scroller) {
throw new Error('PTY scroller missing')
}
act(() => {
scroller.scrollTop = ROW_HEIGHT * 20
fireEvent.scroll(scroller)
})
view.rerender(panel({ ...props, actions: { ...props.actions, errors: new Map() } }))
expect(join).toHaveBeenCalledTimes(joined)
expect(sort).toHaveBeenCalledTimes(sorted)
view.rerender(panel({ ...props, active: false }))
const inactiveSorts = sort.mock.calls.length
view.rerender(panel({ ...props, active: false, worktrees: [...props.worktrees] }))
expect(join).toHaveBeenCalledTimes(joined)
expect(sort).toHaveBeenCalledTimes(inactiveSorts)
expect(view.container.querySelectorAll('[data-resource-pty-row]')).toHaveLength(0)
})
@@ -57,7 +57,11 @@ export function renderResourceUsagePopoverBody({
runtimeHostNotices: readonly string[]
}): React.JSX.Element {
return (
<div ref={setPopoverBodyNode} tabIndex={-1} className="flex h-[420px] flex-col outline-none">
<div
ref={setPopoverBodyNode}
tabIndex={-1}
className="flex min-h-0 flex-1 flex-col outline-none"
>
{(unifiedRepos.length > 0 || resourceSnapshot) && (
<div className="flex items-center justify-between px-3 py-1 bg-muted/30 border-b border-border/50 text-[10px] uppercase tracking-wide shrink-0">
<button
@@ -111,7 +115,7 @@ export function renderResourceUsagePopoverBody({
</div>
)}
<div className="flex-1 overflow-y-auto scrollbar-sleek">
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-sleek">
{runtimeHostNotices.map((notice) => (
<div
key={notice}
@@ -66,22 +66,27 @@ test('a real hidden window performs no Resource Manager diagnostics and vetoes f
await expect(orcaPage.getByText(/Unverifiable: local terminals/)).toHaveCount(0)
})
for (const theme of ['dark', 'light'] as const) {
test(`local PTY review and checked confirmation with 1,000 sessions (${theme})`, async ({
for (const { theme, sessionCount, viewportHeight } of [
{ theme: 'dark', sessionCount: 1_000, viewportHeight: 900 },
{ theme: 'light', sessionCount: 1_000, viewportHeight: 900 },
{ theme: 'dark', sessionCount: 20, viewportHeight: 500 },
{ theme: 'light', sessionCount: 10_000, viewportHeight: 500 }
] as const) {
test(`local PTY review and checked confirmation with ${sessionCount} sessions (${theme}, ${viewportHeight}px)`, async ({
electronApp,
orcaPage
}, testInfo) => {
await waitForSessionReady(orcaPage)
await orcaPage.setViewportSize({ width: 1200, height: 900 })
await electronApp.evaluate(({ ipcMain }) => {
const rows = Array.from({ length: 1_000 }, (_, index) => ({
await orcaPage.setViewportSize({ width: 1200, height: viewportHeight })
await electronApp.evaluate(({ ipcMain }, sessionCount) => {
const rows = Array.from({ length: sessionCount }, (_, index) => ({
id: `local-pty-${String(index).padStart(4, '0')}`,
incarnationId: `incarnation-${index}`,
title: `Terminal ${index}`,
...(index === 999 ? { foregroundProcess: 'node' } : {}),
...(index === sessionCount - 1 ? { foregroundProcess: 'node' } : {}),
cwd: `/workspace/folder-${index}`,
agentOwnership: index === 0 ? 'unknown' : 'absent',
fencedTermination: index !== 998
fencedTermination: index !== sessionCount - 2
}))
const counts: Required<PtyViewCounts> = {
listing: 0,
@@ -113,18 +118,23 @@ for (const theme of ['dark', 'light'] as const) {
counts.kills.push(request)
return { outcome: 'refused', reason: 'attached' }
})
})
}, sessionCount)
await makeResourceManagerFixtureVisible(electronApp, orcaPage)
await orcaPage.evaluate(async (theme) => {
await window.__store!.getState().updateSettingsOrThrow({ theme })
}, theme)
await orcaPage.getByRole('button', { name: /^Resource Manager,/ }).click()
const popover = orcaPage.locator('[data-slot="popover-content"]').filter({
has: orcaPage.getByRole('tab', { name: 'Resources', exact: true })
})
const resourcesHeight = await popover.evaluate((element) => element.clientHeight)
const listStarted = performance.now()
await orcaPage.getByRole('tab', { name: 'PTYs', exact: true }).click()
await expect(
orcaPage.getByText('1000 Orca PTYs on this computer · 1000 without a tab', {
exact: true
})
orcaPage.getByText(
`${sessionCount} Orca PTYs on this computer · ${sessionCount} without a tab`,
{ exact: true }
)
).toBeVisible()
const listMs = performance.now() - listStarted
const before = await electronApp.evaluate(() => {
@@ -137,11 +147,17 @@ for (const theme of ['dark', 'light'] as const) {
await expect(panel.getByRole('searchbox')).toHaveCount(0)
await expect(panel.getByRole('combobox')).toHaveCount(0)
const body = orcaPage.locator('[data-resource-pty-body]')
await expect
.poll(() => popover.evaluate((element) => element.clientHeight))
.toBe(resourcesHeight)
await expect
.poll(() => body.evaluate((element) => element.scrollHeight > element.clientHeight))
.toBe(true)
await body.evaluate((element) => {
element.scrollTop = element.scrollHeight
})
const unsupportedEnd = panel.getByRole('button', {
name: 'End Terminal 998',
name: `End Terminal ${sessionCount - 2}`,
exact: true
})
await expect(unsupportedEnd).toBeDisabled()
@@ -149,17 +165,21 @@ for (const theme of ['dark', 'light'] as const) {
await expect(orcaPage.getByRole('tooltip', { name: /^End is unavailable/ })).toContainText(
'End is unavailable for this terminal. Its host does not support the required safety checks.'
)
const details = panel.getByRole('button', { name: 'Details for Terminal 999', exact: true })
const details = panel.getByRole('button', {
name: `Details for Terminal ${sessionCount - 1}`,
exact: true
})
await details.focus()
await orcaPage.keyboard.press('Enter')
await expect(details).toHaveAttribute('aria-expanded', 'true')
await expect(panel.locator('dl').getByText('local-pty-0999', { exact: true })).toBeVisible()
await expect(panel.locator('dl').getByText('incarnation-999', { exact: true })).toBeVisible()
const lastSessionId = `local-pty-${String(sessionCount - 1).padStart(4, '0')}`
await expect(panel.locator('dl').getByText(lastSessionId, { exact: true })).toBeVisible()
await expect(
panel.locator('dl').getByText(`incarnation-${sessionCount - 1}`, { exact: true })
).toBeVisible()
await expect(panel.locator('dl').getByText('Unavailable', { exact: true })).toHaveCount(2)
await panel.getByRole('button', { name: 'Copy Orca session ID', exact: true }).click()
expect(await electronApp.evaluate(({ clipboard }) => clipboard.readText())).toBe(
'local-pty-0999'
)
expect(await electronApp.evaluate(({ clipboard }) => clipboard.readText())).toBe(lastSessionId)
await orcaPage.screenshot({
path: testInfo.outputPath(`local-pty-details-${theme}.png`),
animations: 'disabled'
@@ -172,7 +192,7 @@ for (const theme of ['dark', 'light'] as const) {
await expect(firstTerminal).toBeVisible()
expect(await orcaPage.getByRole('button', { name: /^End Terminal/ }).count()).toBeLessThan(50)
await firstTerminal.focus()
for (let index = 1; index <= 30; index++) {
for (let index = 1; index <= Math.min(30, sessionCount - 3); index++) {
await orcaPage.keyboard.press('Tab')
await orcaPage.keyboard.press('Tab')
const terminal = orcaPage.getByRole('button', { name: `End Terminal ${index}`, exact: true })
@@ -182,11 +202,17 @@ for (const theme of ['dark', 'light'] as const) {
await body.evaluate((element) => {
element.scrollTop = element.scrollHeight
})
const lastTerminal = body.getByRole('button', { name: 'End Terminal 999', exact: true })
await expect(lastTerminal).toBeVisible()
console.info(JSON.stringify({ theme, rows: 1_000, listMs }))
await expect(body.getByText('Last seen process: node', { exact: false })).toBeVisible()
await expect(body.getByText('/workspace/folder-999', { exact: false })).toBeVisible()
const lastTerminal = body.getByRole('button', {
name: `End Terminal ${sessionCount - 1}`,
exact: true
})
await expect(lastTerminal).toBeInViewport()
console.info(JSON.stringify({ theme, rows: sessionCount, listMs }))
await expect(
body.getByText(`Last seen process: node · /workspace/folder-${sessionCount - 1}`, {
exact: true
})
).toBeVisible()
const after = await electronApp.evaluate(() => {
if (!globalThis.__ptyViewCounts) {
throw new Error('PTY view counters missing')
@@ -196,11 +222,13 @@ for (const theme of ['dark', 'light'] as const) {
expect(after.listing).toBe(before.listing)
expect(after.metrics).toBe(before.metrics)
await lastTerminal.click()
await expect(orcaPage.getByRole('heading', { name: 'End Terminal 999?' })).toBeVisible()
await expect(
orcaPage.getByRole('heading', { name: `End Terminal ${sessionCount - 1}?` })
).toBeVisible()
await expect(
orcaPage
.getByRole('dialog', { name: 'End Terminal 999?' })
.getByText('/workspace/folder-999', { exact: true })
.getByRole('dialog', { name: `End Terminal ${sessionCount - 1}?` })
.getByText(`/workspace/folder-${sessionCount - 1}`, { exact: true })
).toBeVisible()
expect(
(
@@ -230,8 +258,8 @@ for (const theme of ['dark', 'light'] as const) {
).kills
).toEqual([
{
id: 'local-pty-0999',
expectedIncarnationId: 'incarnation-999',
id: lastSessionId,
expectedIncarnationId: `incarnation-${sessionCount - 1}`,
requireNoAgentOwner: true,
requireUnattached: true
}
@@ -246,7 +274,7 @@ for (const theme of ['dark', 'light'] as const) {
})
await testInfo.attach('interaction-measurement', {
body: JSON.stringify({
rows: 1_000,
rows: sessionCount,
listingCalls: before.listing,
metricCalls: before.metrics
}),