diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index 692210b352..fb1f825106 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -175,6 +175,29 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { expect(tripwire.warnings).toEqual([]) }, 120_000) + it.skipIf(MODE === 'record')('keeps a terminal request marker inside the trajectory table', async () => { + await launch(() => ({ + patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: 'invalid api key', code: 'AUTH' } }], + })) + const { settled } = await sendPrompt() + await settled + await page.getByRole('tab', { name: 'Trajectory' }).click() + const tailRequest = page.locator('tr[data-request-only="true"]').last() + await tailRequest.waitFor({ timeout: 10_000 }) + const requestMarker = tailRequest.getByRole('button', { name: /Request #/ }) + + const markerWithinTable = await requestMarker.evaluate((element) => { + const marker = element.getBoundingClientRect() + const table = element.closest('table')?.getBoundingClientRect() + if (table === undefined) throw new Error('request marker has no table') + return marker.bottom <= table.bottom + }) + + expect(markerWithinTable).toBe(true) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }, 120_000) + it.skipIf(MODE === 'record')('recovers a transient SERVER failure through llm-retry and completes', async () => { const derived = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8'))) expect(derived).toHaveLength(1) diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css index 1b30318f68..3e0e96b838 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css @@ -103,6 +103,11 @@ border-bottom: 0; } +.table tbody tr[data-request-only='true']:last-child td { + /* Retain the lower half of the 16px boundary marker at the table's end. */ + height: 9px; +} + .table tbody tr[data-request-only='true'] .turnRail { top: -15px; bottom: 0; diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTimeline.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTimeline.module.css index a2cac22a71..734b2f3326 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTimeline.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryTimeline.module.css @@ -1,4 +1,7 @@ .root { + position: relative; + z-index: 0; + isolation: isolate; flex: none; border-bottom: 1px solid var(--dsw-alias-border-l2); user-select: none; diff --git a/packages/client/ui-trajectory/src/client/views.module.css b/packages/client/ui-trajectory/src/client/views.module.css index 14b4ce6ac2..3dd80c077a 100644 --- a/packages/client/ui-trajectory/src/client/views.module.css +++ b/packages/client/ui-trajectory/src/client/views.module.css @@ -13,11 +13,17 @@ background: var(--dsw-alias-bg-layer-1); } -/* Under the active conversation column (`[data-conversation-scroll]`) the - * parent owns overflow so the sticky composer stays in the same scrollport. */ -:global([data-conversation-scroll]) .root { - overflow: visible; - height: auto; +/* Trajectory keeps the ledger and details panel inside the remaining + * conversation height. Only the ledger pane scrolls; the composer remains + * the fixed flex sibling below this view. */ +:global([data-conversation-scroll]):has(.root) { + overflow: hidden; +} + +:global([data-conversation-scroll]):has(.root) > :first-child { + flex: 1 1 0; + min-height: 0; + overflow: hidden; } .ledger {