fix(client): bound contextual diff comparison work
This commit is contained in:
1 parent
53786d6415
commit
f50309b5d9
15 files changed
+144
-35
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.md
|
||||
2026-09-14-web-diff-context.md: 6fdf61563d78c7a302dcdb8255d9c8c50236a202
|
||||
2026-09-14-web-diff-context.zh.md: 1c0a99408489ef54f20a6deb7ead481967f0b04d
|
||||
2026-09-14-web-diff-context.md: 072af0966689d475d7fdd1df83861fa847f3745f
|
||||
2026-09-14-web-diff-context.zh.md: cd165a443451fc5ba580078b1b431a2788671dbe
|
||||
@@ -10,12 +10,24 @@ Filesystem result metadata carries before/after fragments that include unchanged
|
||||
|
||||
## Decision
|
||||
|
||||
The Web primitive derives line patches with the maintained `diff` library. Each change includes up to three context lines on either side; distant changes use separate hunks. Context appears once in a neutral tone and contributes to neither total. The card and `diffTotals` use the same patch derivation. This remains Client presentation under the [tool presentation ownership decision](../architecture/2026-08-23-client-derived-tool-presentation.md), without changing persisted metadata or public props.
|
||||
The Web primitive derives line patches with the maintained `diff` library, using `maxEditLength: 256`. Each exact change includes up to three neutral context lines on either side; distant changes use separate hunks and shared context contributes to neither total. Beyond 256 additions/deletions per fragment, search stops and the complete old/new fragments render as a coarse replacement, including shared lines in the display, copy, and counts. The card and `diffTotals` use this same deterministic derivation. This remains Client presentation under the [tool presentation ownership decision](../architecture/2026-08-23-client-derived-tool-presentation.md), without changing persisted metadata or public props.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Trimming only a common prefix and suffix cannot recognize unchanged lines between replacements. Extending durable metadata with row kinds would require producers and historical readers to change for a display-only correction. A custom diff algorithm adds maintenance without a distinct requirement.
|
||||
Unbounded comparison stalls collapsed summaries on heavily changed fragments. A deterministic edit-distance limit preserves exact sparse edits regardless of file length; a wall-clock timeout could make the summary and body choose different results under load. A coarse replacement sacrifices alignment above the limit while retaining every input line. Caching or asynchronous rendering adds ownership and invalidation work that the bounded comparison does not require. Extending durable metadata or maintaining a custom diff algorithm is unnecessary for this presentation behavior.
|
||||
|
||||
## Measurement
|
||||
|
||||
A local CPU diagnostic bundled the production `DiffBlock.tsx` entry with esbuild (`--bundle --platform=node --format=esm`) and timed `diffTotals` under Node 26.5.0 on macOS ARM64. Each fragment has 10,000 lines: unique indexed lines replaced completely, 100 evenly spaced replacements, or alternating repeated `old`/`shared` versus `new`/`shared` lines. Input construction and module loading are excluded; returned totals remain reachable. These are function timings, not browser paint or input latency, and carry no CI timing threshold.
|
||||
|
||||
| Input | Unbounded milliseconds | Bounded search milliseconds |
|
||||
| --- | --- | --- |
|
||||
| Complete replacement | 6492.16, 6777.79, 6786.79 | 5.25, 4.66, 4.19 |
|
||||
| 100 sparse replacements | 8.19, 4.58, 4.01 | 7.58, 4.26, 4.13 |
|
||||
| Alternating repeated lines | 3383.13 | 10.26, 7.46, 5.52 |
|
||||
|
||||
The bound admits all 100 sparse replacements unchanged. The 129-replacement regression fails without it because the unbounded implementation returns exact counts instead of the required complete-fragment fallback.
|
||||
|
||||
## Consequences
|
||||
|
||||
The browser build includes `diff`. Comparing very large replacements is synchronous and can delay rendering even for collapsed summaries; the row height cap does not bound this work. An approximate replacement fallback is excluded because it can mislabel unchanged lines. The existing content-line rule treats a trailing newline as a terminator, so newline-only differences remain unrepresented. Component regressions cover shared and distant context, repeated lines, insertion/deletion, copied prefixes, and summary totals.
|
||||
The browser build includes `diff`. The bound limits edit-graph search, not wall-clock duration: normalization, fallback rows, and copied output still scale with input length, and expanded cards also derive their rows separately. The content-line rule treats a final newline as a terminator. Regressions cover exact output at 256 edits, complete coarse output above the limit, a sparse edit in 10,000 lines, shared and distant context, repeated lines, copied prefixes, and summary/footer parity. Authored and borrowed Session snapshots cover coarse and exact browser cards respectively.
|
||||
@@ -10,12 +10,24 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
Web 原语通过维护中的 `diff` 库生成行补丁。每处改动两侧最多保留三行上下文;远距离改动分成独立 hunk。上下文以中性色显示一次,且不计入增删统计。卡片和 `diffTotals` 使用同一补丁推导。这遵循[工具呈现归属决策](../architecture/2026-08-23-client-derived-tool-presentation.zh.md),仍属于 Client 呈现,不改变持久化元数据或公开 props。
|
||||
Web 原语通过维护中的 `diff` 库生成行补丁,使用 `maxEditLength: 256`。每处精确改动两侧最多保留三行中性上下文;远距离改动分成独立 hunk,共享上下文不计入增删统计。每个片段的新增与删除行数超过 256 时,搜索停止,完整新旧片段按粗粒度替换呈现,共享行也计入显示、复制和统计。卡片与 `diffTotals` 使用同一确定性推导。这遵循[工具呈现归属决策](../architecture/2026-08-23-client-derived-tool-presentation.zh.md),仍属于 Client 呈现,不改变持久化元数据或公开 props。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
只裁剪共同前后缀无法识别替换之间的未改动行。给持久化元数据扩展行类型会让纯显示修正要求生产方和历史读取方一同改变。自定义 diff 算法没有独立需求,却增加维护负担。
|
||||
无上限比较会让大量改动片段的折叠摘要停顿。确定性的编辑距离上限能让稀疏编辑保持精确,不受文件长度影响;墙钟超时可能使摘要和正文在负载下选择不同结果。粗粒度替换在超过上限后放弃对齐,但保留全部输入行。缓存或异步渲染会增加归属和失效处理,而有界比较不需要这些机制。该呈现行为无需扩展持久化元数据或维护自定义 diff 算法。
|
||||
|
||||
## Measurement
|
||||
|
||||
本地 CPU 诊断用 esbuild(`--bundle --platform=node --format=esm`)打包生产 `DiffBlock.tsx` 入口,并在 macOS ARM64 的 Node 26.5.0 下计时 `diffTotals`。每个片段有一万行:全部替换带唯一索引的行、均匀分布的 100 处替换,或交替重复的 `old`/`shared` 与 `new`/`shared` 行。不计输入构造和模块加载;返回的统计值保持可达。这些是函数耗时,不是浏览器绘制或输入延迟,也没有作为 CI 时间阈值。
|
||||
|
||||
| 输入 | 无上限毫秒数 | 有界搜索毫秒数 |
|
||||
| --- | --- | --- |
|
||||
| 全部替换 | 6492.16, 6777.79, 6786.79 | 5.25, 4.66, 4.19 |
|
||||
| 100 处稀疏替换 | 8.19, 4.58, 4.01 | 7.58, 4.26, 4.13 |
|
||||
| 交替重复行 | 3383.13 | 10.26, 7.46, 5.52 |
|
||||
|
||||
上限允许全部 100 处稀疏替换保持精确。移除上限时,129 处替换回归会失败,因为无上限实现返回精确统计,而非要求的完整片段回退。
|
||||
|
||||
## Consequences
|
||||
|
||||
浏览器构建包含 `diff`。非常大的替换采用同步比较,即使折叠摘要也可能延迟渲染;行高限制不会约束这项工作。不采用近似的完整替换回退,因为它可能错误标记未改动行。既有内容行规则把末尾换行视为终止符,因此仅末尾换行有无不同仍不展示。组件回归覆盖共享和远距离上下文、重复行、插入与删除、复制前缀及摘要统计。
|
||||
浏览器构建包含 `diff`。上限约束编辑图搜索,不约束墙钟时长:规范化、回退行及复制内容仍随输入长度增长,展开卡片也会单独推导正文行。内容行规则把末尾换行视为终止符。回归覆盖 256 次编辑时的精确输出、超过上限后的完整粗粒度输出、一万行中的稀疏编辑、共享和远距离上下文、重复行、复制前缀及摘要与底部统计一致性。编写和借用的 Session 快照分别覆盖粗粒度和精确的浏览器卡片。
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Cold rendering of the shared filesystem edit Session preserves neutral context. */
|
||||
/** Cold Session rendering covers exact context and bounded whole-fragment replacements. */
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Browser, type Page } from 'playwright'
|
||||
@@ -9,11 +9,15 @@ import {
|
||||
} from './scaffold.ts'
|
||||
import { expandTurnProcesses, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/diff-context', import.meta.url))
|
||||
const SOURCE = fileURLToPath(new URL('../../../snapshots/session/fs-edit/session.v3.jsonl', import.meta.url))
|
||||
const ROOT = fileURLToPath(new URL('../../../snapshots', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: contextual edit diff', () => {
|
||||
const CASES = [
|
||||
{ name: 'diff-context', source: 'session/fs-edit/session.v3.jsonl', totals: '+1 -1', shared: 'level=info', inventory: ['ui.expected.md'] },
|
||||
{ name: 'diff-bounded', source: 'web/diff-bounded/session.v3.jsonl', totals: '+130 -130', shared: 'shared heading', inventory: ['session.v3.jsonl', 'ui.expected.md'] },
|
||||
]
|
||||
|
||||
describe.skipIf(MODE === 'record').each(CASES)('web e2e: $name', (scenario) => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
@@ -21,7 +25,7 @@ describe.skipIf(MODE === 'record')('web e2e: contextual edit diff', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
await seedSession(scaffold, await readFile(SOURCE, 'utf8'), 'diff-context')
|
||||
await seedSession(scaffold, await readFile(`${ROOT}/${scenario.source}`, 'utf8'), scenario.name)
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
@@ -36,8 +40,8 @@ describe.skipIf(MODE === 'record')('web e2e: contextual edit diff', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('shows true totals before expansion and neutral shared context after expansion', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-diff-context'))
|
||||
it('keeps collapsed and expanded counts consistent with the displayed diff', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, `web-e2e-${scenario.name}`))
|
||||
const group = page.locator('[role="treeitem"]').first()
|
||||
await group.waitFor({ timeout: 15_000 })
|
||||
await group.click()
|
||||
@@ -45,16 +49,17 @@ describe.skipIf(MODE === 'record')('web e2e: contextual edit diff', () => {
|
||||
await page.getByText('DONE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expandTurnProcesses(page)
|
||||
const edit = page.locator('[data-variant="edit"]')
|
||||
expect(await edit.textContent()).toContain('+1 -1')
|
||||
expect(await edit.textContent()).toContain(scenario.totals)
|
||||
expect(await edit.locator('[data-diff]').count()).toBe(0)
|
||||
await edit.locator('[data-expandable]').click()
|
||||
const card = edit.locator('[data-diff]')
|
||||
await card.waitFor()
|
||||
expect(await card.getByText('level=info', { exact: true }).count()).toBe(1)
|
||||
expect(await card.textContent()).toContain('+1 -1 · 1 file')
|
||||
await compareOrRefreshGolden(`${SNAPSHOT_DIR}/ui.expected.md`,
|
||||
expect(await card.getByText(scenario.shared, { exact: true }).count()).toBe(1)
|
||||
expect(await card.textContent()).toContain(`${scenario.totals} · 1 file`)
|
||||
const snapshotDir = `${ROOT}/web/${scenario.name}`
|
||||
await compareOrRefreshGolden(`${snapshotDir}/ui.expected.md`,
|
||||
await captureStableAria(page, '[data-variant="edit"]', scaffold.workspaceCwd), MODE)
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
|
||||
await assertFixtureInventory(snapshotDir, scenario.inventory)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
|
||||
README.md: fb2765de406da11db87f9651999d12030c9297a7
|
||||
README.zh.md: 9eb69c6a46d972b3fee4a42d77d534b4dae88920
|
||||
README.md: 631b19de44660c21813bc4f87b7a59dc8dfa6ee3
|
||||
README.zh.md: b23c87f7861555625f28a9fd182aa6acb159b21c
|
||||
@@ -73,7 +73,7 @@ The catalog above lists what each export is for; this section covers the behavio
|
||||
|
||||
`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. When supplied with `UserTextReferences`, file and skill references become keyboard-accessible preview buttons using the same hover and focus styling as prose file links; the first pointer click can open a preview, while subsequent clicks and existing text selections retain native selection handling. Keyboard activation opens previews even when text is selected.
|
||||
|
||||
`DiffBlock` compares the old and new content by line. It shows actual additions and deletions with up to three neutral context lines on each side, separates distant changes with `⋯`, and excludes shared context from both summary and footer totals. Copy includes the local diff with context prefixes. A final newline is treated as a terminator; differences only in the presence of a final newline are not displayed.
|
||||
`DiffBlock` compares the old and new content by line. It shows actual additions and deletions with up to three neutral context lines on each side, separates distant changes with `⋯`, and excludes shared context from both summary and footer totals. Search stops beyond 256 line additions/deletions per fragment; those fragments display and count the complete old and new contents as a coarse replacement, including shared lines. Copy includes the full displayed diff with its prefixes. A final newline is treated as a terminator; differences only in the presence of a final newline are not displayed.
|
||||
|
||||
`JsonTree` clamps collapsed strings to `collapsedStringLines` (three by default). Expanded strings show raw text, retain sibling commas, and fit within the window and outer scrolling containers. Resize and ancestor-scroll events update that limit. Row copy feedback updates independently of JSON value rendering; pending clipboard writes cannot update a different row or an unmounted tree.
|
||||
|
||||
@@ -144,7 +144,7 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap.
|
||||
|
||||
- **Large replacements require synchronous line comparison** — diff totals are computed even while a tool row is collapsed; inputs with many changed lines can delay rendering. The height cap limits displayed rows, not comparison work.
|
||||
- **Diff search is bounded, input processing is linear** — the edit-distance limit trades precise alignment for a coarse replacement on heavily changed fragments. Normalization, fallback rows, and copied output still scale with input size; the height cap limits visible rows, not those allocations.
|
||||
- **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it.
|
||||
- **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path.
|
||||
- **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists.
|
||||
|
||||
@@ -73,7 +73,7 @@ kind: "package-library"
|
||||
|
||||
`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。 传入 `UserTextReferences` 时,文件和 skill 引用成为支持键盘操作的预览按钮,复用正文文件链接的悬停和聚焦样式;第一次指针点击可以打开预览,后续点击和已有选区保留原生选择行为。键盘激活在存在选区时仍可打开预览。
|
||||
|
||||
`DiffBlock` 按行比较新旧内容。它显示实际增删行及两侧最多三行中性上下文,用 `⋯` 分隔远距离改动,摘要和底部统计都不计入共享上下文。复制包含带上下文前缀的局部 diff。末尾换行视为行终止符;仅末尾换行不同不会显示为改动。
|
||||
`DiffBlock` 按行比较新旧内容。它显示实际增删行及两侧最多三行中性上下文,用 `⋯` 分隔远距离改动,摘要和底部统计都不计入共享上下文。若一个片段需要超过 256 次行新增或删除,则停止精确比较;该片段按完整新旧内容显示和统计为粗粒度替换,包含共享行。复制包含完整显示 diff 及其前缀。末尾换行视为行终止符;仅末尾换行不同不会显示为改动。
|
||||
|
||||
`JsonTree` 把折叠字符串限制为 `collapsedStringLines` 行(默认三行)。展开后显示原始文本、保留同级逗号,并限制在窗口与外层滚动容器内;尺寸变化和祖先滚动事件会更新此限制。行复制反馈独立于 JSON 值渲染更新;尚未完成的剪贴板写入不会更新另一行或已卸载的树。
|
||||
|
||||
@@ -144,7 +144,7 @@ kind: "package-library"
|
||||
|
||||
这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。
|
||||
|
||||
- **大规模替换需要同步行比较**:工具行折叠时也会计算 diff 统计;包含大量改动行的输入可能延迟渲染。高度限制只约束显示行数,不限制比较工作量。
|
||||
- **Diff 搜索有上限,输入处理仍为线性**:编辑距离上限使大量改动的片段采用粗粒度替换,不再精确对齐。规范化、回退行及复制内容仍随输入大小增长;高度限制只约束可见行数,不限制这些分配。
|
||||
- **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。
|
||||
- **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。
|
||||
- **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
|
||||
|
||||
@@ -64,15 +64,22 @@ const ROW_CLASS: Record<DiffRow['kind'], string | undefined> = {
|
||||
gap: css.gap,
|
||||
}
|
||||
|
||||
/** Derive local patches with three context lines, using the card's terminator rule. */
|
||||
/** Bound synchronous edit-graph search; one replacement consumes two edits. */
|
||||
const MAX_DIFF_EDIT_LENGTH = 256
|
||||
|
||||
/** Derive exact local patches or a whole-fragment replacement when search exceeds the limit. */
|
||||
function localHunks(diff: DiffHunk) {
|
||||
const normalize = (text: string): string => contentLines(text).map(line => `${line}\n`).join('')
|
||||
return structuredPatch('', '', normalize(diff.oldText ?? ''), normalize(diff.newText),
|
||||
undefined, undefined, { context: 3 }).hunks
|
||||
const oldLines = contentLines(diff.oldText ?? '')
|
||||
const newLines = contentLines(diff.newText)
|
||||
const normalize = (lines: string[]): string => lines.map(line => `${line}\n`).join('')
|
||||
return structuredPatch('', '', normalize(oldLines), normalize(newLines),
|
||||
undefined, undefined, { context: 3, maxEditLength: MAX_DIFF_EDIT_LENGTH })?.hunks
|
||||
?? [{ lines: [...oldLines.map(line => `-${line}`), ...newLines.map(line => `+${line}`)] }]
|
||||
}
|
||||
|
||||
/**
|
||||
* Count actual added and removed lines; shared context contributes to neither total.
|
||||
* Count displayed additions and deletions. Exact patches exclude shared context;
|
||||
* comparisons exceeding the edit limit count both complete fragments as replaced.
|
||||
* Text follows {@link contentLines}'s terminator rule.
|
||||
* @param diffs - the hunks to count.
|
||||
* @returns the +/- totals for summaries and the card footer.
|
||||
|
||||
@@ -96,6 +96,35 @@ describe('DiffBlock structure', () => {
|
||||
})
|
||||
|
||||
describe('DiffBlock local changes', () => {
|
||||
it.each([128, 129])('renders and copies %i replacements with bounded comparison', async (count) => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
|
||||
const oldLines = ['shared context', ...Array.from({ length: count }, (_, i) => `old ${i}`)]
|
||||
const newLines = ['shared context', ...Array.from({ length: count }, (_, i) => `new ${i}`)]
|
||||
const diffs = [{ path: 'large.txt', oldText: oldLines.join('\n'), newText: newLines.join('\n') }]
|
||||
const total = count === 128 ? count : count + 1
|
||||
render(<DiffBlock diffs={diffs} maxLines={1000} />)
|
||||
expect(diffTotals(diffs)).toEqual({ added: total, removed: total })
|
||||
expect(screen.getByText(`└ +${total} -${total} · 1 file`)).toBeTruthy()
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '复制' })) })
|
||||
expect(writeText).toHaveBeenCalledWith(count === 128
|
||||
? ['large.txt', ' shared context', ...oldLines.slice(1).map(line => `- ${line}`), ...newLines.slice(1).map(line => `+ ${line}`)].join('\n')
|
||||
: ['large.txt', ...oldLines.map(line => `- ${line}`), ...newLines.map(line => `+ ${line}`)].join('\n'))
|
||||
})
|
||||
|
||||
it('keeps a sparse edit exact in a ten-thousand-line fragment', () => {
|
||||
const before = Array.from({ length: 10000 }, (_, i) => `line ${i}`)
|
||||
const after = [...before]
|
||||
after[5000] = 'changed'
|
||||
const diffs = [{ path: 'sparse.txt', oldText: before.join('\n'), newText: after.join('\n') }]
|
||||
const { container } = render(<DiffBlock diffs={diffs} />)
|
||||
expect(diffTotals(diffs)).toEqual({ added: 1, removed: 1 })
|
||||
expect(bodyRows(container)).toEqual([
|
||||
'sparse.txt', 'line 4997', 'line 4998', 'line 4999', 'line 5000', 'changed',
|
||||
'line 5001', 'line 5002', 'line 5003',
|
||||
])
|
||||
})
|
||||
|
||||
it('copies shared context once and counts only a changed line', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-tool/README.md
|
||||
README.md: 2887da69937cea89aad68e9d01a5fa507ef248ec
|
||||
README.zh.md: 8df7cca46966f6afcab630dc2e5d77a546b7304d
|
||||
README.md: 15dffa634660a5cf94081452481399821d09b25b
|
||||
README.zh.md: 6f01658e5fea4fefca9eded114971d3e88a13148
|
||||
@@ -64,7 +64,7 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool
|
||||
|
||||
Every card is read in place in the call tree; there is no second, full-height presentation of a selected call. Row renderers share one pure card model for each terminal, read, diff, search, and web card, and the image card's gallery renders through the tool-owned `tool.call.images` slot. These models validate raw call arguments, result content, failure state, persisted metadata, PTC dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. A file-path summary opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar's text preview; `inspect` opens the trajectory view. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules.
|
||||
|
||||
Chat diff cards keep nine rows before folding, enough for a file header, one removed/added pair, and three context lines on either side. The collapsed row reports only actual additions and deletions.
|
||||
Chat diff cards keep nine rows before folding, enough for a file header, one removed/added pair, and three context lines on either side. The collapsed row and expanded footer share the primitive's exact or coarse-replacement counts.
|
||||
|
||||
An Auto denial takes precedence over keyed specialized views. Its generic row preserves the call identity, omits raw arguments, and normalizes the stored reason only for display: trim surrounding whitespace and collapse line separators to spaces, with localized fallback for an empty result. Session and SDK error details keep the original reason.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`
|
||||
|
||||
每张卡片都直接在调用树中查看;选中调用后不会再显示第二个全高视图。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` slot 渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、PTC dispatch 的 `parentCallId` 与会话路径信息。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。
|
||||
|
||||
Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删除与新增行及两侧各三行上下文。折叠工具行只统计实际新增和删除的行。
|
||||
Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删除与新增行及两侧各三行上下文。折叠工具行与展开卡片底部采用原语一致的精确或粗粒度替换统计。
|
||||
|
||||
Auto 拒绝优先于按工具名选择的专门视图。其通用行保留调用身份、省略原始参数,并且只在显示时归一化存储的理由:去除首尾空白,把行分隔符折叠为空格,结果为空时使用本地化通用理由。Session 与 SDK 错误详情保留原始理由。
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{"type":"session","version":3,"id":"{{session:1}}","createdAt":1783352084735,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
|
||||
{"type":"approval/policy","data":{"policy":"never"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Read large.txt, replace all 129 old settings with the corresponding new settings, preserve the shared heading, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Read large.txt, replace all 129 old settings with the corresponding new settings, preserve the shared heading, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Bounded diff example","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"fs-edit-read","name":"read","arguments":"{\"file_path\":\"large.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699361025,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699361025,"index":0,"dt":[],"id":"fs-edit-read","name":"read","args":["{\"file_path\":\"large.txt\"}"]},{"type":"chunk","time":1788699361025,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"fs-edit-read","name":"read","arguments":"{\"file_path\":\"large.txt\"}"}}},{"type":"chunk","time":1788699361025,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699361025,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"fs-edit-read","name":"read","arguments":"{\"file_path\":\"large.txt\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"fs-edit-read"},"content":[{"type":"tool-result","toolCallId":"fs-edit-read","content":[{"type":"text","text":"<path>{{cwd}}/large.txt</path>\n<type>file</type>\n<content>\n1: shared heading\n2: old setting 0\n3: old setting 1\n4: old setting 2\n5: old setting 3\n6: old setting 4\n7: old setting 5\n8: old setting 6\n9: old setting 7\n10: old setting 8\n11: old setting 9\n12: old setting 10\n13: old setting 11\n14: old setting 12\n15: old setting 13\n16: old setting 14\n17: old setting 15\n18: old setting 16\n19: old setting 17\n20: old setting 18\n21: old setting 19\n22: old setting 20\n23: old setting 21\n24: old setting 22\n25: old setting 23\n26: old setting 24\n27: old setting 25\n28: old setting 26\n29: old setting 27\n30: old setting 28\n31: old setting 29\n32: old setting 30\n33: old setting 31\n34: old setting 32\n35: old setting 33\n36: old setting 34\n37: old setting 35\n38: old setting 36\n39: old setting 37\n40: old setting 38\n41: old setting 39\n42: old setting 40\n43: old setting 41\n44: old setting 42\n45: old setting 43\n46: old setting 44\n47: old setting 45\n48: old setting 46\n49: old setting 47\n50: old setting 48\n51: old setting 49\n52: old setting 50\n53: old setting 51\n54: old setting 52\n55: old setting 53\n56: old setting 54\n57: old setting 55\n58: old setting 56\n59: old setting 57\n60: old setting 58\n61: old setting 59\n62: old setting 60\n63: old setting 61\n64: old setting 62\n65: old setting 63\n66: old setting 64\n67: old setting 65\n68: old setting 66\n69: old setting 67\n70: old setting 68\n71: old setting 69\n72: old setting 70\n73: old setting 71\n74: old setting 72\n75: old setting 73\n76: old setting 74\n77: old setting 75\n78: old setting 76\n79: old setting 77\n80: old setting 78\n81: old setting 79\n82: old setting 80\n83: old setting 81\n84: old setting 82\n85: old setting 83\n86: old setting 84\n87: old setting 85\n88: old setting 86\n89: old setting 87\n90: old setting 88\n91: old setting 89\n92: old setting 90\n93: old setting 91\n94: old setting 92\n95: old setting 93\n96: old setting 94\n97: old setting 95\n98: old setting 96\n99: old setting 97\n100: old setting 98\n101: old setting 99\n102: old setting 100\n103: old setting 101\n104: old setting 102\n105: old setting 103\n106: old setting 104\n107: old setting 105\n108: old setting 106\n109: old setting 107\n110: old setting 108\n111: old setting 109\n112: old setting 110\n113: old setting 111\n114: old setting 112\n115: old setting 113\n116: old setting 114\n117: old setting 115\n118: old setting 116\n119: old setting 117\n120: old setting 118\n121: old setting 119\n122: old setting 120\n123: old setting 121\n124: old setting 122\n125: old setting 123\n126: old setting 124\n127: old setting 125\n128: old setting 126\n129: old setting 127\n130: old setting 128\n\n(End of file - total 130 lines)\n</content>"}],"isError":false}],"role":"user","id":"{{message:5}}"},"meta":{"path":"{{cwd}}/large.txt","offset":1,"lines":[{"number":1,"text":"shared heading"},{"number":2,"text":"old setting 0"},{"number":3,"text":"old setting 1"},{"number":4,"text":"old setting 2"},{"number":5,"text":"old setting 3"},{"number":6,"text":"old setting 4"},{"number":7,"text":"old setting 5"},{"number":8,"text":"old setting 6"},{"number":9,"text":"old setting 7"},{"number":10,"text":"old setting 8"},{"number":11,"text":"old setting 9"},{"number":12,"text":"old setting 10"},{"number":13,"text":"old setting 11"},{"number":14,"text":"old setting 12"},{"number":15,"text":"old setting 13"},{"number":16,"text":"old setting 14"},{"number":17,"text":"old setting 15"},{"number":18,"text":"old setting 16"},{"number":19,"text":"old setting 17"},{"number":20,"text":"old setting 18"},{"number":21,"text":"old setting 19"},{"number":22,"text":"old setting 20"},{"number":23,"text":"old setting 21"},{"number":24,"text":"old setting 22"},{"number":25,"text":"old setting 23"},{"number":26,"text":"old setting 24"},{"number":27,"text":"old setting 25"},{"number":28,"text":"old setting 26"},{"number":29,"text":"old setting 27"},{"number":30,"text":"old setting 28"},{"number":31,"text":"old setting 29"},{"number":32,"text":"old setting 30"},{"number":33,"text":"old setting 31"},{"number":34,"text":"old setting 32"},{"number":35,"text":"old setting 33"},{"number":36,"text":"old setting 34"},{"number":37,"text":"old setting 35"},{"number":38,"text":"old setting 36"},{"number":39,"text":"old setting 37"},{"number":40,"text":"old setting 38"},{"number":41,"text":"old setting 39"},{"number":42,"text":"old setting 40"},{"number":43,"text":"old setting 41"},{"number":44,"text":"old setting 42"},{"number":45,"text":"old setting 43"},{"number":46,"text":"old setting 44"},{"number":47,"text":"old setting 45"},{"number":48,"text":"old setting 46"},{"number":49,"text":"old setting 47"},{"number":50,"text":"old setting 48"},{"number":51,"text":"old setting 49"},{"number":52,"teLine truncated
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"fs-edit-replace","name":"edit","arguments":"{\"file_path\":\"large.txt\",\"old_string\":\"shared heading\\nold setting 0\\nold setting 1\\nold setting 2\\nold setting 3\\nold setting 4\\nold setting 5\\nold setting 6\\nold setting 7\\nold setting 8\\nold setting 9\\nold setting 10\\nold setting 11\\nold setting 12\\nold setting 13\\nold setting 14\\nold setting 15\\nold setting 16\\nold setting 17\\nold setting 18\\nold setting 19\\nold setting 20\\nold setting 21\\nold setting 22\\nold setting 23\\nold setting 24\\nold setting 25\\nold setting 26\\nold setting 27\\nold setting 28\\nold setting 29\\nold setting 30\\nold setting 31\\nold setting 32\\nold setting 33\\nold setting 34\\nold setting 35\\nold setting 36\\nold setting 37\\nold setting 38\\nold setting 39\\nold setting 40\\nold setting 41\\nold setting 42\\nold setting 43\\nold setting 44\\nold setting 45\\nold setting 46\\nold setting 47\\nold setting 48\\nold setting 49\\nold setting 50\\nold setting 51\\nold setting 52\\nold setting 53\\nold setting 54\\nold setting 55\\nold setting 56\\nold setting 57\\nold setting 58\\nold setting 59\\nold setting 60\\nold setting 61\\nold setting 62\\nold setting 63\\nold setting 64\\nold setting 65\\nold setting 66\\nold setting 67\\nold setting 68\\nold setting 69\\nold setting 70\\nold setting 71\\nold setting 72\\nold setting 73\\nold setting 74\\nold setting 75\\nold setting 76\\nold setting 77\\nold setting 78\\nold setting 79\\nold setting 80\\nold setting 81\\nold setting 82\\nold setting 83\\nold setting 84\\nold setting 85\\nold setting 86\\nold setting 87\\nold setting 88\\nold setting 89\\nold setting 90\\nold setting 91\\nold setting 92\\nold setting 93\\nold setting 94\\nold setting 95\\nold setting 96\\nold setting 97\\nold setting 98\\nold setting 99\\nold setting 100\\nold setting 101\\nold setting 102\\nold setting 103\\nold setting 104\\nold setting 105\\nold setting 106\\nold setting 107\\nold setting 108\\nold setting 109\\nold setting 110\\nold setting 111\\nold setting 112\\nold setting 113\\nold setting 114\\nold setting 115\\nold setting 116\\nold setting 117\\nold setting 118\\nold setting 119\\nold setting 120\\nold setting 121\\nold setting 122\\nold setting 123\\nold setting 124\\nold setting 125\\nold setting 126\\nold setting 127\\nold setting 128\",\"new_string\":\"shared heading\\nnew setting 0\\nnew setting 1\\nnew setting 2\\nnew setting 3\\nnew setting 4\\nnew setting 5\\nnew setting 6\\nnew setting 7\\nnew setting 8\\nnew setting 9\\nnew setting 10\\nnew setting 11\\nnew setting 12\\nnew setting 13\\nnew setting 14\\nnew setting 15\\nnew setting 16\\nnew setting 17\\nnew setting 18\\nnew setting 19\\nnew setting 20\\nnew setting 21\\nnew setting 22\\nnew setting 23\\nnew setting 24\\nnew setting 25\\nnew setting 26\\nnew setting 27\\nnew setting 28\\nnew setting 29\\nnew setting 30\\nnew setting 31\\nnew setting 32\\nnew setting 33\\nnew setting 34\\nnew setting 35\\nnew setting 36\\nnew setting 37\\nnew setting 38\\nnew setting 39\\nnew setting 40\\nnew setting 41\\nnew setting 42\\nnew setting 43\\nnew setting 44\\nnew setting 45\\nnew setting 46\\nnew setting 47\\nnew setting 48\\nnew setting 49\\nnew setting 50\\nnew setting 51\\nnew setting 52\\nnew setting 53\\nnew setting 54\\nnew setting 55\\nnew setting 56\\nnew setting 57\\nnew setting 58\\nnew setting 59\\nnew setting 60\\nnew setting 61\\nnew setting 62\\nnew setting 63\\nnew setting 64\\nnew setting 65\\nnew setting 66\\nnew setting 67\\nnew setting 68\\nnew setting 69\\nnew setting 70\\nnew setting 71\\nnew setting 72\\nnew setting 73\\nnew setting 74\\nnew setting 75\\nnew setting 76\\nnew setting 77\\nnew setting 78\\nnew setting 79\\nnew setting 80\\nnew setting 81\\nnew setting 82\\nnew setting 83\\nnew setting 84\\nnew setting 85\\nnew setting 86\\nnew setting 87\\nnew setting 88\\nnew setting 89\\nnew setting 90\\nnew setting 91\\nnew setting 92\\nnew setting 93\\nnew setting 94\\nnew setting 95\\nnew setting 96\\nnew setting 97\\nnew setting 98\\nnew setting 99\\nnew setting 100\\nnew setting 101\\nnew setting 102\\nnew setting 103\\nnew setting 104\\nnew setting 105\\nnew setting 106\\nnew setting 107\\nnew setting 108\\nnew setting 109\\nnew setting 110\\nnew setting 111\\nnew setting 112\\nnew setting 113\\nnew setting 114\\nnew setting 115\\nnew setting 116\\nnew setting 117\\nnew setting 118\\nnew setting 119\\nnew setting 120\\nnew setting 121\\nnew setting 122\\nnew setting 123\\nnew setting 124\\nnew setting 125\\nnew setting 126\\nnew setting 127\\nnew setting 128\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699361045,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699361Line truncated
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"fs-edit-replace","name":"edit","arguments":"{\"file_path\":\"large.txt\",\"old_string\":\"shared heading\\nold setting 0\\nold setting 1\\nold setting 2\\nold setting 3\\nold setting 4\\nold setting 5\\nold setting 6\\nold setting 7\\nold setting 8\\nold setting 9\\nold setting 10\\nold setting 11\\nold setting 12\\nold setting 13\\nold setting 14\\nold setting 15\\nold setting 16\\nold setting 17\\nold setting 18\\nold setting 19\\nold setting 20\\nold setting 21\\nold setting 22\\nold setting 23\\nold setting 24\\nold setting 25\\nold setting 26\\nold setting 27\\nold setting 28\\nold setting 29\\nold setting 30\\nold setting 31\\nold setting 32\\nold setting 33\\nold setting 34\\nold setting 35\\nold setting 36\\nold setting 37\\nold setting 38\\nold setting 39\\nold setting 40\\nold setting 41\\nold setting 42\\nold setting 43\\nold setting 44\\nold setting 45\\nold setting 46\\nold setting 47\\nold setting 48\\nold setting 49\\nold setting 50\\nold setting 51\\nold setting 52\\nold setting 53\\nold setting 54\\nold setting 55\\nold setting 56\\nold setting 57\\nold setting 58\\nold setting 59\\nold setting 60\\nold setting 61\\nold setting 62\\nold setting 63\\nold setting 64\\nold setting 65\\nold setting 66\\nold setting 67\\nold setting 68\\nold setting 69\\nold setting 70\\nold setting 71\\nold setting 72\\nold setting 73\\nold setting 74\\nold setting 75\\nold setting 76\\nold setting 77\\nold setting 78\\nold setting 79\\nold setting 80\\nold setting 81\\nold setting 82\\nold setting 83\\nold setting 84\\nold setting 85\\nold setting 86\\nold setting 87\\nold setting 88\\nold setting 89\\nold setting 90\\nold setting 91\\nold setting 92\\nold setting 93\\nold setting 94\\nold setting 95\\nold setting 96\\nold setting 97\\nold setting 98\\nold setting 99\\nold setting 100\\nold setting 101\\nold setting 102\\nold setting 103\\nold setting 104\\nold setting 105\\nold setting 106\\nold setting 107\\nold setting 108\\nold setting 109\\nold setting 110\\nold setting 111\\nold setting 112\\nold setting 113\\nold setting 114\\nold setting 115\\nold setting 116\\nold setting 117\\nold setting 118\\nold setting 119\\nold setting 120\\nold setting 121\\nold setting 122\\nold setting 123\\nold setting 124\\nold setting 125\\nold setting 126\\nold setting 127\\nold setting 128\",\"new_string\":\"shared heading\\nnew setting 0\\nnew setting 1\\nnew setting 2\\nnew setting 3\\nnew setting 4\\nnew setting 5\\nnew setting 6\\nnew setting 7\\nnew setting 8\\nnew setting 9\\nnew setting 10\\nnew setting 11\\nnew setting 12\\nnew setting 13\\nnew setting 14\\nnew setting 15\\nnew setting 16\\nnew setting 17\\nnew setting 18\\nnew setting 19\\nnew setting 20\\nnew setting 21\\nnew setting 22\\nnew setting 23\\nnew setting 24\\nnew setting 25\\nnew setting 26\\nnew setting 27\\nnew setting 28\\nnew setting 29\\nnew setting 30\\nnew setting 31\\nnew setting 32\\nnew setting 33\\nnew setting 34\\nnew setting 35\\nnew setting 36\\nnew setting 37\\nnew setting 38\\nnew setting 39\\nnew setting 40\\nnew setting 41\\nnew setting 42\\nnew setting 43\\nnew setting 44\\nnew setting 45\\nnew setting 46\\nnew setting 47\\nnew setting 48\\nnew setting 49\\nnew setting 50\\nnew setting 51\\nnew setting 52\\nnew setting 53\\nnew setting 54\\nnew setting 55\\nnew setting 56\\nnew setting 57\\nnew setting 58\\nnew setting 59\\nnew setting 60\\nnew setting 61\\nnew setting 62\\nnew setting 63\\nnew setting 64\\nnew setting 65\\nnew setting 66\\nnew setting 67\\nnew setting 68\\nnew setting 69\\nnew setting 70\\nnew setting 71\\nnew setting 72\\nnew setting 73\\nnew setting 74\\nnew setting 75\\nnew setting 76\\nnew setting 77\\nnew setting 78\\nnew setting 79\\nnew setting 80\\nnew setting 81\\nnew setting 82\\nnew setting 83\\nnew setting 84\\nnew setting 85\\nnew setting 86\\nnew setting 87\\nnew setting 88\\nnew setting 89\\nnew setting 90\\nnew setting 91\\nnew setting 92\\nnew setting 93\\nnew setting 94\\nnew setting 95\\nnew setting 96\\nnew setting 97\\nnew setting 98\\nnew setting 99\\nnew setting 100\\nnew setting 101\\nnew setting 102\\nnew setting 103\\nnew setting 104\\nnew setting 105\\nnew setting 106\\nnew setting 107\\nnew setting 108\\nnew setting 109\\nnew setting 110\\nnew setting 111\\nnew setting 112\\nnew setting 113\\nnew setting 114\\nnew setting 115\\nnew setting 116\\nnew setting 117\\nnew setting 118\\nnew setting 119\\nnew setting 120\\nnew setting 121\\nnew setting 122\\nnew setting 123\\nnew setting 124\\nnew setting 125\\nnew setting 126\\nnew setting 127\\nnew setting 128\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"fs-edit-replace"},"content":[{"type":"tool-result","toolCallId":"fs-edit-replace","content":[{"type":"text","text":"The file {{cwd}}/large.txt has been updated successfully."}],"isError":false}],"role":"user","id":"{{message:7}}"},"meta":{"diffs":[{"path":"large.txt","oldText":"shared heading\nold setting 0\nold setting 1\nold setting 2\nold setting 3\nold setting 4\nold setting 5\nold setting 6\nold setting 7\nold setting 8\nold setting 9\nold setting 10\nold setting 11\nold setting 12\nold setting 13\nold setting 14\nold setting 15\nold setting 16\nold setting 17\nold setting 18\nold setting 19\nold setting 20\nold setting 21\nold setting 22\nold setting 23\nold setting 24\nold setting 25\nold setting 26\nold setting 27\nold setting 28\nold setting 29\nold setting 30\nold setting 31\nold setting 32\nold setting 33\nold setting 34\nold setting 35\nold setting 36\nold setting 37\nold setting 38\nold setting 39\nold setting 40\nold setting 41\nold setting 42\nold setting 43\nold setting 44\nold setting 45\nold setting 46\nold setting 47\nold setting 48\nold setting 49\nold setting 50\nold setting 51\nold setting 52\nold setting 53\nold setting 54\nold setting 55\nold setting 56\nold setting 57\nold setting 58\nold setting 59\nold setting 60\nold setting 61\nold setting 62\nold setting 63\nold setting 64\nold setting 65\nold setting 66\nold setting 67\nold setting 68\nold setting 69\nold setting 70\nold setting 71\nold setting 72\nold setting 73\nold setting 74\nold setting 75\nold setting 76\nold setting 77\nold setting 78\nold setting 79\nold setting 80\nold setting 81\nold setting 82\nold setting 83\nold setting 84\nold setting 85\nold setting 86\nold setting 87\nold setting 88\nold setting 89\nold setting 90\nold setting 91\nold setting 92\nold setting 93\nold setting 94\nold setting 95\nold setting 96\nold setting 97\nold setting 98\nold setting 99\nold setting 100\nold setting 101\nold setting 102\nold setting 103\nold setting 104\nold setting 105\nold setting 106\nold setting 107\nold setting 108\nold setting 109\nold setting 110\nold setting 111\nold setting 112\nold setting 113\nold setting 114\nold setting 115\nold setting 116\nold setting 117\nold setting 118\nold setting 119\nold setting 120\nold setting 121\nold setting 122\nold setting 123\nold setting 124\nold setting 125\nold setting 126\nold setting 127\nold setting 128","newText":"shared heading\nnew setting 0\nnew setting 1\nnew setting 2\nnew setting 3\nnew setting 4\nnew setting 5\nnew setting 6\nnew setting 7\nnew setting 8\nnew setting 9\nnew setting 10\nnew setting 11\nnew setting 12\nnew setting 13\nnew setting 14\nnew setting 15\nnew setting 16\nnew setting 17\nnew setting 18\nnew setting 19\nnew setting 20\nnew setting 21\nnew setting 22\nnew setting 23\nnew setting 24\nnew setting 25\nnew setting 26\nnew setting 27\nnew setting 28\nnew setting 29\nnew setting 30\nnew setting 31\nnew setting 32\nnew setting 33\nnew setting 34\nnew setting 35\nnew setting 36\nnew setting 37\nnew setting 38\nnew setting 39\nnew setting 40\nnew setting 41\nnew setting 42\nnew setting 43\nnew setting 44\nnew setting 45\nnew setting 46\nnew setting 47\nnew setting 48\nnew setting 49\nnew setting 50\nnew setting 51\nnew setting 52\nnew setting 53\nnew setting 54\nnew setting 55\nnew setting 56\nnew setting 57\nnew setting 58\nnew setting 59\nnew setting 60\nnew setting 61\nnew setting 62\nnew setting 63\nnew setting 64\nnew setting 65\nnew setting 66\nnew setting 67\nnew setting 68\nnew setting 69\nnew setting 70\nnew setting 71\nnew setting 72\nnew setting 73\nnew setting 74\nnew setting 75\nnew setting 76\nnew setting 77\nnew setting 78\nnew setting 79\nnew setting 80\nnew setting 81\nnew setting 82\nnew setting 83\nnew setting 84\nnew setting 85\nnew setting 86\nnew setting 87\nnew setting 88\nnew setting 89\nnew setting 90\nnew setting 91\nnew setting 92\nnew setting 93\nnew setting 94\nnew setting 95\nnew setting 96\nnew setting 97\nnew setting 98\nnew setting 99\nnew setting 100\nnew setting 101\nnew setting 102\nnew setting 103\nnew setting 104\nnew setting 105\nnew setting 106\nnew setting 107\nnew setting 108\nnew setting 109\nnew setting 110\nnew setting 111\nnew setting 112\nnew setting 113\nnew setting 114\nnew setting 115\nnew setting 116\nnew setting 117\nnew setting 118\nnew setting 119\nnew setting 120\nnew setting 121\nnew setting 122\nnew setting 123\nnew setting 124\nnew setting 125\nnew setting 126\nnew setting 127\nnew setting 128"}]}},"sourceEventSeqs":[19],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788699361070,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699361070,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788699361070,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788699361070,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788699361070,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":3}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -0,0 +1,7 @@
|
||||
version: 1
|
||||
scenario: diff-bounded
|
||||
profile: web
|
||||
composition: default
|
||||
recording: authored
|
||||
header:
|
||||
class: default
|
||||
@@ -0,0 +1,10 @@
|
||||
- button "Edit large.txt +130 -130" [expanded]:
|
||||
- img
|
||||
- text: Edit
|
||||
- button "large.txt"
|
||||
- text: +130 -130
|
||||
- button "Copy"
|
||||
- text: large.txt - shared heading - old setting 0 - old setting 1 - old setting 2
|
||||
- button "Expand 252 more diff lines": … 252 more lines
|
||||
- text: + new setting 125 + new setting 126 + new setting 127 + new setting 128 └ +130 -130 · 1 file
|
||||
- button "Inspect"
|
||||
Reference in new issue
Block a user