Merge remote-tracking branch 'origin/fix/human-transcript-projection' into fix/web-transcript-projection
# Conflicts: # apps/web/tests/snapshots/seeded-history/ui.expected.md # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/src/client/index.ts # packages/client/runtime/src/client/sessions/fold-adapter.ts # packages/client/runtime/tests/fold-adapter.spec.ts # packages/client/ui-trajectory/src/client/layout.ts # packages/client/ui-trajectory/src/client/spans.ts # packages/compact/compact/README.i18n.yaml
This commit is contained in:
117
apps/web/tests/built-boot.snapshot.ts
Normal file
117
apps/web/tests/built-boot.snapshot.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// @vitest-environment jsdom
|
||||
// The built-bundle boot smoke: the ONE assembled-jsdom test that loads the
|
||||
// real `packages/client/*/lib/client.js` artifacts through AppWebEntry's
|
||||
// ModuleLoader path (fetchBundle/executeBundle) and proves the boot graph
|
||||
// assembles — staged activation across the immediately tier and the inject
|
||||
// layers, per-plugin CSS injection, and a rendered journey reaching chat
|
||||
// content from the keyless FixtureApiClient transport.
|
||||
//
|
||||
// Behavior assertions do NOT belong here: component and wiring behavior is
|
||||
// pinned by the per-package suites (SlotTestRuntime benches over src), which
|
||||
// this smoke's plugin set cannot influence — bundling, module-table
|
||||
// resolution, and boot layering are the only failure modes left to it.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
|
||||
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
||||
|
||||
const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
|
||||
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{
|
||||
id: '@deepseek-ai/dsh-client-ui-workspace',
|
||||
dir: 'ui-workspace',
|
||||
url: '/plugins/ui-workspace.js',
|
||||
rev: 'fx',
|
||||
inject: [
|
||||
'@deepseek-ai/dsh-client-runtime',
|
||||
'@deepseek-ai/dsh-client-ui-conversation',
|
||||
'@deepseek-ai/dsh-client-ui-sidebar',
|
||||
],
|
||||
},
|
||||
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
]
|
||||
|
||||
const bundles = new Map(PLUGINS.map(plugin => [
|
||||
plugin.url,
|
||||
readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
|
||||
]))
|
||||
|
||||
interface FixtureWindow extends Window {
|
||||
__DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
|
||||
__ModuleLoader__?: unknown
|
||||
}
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
const win = window as FixtureWindow
|
||||
let unmount: (() => void) | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => { callback(0) }, 0) as unknown as number)
|
||||
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => { unmount?.() })
|
||||
unmount = undefined
|
||||
cleanup()
|
||||
delete win.__DSH_BOOT__
|
||||
delete win.__ModuleLoader__
|
||||
document.body.innerHTML = ''
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('boots the built plugin graph and renders a fixture session end to end', async () => {
|
||||
history.replaceState(null, '', '/?fixture')
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
|
||||
act(() => {
|
||||
const entry = new AppWebEntry(root, {
|
||||
fetchBundle: (url) => {
|
||||
const code = bundles.get(url)
|
||||
return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
|
||||
},
|
||||
executeBundle: (code) => { (0, eval)(code) },
|
||||
})
|
||||
void entry.run()
|
||||
unmount = () => { entry.dispose() }
|
||||
})
|
||||
|
||||
// The sidebar renders from the boot graph: every inject layer activated.
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
await within(tree).findByText('4 sessions')
|
||||
|
||||
// Opening a session reaches chat content through the fixture transport.
|
||||
fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
|
||||
// Every bundle injected its plugin-owned style tag (the loader's CSS path).
|
||||
const styleOwners = [...document.head.querySelectorAll('style[data-plugin]')]
|
||||
.map(style => style.getAttribute('data-plugin'))
|
||||
for (const plugin of ['@deepseek-ai/dsh-client-ui-layout', '@deepseek-ai/dsh-client-ui-sidebar', '@deepseek-ai/dsh-client-ui-conversation']) {
|
||||
expect(styleOwners).toContain(plugin)
|
||||
}
|
||||
})
|
||||
@@ -1,259 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
// Code Mode fixture snapshot over the BUILT client graph (the workspace-flow
|
||||
// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
|
||||
// Opens the fixture history session and pins the run_code turn's rendering:
|
||||
// the code-variant parent row titled by the model-authored description, its
|
||||
// three always-visible nested sub-rows (bash through the sample registration,
|
||||
// read through GenericToolCard, the failing read wearing the error state),
|
||||
// the expanded program body, inert bash / file-link sub-row gestures, and
|
||||
// the trajectory/waterfall tabs' sub-call cells and timing lanes.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
|
||||
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
||||
|
||||
const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
|
||||
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{
|
||||
id: '@deepseek-ai/dsh-client-ui-workspace',
|
||||
dir: 'ui-workspace',
|
||||
url: '/plugins/ui-workspace.js',
|
||||
rev: 'fx',
|
||||
inject: [
|
||||
'@deepseek-ai/dsh-client-runtime',
|
||||
'@deepseek-ai/dsh-client-ui-conversation',
|
||||
'@deepseek-ai/dsh-client-ui-sidebar',
|
||||
],
|
||||
},
|
||||
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
]
|
||||
|
||||
const bundles = new Map(PLUGINS.map(plugin => [
|
||||
plugin.url,
|
||||
readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
|
||||
]))
|
||||
|
||||
interface FixtureWindow extends Window {
|
||||
__DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
|
||||
__ModuleLoader__?: unknown
|
||||
}
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
const win = window as FixtureWindow
|
||||
let unmount: (() => void) | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => { callback(0) }, 0) as unknown as number)
|
||||
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => { unmount?.() })
|
||||
unmount = undefined
|
||||
cleanup()
|
||||
delete win.__DSH_BOOT__
|
||||
delete win.__ModuleLoader__
|
||||
delete (globalThis as Record<string, unknown>).__fxTiming
|
||||
document.body.innerHTML = ''
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** Boot the complete built client graph against the populated fixture branch. */
|
||||
function boot(): void {
|
||||
history.replaceState(null, '', '/?fixture')
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
|
||||
act(() => {
|
||||
const entry = new AppWebEntry(root, {
|
||||
fetchBundle: (url) => {
|
||||
const code = bundles.get(url)
|
||||
return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
|
||||
},
|
||||
executeBundle: (code) => { (0, eval)(code) },
|
||||
})
|
||||
void entry.run()
|
||||
unmount = () => { entry.dispose() }
|
||||
})
|
||||
}
|
||||
|
||||
/** Collapse decorative whitespace while preserving the text a user sees. */
|
||||
function visibleText(element: Element): string {
|
||||
return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** Open the fixture history session (the alpha log carrying the run_code turn) and scroll to its tail. */
|
||||
async function openFixtureSession(): Promise<void> {
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const group = within(tree).getByText('4 sessions').closest<HTMLElement>('[role="treeitem"]')
|
||||
if (group === null) throw new Error('fixture Workspace group missing')
|
||||
if (group.getAttribute('aria-expanded') === 'false') {
|
||||
fireEvent.click(within(group).getByText('fixture'))
|
||||
await waitFor(() => {
|
||||
expect(within(tree).getByText('4 sessions').closest('[role="treeitem"]')?.getAttribute('aria-expanded')).toBe('true')
|
||||
})
|
||||
}
|
||||
const session = await within(tree).findByText('Fixture 历史会话')
|
||||
fireEvent.click(session)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-variant="code"]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
}
|
||||
|
||||
it('renders the fixture run_code turn: code parent row, nested sub-rows, error state', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
|
||||
const codeRoot = document.querySelector('[data-variant="code"]')
|
||||
if (codeRoot === null) throw new Error('code-variant row missing')
|
||||
const nest = codeRoot.closest('[class*="callRow"]')?.querySelector('[data-subcalls]')
|
||||
if (nest === undefined || nest === null) throw new Error('sub-call nest missing under the code row')
|
||||
|
||||
expect({
|
||||
parentRow: visibleText(codeRoot),
|
||||
// The three sub-rows in dispatch order: bash rides the sample plugin's
|
||||
// keyed registration (the same one a native top-level bash row uses),
|
||||
// both reads ride GenericToolCard.
|
||||
bashSample: nest.querySelector('[data-sample="bash-global"]') !== null,
|
||||
subRows: [...nest.querySelectorAll(':scope > *')].map(visibleText),
|
||||
errorSubRow: nest.querySelector('[data-state="error"]') !== null,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bashSample": true,
|
||||
"errorSubRow": true,
|
||||
"parentRow": "CodeRead the notes files and summarize",
|
||||
"subRows": [
|
||||
"BashList notes",
|
||||
"Readnotes/demo.txt",
|
||||
"Readnotes/missing.txt",
|
||||
],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('expands the code row into the program body; sub-row clicks do not open details', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
|
||||
// Expand: the leading control reveals the program (shiki-tokenized: the
|
||||
// text splits into styled spans inside one <pre class="shiki"> tree).
|
||||
const codeRoot = document.querySelector('[data-variant="code"]')
|
||||
if (codeRoot === null) throw new Error('code-variant row missing')
|
||||
const toggle = codeRoot.querySelector('button[aria-expanded]')
|
||||
if (toggle === null) throw new Error('code row expand control missing')
|
||||
fireEvent.click(toggle)
|
||||
await waitFor(() => {
|
||||
// Scope to THIS row: the markdown fixture turn also renders shiki pres.
|
||||
const pre = codeRoot.querySelector('pre.shiki')
|
||||
if (pre === null || !(pre.textContent ?? '').includes('const listing = await tools.bash')) {
|
||||
throw new Error('highlighted program body missing under the code row')
|
||||
}
|
||||
})
|
||||
|
||||
// Tool rows no longer drive the details panel: bash is inert, file paths
|
||||
// are host-open links (fixture openPath is a no-op success).
|
||||
const nest = document.querySelector('[data-subcalls]')
|
||||
if (nest === null) throw new Error('sub-call nest missing')
|
||||
const bashRow = nest.querySelector('[data-sample="bash-global"]')
|
||||
if (bashRow === null) throw new Error('bash sample sub-row missing')
|
||||
const fileLink = nest.querySelector('button')
|
||||
if (fileLink === null) throw new Error('file-path summary link missing on a read sub-row')
|
||||
const frame = document.querySelector('[data-details-collapsed]')
|
||||
if (frame === null) throw new Error('app frame missing')
|
||||
expect(frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
fireEvent.click(bashRow)
|
||||
expect(frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
fireEvent.click(fileLink)
|
||||
expect(frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
expect({
|
||||
fileLink: visibleText(fileLink),
|
||||
detailsCollapsed: frame.getAttribute('data-details-collapsed'),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"detailsCollapsed": "true",
|
||||
"fileLink": "notes/demo.txt",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('trajectory and waterfall surface the run_code sub-calls with real timing', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
|
||||
// Switch to the trajectory tab (same slot ring the chat view registers in).
|
||||
fireEvent.click(await screen.findByRole('tab', { name: 'Trajectory' }))
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-kind="subtool"]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
const subCells = [...document.querySelectorAll('[data-kind="subtool"]')]
|
||||
expect({
|
||||
// Three Sub cells nested under the run_code Tool cell, in dispatch order,
|
||||
// each with a real +N.Ns own-duration off the start/settle pair (the
|
||||
// fixture spaces every event 800ms apart — never the em dash).
|
||||
subCells: subCells.map(cell => visibleText(cell)),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"subCells": [
|
||||
"#51Subbash · {"command":"ls notes","description":"List notes"}+0.8s",
|
||||
"#52Subread · {"path":"notes/demo.txt"}+0.8s",
|
||||
"#53Subread · {"path":"notes/missing.txt"}+0.8s",
|
||||
],
|
||||
}
|
||||
`)
|
||||
|
||||
// Waterfall: each sub-call draws a measured lane scaled into the parent
|
||||
// turn's dispatch window.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Waterfall' }))
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-subspan]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
const lanes = [...document.querySelectorAll('[data-subspan]')]
|
||||
expect({
|
||||
lanes: lanes.map(lane => ({
|
||||
label: visibleText(lane.querySelector('[class*="subTag"]') ?? lane),
|
||||
title: lane.querySelector('[data-timing]')?.getAttribute('title'),
|
||||
timing: lane.querySelector('[data-timing]')?.getAttribute('data-timing'),
|
||||
})),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"lanes": [
|
||||
{
|
||||
"label": "bash",
|
||||
"timing": "measured",
|
||||
"title": "bash · 0.80s",
|
||||
},
|
||||
{
|
||||
"label": "read",
|
||||
"timing": "measured",
|
||||
"title": "read · 0.80s",
|
||||
},
|
||||
{
|
||||
"label": "read",
|
||||
"timing": "measured",
|
||||
"title": "read · 0.80s",
|
||||
},
|
||||
],
|
||||
}
|
||||
`)
|
||||
})
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/code-mode-round/session.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/code-mode-round/ui.expected.md', import.meta.url))
|
||||
@@ -44,7 +44,7 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -118,14 +118,14 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
|
||||
expect(await nest.locator('[data-state="error"]').count()).toBeGreaterThanOrEqual(1)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('a bash sub-row click leaves the details panel collapsed', async () => {
|
||||
it.skipIf(MODE === 'record')('a bash sub-row click leaves the default details panel open', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-code-mode-details'))
|
||||
const nest = page.locator('[data-subcalls]').first()
|
||||
const frame = page.locator('[data-details-collapsed], [class*="frame"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).not.toBeNull()
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
await nest.locator('[data-sample="bash-global"]').first().click()
|
||||
// Tool rows no longer open details; the column stays width 0.
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).not.toBeNull()
|
||||
// Tool rows do not drive layout geometry; the Session's default panel stays open.
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the conversation aria golden with stable anchors', async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/cordis-tool-round/session.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/cordis-tool-round/ui.expected.md', import.meta.url))
|
||||
@@ -62,7 +62,7 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
@@ -8,7 +8,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/lifecycle-chrome/session.jsonl', import.meta.url))
|
||||
const SEED_FIXTURE = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
@@ -40,7 +40,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5 })
|
||||
await seedSession(scaffold, await readFile(SEED_FIXTURE, 'utf8'), 'details-session-lifecycle-seed')
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await appFrame(page).waitFor({ timeout: 30_000 })
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/lifecycle-chrome', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
@@ -43,7 +43,7 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/live-interactions', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
@@ -90,7 +90,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -175,6 +175,29 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps a terminal request marker inside the trajectory table', async () => {
|
||||
await launch(() => ({
|
||||
patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: 'invalid api key', code: 'AUTH' } }],
|
||||
}))
|
||||
const { settled } = await sendPrompt()
|
||||
await settled
|
||||
await page.getByRole('tab', { name: 'Trajectory' }).click()
|
||||
const tailRequest = page.locator('tr[data-request-only="true"]').last()
|
||||
await tailRequest.waitFor({ timeout: 10_000 })
|
||||
const requestMarker = tailRequest.getByRole('button', { name: /Request #/ })
|
||||
|
||||
const markerWithinTable = await requestMarker.evaluate((element) => {
|
||||
const marker = element.getBoundingClientRect()
|
||||
const table = element.closest('table')?.getBoundingClientRect()
|
||||
if (table === undefined) throw new Error('request marker has no table')
|
||||
return marker.bottom <= table.bottom
|
||||
})
|
||||
|
||||
expect(markerWithinTable).toBe(true)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('recovers a transient SERVER failure through llm-retry and completes', async () => {
|
||||
const derived = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8')))
|
||||
expect(derived).toHaveLength(1)
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-actions', import.meta.url))
|
||||
// Borrowed read-only: this scenario needs any settled user+assistant pair, not
|
||||
@@ -40,7 +40,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
expect(fixtureUserPrompts(raw), 'borrowed seed must carry the drive prompt').toEqual([PROMPT])
|
||||
await seedSession(scaffold, raw, SEED_ID)
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -75,7 +75,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
it.skipIf(MODE === 'record')('matches the conversation aria golden with IconActions and clocks', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-actions-aria'))
|
||||
await page.getByRole('button', {
|
||||
name: '选择模型,当前 deepseek-v4-flash',
|
||||
name: 'Select model, current deepseek-v4-flash',
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
// Keep a footer focused so opacity-hidden actions stay in the a11y tree
|
||||
// as an active/focused control during the capture.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Web e2e scenarios: navigation & panes — the view tabs (Trajectory /
|
||||
// Waterfall) and sidebar search, all over ONE rich two-turn seeded fixture
|
||||
// rendered purely from the log (the seeded-history pattern: zero model calls
|
||||
// in replay, so every surface here is the client fold + host history RPC,
|
||||
// not replay binding). The seed is recorded live under the standard
|
||||
// discipline: turn 1 produces a bash call plus two parallel reads in one
|
||||
// assistant message (tool-call density for the trajectory/waterfall lanes),
|
||||
// turn 2 a markdown-rich reply (a second turn so the waterfall has two lanes).
|
||||
// Web e2e scenarios: navigation & panes — the Trajectory view and timing
|
||||
// overview, its local details inspector, and sidebar search, all over ONE rich
|
||||
// two-turn seeded fixture rendered purely from the log (the seeded-history
|
||||
// pattern: zero model calls in replay, so every surface here is the client
|
||||
// fold + host history RPC, not replay binding). The seed is recorded live
|
||||
// under the standard discipline: turn 1 produces a bash call plus two
|
||||
// parallel reads in one assistant message (tool-call density for the
|
||||
// trajectory ledger/timing lanes), turn 2 a markdown-rich reply.
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
@@ -18,12 +18,12 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/navigation-panes', import.meta.url))
|
||||
const SEED = join(SNAPSHOT_DIR, 'seed.jsonl')
|
||||
const TRAJECTORY_EXPECTED = join(SNAPSHOT_DIR, 'trajectory.expected.md')
|
||||
const WATERFALL_EXPECTED = join(SNAPSHOT_DIR, 'waterfall.expected.md')
|
||||
const TERMINAL_EXPECTED = join(SNAPSHOT_DIR, 'terminal-card.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'navigation-panes-web-e2e'
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let slotErrors: string[]
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
@@ -55,8 +56,14 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
await seedSession(scaffold, raw, SEED_ID)
|
||||
}
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
slotErrors = []
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error' && /slot entry crashed/i.test(message.text())) {
|
||||
slotErrors.push(message.text())
|
||||
}
|
||||
})
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
@@ -122,58 +129,157 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
await expect.poll(() => page.locator('[role="treeitem"]').count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('renders the trajectory tab with turn sections and step cells', async () => {
|
||||
it.skipIf(MODE === 'record')('renders the trajectory ledger and opens its local record inspector', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-trajectory'))
|
||||
await page.getByRole('tab', { name: 'Trajectory' }).click()
|
||||
// Two sticky turn sections; turn 1's step group summarizes its tool mix
|
||||
// (bash + the two parallel reads collapse to 'bash read×2').
|
||||
await expect.poll(() => page.getByText('Turn 1', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
|
||||
await expect.poll(() => page.getByText('Turn 2', { exact: true }).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect.poll(() => page.getByText('bash read×2', { exact: false }).count(), { timeout: 10_000 }).toBe(1)
|
||||
await page.waitForTimeout(100)
|
||||
expect({
|
||||
pageErrors: tripwire.pageErrors,
|
||||
slotErrors,
|
||||
warnings: tripwire.warnings,
|
||||
}).toEqual({
|
||||
pageErrors: [],
|
||||
slotErrors: [],
|
||||
warnings: [],
|
||||
})
|
||||
// Turn rules partition the ledger without restoring a separate header row.
|
||||
await expect.poll(() => page.locator('tr[data-turn-start="true"]').count(), { timeout: 15_000 }).toBe(2)
|
||||
await expect.poll(() => page.getByRole('columnheader').count(), { timeout: 10_000 }).toBe(0)
|
||||
await page.locator('tr[data-kind="tool"]').first().click()
|
||||
await expect.poll(() => page.getByRole('complementary', { name: 'Event details' }).count(), { timeout: 10_000 }).toBe(1)
|
||||
await page.getByRole('tab', { name: 'Result' }).click()
|
||||
await expect.poll(() => page.getByText('NAVIGATION_OK', { exact: false }).count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1)
|
||||
const snapshot = (await captureStableAria(page, '[class*="viewArea"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(TRAJECTORY_EXPECTED, snapshot, MODE)
|
||||
await page.getByRole('complementary', { name: 'Event details' })
|
||||
.getByRole('button', { name: 'Close details' }).click()
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('renders the waterfall tab with span stats and one lane per span', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-waterfall'))
|
||||
await page.getByRole('tab', { name: 'Waterfall' }).click()
|
||||
// The stats header rides the waterfall body. The span fold counts THREE
|
||||
// spans for this two-turn log: only assistant/steering nodes carry a turn
|
||||
// number, so the first user message lands in a turn-0 prologue span (a
|
||||
// P-I placeholder shape — pinned as-is; real spans are deferred to
|
||||
// P-III per the view's deviation ledger). Calls: bash + two reads.
|
||||
await expect.poll(() => page.getByText(/3 turns · \d+ steps · 3 tool calls/).count(), { timeout: 15_000 }).toBe(1)
|
||||
// One lane per span, tagged by turn number, prologue included.
|
||||
for (const tag of ['turn 0', 'turn 1', 'turn 2']) {
|
||||
await expect.poll(() => page.getByText(tag, { exact: true }).count(), { timeout: 10_000 }).toBe(1)
|
||||
}
|
||||
const snapshot = (await captureStableAria(page, '[class*="viewArea"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(WATERFALL_EXPECTED, snapshot, MODE)
|
||||
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))
|
||||
const plot = page.getByLabel('Timeline overview; drag horizontally to focus events')
|
||||
const before = await page.locator('tr[data-kind]').count()
|
||||
const box = await plot.boundingBox()
|
||||
if (box === null) throw new Error('trajectory timeline plot has no layout box')
|
||||
await page.mouse.move(box.x + box.width * 0.55, box.y + box.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(box.x + box.width * 0.9, box.y + box.height / 2)
|
||||
await page.mouse.up()
|
||||
await expect.poll(() => page.locator('tr[data-timeline-focus="outside"]').count(), { timeout: 10_000 })
|
||||
.toBeGreaterThan(0)
|
||||
await expect.poll(() => page.locator('tr[data-kind]').count(), { timeout: 10_000 }).toBe(before)
|
||||
await plot.click({ button: 'right' })
|
||||
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 details column collapsed', async () => {
|
||||
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column open', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details'))
|
||||
await page.getByRole('tab', { name: 'Chat' }).click()
|
||||
const bashRow = page.locator('[data-sample="bash-global"]').first()
|
||||
await bashRow.waitFor({ timeout: 15_000 })
|
||||
const frame = page.locator('[data-details-collapsed], [class*="frame"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).not.toBeNull()
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
await bashRow.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).not.toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
// The card's own controls are outside the summary row and must not open
|
||||
// details either — the terminal card is read in place.
|
||||
await page.locator('[data-sample="bash-global"] ~ [data-terminal] [class*="_copyButton_"]').first().click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
// Read summaries are host-open file links; they also must not open details.
|
||||
const fileLink = page.locator('[data-variant="read"] button').first()
|
||||
await fileLink.waitFor({ timeout: 10_000 })
|
||||
await fileLink.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).not.toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('renders the bash row as a terminal card in the real browser', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-terminal'))
|
||||
await page.getByRole('tab', { name: 'Chat' }).click()
|
||||
// The card is resident in the keyed bash row (no expand gesture): the
|
||||
// recorded command's own output sits in the message flow, derived from the
|
||||
// logged call/result presentations alone.
|
||||
const card = page.locator('[data-sample="bash-global"] ~ [data-terminal], [data-sample="bash-global"] [data-terminal]').first()
|
||||
await card.waitFor({ timeout: 15_000 })
|
||||
// Real layout, not jsdom's stub (which computes no geometry at all):
|
||||
// squeeze the output pane below its content width and the line must keep
|
||||
// its single row and overflow sideways instead of folding. Soft-wrapping
|
||||
// here is what shredded the column alignment this card exists to hold.
|
||||
const layout = await card.locator('[class*="_output_"]').first().evaluate((node) => {
|
||||
const pane = node as HTMLElement
|
||||
const row = pane.querySelector<HTMLElement>('[class*="_line_"]')
|
||||
if (row === null) throw new Error('output pane has no line')
|
||||
const before = row.offsetHeight
|
||||
const restore = pane.style.width
|
||||
pane.style.width = '8px'
|
||||
const squeezed = { wrapped: row.offsetHeight > before, scrollsSideways: pane.scrollWidth > pane.clientWidth }
|
||||
pane.style.width = restore
|
||||
return { whiteSpace: getComputedStyle(row).whiteSpace, overflowX: getComputedStyle(pane).overflowX, ...squeezed }
|
||||
})
|
||||
expect(layout).toEqual({ whiteSpace: 'pre', overflowX: 'auto', wrapped: false, scrollsSideways: true })
|
||||
// The run-state dot's color is the whole point of it and is the one thing
|
||||
// jsdom cannot report: --dsw-* tokens resolve only against the real theme
|
||||
// stylesheet. This command settled cleanly, so the dot must be the green
|
||||
// success token — a red one here would read as a failed command.
|
||||
const dot = await card.locator('[class*="_runState_"][data-state]').first().evaluate((node) => {
|
||||
// The token lives on body, so the probe must sit in the same cascade.
|
||||
const probe = document.createElement('span')
|
||||
probe.style.color = 'var(--dsw-alias-state-success-primary)'
|
||||
document.body.appendChild(probe)
|
||||
const success = getComputedStyle(probe).color
|
||||
probe.remove()
|
||||
return {
|
||||
state: node.getAttribute('data-state'),
|
||||
color: getComputedStyle(node as HTMLElement).color,
|
||||
success,
|
||||
// One label per card (the state is the call's), so it hangs off the
|
||||
// prompt column rather than the row the dot sits in.
|
||||
label: node.closest('[class*="_prompt_"]')?.querySelector('[class*="_runStateLabel_"]')?.textContent ?? null,
|
||||
// The dot precedes the prompt label in document order, which is what
|
||||
// puts it to the left of the `$`.
|
||||
beforePrompt: node.compareDocumentPosition(node.parentElement!.querySelector('[class*="_cwd_"]')!)
|
||||
=== Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
// The dot lives in the card's OWN left padding, so it sits inside the
|
||||
// card box yet left of the prompt text. Owning the reservation as padding
|
||||
// rather than margin is what keeps a consumer's own margin from
|
||||
// cancelling it and letting a container clip the dot — geometry jsdom
|
||||
// cannot compute.
|
||||
insideCard: (node as HTMLElement).getBoundingClientRect().left
|
||||
>= (node.closest('[data-terminal]')?.getBoundingClientRect().left ?? Infinity),
|
||||
leftOfPrompt: (node as HTMLElement).getBoundingClientRect().right
|
||||
<= (node.closest('[class*="_promptLine_"]')
|
||||
?.querySelector('[class*="_cwd_"]')
|
||||
?.getBoundingClientRect().left ?? -Infinity),
|
||||
}
|
||||
})
|
||||
expect(dot.state).toBe('done')
|
||||
expect(dot.label).toBe('已完成')
|
||||
expect(dot.beforePrompt).toBe(true)
|
||||
expect(dot.insideCard).toBe(true)
|
||||
expect(dot.leftOfPrompt).toBe(true)
|
||||
// Resolved through the theme token, not a literal hex in the component.
|
||||
expect(dot.success).toMatch(/^rgb/)
|
||||
expect(dot.color).toBe(dot.success)
|
||||
// Golden of the card at rest — captured before the copy click, whose
|
||||
// confirmation label self-reverts on a timer and would not hold still.
|
||||
const snapshot = (await captureStableAria(page, '[data-terminal]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(TERMINAL_EXPECTED, snapshot, MODE)
|
||||
// Copy writes the raw output through the browser's own clipboard, which in
|
||||
// a real page is the async Clipboard API rather than the jsdom fallback.
|
||||
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
|
||||
await card.locator('[class*="_copyButton_"]').first().click()
|
||||
await expect.poll(() => card.locator('[class*="_copyButton_"]').first().textContent(), { timeout: 5_000 })
|
||||
.toBe('复制成功')
|
||||
expect(await page.evaluate(() => navigator.clipboard.readText())).toContain('NAVIGATION_OK')
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and stayed clean', async () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(slotErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'seed.jsonl', 'trajectory.expected.md', 'waterfall.expected.md',
|
||||
'seed.jsonl', 'trajectory.expected.md', 'terminal-card.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/question-composer', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
@@ -44,7 +44,7 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -104,7 +104,7 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
const inner = child.getBoundingClientRect()
|
||||
return Math.max(box.top - inner.top, inner.bottom - box.bottom)
|
||||
})))
|
||||
const list = rows[0]?.parentElement ?? null
|
||||
const list = card.querySelector<HTMLElement>('[data-question-scroll]')
|
||||
return {
|
||||
rows: rows.length,
|
||||
spill: Math.max(...spill),
|
||||
|
||||
119
apps/web/tests/queue-actions.e2e.ts
Normal file
119
apps/web/tests/queue-actions.e2e.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// Keyless browser coverage for pending queue actions through the shipped Web
|
||||
// composition and real HTTP/SSE wire. A replay override parks the active turn
|
||||
// so two ordinary follow-ups remain addressable while the page edits one and
|
||||
// removes one. The queue uses an existing recorded model
|
||||
// call; this scenario owns only the user-visible mid-turn golden.
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterEach, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/queue-actions', import.meta.url))
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/live-interactions/session.jsonl', import.meta.url))
|
||||
const EDITING_EXPECTED = join(SNAPSHOT_DIR, 'editing.expected.md')
|
||||
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
const ACTIVE_PROMPT = 'Reply with a one-sentence description of event sourcing, then stop.'
|
||||
const REMOVE = 'Queue item to remove'
|
||||
const EDIT = 'Queue item to edit'
|
||||
const EDITED = 'Edited queue item'
|
||||
|
||||
describe('web e2e: queue row actions', () => {
|
||||
let scaffold: WebScaffold | undefined
|
||||
let browser: Browser | undefined
|
||||
let page: Page
|
||||
let overrideDir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
const failures: unknown[] = []
|
||||
await browser?.close().catch((error: unknown) => failures.push(error))
|
||||
browser = undefined
|
||||
const closing = scaffold
|
||||
scaffold = undefined
|
||||
await closing?.close().catch((error: unknown) => failures.push(error))
|
||||
if (overrideDir !== undefined) {
|
||||
await rm(overrideDir, { recursive: true, force: true })
|
||||
.catch((error: unknown) => failures.push(error))
|
||||
}
|
||||
overrideDir = undefined
|
||||
if (failures.length === 1) throw failures[0]
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'queue-actions teardown failed')
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('edits and removes exact pending occurrences', async () => {
|
||||
overrideDir = await mkdtemp(join(tmpdir(), 'dsh-web-queue-actions-'))
|
||||
const readyFile = join(overrideDir, '.hang-ready')
|
||||
const overridePath = join(overrideDir, 'replay.override.json')
|
||||
await writeFile(overridePath, JSON.stringify({
|
||||
patches: [{ at: 0, entry: { kind: 'hang', readyFile } }],
|
||||
}))
|
||||
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayOverride: overridePath })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
const tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page)
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-queue-actions'))
|
||||
|
||||
const input = page.locator('textarea').first()
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
await input.fill(ACTIVE_PROMPT)
|
||||
await input.press('Enter')
|
||||
await expect.poll(() => existsSync(readyFile), { timeout: 15_000 }).toBe(true)
|
||||
|
||||
for (const text of [REMOVE, EDIT]) {
|
||||
await input.fill(text)
|
||||
await input.press('Enter')
|
||||
}
|
||||
await expect.poll(
|
||||
() => page.getByRole('button', { name: '删除排队消息' }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(2)
|
||||
|
||||
const editRow = page.getByText(EDIT, { exact: true }).locator('..')
|
||||
await editRow.getByRole('button', { name: '编辑排队消息' }).click()
|
||||
const editor = page.getByRole('textbox', { name: '编辑排队消息' })
|
||||
await editor.fill(EDITED)
|
||||
const editingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(EDITING_EXPECTED, editingSnapshot, MODE)
|
||||
await page.getByRole('button', { name: '保存排队消息' }).click()
|
||||
await page.getByText(EDITED, { exact: true }).waitFor()
|
||||
|
||||
const removeRow = page.getByText(REMOVE, { exact: true }).locator('..')
|
||||
await removeRow.getByRole('button', { name: '删除排队消息' }).click()
|
||||
await expect.poll(() => page.getByText(REMOVE, { exact: true }).count()).toBe(0)
|
||||
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
expect(sessionEvents.filter(event =>
|
||||
event.type === 'user/message' && event.data.source.kind === 'user',
|
||||
)).toHaveLength(1)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
|
||||
const editedRow = page.getByText(EDITED, { exact: true }).locator('..')
|
||||
await editedRow.getByRole('button', { name: '删除排队消息' }).click()
|
||||
await expect.poll(() => page.getByText(EDITED, { exact: true }).count()).toBe(0)
|
||||
await page.getByRole('button', { name: 'Stop generating' }).click()
|
||||
await settled
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps its snapshot inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['editing.expected.md', 'ui.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/fresh-round-trip', import.meta.url))
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url))
|
||||
@@ -43,7 +43,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -106,7 +106,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
await expect(page.getByRole('textbox').first().isVisible()).resolves.toBe(true)
|
||||
expect(await page.getByText('WEB_E2E_OK', { exact: false }).count()).toBeGreaterThanOrEqual(1)
|
||||
await page.getByRole('button', {
|
||||
name: '选择模型,当前 DeepSeek-V4-Flash',
|
||||
name: 'Select model, current DeepSeek-V4-Flash',
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/seeded-history', import.meta.url))
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
@@ -133,7 +133,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
await seedSession(scaffold, withCompaction(raw), SEED_ID)
|
||||
}
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -213,7 +213,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
// This scenario deliberately leaves the LLM seam open to prove zero
|
||||
// model calls. History still restores the selected id, but no catalog
|
||||
// adapter exists to provide its presentation name.
|
||||
name: '选择模型,当前 deepseek-v4-flash',
|
||||
name: 'Select model, current deepseek-v4-flash',
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
@@ -227,10 +227,10 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
// capture; still zero model calls.
|
||||
const fileLink = page.locator('[data-variant="read"] button').first()
|
||||
await fileLink.waitFor({ timeout: 10_000 })
|
||||
const frame = page.locator('[data-details-collapsed], [class*="frame"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).not.toBeNull()
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
await fileLink.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).not.toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
// Path label survives from the recorded args (a.txt).
|
||||
await expect.poll(() => page.getByText('a.txt', { exact: false }).count(), { timeout: 5_000 }).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
|
||||
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
||||
|
||||
const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
|
||||
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-settings', dir: 'ui-settings', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-sidebar'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-settings-general', dir: 'ui-settings-general', url: '/plugins/ui-settings-general.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-client-locale'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-models', dir: 'ui-models', url: '/plugins/ui-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-slash', dir: 'ui-slash', url: '/plugins/ui-slash.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-command', dir: 'ui-command', url: '/plugins/ui-command.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash', '@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-model', dir: 'ui-model', url: '/plugins/ui-model.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-command'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-workspace', dir: 'ui-workspace', url: '/plugins/ui-workspace.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-sidebar'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
]
|
||||
|
||||
const bundles = new Map(PLUGINS.map(plugin => [
|
||||
plugin.url,
|
||||
readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
|
||||
]))
|
||||
|
||||
interface FixtureTiming {
|
||||
appendTitle(id: string, title: string): void
|
||||
}
|
||||
|
||||
interface FixtureWindow extends Window {
|
||||
__DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
|
||||
__ModuleLoader__?: unknown
|
||||
}
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
const win = window as FixtureWindow
|
||||
let unmount: (() => void) | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
history.replaceState(null, '', '/?fixture')
|
||||
document.title = 'DeepSeek Harness'
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => { callback(0) }, 0) as unknown as number)
|
||||
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
|
||||
win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => { unmount?.() })
|
||||
unmount = undefined
|
||||
cleanup()
|
||||
delete win.__DSH_BOOT__
|
||||
delete win.__ModuleLoader__
|
||||
delete (globalThis as Record<string, unknown>).__fxTiming
|
||||
document.body.innerHTML = ''
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** Read only the stable, user-facing title surfaces from the assembled app. */
|
||||
function titleSurfaces(label: string): { sidebar: string; breadcrumb: string; documentTitle: string } {
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
const sidebar = within(tree).getByText(label).textContent ?? ''
|
||||
const breadcrumb = within(screen.getByRole('navigation', { name: 'Session hierarchy' }))
|
||||
.getByRole('button', { name: label }).textContent ?? ''
|
||||
return { sidebar, breadcrumb, documentTitle: document.title }
|
||||
}
|
||||
|
||||
it('projects titles and routes the next turn through the selected model in the built fixture app', async () => {
|
||||
const root = document.querySelector<HTMLElement>('#root')
|
||||
if (root === null) throw new Error('snapshot root missing')
|
||||
act(() => {
|
||||
const entry = new AppWebEntry(root, {
|
||||
fetchBundle: (url) => {
|
||||
const code = bundles.get(url)
|
||||
return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
|
||||
},
|
||||
executeBundle: (code) => { (0, eval)(code) },
|
||||
})
|
||||
void entry.run()
|
||||
unmount = () => { entry.dispose() }
|
||||
})
|
||||
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
// The fixture Intent selects the workspace, so the current-group effect
|
||||
// already expanded it; clicking the header would now collapse (the twist
|
||||
// stays live since intent stopped forcing expansion).
|
||||
await within(tree).findByText('4 sessions')
|
||||
|
||||
const initialLabel = 'Fixture 历史会话'
|
||||
const initialRowLabel = await screen.findByText(initialLabel)
|
||||
const initialRow = initialRowLabel.closest<HTMLElement>('[role="treeitem"]')
|
||||
if (initialRow === null) throw new Error('fixture session row missing')
|
||||
fireEvent.click(initialRow)
|
||||
await waitFor(() => { expect(document.title).toBe(`${initialLabel} — DeepSeek Harness`) })
|
||||
const initial = titleSurfaces(initialLabel)
|
||||
|
||||
const revisedLabel = 'Fixture 修订标题'
|
||||
const timing = (globalThis as Record<string, unknown>).__fxTiming as FixtureTiming
|
||||
act(() => { timing.appendTitle('fx-alpha', revisedLabel) })
|
||||
await waitFor(() => { expect(document.title).toBe(`${revisedLabel} — DeepSeek Harness`) })
|
||||
const revised = titleSurfaces(revisedLabel)
|
||||
|
||||
// fx-alpha carries the fixture's resident answerable approval, so the
|
||||
// approval panel has taken over the composer (the real takeover behavior);
|
||||
// answer it to restore the composer chrome before asserting the model seat.
|
||||
fireEvent.click(await screen.findByRole('button', { name: '允许一次' }))
|
||||
const modelTrigger = await screen.findByRole('button', {
|
||||
name: '选择模型,当前 DeepSeek-V4-Flash,推理等级 High',
|
||||
})
|
||||
fireEvent.click(modelTrigger)
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /Model/ }))
|
||||
fireEvent.click(screen.getByRole('menuitemradio', { name: /GPT-5/ }))
|
||||
await waitFor(() => {
|
||||
expect(modelTrigger.getAttribute('aria-label')).toBe('选择模型,当前 GPT-5,推理等级 Medium')
|
||||
})
|
||||
fireEvent.click(modelTrigger)
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /Effort/ }))
|
||||
fireEvent.click(screen.getByRole('menuitemradio', { name: 'Max' }))
|
||||
await waitFor(() => {
|
||||
expect(modelTrigger.getAttribute('aria-label')).toBe('选择模型,当前 GPT-5,推理等级 Max')
|
||||
})
|
||||
|
||||
// fx-alpha starts in the running state. Selecting above is intentionally
|
||||
// allowed for the next turn; stop the fixture's resident run before sending
|
||||
// the route-report prompt.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Stop generating' }))
|
||||
const composer = await screen.findByPlaceholderText('Message the agent')
|
||||
fireEvent.change(composer, { target: { value: 'report model' } })
|
||||
fireEvent.keyDown(composer, { key: 'Enter' })
|
||||
await screen.findByText('当前模型:openai/gpt-5 · 推理等级:max', {}, { timeout: 10_000 })
|
||||
|
||||
await expect(`${JSON.stringify({ initial, revised }, null, 2)}\n`)
|
||||
.toMatchFileSnapshot('./snapshots/session-title.json')
|
||||
})
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/sidebar-scrollbar', import.meta.url))
|
||||
@@ -275,7 +275,7 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
||||
browser = await chromium.launch()
|
||||
// Shorter than the other scenarios' 1000px so SEED_COUNT rows overflow
|
||||
// the list with room to spare.
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 800 } })
|
||||
page = await newEnglishPage(browser, 800)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
115
apps/web/tests/skill-invocation-policy.e2e.ts
Normal file
115
apps/web/tests/skill-invocation-policy.e2e.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
// Web e2e scenario: the real host filters skill.list to the model-and-user
|
||||
// intersection before the browser slash source renders candidates. A real
|
||||
// chromium connects a fresh workspace seeded with all four policy quadrants;
|
||||
// no model call is issued, so a stray stream fails loud on the open LLM seam.
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
assertFixtureInventory,
|
||||
captureStableAria,
|
||||
compareOrRefreshGolden,
|
||||
launchWebScaffold,
|
||||
watchConsole,
|
||||
webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/skill-invocation-policy', import.meta.url))
|
||||
const MENU_EXPECTED = join(SNAPSHOT_DIR, 'menu.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
interface SeedSkill {
|
||||
name: string
|
||||
description: string
|
||||
frontmatter: string
|
||||
}
|
||||
|
||||
const SKILLS: readonly SeedSkill[] = [
|
||||
{
|
||||
name: 'policy-shared',
|
||||
description: 'Available to both model and user invocation',
|
||||
frontmatter: '',
|
||||
},
|
||||
{
|
||||
name: 'policy-model-only',
|
||||
description: 'Available only to model invocation',
|
||||
frontmatter: 'user-invocable: false\n',
|
||||
},
|
||||
{
|
||||
name: 'policy-user-only',
|
||||
description: 'Available only to user invocation',
|
||||
frontmatter: 'disable-model-invocation: true\n',
|
||||
},
|
||||
{
|
||||
name: 'policy-trusted-only',
|
||||
description: 'Available only to trusted internal callers',
|
||||
frontmatter: 'disable-model-invocation: true\nuser-invocable: false\n',
|
||||
},
|
||||
]
|
||||
|
||||
async function seedSkills(workspaceCwd: string): Promise<void> {
|
||||
for (const skill of SKILLS) {
|
||||
const directory = join(workspaceCwd, 'workspace', '.agents', 'skills', skill.name)
|
||||
await mkdir(directory, { recursive: true })
|
||||
const policyLines = skill.frontmatter === '' ? [] : skill.frontmatter.trimEnd().split('\n')
|
||||
await writeFile(join(directory, 'SKILL.md'), [
|
||||
'---',
|
||||
`name: ${skill.name}`,
|
||||
`description: ${skill.description}`,
|
||||
...policyLines,
|
||||
'---',
|
||||
'',
|
||||
`# ${skill.name}`,
|
||||
'',
|
||||
].join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
describe('web e2e: skill invocation policy through the real host', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
await seedSkills(scaffold.workspaceCwd)
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('renders only the model-and-user intersection in slash candidates', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-skill-invocation-policy'))
|
||||
const input = page.locator('textarea').first()
|
||||
await input.fill('/policy')
|
||||
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
|
||||
await expect.poll(
|
||||
() => menu.getByRole('option', { name: /policy-shared/ }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
|
||||
expect(await menu.getByRole('option', { name: /policy-model-only/ }).count()).toBe(0)
|
||||
expect(await menu.getByRole('option', { name: /policy-user-only/ }).count()).toBe(0)
|
||||
expect(await menu.getByRole('option', { name: /policy-trusted-only/ }).count()).toBe(0)
|
||||
|
||||
const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['menu.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -1,190 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
// Assembled keyless snapshot of the slash/input/session convergence under the
|
||||
// agent-parity model: the New Session view state locks the composer until a
|
||||
// Workspace is picked (connectWorkspace materializes the full Session+Agent),
|
||||
// the '/' menu serves the session's wire command catalog (sessions are always
|
||||
// agent-backed — no draft/materialized split), a leadingInput command claims,
|
||||
// submits over the wire, and notices its result, and the SAME composer
|
||||
// textarea then carries the first plain send, whose ACCEPTANCE (not attempt)
|
||||
// flips blank and surfaces the session in lists. This is the user-visible
|
||||
// acceptance anchor — package mocks do not substitute for the assembled
|
||||
// application transcript.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
|
||||
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
||||
|
||||
const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
|
||||
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-slash', dir: 'ui-slash', url: '/plugins/ui-slash.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout', '@deepseek-ai/dsh-client-ui-slash'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-command', dir: 'ui-command', url: '/plugins/ui-command.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash', '@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-skill', dir: 'ui-skill', url: '/plugins/ui-skill.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-subagent', dir: 'ui-subagent', url: '/plugins/ui-subagent.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash'] },
|
||||
{
|
||||
id: '@deepseek-ai/dsh-client-ui-workspace',
|
||||
dir: 'ui-workspace',
|
||||
url: '/plugins/ui-workspace.js',
|
||||
rev: 'fx',
|
||||
inject: [
|
||||
'@deepseek-ai/dsh-client-runtime',
|
||||
'@deepseek-ai/dsh-client-ui-conversation',
|
||||
'@deepseek-ai/dsh-client-ui-sidebar',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const bundles = new Map(PLUGINS.map(plugin => [
|
||||
plugin.url,
|
||||
readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
|
||||
]))
|
||||
|
||||
interface FixtureWindow extends Window {
|
||||
__DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
|
||||
__ModuleLoader__?: unknown
|
||||
}
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
const win = window as FixtureWindow
|
||||
let unmount: (() => void) | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => { callback(0) }, 0) as unknown as number)
|
||||
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => { unmount?.() })
|
||||
unmount = undefined
|
||||
cleanup()
|
||||
delete win.__DSH_BOOT__
|
||||
delete win.__ModuleLoader__
|
||||
delete (globalThis as Record<string, unknown>).__fxTiming
|
||||
document.body.innerHTML = ''
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** Boot the complete built client graph against one keyless fixture branch. */
|
||||
function boot(search: string): void {
|
||||
history.replaceState(null, '', `/${search}`)
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
|
||||
act(() => {
|
||||
const entry = new AppWebEntry(root, {
|
||||
fetchBundle: (url) => {
|
||||
const code = bundles.get(url)
|
||||
return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
|
||||
},
|
||||
executeBundle: (code) => { (0, eval)(code) },
|
||||
})
|
||||
void entry.run()
|
||||
unmount = () => { entry.dispose() }
|
||||
})
|
||||
}
|
||||
|
||||
/** Collapse decorative whitespace while preserving the text a user sees. */
|
||||
function visibleText(element: Element): string {
|
||||
return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** Type into the machine-driven composer and let the change echo back. */
|
||||
async function typeComposer(composer: HTMLTextAreaElement, value: string): Promise<void> {
|
||||
fireEvent.change(composer, { target: { value } })
|
||||
await waitFor(() => { expect(composer.value).toBe(value) })
|
||||
}
|
||||
|
||||
it('locked view state, connectWorkspace unlock, /echo claim chain, and blank-on-acceptance ride one resident composer', async () => {
|
||||
boot('?fixture=empty')
|
||||
|
||||
// View state: no session entity — the composer renders locked; only the
|
||||
// workspace picker is live.
|
||||
const locked = await screen.findByPlaceholderText<HTMLTextAreaElement>(
|
||||
'Choose a workspace to start', {}, { timeout: 10_000 },
|
||||
)
|
||||
expect(locked.disabled).toBe(true)
|
||||
|
||||
// Pick (create) a Workspace: connectWorkspace materializes the full
|
||||
// Session+Agent and the provider swaps in the live blank-session hero.
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Choose workspace' })
|
||||
.find(el => el.getAttribute('aria-haspopup') === 'menu')!)
|
||||
fireEvent.click(await screen.findByRole('menuitem', { name: 'Create a new workspace' }))
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Create a new workspace' })
|
||||
fireEvent.change(within(dialog).getByRole('textbox', { name: 'New workspace name' }), {
|
||||
target: { value: 'nova' },
|
||||
})
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Create workspace' }))
|
||||
|
||||
const composer = await screen.findByPlaceholderText<HTMLTextAreaElement>(
|
||||
'Describe what you want to build', {}, { timeout: 10_000 },
|
||||
)
|
||||
expect(composer.disabled).toBe(false)
|
||||
|
||||
// '/' opens the menu with the session's wire command catalog (the session
|
||||
// is agent-backed from birth — the catalog is the single-address list).
|
||||
await typeComposer(composer, '/')
|
||||
const menu = await screen.findByRole('listbox', { name: 'Trigger suggestions' })
|
||||
await waitFor(() => { expect(visibleText(menu)).toContain('echo') })
|
||||
const menuText = visibleText(menu)
|
||||
|
||||
// Pick /echo (leadingInput): the claim token lands in the same textarea.
|
||||
fireEvent.mouseDown(screen.getByRole('option', { name: /echo/ }))
|
||||
await waitFor(() => { expect(composer.value).toBe('/echo ') })
|
||||
|
||||
// Type args and submit: the claim executes over the wire and notices its
|
||||
// result; the token is consumed and the draft returns to plain text.
|
||||
await typeComposer(composer, '/echo hello parser')
|
||||
fireEvent.keyDown(composer, { key: 'Enter' })
|
||||
await screen.findByText('hello parser', {}, { timeout: 10_000 })
|
||||
await waitFor(() => { expect(composer.value).toBe('') })
|
||||
|
||||
// Slash execution does not flip blank: the selected row remains New Session.
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
expect(within(tree).getByText('1 session')).toBeDefined()
|
||||
expect(within(tree).getByText('New Session')).toBeDefined()
|
||||
|
||||
// First plain send through the SAME textarea: acceptance logs the user
|
||||
// message and converts the existing sidebar row out of blank.
|
||||
const before = composer
|
||||
await typeComposer(composer, 'build me a parser')
|
||||
fireEvent.keyDown(composer, { key: 'Enter' })
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Let's start building")).toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
|
||||
const after = document.querySelector('textarea')
|
||||
|
||||
expect({
|
||||
menuHadEcho: menuText.includes('echo'),
|
||||
menuHadCompact: menuText.includes('compact'),
|
||||
composerSurvivedConversion: after === before,
|
||||
sessionListed: visibleText(within(tree).getByText('1 session').closest('[role="treeitem"]')!),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"composerSurvivedConversion": true,
|
||||
"menuHadCompact": true,
|
||||
"menuHadEcho": true,
|
||||
"sessionListed": "nova1 session",
|
||||
}
|
||||
`)
|
||||
})
|
||||
@@ -24,7 +24,7 @@ import { pathToFileURL } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { REPO_ROOT, connectFreshWorkspace, probeFreePort, requireDist, saveFailureShot } from './support.ts'
|
||||
import { REPO_ROOT, connectFreshWorkspace, newEnglishPage, probeFreePort, requireDist, saveFailureShot } from './support.ts'
|
||||
|
||||
function waitForReadyLine(child: ChildProcess): Promise<string> {
|
||||
return new Promise((resolveReady, reject) => {
|
||||
@@ -357,22 +357,26 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
requireDist()
|
||||
sessionsDir = mkdtempSync(join(tmpdir(), 'dsh-web-w5-'))
|
||||
const port = await probeFreePort()
|
||||
// tsx boot mirrors demo:web — lib/ may be unbuilt in this worktree. cwd is a
|
||||
// temp dir (persistenceRoot is cwd-relative), so tsx needs the repo's loader
|
||||
// and tsconfig paths pointed at explicitly.
|
||||
// tsx boot mirrors demo:web — lib/ may be unbuilt in this worktree. Isolate
|
||||
// the global Harness home inside the temp world; tsx also needs the repo's
|
||||
// loader and tsconfig paths pointed at explicitly.
|
||||
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
|
||||
child = spawn(
|
||||
process.execPath,
|
||||
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', String(port)],
|
||||
{
|
||||
cwd: sessionsDir,
|
||||
env: { ...process.env, TSX_TSCONFIG_PATH: join(REPO_ROOT, 'tsconfig.json') },
|
||||
env: {
|
||||
...process.env,
|
||||
DSH_HOME: join(sessionsDir, '.dsh'),
|
||||
TSX_TSCONFIG_PATH: join(REPO_ROOT, 'tsconfig.json'),
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
baseUrl = (await waitForReadyLine(child)).replace('0.0.0.0', '127.0.0.1')
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
page.on('pageerror', e => pageErrors.push(String(e)))
|
||||
await page.goto(baseUrl, { waitUntil: 'load' })
|
||||
}, 120_000)
|
||||
@@ -440,17 +444,17 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
await screen(page, '04-round-complete')
|
||||
}, 150_000)
|
||||
|
||||
it('4 view tabs: Chat / Trajectory / Waterfall all switch', async () => {
|
||||
it('view tabs: Chat and Trajectory switch', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'w5-tabs'))
|
||||
await page.locator('button', { hasText: /Trajectory/i }).first().click()
|
||||
await screen(page, '05-trajectory-tab')
|
||||
await page.locator('button', { hasText: /Waterfall/i }).first().click()
|
||||
await screen(page, '06-waterfall-tab')
|
||||
await page.getByLabel('Trajectory timeline').waitFor()
|
||||
await expect.poll(() => page.getByRole('tab', { name: 'Waterfall' }).count()).toBe(0)
|
||||
await page.locator('button', { hasText: /^Chat$/i }).first().click()
|
||||
await screen(page, '07-back-to-chat')
|
||||
})
|
||||
|
||||
it('5 bash differential rendering: tool row click leaves the details column collapsed', async () => {
|
||||
it('5 bash differential rendering: tool row click leaves the default details column open', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'w5-tool-details'))
|
||||
const input = page.locator('textarea').first()
|
||||
await input.fill('请用 bash 工具运行命令 echo w5marker 然后告诉我结果')
|
||||
@@ -462,11 +466,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
const toolRow = page.locator('[data-sample="bash-global"]')
|
||||
await toolRow.waitFor({ timeout: 120_000 })
|
||||
await screen(page, '08-bash-round')
|
||||
expect(await detailsTrack(page)).toBe(0)
|
||||
expect(await detailsTrack(page)).toBe(360)
|
||||
await toolRow.click()
|
||||
// Tool rows no longer drive layout.openDetails; the column stays closed.
|
||||
expect(await detailsTrack(page)).toBe(0)
|
||||
await screen(page, '09-details-closed')
|
||||
// Tool rows no longer drive layout.openDetails; the default column stays open.
|
||||
expect(await detailsTrack(page)).toBe(360)
|
||||
await screen(page, '09-details-open')
|
||||
}, 150_000)
|
||||
|
||||
it('6 sidebar drag widens the column and resets across reload', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785013630399,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785013630411,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785013630411,"data":{"content":[{"type":"text","text":"Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785013630411,"data":{"content":[{"type":"text","text":"Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"1c4a6a0f-a7bd-4642-919d-6ed7395df98b"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785013630418,"data":{"title":"Using ONE run_code program: run","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785013630479,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785013630480,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -12,13 +12,13 @@
|
||||
{"type":"assistant/chunk","seq":202,"time":1785013633103,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":203,"time":1785013633104,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75}}}}
|
||||
{"type":"assistant/chunk","seq":204,"time":1785013633104,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":205,"time":1785013633108,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Runs bash to echo \"CODE_ROUND_OK\"\n2. Tries to read a file \"missing.txt\" and catches the error\n3. Returns an object with both outcomes\n4. They also want me to reply \"DONE\" and stop after\n\nLet me write this program."},{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":205,"time":1785013633108,"data":{"turn":1,"step":1,"usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Runs bash to echo \"CODE_ROUND_OK\"\n2. Tries to read a file \"missing.txt\" and catches the error\n3. Returns an object with both outcomes\n4. They also want me to reply \"DONE\" and stop after\n\nLet me write this program."},{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"0d217af9-3bb3-425b-b53e-7ab0366f0c66"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":206,"time":1785013633108,"data":{"turn":1,"step":1,"callId":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}}
|
||||
{"type":"tool/code-dispatch-start","seq":207,"time":1785013633173,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"}}}
|
||||
{"type":"tool/code-dispatch","seq":208,"time":1785013633196,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"},"isError":false,"content":[{"type":"text","text":"CODE_ROUND_OK\n"}]}}
|
||||
{"type":"tool/code-dispatch-start","seq":209,"time":1785013633197,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"}}}
|
||||
{"type":"tool/code-dispatch","seq":210,"time":1785013633198,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"},"isError":true,"content":[{"type":"text","text":"Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"}]}}
|
||||
{"type":"tool/result","seq":211,"time":1785013633201,"data":{"turn":1,"step":1,"callId":"call_00_6VNoF1gDSerTBKoCfYSH3765","content":[{"type":"text","text":"{\n \"bash\": \"CODE_ROUND_OK\",\n \"readError\": {\n \"toolName\": \"read\",\n \"message\": \"cannot read \\\"{{cwd}}/workspace/missing.txt\\\": not found\"\n }\n}"}],"isError":false},"sourceEventSeqs":[206],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":211,"time":1785013633201,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_6VNoF1gDSerTBKoCfYSH3765"},"content":[{"type":"tool-result","toolCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","content":[{"type":"text","text":"{\n \"bash\": \"CODE_ROUND_OK\",\n \"readError\": {\n \"toolName\": \"read\",\n \"message\": \"cannot read \\\"{{cwd}}/workspace/missing.txt\\\": not found\"\n }\n}"}],"isError":false}],"role":"user","id":"1580d4a8-5760-415f-984f-f93927271d3f"}},"sourceEventSeqs":[206],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":212,"time":1785013633204,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":213,"time":1785013633207,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":214,"time":1785013633985,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -30,6 +30,6 @@
|
||||
{"type":"assistant/chunk","seq":233,"time":1785013634223,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":234,"time":1785013634223,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":196,"outputTokens":17,"cacheReadTokens":8576,"reasoningTokens":14}}}}
|
||||
{"type":"assistant/chunk","seq":235,"time":1785013634223,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":236,"time":1785013634224,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The program ran successfully. Let me now reply DONE as instructed."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":196,"outputTokens":17,"cacheReadTokens":8576,"reasoningTokens":14}},"sourceEventSeqs":[214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":236,"time":1785013634224,"data":{"turn":1,"step":2,"usage":{"inputTokens":196,"outputTokens":17,"cacheReadTokens":8576,"reasoningTokens":14},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The program ran successfully. Let me now reply DONE as instructed."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"3037ec34-6c2a-4e01-91eb-a0ef89ce0d15"}},"sourceEventSeqs":[214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":237,"time":1785013634225,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":238,"time":1785013634225,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,15 +11,11 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- 'button "Think The user wants me to write a single `run_code` program that:"':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to write a single `run_code` program that:"
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
@@ -37,16 +32,13 @@
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} cache hit 52% · 17,490 tokens · 1 turns · 2 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 52% Input 17.2K tok · Output 252 tok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785157562825,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785157562881,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785157562882,"data":{"content":[{"type":"text","text":"Use only Cordis tools. First call cordis_inspect with what \"temporary\". Then call cordis_mount with this exact code: \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\". Read its returned id and call cordis_unmount with that exact id. After all three calls succeed, reply exactly CORDIS_UI_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785157562882,"data":{"content":[{"type":"text","text":"Use only Cordis tools. First call cordis_inspect with what \"temporary\". Then call cordis_mount with this exact code: \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\". Read its returned id and call cordis_unmount with that exact id. After all three calls succeed, reply exactly CORDIS_UI_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"27e1fc72-20d0-4875-bf5b-d34750441f30"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785157562883,"data":{"title":"Use only Cordis tools. First","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785157562937,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785157562938,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -12,9 +12,9 @@
|
||||
{"type":"assistant/chunk","seq":100,"time":1785157565491,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":101,"time":1785157565491,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":15137,"outputTokens":128,"cacheReadTokens":1280,"reasoningTokens":81}}}}
|
||||
{"type":"assistant/chunk","seq":102,"time":1785157565491,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":103,"time":1785157565495,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to:\n1. Call `cordis_inspect` with `what: \"temporary\"`\n2. Call `cordis_mount` with the exact code provided\n3. Read the returned id and call `cordis_unmount` with that exact id\n4. Reply exactly \"CORDIS_UI_DONE\" and stop\n\nLet me start with step 1."},{"type":"tool-call","id":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":15137,"outputTokens":128,"cacheReadTokens":1280,"reasoningTokens":81}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":103,"time":1785157565495,"data":{"turn":1,"step":1,"usage":{"inputTokens":15137,"outputTokens":128,"cacheReadTokens":1280,"reasoningTokens":81},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Call `cordis_inspect` with `what: \"temporary\"`\n2. Call `cordis_mount` with the exact code provided\n3. Read the returned id and call `cordis_unmount` with that exact id\n4. Reply exactly \"CORDIS_UI_DONE\" and stop\n\nLet me start with step 1."},{"type":"tool-call","id":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"a3b56157-c6fd-43f8-bc8a-0e2a0c99a8b5"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":104,"time":1785157565496,"data":{"turn":1,"step":1,"callId":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}}
|
||||
{"type":"tool/result","seq":105,"time":1785157565500,"data":{"turn":1,"step":1,"callId":"call_00_KZk918WtlKan9pHMULIT8794","content":[{"type":"text","text":"## Temporary Plugins\nNo temporary Plugins are running. Temporary Plugins created with cordis_mount disappear when DSH restarts."}],"isError":false},"sourceEventSeqs":[104],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":105,"time":1785157565500,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_KZk918WtlKan9pHMULIT8794"},"content":[{"type":"tool-result","toolCallId":"call_00_KZk918WtlKan9pHMULIT8794","content":[{"type":"text","text":"## Temporary Plugins\nNo temporary Plugins are running. Temporary Plugins created with cordis_mount disappear when DSH restarts."}],"isError":false}],"role":"user","id":"95ae124e-f065-4aab-a9c5-bd33c7eafff1"}},"sourceEventSeqs":[104],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":106,"time":1785157565503,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":107,"time":1785157565503,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":108,"time":1785157566524,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -25,9 +25,9 @@
|
||||
{"type":"assistant/chunk","seq":157,"time":1785157567041,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":158,"time":1785157567042,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":72,"outputTokens":82,"cacheReadTokens":16512,"reasoningTokens":22}}}}
|
||||
{"type":"assistant/chunk","seq":159,"time":1785157567042,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":160,"time":1785157567043,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."},{"type":"tool-call","id":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":72,"outputTokens":82,"cacheReadTokens":16512,"reasoningTokens":22}},"sourceEventSeqs":[108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":160,"time":1785157567043,"data":{"turn":1,"step":2,"usage":{"inputTokens":72,"outputTokens":82,"cacheReadTokens":16512,"reasoningTokens":22},"message":{"role":"assistant","content":[{"type":"reasoning","text":"Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."},{"type":"tool-call","id":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"b90af675-f604-4fb4-9c27-89c91b27f4ce"}},"sourceEventSeqs":[108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":161,"time":1785157567043,"data":{"turn":1,"step":2,"callId":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}}
|
||||
{"type":"tool/result","seq":162,"time":1785157567049,"data":{"turn":1,"step":2,"callId":"call_00_OyrlxqqpnvzTe11sN5Oy2361","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"snapshot-noop\"; available until unmounted or DSH restarts)."}],"isError":false},"sourceEventSeqs":[161],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":162,"time":1785157567049,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_OyrlxqqpnvzTe11sN5Oy2361"},"content":[{"type":"tool-result","toolCallId":"call_00_OyrlxqqpnvzTe11sN5Oy2361","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"snapshot-noop\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"3148fc0a-f511-4b72-abd5-d184e974cd49"}},"sourceEventSeqs":[161],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":163,"time":1785157567050,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":164,"time":1785157567050,"data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/chunk","seq":165,"time":1785157567835,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -38,9 +38,9 @@
|
||||
{"type":"assistant/chunk","seq":204,"time":1785157568279,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":205,"time":1785157568279,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":63,"outputTokens":72,"cacheReadTokens":16640,"reasoningTokens":23}}}}
|
||||
{"type":"assistant/chunk","seq":206,"time":1785157568279,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":207,"time":1785157568280,"data":{"turn":1,"step":3,"content":[{"type":"reasoning","text":"The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."},{"type":"tool-call","id":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":63,"outputTokens":72,"cacheReadTokens":16640,"reasoningTokens":23}},"sourceEventSeqs":[165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":207,"time":1785157568280,"data":{"turn":1,"step":3,"usage":{"inputTokens":63,"outputTokens":72,"cacheReadTokens":16640,"reasoningTokens":23},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."},{"type":"tool-call","id":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"cdc5b840-4302-486a-8065-fa59bf13f2d9"}},"sourceEventSeqs":[165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":208,"time":1785157568280,"data":{"turn":1,"step":3,"callId":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}
|
||||
{"type":"tool/result","seq":209,"time":1785157568281,"data":{"turn":1,"step":3,"callId":"call_00_e38S6zeYdZGvbhecUCil6659","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false},"sourceEventSeqs":[208],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":209,"time":1785157568281,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_00_e38S6zeYdZGvbhecUCil6659"},"content":[{"type":"tool-result","toolCallId":"call_00_e38S6zeYdZGvbhecUCil6659","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"758a7b27-56e3-4997-8c4d-effc699cc5e6"}},"sourceEventSeqs":[208],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":210,"time":1785157568282,"data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","seq":211,"time":1785157568282,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":212,"time":1785157569185,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -51,6 +51,6 @@
|
||||
{"type":"assistant/chunk","seq":244,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CORDIS_UI_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":245,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":29,"outputTokens":30,"cacheReadTokens":16768,"reasoningTokens":22}}}}
|
||||
{"type":"assistant/chunk","seq":246,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":247,"time":1785157569553,"data":{"turn":1,"step":4,"content":[{"type":"reasoning","text":"All three calls succeeded. I should now reply exactly \"CORDIS_UI_DONE\" and stop."},{"type":"text","text":"CORDIS_UI_DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":29,"outputTokens":30,"cacheReadTokens":16768,"reasoningTokens":22}},"sourceEventSeqs":[212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":247,"time":1785157569553,"data":{"turn":1,"step":4,"usage":{"inputTokens":29,"outputTokens":30,"cacheReadTokens":16768,"reasoningTokens":22},"message":{"role":"assistant","content":[{"type":"reasoning","text":"All three calls succeeded. I should now reply exactly \"CORDIS_UI_DONE\" and stop."},{"type":"text","text":"CORDIS_UI_DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"71540121-7bf6-410e-975c-f5ea99d8175a"}},"sourceEventSeqs":[212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":248,"time":1785157569554,"data":{"turn":1,"step":4}}
|
||||
{"type":"turn/end","seq":249,"time":1785157569554,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Use only Cordis tools. First call cordis_inspect with what \"temporary\". Then call cordis_mount with this exact code: \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\". Read its returned id and call cordis_unmount with that exact id. After all three calls succeed, reply exactly CORDIS_UI_DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,15 +11,11 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to:":
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to:"
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
@@ -29,11 +24,6 @@
|
||||
- img
|
||||
- img
|
||||
- text: "Think Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button [expanded]:
|
||||
- img
|
||||
- text: Mount temporary Plugin typescript
|
||||
@@ -43,11 +33,6 @@
|
||||
- img
|
||||
- img
|
||||
- text: "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
@@ -61,16 +46,13 @@
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} cache hit 77% · 66,813 tokens · 1 turns · 4 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 4 steps Tool call {{duration}} Cache hit 77% Input 66.5K tok · Output 312 tok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1784973850091,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1784973850102,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1784973850103,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1784973850103,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"2c594e5a-bbcc-4c64-b5ee-8e84eb5dd949"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1784973850105,"data":{"title":"Use the bash tool to","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1784973850164,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1784973850165,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -12,9 +12,9 @@
|
||||
{"type":"assistant/chunk","seq":52,"time":1784973851493,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":53,"time":1784973851493,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":122,"outputTokens":85,"cacheReadTokens":7680,"reasoningTokens":17}}}}
|
||||
{"type":"assistant/chunk","seq":54,"time":1784973851493,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":55,"time":1784973851498,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a simple bash command and reply with \"DONE\"."},{"type":"tool-call","id":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":122,"outputTokens":85,"cacheReadTokens":7680,"reasoningTokens":17}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":55,"time":1784973851498,"data":{"turn":1,"step":1,"usage":{"inputTokens":122,"outputTokens":85,"cacheReadTokens":7680,"reasoningTokens":17},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to run a simple bash command and reply with \"DONE\"."},{"type":"tool-call","id":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"e5d8d5fb-f555-4fda-948f-9ab178145929"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":56,"time":1784973851499,"data":{"turn":1,"step":1,"callId":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}}
|
||||
{"type":"tool/result","seq":57,"time":1784973851515,"data":{"turn":1,"step":1,"callId":"call_00_BYXlxjFaalMg95YVqEeF2495","content":[{"type":"text","text":"WEB_E2E_OK\n"}],"isError":false},"sourceEventSeqs":[56],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":57,"time":1784973851515,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_BYXlxjFaalMg95YVqEeF2495"},"content":[{"type":"tool-result","toolCallId":"call_00_BYXlxjFaalMg95YVqEeF2495","content":[{"type":"text","text":"WEB_E2E_OK\n"}],"isError":false}],"role":"user","id":"215febf9-ff3c-42b8-93b7-27ce1505c1c2"}},"sourceEventSeqs":[56],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":58,"time":1784973851517,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":59,"time":1784973851518,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":60,"time":1784973852194,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -26,6 +26,6 @@
|
||||
{"type":"assistant/chunk","seq":88,"time":1784973852460,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":89,"time":1784973852460,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":97,"outputTokens":26,"cacheReadTokens":7808,"reasoningTokens":23}}}}
|
||||
{"type":"assistant/chunk","seq":90,"time":1784973852460,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":91,"time":1784973852461,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\"."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":97,"outputTokens":26,"cacheReadTokens":7808,"reasoningTokens":23}},"sourceEventSeqs":[60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":91,"time":1784973852461,"data":{"turn":1,"step":2,"usage":{"inputTokens":97,"outputTokens":26,"cacheReadTokens":7808,"reasoningTokens":23},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\"."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"61e83a98-18a9-4f1f-9151-c8783cc39901"}},"sourceEventSeqs":[60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":92,"time":1784973852461,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":93,"time":1784973852462,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,17 +11,15 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to run a simple bash command and reply with \"DONE\".":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to run a simple bash command and reply with "DONE".
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- img
|
||||
- text: Bash Echo the test string
|
||||
- text: Bash Echo the test string 已完成 workspace echo WEB_E2E_OK
|
||||
- button "复制"
|
||||
- text: WEB_E2E_OK
|
||||
- button "Think The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\".":
|
||||
- img
|
||||
- img
|
||||
@@ -32,16 +29,13 @@
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} cache hit 99% · 15,818 tokens · 1 turns · 2 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 99% Input 15.7K tok · Output 111 tok
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
- img
|
||||
- text: workspace 1 session
|
||||
- treeitem "New Session now" [selected]
|
||||
- button "设置":
|
||||
- button "Settings":
|
||||
- img
|
||||
- text: 设置
|
||||
- text: Settings
|
||||
- text: Let's start building
|
||||
- button "Choose workspace":
|
||||
- img
|
||||
@@ -28,12 +28,8 @@
|
||||
- textbox "Describe what you want to build"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with the single word LIGHTHOUSE and stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,6 +11,7 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to reply with a single word. Let me comply.":
|
||||
- img
|
||||
- img
|
||||
@@ -21,16 +21,13 @@
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} cache hit 99% · 7,810 tokens · 1 turns · 1 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps Cache hit 99% Input 7.8K tok · Output 21 tok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785015039278,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785015039291,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"ee28d2ee-08b2-4ed7-a1e9-84865f55e2af"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785015039294,"data":{"title":"Reply with the single word","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785015039362,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785015039363,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -12,6 +12,6 @@
|
||||
{"type":"assistant/chunk","seq":28,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"LIGHTHOUSE"}}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"2101eadc-3475-4c77-ae0a-0107b12c34bc"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":32,"time":1785015040246,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":33,"time":1785015040247,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,22 +11,20 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- paragraph: partial
|
||||
- text: 已停止
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} 0 tokens · 1 turns · 1 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps Input 0 tok · Output 0 tok
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,15 +11,12 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,6 +11,7 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.":
|
||||
- img
|
||||
- img
|
||||
@@ -21,16 +21,13 @@
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} cache hit 99% · 7,869 tokens · 1 turns · 1 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps Cache hit 99% Input 7.8K tok · Output 79 tok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1784998084441,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1784998084454,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1784998084454,"data":{"content":[{"type":"text","text":"Reply with a one-sentence description of event sourcing, then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1784998084454,"data":{"content":[{"type":"text","text":"Reply with a one-sentence description of event sourcing, then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"dc9a43b2-63a3-49bc-97e9-9dfb6465f6c1"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1784998084457,"data":{"title":"Reply with a one-sentence description","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1784998084519,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1784998084520,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -12,6 +12,6 @@
|
||||
{"type":"assistant/chunk","seq":86,"time":1784998085814,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures."}}}}
|
||||
{"type":"assistant/chunk","seq":87,"time":1784998085814,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":110,"outputTokens":79,"cacheReadTokens":7680,"reasoningTokens":31}}}}
|
||||
{"type":"assistant/chunk","seq":88,"time":1784998085814,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":89,"time":1784998085818,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls."},{"type":"text","text":"Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":110,"outputTokens":79,"cacheReadTokens":7680,"reasoningTokens":31}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":89,"time":1784998085818,"data":{"turn":1,"step":1,"usage":{"inputTokens":110,"outputTokens":79,"cacheReadTokens":7680,"reasoningTokens":31},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls."},{"type":"text","text":"Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures."}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"3396618c-56a2-4f97-a9c7-2a3cbb16a3c8"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":90,"time":1784998085820,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":91,"time":1784998085821,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -17,11 +16,6 @@
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- img
|
||||
- text: Read
|
||||
- button "a.txt"
|
||||
@@ -37,16 +31,13 @@
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} cache hit 98% · 15,962 tokens · 1 turns · 2 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 98% Input 15.8K tok · Output 135 tok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785011380476,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785011380489,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785011380490,"data":{"content":[{"type":"text","text":"NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785011380490,"data":{"content":[{"type":"text","text":"NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"7dcfe547-5555-4e5d-b2a0-3f9d2e0836a9"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785011380492,"data":{"title":"NavScenario: first run bash to","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785011380549,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785011380550,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -18,13 +18,13 @@
|
||||
{"type":"assistant/chunk","seq":130,"time":1785011382087,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":3,"block":{"type":"tool-call","id":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":131,"time":1785011382087,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":140,"outputTokens":197,"cacheReadTokens":7680,"reasoningTokens":66}}}}
|
||||
{"type":"assistant/chunk","seq":132,"time":1785011382087,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":133,"time":1785011382091,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to follow a specific navigation scenario. Let me:\n\n1. Run bash to print \"NAVIGATION_OK\"\n2. Read nav-a.md and nav-b.md in two read calls in ONE message\n3. Reply with \"FIRST_DONE\"\n\nLet me start with the bash command and the reads."},{"type":"tool-call","id":"call_00_kFKHaEXcTYEex0iDZw0C2432","name":"bash","arguments":"{\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"}"},{"type":"tool-call","id":"call_01_tK4hIIRVTMgAvdzs7m9j6212","name":"read","arguments":"{\"file_path\": \"nav-a.md\"}"},{"type":"tool-call","id":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":140,"outputTokens":197,"cacheReadTokens":7680,"reasoningTokens":66}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":133,"time":1785011382091,"data":{"turn":1,"step":1,"usage":{"inputTokens":140,"outputTokens":197,"cacheReadTokens":7680,"reasoningTokens":66},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to follow a specific navigation scenario. Let me:\n\n1. Run bash to print \"NAVIGATION_OK\"\n2. Read nav-a.md and nav-b.md in two read calls in ONE message\n3. Reply with \"FIRST_DONE\"\n\nLet me start with the bash command and the reads."},{"type":"tool-call","id":"call_00_kFKHaEXcTYEex0iDZw0C2432","name":"bash","arguments":"{\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"}"},{"type":"tool-call","id":"call_01_tK4hIIRVTMgAvdzs7m9j6212","name":"read","arguments":"{\"file_path\": \"nav-a.md\"}"},{"type":"tool-call","id":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"83b27ad0-4286-478f-92ef-3bc82bf0589b"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":134,"time":1785011382092,"data":{"turn":1,"step":1,"callId":"call_00_kFKHaEXcTYEex0iDZw0C2432","name":"bash","arguments":"{\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"}"}}
|
||||
{"type":"tool/result","seq":135,"time":1785011382105,"data":{"turn":1,"step":1,"callId":"call_00_kFKHaEXcTYEex0iDZw0C2432","content":[{"type":"text","text":"NAVIGATION_OK\n"}],"isError":false},"sourceEventSeqs":[134],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":135,"time":1785011382105,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_kFKHaEXcTYEex0iDZw0C2432"},"content":[{"type":"tool-result","toolCallId":"call_00_kFKHaEXcTYEex0iDZw0C2432","content":[{"type":"text","text":"NAVIGATION_OK\n"}],"isError":false}],"role":"user","id":"d6e29f9e-8177-4f12-b1b6-2eda4c8f5c31"}},"sourceEventSeqs":[134],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":136,"time":1785011382105,"data":{"turn":1,"step":1,"callId":"call_01_tK4hIIRVTMgAvdzs7m9j6212","name":"read","arguments":"{\"file_path\": \"nav-a.md\"}"}}
|
||||
{"type":"tool/call","seq":137,"time":1785011382106,"data":{"turn":1,"step":1,"callId":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}}
|
||||
{"type":"tool/result","seq":138,"time":1785011382113,"data":{"turn":1,"step":1,"callId":"call_01_tK4hIIRVTMgAvdzs7m9j6212","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-a.md</path>\n<type>file</type>\n<content>\n1: # alpha nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[136],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":139,"time":1785011382114,"data":{"turn":1,"step":1,"callId":"call_02_k8Z6wGirxfnW96Iv8mkz9224","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-b.md</path>\n<type>file</type>\n<content>\n1: # beta nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[137],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":138,"time":1785011382113,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_01_tK4hIIRVTMgAvdzs7m9j6212"},"content":[{"type":"tool-result","toolCallId":"call_01_tK4hIIRVTMgAvdzs7m9j6212","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-a.md</path>\n<type>file</type>\n<content>\n1: # alpha nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"efca7baf-cbb8-462f-ad75-580357b58676"}},"sourceEventSeqs":[136],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":139,"time":1785011382114,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_02_k8Z6wGirxfnW96Iv8mkz9224"},"content":[{"type":"tool-result","toolCallId":"call_02_k8Z6wGirxfnW96Iv8mkz9224","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-b.md</path>\n<type>file</type>\n<content>\n1: # beta nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"e1c70190-9e68-429d-963d-8a30ab779ddd"}},"sourceEventSeqs":[137],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":140,"time":1785011382117,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":141,"time":1785011382118,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":142,"time":1785011382673,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -35,11 +35,11 @@
|
||||
{"type":"assistant/chunk","seq":200,"time":1785011383090,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":201,"time":1785011383090,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":349,"outputTokens":56,"cacheReadTokens":7808,"reasoningTokens":51}}}}
|
||||
{"type":"assistant/chunk","seq":202,"time":1785011383090,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":203,"time":1785011383091,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"All three calls succeeded:\n1. bash printed \"NAVIGATION_OK\"\n2. nav-a.md contains \"# alpha nav\"\n3. nav-b.md contains \"# beta nav\"\n\nNow I need to reply with the single word \"FIRST_DONE\"."},{"type":"text","text":"FIRST_DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":349,"outputTokens":56,"cacheReadTokens":7808,"reasoningTokens":51}},"sourceEventSeqs":[142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":203,"time":1785011383091,"data":{"turn":1,"step":2,"usage":{"inputTokens":349,"outputTokens":56,"cacheReadTokens":7808,"reasoningTokens":51},"message":{"role":"assistant","content":[{"type":"reasoning","text":"All three calls succeeded:\n1. bash printed \"NAVIGATION_OK\"\n2. nav-a.md contains \"# alpha nav\"\n3. nav-b.md contains \"# beta nav\"\n\nNow I need to reply with the single word \"FIRST_DONE\"."},{"type":"text","text":"FIRST_DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"0115844e-fac0-4985-9ba7-dcf1dde63b83"}},"sourceEventSeqs":[142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":204,"time":1785011383091,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":205,"time":1785011383092,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"turn/start","seq":206,"time":1785011383106,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":207,"time":1785011383107,"data":{"content":[{"type":"text","text":"Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":207,"time":1785011383107,"data":{"content":[{"type":"text","text":"Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"1d7a8563-4ae6-49f3-9a65-41f3df300a75"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":208,"time":1785011383107,"data":{"turn":2,"step":1}}
|
||||
{"type":"assistant/chunk","seq":209,"time":1785011383497,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":210,"time0":1785011383497,"data":{"turn":2,"step":1,"index":0,"dt":[125,23,1,0,0,88,0,0,5,0,1,0,0,7,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," a"," specific"," format","."," Let"," me"," do"," that","."]}}
|
||||
@@ -49,6 +49,6 @@
|
||||
{"type":"assistant/chunk","seq":247,"time":1785011383903,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"## Navigation Summary\n\n- alpha nav\n- beta nav\n\n```\necho WATERFALL\n```"}}}}
|
||||
{"type":"assistant/chunk","seq":248,"time":1785011383903,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":141,"outputTokens":36,"cacheReadTokens":8064,"reasoningTokens":16}}}}
|
||||
{"type":"assistant/chunk","seq":249,"time":1785011383903,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":250,"time":1785011383904,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a specific format. Let me do that."},{"type":"text","text":"## Navigation Summary\n\n- alpha nav\n- beta nav\n\n```\necho WATERFALL\n```"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":141,"outputTokens":36,"cacheReadTokens":8064,"reasoningTokens":16}},"sourceEventSeqs":[209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":250,"time":1785011383904,"data":{"turn":2,"step":1,"usage":{"inputTokens":141,"outputTokens":36,"cacheReadTokens":8064,"reasoningTokens":16},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with a specific format. Let me do that."},{"type":"text","text":"## Navigation Summary\n\n- alpha nav\n- beta nav\n\n```\necho WATERFALL\n```"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"70b38db9-38a2-4f9f-8094-1bd799f5f270"}},"sourceEventSeqs":[209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":251,"time":1785011383904,"data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","seq":252,"time":1785011383904,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
- text: 已完成 {{workspace}} echo NAVIGATION_OK
|
||||
- button "复制"
|
||||
- text: NAVIGATION_OK
|
||||
@@ -1 +1,53 @@
|
||||
- text: "Turn 1 Message {{duration}} #1 User NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop. +{{duration}} Step 1 {{duration}} bash read×2 #2 Tool bash · {\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"} +{{duration}} #3 Tool read · {\"file_path\": \"nav-a.md\"} +{{duration}} #4 Tool read · {\"file_path\": \"nav-b.md\"} +{{duration}} Step 2 {{duration}} #5 Message FIRST_DONE 349 56 51 +{{duration}} Turn 2 Message {{duration}} #6 User Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop. +{{duration}} Step 1 {{duration}} #7 Message ## Navigation Summary - alpha nav - beta nav ``` echo WATERFALL ``` 141 36 16 +{{duration}}"
|
||||
- toolbar "Trajectory toolbar":
|
||||
- button "Use actual duration": Duration
|
||||
- button "Collapse turns": Turns
|
||||
- button "Collapse calls": Calls
|
||||
- img
|
||||
- searchbox "Search trajectory"
|
||||
- region "Trajectory timeline"
|
||||
- table:
|
||||
- rowgroup:
|
||||
- row "SYSTEM, Initial System Prompt":
|
||||
- cell "SYSTEM"
|
||||
- cell "Initial System Prompt"
|
||||
- 'row "USER, NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."':
|
||||
- cell "Turn 1 USER"
|
||||
- 'cell "NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."'
|
||||
- 'row "Request 1, ASSISTANT, The user wants me to follow a specific navigation scenario. Let me: Run bash to print \"NAVIGATION_OK\" Read nav-a.md and nav-b.md in two read calls in ONE message Reply with \"FIRST_DONE\" Let me start with the bash command and the reads."':
|
||||
- 'cell "Request #1 ASSISTANT"':
|
||||
- 'button "Request #1"'
|
||||
- text: ASSISTANT
|
||||
- 'cell "The user wants me to follow a specific navigation scenario. Let me: Run bash to print \"NAVIGATION_OK\" Read nav-a.md and nav-b.md in two read calls in ONE message Reply with \"FIRST_DONE\" Let me start with the bash command and the reads."'
|
||||
- 'row "TOOL, bash {\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"}" [selected]':
|
||||
- cell "TOOL"
|
||||
- 'cell "bash{\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"} → NAVIGATION_OK"'
|
||||
- 'row "TOOL, read {\"file_path\": \"nav-a.md\"}"':
|
||||
- cell "TOOL"
|
||||
- 'cell "read{\"file_path\": \"nav-a.md\"} → <path>{{cwd}}/nav-a.md</path> <type>file</type> <content> 1: # alpha nav (End of file - total 1 lines) </content>"'
|
||||
- 'row "TOOL, read {\"file_path\": \"nav-b.md\"}"':
|
||||
- cell "TOOL"
|
||||
- 'cell "read{\"file_path\": \"nav-b.md\"} → <path>{{cwd}}/nav-b.md</path> <type>file</type> <content> 1: # beta nav (End of file - total 1 lines) </content>"'
|
||||
- row "Request 2, ASSISTANT, FIRST_DONE":
|
||||
- 'cell "Request #2 ASSISTANT"':
|
||||
- 'button "Request #2"'
|
||||
- text: ASSISTANT
|
||||
- cell "FIRST_DONE"
|
||||
- 'row "USER, Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."':
|
||||
- cell "Turn 2 USER"
|
||||
- 'cell "Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."'
|
||||
- row "Request 3, ASSISTANT, Navigation Summary alpha nav beta nav echo WATERFALL":
|
||||
- 'cell "Request #3 ASSISTANT"':
|
||||
- 'button "Request #3"'
|
||||
- text: ASSISTANT
|
||||
- cell "Navigation Summary alpha nav beta nav echo WATERFALL"
|
||||
- complementary "Event details":
|
||||
- separator "Resize event details"
|
||||
- text: TOOL Turn 1 · Step 1
|
||||
- button "Close details"
|
||||
- tablist "Event details":
|
||||
- tab "Summary"
|
||||
- tab "Payload"
|
||||
- tab "Result" [selected]
|
||||
- tab "Schema"
|
||||
- tab "Timing"
|
||||
- tabpanel "Result": NAVIGATION_OK
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- text: 3 turns · 3 steps · 3 tool calls turn 0 turn 1 turn 2
|
||||
@@ -4,14 +4,14 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Use the ask_user_question tool to ask me exactly one question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" After I answer, reply with the single word DONE and stop."
|
||||
- text: "Use the ask_user_question tool to ask me exactly one question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" After I answer, reply with the single word DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
@@ -25,16 +25,17 @@
|
||||
- img
|
||||
- text: Think The user answered "Blue". I should now reply with the single word DONE and stop.
|
||||
- paragraph: DONE
|
||||
- text: cache hit 95% · 8,769 tokens · 1 turns · 2 steps
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 95% Input 8.6K tok · Output 180 tok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785150167878,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785150167924,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785150167925,"data":{"content":[{"type":"text","text":"Use the ask_user_question tool to ask me exactly one question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" After I answer, reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785150167925,"data":{"content":[{"type":"text","text":"Use the ask_user_question tool to ask me exactly one question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" After I answer, reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"6deba879-8787-4853-a5f2-0d108a08eb2d"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785150167927,"data":{"title":"Use the ask_user_question tool to","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785150167928,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785150167929,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
@@ -12,9 +12,9 @@
|
||||
{"type":"assistant/chunk","seq":128,"time":1785150169308,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_Cijldc88LYmVPCXYUsRq1617","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"color\", \"question\": \"Which color do you prefer?\", \"header\": \"Pick one\", \"options\": [{\"label\": \"Blue\", \"description\": \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\"}, {\"label\": \"Green\", \"description\": \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\"}]}]}"}}}}
|
||||
{"type":"assistant/chunk","seq":129,"time":1785150169308,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":113,"outputTokens":158,"cacheReadTokens":4096,"reasoningTokens":22}}}}
|
||||
{"type":"assistant/chunk","seq":130,"time":1785150169308,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":131,"time":1785150169311,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that."},{"type":"tool-call","id":"call_00_Cijldc88LYmVPCXYUsRq1617","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"color\", \"question\": \"Which color do you prefer?\", \"header\": \"Pick one\", \"options\": [{\"label\": \"Blue\", \"description\": \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\"}, {\"label\": \"Green\", \"description\": \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\"}]}]}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":113,"outputTokens":158,"cacheReadTokens":4096,"reasoningTokens":22}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":131,"time":1785150169311,"data":{"turn":1,"step":1,"usage":{"inputTokens":113,"outputTokens":158,"cacheReadTokens":4096,"reasoningTokens":22},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that."},{"type":"tool-call","id":"call_00_Cijldc88LYmVPCXYUsRq1617","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"color\", \"question\": \"Which color do you prefer?\", \"header\": \"Pick one\", \"options\": [{\"label\": \"Blue\", \"description\": \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\"}, {\"label\": \"Green\", \"description\": \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\"}]}]}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"cdb1676c-e781-41ee-8f28-a3595371d729"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":132,"time":1785150169312,"data":{"turn":1,"step":1,"callId":"call_00_Cijldc88LYmVPCXYUsRq1617","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"color\", \"question\": \"Which color do you prefer?\", \"header\": \"Pick one\", \"options\": [{\"label\": \"Blue\", \"description\": \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\"}, {\"label\": \"Green\", \"description\": \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\"}]}]}"}}
|
||||
{"type":"tool/result","seq":133,"time":1785150169787,"data":{"turn":1,"step":1,"callId":"call_00_Cijldc88LYmVPCXYUsRq1617","content":[{"type":"text","text":"{\"answers\":[{\"id\":\"color\",\"selected\":[\"Blue\"]}]}"}],"isError":false},"sourceEventSeqs":[132],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":133,"time":1785150169787,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_Cijldc88LYmVPCXYUsRq1617"},"content":[{"type":"tool-result","toolCallId":"call_00_Cijldc88LYmVPCXYUsRq1617","content":[{"type":"text","text":"{\"answers\":[{\"id\":\"color\",\"selected\":[\"Blue\"]}]}"}],"isError":false}],"role":"user","id":"c69ef39f-6f62-439f-b3f8-e8d10fba572f"}},"sourceEventSeqs":[132],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":134,"time":1785150169790,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":135,"time":1785150169790,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":136,"time":1785150170605,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -26,6 +26,6 @@
|
||||
{"type":"assistant/chunk","seq":160,"time":1785150170856,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":161,"time":1785150170856,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":284,"outputTokens":22,"cacheReadTokens":4096,"reasoningTokens":19}}}}
|
||||
{"type":"assistant/chunk","seq":162,"time":1785150170856,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":163,"time":1785150170857,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user answered \"Blue\". I should now reply with the single word DONE and stop."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":284,"outputTokens":22,"cacheReadTokens":4096,"reasoningTokens":19}},"sourceEventSeqs":[136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":163,"time":1785150170857,"data":{"turn":1,"step":2,"usage":{"inputTokens":284,"outputTokens":22,"cacheReadTokens":4096,"reasoningTokens":19},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user answered \"Blue\". I should now reply with the single word DONE and stop."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"b5f572cb-f2a7-4fa0-8097-a5551700a920"}},"sourceEventSeqs":[136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":164,"time":1785150170858,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":165,"time":1785150170858,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
- text: Pick one
|
||||
- heading "Which color do you prefer?" [level=2]
|
||||
- text: 1 / 1
|
||||
- button "上一题" [disabled]:
|
||||
- button "Previous question" [disabled]:
|
||||
- img
|
||||
- button "下一题" [disabled]:
|
||||
- button "Next question" [disabled]:
|
||||
- img
|
||||
- button "放弃整组问题":
|
||||
- button "Dismiss all questions":
|
||||
- img
|
||||
- radiogroup:
|
||||
- radio "Blue":
|
||||
@@ -15,9 +15,9 @@
|
||||
- radio "Green":
|
||||
- text: 2 Green A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.
|
||||
- img
|
||||
- button "其他,请填写自定义答案":
|
||||
- button "Other — enter a custom answer":
|
||||
- img
|
||||
- text: 其他,请填写自定义答案
|
||||
- text: Other — enter a custom answer
|
||||
- status
|
||||
- button "跳过本题"
|
||||
- button "提交" [disabled]
|
||||
- button "Skip this question"
|
||||
- button "Submit" [disabled]
|
||||
|
||||
36
apps/web/tests/snapshots/queue-actions/editing.expected.md
Normal file
36
apps/web/tests/snapshots/queue-actions/editing.expected.md
Normal file
@@ -0,0 +1,36 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- paragraph: partial
|
||||
- list:
|
||||
- listitem:
|
||||
- text: Queue item to remove
|
||||
- button "编辑排队消息":
|
||||
- img
|
||||
- button "删除排队消息":
|
||||
- img
|
||||
- listitem:
|
||||
- textbox "编辑排队消息": Edited queue item
|
||||
- button "保存排队消息":
|
||||
- img
|
||||
- button "取消编辑":
|
||||
- img
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Stop generating"
|
||||
30
apps/web/tests/snapshots/queue-actions/ui.expected.md
Normal file
30
apps/web/tests/snapshots/queue-actions/ui.expected.md
Normal file
@@ -0,0 +1,30 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- paragraph: partial
|
||||
- list:
|
||||
- listitem:
|
||||
- text: Edited queue item
|
||||
- button "编辑排队消息":
|
||||
- img
|
||||
- button "删除排队消息":
|
||||
- img
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Stop generating"
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1784974100747,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1784974100758,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1784974100759,"data":{"content":[{"type":"text","text":"Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1784974100759,"data":{"content":[{"type":"text","text":"Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"38f072be-5254-4cb7-b76e-d612b2ae3b3a"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1784974100761,"data":{"title":"Use the read tool twice","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1784974100827,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1784974100828,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -15,11 +15,11 @@
|
||||
{"type":"assistant/chunk","seq":61,"time":1784974101974,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_01_Hw6AQjhf9gjxnOtppcGx0725","name":"read","arguments":"{\"file_path\": \"b.txt\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":62,"time":1784974101974,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":124,"outputTokens":103,"cacheReadTokens":7680,"reasoningTokens":27}}}}
|
||||
{"type":"assistant/chunk","seq":63,"time":1784974101974,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":64,"time":1784974101978,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel."},{"type":"tool-call","id":"call_00_OsndvlcKnCcUmae7QXal8633","name":"read","arguments":"{\"file_path\": \"a.txt\"}"},{"type":"tool-call","id":"call_01_Hw6AQjhf9gjxnOtppcGx0725","name":"read","arguments":"{\"file_path\": \"b.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":124,"outputTokens":103,"cacheReadTokens":7680,"reasoningTokens":27}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":64,"time":1784974101978,"data":{"turn":1,"step":1,"usage":{"inputTokens":124,"outputTokens":103,"cacheReadTokens":7680,"reasoningTokens":27},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel."},{"type":"tool-call","id":"call_00_OsndvlcKnCcUmae7QXal8633","name":"read","arguments":"{\"file_path\": \"a.txt\"}"},{"type":"tool-call","id":"call_01_Hw6AQjhf9gjxnOtppcGx0725","name":"read","arguments":"{\"file_path\": \"b.txt\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"ec076738-f75d-4525-ba99-c8fc16acf955"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":65,"time":1784974101979,"data":{"turn":1,"step":1,"callId":"call_00_OsndvlcKnCcUmae7QXal8633","name":"read","arguments":"{\"file_path\": \"a.txt\"}"}}
|
||||
{"type":"tool/call","seq":66,"time":1784974101981,"data":{"turn":1,"step":1,"callId":"call_01_Hw6AQjhf9gjxnOtppcGx0725","name":"read","arguments":"{\"file_path\": \"b.txt\"}"}}
|
||||
{"type":"tool/result","seq":67,"time":1784974101985,"data":{"turn":1,"step":1,"callId":"call_00_OsndvlcKnCcUmae7QXal8633","content":[{"type":"text","text":"<path>{{cwd}}/workspace/a.txt</path>\n<type>file</type>\n<content>\n1: alpha\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[65],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":68,"time":1784974101986,"data":{"turn":1,"step":1,"callId":"call_01_Hw6AQjhf9gjxnOtppcGx0725","content":[{"type":"text","text":"<path>{{cwd}}/workspace/b.txt</path>\n<type>file</type>\n<content>\n1: beta\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[66],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":67,"time":1784974101985,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_OsndvlcKnCcUmae7QXal8633"},"content":[{"type":"tool-result","toolCallId":"call_00_OsndvlcKnCcUmae7QXal8633","content":[{"type":"text","text":"<path>{{cwd}}/workspace/a.txt</path>\n<type>file</type>\n<content>\n1: alpha\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"ea2294eb-8652-4492-8a08-9c24d3f8a60f"}},"sourceEventSeqs":[65],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":68,"time":1784974101986,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_01_Hw6AQjhf9gjxnOtppcGx0725"},"content":[{"type":"tool-result","toolCallId":"call_01_Hw6AQjhf9gjxnOtppcGx0725","content":[{"type":"text","text":"<path>{{cwd}}/workspace/b.txt</path>\n<type>file</type>\n<content>\n1: beta\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"f02b3acf-4e03-4b4d-beeb-1a564c9c6d61"}},"sourceEventSeqs":[66],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":69,"time":1784974101988,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":70,"time":1784974101988,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":71,"time":1784974102397,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -31,6 +31,6 @@
|
||||
{"type":"assistant/chunk","seq":105,"time":1784974102749,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":106,"time":1784974102750,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":215,"outputTokens":32,"cacheReadTokens":7808,"reasoningTokens":29}}}}
|
||||
{"type":"assistant/chunk","seq":107,"time":1784974102750,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":108,"time":1784974102750,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":215,"outputTokens":32,"cacheReadTokens":7808,"reasoningTokens":29}},"sourceEventSeqs":[71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":108,"time":1784974102750,"data":{"turn":1,"step":2,"usage":{"inputTokens":215,"outputTokens":32,"cacheReadTokens":7808,"reasoningTokens":29},"message":{"role":"assistant","content":[{"type":"reasoning","text":"Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"07627a5e-4cb2-47ef-9b50-88893aac7406"}},"sourceEventSeqs":[71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":109,"time":1784974102751,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":110,"time":1784974102751,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -16,11 +15,6 @@
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- img
|
||||
- text: Read
|
||||
- button "a.txt"
|
||||
@@ -40,16 +34,12 @@
|
||||
- button "上下文已压缩 点击查看压缩摘要":
|
||||
- img
|
||||
- text: 上下文已压缩 点击查看压缩摘要
|
||||
- text: cache hit 98% · 15,962 tokens · 1 turns · 2 steps
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 98% Input 15.8K tok · Output 135 tok
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"initial": {
|
||||
"sidebar": "Fixture 历史会话",
|
||||
"breadcrumb": "Fixture 历史会话",
|
||||
"documentTitle": "Fixture 历史会话 — DeepSeek Harness"
|
||||
},
|
||||
"revised": {
|
||||
"sidebar": "Fixture 修订标题",
|
||||
"breadcrumb": "Fixture 修订标题",
|
||||
"documentTitle": "Fixture 修订标题 — DeepSeek Harness"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
- listbox "Trigger suggestions":
|
||||
- text: Skills
|
||||
- option "policy-shared Available to both model and user invocation" [selected]
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,28 +11,24 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
- text: "Tool call ask_user_question · {\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]} cache hit 98% · 7,946 tokens · 1 turns · 1 steps"
|
||||
- text: "Tool call ask_user_question · {\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"
|
||||
- region "Ready to continue?":
|
||||
- text: Checkpoint
|
||||
- heading "Ready to continue?" [level=2]
|
||||
- text: 1 / 1
|
||||
- button "上一题" [disabled]:
|
||||
- button "Previous question" [disabled]:
|
||||
- img
|
||||
- button "下一题" [disabled]:
|
||||
- button "Next question" [disabled]:
|
||||
- img
|
||||
- button "放弃整组问题":
|
||||
- button "Dismiss all questions":
|
||||
- img
|
||||
- radiogroup:
|
||||
- radio "Yes":
|
||||
@@ -42,9 +37,9 @@
|
||||
- radio "No":
|
||||
- text: 2 No
|
||||
- img
|
||||
- button "其他,请填写自定义答案":
|
||||
- button "Other — enter a custom answer":
|
||||
- img
|
||||
- text: 其他,请填写自定义答案
|
||||
- text: Other — enter a custom answer
|
||||
- status
|
||||
- button "跳过本题"
|
||||
- button "提交" [disabled]
|
||||
- button "Skip this question"
|
||||
- button "Submit" [disabled]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785004180013,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785004180030,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785004180030,"data":{"content":[{"type":"text","text":"Use the ask_user_question tool to ask me exactly one question with id \"checkpoint\", question \"Ready to continue?\", header \"Checkpoint\", and options labeled \"Yes\" and \"No\". After I answer, reply with one short sentence acknowledging my answer and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785004180030,"data":{"content":[{"type":"text","text":"Use the ask_user_question tool to ask me exactly one question with id \"checkpoint\", question \"Ready to continue?\", header \"Checkpoint\", and options labeled \"Yes\" and \"No\". After I answer, reply with one short sentence acknowledging my answer and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"eb77fa3d-5d60-4028-a592-e1f07a288f35"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785004180033,"data":{"title":"Use the ask_user_question tool to","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785004180105,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785004180106,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
@@ -12,10 +12,10 @@
|
||||
{"type":"assistant/chunk","seq":85,"time":1785004181401,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"}}}}
|
||||
{"type":"assistant/chunk","seq":86,"time":1785004181401,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":151,"outputTokens":115,"cacheReadTokens":7680,"reasoningTokens":29}}}}
|
||||
{"type":"assistant/chunk","seq":87,"time":1785004181402,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":88,"time":1785004181406,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that."},{"type":"tool-call","id":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":151,"outputTokens":115,"cacheReadTokens":7680,"reasoningTokens":29}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":88,"time":1785004181406,"data":{"turn":1,"step":1,"usage":{"inputTokens":151,"outputTokens":115,"cacheReadTokens":7680,"reasoningTokens":29},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that."},{"type":"tool-call","id":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"ac0cfafd-795f-4bc3-9dc9-a8df33538a38"}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":89,"time":1785004181407,"data":{"turn":1,"step":1,"callId":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"}}
|
||||
{"type":"tool/result","seq":90,"time":1785004181867,"data":{"turn":1,"step":1,"callId":"call_00_sAvjivLShvnWVk0sPQPV7661","content":[{"type":"text","text":"{\"answers\":[{\"id\":\"checkpoint\",\"selected\":[\"Yes\"]}]}"}],"isError":false},"sourceEventSeqs":[89],"surfaceOp":"append"}
|
||||
{"type":"steering/message","seq":91,"time":1785004181867,"data":{"turn":1,"content":[{"type":"text","text":"Interjection: include the word BANANA in your final reply."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":90,"time":1785004181867,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_sAvjivLShvnWVk0sPQPV7661"},"content":[{"type":"tool-result","toolCallId":"call_00_sAvjivLShvnWVk0sPQPV7661","content":[{"type":"text","text":"{\"answers\":[{\"id\":\"checkpoint\",\"selected\":[\"Yes\"]}]}"}],"isError":false}],"role":"user","id":"9c2175f8-3cd4-4d6c-9320-498d25f83342"}},"sourceEventSeqs":[89],"surfaceOp":"append"}
|
||||
{"type":"steering/message","seq":91,"time":1785004181867,"data":{"turn":1,"message":{"content":[{"type":"text","text":"Interjection: include the word BANANA in your final reply."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"291e02ad-cf8a-459f-a388-f81ed334e629"}},"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":92,"time":1785004181870,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":93,"time":1785004181870,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":94,"time":1785004182322,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -26,6 +26,6 @@
|
||||
{"type":"assistant/chunk","seq":137,"time":1785004182893,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"Great, let's move forward. BANANA!"}}}}
|
||||
{"type":"assistant/chunk","seq":138,"time":1785004182893,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":172,"outputTokens":41,"cacheReadTokens":7808,"reasoningTokens":29}}}}
|
||||
{"type":"assistant/chunk","seq":139,"time":1785004182893,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":140,"time":1785004182894,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer."},{"type":"text","text":"Great, let's move forward. BANANA!"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":172,"outputTokens":41,"cacheReadTokens":7808,"reasoningTokens":29}},"sourceEventSeqs":[94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":140,"time":1785004182894,"data":{"turn":1,"step":2,"usage":{"inputTokens":172,"outputTokens":41,"cacheReadTokens":7808,"reasoningTokens":29},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer."},{"type":"text","text":"Great, let's move forward. BANANA!"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"b49cca3a-fa40-4f45-91c9-e7c3f15fa233"}},"sourceEventSeqs":[94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":141,"time":1785004182895,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":142,"time":1785004182895,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
|
||||
- button "复制":
|
||||
- img
|
||||
@@ -12,15 +11,11 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
@@ -34,16 +29,13 @@
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}} cache hit 98% · 15,967 tokens · 1 turns · 2 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 98% Input 15.8K tok · Output 156 tok
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
- dialog "选择工作区目录":
|
||||
- heading "选择工作区目录" [level=2]
|
||||
- dialog "Select Workspace Directory":
|
||||
- heading "Select Workspace Directory" [level=2]
|
||||
- navigation:
|
||||
- button "主目录"
|
||||
- button "Home"
|
||||
- img
|
||||
- button "browse-golden"
|
||||
- button "编辑路径"
|
||||
- button "Edit path"
|
||||
- list:
|
||||
- listitem:
|
||||
- button "alpha":
|
||||
@@ -16,8 +16,8 @@
|
||||
- img
|
||||
- text: beta
|
||||
- img
|
||||
- button "新建文件夹":
|
||||
- button "New folder":
|
||||
- img
|
||||
- text: 新建文件夹
|
||||
- button "取消"
|
||||
- button "打开"
|
||||
- text: New folder
|
||||
- button "Cancel"
|
||||
- button "Open"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Web e2e scenario: mid-turn steering, end to end. The composer locks while a
|
||||
// turn runs, so the product UI has no steering gesture yet — the steer is
|
||||
// POSTed from the page itself over the same same-origin /api transport the
|
||||
// client uses (TODO(web-steer-composer): drive this through a composer
|
||||
// gesture once one exists). Everything downstream is product: the gateway
|
||||
// Web e2e scenario: mid-turn steering, end to end. The product composer
|
||||
// deliberately exposes Queue only, so the steer is POSTed from the page
|
||||
// itself over the same same-origin /api transport the client uses.
|
||||
// TODO(web-steer-ui): Drive this through a dedicated steering interaction
|
||||
// once one exists. Everything downstream is product: the gateway
|
||||
// routes mode:'steer' to Agent.steer, the loop drains it at the step
|
||||
// boundary into a durable steering/message event, the SSE mux pushes it, and
|
||||
// the transcript renders the badged interjection bubble. The question
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/steering', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
@@ -67,7 +67,7 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
sessionEvents.push(event)
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -122,6 +122,8 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
|
||||
// blocks, alone. The DOM is stable here (no further SSE frames can
|
||||
// arrive until the question is answered), making this state capturable.
|
||||
expect(await page.getByText('插话').count()).toBe(0)
|
||||
expect(await page.getByText(STEER, { exact: true }).count()).toBe(0)
|
||||
expect(await page.getByRole('button', { name: '编辑排队消息' }).count()).toBe(0)
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MID_EXPECTED, snapshot, MODE)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,28 @@
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { createServer } from 'node:net'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Page } from 'playwright'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
|
||||
/** The built page under test; `pnpm run test:web` rebuilds it before running. */
|
||||
export const DIST_INDEX = fileURLToPath(new URL('../dist/index.html', import.meta.url))
|
||||
|
||||
export const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
/**
|
||||
* Open the standard browser-test page with English selected before client
|
||||
* boot. This keeps role locators and goldens deterministic across localized
|
||||
* component migrations; the settings locale scenario deliberately bypasses
|
||||
* this helper to cover the product's default Chinese state.
|
||||
* @param browser - Playwright browser owning the page.
|
||||
* @param height - Viewport height; width is fixed to the lane baseline.
|
||||
* @returns the initialized page.
|
||||
*/
|
||||
export async function newEnglishPage(browser: Browser, height = 1000): Promise<Page> {
|
||||
const page = await browser.newPage({ viewport: { width: 1680, height } })
|
||||
await page.addInitScript(() => { localStorage.setItem('dsh.locale', 'en') })
|
||||
return page
|
||||
}
|
||||
|
||||
/** Fail loud on a stale checkout instead of testing yesterday's bundle. */
|
||||
export function requireDist(): void {
|
||||
if (!existsSync(DIST_INDEX)) {
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
// Todo display snapshot over the BUILT client graph (the code-mode-fixture
|
||||
// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
|
||||
// Opens the fixture history session and pins the todo_write turn's two
|
||||
// surfaces: the dedicated TodoRow in the chat flow (keyed toolview, summary
|
||||
// derived from the call args) and the TodoPanel plan strip riding the
|
||||
// 'conversation.input.dock' slot (fed by the host `todos` projection via
|
||||
// useProjection, seeded by the tail history page), including the collapse
|
||||
// interaction and the next-turn clearance of the standing plan.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
|
||||
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
||||
|
||||
const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
|
||||
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{
|
||||
id: '@deepseek-ai/dsh-client-ui-workspace',
|
||||
dir: 'ui-workspace',
|
||||
url: '/plugins/ui-workspace.js',
|
||||
rev: 'fx',
|
||||
inject: [
|
||||
'@deepseek-ai/dsh-client-runtime',
|
||||
'@deepseek-ai/dsh-client-ui-conversation',
|
||||
'@deepseek-ai/dsh-client-ui-sidebar',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const bundles = new Map(PLUGINS.map(plugin => [
|
||||
plugin.url,
|
||||
readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
|
||||
]))
|
||||
|
||||
interface FixtureWindow extends Window {
|
||||
__DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
|
||||
__ModuleLoader__?: unknown
|
||||
}
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
const win = window as FixtureWindow
|
||||
let unmount: (() => void) | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => { callback(0) }, 0) as unknown as number)
|
||||
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => { unmount?.() })
|
||||
unmount = undefined
|
||||
cleanup()
|
||||
delete win.__DSH_BOOT__
|
||||
delete win.__ModuleLoader__
|
||||
delete (globalThis as Record<string, unknown>).__fxTiming
|
||||
document.body.innerHTML = ''
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** Boot the complete built client graph against the populated fixture branch. */
|
||||
function boot(): void {
|
||||
history.replaceState(null, '', '/?fixture')
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
|
||||
act(() => {
|
||||
const entry = new AppWebEntry(root, {
|
||||
fetchBundle: (url) => {
|
||||
const code = bundles.get(url)
|
||||
return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
|
||||
},
|
||||
executeBundle: (code) => { (0, eval)(code) },
|
||||
})
|
||||
void entry.run()
|
||||
unmount = () => { entry.dispose() }
|
||||
})
|
||||
}
|
||||
|
||||
/** Collapse decorative whitespace while preserving the text a user sees. */
|
||||
function visibleText(element: Element): string {
|
||||
return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** Open the fixture history session (the alpha log carrying the todo_write turn) and wait for its tail. */
|
||||
async function openFixtureSession(): Promise<void> {
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
// Anchor on the expandable Workspace group row: the title and the blank
|
||||
// session row can both read "fixture", and the session-count meta shifts
|
||||
// when a blank session joins the group.
|
||||
const group = (await within(tree).findAllByText('fixture'))
|
||||
.map(el => el.closest<HTMLElement>('[role="treeitem"]'))
|
||||
.find(el => el?.getAttribute('aria-expanded') !== null)
|
||||
if (group === null || group === undefined) throw new Error('fixture Workspace group missing')
|
||||
if (group.getAttribute('aria-expanded') === 'false') {
|
||||
fireEvent.click(within(group).getByText('fixture'))
|
||||
await waitFor(() => {
|
||||
expect(group.getAttribute('aria-expanded')).toBe('true')
|
||||
})
|
||||
}
|
||||
const session = await within(tree).findByText('Fixture 历史会话')
|
||||
fireEvent.click(session)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-sample="todo-row"]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
}
|
||||
|
||||
it('renders the todo_write turn: dedicated tool row + the dock plan strip', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
|
||||
const row = document.querySelector('[data-sample="todo-row"]')
|
||||
if (row === null) throw new Error('todo row missing')
|
||||
const panel = document.querySelector('[data-testid="todo-panel"]')
|
||||
if (panel === null) throw new Error('todo panel missing from the input dock')
|
||||
|
||||
// Header spans are adjacent inline nodes; textContent joins "To-dos" +
|
||||
// "1/3…" with no space (visual gap is CSS gap: 10px, not a text node).
|
||||
expect({
|
||||
row: visibleText(row),
|
||||
rowState: row.getAttribute('data-state'),
|
||||
panelHeader: visibleText(panel.querySelector('button') ?? panel),
|
||||
panelItems: [...panel.querySelectorAll('li')].map(item => ({
|
||||
status: item.getAttribute('data-status'),
|
||||
text: visibleText(item),
|
||||
})),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"panelHeader": "To-dos1/3 tasks · 1 in progress",
|
||||
"panelItems": [
|
||||
{
|
||||
"status": "completed",
|
||||
"text": "梳理需求",
|
||||
},
|
||||
{
|
||||
"status": "in_progress",
|
||||
"text": "实现 fixture 样本",
|
||||
},
|
||||
{
|
||||
"status": "pending",
|
||||
"text": "浏览器验收",
|
||||
},
|
||||
],
|
||||
"row": "更新任务清单1/3 已完成 · 实现 fixture 样本",
|
||||
"rowState": "ok",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('collapses the plan strip to the count summary and restores it', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
|
||||
const panel = document.querySelector('[data-testid="todo-panel"]')
|
||||
if (panel === null) throw new Error('todo panel missing from the input dock')
|
||||
const header = panel.querySelector('button')
|
||||
if (header === null) throw new Error('todo panel header missing')
|
||||
|
||||
fireEvent.click(header)
|
||||
expect({
|
||||
collapsedHeader: visibleText(header),
|
||||
listGone: panel.querySelector('ul') === null,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"collapsedHeader": "To-dos1/3 tasks · 1 in progress",
|
||||
"listGone": true,
|
||||
}
|
||||
`)
|
||||
|
||||
fireEvent.click(header)
|
||||
expect(panel.querySelectorAll('li')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('hides the plan strip when the next turn starts', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
expect(document.querySelector('[data-testid="todo-panel"]')).not.toBeNull()
|
||||
|
||||
const composer = await screen.findByPlaceholderText('Message the agent', {}, { timeout: 10_000 })
|
||||
fireEvent.change(composer, { target: { value: '下一轮清空计划' } })
|
||||
fireEvent.keyDown(composer, { key: 'Enter' })
|
||||
|
||||
await screen.findByText('下一轮清空计划', { exact: true }, { timeout: 10_000 })
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-testid="todo-panel"]')).toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
|
||||
expect({
|
||||
promptVisible: screen.getByText('下一轮清空计划', { exact: true }).textContent,
|
||||
panelGone: document.querySelector('[data-testid="todo-panel"]') === null,
|
||||
// Historical todo_write row stays in the flow; only the dock strip clears.
|
||||
rowStillPresent: document.querySelector('[data-sample="todo-row"]') !== null,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"panelGone": true,
|
||||
"promptVisible": "下一轮清空计划",
|
||||
"rowStillPresent": true,
|
||||
}
|
||||
`)
|
||||
})
|
||||
@@ -1,397 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
// Assembled keyless snapshots of the New Session flow under the agent-parity
|
||||
// model: startup auto-connects the recent Workspace's blank session when one
|
||||
// exists; without any Workspace the composer is locked in the pure view
|
||||
// state until one is chosen. Picking one materializes the full Session+Agent
|
||||
// (reuse-or-create of the workspace's blank session), the first ACCEPTED
|
||||
// prompt flips blank and surfaces the session in lists, and failures leave
|
||||
// no client-side transaction state: a failed attach keeps the view state
|
||||
// locked, a rejected prompt keeps the session blank with the draft restored.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
|
||||
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
||||
|
||||
const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
|
||||
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-settings', dir: 'ui-settings', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-sidebar'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-settings-general', dir: 'ui-settings-general', url: '/plugins/ui-settings-general.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-client-locale'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-models', dir: 'ui-models', url: '/plugins/ui-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{
|
||||
id: '@deepseek-ai/dsh-client-ui-workspace',
|
||||
dir: 'ui-workspace',
|
||||
url: '/plugins/ui-workspace.js',
|
||||
rev: 'fx',
|
||||
inject: [
|
||||
'@deepseek-ai/dsh-client-runtime',
|
||||
'@deepseek-ai/dsh-client-ui-conversation',
|
||||
'@deepseek-ai/dsh-client-ui-sidebar',
|
||||
],
|
||||
},
|
||||
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
// Dual-face host package: its browser half fills the directory-flow holes
|
||||
// (the same composition row apps/cli mounts for the node-side backend).
|
||||
{
|
||||
id: '@deepseek-ai/dsh-host-directory-picker-browse',
|
||||
dir: '../host/directory-picker-browse',
|
||||
url: '/plugins/directory-picker-browse.js',
|
||||
rev: 'fx',
|
||||
inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-workspace', '@deepseek-ai/dsh-client-locale'],
|
||||
},
|
||||
]
|
||||
|
||||
const bundles = new Map(PLUGINS.map(plugin => [
|
||||
plugin.url,
|
||||
readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
|
||||
]))
|
||||
|
||||
interface FixtureWindow extends Window {
|
||||
__DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
|
||||
__ModuleLoader__?: unknown
|
||||
}
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
const win = window as FixtureWindow
|
||||
let unmount: (() => void) | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => { callback(0) }, 0) as unknown as number)
|
||||
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => { unmount?.() })
|
||||
unmount = undefined
|
||||
cleanup()
|
||||
delete win.__DSH_BOOT__
|
||||
delete win.__ModuleLoader__
|
||||
delete (globalThis as Record<string, unknown>).__fxTiming
|
||||
document.body.innerHTML = ''
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** Boot the complete built client graph against one keyless fixture branch. */
|
||||
function boot(search: string): void {
|
||||
history.replaceState(null, '', `/${search}`)
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
|
||||
act(() => {
|
||||
const entry = new AppWebEntry(root, {
|
||||
fetchBundle: (url) => {
|
||||
const code = bundles.get(url)
|
||||
return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
|
||||
},
|
||||
executeBundle: (code) => { (0, eval)(code) },
|
||||
})
|
||||
void entry.run()
|
||||
unmount = () => { entry.dispose() }
|
||||
})
|
||||
}
|
||||
|
||||
/** Collapse decorative whitespace while preserving the text a user sees. */
|
||||
function visibleText(element: Element): string {
|
||||
return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** Identify the interactive Workspace chip (view state or blank-session hero) by its menu contract. */
|
||||
function workspaceChip(): HTMLElement {
|
||||
const chip = screen.getAllByRole('button', { name: 'Choose workspace' })
|
||||
.find(element => element.getAttribute('aria-haspopup') === 'menu')
|
||||
if (chip === undefined) throw new Error('Workspace chip missing')
|
||||
return chip
|
||||
}
|
||||
|
||||
/** The locked view-state composer (no session yet). */
|
||||
async function findLockedComposer(): Promise<HTMLTextAreaElement> {
|
||||
return await screen.findByPlaceholderText(
|
||||
'Choose a workspace to start', {}, { timeout: 10_000 },
|
||||
)
|
||||
}
|
||||
|
||||
/** The live blank-session hero composer (session materialized). */
|
||||
async function findHeroComposer(): Promise<HTMLTextAreaElement> {
|
||||
return await screen.findByPlaceholderText(
|
||||
'Describe what you want to build', {}, { timeout: 10_000 },
|
||||
)
|
||||
}
|
||||
|
||||
/** Edit the machine-owned controlled input and assert the same-tick echo. */
|
||||
function setComposerText(composer: HTMLElement, value: string): void {
|
||||
fireEvent.change(composer, { target: { value } })
|
||||
expect((composer as HTMLTextAreaElement).value).toBe(value)
|
||||
}
|
||||
|
||||
/** Drive the picker's create flow: chip → Create a new workspace → name dialog. */
|
||||
async function createWorkspaceViaPicker(name: string): Promise<void> {
|
||||
fireEvent.click(workspaceChip())
|
||||
fireEvent.click(await screen.findByRole('menuitem', { name: 'Create a new workspace' }))
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Create a new workspace' })
|
||||
fireEvent.change(within(dialog).getByRole('textbox', { name: 'New workspace name' }), {
|
||||
target: { value: name },
|
||||
})
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Create workspace' }))
|
||||
}
|
||||
|
||||
/** Pick an existing Workspace row from the chip menu. */
|
||||
async function pickWorkspace(title: string): Promise<void> {
|
||||
fireEvent.click(workspaceChip())
|
||||
fireEvent.click(await screen.findByRole('menuitem', { name: title }))
|
||||
}
|
||||
|
||||
it('locks the composer in the New Session view state until a Workspace is chosen', async () => {
|
||||
boot('?fixture=empty')
|
||||
|
||||
const composer = await findLockedComposer()
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
|
||||
expect({
|
||||
headline: visibleText(screen.getByText("Let's start building")),
|
||||
chip: visibleText(workspaceChip()),
|
||||
composerDisabled: composer.disabled,
|
||||
sendDisabled: screen.getByRole<HTMLButtonElement>('button', { name: 'Send message' }).disabled,
|
||||
sidebar: visibleText(tree),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"chip": "Choose workspace",
|
||||
"composerDisabled": true,
|
||||
"headline": "Let's start building",
|
||||
"sendDisabled": true,
|
||||
"sidebar": "No sessions yet",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('adopts a directory through the composed in-app browse flow and lands in its blank session', async () => {
|
||||
boot('?fixture=empty')
|
||||
|
||||
await findLockedComposer()
|
||||
fireEvent.click(workspaceChip())
|
||||
const menu = await screen.findByRole('menu')
|
||||
// The composed flow package occupies the directory-flow hole, so the
|
||||
// picking affordance is present (no advertised-kind read exists anymore).
|
||||
expect(within(menu).getAllByRole('menuitem').map(item => visibleText(item)))
|
||||
.toEqual(['Open local folder…', 'Create a new workspace'])
|
||||
fireEvent.click(within(menu).getByRole('menuitem', { name: 'Open local folder…' }))
|
||||
// The browse occupant renders the Select Workspace Directory dialog at the
|
||||
// fixture home; select Documents, advance into project, and adopt it.
|
||||
const dialog = await screen.findByRole('dialog', { name: '选择工作区目录' }, { timeout: 10_000 })
|
||||
// Row targeting goes through the visible label text: listitem accessible-name
|
||||
// computation differs across dom-accessibility-api environments, while the
|
||||
// row's name span is stable (clicks bubble to the row button).
|
||||
fireEvent.click(await within(dialog).findByText('Documents', {}, { timeout: 10_000 }))
|
||||
fireEvent.click(await within(dialog).findByText('project', {}, { timeout: 10_000 }))
|
||||
// Open disables while the selection's child listing is in flight; wait for
|
||||
// the enabled state or the click lands on a dead button on slow runners.
|
||||
await waitFor(() => {
|
||||
expect(within(dialog).getByRole<HTMLButtonElement>('button', { name: '打开' }).disabled).toBe(false)
|
||||
}, { timeout: 10_000 })
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '打开' }))
|
||||
await findHeroComposer()
|
||||
await waitFor(() => {
|
||||
expect(visibleText(screen.getByRole('tree', { name: 'Sessions' }))).toContain('project')
|
||||
})
|
||||
})
|
||||
|
||||
it('selects the recent Workspace and opens its blank Session on first load', async () => {
|
||||
boot('?fixture')
|
||||
|
||||
const composer = await findHeroComposer()
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
await waitFor(() => { expect(within(tree).getByText('4 sessions')).toBeDefined() }, { timeout: 10_000 })
|
||||
|
||||
expect({
|
||||
chip: visibleText(workspaceChip()),
|
||||
composerDisabled: composer.disabled,
|
||||
blankRow: within(tree).getByText('New Session').textContent,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"blankRow": "New Session",
|
||||
"chip": "fixture",
|
||||
"composerDisabled": false,
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('creating a Workspace materializes and lists its selected blank Session', async () => {
|
||||
boot('?fixture=empty')
|
||||
|
||||
await findLockedComposer()
|
||||
await createWorkspaceViaPicker('nova')
|
||||
|
||||
// The pick connected the workspace: full Session+Agent exists, composer live.
|
||||
const composer = await findHeroComposer()
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() })
|
||||
expect(within(tree).getByText('New Session')).toBeDefined()
|
||||
const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
|
||||
if (group === null) throw new Error('created Workspace projection missing')
|
||||
|
||||
expect({
|
||||
composerDisabled: composer.disabled,
|
||||
chip: visibleText(workspaceChip()),
|
||||
workspace: visibleText(group),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"chip": "nova",
|
||||
"composerDisabled": false,
|
||||
"workspace": "nova1 session",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('New Session reuses the Workspace blank session and converts the single visible row', async () => {
|
||||
boot('?fixture=empty')
|
||||
|
||||
await findLockedComposer()
|
||||
await createWorkspaceViaPicker('nova')
|
||||
await findHeroComposer()
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
|
||||
|
||||
// New Session resolves through the recent Workspace and reuses its blank
|
||||
// session in place: no locked interlude, no second entity.
|
||||
const newSessionButton = screen.getAllByRole('button', { name: 'New session' })
|
||||
.find(button => visibleText(button) === 'New Session')
|
||||
if (newSessionButton === undefined) throw new Error('New Session button missing')
|
||||
fireEvent.click(newSessionButton)
|
||||
const composer = await findHeroComposer()
|
||||
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
|
||||
|
||||
setComposerText(composer, 'first light')
|
||||
fireEvent.keyDown(composer, { key: 'Enter' })
|
||||
|
||||
// Conversion: the accepted prompt flips blank without adding a second row.
|
||||
await screen.findByText('first light', { exact: true }, { timeout: 10_000 })
|
||||
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
|
||||
const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
|
||||
if (group === null) throw new Error('converted Session projection missing')
|
||||
|
||||
expect({
|
||||
workspace: visibleText(group),
|
||||
promptVisible: screen.getByText('first light', { exact: true }).textContent,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"promptVisible": "first light",
|
||||
"workspace": "nova1 session",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('a failed Workspace attach recovers by reusing the published blank session', async () => {
|
||||
boot('?fixture&fixtureAttach=fail')
|
||||
|
||||
// The rejected startup connect surfaces the locked view state first: the
|
||||
// failure leaves no client-side transaction state to unwind.
|
||||
await findLockedComposer()
|
||||
|
||||
// The host published the session before rejecting attachment (blank, with
|
||||
// the workspace cwd), so the next connect — retry or manual pick — reuses
|
||||
// it instead of minting a duplicate, and the hero opens on it.
|
||||
await pickWorkspace('fixture')
|
||||
const composer = await findHeroComposer()
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
const group = within(tree).getByText('3 sessions').closest('[role="treeitem"]')
|
||||
if (group === null) throw new Error('fixture Workspace projection missing')
|
||||
|
||||
expect({
|
||||
headline: visibleText(screen.getByText("Let's start building")),
|
||||
composerDisabled: composer.disabled,
|
||||
chip: visibleText(workspaceChip()),
|
||||
workspace: visibleText(group),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"chip": "fixture",
|
||||
"composerDisabled": false,
|
||||
"headline": "Let's start building",
|
||||
"workspace": "fixture3 sessions",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('a rejected first prompt keeps the session blank and the draft in the machine', async () => {
|
||||
boot('?fixture=empty&fixturePrompt=reject')
|
||||
|
||||
await findLockedComposer()
|
||||
await createWorkspaceViaPicker('nova')
|
||||
const composer = await findHeroComposer()
|
||||
|
||||
setComposerText(composer, 'do not lose this')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send message' }))
|
||||
|
||||
const alert = await screen.findByRole('alert', {}, { timeout: 10_000 })
|
||||
// Failure restore rides the machine (no pendingPrompt transaction): the
|
||||
// draft returns to the same resident textarea one render later. The
|
||||
// attempt flips the composer out of the hero (engaging = retry chrome),
|
||||
// but acceptance never happened: the session row stays New Session.
|
||||
const retained = await screen.findByDisplayValue('do not lose this')
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
|
||||
if (group === null) throw new Error('rejected-send Workspace projection missing')
|
||||
|
||||
expect({
|
||||
error: visibleText(alert),
|
||||
prompt: (retained as HTMLTextAreaElement).value,
|
||||
blankRow: within(tree).getByText('New Session').textContent,
|
||||
workspace: visibleText(group),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"blankRow": "New Session",
|
||||
"error": "fixture: prompt rejected before acceptance (agent-busy)",
|
||||
"prompt": "do not lose this",
|
||||
"workspace": "nova1 session",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('switching Workspace before the first message carries the draft to the new blank session', async () => {
|
||||
boot('?fixture')
|
||||
|
||||
const composer = await findHeroComposer()
|
||||
setComposerText(composer, 'carry me')
|
||||
|
||||
// Switch = session switch: the new workspace's blank session takes over,
|
||||
// the typed draft moves machine-to-machine, the old blank stays hidden.
|
||||
await createWorkspaceViaPicker('nova')
|
||||
await waitFor(() => { expect(visibleText(workspaceChip())).toBe('nova') }, { timeout: 10_000 })
|
||||
const carried = await screen.findByDisplayValue('carry me')
|
||||
const tree = screen.getByRole('tree', { name: 'Sessions' })
|
||||
const fixtureGroup = within(tree).getByText('3 sessions').closest('[role="treeitem"]')
|
||||
const novaGroup = within(tree).getByText('1 session').closest('[role="treeitem"]')
|
||||
if (fixtureGroup === null || novaGroup === null) throw new Error('Workspace projections missing after switch')
|
||||
|
||||
expect({
|
||||
chip: visibleText(workspaceChip()),
|
||||
prompt: (carried as HTMLTextAreaElement).value,
|
||||
fixtureWorkspace: visibleText(fixtureGroup),
|
||||
novaWorkspace: visibleText(novaGroup),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"chip": "nova",
|
||||
"fixtureWorkspace": "fixture3 sessions",
|
||||
"novaWorkspace": "nova1 session",
|
||||
"prompt": "carry me",
|
||||
}
|
||||
`)
|
||||
})
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/workspace-management', import.meta.url))
|
||||
// The seed is another scenario's committed fixture, reused read-only: this
|
||||
@@ -42,12 +42,12 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
const agentsBefore = scaffold.ctx.agents.list().length
|
||||
await page.getByRole('button', { name: 'Create workspace' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Open local folder…' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
|
||||
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '编辑路径' }).click()
|
||||
await dialog.getByLabel('编辑路径').fill(path)
|
||||
await dialog.getByLabel('编辑路径').press('Enter')
|
||||
await dialog.getByRole('button', { name: '打开' }).click()
|
||||
await dialog.getByRole('button', { name: 'Edit path' }).click()
|
||||
await dialog.getByLabel('Edit path').fill(path)
|
||||
await dialog.getByLabel('Edit path').press('Enter')
|
||||
await dialog.getByRole('button', { name: 'Open' }).click()
|
||||
await dialog.waitFor({ state: 'hidden', timeout: 10_000 })
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.workspace.resolveByPath(path),
|
||||
@@ -72,7 +72,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
await writeFile(join(sessionCwd, 'b.txt'), 'beta\n')
|
||||
await seedSession(scaffold, await readFile(SEED, 'utf8'), SEED_ID)
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -363,15 +363,15 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
try {
|
||||
await page.getByRole('button', { name: 'Create workspace' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Open local folder…' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
|
||||
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '编辑路径' }).click()
|
||||
await dialog.getByLabel('编辑路径').fill(staged)
|
||||
await dialog.getByLabel('编辑路径').press('Enter')
|
||||
await dialog.getByRole('button', { name: 'Edit path' }).click()
|
||||
await dialog.getByLabel('Edit path').fill(staged)
|
||||
await dialog.getByLabel('Edit path').press('Enter')
|
||||
await expect.poll(() => dialog.getByText('alpha', { exact: true }).count(), { timeout: 10_000 }).toBe(1)
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(BROWSER_EXPECTED, snapshot, MODE)
|
||||
await dialog.getByRole('button', { name: '取消' }).click()
|
||||
await dialog.getByRole('button', { name: 'Cancel' }).click()
|
||||
await dialog.waitFor({ state: 'hidden', timeout: 10_000 })
|
||||
} finally {
|
||||
if (realHome === undefined) delete process.env.HOME
|
||||
@@ -406,7 +406,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
// card; no aria role — text anchors are the stable selector).
|
||||
await expect.poll(() => page.getByText('Idle', { exact: true }).count(), { timeout: 5_000 }).toBeGreaterThanOrEqual(1)
|
||||
// Leaving the anchor closes it with no delay.
|
||||
await page.getByRole('button', { name: '设置' }).hover()
|
||||
await page.getByRole('button', { name: 'Settings' }).hover()
|
||||
await expect.poll(() => page.getByText('Idle', { exact: true }).count(), { timeout: 5_000 }).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
"tests/sidebar-scrollbar.e2e.ts",
|
||||
"tests/code-mode-round.e2e.ts",
|
||||
"tests/cordis-tool-round.e2e.ts",
|
||||
"tests/message-actions.e2e.ts"
|
||||
"tests/message-actions.e2e.ts",
|
||||
"tests/queue-actions.e2e.ts",
|
||||
"tests/skill-invocation-policy.e2e.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user