Merge pull request #7075 from deepseek-harness/worktree-chatgap

feat(chat): tighten the process-row gap to 4px
This commit is contained in:
imccyu authored and GitHub committed 2026-10-23 19:48:59 +08:00
1 parent 0741cf26a5
commit 22ddee3792
18 files changed
+297 -28

No files matched your search

+4
View File
@@ -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)
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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 样式传递图片数据。
@@ -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;
@@ -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);
@@ -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
@@ -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.
@@ -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 回复间距,因此折叠不会改变它的间距。
已展开的限高组仅在自己的滚动位置处于底部时跟随内容增长。滚离底部暂停该组跟随,滚回底部后恢复,与外层文本记录的跟随状态独立。手动展开未关闭的组时定位到底部并跟随增长;手动展开已关闭的组时从顶部开始且不启用跟随,即使初始内容无需滚动即可容纳。数据中的组变为已关闭,或通过模式切换恢复限高时,不重置已展开组的阅读位置。浏览器查找沿用自身的展开定位。
@@ -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)}`))
@@ -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])
@@ -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
@@ -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.
@@ -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` 扩展名错误,而不限定某一固定路径;其他扩展名、错误码或消息,以及意外成功的豁免导入,仍然报错。
@@ -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":"<path>PREVIEW.md</path>\n<type>file</type>\n<content>\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</content>"}],"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":"<path>data/tasks.json</path>\n<type>file</type>\n<content>\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</content>"}],"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}
@@ -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"
}
}
}
}
}
@@ -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<string>([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)
@@ -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 `<path>data/tasks.json</path>\n<type>file</type>\n<content>\n${lines.map((text, index) => `${String(index + 1)}: ${text}`).join('\n')}\n\n(End of file - total ${String(lines.length)} lines)\n</content>`
}
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<Record<string, unknown>> {
/** Build current-generation fixture files as fixture-relative UTF-8 text; committed predecessors remain untouched. */
export function buildVfsExampleFiles(): ReadonlyMap<string, string> {
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<string, string> {
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<string, string> {
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,