diff --git a/apps/web/tests/session-actions.snapshot.ts b/apps/web/tests/built-boot.snapshot.ts similarity index 50% rename from apps/web/tests/session-actions.snapshot.ts rename to apps/web/tests/built-boot.snapshot.ts index 684afe5532..69d5d5cfae 100644 --- a/apps/web/tests/session-actions.snapshot.ts +++ b/apps/web/tests/built-boot.snapshot.ts @@ -1,6 +1,15 @@ // @vitest-environment jsdom -// Session row actions in the assembled fixture app: Rename opens the -// browser-owned dialog and settles the title from the unary response. +// 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' @@ -16,9 +25,18 @@ const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [ { 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-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-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-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 => [ @@ -42,16 +60,11 @@ 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(() => { @@ -60,7 +73,6 @@ afterEach(() => { cleanup() delete win.__DSH_BOOT__ delete win.__ModuleLoader__ - delete (globalThis as Record).__fxTiming document.body.innerHTML = '' document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() }) document.title = '' @@ -68,9 +80,12 @@ afterEach(() => { vi.unstubAllGlobals() }) -async function bootApp(): Promise { - const root = document.querySelector('#root') - if (root === null) throw new Error('snapshot root missing') +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) => { @@ -82,49 +97,21 @@ async function bootApp(): Promise { void entry.run() unmount = () => { entry.dispose() } }) - await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) -} -/** The session row element carrying the given visible label. */ -function rowOf(label: string): HTMLElement { - const tree = screen.getByRole('tree', { name: 'Sessions' }) - const row = within(tree).getByText(label).closest('[role="treeitem"]') - if (row === null) throw new Error(`session row "${label}" missing`) - return row -} + // 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') -/** Open the row's ... menu and click one action. The anchor button is - * CSS-hover-revealed (real stylesheets are injected in this assembled run, - * so role queries filter it as hidden); target it directly. */ -function pickRowAction(label: string, action: string): void { - const anchor = rowOf(label).querySelector(`button[aria-label="Session actions for ${label}"]`) - if (anchor === null) throw new Error(`row menu anchor for "${label}" missing`) - fireEvent.click(anchor) - fireEvent.click(screen.getByRole('menuitem', { name: action, hidden: true })) -} + // 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 }) -it('renames a session through the row-menu dialog; the row settles from the unary response', async () => { - await bootApp() - const sourceLabel = 'Fixture 历史会话' - await screen.findByText(sourceLabel) - - pickRowAction(sourceLabel, 'Rename') - const input = await screen.findByLabelText('Session name') - expect((input as HTMLInputElement).value).toBe(sourceLabel) - fireEvent.change(input, { target: { value: ' 分叉 实验记录 ' } }) - fireEvent.click(screen.getByRole('button', { name: 'Rename' })) - - // Host-side normalization collapses whitespace; the dialog closes on - // acceptance and the row re-labels without any push-frame wait. - const renamed = '分叉 实验记录' - await waitFor(() => { expect(screen.queryByLabelText('Session name')).toBeNull() }) - await screen.findByText(renamed) - const tree = screen.getByRole('tree', { name: 'Sessions' }) - expect(within(tree).queryByText(sourceLabel)).toBeNull() - - const rows = [...tree.querySelectorAll('[role="treeitem"]')].map(row => ({ - label: row.textContent?.replace(/\s+/g, ' ').trim() ?? '', - })) - await expect(`${JSON.stringify(rows, null, 2)}\n`) - .toMatchFileSnapshot('./snapshots/session-actions/rename-rows.json') + // 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) + } }) diff --git a/apps/web/tests/code-mode-fixture.snapshot.ts b/apps/web/tests/code-mode-fixture.snapshot.ts deleted file mode 100644 index b7ab11f2c0..0000000000 --- a/apps/web/tests/code-mode-fixture.snapshot.ts +++ /dev/null @@ -1,239 +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, -// details-panel resolution of a sub-callId, and the Trajectory tab's sub-call -// cells and timing overview. -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).__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 { - const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) - const group = within(tree).getByText('4 sessions').closest('[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
 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 surfaces run_code sub-calls in the ledger and timing overview', 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 Subtool cells nested under the run_code Tool cell in dispatch
-    // order, each paired with its result preview.
-    subCells: subCells.map(cell => visibleText(cell)),
-  }).toMatchInlineSnapshot(`
-    {
-      "subCells": [
-        "SUBTOOLbash{"command":"ls notes","description":"List notes"}→demo.txt new-demo.txt",
-        "SUBTOOLread{"path":"notes/demo.txt"}→hello fixture",
-        "SUBTOOLread{"path":"notes/missing.txt"}→error",
-      ],
-    }
-  `)
-
-  const timelineSubCalls = [...document.querySelectorAll('[data-timeline-span="subtool"]')]
-  expect({
-    count: timelineSubCalls.length,
-    measured: timelineSubCalls.map(span => span.getAttribute('title')?.endsWith(' · 800 ms')),
-  }).toMatchInlineSnapshot(`
-    {
-      "count": 3,
-      "measured": [
-        true,
-        true,
-        true,
-      ],
-    }
-  `)
-})
diff --git a/apps/web/tests/session-title.snapshot.ts b/apps/web/tests/session-title.snapshot.ts
deleted file mode 100644
index 4667b85079..0000000000
--- a/apps/web/tests/session-title.snapshot.ts
+++ /dev/null
@@ -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).__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('#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('[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).__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('给智能体发消息')
-  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')
-})
diff --git a/apps/web/tests/slash-flow.snapshot.ts b/apps/web/tests/slash-flow.snapshot.ts
deleted file mode 100644
index 5ef759a7d0..0000000000
--- a/apps/web/tests/slash-flow.snapshot.ts
+++ /dev/null
@@ -1,213 +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 renders the session's skill and wire command catalogs
-// (sessions are always agent-backed — no draft/materialized split), a skill
-// pick inserts its reference, 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 {}
-}
-
-// jsdom has no scrollIntoView; the slash menu follows its highlighted option.
-const scrollIntoView = vi.fn()
-const win = window as FixtureWindow
-let unmount: (() => void) | undefined
-
-beforeEach(() => {
-  localStorage.clear()
-  document.title = 'DeepSeek Harness'
-  Element.prototype.scrollIntoView = scrollIntoView
-  scrollIntoView.mockClear()
-  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).__fxTiming
-  document.body.innerHTML = ''
-  document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
-  document.title = ''
-  history.replaceState(null, '', '/')
-  Reflect.deleteProperty(Element.prototype, 'scrollIntoView')
-  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 {
-  fireEvent.change(composer, { target: { value } })
-  await waitFor(() => { expect(composer.value).toBe(value) })
-}
-
-it('locked view state, skill discovery, /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(
-    '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(
-    'Describe what you want to build', {}, { timeout: 10_000 },
-  )
-  expect(composer.disabled).toBe(false)
-
-  // The built skill plugin prewarms the fixture's session-addressed catalog;
-  // this pins client rendering and picking, while the real-host browser lane
-  // owns policy filtering. Picking inserts the literal reference into the
-  // resident composer.
-  await typeComposer(composer, '/fixture')
-  const skillMenu = await screen.findByRole('listbox', { name: 'Trigger suggestions' })
-  const skillOption = await within(skillMenu).findByRole('option', { name: /fixture-demo/ })
-  const skillMenuText = visibleText(skillMenu)
-  fireEvent.mouseDown(skillOption)
-  await waitFor(() => { expect(composer.value).toBe('/fixture-demo ') })
-  const pickedSkill = composer.value
-  await typeComposer(composer, '')
-
-  // '/' 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,
-    skillMenuHadFixtureDemo: skillMenuText.includes('fixture-demo'),
-    skillPickInserted: pickedSkill,
-    sessionListed: visibleText(within(tree).getByText('1 session').closest('[role="treeitem"]')!),
-  }).toMatchInlineSnapshot(`
-    {
-      "composerSurvivedConversion": true,
-      "menuHadCompact": true,
-      "menuHadEcho": true,
-      "sessionListed": "nova1 session",
-      "skillMenuHadFixtureDemo": true,
-      "skillPickInserted": "/fixture-demo ",
-    }
-  `)
-})
diff --git a/apps/web/tests/snapshots/session-actions/rename-rows.json b/apps/web/tests/snapshots/session-actions/rename-rows.json
deleted file mode 100644
index db30b0d121..0000000000
--- a/apps/web/tests/snapshots/session-actions/rename-rows.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
-  {
-    "label": "fixture4 sessions"
-  },
-  {
-    "label": "New Sessionnow"
-  },
-  {
-    "label": "分叉 实验记录now"
-  },
-  {
-    "label": "fixture2min"
-  }
-]
diff --git a/apps/web/tests/snapshots/session-title.json b/apps/web/tests/snapshots/session-title.json
deleted file mode 100644
index 2063036803..0000000000
--- a/apps/web/tests/snapshots/session-title.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "initial": {
-    "sidebar": "Fixture 历史会话",
-    "breadcrumb": "Fixture 历史会话",
-    "documentTitle": "Fixture 历史会话 — DeepSeek Harness"
-  },
-  "revised": {
-    "sidebar": "Fixture 修订标题",
-    "breadcrumb": "Fixture 修订标题",
-    "documentTitle": "Fixture 修订标题 — DeepSeek Harness"
-  }
-}
diff --git a/apps/web/tests/terminal-card.snapshot.ts b/apps/web/tests/terminal-card.snapshot.ts
deleted file mode 100644
index 088a6e8326..0000000000
--- a/apps/web/tests/terminal-card.snapshot.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-// @vitest-environment jsdom
-// Terminal card 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 `card: 'terminal'` render
-// intent at both of its conversation render sites, for both chat-row shapes:
-// turn 60's `fx-bash` on the render-site fallback row (expand-gated body) and
-// turn 65's `bash` on the keyed BashRow registration (resident body). Turn 65
-// carries what turn 60's two clean prompt rows cannot — SGR runs resolved to
-// --dsw-* tokens, output past the chat cap, a nested cwd, and a non-zero exit
-// pill; turn 60 carries the multi-line command's per-line prompt rows.
-//
-// The details panel's Output section is NOT covered here: tool rows stopped
-// being details-panel click targets, and nothing else in the assembled
-// application opens that panel, so the surface cannot be driven end to end.
-// Its terminal rendering stays pinned in ui-conversation's
-// tests/terminal-card.spec.tsx, which mounts DetailsPanel with a selection
-// directly.
-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__
-  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()
-}
-
-/**
- * Read one terminal card's user-visible state. Output lines keep their interior
- * whitespace: holding column alignment is what this card exists for, so
- * collapsing runs of spaces would hide the behavior under test.
- */
-function readCard(card: Element) {
-  const status = card.querySelector('[class*="_status_"]')
-  const expander = card.querySelector('button[aria-expanded]')
-  return {
-    // One entry per command line: a multi-line command is one row per line.
-    prompt: [...card.querySelectorAll('[class*="_promptLine_"]')].map(row =>
-      `${row.querySelector('[class*="_cwd_"]')?.textContent ?? ''} ${row.querySelector('[class*="_command_"]')?.textContent ?? ''}`),
-    // Dots per prompt row: exactly one, on the first row — the exit status the
-    // view carries is the whole call's, so a dot per line would assert a
-    // per-line outcome bash does not report.
-    dotsPerPromptRow: [...card.querySelectorAll('[class*="_promptLine_"]')].map(row =>
-      row.querySelectorAll('[data-state]').length),
-    status: status === null ? null : status.textContent,
-    copy: card.querySelector('[class*="_copyButton_"]')?.textContent ?? null,
-    lines: [...card.querySelectorAll('[class*="_line_"]')].map(line => line.textContent),
-    expander: expander === null ? null : {
-      label: expander.getAttribute('aria-label'),
-      text: expander.textContent,
-      expanded: expander.getAttribute('aria-expanded'),
-    },
-    // The run-state dot at the head of the prompt line, by its StateDot state.
-    runState: card.querySelector('[class*="_runState_"][data-state]')?.getAttribute('data-state') ?? null,
-    runStateLabel: card.querySelector('[class*="_runStateLabel_"]')?.textContent ?? null,
-    // Every color the ANSI parser emits resolves through a --dsw-* token, so
-    // the card follows the theme instead of painting literal terminal rgb.
-    // Scoped to the output lines: the run-state dot is an inline-styled span
-    // too, and its geometry is not an ANSI-resolved color.
-    colors: [...new Set([...card.querySelectorAll('[class*="_line_"] span[style]')]
-      .map(span => span.getAttribute('style')))],
-  }
-}
-
-/** Open the fixture history session (the alpha log carrying both bash turns) and wait for its tail. */
-async function openFixtureSession(): Promise {
-  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".
-  const group = (await within(tree).findAllByText('fixture'))
-    .map(el => el.closest('[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')
-    })
-  }
-  fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
-  await waitFor(() => {
-    expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull()
-  }, { timeout: 10_000 })
-}
-
-/** The keyed BashRow of fixture turn 65 (the one carrying the ANSI sample). */
-function keyedBashRow(): Element {
-  // Anchored on the BashRow wrapper (summary row + resident card), not on the
-  // summary row itself: the summary now shows the presenter's description (the
-  // contract's above-card text), so the command lives only in the card below it.
-  const row = [...document.querySelectorAll('[data-sample="bash-global"]')]
-    .map(node => node.parentElement)
-    .find((node): node is HTMLElement => node !== null && visibleText(node).includes('pnpm run check'))
-  if (row === undefined) throw new Error('keyed bash row for turn 65 missing')
-  return row
-}
-
-/** The turn-60 fallback row, which reaches the terminal card through GenericToolCard/ToolRow. */
-function fallbackBashRow(): Element {
-  const row = document.querySelector('[data-tool="fx-bash"]')
-  if (row === null) throw new Error('fx-bash fallback row missing')
-  return row
-}
-
-it('renders the keyed bash row with a resident terminal card', async () => {
-  boot()
-  await openFixtureSession()
-
-  const row = keyedBashRow()
-  const card = row.parentElement?.querySelector('[data-terminal]')
-  if (card === null || card === undefined) throw new Error('keyed bash row has no resident terminal card')
-  // The prompt shortens the nested cwd to its last segment, the exit pill comes
-  // from the sample's authored exit status (its body deliberately carries no
-  // `[exit code: N]` marker, since the real presenter consumes that one), ANSI
-  // runs land on theme tokens, and the chat cap (8) collapses the middle into a
-  // head/tail split with an expander between them.
-  expect(readCard(card)).toMatchInlineSnapshot(`
-    {
-      "colors": [
-        "font-weight: 700;",
-        "color: var(--dsw-alias-state-success-primary);",
-        "color: var(--dsw-alias-state-error-primary);",
-      ],
-      "copy": "复制",
-      "dotsPerPromptRow": [
-        1,
-      ],
-      "expander": {
-        "expanded": "false",
-        "label": "展开其余 13 行输出",
-        "text": "… 其余 13 行",
-      },
-      "lines": [
-        "Running 4 checks",
-        "✓ typecheck                                          1.82s",
-        "✓ lint                                               0.94s",
-        "✓ duplication                                        2.10s",
-        "StateDot.tsx                100%     100%        100%         -",
-        "markdown/Markdown.tsx       100%     100%        100%         -",
-        "",
-        "1 of 4 checks failed",
-      ],
-      "prompt": [
-        "nested pnpm run check",
-      ],
-      "runState": "error",
-      "runStateLabel": "失败",
-      "status": "退出码 1",
-    }
-  `)
-})
-
-it('the fallback row reaches the same card through its expand control', async () => {
-  boot()
-  await openFixtureSession()
-
-  const row = fallbackBashRow()
-  expect(row.querySelector('[data-terminal]')).toBeNull()
-  const toggle = row.querySelector('button[aria-expanded]')
-  if (toggle === null) throw new Error('fallback row expand control missing')
-  fireEvent.click(toggle)
-  const card = await waitFor(() => {
-    const found = row.querySelector('[data-terminal]')
-    if (found === null) throw new Error('terminal card missing after expanding the fallback row')
-    return found
-  })
-  // Three plain lines under the cap: no ANSI spans, no exit pill, no expander.
-  expect(readCard(card)).toMatchInlineSnapshot(`
-    {
-      "colors": [],
-      "copy": "复制",
-      "dotsPerPromptRow": [
-        1,
-        0,
-      ],
-      "expander": null,
-      "lines": [
-        "total 2",
-        "drwxr-xr-x fixture",
-        "-rw-r--r-- demo.txt",
-      ],
-      "prompt": [
-        "fixture ls -la",
-        "$ echo done",
-      ],
-      "runState": "done",
-      "runStateLabel": "已完成",
-      "status": null,
-    }
-  `)
-})
-
-it('the chat card expands the collapsed middle in place, without opening the details panel', async () => {
-  boot()
-  await openFixtureSession()
-
-  const card = keyedBashRow().parentElement?.querySelector('[data-terminal]')
-  if (card === null || card === undefined) throw new Error('resident terminal card missing')
-  const expander = card.querySelector('button[aria-expanded]')
-  if (expander === null) throw new Error('height-cap expander missing')
-  const capped = card.querySelectorAll('[class*="_line_"]').length
-
-  fireEvent.click(expander)
-  await waitFor(() => {
-    expect(card.querySelector('button[aria-expanded]')?.getAttribute('aria-expanded')).toBe('true')
-  })
-  expect({
-    cappedLines: capped,
-    expandedLines: card.querySelectorAll('[class*="_line_"]').length,
-    expanderLabel: card.querySelector('button[aria-expanded]')?.getAttribute('aria-label'),
-    // The card sits outside the summary row's click target, so toggling it
-    // left the details panel shut.
-    detailsOpen: screen.queryByText('Input') !== null,
-  }).toMatchInlineSnapshot(`
-    {
-      "cappedLines": 8,
-      "detailsOpen": false,
-      "expandedLines": 21,
-      "expanderLabel": "收起输出",
-    }
-  `)
-})
diff --git a/apps/web/tests/todo-display.snapshot.ts b/apps/web/tests/todo-display.snapshot.ts
deleted file mode 100644
index da621b6e35..0000000000
--- a/apps/web/tests/todo-display.snapshot.ts
+++ /dev/null
@@ -1,213 +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).__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 {
-  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('[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": [],
-      "row": "更新任务清单1/3 已完成 · 实现 fixture 样本",
-      "rowState": "ok",
-    }
-  `)
-})
-
-it('expands the default-collapsed plan strip and restores its folded state', 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')
-
-  expect({
-    collapsedHeader: visibleText(header),
-    expanded: header.getAttribute('aria-expanded'),
-    listGone: panel.querySelector('ul') === null,
-  }).toMatchInlineSnapshot(`
-    {
-      "collapsedHeader": "To-dos1/3 tasks · 1 in progress",
-      "expanded": "false",
-      "listGone": true,
-    }
-  `)
-
-  fireEvent.click(header)
-  expect(panel.querySelectorAll('li')).toHaveLength(3)
-  expect(header.getAttribute('aria-expanded')).toBe('true')
-
-  fireEvent.click(header)
-  expect(panel.querySelector('ul')).toBeNull()
-  expect(header.getAttribute('aria-expanded')).toBe('false')
-})
-
-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('给智能体发消息', {}, { 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,
-    }
-  `)
-})
diff --git a/apps/web/tests/workspace-flow.snapshot.ts b/apps/web/tests/workspace-flow.snapshot.ts
deleted file mode 100644
index 86a20e73fe..0000000000
--- a/apps/web/tests/workspace-flow.snapshot.ts
+++ /dev/null
@@ -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).__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 {
-  return await screen.findByPlaceholderText(
-    'Choose a workspace to start', {}, { timeout: 10_000 },
-  )
-}
-
-/** The live blank-session hero composer (session materialized). */
-async function findHeroComposer(): Promise {
-  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 {
-  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 {
-  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('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('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",
-    }
-  `)
-})
diff --git a/packages/client/runtime/tests/workspaces-service.spec.ts b/packages/client/runtime/tests/workspaces-service.spec.ts
index 54ec218765..7fd6934827 100644
--- a/packages/client/runtime/tests/workspaces-service.spec.ts
+++ b/packages/client/runtime/tests/workspaces-service.spec.ts
@@ -286,3 +286,78 @@ describe('WorkspacesService', () => {
     await expect(workspaces.delete(wid('ghost'))).rejects.toThrow(/workspace-not-found: gone/)
   })
 })
+
+describe('startInitialSelection', () => {
+  function bench() {
+    const ctx = new Context()
+    const api = new FakeApiClient()
+    const sessions = new SessionsService(ctx, api)
+    const workspaces = new WorkspacesService(ctx, api, sessions)
+    return { api, sessions, workspaces }
+  }
+
+  it('connects the recent Workspace blank session once baselines are ready and opens it', async () => {
+    const b = bench()
+    const stop = b.workspaces.startInitialSelection()
+    // Nothing happens before both baselines land.
+    expect(b.api.callsOf('session.create')).toHaveLength(0)
+
+    b.api.onWorkspaceList = () => Promise.resolve(ok({
+      items: [workspace('recent', [], '2026-01-02T00:00:00.000Z')] as never[],
+    }))
+    b.api.onCreate = () => Promise.resolve(ok({ sessionId: sid('s-new') }))
+    await b.workspaces.refresh()
+    await b.sessions.refresh()
+    // Store notifications and the connect round trip are microtask-batched.
+    await new Promise(resolve => setTimeout(resolve, 0))
+    expect(b.api.callsOf('session.create')).toEqual([{ workspaceId: 'recent' }])
+    expect(b.sessions.list.getSnapshot().current).toBe('s-new')
+    stop()
+  })
+
+  it('stays idle when a session is already current or no recent Workspace exists', async () => {
+    const withCurrent = bench()
+    withCurrent.api.onList = () => Promise.resolve(ok({
+      items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }] as never[],
+    }))
+    await withCurrent.sessions.refresh()
+    withCurrent.sessions.open(sid('s1'))
+    withCurrent.api.onWorkspaceList = () => Promise.resolve(ok({ items: [workspace('w1', [sid('s1')])] as never[] }))
+    const stopCurrent = withCurrent.workspaces.startInitialSelection()
+    await withCurrent.workspaces.refresh()
+    await new Promise(resolve => setTimeout(resolve, 0))
+    expect(withCurrent.api.callsOf('session.create')).toHaveLength(0)
+    stopCurrent()
+
+    const noRecent = bench()
+    const stopEmpty = noRecent.workspaces.startInitialSelection()
+    await noRecent.workspaces.refresh()
+    await noRecent.sessions.refresh()
+    await new Promise(resolve => setTimeout(resolve, 0))
+    expect(noRecent.api.callsOf('session.create')).toHaveLength(0)
+    expect(() => noRecent.workspaces.startInitialSelection()).toThrow(/already started/)
+    stopEmpty()
+  })
+
+  it('a failed connect returns to waiting and retries on the next list change', async () => {
+    const b = bench()
+    b.api.onWorkspaceList = () => Promise.resolve(ok({
+      items: [workspace('recent', [], '2026-01-02T00:00:00.000Z')] as never[],
+    }))
+    b.api.onCreate = () => Promise.resolve(err({ code: 'internal', message: 'attach exploded', details: {} }))
+    const stop = b.workspaces.startInitialSelection()
+    await b.workspaces.refresh()
+    await b.sessions.refresh()
+    await new Promise(resolve => setTimeout(resolve, 0))
+    expect(b.api.callsOf('session.create')).toHaveLength(1)
+    expect(b.sessions.list.getSnapshot().current).toBeUndefined()
+
+    // Recovery: the next workspace-list change re-runs the reconcile.
+    b.api.onCreate = () => Promise.resolve(ok({ sessionId: sid('s-retry') }))
+    await b.workspaces.refresh()
+    await new Promise(resolve => setTimeout(resolve, 0))
+    expect(b.api.callsOf('session.create')).toHaveLength(2)
+    expect(b.sessions.list.getSnapshot().current).toBe('s-retry')
+    stop()
+  })
+})
diff --git a/packages/client/test-runtime/src/sessions.ts b/packages/client/test-runtime/src/sessions.ts
index fb117d74c4..4fdfb32cc0 100644
--- a/packages/client/test-runtime/src/sessions.ts
+++ b/packages/client/test-runtime/src/sessions.ts
@@ -245,6 +245,20 @@ export class TestSessions implements ISessions {
     await this.stabilize(() => { record.snapshot.update(mutate) })
   }
 
+  /**
+   * Update a session's list row (the wire-echo stand-in: title settles,
+   * running flips — components subscribed via useSessions re-render).
+   * @param id - session id.
+   * @param patch - summary fields to merge over the row.
+   */
+  async updateSummary(id: string, patch: Partial>): Promise {
+    const record = this.require(id)
+    record.summary = { ...record.summary, ...patch }
+    await this.stabilize(() => {
+      this.list.update((draft) => { draft.byId[id as SessionId] = record.summary })
+    })
+  }
+
   /**
    * Switch the current selection (undefined = the no-session empty state).
    * @param id - session id to select, or undefined to clear.
diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx
new file mode 100644
index 0000000000..5d7f4c05e7
--- /dev/null
+++ b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx
@@ -0,0 +1,245 @@
+// @vitest-environment jsdom
+/**
+ * Assembly-level acceptance on SlotTestRuntime (real apply, real slot
+ * machinery, real renderer; data fed as fixtures) for surfaces that were
+ * previously pinned only by the assembled-app jsdom snapshots
+ * (apps/web/tests/{todo-display,terminal-card,slash-flow}.snapshot.ts):
+ *
+ * - the todo_write turn reaches BOTH surfaces through the product
+ *   registrations (keyed toolview row in the flow, plan strip in the input
+ *   dock via the 'todos' projection) and the strip follows projection
+ *   retirement;
+ * - the bash keyed row carries its resident terminal card, and the fallback
+ *   row reaches the same card through its expand control;
+ * - the resident composer textarea survives the blank→active conversion as
+ *   the SAME DOM node (focus/IME continuity rides React reconciliation:
+ *   component identity + tree position, which this assembled tree pins).
+ *
+ * Component-level behavior (collapse interaction, card model arms, summary
+ * derivations) lives in todo-panel.spec.tsx / terminal-card.spec.tsx; this
+ * suite only proves the assembled wiring.
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, fireEvent, waitFor, within } from '@testing-library/react'
+import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
+import type { ISession, SessionId, TodoItem, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
+import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
+import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
+import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client'
+
+const SID = 's1' as SessionId
+
+afterEach(cleanup)
+beforeEach(() => {
+  localStorage.clear()
+})
+
+const TODOS: TodoItem[] = [
+  { content: '梳理需求', status: 'completed' },
+  { content: '实现 fixture 样本', status: 'in_progress' },
+  { content: '浏览器验收', status: 'pending' },
+]
+
+const todoResult = (seq: number): ToolResultNode => ({
+  kind: 'tool-result', seq, time: seq * 1_000, callId: `todo-${seq}`,
+  call: { name: 'todo_write', argsRaw: JSON.stringify({ todos: TODOS }) },
+  callTime: seq * 1_000 - 500,
+  content: [], isError: false, callView: null, resultView: null,
+})
+
+const bashResult = (seq: number, callId: string, over?: Partial): ToolResultNode => ({
+  kind: 'tool-result', seq, time: seq * 1_000, callId,
+  call: { name: 'bash', argsRaw: '{"command":"ls -la","description":"List files"}' },
+  callTime: seq * 1_000 - 500,
+  content: [{ type: 'text', text: 'total 2\ndemo.txt\n' }], isError: false,
+  callView: { card: 'terminal', title: 'ls -la', description: 'List files' },
+  resultView: { card: 'terminal', output: 'total 2\ndemo.txt\n', exitCode: 0 },
+  ...over,
+})
+
+/** Test-owned AppFrame role: declares and renders the resident conversation area. */
+type AppRootProps = PropsRenderSlots<'conversation' | 'details'>
+function AppRoot({ renderSlot }: AppRootProps) {
+  return <>{renderSlot('conversation', {})}
+}
+
+const LAYOUT_CHILDREN = {
+  'conversation': { kind: 'single', scope: 'session-maybe' },
+  'details': { kind: 'single', scope: 'session' },
+} as const
+
+async function bench(nodes: ToolResultNode[], opts?: { blank?: boolean }) {
+  const runtime = await SlotTestRuntime.create()
+  runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
+  runtime.provide('locale', new LocaleService(runtime.ctx))
+  await runtime.sessions.add({
+    id: SID,
+    summary: { title: 'S', displayTitle: 'S', cwd: '/proj' },
+    snapshot: {
+      nodes,
+      ...(opts?.blank === true ? { blank: true, composerPhase: 'blank' as const } : {}),
+    },
+    session: {
+      loadOlder: vi.fn(),
+      prompt: vi.fn(async () => ({ ok: true, value: { accepted: true } })),
+    },
+  })
+  await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
+  await runtime.mount({ inject: [...inject], apply })
+  return runtime
+}
+
+describe('todo_write assembly (product registrations, no outlet twins)', () => {
+  it('reaches the keyed toolview row and the dock plan strip, and the strip follows projection retirement', async () => {
+    const runtime = await bench([todoResult(3)])
+    // The dock strip reads the host-computed 'todos' projection.
+    runtime.sessions.behavior(SID).projections.set('todos', TODOS)
+    const view = runtime.renderRoot()
+
+    // Keyed toolview registration took the row (summary derived from args).
+    const row = view.container.querySelector('[data-sample="todo-row"]')
+    expect(row).not.toBeNull()
+    expect(row!.textContent).toContain('1/3 已完成 · 实现 fixture 样本')
+
+    // The plan strip sits in the input dock, fed by the projection
+    // (default-collapsed: the header summary shows; rows appear on expand).
+    const panel = view.container.querySelector('[data-testid="todo-panel"]')
+    expect(panel).not.toBeNull()
+    expect(panel!.textContent).toContain('1/3 tasks · 1 in progress')
+    fireEvent.click(panel!.querySelector('button')!)
+    expect([...panel!.querySelectorAll('li')].map(li => li.getAttribute('data-status')))
+      .toEqual(['completed', 'in_progress', 'pending'])
+
+    // Next turn retires the standing plan (host pushes null): the strip
+    // clears while the historical row stays in the flow.
+    await runtime.flush()
+    runtime.sessions.behavior(SID).projections.set('todos', null)
+    await waitFor(() => {
+      expect(view.container.querySelector('[data-testid="todo-panel"]')).toBeNull()
+    })
+    expect(view.container.querySelector('[data-sample="todo-row"]')).not.toBeNull()
+    await runtime.dispose()
+  })
+})
+
+describe('terminal card assembly', () => {
+  it('the keyed bash row carries a resident terminal card; the fallback row reaches one through expand', async () => {
+    const runtime = await bench([
+      bashResult(3, 'c-keyed'),
+      // An unregistered tool with terminal views: GenericToolCard fallback.
+      bashResult(4, 'c-fallback', { call: { name: 'fx-bash', argsRaw: '{"command":"ls -la"}' } }),
+    ])
+    const view = runtime.renderRoot()
+
+    // Keyed BashRow renders the card residently (no expand gesture).
+    const keyed = view.container.querySelector('[data-sample="bash-global"]')?.parentElement
+    expect(keyed?.querySelector('[data-terminal]')).not.toBeNull()
+
+    // Fallback row: card appears only after its expand control.
+    const fallback = view.container.querySelector('[data-tool="fx-bash"]')
+    expect(fallback).not.toBeNull()
+    expect(fallback!.querySelector('[data-terminal]')).toBeNull()
+    fireEvent.click(fallback!.querySelector('button[aria-expanded]')!)
+    await waitFor(() => {
+      expect(fallback!.querySelector('[data-terminal]')).not.toBeNull()
+    })
+    await runtime.dispose()
+  })
+})
+
+describe('resident composer', () => {
+  it('renders the locked view state while no session exists at all', async () => {
+    const runtime = await SlotTestRuntime.create()
+    runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
+    runtime.provide('locale', new LocaleService(runtime.ctx))
+    await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
+    await runtime.mount({ inject: [...inject], apply })
+    const view = runtime.renderRoot()
+    // No session entity: the inert twin renders (disabled textarea), and the
+    // workspace picker chip is the only live control.
+    const textarea = view.container.querySelector('textarea')
+    expect(textarea).not.toBeNull()
+    expect(textarea!.disabled).toBe(true)
+    expect(view.getByRole('button', { name: 'Choose workspace' })).toBeTruthy()
+    await runtime.dispose()
+  })
+
+
+  it('the textarea survives the blank→active conversion as the same DOM node', async () => {
+    const runtime = await bench([], { blank: true })
+    // The hero renders the LIVE composer only when the blank session's
+    // workspace resolves a chip title; an ownerless blank session shows the
+    // disabled twin instead (deleted-workspace semantics).
+    await runtime.workspaces.update((draft) => {
+      draft.items = [{ workspaceId: 'w1', title: 'Proj', path: '/proj', sessionIds: [SID] }] as never
+    })
+    const view = runtime.renderRoot()
+    const hero = view.container.querySelector('textarea')
+    expect(hero).not.toBeNull()
+    expect(hero!.disabled).toBe(false)
+
+    // First acceptance: the session leaves blank and the composer docks.
+    await runtime.sessions.updateSnapshot(SID, (draft) => {
+      draft.blank = false
+      draft.composerPhase = 'active'
+    })
+    const docked = view.container.querySelector('textarea')
+    expect(docked).toBe(hero)
+    await runtime.dispose()
+  })
+})
+
+describe('prompt rejection through the assembled composer', () => {
+  it('renders the promptError alert strip and keeps the draft in the machine', async () => {
+    const runtime = await SlotTestRuntime.create()
+    runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
+    runtime.provide('locale', new LocaleService(runtime.ctx))
+    const prompt = vi.fn(async () => ({
+      ok: false, error: { code: 'agent-busy', message: 'prompt rejected before acceptance', details: { reason: 'busy' } },
+    }))
+    await runtime.sessions.add({
+      id: SID,
+      summary: { title: 'S', displayTitle: 'S', cwd: '/proj' },
+      session: { prompt, loadOlder: vi.fn() },
+    })
+    await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
+    await runtime.mount({ inject: [...inject], apply })
+    const view = runtime.renderRoot()
+
+    const composer = view.container.querySelector('textarea')!
+    fireEvent.change(composer, { target: { value: 'do not lose this' } })
+    fireEvent.keyDown(composer, { key: 'Enter' })
+    await waitFor(() => { expect(prompt).toHaveBeenCalledOnce() })
+
+    // The rejection lands in snapshot.promptError (the Session's own path);
+    // the fixture mirrors that hop — the assembled InputBar renders it.
+    await runtime.sessions.updateSnapshot(SID, (draft) => {
+      draft.promptError = {
+        op: 'send',
+        error: { code: 'agent-busy', message: 'prompt rejected before acceptance', details: { reason: 'busy' } },
+      }
+    })
+    const alert = await view.findByRole('alert')
+    expect(alert.textContent).toContain('prompt rejected before acceptance (agent-busy)')
+    // Failure restore: the machine returned the draft to the same textarea.
+    await waitFor(() => {
+      expect((view.container.querySelector('textarea'))!.value).toBe('do not lose this')
+    })
+    await runtime.dispose()
+  })
+})
+
+describe('title projection across assembled surfaces', () => {
+  it('one summary update re-labels the breadcrumb and document.title consumers together', async () => {
+    const runtime = await bench([])
+    const view = runtime.renderRoot()
+    // The strict session header breadcrumb reads useSessions ancestry.
+    const crumb = within(view.container.querySelector('[aria-label="Session hierarchy"]') as HTMLElement)
+    expect(crumb.getByText('S')).toBeTruthy()
+
+    await runtime.sessions.updateSummary(SID, { displayTitle: '修订标题', title: '修订标题' })
+    await waitFor(() => { expect(crumb.getByText('修订标题')).toBeTruthy() })
+    expect(crumb.queryByText('S')).toBeNull()
+    await runtime.dispose()
+  })
+})
diff --git a/packages/client/ui-workspace/tests/rename-assembly.spec.tsx b/packages/client/ui-workspace/tests/rename-assembly.spec.tsx
new file mode 100644
index 0000000000..bdbeee845f
--- /dev/null
+++ b/packages/client/ui-workspace/tests/rename-assembly.spec.tsx
@@ -0,0 +1,118 @@
+// @vitest-environment jsdom
+/**
+ * The session-rename assembly chain on SlotTestRuntime (real apply, real
+ * WorkspaceBrowser occupying the sidebar hole): row menu → rename dialog →
+ * the injected renameSession hop (sessions.binding → ISession.rename) → on
+ * the accepted unary response the dialog closes and the row re-labels from
+ * the list state — no push-frame wait. Previously pinned only by the
+ * assembled-app snapshot (apps/web/tests/session-actions.snapshot.ts); the
+ * verb's wire behavior stays with the runtime package
+ * (session.spec.ts#rename), the dialog's own arms with rows.spec /
+ * workspace-browser.spec.
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, fireEvent, waitFor, within } from '@testing-library/react'
+import type { ISession, SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
+import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
+import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
+import { apply, inject } from '@deepseek-ai/dsh-client-ui-workspace/client'
+
+const SID = 's1' as SessionId
+
+afterEach(cleanup)
+beforeEach(() => { localStorage.clear() })
+
+/** Test-owned sidebar shell role: declares and renders the browsing region. */
+type FrameProps = PropsRenderSlots<'sidebar.workspaces'>
+function SidebarFrame({ renderSlot }: FrameProps) {
+  return <>{renderSlot('sidebar.workspaces', { wide: true, expandSidebar: () => {} })}
+}
+
+describe('session rename through the assembled browser', () => {
+  it('renames via the row menu: binding.session.rename fires, the dialog closes, the row re-labels from the list', async () => {
+    const runtime = await SlotTestRuntime.create()
+    const rename = vi.fn(async title => ({
+      ok: true, value: { title: title.trim().replace(/\s+/g, ' '), seq: 7 },
+    }))
+    await runtime.sessions.add({
+      id: SID,
+      summary: { title: '旧标题', displayTitle: '旧标题', cwd: '/w/alpha' },
+      session: { rename },
+    })
+    await runtime.workspaces.update((draft) => {
+      draft.items = [{
+        workspaceId: 'w1' as WorkspaceId, title: 'alpha', path: '/w/alpha',
+        sessionIds: [SID], createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
+      }] as never
+    })
+    await runtime.root.declare(
+      { 'sidebar.workspaces': { kind: 'single', scope: 'root' } } as never,
+      SidebarFrame as never,
+    )
+    await runtime.mount({ inject: [...inject], apply })
+    const view = runtime.renderRoot()
+
+    // The current session's group auto-expands; open the row's action menu.
+    const row = (await view.findByText('旧标题')).closest('[role="treeitem"]')!
+    fireEvent.click(within(row as HTMLElement).getByLabelText('Session actions for 旧标题'))
+    fireEvent.click(view.getByRole('menuitem', { name: 'Rename', hidden: true }))
+
+    // The dialog seeds from the current title; submit a padded value.
+    const input = await view.findByLabelText('Session name') as HTMLInputElement
+    expect(input.value).toBe('旧标题')
+    fireEvent.change(input, { target: { value: '  分叉  实验记录  ' } })
+    fireEvent.click(view.getByRole('button', { name: 'Rename' }))
+
+    // The injected hop reached the session face with the edge-trimmed draft
+    // (the dialog trims edges; interior normalization is host-side).
+    await waitFor(() => { expect(rename).toHaveBeenCalledWith('分叉  实验记录') })
+    // Acceptance closes the dialog without any push-frame wait.
+    await waitFor(() => { expect(view.queryByLabelText('Session name')).toBeNull() })
+    // The manager lands the unary echo in the list store (its own package
+    // tests own that hop); the row re-labels from list state alone.
+    await runtime.sessions.updateSummary(SID, { displayTitle: '分叉 实验记录', title: '分叉 实验记录' })
+    await view.findByText('分叉 实验记录')
+    expect(view.queryByText('旧标题')).toBeNull()
+    await runtime.dispose()
+  })
+
+  it('a rejected rename keeps the dialog open with the error surfaced', async () => {
+    const runtime = await SlotTestRuntime.create()
+    const rename = vi.fn(async () => ({
+      ok: false, error: { code: 'internal', message: 'title write failed', details: {} },
+    }))
+    await runtime.sessions.add({
+      id: SID,
+      summary: { title: '旧标题', displayTitle: '旧标题', cwd: '/w/alpha' },
+      session: { rename },
+    })
+    await runtime.workspaces.update((draft) => {
+      draft.items = [{
+        workspaceId: 'w1' as WorkspaceId, title: 'alpha', path: '/w/alpha',
+        sessionIds: [SID], createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
+      }] as never
+    })
+    await runtime.root.declare(
+      { 'sidebar.workspaces': { kind: 'single', scope: 'root' } } as never,
+      SidebarFrame as never,
+    )
+    await runtime.mount({ inject: [...inject], apply })
+    const view = runtime.renderRoot()
+    await runtime.flush()
+
+    const row = (await view.findByText('旧标题')).closest('[role="treeitem"]')!
+    fireEvent.click(within(row as HTMLElement).getByLabelText('Session actions for 旧标题'))
+    fireEvent.click(view.getByRole('menuitem', { name: 'Rename', hidden: true }))
+    const input = await view.findByLabelText('Session name')
+    fireEvent.change(input, { target: { value: '新名' } })
+    fireEvent.click(view.getByRole('button', { name: 'Rename' }))
+
+    // Failure: the injected hop rethrows the business error; the dialog
+    // stays open with the alert and the row keeps its title.
+    const alert = await view.findByRole('alert')
+    expect(alert.textContent).toContain('title write failed')
+    expect(view.getByLabelText('Session name')).toBeTruthy()
+    expect(view.getByText('旧标题')).toBeTruthy()
+    await runtime.dispose()
+  })
+})