)
+ await act(async () => { await Promise.resolve() })
expect(row.getAttribute('hidden')).toBe(hiddenValue)
expectCleared(row)
expect(motion.foldActive()).toBe(false)
diff --git a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx
index fbd21baf11..682777dbef 100644
--- a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx
+++ b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx
@@ -21,6 +21,119 @@ afterEach(() => {
const t = makeTranslate(zh, commonZh)
const renderMessageImages: AssistantMarkdownProps['renderMessageImages'] = () => null
+describe('AssistantMarkdown reasoning blocks', () => {
+ const props = {
+ renderSlot, useDisclosure, useGroupAction: useSearchableHidden,
+ usePresentation: useDetailedPresentation, renderMessageImages, t,
+ }
+
+ it.each([false, true])('omits blank blocks without trimming body text when streaming=%s', (streaming) => {
+ const text = '\n **Visible reasoning**\n\nOriginal body \n'
+ const texts = ['', ' \t\r\n', text, '']
+ const blocks = Object.freeze(texts.map(text => Object.freeze({ kind: 'reasoning' as const, text })))
+ const calls = vi.fn()
+ const extension: AssistantMarkdownProps['renderSlot'] = (key, owner, options) => {
+ calls(key, owner)
+ return renderSlot(key, owner, options)
+ }
+ const view = render()
+ expect(view.getAllByRole('button')).toHaveLength(1)
+ expect(view.container.querySelectorAll('[data-variant="think"]')).toHaveLength(1)
+ fireEvent.click(view.getByRole('button'))
+ expect(calls).toHaveBeenCalledWith('conversation.chat.reasoning.body', { text, running: false })
+ expect(view.getByText('Visible reasoning').tagName).toBe('STRONG')
+ expect(blocks.map(block => block.text)).toEqual(texts)
+ })
+
+ it.each([false, true])('waits for non-whitespace reasoning when streaming=%s', (streaming) => {
+ const view = render()
+ expect(view.queryAllByRole('button')).toHaveLength(0)
+ for (const text of [' ', '\t', '\r\n', ' \t\r\n', '\u00a0']) {
+ view.rerender()
+ expect(view.queryAllByRole('button')).toHaveLength(0)
+ }
+ view.rerender()
+ expect(view.getAllByRole('button')).toHaveLength(1)
+ })
+
+ it('retains an expanded earlier row when a blank streaming tail receives text', () => {
+ const first = { kind: 'reasoning' as const, text: 'First thought\n\nEarlier body' }
+ const blank = { kind: 'reasoning' as const, text: ' \t\r\n' }
+ const view = render()
+ const earlierToggle = view.getByRole('button')
+ const earlierRow = earlierToggle.closest('[data-variant="think"]')
+ expect(earlierRow?.getAttribute('data-state')).toBe('ok')
+ expect(view.queryByText('运行中')).toBeNull()
+ fireEvent.click(earlierToggle)
+ const earlierBody = view.getByText('Earlier body')
+
+ const tail = { kind: 'reasoning' as const, text: 'An unfinished tail' }
+ const blocks = [first, blank, tail]
+ view.rerender()
+ const toggles = view.getAllByRole('button')
+ expect(toggles).toHaveLength(2)
+ expect(toggles[0]).toBe(earlierToggle)
+ expect(view.getByText('Earlier body')).toBe(earlierBody)
+ expect(earlierToggle.getAttribute('aria-expanded')).toBe('true')
+ expect(earlierRow?.getAttribute('data-state')).toBe('ok')
+ const tailToggle = toggles[1]!
+ const tailRow = tailToggle.closest('[data-variant="think"]')
+ expect(tailRow?.getAttribute('data-state')).toBe('running')
+ expect(view.queryByText(tail.text)).toBeNull()
+ fireEvent.click(tailToggle)
+ expect(view.getByText(tail.text)).toBeTruthy()
+ expect(earlierToggle.getAttribute('aria-expanded')).toBe('true')
+
+ view.rerender()
+ const settledToggles = view.getAllByRole('button')
+ expect(settledToggles).toHaveLength(2)
+ expect(settledToggles[0]).toBe(earlierToggle)
+ expect(settledToggles[1]).toBe(tailToggle)
+ expect(view.getByText('Earlier body')).toBe(earlierBody)
+ expect(tailRow?.getAttribute('data-state')).toBe('ok')
+ expect(tailToggle.getAttribute('aria-expanded')).toBe('true')
+ fireEvent.click(tailToggle)
+ expect(earlierToggle.getAttribute('aria-expanded')).toBe('true')
+ expect(tailToggle.getAttribute('aria-expanded')).toBe('false')
+ })
+
+ it.each(['Partial thought', '', ' \t\r\n'])('retains the stopped marker with blank reasoning around %j', (text) => {
+ const blocks = ['', text, ' \t\r\n'].map(text => ({ kind: 'reasoning' as const, text }))
+ const view = render()
+ expect(view.queryAllByRole('button')).toHaveLength(text.trim() === '' ? 0 : 1)
+ expect(view.getByText(t('message.stopped'))).toBeTruthy()
+ expect(view.queryByText('运行中')).toBeNull()
+ })
+
+ it('keeps non-blank rows and their expanded bodies across modes, excluding them from the response part', () => {
+ const mode = createSnapshotStore('compact')
+ const usePresentation = bindSnapshotSelector(derivePresentationPolicy(mode, createSnapshotStore('completion')))
+ const blocks: AssistantMarkdownProps['blocks'] = [
+ { kind: 'reasoning', text: '' },
+ { kind: 'reasoning', text: 'Mode thought\n\nMode body' },
+ { kind: 'reasoning', text: ' \t\r\n' },
+ { kind: 'text', text: 'Answer' },
+ ]
+ const view = render()
+ const toggle = view.getByRole('button')
+ fireEvent.click(toggle)
+ const body = view.getByText('Mode body')
+ for (const next of ['standard', 'detailed', 'verbose', 'compact'] as const) {
+ act(() => { mode.set(next) })
+ expect(view.getAllByRole('button')).toHaveLength(1)
+ expect(view.getByRole('button')).toBe(toggle)
+ expect(toggle.getAttribute('aria-expanded')).toBe('true')
+ expect(view.getByText('Mode body')).toBe(body)
+ expect(view.queryByText('Answer')).toBeNull()
+ }
+ view.rerender()
+ expect(view.queryAllByRole('button')).toHaveLength(0)
+ expect(view.getByText('Answer')).toBeTruthy()
+ })
+})
+
describe('ReasoningRow', () => {
it('offers only text and streaming state to the standard Body Slot after expansion', () => {
const calls = vi.fn()
diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml
index c2510a38bf..e622c16fa1 100644
--- a/packages/client/ui-primitives/README.i18n.yaml
+++ b/packages/client/ui-primitives/README.i18n.yaml
@@ -42,8 +42,8 @@
en: 8bbfa14c27f3bfd7
zh: 88f7548ac3a114e1
/deepseek-ai-dsh-client-ui-primitives/understand-the-implementation/geometry-and-overflow:
- en: e283bf2366b43879
- zh: d089076d7834e853
+ en: 45f4cd1ef96bb6d2
+ zh: f264d25b780ad1dc
/deepseek-ai-dsh-client-ui-primitives/further-exploration:
en: e82b713cdb2f954b
zh: a877f34b148f433f
diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md
index c4f3467c8e..53643c1e1f 100644
--- a/packages/client/ui-primitives/README.md
+++ b/packages/client/ui-primitives/README.md
@@ -161,7 +161,7 @@ While a reply streams, `MarkdownText` parses incrementally: all but the trailing
### Geometry and overflow
-The output cards share one geometry model: `white-space: pre` with horizontal scrolling so column-aligned content keeps its alignment, and a head-plus-tail slice behind an expand button past `maxLines` (default 16) so a long body never stretches the card. `TerminalBlock` uses `CommandText` to keep each authored command line selectable and horizontally scrollable; the prompt and status stay fixed. Only horizontally overflowing text enters the Tab order as a named group, using the owner’s localized `commandLine(line)` label. Text that fits, including wrapped Jobs commands, adds no Tab stop. Size, text, and font changes update focusability without changing the scroll position. `TerminalBlock` parses ANSI into React spans with a per-line column buffer for cursor movement, honoring erase-in-line, tab stops, and character width. Hosts opt out of the shared geometry per surface: rebinding `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` to `pre-wrap` wraps commands and output in full with no sideways scroll, `maxLines: Infinity` disables the fold for hosts capping height through `--dsl-terminal-output-max-height` instead, `copyText` overrides the copy payload (and keeps the control rendered before any output), and `runStateDot: false` omits the run-state dot when the surrounding row already carries the state, reclaiming its gutter via `--dsl-terminal-gutter`. The banner divider follows the rendered body, so a running card that streams live output separates its command from the text like a settled card.
+The output cards share one geometry model: `white-space: pre` with horizontal scrolling so column-aligned content keeps its alignment, and a head-plus-tail slice behind an expand button past `maxLines` (default 16) so a long body never stretches the card. `TerminalBlock` uses `CommandText` to keep each authored command line selectable and horizontally scrollable; the prompt and status stay fixed. The run-state dot aligns with the first visual command line, including when the command wraps. Only horizontally overflowing text enters the Tab order as a named group, using the owner’s localized `commandLine(line)` label. Text that fits, including wrapped Jobs commands, adds no Tab stop. Size, text, and font changes update focusability without changing the scroll position. `TerminalBlock` parses ANSI into React spans with a per-line column buffer for cursor movement, honoring erase-in-line, tab stops, and character width. Hosts opt out of the shared geometry per surface: rebinding `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` to `pre-wrap` wraps commands and output in full with no sideways scroll, `maxLines: Infinity` disables the fold for hosts capping height through `--dsl-terminal-output-max-height` instead, `copyText` overrides the copy payload (and keeps the control rendered before any output), and `runStateDot: false` omits the run-state dot when the surrounding row already carries the state, reclaiming its gutter via `--dsl-terminal-gutter`. `background` defaults to false and carries the call's background flag. For a background call, `running` indicates launch-call progress: Start shows that indicator; the acknowledgement sets `running` to false and leaves the command static without a state dot or assistive label. Background calls omit output, empty-output placeholders, exit pills, and the default output Copy control; an explicit `copyText` override remains available. The banner divider follows the rendered body, so a running card that streams live output separates its command from the text like a settled card.
diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md
index 48cec6c9c3..2a23be3e95 100644
--- a/packages/client/ui-primitives/README.zh.md
+++ b/packages/client/ui-primitives/README.zh.md
@@ -161,7 +161,7 @@ kind: "package-library"
### 几何与溢出
-输出卡片共享同一套几何模型:`white-space: pre` 并横向滚动,让按列对齐的内容保持对齐;超过 `maxLines`(默认 16)时折叠为头部切片加尾部切片,由展开按钮控制,长正文不会撑高卡片。`TerminalBlock` 通过 `CommandText` 让每条原始命令行可选中并横向滚动;提示符和状态保持固定。只有横向溢出的文本以具名 group 进入 Tab 顺序,名称使用 owner 的本地化 `commandLine(line)` 标签。能完整显示的文本(包括 Jobs 中已换行的命令)不会添加 Tab 停靠点。尺寸、文本与字体变化会更新可聚焦状态,不改变滚动位置。`TerminalBlock` 把 ANSI 解析为 React span,并带逐行列缓冲处理光标移动,遵循行内擦除、制表位与字符宽度。宿主可按表层选择退出共享几何:把 `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` 重绑为 `pre-wrap` 让命令与输出完整换行且不横向滚动;`maxLines: Infinity` 为改用 `--dsl-terminal-output-max-height` 限高滚动的宿主禁用折叠;`copyText` 覆盖复制载荷(并让控件在任何输出出现之前就保持渲染);`runStateDot: false` 在外围行已携带同一状态时省去状态点,并经 `--dsl-terminal-gutter` 收回其落区。横幅分割线跟随渲染出的正文:正在流式输出的 running 卡片像已结束卡片一样把命令与文本分隔开。
+输出卡片共享同一套几何模型:`white-space: pre` 并横向滚动,让按列对齐的内容保持对齐;超过 `maxLines`(默认 16)时折叠为头部切片加尾部切片,由展开按钮控制,长正文不会撑高卡片。`TerminalBlock` 通过 `CommandText` 让每条原始命令行可选中并横向滚动;提示符和状态保持固定。状态点始终对齐命令的第一个视觉行,包括命令自动折行时。只有横向溢出的文本以具名 group 进入 Tab 顺序,名称使用 owner 的本地化 `commandLine(line)` 标签。能完整显示的文本(包括 Jobs 中已换行的命令)不会添加 Tab 停靠点。尺寸、文本与字体变化会更新可聚焦状态,不改变滚动位置。`TerminalBlock` 把 ANSI 解析为 React span,并带逐行列缓冲处理光标移动,遵循行内擦除、制表位与字符宽度。宿主可按表层选择退出共享几何:把 `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` 重绑为 `pre-wrap` 让命令与输出完整换行且不横向滚动;`maxLines: Infinity` 为改用 `--dsl-terminal-output-max-height` 限高滚动的宿主禁用折叠;`copyText` 覆盖复制载荷(并让控件在任何输出出现之前就保持渲染);`runStateDot: false` 在外围行已携带同一状态时省去状态点,并经 `--dsl-terminal-gutter` 收回其落区。`background` 默认为 false,透传调用的后台标志。后台调用的 `running` 表示启动调用进度:Start 显示该指示,回执使 `running` 变为 false,命令保持静态且不显示状态点或辅助标签。后台调用不显示输出、空输出提示、退出状态 pill 或默认输出 Copy 控件,显式 `copyText` 覆盖仍可使用。横幅分割线跟随渲染出的正文:正在流式输出的 running 卡片像已结束卡片一样把命令与文本分隔开。
diff --git a/packages/client/ui-primitives/src/TerminalBlock.module.css b/packages/client/ui-primitives/src/TerminalBlock.module.css
index 71196558b1..5d8998d647 100644
--- a/packages/client/ui-primitives/src/TerminalBlock.module.css
+++ b/packages/client/ui-primitives/src/TerminalBlock.module.css
@@ -87,11 +87,11 @@
/* Out of flow inside the card's own gutter padding, so the reservation and the
dot move together and no consumer margin can pull them apart; the dot neither
indents its command nor depends on the command's text metrics to line up.
- Centered against the row's line box, not the code font's baseline. */
+ Centered against the first visual line box, not the code font's baseline. */
.runState {
position: absolute;
left: calc(-1 * var(--dsl-terminal-gutter) + 8px);
- top: 50%;
+ top: calc(var(--dsl-terminal-line-height) / 2);
transform: translateY(-50%);
}
diff --git a/packages/client/ui-primitives/src/TerminalBlock.tsx b/packages/client/ui-primitives/src/TerminalBlock.tsx
index a6737605a8..19c5c3296e 100644
--- a/packages/client/ui-primitives/src/TerminalBlock.tsx
+++ b/packages/client/ui-primitives/src/TerminalBlock.tsx
@@ -64,6 +64,12 @@ export interface TerminalBlockProps {
* line alone; with output it renders the live text under the running state.
*/
running?: boolean | undefined
+ /**
+ * The call launches background work (default false). `running` describes the
+ * launch call only; after it returns, retain the command without job status
+ * or output. An explicit `copyText` override remains available.
+ */
+ background?: boolean | undefined
/** Height cap in output lines before the middle collapses (default {@link DEFAULT_TERMINAL_MAX_LINES}); Infinity disables the cap. */
maxLines?: number | undefined
/**
@@ -172,6 +178,7 @@ export function TerminalBlock({
exitCode,
signal,
running = false,
+ background = false,
maxLines = DEFAULT_TERMINAL_MAX_LINES,
copyText,
runStateDot = true,
@@ -201,8 +208,9 @@ export function TerminalBlock({
const onToggle = useCallback(() => { setExpanded(value => !value) }, [])
- const status = statusText(exitCode, signal, copy)
+ const status = background ? undefined : statusText(exitCode, signal, copy)
const state = runState(running, exitCode, signal, copy)
+ const showState = runStateDot && (!background || running)
// A multi-line command gets one prompt row per line, so a two-command shell
// snippet reads as the two commands it is instead of collapsing into one
// ellipsized row. A trailing newline is a terminator, not an empty command.
@@ -219,7 +227,7 @@ export function TerminalBlock({
const { hidden, capped, headLines, tailLines } = headTailCap(lines.length, maxLines, expanded)
// A running command with nothing printed yet is banner-only; once it has
// printed, the output streams in under the banner as it would in a terminal.
- const body = !running || !empty
+ const body = !background && (!running || !empty)
return (
{/* One dot for the card, on the first row: the exit status the
view carries is the whole call's, and bash reports no
per-command status, so a dot per row would assert a
per-line outcome nothing here knows. */}
- {index === 0 && runStateDot && }
+ {index === 0 && showState && }
{/* The cwd labels the CALL, so only its first row carries it. The
view knows one working directory — where the call started —
and a later line may well run somewhere else (a `cd` in the
@@ -252,7 +260,7 @@ export function TerminalBlock({
))}
{status !== undefined && {status}}
- {(copyText !== undefined || (!running && !empty)) && (
+ {(copyText !== undefined || (!background && !running && !empty)) && (
diff --git a/packages/client/ui-primitives/tests/terminal-block-styles.client.spec.ts b/packages/client/ui-primitives/tests/terminal-block-styles.client.spec.ts
new file mode 100644
index 0000000000..0a809a7fa4
--- /dev/null
+++ b/packages/client/ui-primitives/tests/terminal-block-styles.client.spec.ts
@@ -0,0 +1,17 @@
+/** Terminal status positioning relative to the command's first visual line. */
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { expect, it } from 'vitest'
+
+const css = readFileSync(fileURLToPath(new URL('../src/TerminalBlock.module.css', import.meta.url)), 'utf8')
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
+
+it('centers the status dot on one command line rather than the wrapped prompt height', () => {
+ const state = /\.runState\s*\{([^{}]*)\}/.exec(css)?.[1]
+ const prompt = /\.promptLine\s*\{([^{}]*)\}/.exec(css)?.[1]
+ expect(state).toBeDefined()
+ expect(prompt).toBeDefined()
+ expect(state).toContain('top: calc(var(--dsl-terminal-line-height) / 2)')
+ expect(state).toContain('transform: translateY(-50%)')
+ expect(prompt).toContain('line-height: var(--dsl-terminal-line-height)')
+})
diff --git a/packages/client/ui-primitives/tests/terminal-block.client.spec.tsx b/packages/client/ui-primitives/tests/terminal-block.client.spec.tsx
index 432a20fde2..b801dd6e91 100644
--- a/packages/client/ui-primitives/tests/terminal-block.client.spec.tsx
+++ b/packages/client/ui-primitives/tests/terminal-block.client.spec.tsx
@@ -89,6 +89,42 @@ describe('TerminalBlock prompt label', () => {
})
describe('TerminalBlock states', () => {
+ it.each([
+ { output: undefined, running: false, exitCode: undefined },
+ { output: 'launch acknowledgement', running: false, exitCode: 7, signal: 'SIGTERM' },
+ { output: 'launch acknowledgement', running: false, exitCode: 0 },
+ { output: '', running: false, exitCode: 0 },
+ ])('background suppresses execution presentation for %j', (execution) => {
+ const view = render()
+ expect(promptRows(view.container)).toEqual(['appworker --watch', '$printf ready'])
+ expect(runStateOf(view.container)).toEqual({ state: null, label: undefined })
+ expect(outputLines(view.container)).toEqual([])
+ expect(view.queryByText('launch acknowledgement')).toBeNull()
+ expect(view.queryByText('无输出')).toBeNull()
+ expect(view.queryByText('信号 SIGTERM')).toBeNull()
+ expect(view.queryByRole('button')).toBeNull()
+ expect(view.container.firstElementChild?.hasAttribute('data-running')).toBe(false)
+ expect(view.container.firstElementChild?.hasAttribute('data-body')).toBe(false)
+ })
+
+ it('keeps command and prompt DOM when a background launch returns', () => {
+ const view = render()
+ const command = view.container.querySelector('[data-command-text]')
+ expect(runStateOf(view.container).state).toBe('ongoing')
+ view.rerender()
+ expect(view.container.querySelector('[data-command-text]')).toBe(command)
+ expect(runStateOf(view.container)).toEqual({ state: null, label: undefined })
+ expect(view.queryByText('无输出')).toBeNull()
+ expect(view.queryByRole('button')).toBeNull()
+ })
+
+ it('keeps an explicit copyText override available in background mode', () => {
+ const view = render()
+ expect(view.getByRole('button', { name: '复制' })).toBeTruthy()
+ expect(view.queryByText('无输出')).toBeNull()
+ expect(runStateOf(view.container)).toEqual({ state: null, label: undefined })
+ })
+
it('running with no output shows the command line only: no output box, no placeholder, no copy', () => {
const view = render()
expect(view.getByText('sleep 5')).toBeTruthy()
diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml
index 6a35e6a8d7..67ad419821 100644
--- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml
+++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml
@@ -15,11 +15,11 @@
en: 0dd8c3a22e28aacf
zh: c27c11bf4b5cceba
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/what-it-registers:
- en: a8eaf01a3a3390ee
- zh: ddea36dbcc4a0159
+ en: 54a50f4a3a6e4d45
+ zh: af905ed570812432
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/addresses:
- en: 949156aa02ec8b07
- zh: d3675754e2a37b5d
+ en: 57942270e23d3a8e
+ zh: 6617124e652b3f67
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/how-it-reads:
en: b056bbe0f74fe328
zh: fd9ac88f08825afc
diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md
index 588a95a7e0..51e23e2208 100644
--- a/packages/client/ui-sidebar-documentpreview/README.md
+++ b/packages/client/ui-sidebar-documentpreview/README.md
@@ -28,7 +28,7 @@ Preview files in the right Sidebar and choose among registered renderers. Markdo
## What it registers
-- **The type** — `ctx.sidebarRightTabs.register(...)` with id `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` (this implementation's identity in the tab system, and the key its body registers under), kind `text`, pattern `dsh-resource://file/**`, band `fallback`. `canOpen` accepts only Session addresses, whose paths may be relative or absolute; bare `absolute` addresses are not claimed. A type registered at the `extension` or `builtin` band for a narrower pattern (say `*.png`) takes those addresses; other supported files land here. The whole address is the content identity, so two files with one name in different directories, or one path under two sessions, are two tabs; the decoded basename is the tab title, and the keyed `sidebar.right.pane.tab.title` seat places its extension-specific `FileTypeIcon` before that title.
+- **The type** — `ctx.sidebarRightTabs.register(...)` with id `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` (this implementation's identity in the tab system, and the key its body registers under), kind `text`, a whole-address file-resource pattern, band `fallback`. `canOpen` accepts only Session addresses, whose paths may be relative or absolute; bare `absolute` addresses are not claimed. A type registered at the `extension` or `builtin` band for a narrower pattern (say `*.png`) takes those addresses; other supported files land here. The whole address is the content identity, so two files with one name in different directories, or one path under two sessions, are two tabs; the decoded basename is the tab title, and the keyed `sidebar.right.pane.tab.title` seat places its extension-specific `FileTypeIcon` before that title.
- **The body** — the keyed `sidebar.right.pane.tab` seat under the type's id. Its fixed header shows the Host's absolute path when available, otherwise the requested path; the shared [`PathLabel`](../ui-primitives/README.md#component-catalog) subdues directories, preserves a clipped path’s trailing characters with a left-edge fade, and exposes the full value on hover. A dropdown appears when multiple supported renderers are available. Plain text is offered only for text-compatible sources; a single renderer shows no viewer control. A known binary container suffix without a registered renderer shows the file-type icon and an unsupported-preview message under the path header, without issuing a read. Two Session-scoped list child slots hand the previewed file to other plugins once its metadata reports a Host path, both with `absolutePath` as the owner prop: `sidebar.right.tab.document.actions` renders after the header's own controls in every state that shows the header, and `sidebar.right.tab.document.unpreviewable` renders in the unsupported empty state and in the empty state of a readable file the preview cannot render (`not-text`, `too-large`), where Retry would otherwise stand; a carrier or unclassified read failure keeps Retry, and a missing or non-regular path offers only its explanation. The shipped occupant of both is [`ui-open-in-app`](../ui-open-in-app/README.md). A wrap toggle appears only when the selected renderer declares `wrap: true`; its glyph describes the mode the click selects, and the per-tab preference starts on. Reload stays in this header, not the Sidebar's tab strip. Automatic refresh defaults to enabled; its separate toggle is hidden, with state, labels, styles, and toggle logic retained. The body reaches every pane edge; each renderer owns its content inset and may own an inner scrollport. This intentionally differs from the Files tab's 2px right-side scrollbar offset: previews keep the full pane width so edge-to-edge HTML and code scrollports end at the pane edge.
- **Shared loading and view state**, session-scoped and bucketed by tab id. The store holds accumulated pages or complete bytes, read and observed versions, pending Resource changes, automatic-refresh state, loading/failure state, renderer choice, scroll offset, wrap, and the answered navigation revision. The ordinary inject face calls Remote readers and writes through declared store actions. Reloads and loading-mode changes retire older requests; the tab's abort signal forgets its state.
@@ -39,7 +39,7 @@ Document implementations register metadata with `ctx.documentPreviews.register({
## Addresses
-A tab uses the Session address built by `fileAddressFor`, carrying a relative or absolute path. `hostFileOf(address)` takes the Session only from that address, with no external Session argument; neither current nor Tab Session is borrowed. The Host resolves file and related paths through the Session filesystem, whose backend controls read authority. Metadata for the same complete address is shared by every UI, including Global components. The [Workspace Files README](../../api/workspace-files/README.md) owns these rules; renderer selection does not change the navigation address.
+A tab uses the Session address built by `fileAddressFor`, carrying a relative or absolute path. Routing preserves `.` and `..` path segments in the address and its content identity; the Host resolves them. `hostFileOf(address)` takes the Session only from that address, with no external Session argument; neither current nor Tab Session is borrowed. The Host resolves file and related paths through the Session filesystem, whose backend controls read authority. Metadata for the same complete address is shared by every UI, including Global components. The [Workspace Files README](../../api/workspace-files/README.md) owns these rules; renderer selection does not change the navigation address.
After all text pages load, Markdown images use the authenticated `/api/file` route for absolute file paths and paths relative to the source document's directory. Relative images wait for the Host's absolute document path. URL escapes are decoded once; query and fragment suffixes are excluded from the filename. HTTP(S) images retain their authored URLs, and failed image loads show alt text. Local images require an HTTP(S) application base URL; image files are not added to automatic-refresh dependencies.
diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md
index 89d90091f5..1c5be3fdc3 100644
--- a/packages/client/ui-sidebar-documentpreview/README.zh.md
+++ b/packages/client/ui-sidebar-documentpreview/README.zh.md
@@ -28,7 +28,7 @@ kind: "package-reference"
## 注册了什么
-- **类型** —— `ctx.sidebarRightTabs.register(...)`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`(这个实现在 tab 系统中的身份,也是其正文注册所用的键),kind `text`,pattern `dsh-resource://file/**`,档位 `fallback`。`canOpen` 只接受 Session 地址,其中路径可为相对或绝对路径;不认领裸 `absolute` 地址。在 `extension` 或 `builtin` 档以更窄 pattern(比如 `*.png`)注册的类型接走那些地址;其他受支持文件落到这里。整个地址就是内容身份,所以不同目录下同名的两个文件、或同一路径在两个会话之下,是两个 tab;解码后的 basename 是 tab 标题,keyed slot `sidebar.right.pane.tab.title` 会在标题前放置按扩展名选择的 `FileTypeIcon`。
+- **类型** —— `ctx.sidebarRightTabs.register(...)`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`(这个实现在 tab 系统中的身份,也是其正文注册所用的键),kind `text`,匹配完整文件资源地址的 pattern,档位 `fallback`。`canOpen` 只接受 Session 地址,其中路径可为相对或绝对路径;不认领裸 `absolute` 地址。在 `extension` 或 `builtin` 档以更窄 pattern(比如 `*.png`)注册的类型接走那些地址;其他受支持文件落到这里。整个地址就是内容身份,所以不同目录下同名的两个文件、或同一路径在两个会话之下,是两个 tab;解码后的 basename 是 tab 标题,keyed slot `sidebar.right.pane.tab.title` 会在标题前放置按扩展名选择的 `FileTypeIcon`。
- **正文** —— keyed slot `sidebar.right.pane.tab`,键为类型的 id。固定头部在可用时显示 Host 的绝对路径,否则显示请求路径;共享的 [`PathLabel`](../ui-primitives/README.zh.md#component-catalog) 弱化目录颜色,以左侧渐隐保留被裁剪路径的尾部字符,并在悬停时展示完整值。有多个受支持的渲染器时才显示下拉菜单。仅文本兼容的源文件提供纯文本选项;只有一个渲染器时不显示查看器控件。已知的二进制容器后缀没有注册渲染器时,在路径头部下方显示文件类型图标和不支持预览的说明,并且不会发起读取。两个 Session 作用域的 list 子 slot 在文件元数据报出 Host 路径后把当前文件交给其他插件,owner prop 为 `absolutePath`:`sidebar.right.tab.document.actions` 在每个显示头部的状态里渲染在头部自有控件之后;`sidebar.right.tab.document.unpreviewable` 渲染在不支持预览的空态里,以及可读但本预览无法渲染的文件(`not-text`、`too-large`)的空态里,占据原本放重试的位置;载体或未分类的读取失败仍保留重试,不存在或非普通文件的路径只给说明。两者已发布的占位者都是 [`ui-open-in-app`](../ui-open-in-app/README.zh.md)。仅当所选渲染器声明 `wrap: true` 时显示换行开关;图标表示点击后切换到的模式,该偏好按 tab 保存,初始开启。重新载入仍在此头部,不放入 Sidebar 的 tab 条。自动刷新默认开启;独立开关暂时隐藏,状态、文案、样式与切换逻辑保留。正文贴合格的每条边,各渲染器自行提供内容留白,并可拥有内部滚动区。这与 Files tab 右侧预留 2px 滚动条间距的布局有意不同:Preview 使用格的完整宽度,使贴边 HTML 与代码滚动区终止于格的边缘。
- **共享加载与视图状态**,会话作用域、按 tab id 分桶。store 持有累计页或完整字节、读取与观察版本、待处理 Resource 变化、自动刷新状态、加载/失败状态、渲染器选择、滚动位置、换行和已响应的导航 revision。普通 inject face 调用 Remote 读取,并经声明的 store action 写入。重新载入和加载模式变化会淘汰旧请求;tab 的中止信号清理其状态。
@@ -39,7 +39,7 @@ kind: "package-reference"
## 地址
-tab 使用 `fileAddressFor` 构造的 Session 地址,携带相对或绝对路径。`hostFileOf(address)` 仅从地址取得 Session,不接收外部 Session 参数,也不借用当前或 Tab Session。Host 通过 Session 文件系统解析文件及关联路径,由该后端控制读取权限。任何 UI(包括 Global 组件)都共享同一完整地址的元数据。[Workspace Files README](../../api/workspace-files/README.zh.md) 定义这些规则;渲染器选择不改变导航地址。
+tab 使用 `fileAddressFor` 构造的 Session 地址,携带相对或绝对路径。路由在地址及其内容身份中保留 `.` 与 `..` 路径段,由 Host 解析。`hostFileOf(address)` 仅从地址取得 Session,不接收外部 Session 参数,也不借用当前或 Tab Session。Host 通过 Session 文件系统解析文件及关联路径,由该后端控制读取权限。任何 UI(包括 Global 组件)都共享同一完整地址的元数据。[Workspace Files README](../../api/workspace-files/README.zh.md) 定义这些规则;渲染器选择不改变导航地址。
全部文本分页加载完成后,Markdown 图片通过已鉴权的 `/api/file` 路由读取绝对文件路径,以及相对于源文档所在目录的路径。相对图片等待 Host 返回文档绝对路径。URL 转义只解码一次;查询参数与片段后缀不计入文件名。HTTP(S) 图片保留原始 URL,加载失败的图片显示 alt 文本。本地图片要求应用基址为 HTTP(S) URL;图片文件不加入自动刷新依赖。
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/definition.ts b/packages/client/ui-sidebar-documentpreview/src/client/definition.ts
index 7f2892cffd..914616e0fb 100644
--- a/packages/client/ui-sidebar-documentpreview/src/client/definition.ts
+++ b/packages/client/ui-sidebar-documentpreview/src/client/definition.ts
@@ -46,7 +46,8 @@ export function textDefinition(): SidebarRightTabDefinition {
return {
id: TEXTPREVIEW_ID,
kind: TEXTPREVIEW_KIND,
- patterns: ['dsh-resource://file/**'],
+ // A character-class repetition includes literal dot segments that globstar excludes.
+ patterns: ['dsh-resource://file/[\\s\\S]+'],
priority: 'fallback',
canOpen: address => parseFileAddress(address)?.scope === 'session',
title: basenameOf,
diff --git a/packages/client/ui-sidebar-documentpreview/tests/definition.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/definition.client.spec.ts
index 63984f41ca..a842b76b24 100644
--- a/packages/client/ui-sidebar-documentpreview/tests/definition.client.spec.ts
+++ b/packages/client/ui-sidebar-documentpreview/tests/definition.client.spec.ts
@@ -9,7 +9,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
-import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path'
+import { parseFileAddress, sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path'
import { SidebarRightTabRegistry } from '@deepseek-ai/dsh-client-ui-sidebar-right/src/client/tab-registry.ts'
import { TEXTPREVIEW_ID, TEXTPREVIEW_KIND, basenameOf, textDefinition } from '../src/client/definition.ts'
@@ -32,7 +32,7 @@ describe('textDefinition', () => {
const definition = textDefinition()
expect(definition.id).toBe(TEXTPREVIEW_ID)
expect(definition.kind).toBe(TEXTPREVIEW_KIND)
- expect(definition.patterns).toEqual(['dsh-resource://file/**'])
+ expect(definition.patterns).toEqual(['dsh-resource://file/[\\s\\S]+'])
expect(definition.priority).toBe('fallback')
expect(definition.title('dsh-resource://file/session/s-1/work/README.md')).toBe('README.md')
// Reads require an addressed Session, even when the file path is absolute.
@@ -66,6 +66,51 @@ describe('text type in the registry', () => {
}
})
+ it.each([
+ '../AGENTS.md',
+ '../../AGENTS.md',
+ 'dir/../AGENTS.md',
+ 'dir/./AGENTS.md',
+ '/workspace/../AGENTS.md',
+ 'C:/workspace/../AGENTS.md',
+ '//host/share/workspace/../AGENTS.md',
+ '',
+ ])('claims unresolved Session paths without rewriting their identity: %s', (path) => {
+ const tabs = registry()
+ const address = sessionFileAddress('s-1', path)
+ expect(tabs.claim(address)).toEqual({ kind: TEXTPREVIEW_KIND, contentId: address, title: basenameOf(address) })
+ expect(tabs.claim(address).contentId).toBe(address)
+ expect(parseFileAddress(address)).toEqual({ scope: 'session', sessionId: 's-1', path })
+ })
+
+ it.each([
+ ['./AGENTS.md', './AGENTS.md'],
+ ['%2e%2E/AGENTS.md', '../AGENTS.md'],
+ ['dir/%2E/AGENTS.md', 'dir/./AGENTS.md'],
+ ['%252e%252e/AGENTS.md', '%2e%2e/AGENTS.md'],
+ ['../a%20b%23c%3F.txt?ignored=1#section', '../a b#c?.txt'],
+ ])('claims an authored address while decoding its path once: %s', (encoded, path) => {
+ const tabs = registry()
+ const address = `dsh-resource://file/session/s%2F1/${encoded}`
+ expect(tabs.claim(address).contentId).toBe(address)
+ expect(parseFileAddress(address)).toEqual({ scope: 'session', sessionId: 's/1', path })
+ })
+
+ it.each([
+ 'dsh-resource://file/session',
+ 'dsh-resource://file/session//../a.md',
+ 'dsh-resource://file/session/s-1',
+ 'dsh-resource://file/session/%/../a.md',
+ 'dsh-resource://file/session/s-1/../%ZZ.md',
+ 'DSH-RESOURCE://file/session/s-1/../a.md',
+ 'dsh-resource://File/session/s-1/../a.md',
+ ])('keeps the Session parser veto after matching a file prefix: %s', (address) => {
+ const tabs = registry()
+ expect(tabs.candidates(address)).toEqual([])
+ expect(() => tabs.claim(address)).toThrow('no registered tab type claims')
+ expect(() => tabs.claim(address, TEXTPREVIEW_KIND)).toThrow(`tab type "${TEXTPREVIEW_KIND}" refuses`)
+ })
+
it.each([
'dsh-resource://file/shared/team/notes.md',
'dsh-resource://file/absolute/home/me/notes.md',
@@ -86,6 +131,38 @@ describe('text type in the registry', () => {
expect(tabs.candidates('dsh-resource://file/session/s-1/w/logo.png').map(type => type.kind)).toEqual(['image', TEXTPREVIEW_KIND])
})
+ it.each(['builtin', 'extension'] as const)('yields parent paths to the %s band and honors its veto', (priority) => {
+ const tabs = registry()
+ const address = sessionFileAddress('s-1', '../AGENTS.md')
+ const release = tabs.register({
+ id: 'test/markdown', kind: 'markdown', patterns: ['*.md'], priority, title: () => 'Markdown',
+ })
+ expect(tabs.candidates(address).map(type => type.kind)).toEqual(['markdown', TEXTPREVIEW_KIND])
+ expect(tabs.claim(address)).toEqual({ kind: 'markdown', contentId: address, title: 'Markdown' })
+ release()
+ tabs.register({
+ id: 'test/markdown', kind: 'markdown', patterns: ['*.md'], priority, canOpen: () => false, title: () => 'Markdown',
+ })
+ expect(tabs.claim(address).kind).toBe(TEXTPREVIEW_KIND)
+ })
+
+ it('ranks the file declaration by its literal length within the fallback band', () => {
+ const tabs = registry()
+ tabs.register({
+ id: 'test/shorter', kind: 'shorter', patterns: ['dsh-resource://file/**/*.*'], priority: 'fallback', title: () => 'Shorter',
+ })
+ const releaseLonger = tabs.register({
+ id: 'test/longer', kind: 'longer', patterns: ['dsh-resource://file/session/**/*.md'], priority: 'fallback', title: () => 'Longer',
+ })
+ tabs.register({ ...textDefinition(), id: 'test/tied', kind: 'tied' })
+ const address = sessionFileAddress('s-1', 'AGENTS.md')
+ // Specificity uses the literal pattern length, not the size of its accepted address set.
+ expect(tabs.candidates(address).map(type => type.kind)).toEqual(['longer', TEXTPREVIEW_KIND, 'tied', 'shorter'])
+ expect(tabs.claim(address).kind).toBe('longer')
+ releaseLonger()
+ expect(tabs.claim(address).kind).toBe(TEXTPREVIEW_KIND)
+ })
+
it('does not claim addresses of other schemes', () => {
const tabs = registry()
expect(() => tabs.claim('sidebar://guide')).toThrow('no registered tab type claims')
diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml
index 04158a1ca7..7facde8709 100644
--- a/packages/client/ui-tool/README.i18n.yaml
+++ b/packages/client/ui-tool/README.i18n.yaml
@@ -21,8 +21,8 @@
en: 2e3c306b13d3bc25
zh: 77127f01763b3976
/deepseek-ai-dsh-client-ui-tool/use-this-package/built-in-views:
- en: 4d885fc693b497f4
- zh: 2f68d9ea92f2ebd6
+ en: 3643ffbb8cf80e4c
+ zh: b49654dc3b94b2be
/deepseek-ai-dsh-client-ui-tool/understand-the-implementation:
en: 00bc7b0c84edc603
zh: 1cfcf5b435248a91
diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md
index c8f88095b1..cf1d9d1d78 100644
--- a/packages/client/ui-tool/README.md
+++ b/packages/client/ui-tool/README.md
@@ -51,7 +51,7 @@ Every registered view receives the explicit `preparing`, `start`, and `result` p
File-tool rows prefer a complete `file_path`; other generic rows, including search and unregistered tools, prefer a non-empty `description` before their variant-specific argument fallback. Read `args` during rendering or view construction: a preparing reader grows in place, and only previously observed answer changes trigger subsequent block publication.
-This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running atomic rows share one left-to-right highlight across their titles, separators, summaries, and suffixes, while settled rows stay static. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable. Background Bash calls expand to their original arguments and job acknowledgement. Their input scrolls horizontally without soft wrapping and remains selectable; only horizontal overflow adds a named keyboard focus target. Inspect is available when provided. The acknowledgement does not report the job's exit status. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. `ui-skill` demonstrates a business-owned registration for `skill`.
+This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running atomic rows share one left-to-right highlight across their titles, separators, summaries, and suffixes, while settled rows stay static. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable. Valid explicit background `bash`/`pwsh` calls use the terminal Start presentation; a successful launch acknowledgement retains the command and cwd in a static command-only card without output or execution status. Launch failures retain generic arguments and errors, while background `terminal_send` stays generic. Inspect preserves the original arguments and acknowledgement; later job output is not attached to this card ([decision](../../../.agents/notes/implemented/bug-fix/2026-10-10-background-shell-command-display.md)). Expanded terminal commands and background inputs wrap to the available width while preserving authored line breaks and selectable text; terminal output keeps its column-preserving layout. Only horizontal overflow adds a named keyboard focus target. Inspect is available when provided. The acknowledgement does not report the job's exit status. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. `ui-skill` demonstrates a business-owned registration for `skill`.
Question rows pair answers with questions by id, using the tool result or the `userQuestions` projection for late replies. An answerable call has a button to reopen its panel. Once a late reply is in the agent Inbox, the row shows its submitted answers; “View answers” expands its read-only transcript while admission is pending. A settled call with answers opens the panel read-only. Closed calls without answers and invalid records offer no button. Cancelled and interrupted rows show their verdict without inventing answers. If no panel provider accepts the button action, it expands the row. Unsupported or ambiguous input falls back to flattened Tool text.
diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md
index 6b0d28087b..607fae545a 100644
--- a/packages/client/ui-tool/README.zh.md
+++ b/packages/client/ui-tool/README.zh.md
@@ -51,7 +51,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字
文件工具行优先使用完整的 `file_path`;其他通用行(包括搜索和未注册的工具)优先使用非空 `description`,再回退到各自类别的参数摘要。应在渲染或构造视图时读取 `args`:准备态读器原地增长,后续仅在已观察答案变化时重新发布块。
-本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中的原子工具行以同一道高光从左到右扫过标题、分隔符、摘要与后缀;已完成行保持静态。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开。后台 Bash 调用可展开查看原始参数与后台任务回执。输入内容可横向滚动,不自动折行,并可选中;仅横向溢出时添加具名的键盘聚焦目标。提供 Inspect 时也可使用。回执不表示后台任务的退出状态。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
+本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中的原子工具行以同一道高光从左到右扫过标题、分隔符、摘要与后缀;已完成行保持静态。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开。合法的主动后台 `bash`/`pwsh` 调用使用 terminal Start 展示;成功启动回执后,静态仅命令卡片保留 command 和 cwd,不显示输出或执行状态。启动失败保留通用参数与错误展示,后台 `terminal_send` 仍为 generic。Inspect 保留原始参数及回执,后续 job 输出不接回此卡片([决定](../../../.agents/notes/implemented/bug-fix/2026-10-10-background-shell-command-display.zh.md))。展开的终端命令与后台输入按可用宽度自动折行,保留原有换行和可选文本;终端输出保持按列对齐的排版。仅横向溢出时添加具名的键盘聚焦目标。提供 Inspect 时也可使用。回执不表示后台任务的退出状态。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
问题行按 id 配对问题与答案;答案来自工具结果,迟到回答则来自 `userQuestions` 投影。可回答的调用提供按钮以重新打开面板。迟到回复进入 agent Inbox 后,问题行显示已提交的答案;等待准入期间,「查看回答」展开只读的行内记录。带有答案的已结算调用则以只读方式打开面板。没有答案的已关闭调用和无效记录不提供按钮。已取消和已中断的行显示裁决,不虚构答案。若面板提供方不接受按钮操作,点击会展开该行。不受支持或含糊的输入回退为压平的工具文本。
diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css
index 5be4d641be..acf675a13e 100644
--- a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css
+++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css
@@ -304,6 +304,7 @@
--dsl-terminal-font: var(--dsw-font-markdown-code-block-small);
--dsl-terminal-line-height: 18px;
--dsl-terminal-output-max-height: 224px;
+ --dsl-terminal-command-whitespace: pre-wrap;
border: 0.5px solid var(--dsw-alias-border-l1);
}
diff --git a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts
index d6fe0fa3e0..6331f89592 100644
--- a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts
+++ b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts
@@ -44,7 +44,7 @@ export interface TerminalCardModel {
* The locale-neutral props {@link TerminalBlock} draws. The render site adds
* `command` after resolving {@link copy} through its locale seat.
*/
- card: Pick
+ card: Pick
/**
* Verbatim Tool data or semantic `terminal_send` data. Product copy stays
* unresolved until a render site supplies its locale seat.
@@ -55,7 +55,7 @@ export interface TerminalCardModel {
}
interface LocalizedTerminalCardModel {
- readonly card: Pick
+ readonly card: Pick
readonly description: string | undefined
}
@@ -288,8 +288,9 @@ function parseExitStatus(text: string): { output: string; exitCode?: number; sig
/**
* Derive terminal props for supported shell and terminal-send calls, including
* nested PTC dispatch calls. Standard shell results parse their final status
- * marker; persistent shell results, spill previews, background calls, errors,
- * and malformed input use the generic path. {@link isSettledPersistentShellCall} lets that generic
+ * marker. Successful background shell launches retain only their command after
+ * settlement; persistent shell results, spill previews, background terminal sends,
+ * errors, and malformed input use the generic path. {@link isSettledPersistentShellCall} lets that generic
* persistent result remain expandable without inventing one process status.
* @param block - running or settled Tool block.
* @param sessionCwd - original workspace root used only for old results without recorded cwd.
@@ -302,7 +303,7 @@ export function terminalCardModel(
const parsed = parsedToolCall(block)
if (parsed === null) return null
const call = shellCall(parsed.name, parsed.args) ?? terminalSendCall(parsed.name, parsed.args)
- if (call === null || call.background) return null
+ if (call === null || (call.kind === 'terminal-send' && call.background)) return null
const copy: TerminalCardModel['copy'] = call.kind === 'shell'
? { kind: 'shell', command: call.command, description: call.description }
@@ -322,10 +323,14 @@ export function terminalCardModel(
exitCode: undefined,
signal: undefined,
running: true,
+ ...(call.kind === 'shell' && call.background ? { background: call.background } : {}),
},
}
}
if (block.isError || (call.kind === 'shell' && call.persistent) || isSpilledShellCall(block)) return null
+ if (call.kind === 'shell' && call.background) {
+ return { copy, card: { cwd, running: false, background: call.background } }
+ }
const output = singleResultText(block)
if (output === undefined) return null
const status = call.kind === 'terminal-send' ? { output } : parseExitStatus(output)
diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css
index e6ba3f505e..52792b8722 100644
--- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css
+++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css
@@ -16,6 +16,7 @@
--dsl-terminal-font: var(--dsw-font-markdown-code-block-small);
--dsl-terminal-line-height: 18px;
--dsl-terminal-output-max-height: 224px;
+ --dsl-terminal-command-whitespace: pre-wrap;
margin: 4px 0 4px 4px;
border: 0.5px solid var(--dsw-alias-border-l1);
}
@@ -78,6 +79,7 @@
}
.commandInput {
+ --dsl-command-whitespace: pre-wrap;
color: var(--dsw-alias-label-secondary);
}
diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx
index f23e369d0b..52ffd1555a 100644
--- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx
+++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx
@@ -57,9 +57,8 @@ export const BashRow = memo(function BashRow({ toolName, block, sessionId, useSe
const status = stateStatus(state, t)
const { expanded, toggle: toggleExpand } = useDisclosure()
const background = isBackgroundShellCall(block)
- // Background launches expose their acknowledgement without assigning the
- // job an exit status. Failures, persistent results, and spill previews also
- // keep the generic input/output presentation.
+ // Failed or interrupted background launches retain raw input/output, as do
+ // other execution failures, persistent results, and spill previews.
const genericBody = terminal === null
&& (model.state === 'error' || isSettledPersistentShellCall(block) || isSpilledShellCall(block) || background)
&& (model.bodyRaw !== null || model.output !== null)
diff --git a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx
index a891bad758..24117339cd 100644
--- a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx
+++ b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx
@@ -2,7 +2,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useDisclosure } from '@deepseek-ai/dsh-client-ui-chat/src/client/chat/use-disclosure.ts'
-import { cleanup, fireEvent, render } from '@testing-library/react'
+import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { StartedToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-ui-chat/client'
@@ -264,8 +264,35 @@ describe('terminalCardModel', () => {
}))).not.toBeNull()
})
- it('returns null for background, errors, malformed args, unsupported tools, and non-text results', () => {
- expect(terminalCardModel(running({ argsRaw: shellArgs({ run_in_background: true }) }))).toBeNull()
+ it.each(['bash', 'pwsh'])('derives %s background launch commands without treating acknowledgement as output', (name) => {
+ const argsRaw = shellArgs({ run_in_background: true, workdir: '/requested' })
+ for (const parentCallId of [undefined, 'parent']) {
+ const parent = parentCallId === undefined ? {} : { parentCallId }
+ expect(terminalCardModel(running({ name, argsRaw, ...parent }))).toEqual({
+ copy: { kind: 'shell', command: 'ls -la', description: 'List files' },
+ card: { cwd: '/requested', running: true, background: true, output: undefined, exitCode: undefined, signal: undefined },
+ })
+ for (const content of [[], [{ type: 'text' as const, text: 'started background job bash-1\n[exit code: 7]' }]]) {
+ const block = settled({ call: { name, argsRaw }, ...parent, meta: { cwd: '/recorded' }, content })
+ const model = terminalCardModel(block)!
+ expect(model).toEqual({
+ copy: { kind: 'shell', command: 'ls -la', description: 'List files' },
+ card: { cwd: '/recorded', running: false, background: true },
+ })
+ expect(terminalFailed(model)).toBe(false)
+ expect(localizeTerminalCardModel(model, t).card).toEqual({
+ command: 'ls -la', cwd: '/recorded', running: false, background: true,
+ })
+ expect(terminalCardModel({ ...block, isError: true })).toBeNull()
+ expect(block.content).toBe(content)
+ }
+ }
+ })
+
+ it('returns null for background terminal sends, errors, malformed args, unsupported tools, and non-text results', () => {
+ expect(terminalCardModel(running({ name: 'terminal_send', argsRaw: JSON.stringify({
+ sessionId: 'pty-3', text: 'make', run_in_background: true,
+ }) }))).toBeNull()
expect(terminalCardModel(settled({ isError: true }))).toBeNull()
expect(terminalCardModel(running({ argsRaw: '{' }))).toBeNull()
expect(terminalCardModel(running({ name: 'read' }))).toBeNull()
@@ -405,12 +432,13 @@ describe('chat row terminal body', () => {
expect(view.getByText(command)).toBeTruthy()
})
- it('a non-terminal call keeps the args-JSON text body', () => {
+ it('a background terminal_send keeps the args-JSON text body', () => {
const view = render()
+ call: { name: 'terminal_send', argsRaw: JSON.stringify({ sessionId: 'pty-3', text: 'make', run_in_background: true }) },
+ }))} toolName="terminal_send" />)
toggleRow(view)
- expect(view.getByText(/"command"/)).toBeTruthy()
+ expect(view.getByText(/"sessionId"/)).toBeTruthy()
+ expect(view.container.querySelector('[data-terminal]')).toBeNull()
})
it('malformed empty args use the generic output body', () => {
@@ -527,45 +555,112 @@ describe('BashRow terminal card', () => {
expect(view.container.querySelectorAll('[class*="_ioText_"]')[1]?.textContent).toBe(output)
})
- it.each([undefined, 'dispatch-parent'])('expands a background launch and its acknowledgement without reporting a job exit (parent: %s)', (parentCallId) => {
- const command = 'pnpm --filter @deepseek-ai/dsh-client-ui-tool run build --watch --verbose'
- const args = { command, description: 'Wait', run_in_background: true }
- const argsRaw = JSON.stringify(args)
+ it.each([
+ ['bash', undefined], ['bash', 'dispatch-parent'], ['pwsh', undefined], ['pwsh', 'dispatch-parent'],
+ ] as const)('keeps the %s background command open after launch without reporting job status (parent: %s)', (name, parentCallId) => {
+ const command = ` node worker.mjs --trace=${'background-worker-'.repeat(24)}\n\tprintf "done"\n`
+ const argsRaw = shellArgs({ command, description: 'Wait', workdir: '/workspace/app', run_in_background: true })
const inspect = vi.fn()
const parent = parentCallId === undefined ? {} : { parentCallId }
- const view = render()
- const row = view.container.querySelector('[data-sample="bash"]')!
+ const renderRow = (block: StartedToolCall | ToolResultNode) => {
+ const props = { ...rowProps(block), toolName: name, inspect }
+ return name === 'bash' ? :
+ }
+ const view = render(renderRow(running({ name, argsRaw, ...parent })))
+ const row = view.getByRole('button')
expect(view.getByText('Wait')).toBeTruthy()
expect(row.getAttribute('aria-expanded')).toBe('false')
-
- fireEvent.keyDown(row, { key: 'Enter' })
-
- expect(row.getAttribute('aria-expanded')).toBe('true')
- expect(view.container.querySelector('[data-command-text]')?.textContent)
- .toBe(JSON.stringify(args, null, 2))
- expect(view.container.querySelector('[data-command-text]')?.hasAttribute('tabindex')).toBe(false)
- expect(view.queryByText('输出')).toBeNull()
- expect(view.container.querySelector('[data-terminal]')).toBeNull()
+ if (name === 'bash') fireEvent.keyDown(row, { key: 'Enter' })
+ else fireEvent.click(row)
+ const terminal = view.container.querySelector('[data-terminal]')
+ const firstCommand = view.container.querySelector('[data-command-text]')
+ expect(terminal).not.toBeNull()
+ expect(runStateOf(view.container)).toBe('ongoing')
+ expect([...view.container.querySelectorAll('[data-command-text]')].map(line => line.textContent))
+ .toEqual(command.slice(0, -1).split('\n'))
+ expect(view.getByText('app')).toBeTruthy()
+ expect(view.queryByText('无输出')).toBeNull()
+ expect(view.queryByRole('button', { name: '复制' })).toBeNull()
const acknowledgement = 'started background job job-1'
- view.rerender()
-
+ const result = settled({ call: { name, argsRaw }, ...parent, meta: { cwd: '/workspace/app' },
+ content: [{ type: 'text', text: acknowledgement }] })
+ view.rerender(renderRow(result))
expect(row.getAttribute('aria-expanded')).toBe('true')
- expect(view.container.querySelector('[data-command-text]')?.textContent)
- .toBe(JSON.stringify(args, null, 2))
- expect(view.getByText(acknowledgement)).toBeTruthy()
- expect(view.container.querySelector('[data-terminal], [data-state="done"]')).toBeNull()
+ expect(view.container.querySelector('[data-terminal]')).toBe(terminal)
+ expect(view.container.querySelector('[data-command-text]')).toBe(firstCommand)
+ expect(view.container.querySelector('[data-terminal] [data-state], [data-running]')).toBeNull()
+ expect(view.queryByText('运行中')).toBeNull()
+ expect(view.queryByText('已完成')).toBeNull()
+ expect(view.queryByText('无输出')).toBeNull()
+ expect(view.queryByText(acknowledgement)).toBeNull()
+ expect(view.queryByRole('button', { name: '复制' })).toBeNull()
+ expect(result.call?.argsRaw).toBe(argsRaw)
+ expect(result.content).toEqual([{ type: 'text', text: acknowledgement }])
fireEvent.click(view.getByRole('button', { name: t('row.inspect') }))
expect(inspect).toHaveBeenCalledOnce()
fireEvent.click(row)
expect(row.getAttribute('aria-expanded')).toBe('false')
+ if (name === 'bash') fireEvent.keyDown(row, { key: ' ' })
+ else fireEvent.click(row)
+ expect(row.getAttribute('aria-expanded')).toBe('true')
+ expect(view.container.querySelector('[data-command-text]')?.textContent).toBe(command.split('\n')[0])
expect(view.queryByText(acknowledgement)).toBeNull()
})
+ it.each([
+ ['bash', false], ['bash', true], ['pwsh', false], ['pwsh', true],
+ ] as const)('keeps %s launch failures on the original generic body (interrupted: %s)', (name, interrupted) => {
+ const argsRaw = shellArgs({ run_in_background: true })
+ const renderRow = (block: StartedToolCall | ToolResultNode) => {
+ const props = { ...rowProps(block), toolName: name }
+ return name === 'bash' ? :
+ }
+ const view = render(renderRow(running({ name, argsRaw })))
+ const row = view.getByRole('button')
+ fireEvent.click(row)
+ view.rerender(renderRow(settled({ call: { name, argsRaw }, isError: true,
+ ...interrupted ? { error: { name: 'Interrupted', code: 'interrupted' } } : {},
+ content: [{ type: 'text', text: 'Launch did not finish' }] })))
+ expect(row.getAttribute('aria-expanded')).toBe('true')
+ expect(view.container.querySelector('[data-terminal]')).toBeNull()
+ expect(view.getByText(/"run_in_background": true/)).toBeTruthy()
+ expect(view.getAllByText('Launch did not finish').length).toBeGreaterThan(0)
+ expect(view.container.querySelector(`[data-state="${interrupted ? 'stopped' : 'error'}"]`)).not.toBeNull()
+ })
+
+ it.each(['BashRow', 'GenericToolCard'] as const)('%s preserves authored command lines and copies only raw output', async (renderer) => {
+ const clipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard')
+ const writeText = vi.fn().mockResolvedValue(undefined)
+ const lines = [` node worker.mjs --trace=${'command-token-'.repeat(24)} `, '', '\tprintf "done"']
+ const command = `${lines.join('\n')}\n`
+ const output = ' first\tcolumn\n\nlast \n'
+ const props = rowProps(settled({
+ call: { name: 'bash', argsRaw: shellArgs({ command }) },
+ content: [{ type: 'text', text: `${output}\n[exit code: 0]` }],
+ }))
+ vi.useFakeTimers()
+ try {
+ Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
+ const view = render(renderer === 'BashRow' ? : )
+ fireEvent.click(view.container.querySelector('[data-expandable]')!)
+ expect([...view.container.querySelectorAll('[data-command-text]')].map(line => line.textContent)).toEqual(lines)
+ expect(view.getByText(' first\tcolumn', RAW)).toBeTruthy()
+ expect(view.getByText('last ', RAW)).toBeTruthy()
+ await act(async () => { fireEvent.click(view.getByRole('button', { name: '复制' })) })
+ expect(writeText).toHaveBeenCalledOnce()
+ expect(writeText).toHaveBeenCalledWith(output)
+ await act(async () => { await vi.runOnlyPendingTimersAsync() })
+ } finally {
+ cleanup()
+ vi.clearAllTimers()
+ vi.useRealTimers()
+ if (clipboard === undefined) Reflect.deleteProperty(navigator, 'clipboard')
+ else Object.defineProperty(navigator, 'clipboard', clipboard)
+ }
+ })
+
it('keeps malformed shell arguments collapsed even when background execution is requested', () => {
const view = render( part.trim()).filter(Boolean)
}
+describe('expanded command wrapping', () => {
+ it.each([
+ ['../src/client/tool/toolviews/bash-sample.module.css', '.terminal', '--dsl-terminal-command-whitespace'],
+ ['../src/client/tool/toolviews/bash-sample.module.css', '.commandInput', '--dsl-command-whitespace'],
+ ['../src/client/tool/components/ToolRow.module.css', '.terminalBody', '--dsl-terminal-command-whitespace'],
+ ] as const)('%s opts %s into command wrapping only', (path, selector, property) => {
+ const sheet = readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
+ expect(declarations(selector, sheet)).toContain(`${property}: pre-wrap`)
+ expect(sheet).not.toContain('--dsl-terminal-line-whitespace')
+ expect(declarations('.summary', sheet)).toEqual(expect.arrayContaining([
+ 'white-space: nowrap', 'overflow: hidden', 'text-overflow: ellipsis',
+ ]))
+ })
+})
+
describe('ToolRow.module.css summary line', () => {
it.each([
['ToolRow', '../src/client/tool/components/ToolRow.module.css'],
diff --git a/packages/experimental/webworker-packer/tests/preview.spec.ts b/packages/experimental/webworker-packer/tests/preview.spec.ts
index f3b5937ab9..0e0c7f7e12 100644
--- a/packages/experimental/webworker-packer/tests/preview.spec.ts
+++ b/packages/experimental/webworker-packer/tests/preview.spec.ts
@@ -42,7 +42,7 @@ function restore(bytes: Uint8Array | undefined) {
}
describe('Node preparation of Preview Session data', () => {
- it('packs strictly migrated current successors beside every unchanged example file', () => {
+ it('packs current Sessions and migrated children beside every unchanged example file', () => {
const fixture = previewFixtures(repoRoot)[0]!
const original = packVfsOverlay(fixture.trees)
const projected = packPreviewFixture(fixture.trees)
@@ -50,15 +50,13 @@ describe('Node preparation of Preview Session data', () => {
expect(currentFiles).toHaveLength(3)
for (const [path, bytes] of Object.entries(original.files)) expect(projected.files[path], path).toEqual(bytes)
for (const path of currentFiles) {
- const sourcePath = path.replace(/\/session\.v\d+\.jsonl$/u, '/session.v3.jsonl')
- const historical = restore(original.files[sourcePath])
const current = restore(projected.files[path])
- const appended = path.includes('/preview-showcase/') ? [
- { version: 0, childId: 'preview-architecture-review', childCreatedAt: 1787472100000, mode: 'one-shot', label: 'Review preview architecture' },
- { version: 0, childId: 'preview-follow-up-builder', childCreatedAt: 1787472200000, mode: 'continuable', label: 'Continue preview verification' },
- ].map((data, index) => ({ type: 'subagent/catalog', seq: historical.events.length + index,
- time: historical.events.at(-1)!.time, data })) : []
- expect(current).toEqual({ ...historical, events: [...historical.events, ...appended] })
+ if (original.files[path] !== undefined) {
+ expect(projected.files[path]).toEqual(original.files[path])
+ } else {
+ const sourcePath = path.replace(/\/session\.v\d+\.jsonl$/u, '/session.v3.jsonl')
+ expect(current).toEqual(restore(original.files[sourcePath]))
+ }
expect(JSON.parse(text(projected.files[path]).split('\n')[0]!)).toMatchObject({ version: currentVersion })
}
expect(packPreviewFixture(fixture.trees).image).toEqual(projected.image)
diff --git a/packages/experimental/webworker-runtime/README.i18n.yaml b/packages/experimental/webworker-runtime/README.i18n.yaml
index d70dd93cb7..3ac3336a7a 100644
--- a/packages/experimental/webworker-runtime/README.i18n.yaml
+++ b/packages/experimental/webworker-runtime/README.i18n.yaml
@@ -15,8 +15,8 @@
en: 4e53164159b5ce41
zh: 89582dc0294d0776
/deepseek-ai-dsh-experimental-webworker-runtime/use-this-package:
- en: 6dd3d8495bc6ee61
- zh: 71f7da310233a37b
+ en: aed3c07b75b29e34
+ zh: b609ae0f5796aa34
/deepseek-ai-dsh-experimental-webworker-runtime/model-experience:
en: 61fce9b4c7dc96e6
zh: dc2d931063a38327
diff --git a/packages/experimental/webworker-runtime/README.md b/packages/experimental/webworker-runtime/README.md
index 8394296bf2..2f5619640f 100644
--- a/packages/experimental/webworker-runtime/README.md
+++ b/packages/experimental/webworker-runtime/README.md
@@ -32,7 +32,7 @@ Three build artifacts and one source-owned process layer:
- **`src/shell/` (the worker's own process layer)** — a browser worker cannot fork, so `node:child_process` is not a stub but an implementation: `spawn` starts the command in its own Web Worker — this same bundle, told by its first frame to be a shell process — and reports it through the `ChildProcess` surface the subprocess service consumes. The command runs off the host's thread, `SIGKILL` terminates it whatever it is doing, and it reaches the VFS only by message (the host serves those frames). Worker platform executables preserve native-package protocols such as Landlock without replacing their JavaScript packages or coupling their implementations to `node:child_process`; `xdg-open` sends one VFS text file to the page's read-only viewer, so desktop-opener actions such as **Open configuration file** show the file in the page; ordinary commands use the package's evaluator and coreutils command table. The grammar is `@yarnpkg/parsers`' `parseShell`, while `execSync`/`fork` still refuse because they need a real process.
- **`lib/client.js` (page half)** — startup has two independent stages. `chooseWorkerHostSource({ image?, fixtureManifest? })` optionally owns the boot barrier and fixture manifest: without `preview-fixture` it waits at the source chooser, while a valid query selects directly; either path returns ordered overlays. `connectWorkerHost(worker, { image?, overlays? })` remains the public base-runtime connector; callers that skip the chooser get an empty overlay list. `apps/web` invokes both and supplies its statically bundled Worker. The opening `init` frame carries the base and ordered overlay URLs, the boot payload delivers the structured index-injection table, and `applyIndexInjections` executes it before the shell entry runs. Script preload rows are advisory and skipped because `/plugins` resources resolve only through the tunnel; `loadBundle` fetches each combo on first demand, embeds its tunnel-only source map as a Base64 data URL, and executes the script as a Blob. The worker's unsolicited `view-text` frame opens the page's modal text viewer. The tunnel also exposes fetch-shaped transport, the independent file-upload carrier, and the API client. Request frames preserve Blob bodies through structured clone and transfer `ReadableStream` ownership. The Host Worker streams both forms into the route, so neither browser thread creates a complete byte array for a generic-file upload. A Remote stream's uplink crosses as `stream-uplink-item` / `stream-uplink-end` frames into an unbounded `TunnelUplink` that drops items arriving after `end`: the page and its worker are one trusted same-origin boundary, so the Host inbox cap does not apply.
-Acceptance lives in `apps/web/tests/preview-boot.e2e.ts`, which serves the real built pages and drives the pre-boot chooser plus Worker activation in headless Chromium. The empty selection exercises first-run startup. The `vfs-example` overlay supplies ordinary workspace files and plaintext persistence artifacts for cold Workspace/Session discovery, tool presentation, subagent navigation, and history paging without a model request. The [packer](../webworker-packer/README.md) prepares current-generation successors from committed historical logs in Node; those source logs remain byte-identical in the overlay. The fixture generator owns the acceptance test's current projection cache. The chooser reserves WebFS as a separate user-authorized source; that provider does not read the built-in fixture.
+Acceptance lives in `apps/web/tests/preview-boot.e2e.ts`, which serves the real built pages and drives the pre-boot chooser plus Worker activation in headless Chromium. The empty selection exercises first-run startup. The `vfs-example` overlay supplies ordinary workspace files and plaintext persistence artifacts for cold Workspace/Session discovery, tool presentation, subagent navigation, and history paging without a model request. The showcase mixes empty, whitespace-only, and non-empty reasoning with a long multiline command and a parent-path file link; these are authored fixtures, not live model output. The [packer](../webworker-packer/README.md) prepares current-generation successors from committed historical logs in Node; those source logs remain byte-identical in the overlay. The fixture generator owns the acceptance test's current projection cache. The chooser reserves WebFS as a separate user-authorized source; that provider does not read the built-in fixture.
The [built-bundle import sweep](tests/compile/transform-corpus-check.ts) checks bare Node imports after the library build. Its Dockkit exception accepts Node’s unknown-`.css`-extension refusal for any stylesheet rather than one pinned path; another extension, error code, or message, and an unexpectedly successful exempt import, remain errors.
diff --git a/packages/experimental/webworker-runtime/README.zh.md b/packages/experimental/webworker-runtime/README.zh.md
index b63f81247b..dffbb30718 100644
--- a/packages/experimental/webworker-runtime/README.zh.md
+++ b/packages/experimental/webworker-runtime/README.zh.md
@@ -32,7 +32,7 @@ kind: "package-library"
- **`src/shell/`(worker 自己的进程层)**——浏览器 worker 无法 fork,所以 `node:child_process` 不是 stub 而是实现:`spawn` 把命令放进它自己的 Web Worker——就是这同一个束,由首帧告诉它「你是 shell 进程」——并以 subprocess 服务消费的 `ChildProcess` 面报告结果。命令不占宿主线程,`SIGKILL` 不管它在干什么都能终止它,而它只能靠消息触达 VFS(由宿主应答这些帧)。Worker 平台 executable 在不替换 JavaScript 包、也不把具体实现耦合进 `node:child_process` 的情况下保持 Landlock 等 native 包协议;`xdg-open` 把一个 VFS 文本文件交给页面的只读查看器,因此 **打开配置文件** 等桌面打开操作会在页面内显示该文件;普通命令使用本包的求值器与 coreutils 命令表。语法来自 `@yarnpkg/parsers` 的 `parseShell`,而 `execSync`/`fork` 依然拒绝,因为它们需要真进程。
- **`lib/client.js`(页面半)**——启动分为相互独立的两段。`chooseWorkerHostSource({ image?, fixtureManifest? })` 可选地拥有 boot barrier 与 fixture manifest:没有 `preview-fixture` 时停在来源选择面板,合法 query 则直接选择;两条路径都返回按序排列的 overlays。`connectWorkerHost(worker, { image?, overlays? })` 仍是公开的基础运行态连接器;调用方跳过选择器时 overlay 列表为空。`apps/web` 调用这两段并提供静态打包的 Worker。开局 `init` 帧携带基础镜像与按序排列的 overlay URL,boot 载荷送达结构化 index 注入表,`applyIndexInjections` 在壳入口运行前逐行执行。脚本 preload 行只是提示,因此会被跳过:`/plugins` 资源只能经 tunnel 解析,`loadBundle` 会在首次需要时获取 combo、把仅 tunnel 可达的 sourcemap 内嵌为 Base64 data URL,再以 Blob 执行脚本。worker 主动发出的 `view-text` 帧会打开页面的模态文本查看器。Tunnel 还暴露 fetch 形式的传输、独立文件上传载体和 API 客户端。请求帧通过结构化克隆保留 Blob 请求体,并转移 `ReadableStream` 的所有权。Host Worker 将两种请求体都逐块送入路由,因此通用文件上传不会在任何浏览器线程创建完整字节数组。Remote 流的上行以 `stream-uplink-item` / `stream-uplink-end` 帧进入没有上限的 `TunnelUplink`,`end` 之后到达的项被丢弃:页面与其 worker 是同一个可信的同源边界,Host 的 inbox 上限在此不适用。
-验收在 `apps/web/tests/preview-boot.e2e.ts`:静态服务真实构建页面,在 headless Chromium 里驱动 pre-boot 选择面板与 Worker 激活。空白选择验证首次启动;`vfs-example` overlay 提供普通 workspace 文件与明文 persistence 产物,无需模型请求即可验证 Workspace/Session 冷发现、工具呈现、subagent 导航和历史分页。[打包器](../webworker-packer/README.zh.md)在 Node 中根据已提交的历史日志准备当前代后继文件;这些源日志在 overlay 中逐字节保持不变。fixture 生成器负责验收测试的当前投影缓存。选择面板为 WebFS 保留独立的用户授权来源;该 provider 不读取内置 fixture。
+验收在 `apps/web/tests/preview-boot.e2e.ts`:静态服务真实构建页面,在 headless Chromium 里驱动 pre-boot 选择面板与 Worker 激活。空白选择验证首次启动;`vfs-example` overlay 提供普通 workspace 文件与明文 persistence 产物,无需模型请求即可验证 Workspace/Session 冷发现、工具呈现、subagent 导航和历史分页。showcase 混排空字符串、纯空白与非空推理块,并包含多行长命令和父路径文件链接;这些是人工构造的 fixture,不是真实模型输出。[打包器](../webworker-packer/README.zh.md)在 Node 中根据已提交的历史日志准备当前代后继文件;这些源日志在 overlay 中逐字节保持不变。fixture 生成器负责验收测试的当前投影缓存。选择面板为 WebFS 保留独立的用户授权来源;该 provider 不读取内置 fixture。
[已构建 bundle 导入检查](tests/compile/transform-corpus-check.ts)在库构建后检查裸 Node 导入。Dockkit 例外接受 Node 针对任意样式表报告的未知 `.css` 扩展名错误,而不限定某一固定路径;其他扩展名、错误码或消息,以及意外成功的豁免导入,仍然报错。
diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.v4.jsonl b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.v4.jsonl
new file mode 100644
index 0000000000..abed3fcfd9
--- /dev/null
+++ b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.v4.jsonl
@@ -0,0 +1,204 @@
+{"type":"session","version":4,"id":"preview-showcase","createdAt":1787472000000,"cwd":"/dsh/workspace","isSeeded":false,"delegationDepth":0,"agentPreset":"standard"}
+{"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472000000}
+{"type":"user/message","data":{"id":"preview-user-01","role":"user","content":[{"type":"text","text":"History checkpoint 01: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472000001}
+{"type":"session/title","data":{"title":"WebWorker Preview Showcase","messageSeqs":[],"source":{"kind":"user"}},"seq":2,"time":1787472000002}
+{"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472000003}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-assistant-01","role":"assistant","content":[{"type":"text","text":"Checkpoint 01 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 01 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 01 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":4,"time":1787472000004}
+{"type":"step/end","data":{"turn":1,"step":1},"seq":5,"time":1787472000005}
+{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}},"seq":6,"time":1787472000006}
+{"type":"turn/start","data":{"turn":2},"seq":7,"time":1787472000007}
+{"type":"user/message","data":{"id":"preview-user-02","role":"user","content":[{"type":"text","text":"History checkpoint 02: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":8,"time":1787472000008}
+{"type":"step/start","data":{"turn":2,"step":1},"seq":9,"time":1787472000009}
+{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"id":"preview-assistant-02","role":"assistant","content":[{"type":"text","text":"Checkpoint 02 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 02 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 02 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":10,"time":1787472000010}
+{"type":"step/end","data":{"turn":2,"step":1},"seq":11,"time":1787472000011}
+{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}},"seq":12,"time":1787472000012}
+{"type":"turn/start","data":{"turn":3},"seq":13,"time":1787472000013}
+{"type":"user/message","data":{"id":"preview-user-03","role":"user","content":[{"type":"text","text":"History checkpoint 03: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":14,"time":1787472000014}
+{"type":"step/start","data":{"turn":3,"step":1},"seq":15,"time":1787472000015}
+{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"id":"preview-assistant-03","role":"assistant","content":[{"type":"text","text":"Checkpoint 03 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 03 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 03 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":16,"time":1787472000016}
+{"type":"step/end","data":{"turn":3,"step":1},"seq":17,"time":1787472000017}
+{"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}},"seq":18,"time":1787472000018}
+{"type":"turn/start","data":{"turn":4},"seq":19,"time":1787472000019}
+{"type":"user/message","data":{"id":"preview-user-04","role":"user","content":[{"type":"text","text":"History checkpoint 04: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":20,"time":1787472000020}
+{"type":"step/start","data":{"turn":4,"step":1},"seq":21,"time":1787472000021}
+{"type":"assistant/message","data":{"turn":4,"step":1,"message":{"id":"preview-assistant-04","role":"assistant","content":[{"type":"text","text":"Checkpoint 04 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 04 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 04 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":22,"time":1787472000022}
+{"type":"step/end","data":{"turn":4,"step":1},"seq":23,"time":1787472000023}
+{"type":"turn/end","data":{"turn":4,"reason":{"kind":"completed"}},"seq":24,"time":1787472000024}
+{"type":"turn/start","data":{"turn":5},"seq":25,"time":1787472000025}
+{"type":"user/message","data":{"id":"preview-user-05","role":"user","content":[{"type":"text","text":"History checkpoint 05: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":26,"time":1787472000026}
+{"type":"step/start","data":{"turn":5,"step":1},"seq":27,"time":1787472000027}
+{"type":"assistant/message","data":{"turn":5,"step":1,"message":{"id":"preview-assistant-05","role":"assistant","content":[{"type":"text","text":"Checkpoint 05 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 05 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 05 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":28,"time":1787472000028}
+{"type":"step/end","data":{"turn":5,"step":1},"seq":29,"time":1787472000029}
+{"type":"turn/end","data":{"turn":5,"reason":{"kind":"completed"}},"seq":30,"time":1787472000030}
+{"type":"turn/start","data":{"turn":6},"seq":31,"time":1787472000031}
+{"type":"user/message","data":{"id":"preview-user-06","role":"user","content":[{"type":"text","text":"History checkpoint 06: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":32,"time":1787472000032}
+{"type":"step/start","data":{"turn":6,"step":1},"seq":33,"time":1787472000033}
+{"type":"assistant/message","data":{"turn":6,"step":1,"message":{"id":"preview-assistant-06","role":"assistant","content":[{"type":"text","text":"Checkpoint 06 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 06 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 06 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":34,"time":1787472000034}
+{"type":"step/end","data":{"turn":6,"step":1},"seq":35,"time":1787472000035}
+{"type":"turn/end","data":{"turn":6,"reason":{"kind":"completed"}},"seq":36,"time":1787472000036}
+{"type":"turn/start","data":{"turn":7},"seq":37,"time":1787472000037}
+{"type":"user/message","data":{"id":"preview-user-07","role":"user","content":[{"type":"text","text":"History checkpoint 07: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":38,"time":1787472000038}
+{"type":"step/start","data":{"turn":7,"step":1},"seq":39,"time":1787472000039}
+{"type":"assistant/message","data":{"turn":7,"step":1,"message":{"id":"preview-assistant-07","role":"assistant","content":[{"type":"text","text":"Checkpoint 07 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 07 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 07 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":40,"time":1787472000040}
+{"type":"step/end","data":{"turn":7,"step":1},"seq":41,"time":1787472000041}
+{"type":"turn/end","data":{"turn":7,"reason":{"kind":"completed"}},"seq":42,"time":1787472000042}
+{"type":"turn/start","data":{"turn":8},"seq":43,"time":1787472000043}
+{"type":"user/message","data":{"id":"preview-user-08","role":"user","content":[{"type":"text","text":"History checkpoint 08: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":44,"time":1787472000044}
+{"type":"step/start","data":{"turn":8,"step":1},"seq":45,"time":1787472000045}
+{"type":"assistant/message","data":{"turn":8,"step":1,"message":{"id":"preview-assistant-08","role":"assistant","content":[{"type":"text","text":"Checkpoint 08 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 08 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 08 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":46,"time":1787472000046}
+{"type":"step/end","data":{"turn":8,"step":1},"seq":47,"time":1787472000047}
+{"type":"turn/end","data":{"turn":8,"reason":{"kind":"completed"}},"seq":48,"time":1787472000048}
+{"type":"turn/start","data":{"turn":9},"seq":49,"time":1787472000049}
+{"type":"user/message","data":{"id":"preview-user-09","role":"user","content":[{"type":"text","text":"History checkpoint 09: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":50,"time":1787472000050}
+{"type":"step/start","data":{"turn":9,"step":1},"seq":51,"time":1787472000051}
+{"type":"assistant/message","data":{"turn":9,"step":1,"message":{"id":"preview-assistant-09","role":"assistant","content":[{"type":"text","text":"Checkpoint 09 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 09 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 09 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":52,"time":1787472000052}
+{"type":"step/end","data":{"turn":9,"step":1},"seq":53,"time":1787472000053}
+{"type":"turn/end","data":{"turn":9,"reason":{"kind":"completed"}},"seq":54,"time":1787472000054}
+{"type":"turn/start","data":{"turn":10},"seq":55,"time":1787472000055}
+{"type":"user/message","data":{"id":"preview-user-10","role":"user","content":[{"type":"text","text":"History checkpoint 10: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":56,"time":1787472000056}
+{"type":"step/start","data":{"turn":10,"step":1},"seq":57,"time":1787472000057}
+{"type":"assistant/message","data":{"turn":10,"step":1,"message":{"id":"preview-assistant-10","role":"assistant","content":[{"type":"text","text":"Checkpoint 10 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 10 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 10 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":58,"time":1787472000058}
+{"type":"step/end","data":{"turn":10,"step":1},"seq":59,"time":1787472000059}
+{"type":"turn/end","data":{"turn":10,"reason":{"kind":"completed"}},"seq":60,"time":1787472000060}
+{"type":"turn/start","data":{"turn":11},"seq":61,"time":1787472000061}
+{"type":"user/message","data":{"id":"preview-user-11","role":"user","content":[{"type":"text","text":"History checkpoint 11: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":62,"time":1787472000062}
+{"type":"step/start","data":{"turn":11,"step":1},"seq":63,"time":1787472000063}
+{"type":"assistant/message","data":{"turn":11,"step":1,"message":{"id":"preview-assistant-11","role":"assistant","content":[{"type":"text","text":"Checkpoint 11 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 11 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 11 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":64,"time":1787472000064}
+{"type":"step/end","data":{"turn":11,"step":1},"seq":65,"time":1787472000065}
+{"type":"turn/end","data":{"turn":11,"reason":{"kind":"completed"}},"seq":66,"time":1787472000066}
+{"type":"turn/start","data":{"turn":12},"seq":67,"time":1787472000067}
+{"type":"user/message","data":{"id":"preview-user-12","role":"user","content":[{"type":"text","text":"History checkpoint 12: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":68,"time":1787472000068}
+{"type":"step/start","data":{"turn":12,"step":1},"seq":69,"time":1787472000069}
+{"type":"assistant/message","data":{"turn":12,"step":1,"message":{"id":"preview-assistant-12","role":"assistant","content":[{"type":"text","text":"Checkpoint 12 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 12 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 12 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":70,"time":1787472000070}
+{"type":"step/end","data":{"turn":12,"step":1},"seq":71,"time":1787472000071}
+{"type":"turn/end","data":{"turn":12,"reason":{"kind":"completed"}},"seq":72,"time":1787472000072}
+{"type":"turn/start","data":{"turn":13},"seq":73,"time":1787472000073}
+{"type":"user/message","data":{"id":"preview-user-13","role":"user","content":[{"type":"text","text":"History checkpoint 13: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":74,"time":1787472000074}
+{"type":"step/start","data":{"turn":13,"step":1},"seq":75,"time":1787472000075}
+{"type":"assistant/message","data":{"turn":13,"step":1,"message":{"id":"preview-assistant-13","role":"assistant","content":[{"type":"text","text":"Checkpoint 13 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 13 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 13 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":76,"time":1787472000076}
+{"type":"step/end","data":{"turn":13,"step":1},"seq":77,"time":1787472000077}
+{"type":"turn/end","data":{"turn":13,"reason":{"kind":"completed"}},"seq":78,"time":1787472000078}
+{"type":"turn/start","data":{"turn":14},"seq":79,"time":1787472000079}
+{"type":"user/message","data":{"id":"preview-user-14","role":"user","content":[{"type":"text","text":"History checkpoint 14: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":80,"time":1787472000080}
+{"type":"step/start","data":{"turn":14,"step":1},"seq":81,"time":1787472000081}
+{"type":"assistant/message","data":{"turn":14,"step":1,"message":{"id":"preview-assistant-14","role":"assistant","content":[{"type":"text","text":"Checkpoint 14 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 14 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 14 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":82,"time":1787472000082}
+{"type":"step/end","data":{"turn":14,"step":1},"seq":83,"time":1787472000083}
+{"type":"turn/end","data":{"turn":14,"reason":{"kind":"completed"}},"seq":84,"time":1787472000084}
+{"type":"turn/start","data":{"turn":15},"seq":85,"time":1787472000085}
+{"type":"user/message","data":{"id":"preview-user-15","role":"user","content":[{"type":"text","text":"History checkpoint 15: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":86,"time":1787472000086}
+{"type":"step/start","data":{"turn":15,"step":1},"seq":87,"time":1787472000087}
+{"type":"assistant/message","data":{"turn":15,"step":1,"message":{"id":"preview-assistant-15","role":"assistant","content":[{"type":"text","text":"Checkpoint 15 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 15 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 15 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":88,"time":1787472000088}
+{"type":"step/end","data":{"turn":15,"step":1},"seq":89,"time":1787472000089}
+{"type":"turn/end","data":{"turn":15,"reason":{"kind":"completed"}},"seq":90,"time":1787472000090}
+{"type":"turn/start","data":{"turn":16},"seq":91,"time":1787472000091}
+{"type":"user/message","data":{"id":"preview-user-16","role":"user","content":[{"type":"text","text":"History checkpoint 16: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":92,"time":1787472000092}
+{"type":"step/start","data":{"turn":16,"step":1},"seq":93,"time":1787472000093}
+{"type":"assistant/message","data":{"turn":16,"step":1,"message":{"id":"preview-assistant-16","role":"assistant","content":[{"type":"text","text":"Checkpoint 16 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 16 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 16 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":94,"time":1787472000094}
+{"type":"step/end","data":{"turn":16,"step":1},"seq":95,"time":1787472000095}
+{"type":"turn/end","data":{"turn":16,"reason":{"kind":"completed"}},"seq":96,"time":1787472000096}
+{"type":"turn/start","data":{"turn":17},"seq":97,"time":1787472000097}
+{"type":"user/message","data":{"id":"preview-user-17","role":"user","content":[{"type":"text","text":"History checkpoint 17: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":98,"time":1787472000098}
+{"type":"step/start","data":{"turn":17,"step":1},"seq":99,"time":1787472000099}
+{"type":"assistant/message","data":{"turn":17,"step":1,"message":{"id":"preview-assistant-17","role":"assistant","content":[{"type":"text","text":"Checkpoint 17 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 17 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 17 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":100,"time":1787472000100}
+{"type":"step/end","data":{"turn":17,"step":1},"seq":101,"time":1787472000101}
+{"type":"turn/end","data":{"turn":17,"reason":{"kind":"completed"}},"seq":102,"time":1787472000102}
+{"type":"turn/start","data":{"turn":18},"seq":103,"time":1787472000103}
+{"type":"user/message","data":{"id":"preview-user-18","role":"user","content":[{"type":"text","text":"History checkpoint 18: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":104,"time":1787472000104}
+{"type":"step/start","data":{"turn":18,"step":1},"seq":105,"time":1787472000105}
+{"type":"assistant/message","data":{"turn":18,"step":1,"message":{"id":"preview-assistant-18","role":"assistant","content":[{"type":"text","text":"Checkpoint 18 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 18 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 18 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":106,"time":1787472000106}
+{"type":"step/end","data":{"turn":18,"step":1},"seq":107,"time":1787472000107}
+{"type":"turn/end","data":{"turn":18,"reason":{"kind":"completed"}},"seq":108,"time":1787472000108}
+{"type":"turn/start","data":{"turn":19},"seq":109,"time":1787472000109}
+{"type":"user/message","data":{"id":"preview-user-19","role":"user","content":[{"type":"text","text":"History checkpoint 19: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":110,"time":1787472000110}
+{"type":"step/start","data":{"turn":19,"step":1},"seq":111,"time":1787472000111}
+{"type":"assistant/message","data":{"turn":19,"step":1,"message":{"id":"preview-assistant-19","role":"assistant","content":[{"type":"text","text":"Checkpoint 19 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 19 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 19 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":112,"time":1787472000112}
+{"type":"step/end","data":{"turn":19,"step":1},"seq":113,"time":1787472000113}
+{"type":"turn/end","data":{"turn":19,"reason":{"kind":"completed"}},"seq":114,"time":1787472000114}
+{"type":"turn/start","data":{"turn":20},"seq":115,"time":1787472000115}
+{"type":"user/message","data":{"id":"preview-user-20","role":"user","content":[{"type":"text","text":"History checkpoint 20: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":116,"time":1787472000116}
+{"type":"step/start","data":{"turn":20,"step":1},"seq":117,"time":1787472000117}
+{"type":"assistant/message","data":{"turn":20,"step":1,"message":{"id":"preview-assistant-20","role":"assistant","content":[{"type":"text","text":"Checkpoint 20 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 20 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 20 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":118,"time":1787472000118}
+{"type":"step/end","data":{"turn":20,"step":1},"seq":119,"time":1787472000119}
+{"type":"turn/end","data":{"turn":20,"reason":{"kind":"completed"}},"seq":120,"time":1787472000120}
+{"type":"turn/start","data":{"turn":21},"seq":121,"time":1787472000121}
+{"type":"user/message","data":{"id":"preview-user-21","role":"user","content":[{"type":"text","text":"History checkpoint 21: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":122,"time":1787472000122}
+{"type":"step/start","data":{"turn":21,"step":1},"seq":123,"time":1787472000123}
+{"type":"assistant/message","data":{"turn":21,"step":1,"message":{"id":"preview-assistant-21","role":"assistant","content":[{"type":"text","text":"Checkpoint 21 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 21 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 21 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":124,"time":1787472000124}
+{"type":"step/end","data":{"turn":21,"step":1},"seq":125,"time":1787472000125}
+{"type":"turn/end","data":{"turn":21,"reason":{"kind":"completed"}},"seq":126,"time":1787472000126}
+{"type":"turn/start","data":{"turn":22},"seq":127,"time":1787472000127}
+{"type":"user/message","data":{"id":"preview-user-22","role":"user","content":[{"type":"text","text":"History checkpoint 22: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":128,"time":1787472000128}
+{"type":"step/start","data":{"turn":22,"step":1},"seq":129,"time":1787472000129}
+{"type":"assistant/message","data":{"turn":22,"step":1,"message":{"id":"preview-assistant-22","role":"assistant","content":[{"type":"text","text":"Checkpoint 22 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 22 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 22 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":130,"time":1787472000130}
+{"type":"step/end","data":{"turn":22,"step":1},"seq":131,"time":1787472000131}
+{"type":"turn/end","data":{"turn":22,"reason":{"kind":"completed"}},"seq":132,"time":1787472000132}
+{"type":"turn/start","data":{"turn":23},"seq":133,"time":1787472000133}
+{"type":"user/message","data":{"id":"preview-user-23","role":"user","content":[{"type":"text","text":"History checkpoint 23: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":134,"time":1787472000134}
+{"type":"step/start","data":{"turn":23,"step":1},"seq":135,"time":1787472000135}
+{"type":"assistant/message","data":{"turn":23,"step":1,"message":{"id":"preview-assistant-23","role":"assistant","content":[{"type":"text","text":"Checkpoint 23 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 23 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 23 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":136,"time":1787472000136}
+{"type":"step/end","data":{"turn":23,"step":1},"seq":137,"time":1787472000137}
+{"type":"turn/end","data":{"turn":23,"reason":{"kind":"completed"}},"seq":138,"time":1787472000138}
+{"type":"turn/start","data":{"turn":24},"seq":139,"time":1787472000139}
+{"type":"user/message","data":{"id":"preview-user-24","role":"user","content":[{"type":"text","text":"History checkpoint 24: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":140,"time":1787472000140}
+{"type":"step/start","data":{"turn":24,"step":1},"seq":141,"time":1787472000141}
+{"type":"assistant/message","data":{"turn":24,"step":1,"message":{"id":"preview-assistant-24","role":"assistant","content":[{"type":"text","text":"Checkpoint 24 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 24 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 24 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":142,"time":1787472000142}
+{"type":"step/end","data":{"turn":24,"step":1},"seq":143,"time":1787472000143}
+{"type":"turn/end","data":{"turn":24,"reason":{"kind":"completed"}},"seq":144,"time":1787472000144}
+{"type":"turn/start","data":{"turn":25},"seq":145,"time":1787472000145}
+{"type":"user/message","data":{"id":"preview-user-25","role":"user","content":[{"type":"text","text":"History checkpoint 25: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":146,"time":1787472000146}
+{"type":"step/start","data":{"turn":25,"step":1},"seq":147,"time":1787472000147}
+{"type":"assistant/message","data":{"turn":25,"step":1,"message":{"id":"preview-assistant-25","role":"assistant","content":[{"type":"text","text":"Checkpoint 25 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 25 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 25 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":148,"time":1787472000148}
+{"type":"step/end","data":{"turn":25,"step":1},"seq":149,"time":1787472000149}
+{"type":"turn/end","data":{"turn":25,"reason":{"kind":"completed"}},"seq":150,"time":1787472000150}
+{"type":"turn/start","data":{"turn":26},"seq":151,"time":1787472000151}
+{"type":"user/message","data":{"id":"preview-user-26","role":"user","content":[{"type":"text","text":"History checkpoint 26: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":152,"time":1787472000152}
+{"type":"step/start","data":{"turn":26,"step":1},"seq":153,"time":1787472000153}
+{"type":"assistant/message","data":{"turn":26,"step":1,"message":{"id":"preview-assistant-26","role":"assistant","content":[{"type":"text","text":"Checkpoint 26 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 26 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 26 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":154,"time":1787472000154}
+{"type":"step/end","data":{"turn":26,"step":1},"seq":155,"time":1787472000155}
+{"type":"turn/end","data":{"turn":26,"reason":{"kind":"completed"}},"seq":156,"time":1787472000156}
+{"type":"turn/start","data":{"turn":27},"seq":157,"time":1787472000157}
+{"type":"user/message","data":{"id":"preview-user-27","role":"user","content":[{"type":"text","text":"History checkpoint 27: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":158,"time":1787472000158}
+{"type":"step/start","data":{"turn":27,"step":1},"seq":159,"time":1787472000159}
+{"type":"assistant/message","data":{"turn":27,"step":1,"message":{"id":"preview-assistant-27","role":"assistant","content":[{"type":"text","text":"Checkpoint 27 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 27 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 27 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":160,"time":1787472000160}
+{"type":"step/end","data":{"turn":27,"step":1},"seq":161,"time":1787472000161}
+{"type":"turn/end","data":{"turn":27,"reason":{"kind":"completed"}},"seq":162,"time":1787472000162}
+{"type":"turn/start","data":{"turn":28},"seq":163,"time":1787472000163}
+{"type":"user/message","data":{"id":"preview-user-28","role":"user","content":[{"type":"text","text":"History checkpoint 28: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":164,"time":1787472000164}
+{"type":"step/start","data":{"turn":28,"step":1},"seq":165,"time":1787472000165}
+{"type":"assistant/message","data":{"turn":28,"step":1,"message":{"id":"preview-assistant-28","role":"assistant","content":[{"type":"text","text":"Checkpoint 28 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["Checkpoint 28 is recorded."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Checkpoint 28 is recorded."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":166,"time":1787472000166}
+{"type":"step/end","data":{"turn":28,"step":1},"seq":167,"time":1787472000167}
+{"type":"turn/end","data":{"turn":28,"reason":{"kind":"completed"}},"seq":168,"time":1787472000168}
+{"type":"turn/start","data":{"turn":29},"seq":169,"time":1787472000169}
+{"type":"user/message","data":{"id":"preview-gallery-user","role":"user","content":[{"type":"text","text":"Show the seeded workspace, tool cards, subagents, and pagination in one tour."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":170,"time":1787472000170}
+{"type":"step/start","data":{"turn":29,"step":1},"seq":171,"time":1787472000171}
+{"type":"assistant/message","data":{"turn":29,"step":1,"message":{"id":"preview-gallery-tools","role":"assistant","content":[{"type":"reasoning","text":""},{"type":"reasoning","text":" \t\n"},{"type":"reasoning","text":"I will inspect the deterministic workspace and collect each preview surface."},{"type":"reasoning","text":""},{"type":"reasoning","text":"Check the Chat presentation\n\nI will inspect the wrapped command, its first-line status, and the parent-path file link."},{"type":"reasoning","text":"\n "},{"type":"tool-call","id":"preview-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},{"type":"tool-call","id":"preview-write","name":"write","arguments":"{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"},{"type":"tool-call","id":"preview-bash","name":"bash","arguments":"{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"},{"type":"tool-call","id":"preview-wrapped-bash","name":"bash","arguments":"{\"command\":\"printf 'preview command fixture\\\\n' # wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-\\nprintf 'wrap complete\\\\n'\\n\",\"description\":\"Show wrapped command and first-line status\"}"},{"type":"tool-call","id":"preview-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"},{"type":"tool-call","id":"preview-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"},{"type":"tool-call","id":"preview-web-search","name":"web_search","arguments":"{\"queries\":[\"Web Worker filesystem compatibility\"]}"},{"type":"tool-call","id":"preview-todo","name":"todo_write","arguments":"{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"},{"type":"tool-call","id":"preview-subagent","name":"subagent","arguments":"{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"},{"type":"tool-call","id":"preview-subagent-fork","name":"subagent_fork","arguments":"{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"},{"type":"tool-call","id":"preview-failure","name":"read","arguments":"{\"file_path\":\"missing.txt\"}"}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":0,"dt":[],"texts":[""]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":""}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":1,"dt":[],"texts":[" \t\n"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"reasoning","text":" \t\n"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":2,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":2,"dt":[],"texts":["I will inspect the deterministic workspace and collect each preview surface."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":2,"block":{"type":"reasoning","text":"I will inspect the deterministic workspace and collect each preview surface."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":3,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":3,"dt":[],"texts":[""]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":3,"block":{"type":"reasoning","text":""}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":4,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":4,"dt":[],"texts":["Check the Chat presentation\n\nI will inspect the wrapped command, its first-line status, and the parent-path file link."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":4,"block":{"type":"reasoning","text":"Check the Chat presentation\n\nI will inspect the wrapped command, its first-line status, and the parent-path file link."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":5,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":5,"dt":[],"texts":["\n "]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":5,"block":{"type":"reasoning","text":"\n "}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":6,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":6,"dt":[],"id":"preview-read","name":"read","args":["{\"file_path\":\"PREVIEW.md\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":6,"block":{"type":"tool-call","id":"preview-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":7,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":7,"dt":[],"id":"preview-write","name":"write","args":["{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":7,"block":{"type":"tool-call","id":"preview-write","name":"write","arguments":"{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":8,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":8,"dt":[],"id":"preview-bash","name":"bash","args":["{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":8,"block":{"type":"tool-call","id":"preview-bash","name":"bash","arguments":"{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":9,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":9,"dt":[],"id":"preview-wrapped-bash","name":"bash","args":["{\"command\":\"printf 'preview command fixture\\\\n' # wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-\\nprintf 'wrap complete\\\\n'\\n\",\"description\":\"Show wrapped command and first-line status\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":9,"block":{"type":"tool-call","id":"preview-wrapped-bash","name":"bash","arguments":"{\"command\":\"printf 'preview command fixture\\\\n' # wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-\\nprintf 'wrap complete\\\\n'\\n\",\"description\":\"Show wrapped command and first-line status\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":10,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":10,"dt":[],"id":"preview-glob","name":"glob","args":["{\"pattern\":\"**/*\",\"path\":\".\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":10,"block":{"type":"tool-call","id":"preview-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":11,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":11,"dt":[],"id":"preview-grep","name":"grep","args":["{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":11,"block":{"type":"tool-call","id":"preview-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":12,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":12,"dt":[],"id":"preview-web-search","name":"web_search","args":["{\"queries\":[\"Web Worker filesystem compatibility\"]}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":12,"block":{"type":"tool-call","id":"preview-web-search","name":"web_search","arguments":"{\"queries\":[\"Web Worker filesystem compatibility\"]}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":13,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":13,"dt":[],"id":"preview-todo","name":"todo_write","args":["{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":13,"block":{"type":"tool-call","id":"preview-todo","name":"todo_write","arguments":"{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":14,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":14,"dt":[],"id":"preview-subagent","name":"subagent","args":["{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":14,"block":{"type":"tool-call","id":"preview-subagent","name":"subagent","arguments":"{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":15,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":15,"dt":[],"id":"preview-subagent-fork","name":"subagent_fork","args":["{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":15,"block":{"type":"tool-call","id":"preview-subagent-fork","name":"subagent_fork","arguments":"{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":16,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":16,"dt":[],"id":"preview-failure","name":"read","args":["{\"file_path\":\"missing.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":16,"block":{"type":"tool-call","id":"preview-failure","name":"read","arguments":"{\"file_path\":\"missing.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append","seq":172,"time":1787472000172}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},"seq":173,"time":1787472000173}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-read-result","role":"tool","toolCallId":"preview-read","source":{"kind":"tool","callId":"preview-read"},"content":[{"type":"text","text":"PREVIEW.md\nfile\n\n1: # Preview Workspace\n2: \n3: This deterministic workspace is bundled with the browser-only preview.\n4: \n5: - `src/preview.ts` is the file changed by the example write result.\n6: - `data/tasks.json` mirrors the completed preview checklist.\n7: - `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing.\n8: \n9: Refresh the preview to restore these image bytes.\n\n(End of file - total 9 lines)\n"}],"isError":false},"meta":{"path":"PREVIEW.md","offset":1,"lines":[{"number":1,"text":"# Preview Workspace"},{"number":2,"text":""},{"number":3,"text":"This deterministic workspace is bundled with the browser-only preview."},{"number":4,"text":""},{"number":5,"text":"- `src/preview.ts` is the file changed by the example write result."},{"number":6,"text":"- `data/tasks.json` mirrors the completed preview checklist."},{"number":7,"text":"- `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing."},{"number":8,"text":""},{"number":9,"text":"Refresh the preview to restore these image bytes."}],"totalLines":9,"lang":"md"}},"surfaceOp":"append","seq":174,"time":1787472000174}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-write","name":"write","arguments":"{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"},"seq":175,"time":1787472000175}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-write-result","role":"tool","toolCallId":"preview-write","source":{"kind":"tool","callId":"preview-write"},"content":[{"type":"text","text":"src/preview.ts\nfile\n\nUpdated file\n"}],"isError":false},"meta":{"diffs":[{"path":"src/preview.ts","oldText":"export const previewStatus = 'draft'\n","newText":"export const previewStatus = 'ready'\n\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\n"}]}},"surfaceOp":"append","seq":176,"time":1787472000176}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-bash","name":"bash","arguments":"{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"},"seq":177,"time":1787472000177}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-bash-result","role":"tool","toolCallId":"preview-bash","source":{"kind":"tool","callId":"preview-bash"},"content":[{"type":"text","text":"preview ready\n"}],"isError":false}},"surfaceOp":"append","seq":178,"time":1787472000178}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-wrapped-bash","name":"bash","arguments":"{\"command\":\"printf 'preview command fixture\\\\n' # wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-wrapped-argument-\\nprintf 'wrap complete\\\\n'\\n\",\"description\":\"Show wrapped command and first-line status\"}"},"seq":179,"time":1787472000179}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-wrapped-bash-result","role":"tool","toolCallId":"preview-wrapped-bash","source":{"kind":"tool","callId":"preview-wrapped-bash"},"content":[{"type":"text","text":"preview command fixture\nwrap complete\n[exit code: 0]"}],"isError":false}},"surfaceOp":"append","seq":180,"time":1787472000180}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"},"seq":181,"time":1787472000181}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-glob-result","role":"tool","toolCallId":"preview-glob","source":{"kind":"tool","callId":"preview-glob"},"content":[{"type":"text","text":"PREVIEW.md\ndata/tasks.json\nsrc/preview.ts"}],"isError":false},"meta":{"shape":"paths","paths":["PREVIEW.md","data/tasks.json","src/preview.ts"],"truncated":false,"total":3}},"surfaceOp":"append","seq":182,"time":1787472000182}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"},"seq":183,"time":1787472000183}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-grep-result","role":"tool","toolCallId":"preview-grep","source":{"kind":"tool","callId":"preview-grep"},"content":[{"type":"text","text":"PREVIEW.md:3:This deterministic workspace is bundled with the browser-only preview.\nsrc/preview.ts:1:export const previewStatus = 'ready'"}],"isError":false},"meta":{"shape":"matches","files":[{"path":"PREVIEW.md","matches":[{"lineNumber":3,"line":"This deterministic workspace is bundled with the browser-only preview."}]},{"path":"src/preview.ts","matches":[{"lineNumber":1,"line":"export const previewStatus = 'ready'"}]}],"truncated":false,"total":2}},"surfaceOp":"append","seq":184,"time":1787472000184}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-web-search","name":"web_search","arguments":"{\"queries\":[\"Web Worker filesystem compatibility\"]}"},"seq":185,"time":1787472000185}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-web-search-result","role":"tool","toolCallId":"preview-web-search","source":{"kind":"tool","callId":"preview-web-search"},"content":[{"type":"text","text":"Browser workers can host deterministic in-memory filesystems.\n\nSources:\n1. MDN Web Workers API — https://developer.mozilla.org/docs/Web/API/Web_Workers_API"}],"isError":false},"meta":{"sources":[{"url":"https://developer.mozilla.org/docs/Web/API/Web_Workers_API","title":"Web Workers API","snippet":"Web Workers run scripts in background threads."}],"truncated":false,"answer":"Browser workers can host deterministic in-memory filesystems."}},"surfaceOp":"append","seq":186,"time":1787472000186}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-todo","name":"todo_write","arguments":"{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"},"seq":187,"time":1787472000187}
+{"type":"todo/write","data":{"todos":[{"content":"Inspect tool cards","status":"completed"},{"content":"Open both subagents","status":"completed"},{"content":"Load earlier history","status":"in_progress"}]},"seq":188,"time":1787472000188}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-todo-result","role":"tool","toolCallId":"preview-todo","source":{"kind":"tool","callId":"preview-todo"},"content":[{"type":"text","text":"Updated todo list: 0 pending, 1 in progress, 2 completed."}],"isError":false}},"surfaceOp":"append","seq":189,"time":1787472000189}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-subagent","name":"subagent","arguments":"{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"},"seq":190,"time":1787472000190}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-subagent-result","role":"tool","toolCallId":"preview-subagent","source":{"kind":"tool","callId":"preview-subagent"},"content":[{"type":"text","text":"started subagent preview-follow-up-builder"}],"isError":false}},"surfaceOp":"append","seq":191,"time":1787472000191}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-subagent-fork","name":"subagent_fork","arguments":"{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"},"seq":192,"time":1787472000192}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-subagent-fork-result","role":"tool","toolCallId":"preview-subagent-fork","source":{"kind":"tool","callId":"preview-subagent-fork"},"content":[{"type":"text","text":"The preview fixture remains separate from user-owned WebFS data."}],"isError":false}},"surfaceOp":"append","seq":193,"time":1787472000193}
+{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-failure","name":"read","arguments":"{\"file_path\":\"missing.txt\"}"},"seq":194,"time":1787472000194}
+{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-failure-result","role":"tool","toolCallId":"preview-failure","source":{"kind":"tool","callId":"preview-failure"},"content":[{"type":"text","text":"Error: ENOENT: no such file, open missing.txt"}],"isError":true},"error":{"name":"FsError","code":"ENOENT"}},"surfaceOp":"append","seq":195,"time":1787472000195}
+{"type":"subagent/catalog","data":{"version":0,"childId":"preview-architecture-review","childCreatedAt":1787472100000,"mode":"one-shot","label":"Review preview architecture"},"seq":196,"time":1787472000196}
+{"type":"subagent/catalog","data":{"version":0,"childId":"preview-follow-up-builder","childCreatedAt":1787472200000,"mode":"continuable","label":"Continue preview verification"},"seq":197,"time":1787472000197}
+{"type":"step/end","data":{"turn":29,"step":1},"seq":198,"time":1787472000198}
+{"type":"step/start","data":{"turn":29,"step":2},"seq":199,"time":1787472000199}
+{"type":"assistant/message","data":{"turn":29,"step":2,"message":{"id":"preview-gallery-final","role":"assistant","content":[{"type":"text","text":"## Preview tour complete\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.\n\nThis is fixture-authored demonstration data, not a live model run. Open [Parent workspace guide:1–8](<../workspace/PREVIEW.md#L1-L8>) to inspect the same workspace file through a parent path."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["## Preview tour complete\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.\n\nThis is fixture-authored demonstration data, not a live model run. Open [Parent workspace guide:1–8](<../workspace/PREVIEW.md#L1-L8>) to inspect the same workspace file through a parent path."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"## Preview tour complete\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.\n\nThis is fixture-authored demonstration data, not a live model run. Open [Parent workspace guide:1–8](<../workspace/PREVIEW.md#L1-L8>) to inspect the same workspace file through a parent path."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":200,"time":1787472000200}
+{"type":"step/end","data":{"turn":29,"step":2},"seq":201,"time":1787472000201}
+{"type":"turn/end","data":{"turn":29,"reason":{"kind":"completed"}},"seq":202,"time":1787472000202}
diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json
index 3ecdae0660..d26f67472b 100644
--- a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json
+++ b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json
@@ -8,7 +8,7 @@
"sessions": {
"preview-showcase": {
"identity": {
- "formatVersion": 3,
+ "formatVersion": 4,
"createdAt": 1787472000000,
"cwd": "/dsh/workspace",
"isSeeded": false,
@@ -17,7 +17,7 @@
"rows": {
"title": {
"ver": 1,
- "seq": 198,
+ "seq": 202,
"val": "WebWorker Preview Showcase"
}
}
diff --git a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts
index 8ae9dcf6fa..5e588839a4 100644
--- a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts
+++ b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts
@@ -14,6 +14,8 @@ import {
buildVfsExampleFiles,
VFS_EXAMPLE_OLDEST_MESSAGE,
VFS_EXAMPLE_ROOT,
+ VFS_EXAMPLE_REASONING,
+ VFS_EXAMPLE_WRAPPED_COMMAND,
VFS_EXAMPLE_SCHEDULE_IDS,
VFS_EXAMPLE_SESSION_IDS,
VFS_EXAMPLE_TAIL_MESSAGE,
@@ -59,27 +61,29 @@ function textOf(event: SessionEvent): string {
}
describe('WebWorker preview VFS example', () => {
- it('retains V3 inputs that strictly restore to the deterministic current output', () => {
+ it('keeps the current showcase and strictly restorable historical child inputs beside their predecessors', () => {
const expected = buildVfsExampleFiles()
const currentName = generationLogFilename(SESSION_FORMAT_VERSION, 'none')
- const committedPath = (path: string): string => path.endsWith(`/${currentName}`)
+ const main = `home/sessions/--dsh-workspace--/${VFS_EXAMPLE_SESSION_IDS.main}/`
+ const committedPath = (path: string): string => path.endsWith(`/${currentName}`) && !path.startsWith(main)
? `${path.slice(0, -currentName.length)}session.v3.jsonl`
: path
const predecessors = Object.values(VFS_EXAMPLE_SESSION_IDS)
.map(id => `home/sessions/--dsh-workspace--/${id}/session.v2.jsonl`)
+ predecessors.push(`${main}session.v3.jsonl`)
expect(filesUnder(VFS_EXAMPLE_ROOT)).toEqual([...expected.keys()].map(committedPath).concat(predecessors).sort())
for (const [path, content] of expected) {
- if (path === 'home/storages/session_projcache.json') continue
const committed = readFileSync(join(VFS_EXAMPLE_ROOT, committedPath(path)), 'utf8')
- if (path.endsWith(`/${currentName}`)) {
+ if (path.endsWith(`/${currentName}`) && !path.startsWith(main)) {
expect(restoreSession(committed, 3), path).toEqual(restoreSession(content, SESSION_FORMAT_VERSION))
} else {
expect(committed, path).toBe(content)
+ if (path.startsWith(main)) restoreSession(committed, SESSION_FORMAT_VERSION)
}
}
})
- it('retains V3 cache identity while the generated cache uses the current writer', () => {
+ it('keeps the projection cache aligned with the current showcase writer and tail', () => {
const committed = JSON.parse(readFileSync(
join(VFS_EXAMPLE_ROOT, 'home/storages/session_projcache.json'),
'utf8',
@@ -101,12 +105,10 @@ describe('WebWorker preview VFS example', () => {
const generatedText = buildVfsExampleFiles().get('home/storages/session_projcache.json')
if (generatedText === undefined) throw new Error('missing generated VFS example projection cache')
const generated = JSON.parse(generatedText) as typeof committed
- const expectedCurrent = structuredClone(committed)
- for (const session of Object.values(expectedCurrent.tables.sessions)) {
- expect(session.identity.formatVersion).toBe(3)
- session.identity.formatVersion = SESSION_FORMAT_VERSION
- }
- expect(generated).toEqual(expectedCurrent)
+ expect(generated).toEqual(committed)
+ expect(generated.tables.sessions[VFS_EXAMPLE_SESSION_IDS.main]?.rows.title).toMatchObject({
+ seq: readSession(VFS_EXAMPLE_SESSION_IDS.main).events.at(-1)?.seq,
+ })
expect(generated.unit).toEqual({
name: projectionCacheDomainSpec.name,
version: projectionCacheDomainSpec.version,
@@ -180,6 +182,46 @@ describe('WebWorker preview VFS example', () => {
expect(events.some(event => event.type === 'tool/result' && event.data.message.isError === true)).toBe(true)
})
+ it('preserves the 28-turn history prefix and both child catalog identities', () => {
+ const main = readSession(VFS_EXAMPLE_SESSION_IDS.main)
+ const child = readSession(VFS_EXAMPLE_SESSION_IDS.oneShot)
+ const historical = restoreSession(readFileSync(join(VFS_EXAMPLE_ROOT,
+ `home/sessions/--dsh-workspace--/${VFS_EXAMPLE_SESSION_IDS.main}/session.v3.jsonl`), 'utf8'), 3)
+ const prefix = main.events.slice(0, child.inheritedEventCount)
+ expect(prefix.filter(event => event.type === 'turn/start')).toHaveLength(28)
+ expect(prefix).toEqual(historical.events.slice(0, child.inheritedEventCount))
+ expect(child.events.slice(0, child.inheritedEventCount)).toEqual(prefix)
+ expect(main.events.filter(event => event.type === 'subagent/catalog').map(event => event.data)).toEqual([
+ { version: 0, childId: VFS_EXAMPLE_SESSION_IDS.oneShot, childCreatedAt: 1_787_472_100_000,
+ mode: 'one-shot', label: 'Review preview architecture' },
+ { version: 0, childId: VFS_EXAMPLE_SESSION_IDS.continuable, childCreatedAt: 1_787_472_200_000,
+ mode: 'continuable', label: 'Continue preview verification' },
+ ])
+ })
+
+ it('carries mixed reasoning, a wrapped command, and an authored parent-path link in the existing tour', () => {
+ const { events } = readSession(VFS_EXAMPLE_SESSION_IDS.main)
+ const gallery = events.find(event => event.type === 'assistant/message' && event.data.message.id === 'preview-gallery-tools')
+ if (gallery?.type !== 'assistant/message') throw new Error('missing preview gallery message')
+ const reasoning = gallery.data.message.content.flatMap(block => block.type === 'reasoning' ? [block.text] : [])
+ expect(reasoning).toEqual(['', ' \t\n', VFS_EXAMPLE_REASONING[0], '', VFS_EXAMPLE_REASONING[1], '\n '])
+ expect(reasoning.filter(text => text.trim() !== '')).toEqual(VFS_EXAMPLE_REASONING)
+ const call = events.find(event => event.type === 'tool/call' && event.data.callId === 'preview-wrapped-bash')
+ if (call?.type !== 'tool/call') throw new Error('missing wrapped command call')
+ expect(JSON.parse(call.data.arguments)).toEqual({
+ command: VFS_EXAMPLE_WRAPPED_COMMAND, description: 'Show wrapped command and first-line status',
+ })
+ expect(VFS_EXAMPLE_WRAPPED_COMMAND.split('\n')[0]?.length).toBeGreaterThan(400)
+ expect(VFS_EXAMPLE_WRAPPED_COMMAND.endsWith('\n')).toBe(true)
+ expect(events.find(event => event.type === 'tool/result' && event.data.message.source.callId === 'preview-wrapped-bash'))
+ .toMatchObject({ data: { message: { isError: false, content: [{ type: 'text', text: 'preview command fixture\nwrap complete\n[exit code: 0]' }] } } })
+ const tail = events.findLast(event => event.type === 'assistant/message')
+ if (tail === undefined) throw new Error('missing preview tour tail')
+ expect(textOf(tail)).toContain(`## ${VFS_EXAMPLE_TAIL_MESSAGE}`)
+ expect(textOf(tail)).toContain('[Parent workspace guide:1–8](<../workspace/PREVIEW.md#L1-L8>)')
+ expect(textOf(tail)).toContain('fixture-authored demonstration data, not a live model run')
+ })
+
it('restores one-shot and continuable child Sessions with durable descriptors', () => {
const expected = [
[VFS_EXAMPLE_SESSION_IDS.oneShot, 'one-shot'],
diff --git a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts
index fd00a86139..2f015bf246 100644
--- a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts
+++ b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts
@@ -17,6 +17,7 @@ import {
import { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
import { scheduleDomain } from '@deepseek-ai/dsh-schedule'
import { snapshotSubagentDescriptor, SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent'
+import { SUBAGENT_CATALOG_VERSION } from '@deepseek-ai/dsh-subagent/src/catalog.ts'
/** Root copied by the preview image's repository adapter. */
export const VFS_EXAMPLE_ROOT = fileURLToPath(new URL('./fixtures/vfs-example', import.meta.url))
@@ -37,6 +38,15 @@ export const VFS_EXAMPLE_OLDEST_MESSAGE = 'History checkpoint 01: verify determi
/** Settled tail marker used by browser acceptance and the demonstration GIF. */
export const VFS_EXAMPLE_TAIL_MESSAGE = 'Preview tour complete'
+/** Non-empty reasoning bodies surrounded by empty and whitespace-only fixture blocks. */
+export const VFS_EXAMPLE_REASONING = [
+ 'I will inspect the deterministic workspace and collect each preview surface.',
+ 'Check the Chat presentation\n\nI will inspect the wrapped command, its first-line status, and the parent-path file link.',
+] as const
+
+/** Authored long command and a second line for wrapping and status-dot acceptance. */
+export const VFS_EXAMPLE_WRAPPED_COMMAND = `printf 'preview command fixture\\n' # ${'wrapped-argument-'.repeat(28)}\nprintf 'wrap complete\\n'\n`
+
/** Durable task ids for the Host Schedule rows seeded into the preview's `DSH_HOME`. */
export const VFS_EXAMPLE_SCHEDULE_IDS = {
daily: 'preview-daily-digest',
@@ -211,6 +221,12 @@ function galleryCalls(): GalleryCall[] {
args: { command: "printf 'preview ready\\n'", description: 'Print the preview readiness marker' },
result: 'preview ready\n',
},
+ {
+ id: 'preview-wrapped-bash',
+ name: 'bash',
+ args: { command: VFS_EXAMPLE_WRAPPED_COMMAND, description: 'Show wrapped command and first-line status' },
+ result: 'preview command fixture\nwrap complete\n[exit code: 0]',
+ },
{
id: 'preview-glob',
name: 'glob',
@@ -327,7 +343,8 @@ function mainLog(): {
log.add(userMessage('preview-gallery-user', 'Show the seeded workspace, tool cards, subagents, and pagination in one tour.'))
log.add({ type: 'step/start', data: { turn, step: 1 } })
log.add(assistantMessage('preview-gallery-tools', turn, 1, [
- { type: 'reasoning', text: 'I will inspect the deterministic workspace and collect each preview surface.' },
+ ...['', ' \t\n', VFS_EXAMPLE_REASONING[0], '', VFS_EXAMPLE_REASONING[1], '\n ']
+ .map(text => ({ type: 'reasoning', text })),
...calls.map(call => ({ type: 'tool-call', id: call.id, name: call.name, arguments: JSON.stringify(call.args) })),
]))
for (const call of calls) {
@@ -355,11 +372,17 @@ function mainLog(): {
surfaceOp: 'append',
})
}
+ for (const child of [
+ { childId: VFS_EXAMPLE_SESSION_IDS.oneShot, childCreatedAt: CREATED_AT + 100_000, mode: 'one-shot', label: 'Review preview architecture' },
+ { childId: VFS_EXAMPLE_SESSION_IDS.continuable, childCreatedAt: CREATED_AT + 200_000, mode: 'continuable', label: 'Continue preview verification' },
+ ] as const) {
+ log.add({ type: 'subagent/catalog', data: { version: SUBAGENT_CATALOG_VERSION, ...child } })
+ }
log.add({ type: 'step/end', data: { turn, step: 1 } })
log.add({ type: 'step/start', data: { turn, step: 2 } })
log.add(assistantMessage('preview-gallery-final', turn, 2, [{
type: 'text',
- text: `## ${VFS_EXAMPLE_TAIL_MESSAGE}\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.`,
+ text: `## ${VFS_EXAMPLE_TAIL_MESSAGE}\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.\n\nThis is fixture-authored demonstration data, not a live model run. Open [Parent workspace guide:1–8](<../workspace/PREVIEW.md#L1-L8>) to inspect the same workspace file through a parent path.`,
}]))
log.add({ type: 'step/end', data: { turn, step: 2 } })
log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })