diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 80aa4603f6..a2dbd13a94 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -43,6 +43,7 @@ import { import { VFS_EXAMPLE_SESSION_IDS, VFS_EXAMPLE_REASONING, + VFS_EXAMPLE_SPACING_TITLE, VFS_EXAMPLE_WRAPPED_COMMAND, buildVfsExampleFiles, } from '../../../packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts' @@ -470,6 +471,9 @@ async function bootPreview(site: Site, manifest: ImageManifest, browser: Browser const sessions = page.getByRole('tree', { name: 'Sessions' }) const showcase = sessions.getByRole('treeitem').filter({ hasText: SHOWCASE_TITLE }) await expect.poll(() => showcase.count(), { timeout: 15_000 }).toBe(1) + // The current-generation spacing sample ships beside the showcase and must be listed cold. + const spacingSample = sessions.getByRole('treeitem').filter({ hasText: VFS_EXAMPLE_SPACING_TITLE }) + await expect.poll(() => spacingSample.count(), { timeout: 15_000 }).toBe(1) // The packed deployment ships the Schedule rows, and the preview storage // seeds the showcase Session with active tasks. expect(await showcase.locator('[data-session-schedule-mark]').count()).toBe(1) diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 7cc337dbb2..2ff8f221ce 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -33,8 +33,8 @@ en: b0d806604bc17006 zh: 18dabe5dc2d0d419 /deepseek-ai-dsh-client-ui-chat/grouped-rendering: - en: a14c74ed7de0ef9c - zh: d5e618b2ef809150 + en: 3afad0b4c6bf9ce0 + zh: d8179ba4ee9fcd61 /deepseek-ai-dsh-client-ui-chat/scroll-ownership: en: 009ecb4fecd4ee9e zh: ec2714337a1102c5 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 85b3e65c8d..ceeae1d086 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -113,7 +113,7 @@ Live tool deltas share reasoning's frame-batched publication; durable calls and The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. The `conversation.chat.flow` slot adds a `display: contents` DOM anchor; row selectors match that anchor directly, without repeating its column ancestry. The running status shares the rows' parent, so its divider uses the same visible-sibling rules. The anchor contributes no spacing of its own. The earlier-history control directly precedes the flow anchor; their sibling relationship supplies the leading gap. CSS variables do not belong in the Group Definition. -Running group titles, reasoning previews, tool and command rows, the active retry line, and the bottom running status use the shared [TextShimmer](../ui-primitives/README.md#component-catalog). Each row’s text shares one left-to-right highlight, including collapsed group titles; icons and hover chevrons retain their base appearance. Tool and process rows sit 6px apart. An expanded group title has 8px before its content, Assistant responses have 12px from adjacent process rows, and completed-Turn duration/status controls have 16px from their input and from a directly following visible row. A reply that follows the control after folded rows keeps the 12px reply gap, so folding never changes its spacing. Hidden and empty rows, including retained empty slot anchors, add no gap. The running “Deep diving” text retains its blue base while its theme-specific highlight sweeps across it. +Running group titles, reasoning previews, tool and command rows, the active retry line, and the bottom running status use the shared [TextShimmer](../ui-primitives/README.md#component-catalog). Each row’s text shares one left-to-right highlight, including collapsed group titles; icons and hover chevrons retain their base appearance. Ordinary flow rows sit 4px apart. An expanded group title has 8px before its content, Assistant responses have 12px from adjacent process rows, and completed-Turn duration/status controls have 16px from their input and from a directly following visible row. A reply that follows the control after folded rows keeps the 12px reply gap, so folding never changes its spacing. Hidden and empty rows, including retained empty slot anchors, add no gap. The running “Deep diving” text retains its blue base while its theme-specific highlight sweeps across it. The running status shows a whale whose APNG mask inherits the text color. CSS selects the static SVG when reduced motion or forced colors are active, or alpha masks are unsupported. The APNG is a [package asset](../AGENTS.md#dependency-declaration) referenced by the plugin stylesheet; React does not pass image data through DOM styles. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 486da291e6..9113ed2f81 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -113,7 +113,7 @@ Chat Builder 按节点发布 `bottomSource`:节点可见且属于已加载的 过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。`conversation.chat.flow` 插槽添加一个 `display: contents` DOM 锚点;行选择器直接匹配该锚点,不重复匹配其正文容器祖先。运行状态与各行共享父级,使分隔线使用同一套可见兄弟节点规则。锚点自身不产生间距。更早历史控件紧邻流锚点之前,两者的兄弟关系决定首行间距。CSS 变量不属于 Group Definition。 -运行中的分组标题、思考预览、工具与命令行、当前重试行以及底部运行状态使用共享的 [TextShimmer](../ui-primitives/README.zh.md#component-catalog)。每行的文字共用一道从左到右的高光,包括折叠的分组标题;图标和悬停箭头保留底色。工具和过程行之间相隔 6px;展开的分组标题与内容之间相隔 8px;Assistant 回复与相邻过程行之间相隔 12px;每轮完成/耗时控件与其输入、以及紧随其后的可见行之间保留 16px。控件与回复之间隔着已收起的行时,回复保持 12px 回复间距,因此折叠不会改变它的间距。隐藏行与空行(包括保留的空 slot 锚点)不产生间距。“深度求索”运行文字保留蓝色底色,并使用主题专用高光扫过文字。 +运行中的分组标题、思考预览、工具与命令行、当前重试行以及底部运行状态使用共享的 [TextShimmer](../ui-primitives/README.zh.md#component-catalog)。每行的文字共用一道从左到右的高光,包括折叠的分组标题;图标和悬停箭头保留底色。普通流程行之间相隔 4px;展开的分组标题与内容之间相隔 8px;Assistant 回复与相邻过程行之间相隔 12px;每轮完成/耗时控件与其输入、以及紧随其后的可见行之间保留 16px。控件与回复之间隔着已收起的行时,回复保持 12px 回复间距,因此折叠不会改变它的间距。隐藏行与空行(包括保留的空 slot 锚点)不产生间距。“深度求索”运行文字保留蓝色底色,并使用主题专用高光扫过文字。 运行状态显示一只鲸鱼,其 APNG 遮罩继承文字颜色。启用减少动态效果或强制颜色模式、或浏览器不支持 alpha 遮罩时,CSS 选择静态 SVG。APNG 是插件样式表引用的[包资源](../AGENTS.md#dependency-declaration);React 不通过 DOM 样式传递图片数据。 diff --git a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css index 979cef25bb..ba58b53eff 100644 --- a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css @@ -100,7 +100,7 @@ } .body { - --dsh-chat-flow-gap: 6px; + --dsh-chat-flow-gap: 4px; max-height: min(400px, 50vh); overflow-y: auto; overscroll-behavior-y: auto; @@ -146,7 +146,7 @@ } .content > * { flex-shrink: 0; } .content > :not([hidden]):not(:empty) ~ :not([hidden]):not(:empty) { - margin-top: var(--dsh-chat-flow-gap, 6px); + margin-top: var(--dsh-chat-flow-gap, 4px); } .expandedBody { max-height: none; diff --git a/packages/client/ui-chat/src/client/chat/ChatView.module.css b/packages/client/ui-chat/src/client/chat/ChatView.module.css index 341a33f612..f05bad098f 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatView.module.css @@ -78,10 +78,10 @@ ~ :not([hidden]):not(.textInteraction):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))), .column > .older + :global([data-slot="conversation.chat.flow"]) > :not([hidden]):not(.textInteraction):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))) { - margin-top: var(--dsh-chat-flow-gap, 6px); + margin-top: var(--dsh-chat-flow-gap, 4px); } -/* Process rows sit 6px apart; an Assistant response is separated from its +/* Process rows sit 4px apart; an Assistant response is separated from its neighbours by 12px. */ .flowItem[data-chat-group-part="response"] { --dsh-chat-flow-gap: 12px; @@ -136,7 +136,7 @@ steering row of each claim that Chat orders after the Turn's process control; that row keeps the slot above it, the way a completed Turn's actions row precedes the next user message, while rows claimed together - stay 6px apart and opening inputs keep ordinary spacing. The first waiting + stay 4px apart and opening inputs keep ordinary spacing. The first waiting echo sits directly below the running status, so admission swaps the two without moving any bubble. Neighbour gap rules exclude the batch start so this slot always applies. */ @@ -178,7 +178,7 @@ /* While the running Step has no output, the status sits directly below the input. When the first output row appears, the status enters its slot below that row from where it stood: it travels the row's gap plus one 24px row, - which follows the content font size. The gap is 6px for a process row or + which follows the content font size. The gap is 4px for a process row or group header (the default declared on `.running`), 12px for a response, and 16px after the running Turn's process control, which renders empty until the Turn closes, so it precedes a new Turn's first output. The default sits on @@ -260,7 +260,7 @@ flex-direction: column; align-items: flex-start; --dsh-chat-flow-gap: 12px; - --dsh-running-follow: calc(6px + 24px + var(--dsh-content-font-delta, 0px)); + --dsh-running-follow: calc(4px + 24px + var(--dsh-content-font-delta, 0px)); --dsw-alias-label-shimmer: var(--dsw-alias-label-deep-diving-shimmer); color: var(--dsw-alias-label-deep-diving); font-size: calc(var(--dsh-content-font-size, 14px) - 2px); diff --git a/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml b/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml index b937357a91..1dee9a0b92 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml +++ b/packages/client/ui-chat/src/client/conversation-nodes/README.i18n.yaml @@ -39,8 +39,8 @@ en: 8e775c43878ae31a zh: 4c1063c0071ebdbd /chat-conversation-node-rules/display-modes/group-title-rules: - en: 8c5b57a66fe0247c - zh: 5a9e37f255761ac6 + en: f78fb52d4733e6ce + zh: 119e5319b23762ba /chat-conversation-node-rules/process-grouping: en: 3d28b3bb8757bf55 zh: 3b0f451b48db6048 diff --git a/packages/client/ui-chat/src/client/conversation-nodes/README.md b/packages/client/ui-chat/src/client/conversation-nodes/README.md index 50f598d35c..c0ea49ba5e 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/README.md +++ b/packages/client/ui-chat/src/client/conversation-nodes/README.md @@ -187,7 +187,7 @@ The labels below describe recorded activity, not successful outcomes. For exampl English lowercases the initial letter of joined labels after the first. Closing a group immediately selects the completed summary; the 150ms minimum applies to running-title changes, not to delaying completion. Detailed hides group headers in running Turns, including groups ended by a reply or steering before their Turn ends. Verbose also hides historical group headers. -Group headers show a category icon, replace it with a down arrow on hover or keyboard focus, and show an up arrow while open. Manually expanded group bodies use 6px row spacing, a `min(400px, 50vh)` height cap, and 24px directional fades. Wheel scrolling can continue into the outer transcript at an edge. Detailed removes the group-level cap in running Turns; Verbose applies this layout to historical Turns as well. Process rows retain 6px spacing in both modes; Assistant responses have 12px clearance. Group headers have the same row height as process rows. Expanded group titles sit 8px above their content; Turn process controls retain 16px clearance from their input and from a directly following visible row; a reply separated from the control by folded rows keeps the 12px reply gap, so folding never changes its spacing. +Group headers show a category icon, replace it with a down arrow on hover or keyboard focus, and show an up arrow while open. Manually expanded group bodies use 4px row spacing, a `min(400px, 50vh)` height cap, and 24px directional fades. Wheel scrolling can continue into the outer transcript at an edge. Detailed removes the group-level cap in running Turns; Verbose applies this layout to historical Turns as well. Process rows retain 4px spacing in both modes; Assistant responses have 12px clearance. Group headers have the same row height as process rows. Expanded group titles sit 8px above their content; Turn process controls retain 16px clearance from their input and from a directly following visible row; a reply separated from the control by folded rows keeps the 12px reply gap, so folding never changes its spacing. An open capped group follows content growth only while its own scroll position is at the bottom. Scrolling away pauses that group's following; returning to the bottom resumes it, independently of outer transcript following. Manually opening an unclosed group starts at the bottom and follows growth; manually opening a closed group starts at the top with following disabled, even when its initial content fits without scrolling. Closing the group in the data or restoring its height cap through a mode change does not reset an already-open reader's position. Browser find retains its own reveal position. diff --git a/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md b/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md index b7d89f7ec6..c4e94caec3 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md +++ b/packages/client/ui-chat/src/client/conversation-nodes/README.zh.md @@ -187,7 +187,7 @@ description: "Chat 过程分组与活动摘要的明细业务规则。" 英文组合标题将第一项之后各项的首字母转为小写。组结束时立即切换完成摘要;150ms 最短保留时间针对运行中标题变化,不延迟完成状态。详细模式隐藏运行中轮次的组头,包括被回复或 steering 截断、但所属轮次尚未结束的小组;完全展开模式也隐藏历史组头。 -组头显示类别图标,悬停或键盘聚焦时替换为向下箭头,展开时显示向上箭头。手动展开的组正文使用 6px 行间距、`min(400px, 50vh)` 限高及 24px 方向渐隐;到达边缘时滚轮可以继续滚动外层文本记录。详细模式对运行中轮次取消组级限高;完全展开模式对历史轮次也采用该布局。两种模式下过程行均保留 6px 间距,Assistant 回复周围保留 12px 间距。组头与过程行同高。展开组头与内容相隔 8px,整轮过程控件与其输入、以及紧随其后的可见行之间保留 16px 间距;与控件之间隔着已收起行的回复保持 12px 回复间距,因此折叠不会改变它的间距。 +组头显示类别图标,悬停或键盘聚焦时替换为向下箭头,展开时显示向上箭头。手动展开的组正文使用 4px 行间距、`min(400px, 50vh)` 限高及 24px 方向渐隐;到达边缘时滚轮可以继续滚动外层文本记录。详细模式对运行中轮次取消组级限高;完全展开模式对历史轮次也采用该布局。两种模式下过程行均保留 4px 间距,Assistant 回复周围保留 12px 间距。组头与过程行同高。展开组头与内容相隔 8px,整轮过程控件与其输入、以及紧随其后的可见行之间保留 16px 间距;与控件之间隔着已收起行的回复保持 12px 回复间距,因此折叠不会改变它的间距。 已展开的限高组仅在自己的滚动位置处于底部时跟随内容增长。滚离底部暂停该组跟随,滚回底部后恢复,与外层文本记录的跟随状态独立。手动展开未关闭的组时定位到底部并跟随增长;手动展开已关闭的组时从顶部开始且不启用跟随,即使初始内容无需滚动即可容纳。数据中的组变为已关闭,或通过模式切换恢复限高时,不重置已展开组的阅读位置。浏览器查找沿用自身的展开定位。 diff --git a/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts b/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts index ae86bbed89..8bc6bcd08f 100644 --- a/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts +++ b/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts @@ -8,12 +8,18 @@ const read = (name: string): string => describe('Turn tail slot spacing', () => { it('combines the flow and footer offsets around turn-tail content', () => { - expect(read('ChatView.module.css')).toMatch(/margin-top:\s*var\(--dsh-chat-flow-gap, 6px\)/) + expect(read('ChatView.module.css')).toMatch(/margin-top:\s*var\(--dsh-chat-flow-gap, 4px\)/) const tail = read('TurnTailNodeView.module.css') expect(tail).toMatch(/\.root\s*\{[^}]*gap:\s*16px/s) expect(tail).toMatch(/\.actions\s*\{[^}]*margin-top:\s*4px/s) }) + it('keeps process rows inside and outside a group on the same gap', () => { + const group = read('ChatGroupSeat.module.css') + expect(group).toMatch(/\.body \{[^}]*--dsh-chat-flow-gap: 4px;/s) + expect(group).toMatch(/margin-top:\s*var\(--dsh-chat-flow-gap, 4px\)/) + }) + it('keeps the tail slot above a marked steering batch start and out of neighbour gaps', () => { const view = read('ChatView.module.css') const marked = String.raw`:has\(> \[data-slot="conversation\.chat\.node"\] > \[data-chat-steering-batch` @@ -44,7 +50,7 @@ describe('Running status move below the first output', () => { const view = read('ChatView.module.css') // A process row or group header after a mid-Turn input: the default sits on the single-class // status rule, the lowest-specificity selector, so each variant below outranks it. - expect(view).toMatch(new RegExp(String.raw`\n\.running \{[^}]*${travel(6)}`)) + expect(view).toMatch(new RegExp(String.raw`\n\.running \{[^}]*${travel(4)}`)) expect(view.match(/--dsh-running-follow: calc\(/g)).toHaveLength(3) // A response keeps its 12px clearance. expect(view).toMatch(new RegExp(String.raw`\)\)\)\)\[data-chat-group-part="response"\] ~ \.running \{\s*${travel(12)}`)) diff --git a/packages/experimental/webworker-packer/tests/preview.spec.ts b/packages/experimental/webworker-packer/tests/preview.spec.ts index 0e0c7f7e12..d816800e05 100644 --- a/packages/experimental/webworker-packer/tests/preview.spec.ts +++ b/packages/experimental/webworker-packer/tests/preview.spec.ts @@ -47,7 +47,7 @@ describe('Node preparation of Preview Session data', () => { const original = packVfsOverlay(fixture.trees) const projected = packPreviewFixture(fixture.trees) const currentFiles = Object.keys(projected.files).filter(path => path.endsWith('/' + sessionFormatLogFilename(currentVersion))) - expect(currentFiles).toHaveLength(3) + expect(currentFiles).toHaveLength(4) for (const [path, bytes] of Object.entries(original.files)) expect(projected.files[path], path).toEqual(bytes) for (const path of currentFiles) { const current = restore(projected.files[path]) diff --git a/packages/experimental/webworker-runtime/README.i18n.yaml b/packages/experimental/webworker-runtime/README.i18n.yaml index 8e756f5198..6dca30e4d1 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: e55bf1e080a6e075 - zh: 1d3829a5b7fd2d13 + en: 24f1bb4adc4a9bb6 + zh: 4a2caa4bfe2d8911 /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 71e2cd7937..7e495b41a1 100644 --- a/packages/experimental/webworker-runtime/README.md +++ b/packages/experimental/webworker-runtime/README.md @@ -35,7 +35,7 @@ Three build artifacts and one source-owned process layer: Callbacks and timers registered under an explicit root context retain its empty stores, so they cannot inherit an unrelated transaction. -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. +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. A second root Session, `preview-turn-spacing`, carries three multi-step Turns whose bodies compare the tightened process-row spacing with the reply clearance. 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 5340c2561e..7fb89902ce 100644 --- a/packages/experimental/webworker-runtime/README.zh.md +++ b/packages/experimental/webworker-runtime/README.zh.md @@ -35,7 +35,7 @@ kind: "package-library" 在显式根上下文中登记的回调和定时器保留其空 store,避免继承无关事务。 -验收在 `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。 +验收在 `apps/web/tests/preview-boot.e2e.ts`:静态服务真实构建页面,在 headless Chromium 里驱动 pre-boot 选择面板与 Worker 激活。空白选择验证首次启动;`vfs-example` overlay 提供普通 workspace 文件与明文 persistence 产物,无需模型请求即可验证 Workspace/Session 冷发现、工具呈现、subagent 导航和历史分页。showcase 混排空字符串、纯空白与非空推理块,并包含多行长命令和父路径文件链接;这些是人工构造的 fixture,不是真实模型输出。另一个根 Session `preview-turn-spacing` 含三个多步骤轮次,其正文用于对照收紧后的过程行间距与回复间距。[打包器](../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-turn-spacing/session.v4.jsonl b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-turn-spacing/session.v4.jsonl new file mode 100644 index 0000000000..21e04e8e34 --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-turn-spacing/session.v4.jsonl @@ -0,0 +1,44 @@ +{"type":"session","version":4,"id":"preview-turn-spacing","createdAt":1787472300000,"cwd":"/dsh/workspace","isSeeded":false,"delegationDepth":0,"agentPreset":"standard"} +{"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472300000} +{"type":"user/message","data":{"id":"preview-spacing-user-1","role":"user","content":[{"type":"text","text":"Show how multiple Turns and their process steps sit on the tightened spacing."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472300001} +{"type":"session/title","data":{"title":"Turn spacing sample","messageSeqs":[],"source":{"kind":"user"}},"seq":2,"time":1787472300002} +{"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472300003} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-spacing-1-tools","role":"assistant","content":[{"type":"reasoning","text":"Read the workspace guide, then list the files this sample references."},{"type":"tool-call","id":"preview-spacing-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},{"type":"tool-call","id":"preview-spacing-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"}],"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":["Read the workspace guide, then list the files this sample references."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Read the workspace guide, then list the files this sample references."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"preview-spacing-read","name":"read","args":["{\"file_path\":\"PREVIEW.md\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"preview-spacing-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":2,"dt":[],"id":"preview-spacing-glob","name":"glob","args":["{\"pattern\":\"**/*\",\"path\":\".\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"preview-spacing-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append","seq":4,"time":1787472300004} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"preview-spacing-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},"seq":5,"time":1787472300005} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"preview-spacing-read-result","role":"tool","toolCallId":"preview-spacing-read","source":{"kind":"tool","callId":"preview-spacing-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}},"surfaceOp":"append","seq":6,"time":1787472300006} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"preview-spacing-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"},"seq":7,"time":1787472300007} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"preview-spacing-glob-result","role":"tool","toolCallId":"preview-spacing-glob","source":{"kind":"tool","callId":"preview-spacing-glob"},"content":[{"type":"text","text":"PREVIEW.md\ndata/tasks.json\nsrc/preview.ts"}],"isError":false}},"surfaceOp":"append","seq":8,"time":1787472300008} +{"type":"step/end","data":{"turn":1,"step":1},"seq":9,"time":1787472300009} +{"type":"step/start","data":{"turn":1,"step":2},"seq":10,"time":1787472300010} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"id":"preview-spacing-1-command","role":"assistant","content":[{"type":"reasoning","text":"Confirm the sample command result before writing the body."},{"type":"tool-call","id":"preview-spacing-bash","name":"bash","arguments":"{\"command\":\"printf 'spacing sample 1\\\\n'\",\"description\":\"Print the first spacing sample marker\"}"}],"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":["Confirm the sample command result before writing the body."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Confirm the sample command result before writing the body."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"preview-spacing-bash","name":"bash","args":["{\"command\":\"printf 'spacing sample 1\\\\n'\",\"description\":\"Print the first spacing sample marker\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"preview-spacing-bash","name":"bash","arguments":"{\"command\":\"printf 'spacing sample 1\\\\n'\",\"description\":\"Print the first spacing sample marker\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append","seq":11,"time":1787472300011} +{"type":"tool/call","data":{"turn":1,"step":2,"callId":"preview-spacing-bash","name":"bash","arguments":"{\"command\":\"printf 'spacing sample 1\\\\n'\",\"description\":\"Print the first spacing sample marker\"}"},"seq":12,"time":1787472300012} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"id":"preview-spacing-bash-result","role":"tool","toolCallId":"preview-spacing-bash","source":{"kind":"tool","callId":"preview-spacing-bash"},"content":[{"type":"text","text":"spacing sample 1\n"}],"isError":false}},"surfaceOp":"append","seq":13,"time":1787472300013} +{"type":"step/end","data":{"turn":1,"step":2},"seq":14,"time":1787472300014} +{"type":"step/start","data":{"turn":1,"step":3},"seq":15,"time":1787472300015} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"id":"preview-spacing-1-body","role":"assistant","content":[{"type":"text","text":"## Turn 1 — setup\n\nThis Turn has three steps: two process steps and one closing body. Inside an expanded group the process rows use the tightened gap, while this body keeps the reply clearance from the rows above.\n\nExpand the Turn control above to compare the tightened process rhythm with the reply rhythm."}],"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":["## Turn 1 — setup\n\nThis Turn has three steps: two process steps and one closing body. Inside an expanded group the process rows use the tightened gap, while this body keeps the reply clearance from the rows above.\n\nExpand the Turn control above to compare the tightened process rhythm with the reply rhythm."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"## Turn 1 — setup\n\nThis Turn has three steps: two process steps and one closing body. Inside an expanded group the process rows use the tightened gap, while this body keeps the reply clearance from the rows above.\n\nExpand the Turn control above to compare the tightened process rhythm with the reply rhythm."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":16,"time":1787472300016} +{"type":"step/end","data":{"turn":1,"step":3},"seq":17,"time":1787472300017} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}},"seq":18,"time":1787472300018} +{"type":"turn/start","data":{"turn":2},"seq":19,"time":1787472300019} +{"type":"user/message","data":{"id":"preview-spacing-user-2","role":"user","content":[{"type":"text","text":"Continue the sample with a second body and a shorter process."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":20,"time":1787472300020} +{"type":"step/start","data":{"turn":2,"step":1},"seq":21,"time":1787472300021} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"id":"preview-spacing-2-tools","role":"assistant","content":[{"type":"reasoning","text":"Search the sample workspace for the spacing note."},{"type":"tool-call","id":"preview-spacing-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"}],"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":["Search the sample workspace for the spacing note."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Search the sample workspace for the spacing note."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"preview-spacing-grep","name":"grep","args":["{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"preview-spacing-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append","seq":22,"time":1787472300022} +{"type":"tool/call","data":{"turn":2,"step":1,"callId":"preview-spacing-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"},"seq":23,"time":1787472300023} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"id":"preview-spacing-grep-result","role":"tool","toolCallId":"preview-spacing-grep","source":{"kind":"tool","callId":"preview-spacing-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}},"surfaceOp":"append","seq":24,"time":1787472300024} +{"type":"step/end","data":{"turn":2,"step":1},"seq":25,"time":1787472300025} +{"type":"step/start","data":{"turn":2,"step":2},"seq":26,"time":1787472300026} +{"type":"assistant/message","data":{"turn":2,"step":2,"message":{"id":"preview-spacing-2-body","role":"assistant","content":[{"type":"text","text":"## Turn 2 — follow-up\n\nA shorter Turn still separates its process group from this body by the reply clearance, so consecutive Turns stay readable.\n\nThe gap between the previous Turn and this user message is the ordinary flow gap."}],"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":["## Turn 2 — follow-up\n\nA shorter Turn still separates its process group from this body by the reply clearance, so consecutive Turns stay readable.\n\nThe gap between the previous Turn and this user message is the ordinary flow gap."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"## Turn 2 — follow-up\n\nA shorter Turn still separates its process group from this body by the reply clearance, so consecutive Turns stay readable.\n\nThe gap between the previous Turn and this user message is the ordinary flow gap."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":27,"time":1787472300027} +{"type":"step/end","data":{"turn":2,"step":2},"seq":28,"time":1787472300028} +{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}},"seq":29,"time":1787472300029} +{"type":"turn/start","data":{"turn":3},"seq":30,"time":1787472300030} +{"type":"user/message","data":{"id":"preview-spacing-user-3","role":"user","content":[{"type":"text","text":"Finish the sample with one more multi-step Turn."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":31,"time":1787472300031} +{"type":"step/start","data":{"turn":3,"step":1},"seq":32,"time":1787472300032} +{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"id":"preview-spacing-3-tools","role":"assistant","content":[{"type":"reasoning","text":"Read the task list and print the final sample marker."},{"type":"tool-call","id":"preview-spacing-tasks","name":"read","arguments":"{\"file_path\":\"data/tasks.json\"}"},{"type":"tool-call","id":"preview-spacing-final-bash","name":"bash","arguments":"{\"command\":\"printf 'spacing sample complete\\\\n'\",\"description\":\"Print the final spacing sample marker\"}"}],"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":["Read the task list and print the final sample marker."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Read the task list and print the final sample marker."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"preview-spacing-tasks","name":"read","args":["{\"file_path\":\"data/tasks.json\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"preview-spacing-tasks","name":"read","arguments":"{\"file_path\":\"data/tasks.json\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":2,"dt":[],"id":"preview-spacing-final-bash","name":"bash","args":["{\"command\":\"printf 'spacing sample complete\\\\n'\",\"description\":\"Print the final spacing sample marker\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"preview-spacing-final-bash","name":"bash","arguments":"{\"command\":\"printf 'spacing sample complete\\\\n'\",\"description\":\"Print the final spacing sample marker\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append","seq":33,"time":1787472300033} +{"type":"tool/call","data":{"turn":3,"step":1,"callId":"preview-spacing-tasks","name":"read","arguments":"{\"file_path\":\"data/tasks.json\"}"},"seq":34,"time":1787472300034} +{"type":"tool/result","data":{"turn":3,"step":1,"message":{"id":"preview-spacing-tasks-result","role":"tool","toolCallId":"preview-spacing-tasks","source":{"kind":"tool","callId":"preview-spacing-tasks"},"content":[{"type":"text","text":"data/tasks.json\nfile\n\n1: {\n2: \"title\": \"Preview verification\",\n3: \"tasks\": [\n4: {\n5: \"name\": \"Inspect tool cards\",\n6: \"status\": \"completed\"\n7: },\n8: {\n9: \"name\": \"Open both subagents\",\n10: \"status\": \"completed\"\n11: },\n12: {\n13: \"name\": \"Load earlier history\",\n14: \"status\": \"completed\"\n15: }\n16: ]\n17: }\n\n(End of file - total 17 lines)\n"}],"isError":false}},"surfaceOp":"append","seq":35,"time":1787472300035} +{"type":"tool/call","data":{"turn":3,"step":1,"callId":"preview-spacing-final-bash","name":"bash","arguments":"{\"command\":\"printf 'spacing sample complete\\\\n'\",\"description\":\"Print the final spacing sample marker\"}"},"seq":36,"time":1787472300036} +{"type":"tool/result","data":{"turn":3,"step":1,"message":{"id":"preview-spacing-final-bash-result","role":"tool","toolCallId":"preview-spacing-final-bash","source":{"kind":"tool","callId":"preview-spacing-final-bash"},"content":[{"type":"text","text":"spacing sample complete\n"}],"isError":false}},"surfaceOp":"append","seq":37,"time":1787472300037} +{"type":"step/end","data":{"turn":3,"step":1},"seq":38,"time":1787472300038} +{"type":"step/start","data":{"turn":3,"step":2},"seq":39,"time":1787472300039} +{"type":"assistant/message","data":{"turn":3,"step":2,"message":{"id":"preview-spacing-3-body","role":"assistant","content":[{"type":"text","text":"## Turn 3 — closing\n\nThe last Turn has a two-step shape: one process step and one closing body.\n\nSpacing sample complete: compare these process rows with the reply clearance around each Turn body."}],"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":["## Turn 3 — closing\n\nThe last Turn has a two-step shape: one process step and one closing body.\n\nSpacing sample complete: compare these process rows with the reply clearance around each Turn body."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"## Turn 3 — closing\n\nThe last Turn has a two-step shape: one process step and one closing body.\n\nSpacing sample complete: compare these process rows with the reply clearance around each Turn body."}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append","seq":40,"time":1787472300040} +{"type":"step/end","data":{"turn":3,"step":2},"seq":41,"time":1787472300041} +{"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}},"seq":42,"time":1787472300042} 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 d26f67472b..97a8fec768 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 @@ -21,6 +21,22 @@ "val": "WebWorker Preview Showcase" } } + }, + "preview-turn-spacing": { + "identity": { + "formatVersion": 4, + "createdAt": 1787472300000, + "cwd": "/dsh/workspace", + "isSeeded": false, + "inheritedEventCount": 0 + }, + "rows": { + "title": { + "ver": 1, + "seq": 42, + "val": "Turn spacing sample" + } + } } } } 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 5e588839a4..712595dabf 100644 --- a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts +++ b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts @@ -15,6 +15,7 @@ import { VFS_EXAMPLE_OLDEST_MESSAGE, VFS_EXAMPLE_ROOT, VFS_EXAMPLE_REASONING, + VFS_EXAMPLE_SPACING_TITLE, VFS_EXAMPLE_WRAPPED_COMMAND, VFS_EXAMPLE_SCHEDULE_IDS, VFS_EXAMPLE_SESSION_IDS, @@ -61,24 +62,30 @@ function textOf(event: SessionEvent): string { } describe('WebWorker preview VFS example', () => { - it('keeps the current showcase and strictly restorable historical child inputs beside their predecessors', () => { + it('keeps current Sessions byte-identical and historical child inputs restorable beside their predecessors', () => { const expected = buildVfsExampleFiles() const currentName = generationLogFilename(SESSION_FORMAT_VERSION, 'none') const main = `home/sessions/--dsh-workspace--/${VFS_EXAMPLE_SESSION_IDS.main}/` - const committedPath = (path: string): string => path.endsWith(`/${currentName}`) && !path.startsWith(main) + // The showcase and the spacing sample ship their current generation; only the children keep a + // released v3 predecessor that the packer upgrades at build time. + const currentOnly = new Set([VFS_EXAMPLE_SESSION_IDS.main, VFS_EXAMPLE_SESSION_IDS.spacing]) + const sessionIdOf = (path: string): string => path.split('/').at(-2) ?? '' + const committedPath = (path: string): string => path.endsWith(`/${currentName}`) && !currentOnly.has(sessionIdOf(path)) ? `${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`) + const predecessors = [ + VFS_EXAMPLE_SESSION_IDS.main, VFS_EXAMPLE_SESSION_IDS.oneShot, VFS_EXAMPLE_SESSION_IDS.continuable, + ].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) { const committed = readFileSync(join(VFS_EXAMPLE_ROOT, committedPath(path)), 'utf8') - if (path.endsWith(`/${currentName}`) && !path.startsWith(main)) { + const current = path.endsWith(`/${currentName}`) && currentOnly.has(sessionIdOf(path)) + if (path.endsWith(`/${currentName}`) && !current) { 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) + if (current) restoreSession(committed, SESSION_FORMAT_VERSION) } } }) @@ -182,6 +189,28 @@ describe('WebWorker preview VFS example', () => { expect(events.some(event => event.type === 'tool/result' && event.data.message.isError === true)).toBe(true) }) + it('restores the multi-Turn spacing sample with a body and several steps per Turn', () => { + const { meta, events } = readSession(VFS_EXAMPLE_SESSION_IDS.spacing) + expect(meta).toMatchObject({ + id: VFS_EXAMPLE_SESSION_IDS.spacing, + cwd: '/dsh/workspace', + delegationDepth: 0, + }) + expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index)) + expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } }) + expect(events.some(event => event.type === 'session/title' + && event.data.title === VFS_EXAMPLE_SPACING_TITLE)).toBe(true) + for (const turn of [1, 2, 3]) { + const steps = events.filter(event => event.type === 'step/start' && event.data.turn === turn) + expect(steps.length).toBeGreaterThanOrEqual(2) + const body = events + .filter(event => event.type === 'assistant/message' && event.data.turn === turn) + .map(textOf) + .join('\n') + expect(body).toContain('## Turn') + } + }) + 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) diff --git a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts index 2f015bf246..396bb29fe7 100644 --- a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts +++ b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts @@ -27,11 +27,15 @@ export const VFS_EXAMPLE_SESSION_IDS = { main: SessionId('preview-showcase'), oneShot: SessionId('preview-architecture-review'), continuable: SessionId('preview-follow-up-builder'), + spacing: SessionId('preview-turn-spacing'), } as const /** Stable title rendered in the root Session list. */ export const VFS_EXAMPLE_TITLE = 'WebWorker Preview Showcase' +/** Title of the multi-Turn spacing sample in the root Session list. */ +export const VFS_EXAMPLE_SPACING_TITLE = 'Turn spacing sample' + /** Oldest prompt, intentionally outside the first 50-message history page. */ export const VFS_EXAMPLE_OLDEST_MESSAGE = 'History checkpoint 01: verify deterministic preview state.' @@ -198,6 +202,12 @@ function readResult(): { text: string; meta: unknown } { } } +/** Render the workspace task file the way a `read` result shows it. */ +function tasksReadResult(): string { + const lines = TASKS.trimEnd().split('\n') + return `data/tasks.json\nfile\n\n${lines.map((text, index) => `${String(index + 1)}: ${text}`).join('\n')}\n\n(End of file - total ${String(lines.length)} lines)\n` +} + function galleryCalls(): GalleryCall[] { const read = readResult() return [ @@ -432,6 +442,149 @@ function continuableLog(): SessionEvent[] { return log.events } +/** One step of the multi-Turn spacing sample: reasoning, tool calls, or a closing body. */ +interface SpacingStep { + /** Assistant message id. */ + readonly id: string + /** Reasoning text shown in the step's Think row. */ + readonly reasoning?: string + /** Tool calls issued in this step, each with its recorded result. */ + readonly calls?: readonly GalleryCall[] + /** Closing Markdown body for a step without tool calls. */ + readonly body?: string +} + +function addSpacingTurn( + log: EventLog, + turn: number, + prompt: { readonly id: string; readonly text: string }, + steps: readonly SpacingStep[], +): void { + log.add({ type: 'turn/start', data: { turn } }) + log.add(userMessage(prompt.id, prompt.text)) + if (turn === 1) { + log.add({ + type: 'session/title', + data: { title: VFS_EXAMPLE_SPACING_TITLE, messageSeqs: [], source: { kind: 'user' } }, + }) + } + for (const [index, step] of steps.entries()) { + const stepNumber = index + 1 + log.add({ type: 'step/start', data: { turn, step: stepNumber } }) + const content: unknown[] = [] + if (step.reasoning !== undefined) content.push({ type: 'reasoning', text: step.reasoning }) + for (const call of step.calls ?? []) { + content.push({ type: 'tool-call', id: call.id, name: call.name, arguments: JSON.stringify(call.args) }) + } + if (step.body !== undefined) content.push({ type: 'text', text: step.body }) + log.add(assistantMessage(step.id, turn, stepNumber, content)) + for (const call of step.calls ?? []) { + log.add({ + type: 'tool/call', + data: { turn, step: stepNumber, callId: call.id, name: call.name, arguments: JSON.stringify(call.args) }, + }) + log.add({ + type: 'tool/result', + data: { + turn, + step: stepNumber, + message: { + id: `${call.id}-result`, + role: 'tool', + toolCallId: call.id, + source: { kind: 'tool', callId: call.id }, + content: [{ type: 'text', text: call.result }], + isError: false, + }, + }, + surfaceOp: 'append', + }) + } + log.add({ type: 'step/end', data: { turn, step: stepNumber } }) + } + log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) +} + +/** + * Three Turns with multi-step processes and multi-paragraph bodies, so the + * preview shows the tightened process-row gap beside the reply clearance. + */ +function spacingLog(): SessionEvent[] { + const log = new EventLog(CREATED_AT + 300_000) + addSpacingTurn(log, 1, { + id: 'preview-spacing-user-1', + text: 'Show how multiple Turns and their process steps sit on the tightened spacing.', + }, [ + { + id: 'preview-spacing-1-tools', + reasoning: 'Read the workspace guide, then list the files this sample references.', + calls: [ + { id: 'preview-spacing-read', name: 'read', args: { file_path: 'PREVIEW.md' }, result: readResult().text }, + { + id: 'preview-spacing-glob', name: 'glob', args: { pattern: '**/*', path: '.' }, + result: 'PREVIEW.md\ndata/tasks.json\nsrc/preview.ts', + }, + ], + }, + { + id: 'preview-spacing-1-command', + reasoning: 'Confirm the sample command result before writing the body.', + calls: [{ + id: 'preview-spacing-bash', name: 'bash', + args: { command: "printf 'spacing sample 1\\n'", description: 'Print the first spacing sample marker' }, + result: 'spacing sample 1\n', + }], + }, + { + id: 'preview-spacing-1-body', + body: '## Turn 1 — setup\n\nThis Turn has three steps: two process steps and one closing body. Inside an expanded group the process rows use the tightened gap, while this body keeps the reply clearance from the rows above.\n\nExpand the Turn control above to compare the tightened process rhythm with the reply rhythm.', + }, + ]) + addSpacingTurn(log, 2, { + id: 'preview-spacing-user-2', + text: 'Continue the sample with a second body and a shorter process.', + }, [ + { + id: 'preview-spacing-2-tools', + reasoning: 'Search the sample workspace for the spacing note.', + calls: [{ + id: 'preview-spacing-grep', name: 'grep', + args: { pattern: 'preview', path: '.', include: '*.{md,ts,json}' }, + result: 'PREVIEW.md:3:This deterministic workspace is bundled with the browser-only preview.\nsrc/preview.ts:1:export const previewStatus = \'ready\'', + }], + }, + { + id: 'preview-spacing-2-body', + body: '## Turn 2 — follow-up\n\nA shorter Turn still separates its process group from this body by the reply clearance, so consecutive Turns stay readable.\n\nThe gap between the previous Turn and this user message is the ordinary flow gap.', + }, + ]) + addSpacingTurn(log, 3, { + id: 'preview-spacing-user-3', + text: 'Finish the sample with one more multi-step Turn.', + }, [ + { + id: 'preview-spacing-3-tools', + reasoning: 'Read the task list and print the final sample marker.', + calls: [ + { + id: 'preview-spacing-tasks', name: 'read', args: { file_path: 'data/tasks.json' }, + result: tasksReadResult(), + }, + { + id: 'preview-spacing-final-bash', name: 'bash', + args: { command: "printf 'spacing sample complete\\n'", description: 'Print the final spacing sample marker' }, + result: 'spacing sample complete\n', + }, + ], + }, + { + id: 'preview-spacing-3-body', + body: '## Turn 3 — closing\n\nThe last Turn has a two-step shape: one process step and one closing body.\n\nSpacing sample complete: compare these process rows with the reply clearance around each Turn body.', + }, + ]) + return log.events +} + function header( id: SessionHeader['id'], createdAt: number, @@ -524,6 +677,7 @@ function scheduleTasks(): Readonly> { /** Build current-generation fixture files as fixture-relative UTF-8 text; committed predecessors remain untouched. */ export function buildVfsExampleFiles(): ReadonlyMap { const main = mainLog() + const spacing = spacingLog() const project = projectKey(WORKSPACE) const sessionPath = (id: string): string => `home/sessions/${project}/${id}/${generationLogFilename(SESSION_FORMAT_VERSION, 'none')}` @@ -544,6 +698,18 @@ export function buildVfsExampleFiles(): ReadonlyMap { title: { ver: 1, seq: main.events.at(-1)?.seq ?? -1, val: VFS_EXAMPLE_TITLE }, }, }, + [VFS_EXAMPLE_SESSION_IDS.spacing]: { + identity: { + formatVersion: SESSION_FORMAT_VERSION, + createdAt: CREATED_AT + 300_000, + cwd: WORKSPACE, + isSeeded: false, + inheritedEventCount: 0, + }, + rows: { + title: { ver: 1, seq: spacing.at(-1)?.seq ?? -1, val: VFS_EXAMPLE_SPACING_TITLE }, + }, + }, }, }, }, null, 2)}\n` @@ -557,6 +723,10 @@ export function buildVfsExampleFiles(): ReadonlyMap { header(VFS_EXAMPLE_SESSION_IDS.main, CREATED_AT), main.events, )], + [sessionPath(VFS_EXAMPLE_SESSION_IDS.spacing), renderLog( + header(VFS_EXAMPLE_SESSION_IDS.spacing, CREATED_AT + 300_000), + spacing, + )], [sessionPath(VFS_EXAMPLE_SESSION_IDS.oneShot), renderLog( header(VFS_EXAMPLE_SESSION_IDS.oneShot, CREATED_AT + 100_000, { parentSession: VFS_EXAMPLE_SESSION_IDS.main,