From fe5aee7e718f558adfdd0ab532cf055795957e31 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:23:19 +0800 Subject: [PATCH] test(web): cover Sidebar interactions and refresh recorded UI expectations --- .../tests/details-session-lifecycle.e2e.ts | 234 ++++- .../agent-preset-selection/header.expected.md | 2 + .../clickable-links-gallery/ui.expected.md | 3 +- .../conversation-expanded.expected.md | 2 + .../conversation.expected.md | 2 + .../goal-command-presentation/ui.expected.md | 2 + .../markdown-cjk-strong/ui.expected.md | 2 + .../expected/markdown-images/ui.expected.md | 2 + .../markdown-inline-code-links/ui.expected.md | 2 + .../expected/math-rendering/ui.expected.md | 2 + .../reference-composer/order.expected.md | 2 + .../skill-user-invoke/ui-expanded.expected.md | 2 + .../expected/skill-user-invoke/ui.expected.md | 2 + .../stats-paged-history/ui.expected.md | 2 + .../expected/steer-all/mid-steer.expected.md | 2 + .../steer-all/settled-expanded.expected.md | 2 + .../expected/steer-all/settled.expected.md | 2 + apps/web/tests/markdown-wide-table.e2e.ts | 43 +- apps/web/tests/navigation-panes.e2e.ts | 46 +- apps/web/tests/produced-files.e2e.ts | 27 +- apps/web/tests/ptc-round.e2e.ts | 6 +- apps/web/tests/seeded-history.e2e.ts | 60 +- apps/web/tests/sidebar-right.e2e.ts | 847 ++++++++++++++++++ .../mid-stream.expected.md | 2 + snapshots/web/bash-abort-row/ui.expected.md | 2 + .../web/cordis-tool-round/ui.expected.md | 2 + .../sidebar.expected.md | 496 ++++++++++ .../feedback-command/ack-expanded.expected.md | 2 + .../web/feedback-command/ack.expected.md | 2 + .../feedback-release/ack-expanded.expected.md | 2 + .../web/feedback-release/ack.expected.md | 2 + .../web/file-upload-round/ui.expected.md | 2 + .../fresh-round-trip/ui-expanded.expected.md | 2 + snapshots/web/fresh-round-trip/ui.expected.md | 2 + .../ui-expanded.expected.md | 2 + .../goal-multi-turn-actions/ui.expected.md | 2 + .../web/lifecycle-chrome/hero.expected.md | 3 - .../lifecycle-chrome/plan-active.expected.md | 3 - .../reloaded-expanded.expected.md | 2 + .../web/lifecycle-chrome/reloaded.expected.md | 2 + .../cancel-expanded.expected.md | 2 + .../web/live-interactions/cancel.expected.md | 2 + .../live-interactions/error-auth.expected.md | 2 + .../web/live-interactions/loading.expected.md | 2 + .../retry-exhausted.expected.md | 2 + .../retry-expanded.expected.md | 2 + .../web/live-interactions/retry.expected.md | 2 + .../running-draft.expected.md | 2 + snapshots/web/message-actions/ui.expected.md | 2 + snapshots/web/minimal-preset/ui.expected.md | 2 + .../plan-review/approved-expanded.expected.md | 2 + .../web/plan-review/approved.expected.md | 2 + snapshots/web/ptc-round/ui.expected.md | 2 + .../answered-expanded.expected.md | 2 + .../question-composer/answered.expected.md | 2 + .../question-composer/cancelled.expected.md | 2 + .../web/queue-actions/collapsed.expected.md | 2 + .../web/queue-actions/editing.expected.md | 2 + .../web/queue-actions/layout.expected.md | 2 + .../preserved-expanded.expected.md | 2 + .../web/queue-actions/preserved.expected.md | 2 + .../web/queue-actions/sending.expected.md | 2 + snapshots/web/queue-actions/ui.expected.md | 2 + .../web/queued-image/delivered.expected.md | 2 + snapshots/web/queued-image/queued.expected.md | 2 + .../seeded-history/command-row.expected.md | 2 + .../seeded-history/feedback-row.expected.md | 2 + .../file-open-failure.expected.md | 7 - .../seeded-history/ui-expanded.expected.md | 2 + snapshots/web/seeded-history/ui.expected.md | 2 + snapshots/web/skill-tool-row/ui.expected.md | 2 + snapshots/web/steering/mid-steer.expected.md | 2 + .../web/steering/settled-expanded.expected.md | 2 + snapshots/web/steering/settled.expected.md | 2 + .../subagent-conversation/nested.expected.md | 2 + .../ui-expanded.expected.md | 2 + .../web/subagent-conversation/ui.expected.md | 2 + .../offline-composer.expected.md | 2 + .../turn-tail-actions/completed.expected.md | 2 + .../web/turn-tail-actions/focused.expected.md | 2 + .../web/turn-tail-actions/running.expected.md | 2 + .../web/turn-tail-actions/settled.expected.md | 2 + .../usage-expanded.expected.md | 2 + snapshots/web/web-search-round/ui.expected.md | 2 + 84 files changed, 1767 insertions(+), 152 deletions(-) create mode 100644 apps/web/tests/sidebar-right.e2e.ts create mode 100644 snapshots/web/details-session-lifecycle/sidebar.expected.md delete mode 100644 snapshots/web/seeded-history/file-open-failure.expected.md diff --git a/apps/web/tests/details-session-lifecycle.e2e.ts b/apps/web/tests/details-session-lifecycle.e2e.ts index b903c0c102..b0d59bb676 100644 --- a/apps/web/tests/details-session-lifecycle.e2e.ts +++ b/apps/web/tests/details-session-lifecycle.e2e.ts @@ -1,10 +1,8 @@ -// Keyless browser regression for the details column's default visibility and Session ownership. -// The shipped composition starts closed after selection and reload, retains an explicitly opened width through -// unselected states, and closes it only when a different Session takes ownership. -import { readFile } from 'node:fs/promises' +// Recorded-session Sidebar geometry and per-Session view state through the shipped browser composition. +import { mkdir, readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { join } from 'node:path' -import type { Browser, Page } from 'playwright' +import type { Browser, Locator, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { @@ -16,6 +14,8 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/details-session-lifecycle', import.meta.url)) const HANDLES_EXPECTED = join(SNAPSHOT_DIR, 'handles.expected.md') +const SIDEBAR_EXPECTED = join(SNAPSHOT_DIR, 'sidebar.expected.md') +const SHOT_DIR = fileURLToPath(new URL('../../../.artifacts/screenshots/0907-sidebar-rules', import.meta.url)) const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome/session.v2.jsonl', import.meta.url)) const SEED_FIXTURE = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.v2.jsonl', import.meta.url)) const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.' @@ -64,6 +64,63 @@ async function handleSnapshot(page: Page): Promise { ].join('\n').trimEnd() } +/** Rendered frame tracks, rounded only to remove browser subpixel allocation. */ +async function columns(page: Page): Promise { + return await appFrame(page).evaluate(element => + getComputedStyle(element).gridTemplateColumns.split(' ').map(value => Math.round(Number.parseFloat(value)))) +} + +/** Tab order and selection inside each docked pane, independent of generated ids. */ +async function paneSnapshot(page: Page) { + return await page.locator('[data-rightbar-col] [data-dockkit-pane]').evaluateAll(panes => panes.map(pane => ({ + active: pane.hasAttribute('data-dockkit-pane-active'), + tabs: [...pane.querySelectorAll('[data-dockkit-tab]')].map(tab => ({ + title: tab.querySelector('[data-dockkit-tab-title]')?.textContent?.trim(), + selected: tab.getAttribute('aria-selected') === 'true', + })), + }))) +} + +/** Product-visible geometry, pane state, and expanded Files directories at a settled checkpoint. */ +async function sidebarSnapshot(page: Page) { + const geometry = await appFrame(page).evaluate((frame) => { + const panel = frame.querySelector('[data-sidebar-right-panel]') + if (panel === null) throw new Error('Sidebar panel is not mounted') + const expanded = panel.hasAttribute('data-sidebar-right-open') + const rect = panel.getBoundingClientRect() + const style = getComputedStyle(panel) + const handle = frame.querySelector('[data-side="rightbar"]') + return { + viewport: [window.innerWidth, window.innerHeight], + columns: getComputedStyle(frame).gridTemplateColumns.split(' ').map(value => Math.round(Number.parseFloat(value))), + expanded, + mode: panel.getAttribute('data-sidebar-right-panel'), + panelContentWidth: expanded ? Math.round(Number.parseFloat(style.width)) : 0, + panelOuterWidth: expanded ? Math.round(rect.width) : 0, + coversViewport: expanded && rect.x === 0 && rect.y === 0 + && Math.round(rect.width) === window.innerWidth && Math.round(rect.height) === window.innerHeight, + resizeHandleWidth: handle === null ? 0 : Math.round(handle.getBoundingClientRect().width), + expandedDirectories: [...panel.querySelectorAll('[data-files-entry="directory"] > button[aria-expanded="true"]')] + .map(button => button.textContent?.trim()), + } + }) + return { ...geometry, panes: await paneSnapshot(page) } +} + +/** The native pointer gesture used for the left column's width preference. */ +async function dragSidebar(page: Page, target: number): Promise { + const grip = await page.locator('[data-side="sidebar"]').boundingBox() + if (grip === null) throw new Error('Sidebar resize handle is not rendered') + await page.mouse.move(grip.x + grip.width / 2, grip.y + grip.height / 2) + await page.mouse.down() + try { + await page.mouse.move(target, grip.y + grip.height / 2, { steps: 6 }) + } finally { + await page.mouse.up() + } + await expect.poll(() => sidebarTrack(page)).toBe(target) +} + describe.skipIf(MODE === 'record')('web e2e: details panel follows the current Session lifecycle', () => { let scaffold: WebScaffold let browser: Browser @@ -84,12 +141,18 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S }, 120_000) afterAll(async () => { - await browser?.close() - await scaffold?.close() + try { + await browser?.close() + } finally { + await scaffold?.close() + } }) - it('starts and reloads closed, then stays closed across Session ownership changes', async () => { - onTestFailed(() => saveFailureShot(page, 'web-e2e-details-session-lifecycle')) + it('retains each Session sidebar and applies normal, fullscreen, and capacity-close geometry', async () => { + onTestFailed(async () => { + await mkdir(SHOT_DIR, { recursive: true }) + await saveFailureShot(page, `screenshots/0907-sidebar-rules/details-session-lifecycle-${MODE}-${process.pid}`) + }) const settled = scaffold.whenTurnSettled() const input = page.locator('[data-composer-input]').first() await input.fill(PROMPT) @@ -125,7 +188,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false) - const original = page.locator('[role=treeitem]').filter({ hasText: 'Reply with the single word' }).first() + const original = page.locator('[role="treeitem"][aria-selected]').filter({ hasText: 'Reply with the single word' }).first() await original.click() await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) @@ -133,20 +196,149 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S const ungrouped = page.getByText('Ungrouped', { exact: true }) const ungroupedRow = ungrouped.locator('..').locator('..') - const ungroupedSection = ungroupedRow.locator('..') - await expect.poll(async () => { - if (await ungroupedRow.getAttribute('aria-expanded') !== 'true') { - await ungrouped.click() - await page.waitForTimeout(50) - } - return await ungroupedRow.getAttribute('aria-expanded') - }, { timeout: 5_000 }).toBe('true') - const seeded = ungroupedSection.locator('[role="treeitem"]').nth(1) + if (await ungroupedRow.getAttribute('aria-expanded') !== 'true') await ungrouped.click() + await expect.poll(() => ungroupedRow.getAttribute('aria-expanded')).toBe('true') + // A cold Session's row may still show its cwd until its history loads. + const seeded = ungroupedRow.locator('..').locator('[role="treeitem"][aria-selected]') + await expect.poll(() => seeded.count()).toBe(1) await seeded.click() await page.getByText('DONE', { exact: true }).waitFor({ timeout: 15_000 }) await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) + + const viewport = page.viewportSize() + if (viewport === null) throw new Error('expected a fixed viewport') + const column = page.locator('[data-rightbar-col]') + const panel = column.locator('[data-sidebar-right-panel]') + const panes = column.locator('[data-dockkit-pane]') + const normalWidth = Math.round(viewport.width * 0.45) + const normalColumns = [280, viewport.width - 280 - normalWidth, normalWidth] + const checkpoints: string[] = ['# Recorded-session Sidebar states'] + const checkpoint = async (label: string): Promise => { + checkpoints.push(`## ${label}\n\n\`\`\`json\n${JSON.stringify(await sidebarSnapshot(page), null, 2)}\n\`\`\``) + } + const select = async (row: Locator, reply: string): Promise => { + await row.click() + await expect.poll(() => row.getAttribute('aria-selected')).toBe('true') + await page.getByText(reply, { exact: true }).waitFor({ timeout: 15_000 }) + } + const open = async (): Promise => { + await page.locator('[data-sidebar-right-expand]').click() + await expect.poll(() => column.locator('[data-sidebar-right-open]').count()).toBe(1) + await expect.poll(() => columns(page)).toEqual(normalColumns) + } + const close = async (): Promise => { + await column.locator('[data-sidebar-right-toggle]').click() + await expect.poll(() => column.locator('[data-sidebar-right-open]').count()).toBe(0) + await expect.poll(() => detailsTrack(page)).toBe(0) + } + + await select(original, 'LIGHTHOUSE') + await open() + // The content-box panel adds its one rendered border pixel outside the + // CSS width assigned by the grid solver. + await expect.poll(() => sidebarSnapshot(page), { timeout: 5_000 }) + .toMatchObject({ mode: 'push', panelContentWidth: normalWidth, panelOuterWidth: normalWidth + 1, resizeHandleWidth: 8 }) + await column.locator('[data-sidebar-right-guide-entry="files"]').click() + await column.locator('[data-files-state="tree"]').waitFor({ timeout: 15_000 }) + await column.locator('[data-dockkit-add-tab]').click() + const split = column.locator('[data-dockkit-split-button]').first() + await expect.poll(() => split.isDisabled()).toBe(false) + await split.click() + await expect.poll(() => panes.count()).toBe(2) + await panes.first().locator('[data-dockkit-tab]').filter({ hasText: 'Files' }).click() + await expect.poll(() => panes.first().locator('[data-files-state="tree"]').count()).toBe(1) + const retainedA = await paneSnapshot(page) + expect(retainedA.map(pane => pane.tabs.map(tab => tab.title))).toEqual([['Files', 'Start'], ['Start']]) + await checkpoint('A normal: two panes') + + await column.locator('[data-sidebar-right-mode="fullscreen"]').click() + await expect.poll(() => panel.boundingBox()).toEqual({ x: 0, y: 0, ...viewport }) + expect(await columns(page)).toEqual(normalColumns) + expect(await sidebarSnapshot(page)).toMatchObject({ mode: 'fullscreen', resizeHandleWidth: 0, coversViewport: true }) + expect(await paneSnapshot(page)).toEqual(retainedA) + await checkpoint('A manual fullscreen: underlying columns retained') + + // Closing the fullscreen panel exposes Session navigation without changing + // its manual mode; reopening after the round trip must restore that mode. + await close() + await checkpoint('A closed with manual fullscreen retained') + await select(seeded, 'DONE') + await expect.poll(() => detailsTrack(page)).toBe(0) + await open() + expect(await panel.getAttribute('data-sidebar-right-panel')).toBe('push') + await column.locator('[data-sidebar-right-guide-entry="files"]').click() + const workspaceDirectory = column.locator('[data-files-entry="directory"] > button').filter({ hasText: /^workspace$/ }) + await workspaceDirectory.waitFor({ timeout: 15_000 }) + await workspaceDirectory.click() + await expect.poll(() => workspaceDirectory.getAttribute('aria-expanded')).toBe('true') + await expect.poll(() => column.locator('[data-files-row="loading"]').count()).toBe(0) + expect(await column.locator('[data-files-row="failed"]').count()).toBe(0) + const retainedB = await paneSnapshot(page) + expect(retainedB.map(pane => pane.tabs.map(tab => tab.title))).toEqual([['Files']]) + await close() + await checkpoint('B closed: independent pane and expanded workspace directory') + + await select(original, 'LIGHTHOUSE') + await expect.poll(() => detailsTrack(page)).toBe(0) + expect(await panel.getAttribute('data-sidebar-right-panel')).toBe('fullscreen') + expect(await paneSnapshot(page)).toEqual(retainedA) + await open() + await expect.poll(() => panel.boundingBox()).toEqual({ x: 0, y: 0, ...viewport }) + expect(await paneSnapshot(page)).toEqual(retainedA) + await checkpoint('A restored: manual fullscreen, tabs, and panes') + await column.locator('[data-sidebar-right-mode="push"]').click() + await expect.poll(async () => (await sidebarSnapshot(page)).panelContentWidth, { timeout: 5_000 }).toBe(normalWidth) + + await select(seeded, 'DONE') + await expect.poll(() => detailsTrack(page)).toBe(0) + expect(await panel.getAttribute('data-sidebar-right-panel')).toBe('push') + expect(await paneSnapshot(page)).toEqual(retainedB) + await open() + await expect.poll(() => workspaceDirectory.getAttribute('aria-expanded')).toBe('true') + expect(await paneSnapshot(page)).toEqual(retainedB) + await checkpoint('B restored: normal mode and Files directory state') + await close() + await select(original, 'LIGHTHOUSE') + await expect.poll(() => columns(page)).toEqual(normalColumns) + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(1) + expect(await paneSnapshot(page)).toEqual(retainedA) + await checkpoint('A restored: expanded normal panel') + + try { + await page.setViewportSize({ width: 1024, height: viewport.height }) + await expect.poll(() => columns(page)).toEqual([280, 400, 344]) + await dragSidebar(page, 420) + await expect.poll(() => columns(page)).toEqual([420, 604, 0]) + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + expect(await paneSnapshot(page)).toEqual(retainedA) + await checkpoint('A capacity-closed: wide left preference protected') + await page.setViewportSize(viewport) + await expect.poll(() => columns(page)).toEqual([420, viewport.width - 420, 0]) + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + await checkpoint('A widened: remains closed') + + await page.locator('[data-sidebar-right-expand]').click() + await expect.poll(() => columns(page)).toEqual([420, viewport.width - 420 - normalWidth, normalWidth]) + await page.setViewportSize({ width: 767, height: viewport.height }) + await expect.poll(() => panel.boundingBox()).toEqual({ x: 0, y: 0, width: 767, height: viewport.height }) + await expect.poll(() => columns(page)).toEqual([56, 711, 0]) + expect(await sidebarSnapshot(page)).toMatchObject({ mode: 'fullscreen', resizeHandleWidth: 0, coversViewport: true }) + await checkpoint('A automatic fullscreen at 767px') + await column.locator('[data-sidebar-right-mode="push"]').click() + await expect.poll(() => column.locator('[data-sidebar-right-open]').count()).toBe(0) + await page.setViewportSize(viewport) + await expect.poll(() => columns(page)).toEqual([420, viewport.width - 420, 0]) + expect(await paneSnapshot(page)).toEqual(retainedA) + expect(await panel.getAttribute('data-sidebar-right-panel')).toBe('push') + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + await checkpoint('A automatic fullscreen exited: widening does not reopen') + } finally { + await page.setViewportSize(viewport) + } + + await compareOrRefreshGolden(SIDEBAR_EXPECTED, checkpoints.join('\n\n'), MODE) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) - await assertFixtureInventory(SNAPSHOT_DIR, ['handles.expected.md']) - }, 90_000) + await assertFixtureInventory(SNAPSHOT_DIR, ['handles.expected.md', 'sidebar.expected.md']) + }) }) diff --git a/apps/web/tests/expected/agent-preset-selection/header.expected.md b/apps/web/tests/expected/agent-preset-selection/header.expected.md index 9a9f380833..2a59c43dde 100644 --- a/apps/web/tests/expected/agent-preset-selection/header.expected.md +++ b/apps/web/tests/expected/agent-preset-selection/header.expected.md @@ -9,3 +9,5 @@ - button "Session log": - text: Session log - img +- button "Open the sidebar": + - img diff --git a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md index c247f68e74..3b43a6c687 100644 --- a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md +++ b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" @@ -176,7 +178,6 @@ - button "Open site/app.js": app.js - button "Open src/tokens.css": tokens.css - text: + 1 file -- button "Show in folder" - button "Copy": - img - button "Good response": diff --git a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md index e5e246ad19..dc24d0bebf 100644 --- a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md @@ -17,6 +17,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/github-ready-review/conversation.expected.md b/apps/web/tests/expected/github-ready-review/conversation.expected.md index fa6bf3f25c..c1d92a5aa7 100644 --- a/apps/web/tests/expected/github-ready-review/conversation.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation.expected.md @@ -17,6 +17,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/goal-command-presentation/ui.expected.md b/apps/web/tests/expected/goal-command-presentation/ui.expected.md index 432a51de4d..cda91117e4 100644 --- a/apps/web/tests/expected/goal-command-presentation/ui.expected.md +++ b/apps/web/tests/expected/goal-command-presentation/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md index c9709a6386..b43af467c9 100644 --- a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md +++ b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/markdown-images/ui.expected.md b/apps/web/tests/expected/markdown-images/ui.expected.md index f26dfd8214..bd51c1ecd9 100644 --- a/apps/web/tests/expected/markdown-images/ui.expected.md +++ b/apps/web/tests/expected/markdown-images/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md index 41424d0ee1..2581f08dd3 100644 --- a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md +++ b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/math-rendering/ui.expected.md b/apps/web/tests/expected/math-rendering/ui.expected.md index 21023fdc64..19314b2828 100644 --- a/apps/web/tests/expected/math-rendering/ui.expected.md +++ b/apps/web/tests/expected/math-rendering/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/reference-composer/order.expected.md b/apps/web/tests/expected/reference-composer/order.expected.md index 3ff4bf9c21..8187c93074 100644 --- a/apps/web/tests/expected/reference-composer/order.expected.md +++ b/apps/web/tests/expected/reference-composer/order.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md index d55c6b0d35..b83c67251a 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/skill-user-invoke/ui.expected.md b/apps/web/tests/expected/skill-user-invoke/ui.expected.md index 545e0c93b4..8af7e9b7da 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/stats-paged-history/ui.expected.md b/apps/web/tests/expected/stats-paged-history/ui.expected.md index 9354a75fae..462cab160d 100644 --- a/apps/web/tests/expected/stats-paged-history/ui.expected.md +++ b/apps/web/tests/expected/stats-paged-history/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/steer-all/mid-steer.expected.md b/apps/web/tests/expected/steer-all/mid-steer.expected.md index 1456ffa683..5e8c46a9d2 100644 --- a/apps/web/tests/expected/steer-all/mid-steer.expected.md +++ b/apps/web/tests/expected/steer-all/mid-steer.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/steer-all/settled-expanded.expected.md b/apps/web/tests/expected/steer-all/settled-expanded.expected.md index c3399b4739..5ce25967c9 100644 --- a/apps/web/tests/expected/steer-all/settled-expanded.expected.md +++ b/apps/web/tests/expected/steer-all/settled-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/expected/steer-all/settled.expected.md b/apps/web/tests/expected/steer-all/settled.expected.md index cf24996c06..cb98d44fac 100644 --- a/apps/web/tests/expected/steer-all/settled.expected.md +++ b/apps/web/tests/expected/steer-all/settled.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/markdown-wide-table.e2e.ts b/apps/web/tests/markdown-wide-table.e2e.ts index 44a992b6f1..682aafd7b4 100644 --- a/apps/web/tests/markdown-wide-table.e2e.ts +++ b/apps/web/tests/markdown-wide-table.e2e.ts @@ -197,20 +197,13 @@ interface TableStop { } /** - * Close the details pane so the transcript spans the viewport. Open, it pins - * the transcript to exactly the message column and every breakout relation - * would go vacuous. - * @param target - the page whose pane to close. + * Wait for the right column to sit at its rail. Expanded, it would pin the + * transcript to exactly the message column and every breakout relation would + * go vacuous; the frame's collapse marker is the settled signal. + * @param target - the page whose frame to read. */ -async function closeDetailsPane(target: Page): Promise { - await target.getByRole('button', { name: 'Close details', exact: true }).waitFor({ timeout: 10_000 }) - await target.evaluate(() => { - document.querySelector('button[aria-label="Close details"]')?.click() - }) - // Closed details resolve to zero width but never unmount (ui-layout - // columns contract), so the settled signal is the frame's collapse marker, - // not the button's detachment. - await target.waitForSelector('[data-details-collapsed]', { timeout: 5_000 }) +async function awaitRightRail(target: Page): Promise { + await target.waitForSelector('[data-rightbar-collapsed]', { timeout: 5_000 }) } /** @@ -265,21 +258,17 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro await sessionRow.waitFor({ timeout: 10_000 }) await sessionRow.click() await page.getByText(TAIL_MARKER, { exact: true }).waitFor({ timeout: 15_000 }) - // Collapse the sidebar and close the details pane for the whole sweep: - // classic-scrollbar platforms (Linux CI) lose ~15px of layout width, - // which shifts how much of a narrow viewport the panes leave the + // Collapse the sidebar and keep the right column at its rail for the whole + // sweep: classic-scrollbar platforms (Linux CI) lose ~15px of layout + // width, which shifts how much of a narrow viewport the panes leave the // transcript and lands the narrow stop's readings far from the macOS - // ones — and the details pane alone pins the transcript to exactly the - // message column, which would make every breakout relation vacuous. - // With both out of the equation the transcript follows the viewport - // identically on every platform, which is what keeps one committed - // golden true for all lanes. + // ones — and an expanded right column alone pins the transcript to + // exactly the message column, which would make every breakout relation + // vacuous. With both out of the equation the transcript follows the + // viewport identically on every platform, which is what keeps one + // committed golden true for all lanes. await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).click() - // JS click: after the transcript scrolled to its tail, the pane's close - // button can sit under the sticky header where a pointer click is - // intercepted; the pane itself is scaffolding, not the behavior under - // test, so actionability adds nothing here. - await closeDetailsPane(page) + await awaitRightRail(page) }, 180_000) afterAll(async () => { @@ -436,7 +425,7 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro await sessionRow.click() await hidpiPage.getByText(TAIL_MARKER, { exact: true }).waitFor({ timeout: 15_000 }) await hidpiPage.getByRole('button', { name: 'Collapse sidebar', exact: true }).click() - await closeDetailsPane(hidpiPage) + await awaitRightRail(hidpiPage) // The pane collapses ease over the layout transition: compare only a // settled reading (two consecutive equal wide-wrapper widths). let readings: TableReading[] = [] diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts index 0e06e222ee..0d9b32f1df 100644 --- a/apps/web/tests/navigation-panes.e2e.ts +++ b/apps/web/tests/navigation-panes.e2e.ts @@ -12,7 +12,7 @@ import { join } from 'node:path' import type { Browser, Page, Response } from 'playwright' import { chromium } from 'playwright' import { strFromU8, unzipSync } from 'fflate' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed } from 'vitest' import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay' import { SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session' import { @@ -277,13 +277,17 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { const exportButton = page.getByRole('button', { name: 'Session log' }) expect(await exportButton.isDisabled()).toBe(false) const header = exportButton.locator('xpath=ancestor::header[1]') - const [buttonBox, headerBox] = await Promise.all([ - exportButton.boundingBox(), header.boundingBox(), + // The right Sidebar's expand button holds the header's corner; the export + // control sits immediately to its left. + const sidebarButton = page.getByRole('button', { name: 'Open the sidebar' }) + const [buttonBox, sidebarBox, headerBox] = await Promise.all([ + exportButton.boundingBox(), sidebarButton.boundingBox(), header.boundingBox(), ]) - if (buttonBox === null || headerBox === null) { + if (buttonBox === null || sidebarBox === null || headerBox === null) { throw new Error('Session Header export geometry is unavailable') } - expect(headerBox.x + headerBox.width - (buttonBox.x + buttonBox.width)).toBeLessThanOrEqual(32) + expect(headerBox.x + headerBox.width - (sidebarBox.x + sidebarBox.width)).toBeLessThanOrEqual(32) + expect(sidebarBox.x - (buttonBox.x + buttonBox.width)).toBeLessThanOrEqual(32) const responsePromise = page.waitForResponse(response => response.request().method() === 'HEAD' && new URL(response.url()).pathname === '/api/session.export', { timeout: 30_000 }) @@ -382,33 +386,33 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { await expect.poll(() => page.locator('tr[data-timeline-focus]').count(), { timeout: 10_000 }).toBe(0) }, 60_000) - it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column closed', async () => { - onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details')) + it.skipIf(MODE === 'record')('bash rows leave the right column at its rail; a file link opens the Sidebar', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-rightbar')) await ensureSeedOpen(page) const bashRow = page.locator('[data-sample="bash"]').first() await expandOwningTurnProcess(page, bashRow) await bashRow.waitFor({ timeout: 15_000 }) const frame = page.locator('[style*="grid-template-columns"]').first() - expect(await frame.getAttribute('data-details-collapsed')).toBe('true') + expect(await frame.getAttribute('data-rightbar-collapsed')).toBe('true') // The row click is the card's expand toggle (unified tool-row // interaction); it must not drive layout geometry either way. await bashRow.click() - await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') - // The card's own controls are outside the summary row and must not open - // details either — the expanded terminal card is read in place. + await expect.poll(() => frame.getAttribute('data-rightbar-collapsed'), { timeout: 5_000 }).toBe('true') + // The card's own controls are outside the summary row and must not expand + // the right column either — the expanded terminal card is read in place. await page.locator('[data-sample="bash"] ~ div [data-terminal] [class*="_copyButton_"]').first().click() - await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') - // Read summaries are host-open file links; they also must not open details. + await expect.poll(() => frame.getAttribute('data-rightbar-collapsed'), { timeout: 5_000 }).toBe('true') + // Read summaries are file links: one click opens the file as a text-preview + // tab in the right Sidebar, which expands to show it beside the guide tab. const fileLink = page.locator('[data-variant="read"] button').first() await fileLink.waitFor({ timeout: 10_000 }) - const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') - .mockResolvedValue({ opened: true }) - try { - await fileLink.click() - await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') - } finally { - openPath.mockRestore() - } + await fileLink.click() + await expect.poll(() => frame.getAttribute('data-rightbar-collapsed'), { timeout: 5_000 }).toBe(null) + const column = page.locator('[data-rightbar-col]') + await expect.poll(() => column.locator('[data-dockkit-tab-title]').count(), { timeout: 5_000 }).toBe(2) + // Put the column back so later cases start from the default frame. + await column.locator('[data-sidebar-right-toggle]').click() + await expect.poll(() => frame.getAttribute('data-rightbar-collapsed'), { timeout: 5_000 }).toBe('true') }, 60_000) it.skipIf(MODE === 'record')('renders the bash row as a terminal card in the real browser', async () => { diff --git a/apps/web/tests/produced-files.e2e.ts b/apps/web/tests/produced-files.e2e.ts index 8f10654a4a..1097ed3b1e 100644 --- a/apps/web/tests/produced-files.e2e.ts +++ b/apps/web/tests/produced-files.e2e.ts @@ -1,12 +1,12 @@ // Web e2e scenario: the single-line produced-files summary a finished turn // ends with. Cold-seeds ten writes (zero model calls), then verifies the real -// assembled lane adapts from a coarse width budget and keeps a capability-gated folder handoff. -// The folder request is intercepted so one real browser click can exercise -// the full client carrier without launching a native application in CI. +// assembled lane adapts from a coarse width budget and offers no folder +// handoff: chips open in the right Sidebar's text preview, which has no +// directory form, so the row shows nothing rather than a dead button. import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-session-title' @@ -152,24 +152,11 @@ describe('web e2e: a finished turn ends with the files it produced', () => { expect(await chips.nth(1).innerText()).toBe('index.html') expect(await chips.nth(4).innerText()).toBe('app.ts') await expect.poll(() => row.getByText('+ 5 files', { exact: true }).isVisible()).toBe(true) - const showFolder = page.getByRole('button', { name: 'Show in folder', exact: true }) - expect(await showFolder.count()).toBe(1) + // Chips open in the right Sidebar's text preview, and a directory is not + // something that preview can show, so the row offers no folder action. + expect(await page.getByRole('button', { name: /folder/i }).count()).toBe(0) expect(await page.getByText('Produced', { exact: true }).count()).toBe(1) - const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') - .mockResolvedValue({ opened: true }) - try { - const [response] = await Promise.all([ - page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/openWorkspacePath'), - showFolder.click({ clickCount: 1 }), - ]) - expect(response.status()).toBe(200) - expect(openPath).toHaveBeenCalledTimes(1) - expect(openPath.mock.calls[0]![0]).toMatchObject({ path: `${scaffold.workspaceCwd}/.` }) - } finally { - openPath.mockRestore() - } - const tops = await row.locator(':scope > *:visible').evaluateAll(elements => elements.map(element => element.getBoundingClientRect().top)) expect(new Set(tops.map(top => Math.round(top))).size).toBe(1) diff --git a/apps/web/tests/ptc-round.e2e.ts b/apps/web/tests/ptc-round.e2e.ts index 66ff93de30..f81ba01ef6 100644 --- a/apps/web/tests/ptc-round.e2e.ts +++ b/apps/web/tests/ptc-round.e2e.ts @@ -103,7 +103,7 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => { }, 60_000) it.skipIf(MODE === 'record')('expands the nested bash terminal inline before and after reload', async () => { - onTestFailed(() => saveFailureShot(page, 'web-e2e-ptc-details')) + onTestFailed(() => saveFailureShot(page, 'web-e2e-ptc-rightbar')) let liveTerminalAria: string | undefined for (const reloaded of [false, true]) { if (reloaded) { @@ -114,7 +114,7 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => { } const nest = page.locator('[data-subcalls]').first() const frame = page.locator('[style*="grid-template-columns"]').first() - expect(await frame.getAttribute('data-details-collapsed')).toBe('true') + expect(await frame.getAttribute('data-rightbar-collapsed')).toBe('true') await expandOwningTurnProcess(page, nest) const row = nest.locator('[data-sample="bash"]').first() await expect.poll(() => row.getAttribute('data-state')).toBe('ok') @@ -126,7 +126,7 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => { await terminal.getByText('echo CODE_ROUND_OK', { exact: true }).waitFor() await terminal.getByText('CODE_ROUND_OK', { exact: true }).waitFor() await expect.poll(() => terminal.locator('[data-state]').getAttribute('data-state')).toBe('done') - await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') + await expect.poll(() => frame.getAttribute('data-rightbar-collapsed'), { timeout: 5_000 }).toBe('true') const aria = await terminal.ariaSnapshot() if (reloaded) expect(aria).toBe(liveTerminalAria) else liveTerminalAria = aria diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 925bb5322b..f0016792a9 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -14,7 +14,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm' import { deriveEventMessage, SessionId } from '@deepseek-ai/dsh-session' @@ -38,7 +38,6 @@ const UI_EXPANDED_EXPECTED = fileURLToPath( // Command-row goldens over the same conversation after direct host commands. const COMMAND_ROW_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/command-row.expected.md', import.meta.url)) const FEEDBACK_ROW_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/feedback-row.expected.md', import.meta.url)) -const FILE_OPEN_FAILURE_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/file-open-failure.expected.md', import.meta.url)) const MODE = webSnapshotMode() const SEED_ID = 'seeded-history-web-e2e' @@ -407,56 +406,25 @@ describe('web e2e: seeded history renders through cold resume', () => { await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('false') }) - it.skipIf(MODE === 'record')('file-path tool rows rebuilt from the cold log stay details-inert', async () => { + it.skipIf(MODE === 'record')('file-path tool rows rebuilt from the cold log open the right Sidebar', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-toolrow')) - // Interaction over cold-resumed history: read summaries are host-open - // file links (not expand-in-place / not details). Runs after the golden - // capture; still zero model calls. + // Interaction over cold-resumed history: read summaries are file links + // that open a text-preview tab in the right Sidebar (not expand-in-place). + // Runs after the golden capture; still zero model calls. const fileLink = page.locator('[data-variant="read"] button').first() await expandOwningTurnProcess(page, fileLink) await fileLink.waitFor({ timeout: 10_000 }) const frame = page.locator('[style*="grid-template-columns"]').first() - expect(await frame.getAttribute('data-details-collapsed')).toBe('true') - const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') - .mockResolvedValue({ opened: true }) - try { - await fileLink.click() - await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') - } finally { - openPath.mockRestore() - } + expect(await frame.getAttribute('data-rightbar-collapsed')).toBe('true') + await fileLink.click() + await expect.poll(() => frame.getAttribute('data-rightbar-collapsed'), { timeout: 5_000 }).toBe(null) + const column = page.locator('[data-rightbar-col]') + await expect.poll(() => column.locator('[data-dockkit-tab-title]').count(), { timeout: 5_000 }).toBe(2) // Path label survives from the recorded args (a.txt). await expect.poll(() => page.getByText('a.txt', { exact: false }).count(), { timeout: 5_000 }).toBeGreaterThan(0) - }) - - it.skipIf(MODE === 'record')('a Host open refusal keeps the reason and retries the same path', async () => { - onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-file-open-failure')) - const fileLink = page.locator('[data-variant="read"] button').first() - await fileLink.waitFor({ timeout: 10_000 }) - const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') - .mockRejectedValue(new Error('xdg-open is not available')) - try { - await fileLink.click() - const dialog = page.getByRole('dialog', { name: 'Couldn’t open file' }) - await dialog.waitFor({ timeout: 5_000 }) - const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) - await compareOrRefreshGolden(FILE_OPEN_FAILURE_EXPECTED, snapshot, MODE) - await expect.poll(() => dialog.innerText(), { timeout: 5_000 }) - .toContain('path open failed: xdg-open is not available') - await page.getByRole('button', { name: 'Retry' }).click() - await expect.poll(() => openPath.mock.calls.length, { timeout: 5_000 }).toBe(2) - expect(openPath.mock.calls[0]![0]).toEqual(openPath.mock.calls[1]![0]) - await page.getByRole('button', { name: 'Cancel' }).click() - await expect.poll(() => page.getByRole('dialog', { name: 'Couldn’t open file' }).count(), { - timeout: 5_000, - }).toBe(0) - } finally { - // Shared page: a leftover mask blocks later cases even when this one fails. - if (await page.getByRole('dialog', { name: 'Couldn’t open file' }).count() > 0) { - await page.keyboard.press('Escape') - } - openPath.mockRestore() - } + // Put the column back so the later goldens see the default frame. + await column.locator('[data-sidebar-right-toggle]').click() + await expect.poll(() => frame.getAttribute('data-rightbar-collapsed'), { timeout: 5_000 }).toBe('true') }) it.skipIf(MODE === 'record')('expands the cold-resumed compact summary', async () => { @@ -563,7 +531,7 @@ describe('web e2e: seeded history renders through cold resume', () => { expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) await assertFixtureInventory(SNAPSHOT_DIR, [ - 'command-row.expected.md', 'feedback-row.expected.md', 'file-open-failure.expected.md', + 'command-row.expected.md', 'feedback-row.expected.md', 'session.v2.jsonl', 'ui.expected.md', 'ui-expanded.expected.md', ]) }) diff --git a/apps/web/tests/sidebar-right.e2e.ts b/apps/web/tests/sidebar-right.e2e.ts new file mode 100644 index 0000000000..2caea83174 --- /dev/null +++ b/apps/web/tests/sidebar-right.e2e.ts @@ -0,0 +1,847 @@ +// Keyless assembled-browser coverage for the shipped right Sidebar: the official +// roster row, the real plugin graph, and one Chromium. No overlay is applied — +// this scenario proves the surface is in the product's own composition. +// +// The frame owns the right column as a track; the Sidebar anchors its panel to +// the column's edge and slides it in and out. Which of the two presentations +// draws the panel is a recorded, reversible choice, so this file asserts against +// the frame's track as much as against the panel itself. The way back in while +// collapsed is not in the column at all: it is one button in the conversation +// header, and it leaves when the panel opens. +// +// Ordering is the product's own: the hero comes before any session, so the +// empty right edge is asserted first and the session-bound cases follow in a +// nested block that seeds one turn. +// +// Copy is asserted in English because this page advertises English, which is +// itself the point: every string in this column now comes from the dictionary, +// so an English page renders English. The Chinese draft the product ships is +// asserted, and captured for review, on its own page at the end. +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Browser, Locator, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { launchWebScaffold, watchConsole, type WebScaffold } from './scaffold.ts' +import { + connectFreshWorkspace, newEnglishPage, saveFailureShot, ZH_BROWSER_LOCALE, +} from './support.ts' + +/** The produced file the seeded turn writes, and what the preview should show. */ +const SAMPLE_NAME = 'notes.txt' +const SAMPLE_TEXT = 'produced by the seeded turn\nsecond line\n' + +/** Where this batch's accepted product forms are archived. */ +const SHOT_DIR = fileURLToPath(new URL('../../../.artifacts/screenshots/0907-sidebar-rules', import.meta.url)) + +/** Archive one accepted product form; the batch receipt cites these by name. */ +async function shot(page: Page, name: string): Promise { + mkdirSync(SHOT_DIR, { recursive: true }) + await page.screenshot({ path: `${SHOT_DIR}/${name}.png`, fullPage: true }) +} + +/** Centre of a rendered element, in viewport coordinates. */ +async function centre(locator: Locator): Promise<{ x: number; y: number }> { + const box = await locator.boundingBox() + if (box === null) throw new Error('element is not rendered') + return { x: box.x + box.width / 2, y: box.y + box.height / 2 } +} + +/** + * Press a tab chip and release it over a point. + * + * The press stays left of the chip's nested close control. An optional preview + * verifies the browser recognized the drop target before the release. + */ +async function dragTo(page: Page, tab: Locator, to: { x: number; y: number }, preview?: Locator): Promise { + const box = await tab.boundingBox() + if (box === null) throw new Error('tab is not rendered') + const from = { x: box.x + 6, y: box.y + box.height / 2 } + await page.mouse.move(from.x, from.y) + await page.mouse.down() + try { + await page.mouse.move(to.x, to.y, { steps: 8 }) + await preview?.waitFor({ state: 'visible' }) + } finally { + await page.mouse.up() + } +} + +/** Press an element itself and release over a point (no tab-title indirection). */ +async function dragElement(page: Page, handle: Locator, to: { x: number; y: number }): Promise { + const from = await centre(handle) + await page.mouse.move(from.x, from.y) + await page.mouse.down() + await page.mouse.move(to.x, to.y, { steps: 8 }) + await page.mouse.up() +} + +/** A point inside `locator`, offset by fractions of its own box. */ +async function pointIn(locator: Locator, fx: number, fy: number): Promise<{ x: number; y: number }> { + const box = await locator.boundingBox() + if (box === null) throw new Error('element is not rendered') + return { x: box.x + box.width * fx, y: box.y + box.height * fy } +} + +/** The expand button in the conversation header, present only while collapsed. */ +function expandOf(page: Page): Locator { + return page.locator('[data-sidebar-right-expand]') +} + +/** + * Make sure the panel is open. + * + * These cases share one page and run in order, so an earlier one may have left + * the panel collapsed; a case that needs tabs says so rather than inheriting + * whatever the previous one happened to leave. The way in while collapsed is the + * header's expand button, which lives in the conversation, not the column. + */ +async function ensureExpanded(page: Page, column: Locator): Promise { + if (await column.locator('[data-sidebar-right-open]').count() > 0) return + await expandOf(page).click() + await column.locator('[data-sidebar-right-open]').waitFor({ timeout: 10_000 }) +} + +/** Reload the session's transient sidebar state before an independent gesture case. */ +async function resetSidebar(page: Page): Promise { + await page.reload({ waitUntil: 'load' }) + const column = page.locator('[data-rightbar-col]') + await expandOf(page).waitFor({ timeout: 15_000 }) + await ensureExpanded(page, column) + await expect.poll(async () => await tabTitles(column)).toEqual(['Start']) + await width(column) + return column +} + +/** Whether the element at the centre of `locator` is the locator's own element or a descendant. */ +async function hitsItself(locator: Locator): Promise { + const point = await centre(locator) + return await locator.evaluate((node, at) => { + const hit = document.elementFromPoint(at.x, at.y) + return hit !== null && node.contains(hit) + }, point) +} + +/** + * Float a docked tab the way a user does: drag its chip clear of the docked + * surface and release over the conversation. + */ +async function floatByDrag(page: Page, tab: Locator): Promise { + const surface = page.locator('[data-dockkit-surface]').first() + const box = await surface.boundingBox() + if (box === null) throw new Error('surface is not rendered') + await dragTo(page, tab, { x: box.x - 240, y: box.y + box.height / 2 }) +} + +/** + * Drag the frame's rightbar handle until the panel is `target` px wide (the + * frame clamps to its own range). The handle sits on the panel's left edge, so + * widening is a drag to the left. + */ +async function setPanelWidth(page: Page, target: number): Promise { + const panel = page.locator('[data-sidebar-right-panel]') + const handle = page.locator('[data-side="rightbar"]').first() + for (let attempt = 0; attempt < 3; attempt += 1) { + const box = await panel.boundingBox() + if (box === null) throw new Error('panel is not rendered') + const delta = target - box.width + if (Math.abs(delta) < 2) return + const grip = await centre(handle) + await page.mouse.move(grip.x, grip.y) + await page.mouse.down() + await page.mouse.move(grip.x - delta, grip.y, { steps: 8 }) + await page.mouse.up() + await page.waitForTimeout(300) + } +} + +/** Tab titles inside one container, in strip order. */ +async function tabTitles(root: Locator): Promise { + return await root.locator('[data-dockkit-tab-title]').allInnerTexts() +} + +/** + * A rendered width, read once the frame's track transition has settled. + * + * The frame eases its grid tracks, so a single sample taken right after a + * gesture reports a frame of the animation. Column arithmetic is only exact at + * rest, so this samples until three consecutive readings agree. + */ +async function width(locator: Locator): Promise { + let last = Number.NaN + let steady = 0 + for (let attempt = 0; attempt < 80; attempt += 1) { + // Layout width, not the visible box: a zero-width track is still an answer. + const now = Math.round(await locator.evaluate(node => node.getBoundingClientRect().width)) + steady = now === last ? steady + 1 : 0 + if (steady === 2) return now + last = now + await locator.page().waitForTimeout(50) + } + throw new Error(`width never settled (last ${last}px)`) +} + +describe('web e2e: shipped right Sidebar', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold() + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 180_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('shows nothing on the right while no session keys a surface', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-hero')) + const frame = page.locator('[class*="frame"]').first() + const column = page.locator('[data-rightbar-col]') + await column.waitFor({ state: 'attached', timeout: 15_000 }) + + // With no session there is no surface: no panel in the column, no expand + // button in the header, and no track — the conversation reaches the frame's edge. + expect(await frame.getAttribute('data-rightbar-collapsed')).toBe('true') + expect(await column.locator('[data-sidebar-right-panel]').count()).toBe(0) + expect(await expandOf(page).count()).toBe(0) + expect(await width(column)).toBe(0) + await shot(page, '01-hero-no-sidebar') + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + describe('with a settled session', () => { + beforeAll(async () => { + await connectFreshWorkspace(page, scaffold.workspaceCwd) + // A settled session is what keys the surface; seed one turn through the + // real append path so the Chat surface is live before the Sidebar is driven. + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('connected workspace did not create an Agent') + // The wire parameter is `agentId`; the client passes a session id. If the + // scaffold's Agent and Session carry different ids, that mismatch is the + // silent lookup failure. + expect(String(agent.id)).toBe(String(agent.session.id)) + agent.session.append('turn/start', { turn: 1 }) + agent.session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'Show the right sidebar.' }], + source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + agent.session.append('step/start', { turn: 1, step: 1 }) + // A successful mutation is what makes the turn tail offer a produced-file + // chip — the product's own way into the Sidebar. The file is written for + // real because the preview reads it through the workspace endpoint. + // + // It goes in the SESSION's cwd, not the scaffold's: the endpoint resolves + // relative paths against `sandboxPolicy.resolve({session}).workspaceRoot`, + // which is the session header's cwd. Writing anywhere else makes the read + // fail with workspace-file/not-found, which is the endpoint being right. + writeFileSync(join(agent.session.header.cwd ?? scaffold.workspaceCwd, SAMPLE_NAME), SAMPLE_TEXT, 'utf8') + agent.session.append('tool/call', { + turn: 1, + step: 1, + callId: 'call-write-1', + name: 'write', + arguments: JSON.stringify({ file_path: SAMPLE_NAME, content: SAMPLE_TEXT }), + } as never) + agent.session.append('tool/result', { + turn: 1, + step: 1, + message: { + id: 'result-call-write-1', + role: 'user', + source: { kind: 'tool', callId: 'call-write-1' }, + content: [{ type: 'tool-result', toolCallId: 'call-write-1', content: [{ type: 'text', text: 'ok' }] }], + }, + } as never, { surfaceOp: 'append' }) + agent.session.append('assistant/message', { + stream: [], + turn: 1, + step: 1, + message: createMessage({ + role: 'assistant', + content: [{ type: 'text', text: 'Ready.' }], + source: { kind: 'model', provider: 'fixture', model: 'fixture' }, + }), + }, { surfaceOp: 'append' }) + agent.session.append('step/end', { turn: 1, step: 1 }) + agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + await scaffold.ctx.sessions.flush(agent.session) + await page.getByText('Ready.').waitFor({ timeout: 10_000 }) + }, 120_000) + + it('CONTROL: the old fileReferences namespace answers over the same wire', async () => { + const composer = page.locator('textarea, [contenteditable="true"]').first() + await composer.click() + await composer.pressSequentially('@') + // Any candidate list means the agent-scoped lookup and the gateway route + // both work in this scaffold; nothing rendered means the wire is the fault. + const answered = await page.locator('[data-input-trigger], [role="listbox"], [data-reference-list]') + .first().waitFor({ timeout: 10_000 }).then(() => true, () => false) + await page.keyboard.press('Escape') + // Leave the composer as it was found: these cases share one page, and a + // stray '@' rides into every later assertion and screenshot. `fill('')` + // does NOT clear this editor — it reported success while the character + // stayed — so the reset is a real keystroke, and it is asserted rather + // than assumed. + await composer.click() + await page.keyboard.press('ControlOrMeta+a') + await page.keyboard.press('Backspace') + await expect.poll(async () => (await composer.innerText()).trim()).toBe('') + expect(answered).toBe(true) + }) + + it('starts collapsed behind a header button and squeezes the conversation when opened', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right')) + const frame = page.locator('[class*="frame"]').first() + const column = page.locator('[data-rightbar-col]') + const conversation = page.locator('[class*="centerCol"]').first() + const expand = expandOf(page) + + // Collapsed default: no track, the panel sits off the frame's edge, and + // the only way in is the header button — on the same row as the other + // header utilities, at its far right. + await expand.waitFor({ timeout: 15_000 }) + expect(await frame.getAttribute('data-rightbar-collapsed')).toBe('true') + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + const utilities = page.locator('[class*="headerUtilities"]') + const expandBox = await expand.boundingBox() + const rowBox = await utilities.boundingBox() + if (expandBox === null || rowBox === null) throw new Error('header utilities are not rendered') + expect(Math.round(expandBox.y + expandBox.height / 2)).toBe(Math.round(rowBox.y + rowBox.height / 2)) + // Its own corner seat, past the utilities' right edge — not a utility. + expect(expandBox.x).toBeGreaterThan(rowBox.x + rowBox.width) + const conversationBoxBefore = await conversation.boundingBox() + if (conversationBoxBefore === null) throw new Error('conversation is not rendered') + // How far the utilities' right edge sits from the conversation's own. + const gapBefore = (conversationBoxBefore.x + conversationBoxBefore.width) - (rowBox.x + rowBox.width) + const centerBefore = await width(conversation) + await shot(page, '02a-collapsed-header-button') + + // Opening squeezes by default: the column takes a track of the panel's + // width, the conversation gives up exactly that much room, and the header + // button leaves with the panel's arrival. + await expand.click() + await expect.poll(async () => await frame.getAttribute('data-rightbar-collapsed')).toBe(null) + await expect.poll(async () => await column.locator('[data-sidebar-right-open]').count()).toBe(1) + const panelWidth = await width(column) + expect(panelWidth).toBeGreaterThan(0) + expect(await width(conversation)).toBe(centerBefore - panelWidth) + await expect.poll(async () => await expand.count()).toBe(0) + // The corner keeps its footprint, so the utilities' right edge stays where + // it was relative to the conversation's own right edge. + expect(await page.locator('[data-sidebar-right-expand-placeholder]').count()).toBe(1) + const utilitiesAfter = await utilities.boundingBox() + const conversationAfter = await conversation.boundingBox() + if (utilitiesAfter === null || conversationAfter === null) throw new Error('header is not rendered') + const gapAfter = (conversationAfter.x + conversationAfter.width) - (utilitiesAfter.x + utilitiesAfter.width) + expect(Math.round(gapAfter)).toBe(Math.round(gapBefore)) + + // The panel is in the column, not over it, and carries the seeded tab — + // whose body arrives through the guide type's keyed registration, not from + // any dispatch inside the seat. Its two controls sit at the end of the + // top-right pane's strip: the panel has no header row of its own. + expect(await column.locator('[data-sidebar-right-panel="push"]').count()).toBe(1) + const chrome = column.locator('[data-dockkit-strip-chrome]') + expect(await chrome.count()).toBe(1) + expect(await chrome.locator('[data-sidebar-right-mode]').count()).toBe(1) + expect(await chrome.locator('[data-sidebar-right-toggle]').count()).toBe(1) + + // One centre line across the strip: chip text, split, and the two panel + // controls all sit at the same height. The add control joins the check + // below, once the strip draws it. + const centreY = async (selector: string): Promise => { + const box = await column.locator(selector).first().boundingBox() + if (box === null) throw new Error(`${selector} is not rendered`) + return Math.round(box.y + box.height / 2) + } + const textLine = await centreY('[data-dockkit-tab-title]') + for (const selector of ['[data-dockkit-split-button]', '[data-sidebar-right-mode]', '[data-sidebar-right-toggle]']) { + expect(await centreY(selector), selector).toBe(textLine) + } + + // The guide is unique per pane, so while this pane holds one its strip + // offers no add control. Closing it brings the control back, and the + // control opens the guide again in that pane. + const addTab = column.locator('[data-dockkit-add-tab]') + expect(await addTab.count()).toBe(0) + await page.getByRole('button', { name: `Open ${SAMPLE_NAME}` }).click() + await expect.poll(async () => await tabTitles(column)).toEqual(['Start', SAMPLE_NAME]) + await column.locator('[data-dockkit-tab-close]').first().click() + await expect.poll(async () => await tabTitles(column)).toEqual([SAMPLE_NAME]) + await expect.poll(async () => await addTab.count()).toBe(1) + expect(await centreY('[data-dockkit-add-tab]')).toBe(textLine) + await addTab.click() + await expect.poll(async () => await tabTitles(column)).toEqual([SAMPLE_NAME, 'Start']) + await expect.poll(async () => await column.locator('[data-sidebar-right-guide]').count()).toBe(1) + await expect.poll(async () => await addTab.count()).toBe(0) + // Back to the seeded shape the cases below start from. + await column.locator('[data-dockkit-tab-close]').first().click() + await expect.poll(async () => await tabTitles(column)).toEqual(['Start']) + await shot(page, '02-squeezed-panel') + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + it('covers the viewport in fullscreen without changing the underlying columns', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-mode')) + const frame = page.locator('[class*="frame"]').first() + const column = page.locator('[data-rightbar-col]') + const panel = column.locator('[data-sidebar-right-panel]') + const conversation = page.locator('[class*="centerCol"]').first() + const squeezed = await width(conversation) + const before = await panel.boundingBox() + const trackWidth = await width(column) + + await column.locator('[data-sidebar-right-mode="fullscreen"]').click() + await expect.poll(async () => await panel.getAttribute('data-sidebar-right-panel')).toBe('fullscreen') + expect(await frame.getAttribute('data-rightbar-collapsed')).toBe(null) + expect(await width(conversation)).toBe(squeezed) + expect(await width(column)).toBe(trackWidth) + const viewport = page.viewportSize() + if (viewport === null) throw new Error('expected a fixed viewport') + await expect.poll(async () => await panel.boundingBox()).toEqual({ x: 0, y: 0, ...viewport }) + expect(await expandOf(page).count()).toBe(0) + expect(await frame.locator('[data-side="rightbar"]').count()).toBe(0) + await shot(page, '03-fullscreen-panel') + + await column.locator('[data-sidebar-right-mode="push"]').click() + await expect.poll(async () => await panel.getAttribute('data-sidebar-right-panel')).toBe('push') + expect(await width(conversation)).toBe(squeezed) + expect(await panel.boundingBox()).toEqual(before) + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + it('keeps a capacity-closed panel closed after widening and uses fullscreen on a narrow viewport', async () => { + const viewport = page.viewportSize() + if (viewport === null) throw new Error('expected a fixed viewport') + const frame = page.locator('[class*="frame"]').first() + const column = page.locator('[data-rightbar-col]') + const sidebar = page.locator('[class*="sidebarCol"]').first() + const panel = column.locator('[data-sidebar-right-panel]') + try { + await page.setViewportSize({ width: 1024, height: viewport.height }) + const leftGrip = frame.locator('[data-side="sidebar"]') + const grip = await centre(leftGrip) + await dragElement(page, leftGrip, { x: 420, y: grip.y }) + await expect.poll(async () => await width(sidebar)).toBe(420) + await expect.poll(async () => await column.locator('[data-sidebar-right-open]').count()).toBe(0) + await page.setViewportSize(viewport) + await expect.poll(async () => await width(sidebar)).toBe(420) + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + await expandOf(page).click() + await expect.poll(async () => await column.locator('[data-sidebar-right-open]').count()).toBe(1) + + await page.setViewportSize({ width: 767, height: viewport.height }) + await expect.poll(async () => await panel.getAttribute('data-sidebar-right-panel')).toBe('fullscreen') + await expect.poll(async () => await width(panel)).toBe(767) + expect(await frame.locator('[data-side="rightbar"]').count()).toBe(0) + await column.locator('[data-sidebar-right-mode="push"]').click() + await expect.poll(async () => await column.locator('[data-sidebar-right-open]').count()).toBe(0) + await page.setViewportSize(viewport) + await expect.poll(async () => await width(sidebar)).toBe(420) + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + } finally { + await page.setViewportSize(viewport) + const grip = await centre(frame.locator('[data-side="sidebar"]')) + await dragElement(page, frame.locator('[data-side="sidebar"]'), { x: 280, y: grip.y }) + await expect.poll(async () => await width(sidebar)).toBe(280) + await ensureExpanded(page, column) + } + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + it('CONTROL: the host endpoint answers when called directly, bypassing the wire', async () => { + const files = (scaffold.ctx as unknown as { + get(name: string): { + read(agent: unknown, path: string, range: object, signal: AbortSignal): Promise<{ text: string; eof: boolean }> + } | undefined + }).get('workspaceFiles') + if (files === undefined) throw new Error('host endpoint is not provided') + const agent = scaffold.ctx.agents.list()[0] + if (agent === undefined) throw new Error('no Agent to read for') + + // Raced against a timer so a hang reports a verdict instead of stalling + // the suite: this case exists to tell host logic apart from the wire. + // A page is the file's lines joined by `\n`, without the final terminator. + const verdict = await Promise.race([ + files.read(agent, SAMPLE_NAME, {}, new AbortController().signal) + .then(value => ({ kind: 'settled' as const, text: value.text, eof: value.eof })) + .catch((error: unknown) => ({ kind: 'threw' as const, text: String(error), eof: false })), + new Promise<{ kind: 'hung'; text: string; eof: boolean }>((resolve) => { + setTimeout(() => { resolve({ kind: 'hung', text: 'no settlement in 10s', eof: false }) }, 10_000) + }), + ]) + expect(verdict).toEqual({ kind: 'settled', text: SAMPLE_TEXT.replace(/\n$/u, ''), eof: true }) + }, 30_000) + + it('opens content once, splits, and floats it outside the column', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-content')) + const column = page.locator('[data-rightbar-col]') + const panes = column.locator('[data-dockkit-pane]') + const floats = page.locator('[data-sidebar-right-float-host] [data-dockkit-float]') + + // Observation before action: does the read ever leave the browser? The + // assertion states the healthy answer so a failure prints the real one. + // + // CAVEAT: `sent` is trustworthy — a request frame carries the endpoint + // name. `received` is NOT: a response frame carries only its rpc id, so a + // zero here means "my filter saw nothing", not "the host never answered". + // Correlate by rpc id before drawing any conclusion from it. + const wire = { sent: 0, received: 0 } + const watch = (payload: string): void => { + if (!payload.includes('workspaceFiles')) return + wire.sent += 1 + } + page.on('websocket', (ws) => { + ws.on('framesent', (frame) => { watch(String(frame.payload)) }) + ws.on('framereceived', (frame) => { + if (String(frame.payload).includes('workspaceFiles')) wire.received += 1 + }) + }) + page.on('request', (request) => { + if (request.url().includes('workspaceFiles')) wire.sent += 1 + }) + + // The product's own entry point: the turn tail's produced-file chip. It + // reaches the Sidebar through openFile → ctx.sidebarRight.openResource, and the + // text type claims the address. + const chip = page.getByRole('button', { name: `Open ${SAMPLE_NAME}` }) + await chip.click() + await expect.poll(async () => await tabTitles(column)).toEqual(['Start', SAMPLE_NAME]) + + // Opening the same content again focuses rather than duplicating. + await panes.first().locator('[data-dockkit-tab]').first().click() + await chip.click() + await expect.poll(async () => await tabTitles(column)).toEqual(['Start', SAMPLE_NAME]) + + // The body arrives through the text type's keyed registration, and its + // content came over the wire from the real file. + // A real Remote round-trip settles well after the default poll window. + await column.locator('[data-textpreview-state="text"]') + .waitFor({ timeout: 15_000 }) + .catch(() => { throw new Error(`preview never settled; wire=${JSON.stringify(wire)}`) }) + expect(await column.locator('pre').first().innerText()).toContain('produced by the seeded turn') + // The whole batch-E chain in one frame: a produced-file chip in the + // conversation, the tab it opened, and the file's real content read over + // the workspace endpoint. + await shot(page, '06-produced-chip-to-preview') + + // The directory scenario's V1 behaviour, asserted in the shipped product: + // there is no folder affordance at all. `openFile('.')` would name a + // directory, which a text preview correctly refuses, and the native opener + // it used to reach is gone — so the row offers nothing rather than a + // button that always fails. + expect(await page.getByRole('button', { name: /folder/i }).count()).toBe(0) + + // Split, then dock-drag: the kit's gestures drive the store's actions. + await panes.first().locator('[data-dockkit-split-button]').click() + await expect.poll(async () => await panes.count()).toBe(2) + await dragTo( + page, + column.locator('[data-dockkit-tab]').filter({ hasText: SAMPLE_NAME }).first(), + await pointIn(panes.nth(1), 0.5, 0.94), + ) + await expect.poll(async () => await panes.count()).toBe(2) + + await panes.nth(1).locator('[data-dockkit-tab]').filter({ hasText: 'Start' }) + .locator('[data-dockkit-tab-close]').click() + await expect.poll(async () => await tabTitles(panes.nth(1))).toEqual([SAMPLE_NAME]) + + // The guide is unique per pane: panes seeded with one offer no + // add control; the pane holding only the file is the one that does. + const filePane = panes.filter({ has: page.locator('[data-dockkit-tab-title]', { hasText: SAMPLE_NAME }) }) + await expect.poll(async () => await filePane.locator('[data-dockkit-add-tab]').count()).toBe(1) + expect(await column.locator('[data-dockkit-add-tab]').count()).toBe(1) + + // Floating leaves the column entirely, and survives collapsing it. The + // pane the tab was alone in goes with it: an emptied pane never stays. + const tab = column.locator('[data-dockkit-tab]').filter({ hasText: SAMPLE_NAME }).first() + await floatByDrag(page, tab) + await expect.poll(async () => await floats.count()).toBe(1) + expect(await column.locator('[data-dockkit-float]').count()).toBe(0) + await expect.poll(async () => await panes.count()).toBe(1) + await shot(page, '04-split-and-float') + + await column.locator('[data-sidebar-right-toggle]').click() + await expect.poll(async () => await column.locator('[data-sidebar-right-open]').count()).toBe(0) + expect(await floats.count()).toBe(1) + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + // §9.2 (an explicit second copy of the same content) has no control on the + // panel by product decision, and copy has no service method yet: + // `duplicateTab` is a store/kit intent only, which service.client.spec.ts covers. + + it('keeps each session\'s surface to itself, and restores it on return', async () => { + const fx = await newEnglishPage(browser) + const fxTripwire = watchConsole(fx) + onTestFailed(() => saveFailureShot(fx, 'web-e2e-sidebar-right-sessions')) + try { + await fx.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + const settled = fx.getByRole('treeitem', { name: /Show the right sidebar\./u }).first() + await settled.click() + await expect.poll(async () => await settled.getAttribute('aria-selected')).toBe('true') + const frame = fx.locator('[class*="frame"]').first() + const column = fx.locator('[data-rightbar-col]') + await ensureExpanded(fx, column) + await width(column) + await fx.getByRole('button', { name: `Open ${SAMPLE_NAME}` }).click() + await column.locator('[data-textpreview-state="text"]').waitFor({ timeout: 15_000 }) + const wrap = column.locator('[data-textpreview-tool="wrap"]') + expect(await wrap.getAttribute('aria-pressed')).toBe('true') + await wrap.click() + await expect.poll(async () => await wrap.getAttribute('aria-pressed')).toBe('false') + await column.locator('[data-dockkit-split-button]').first().click() + const panes = column.locator('[data-dockkit-pane]') + await expect.poll(async () => await panes.count()).toBe(2) + const records = async (): Promise<{ panes: string[]; tabs: string[]; titles: string[] }> => ({ + panes: await panes.evaluateAll(nodes => nodes.map(node => node.getAttribute('data-dockkit-pane')!)), + tabs: await column.locator('[data-dockkit-tab]').evaluateAll(nodes => nodes.map(node => node.getAttribute('data-dockkit-tab')!)), + titles: await tabTitles(column), + }) + const before = await records() + + // The real New Session action selects a distinct blank Session; its + // collapsed surface must not inherit the settled Session's tabs. + await fx.getByRole('button', { name: 'New session', exact: true }).last().click() + await expect.poll(async () => await settled.getAttribute('aria-selected')).toBe('false') + await expect.poll(async () => await frame.getAttribute('data-rightbar-collapsed')).toBe('true') + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + expect(await column.locator('[data-textpreview-state="text"]').count()).toBe(0) + + await settled.click() + await expect.poll(async () => await settled.getAttribute('aria-selected')).toBe('true') + await expect.poll(records, { timeout: 15_000 }).toEqual(before) + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(1) + expect(await wrap.getAttribute('aria-pressed')).toBe('false') + expect(await column.locator('pre').first().innerText()).toContain('produced by the seeded turn') + expect(fxTripwire.pageErrors).toEqual([]) + expect(fxTripwire.warnings).toEqual([]) + } finally { + await fx.close() + } + }, 120_000) + + it('§9.3/§9.4 runs the whole pointer chain in a real browser', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-gestures')) + const column = await resetSidebar(page) + const panes = column.locator('[data-dockkit-pane]') + const floats = page.locator('[data-sidebar-right-float-host] [data-dockkit-float]') + + // Chromium cancels pointer capture if a render replaces the pressed + // element; jsdom cannot establish that the whole gesture survives. + + // 1. Reorder inside one strip: drop the last tab left of its neighbours. + // The first pane needs two tabs for this — and for the move below to + // leave it standing, since a pane emptied by a move is dropped. + const first = panes.first() + const strip = first.locator('[data-dockkit-strip]') + await page.getByRole('button', { name: `Open ${SAMPLE_NAME}` }).click() + await expect.poll(async () => await tabTitles(first)).toEqual(['Start', SAMPLE_NAME]) + const order = await tabTitles(first) + // The insertion index is measured against chip midpoints, not strip width. + await dragTo(page, first.locator('[data-dockkit-tab]').last(), + await pointIn(first.locator('[data-dockkit-tab]').first(), 0.25, 0.5), + strip.locator('[data-dockkit-caret="0"]')) + await expect.poll(async () => await tabTitles(first)).toEqual([...order].reverse()) + + // 2. Cross-pane move into a second pane: the tab leaves one pane's strip + // for another's. + if (await panes.count() < 2) { + await first.locator('[data-dockkit-split-button]').click() + await expect.poll(async () => await panes.count()).toBe(2) + } + const moving = first.locator('[data-dockkit-tab]').first() + const title = await moving.locator('[data-dockkit-tab-title]').innerText() + await dragTo(page, moving, await pointIn(panes.nth(1), 0.5, 0.5)) + await expect.poll(async () => await tabTitles(panes.nth(1))).toContain(title) + + const splitOf = (pane: Locator): Locator => pane.locator('[data-dockkit-split-button]') + await expect.poll(async () => await splitOf(panes.first()).isDisabled()).toBe(true) + expect(await splitOf(panes.first()).getAttribute('data-dockkit-split-blocked')).toBe('budget') + expect(await panes.count()).toBe(2) + await setPanelWidth(page, 560) + await expect.poll(async () => await splitOf(panes.last()).isDisabled()).toBe(true) + expect(await splitOf(panes.last()).getAttribute('data-dockkit-split-blocked')).toBe('budget') + + const outer = column.locator('[data-dockkit-divider]').first() + const before = await width(panes.last()) + const grip = await centre(outer) + await dragElement(page, outer, { x: grip.x - 100, y: grip.y }) + await expect.poll(async () => await width(panes.last())).toBeGreaterThan(before) + await dragElement(page, outer, { x: 0, y: grip.y }) + const ratio = async (): Promise => { + const left = await width(panes.first()) + const right = await width(panes.last()) + return left / (left + right) + } + await expect.poll(ratio).toBeCloseTo(0.2, 2) + const surfaceBox = await column.locator('[data-dockkit-surface]').boundingBox() + if (surfaceBox === null) throw new Error('surface is not rendered') + await dragElement(page, outer, { x: surfaceBox.x + surfaceBox.width / 2, y: grip.y }) + await expect.poll(ratio).toBeCloseTo(0.5, 2) + expect(await panes.count()).toBe(2) + expect(await splitOf(panes.first()).isDisabled()).toBe(true) + expect(await splitOf(panes.first()).getAttribute('data-dockkit-split-blocked')).toBe('budget') + expect(await splitOf(panes.first()).getAttribute('title')).toBe('Two panes is the limit') + + // 5. Two floats coexist, and one of them moves. Both leave the widest + // pane; a pane emptied by the first float is merged away, so the + // second one comes from whichever pane is widest by then. + const floatOne = panes.last().locator('[data-dockkit-tab]').first() + await floatByDrag(page, floatOne) + await expect.poll(async () => await floats.count()).toBe(1) + const box = await floats.first().boundingBox() + if (box === null) throw new Error('float is not rendered') + await dragElement(page, floats.first().locator('[data-dockkit-float-grip]'), { x: box.x + 140, y: box.y + 90 }) + await expect.poll(async () => (await floats.first().boundingBox())?.x ?? box.x).not.toBe(box.x) + + const second = panes.last().locator('[data-dockkit-tab]').first() + await floatByDrag(page, second) + await expect.poll(async () => await floats.count()).toBe(2) + + // 6. Dock one back: the docked tree takes it, the other float stays. Dock + // the TOPMOST float — floats render bottom-to-top, so the newest one + // covers the older one's controls and would intercept the click. + await floats.last().locator('[data-dockkit-float-dock]').click() + await expect.poll(async () => await floats.count()).toBe(1) + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }, 90_000) + + it('drops a pane whose last tab closes, and reseeds the guide when none is left', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-settle')) + const column = await resetSidebar(page) + const panes = column.locator('[data-dockkit-pane]') + await panes.first().locator('[data-dockkit-split-button]').click() + await expect.poll(async () => await panes.count()).toBe(2) + const closeAllIn = async (pane: Locator): Promise => { + const tabs = await pane.locator('[data-dockkit-tab]').count() + for (let i = 0; i < tabs; i += 1) { + await pane.locator('[data-dockkit-tab-close]').first().click() + } + } + + // Closing a pane's last tab drops the pane: there is no separate + // "close pane" gesture, and none is needed. + let count = await panes.count() + expect(count).toBeGreaterThan(1) + while (count > 1) { + await closeAllIn(panes.nth(count - 1)) + await expect.poll(async () => await panes.count()).toBe(count - 1) + count -= 1 + } + + // The last pane cannot be dropped, so closing everything in it reseeds + // the guide: the surface always has one tab to look at. + await closeAllIn(panes.first()) + await expect.poll(async () => await tabTitles(column)).toEqual(['Start']) + expect(await column.locator('[data-sidebar-right-guide]').count()).toBe(1) + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }, 90_000) + + it('§9.7 returns to the default surface after a reload', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-reload')) + await page.reload({ waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + const frame = page.locator('[class*="frame"]').first() + const column = page.locator('[data-rightbar-col]') + await column.waitFor({ state: 'attached', timeout: 15_000 }) + // The surface is view state, not durable session data: a reload zeroes it + // back to the collapsed default. Expected behaviour, not a defect. + await expect.poll(async () => await frame.getAttribute('data-rightbar-collapsed')).toBe('true') + await expect.poll(async () => await expandOf(page).count()).toBe(1) + expect(await column.locator('[data-sidebar-right-open]').count()).toBe(0) + }) + + it('opens a context menu on right-click that the strip cannot clip', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-menu')) + const column = page.locator('[data-rightbar-col]') + + await ensureExpanded(page, column) + await expect.poll(async () => await column.locator('[data-dockkit-tab]').count()).toBeGreaterThan(0) + // No "more" control on the chip: the chip carries its close, and the menu + // is the secondary press. + expect(await column.locator('[data-dockkit-tab-more]').count()).toBe(0) + await column.locator('[data-dockkit-tab]').first().click({ button: 'right' }) + const menu = page.locator('[data-dockkit-tab-menu]') + await expect.poll(async () => await menu.count()).toBe(1) + + // The kit's one item; the extension seat is declared and rendered, and + // with no registrant it contributes nothing, which is what "declared, not + // speculative" looks like from the outside. + await expect.poll(async () => await menu.getByRole('menuitem').allInnerTexts()).toEqual(['Close']) + // The menu hangs below the strip that clips its overflow. Its pixels are + // its own: a hit test at its centre lands on it, not on whatever the + // strip would have shown through a clipped box. + expect(await hitsItself(menu)).toBe(true) + const box = await menu.boundingBox() + const viewport = page.viewportSize() + if (box === null || viewport === null) throw new Error('menu or viewport is not measurable') + expect(box.x).toBeGreaterThanOrEqual(0) + expect(box.x + box.width).toBeLessThanOrEqual(viewport.width) + await page.keyboard.press('Escape') + + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + // The product ships Chinese; the cases above advertise English so their role + // locators stay stable. This is the other half of the same seam, and the + // screenshot it takes is what the copy draft gets reviewed from. It lives in + // this block because a settled session is its precondition too — a case that + // depends on a sibling block's setup passes only in the right order. + it('renders the shipped Chinese copy on a Chinese page', async () => { + const zhPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) + const zhTripwire = watchConsole(zhPage) + onTestFailed(() => saveFailureShot(zhPage, 'web-e2e-sidebar-right-zh')) + try { + await zhPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await zhPage.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // A fresh page opens the workspace on a blank session's hero, which has + // no session header and so no expand button. The settled session is the + // second row of the tree; pick it the way a user would. + await zhPage.getByRole('treeitem', { name: /Show the right sidebar\./u }).first().click() + const column = zhPage.locator('[data-rightbar-col]') + await expandOf(zhPage).waitFor({ timeout: 20_000 }) + await expandOf(zhPage).click() + + const guide = column.locator('[data-sidebar-right-guide]') + await expect.poll(async () => await guide.count()).toBe(1) + // Wait for the track, not just the panel: the copy is only legible once + // the column has the width, and a screenshot taken mid-transition reads + // as a layout defect that is not there. + expect(await width(column)).toBeGreaterThan(300) + await expect.poll(async () => await tabTitles(column)).toEqual(['开始']) + await expect.poll(async () => await guide.locator('p').first().innerText()) + .toBe('侧栏用来放你想一直看着的东西。') + await shot(zhPage, '05-guide-copy-zh') + + expect(zhTripwire.pageErrors).toEqual([]) + expect(zhTripwire.warnings).toEqual([]) + } finally { + await zhPage.close() + } + }, 120_000) + }) +}) diff --git a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md index fec0614de5..81c1e347a7 100644 --- a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md +++ b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/bash-abort-row/ui.expected.md b/snapshots/web/bash-abort-row/ui.expected.md index fb3437d266..02083de5c3 100644 --- a/snapshots/web/bash-abort-row/ui.expected.md +++ b/snapshots/web/bash-abort-row/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/cordis-tool-round/ui.expected.md b/snapshots/web/cordis-tool-round/ui.expected.md index 07b054429a..955c1ec28e 100644 --- a/snapshots/web/cordis-tool-round/ui.expected.md +++ b/snapshots/web/cordis-tool-round/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/details-session-lifecycle/sidebar.expected.md b/snapshots/web/details-session-lifecycle/sidebar.expected.md new file mode 100644 index 0000000000..bb818ae97d --- /dev/null +++ b/snapshots/web/details-session-lifecycle/sidebar.expected.md @@ -0,0 +1,496 @@ +# Recorded-session Sidebar states + +## A normal: two panes + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 280, + 644, + 756 + ], + "expanded": true, + "mode": "push", + "panelContentWidth": 756, + "panelOuterWidth": 757, + "coversViewport": false, + "resizeHandleWidth": 8, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## A manual fullscreen: underlying columns retained + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 280, + 644, + 756 + ], + "expanded": true, + "mode": "fullscreen", + "panelContentWidth": 1680, + "panelOuterWidth": 1680, + "coversViewport": true, + "resizeHandleWidth": 0, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## A closed with manual fullscreen retained + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 280, + 1400, + 0 + ], + "expanded": false, + "mode": "fullscreen", + "panelContentWidth": 0, + "panelOuterWidth": 0, + "coversViewport": false, + "resizeHandleWidth": 0, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## B closed: independent pane and expanded workspace directory + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 280, + 1400, + 0 + ], + "expanded": false, + "mode": "push", + "panelContentWidth": 0, + "panelOuterWidth": 0, + "coversViewport": false, + "resizeHandleWidth": 0, + "expandedDirectories": [ + "workspace" + ], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + } + ] + } + ] +} +``` + +## A restored: manual fullscreen, tabs, and panes + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 280, + 644, + 756 + ], + "expanded": true, + "mode": "fullscreen", + "panelContentWidth": 1680, + "panelOuterWidth": 1680, + "coversViewport": true, + "resizeHandleWidth": 0, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## B restored: normal mode and Files directory state + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 280, + 644, + 756 + ], + "expanded": true, + "mode": "push", + "panelContentWidth": 756, + "panelOuterWidth": 757, + "coversViewport": false, + "resizeHandleWidth": 8, + "expandedDirectories": [ + "workspace" + ], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + } + ] + } + ] +} +``` + +## A restored: expanded normal panel + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 280, + 644, + 756 + ], + "expanded": true, + "mode": "push", + "panelContentWidth": 756, + "panelOuterWidth": 757, + "coversViewport": false, + "resizeHandleWidth": 8, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## A capacity-closed: wide left preference protected + +```json +{ + "viewport": [ + 1024, + 1000 + ], + "columns": [ + 420, + 604, + 0 + ], + "expanded": false, + "mode": "push", + "panelContentWidth": 0, + "panelOuterWidth": 0, + "coversViewport": false, + "resizeHandleWidth": 0, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## A widened: remains closed + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 420, + 1260, + 0 + ], + "expanded": false, + "mode": "push", + "panelContentWidth": 0, + "panelOuterWidth": 0, + "coversViewport": false, + "resizeHandleWidth": 0, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## A automatic fullscreen at 767px + +```json +{ + "viewport": [ + 767, + 1000 + ], + "columns": [ + 56, + 711, + 0 + ], + "expanded": true, + "mode": "fullscreen", + "panelContentWidth": 767, + "panelOuterWidth": 767, + "coversViewport": true, + "resizeHandleWidth": 0, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` + +## A automatic fullscreen exited: widening does not reopen + +```json +{ + "viewport": [ + 1680, + 1000 + ], + "columns": [ + 420, + 1260, + 0 + ], + "expanded": false, + "mode": "push", + "panelContentWidth": 0, + "panelOuterWidth": 0, + "coversViewport": false, + "resizeHandleWidth": 0, + "expandedDirectories": [], + "panes": [ + { + "active": true, + "tabs": [ + { + "title": "Files", + "selected": true + }, + { + "title": "Start", + "selected": false + } + ] + }, + { + "active": false, + "tabs": [ + { + "title": "Start", + "selected": true + } + ] + } + ] +} +``` diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md index 4d6f0f24c0..f5ce4e137a 100644 --- a/snapshots/web/feedback-command/ack-expanded.expected.md +++ b/snapshots/web/feedback-command/ack-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md index 7dfd7fe123..d702f94f3a 100644 --- a/snapshots/web/feedback-command/ack.expected.md +++ b/snapshots/web/feedback-command/ack.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md index 4d6f0f24c0..f5ce4e137a 100644 --- a/snapshots/web/feedback-release/ack-expanded.expected.md +++ b/snapshots/web/feedback-release/ack-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md index 7dfd7fe123..d702f94f3a 100644 --- a/snapshots/web/feedback-release/ack.expected.md +++ b/snapshots/web/feedback-release/ack.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/file-upload-round/ui.expected.md b/snapshots/web/file-upload-round/ui.expected.md index 2541ec1c4a..0a8849c1d1 100644 --- a/snapshots/web/file-upload-round/ui.expected.md +++ b/snapshots/web/file-upload-round/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md index b65f5d2ae3..70b8e58bf5 100644 --- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md +++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md index f0449b34f2..2274d37a76 100644 --- a/snapshots/web/fresh-round-trip/ui.expected.md +++ b/snapshots/web/fresh-round-trip/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md index aeb9ecd7a9..f26864b10d 100644 --- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/goal-multi-turn-actions/ui.expected.md b/snapshots/web/goal-multi-turn-actions/ui.expected.md index eeaf7d1cca..b26c923482 100644 --- a/snapshots/web/goal-multi-turn-actions/ui.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/lifecycle-chrome/hero.expected.md b/snapshots/web/lifecycle-chrome/hero.expected.md index a4694918d4..400ac58929 100644 --- a/snapshots/web/lifecycle-chrome/hero.expected.md +++ b/snapshots/web/lifecycle-chrome/hero.expected.md @@ -40,6 +40,3 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: Details -- button "Close details" -- text: Click a tool row in the message flow to view its details diff --git a/snapshots/web/lifecycle-chrome/plan-active.expected.md b/snapshots/web/lifecycle-chrome/plan-active.expected.md index e886dd913e..ccb4e102e5 100644 --- a/snapshots/web/lifecycle-chrome/plan-active.expected.md +++ b/snapshots/web/lifecycle-chrome/plan-active.expected.md @@ -40,6 +40,3 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: Details -- button "Close details" -- text: Click a tool row in the message flow to view its details diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md index 6cde9ddcba..a2285c7a0d 100644 --- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md index dff102b7cb..fb568b1c26 100644 --- a/snapshots/web/lifecycle-chrome/reloaded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/cancel-expanded.expected.md b/snapshots/web/live-interactions/cancel-expanded.expected.md index 635ddf6d9f..58a183ae56 100644 --- a/snapshots/web/live-interactions/cancel-expanded.expected.md +++ b/snapshots/web/live-interactions/cancel-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/cancel.expected.md b/snapshots/web/live-interactions/cancel.expected.md index 84320a45d0..4598bb6f79 100644 --- a/snapshots/web/live-interactions/cancel.expected.md +++ b/snapshots/web/live-interactions/cancel.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/error-auth.expected.md b/snapshots/web/live-interactions/error-auth.expected.md index e5b3c6ca64..cf060b5584 100644 --- a/snapshots/web/live-interactions/error-auth.expected.md +++ b/snapshots/web/live-interactions/error-auth.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/loading.expected.md b/snapshots/web/live-interactions/loading.expected.md index a16ba7d1ee..48a4ca5b5f 100644 --- a/snapshots/web/live-interactions/loading.expected.md +++ b/snapshots/web/live-interactions/loading.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/retry-exhausted.expected.md b/snapshots/web/live-interactions/retry-exhausted.expected.md index 237543a043..c093363f43 100644 --- a/snapshots/web/live-interactions/retry-exhausted.expected.md +++ b/snapshots/web/live-interactions/retry-exhausted.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md index 3919ce3dfc..9cfda2b0ae 100644 --- a/snapshots/web/live-interactions/retry-expanded.expected.md +++ b/snapshots/web/live-interactions/retry-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md index 1cfe5382f5..08061e9718 100644 --- a/snapshots/web/live-interactions/retry.expected.md +++ b/snapshots/web/live-interactions/retry.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/live-interactions/running-draft.expected.md b/snapshots/web/live-interactions/running-draft.expected.md index fa61237f73..9e1a1be89e 100644 --- a/snapshots/web/live-interactions/running-draft.expected.md +++ b/snapshots/web/live-interactions/running-draft.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md index 3ca1911afc..9070601856 100644 --- a/snapshots/web/message-actions/ui.expected.md +++ b/snapshots/web/message-actions/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index 91a71e8e57..b1d4efd49f 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md index 58abbd47bd..0e7c1a3cda 100644 --- a/snapshots/web/plan-review/approved-expanded.expected.md +++ b/snapshots/web/plan-review/approved-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md index cc7b07f86d..9b2fea933a 100644 --- a/snapshots/web/plan-review/approved.expected.md +++ b/snapshots/web/plan-review/approved.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/ptc-round/ui.expected.md b/snapshots/web/ptc-round/ui.expected.md index 1d2c9b9abd..f07f6b2837 100644 --- a/snapshots/web/ptc-round/ui.expected.md +++ b/snapshots/web/ptc-round/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md index 34c5652e9a..aba4bedb58 100644 --- a/snapshots/web/question-composer/answered-expanded.expected.md +++ b/snapshots/web/question-composer/answered-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md index 1a879c04c5..a2f7298f6b 100644 --- a/snapshots/web/question-composer/answered.expected.md +++ b/snapshots/web/question-composer/answered.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/question-composer/cancelled.expected.md b/snapshots/web/question-composer/cancelled.expected.md index d6f6be3bba..4b48983c38 100644 --- a/snapshots/web/question-composer/cancelled.expected.md +++ b/snapshots/web/question-composer/cancelled.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queue-actions/collapsed.expected.md b/snapshots/web/queue-actions/collapsed.expected.md index 0b3e7b65b0..6645c331fd 100644 --- a/snapshots/web/queue-actions/collapsed.expected.md +++ b/snapshots/web/queue-actions/collapsed.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queue-actions/editing.expected.md b/snapshots/web/queue-actions/editing.expected.md index f32a37c548..d0586fb967 100644 --- a/snapshots/web/queue-actions/editing.expected.md +++ b/snapshots/web/queue-actions/editing.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queue-actions/layout.expected.md b/snapshots/web/queue-actions/layout.expected.md index 0e0835f2bb..79f239edd8 100644 --- a/snapshots/web/queue-actions/layout.expected.md +++ b/snapshots/web/queue-actions/layout.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queue-actions/preserved-expanded.expected.md b/snapshots/web/queue-actions/preserved-expanded.expected.md index 041650fb7b..85489c97ef 100644 --- a/snapshots/web/queue-actions/preserved-expanded.expected.md +++ b/snapshots/web/queue-actions/preserved-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queue-actions/preserved.expected.md b/snapshots/web/queue-actions/preserved.expected.md index e9a74f7b05..a4330098cc 100644 --- a/snapshots/web/queue-actions/preserved.expected.md +++ b/snapshots/web/queue-actions/preserved.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queue-actions/sending.expected.md b/snapshots/web/queue-actions/sending.expected.md index 10115fe73b..c31dbd674a 100644 --- a/snapshots/web/queue-actions/sending.expected.md +++ b/snapshots/web/queue-actions/sending.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queue-actions/ui.expected.md b/snapshots/web/queue-actions/ui.expected.md index e314229b31..e7aa55934d 100644 --- a/snapshots/web/queue-actions/ui.expected.md +++ b/snapshots/web/queue-actions/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queued-image/delivered.expected.md b/snapshots/web/queued-image/delivered.expected.md index 23fad54c88..7fdc60ffaa 100644 --- a/snapshots/web/queued-image/delivered.expected.md +++ b/snapshots/web/queued-image/delivered.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/queued-image/queued.expected.md b/snapshots/web/queued-image/queued.expected.md index 457affb944..68d4ef90b7 100644 --- a/snapshots/web/queued-image/queued.expected.md +++ b/snapshots/web/queued-image/queued.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md index 3e9691ff71..67adda5e9e 100644 --- a/snapshots/web/seeded-history/command-row.expected.md +++ b/snapshots/web/seeded-history/command-row.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md index 7ce1e515d4..d2ce9690cb 100644 --- a/snapshots/web/seeded-history/feedback-row.expected.md +++ b/snapshots/web/seeded-history/feedback-row.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/seeded-history/file-open-failure.expected.md b/snapshots/web/seeded-history/file-open-failure.expected.md deleted file mode 100644 index fece247a7a..0000000000 --- a/snapshots/web/seeded-history/file-open-failure.expected.md +++ /dev/null @@ -1,7 +0,0 @@ -- dialog "Couldn’t open file": - - heading "Couldn’t open file" [level=2] - - button "Close": - - img - - paragraph: "path open failed: xdg-open is not available" - - button "Cancel" - - button "Retry" diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md index 70c6204338..84c770538d 100644 --- a/snapshots/web/seeded-history/ui-expanded.expected.md +++ b/snapshots/web/seeded-history/ui-expanded.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md index 7ea48e69f6..ed9567ae05 100644 --- a/snapshots/web/seeded-history/ui.expected.md +++ b/snapshots/web/seeded-history/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md index 74903e7e6e..8f739e02c1 100644 --- a/snapshots/web/skill-tool-row/ui.expected.md +++ b/snapshots/web/skill-tool-row/ui.expected.md @@ -4,6 +4,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/steering/mid-steer.expected.md b/snapshots/web/steering/mid-steer.expected.md index 557e5bc0ff..94b2356941 100644 --- a/snapshots/web/steering/mid-steer.expected.md +++ b/snapshots/web/steering/mid-steer.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md index 5c2738c768..dbf54a6910 100644 --- a/snapshots/web/steering/settled-expanded.expected.md +++ b/snapshots/web/steering/settled-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md index 237bab0dfc..d84e7d053c 100644 --- a/snapshots/web/steering/settled.expected.md +++ b/snapshots/web/steering/settled.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/subagent-conversation/nested.expected.md b/snapshots/web/subagent-conversation/nested.expected.md index e5d8498557..1ed0fc0b02 100644 --- a/snapshots/web/subagent-conversation/nested.expected.md +++ b/snapshots/web/subagent-conversation/nested.expected.md @@ -8,6 +8,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md index 37e05fa0d0..a31c527d2a 100644 --- a/snapshots/web/subagent-conversation/ui-expanded.expected.md +++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md @@ -11,6 +11,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md index f4f25c4224..887f0a722b 100644 --- a/snapshots/web/subagent-conversation/ui.expected.md +++ b/snapshots/web/subagent-conversation/ui.expected.md @@ -11,6 +11,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/subagent-interrupt/offline-composer.expected.md b/snapshots/web/subagent-interrupt/offline-composer.expected.md index b34acde2d7..e7f7483bc3 100644 --- a/snapshots/web/subagent-interrupt/offline-composer.expected.md +++ b/snapshots/web/subagent-interrupt/offline-composer.expected.md @@ -8,6 +8,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md index ed25b8d556..4ef04fc9a1 100644 --- a/snapshots/web/turn-tail-actions/completed.expected.md +++ b/snapshots/web/turn-tail-actions/completed.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md index 0c2c806aad..fc39e90660 100644 --- a/snapshots/web/turn-tail-actions/focused.expected.md +++ b/snapshots/web/turn-tail-actions/focused.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/turn-tail-actions/running.expected.md b/snapshots/web/turn-tail-actions/running.expected.md index 502147691d..5808186525 100644 --- a/snapshots/web/turn-tail-actions/running.expected.md +++ b/snapshots/web/turn-tail-actions/running.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md index 5aff82d1a1..2552b536b2 100644 --- a/snapshots/web/turn-tail-actions/settled.expected.md +++ b/snapshots/web/turn-tail-actions/settled.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md index a01ee4fd78..92686f6691 100644 --- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md +++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md index 5ab42e660c..8dc1f0698c 100644 --- a/snapshots/web/web-search-round/ui.expected.md +++ b/snapshots/web/web-search-round/ui.expected.md @@ -6,6 +6,8 @@ - button "Session log": - text: Session log - img + - button "Open the sidebar": + - img - tablist: - tab "Chat" [selected] - tab "Trajectory"