diff --git a/.agents/notes/implemented/feature/2026-07-30-web-diff-card.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-web-diff-card.i18n.yaml
index 7ed620736c..f9b2cfa908 100644
--- a/.agents/notes/implemented/feature/2026-07-30-web-diff-card.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-30-web-diff-card.i18n.yaml
@@ -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/feature/2026-07-30-web-diff-card.md
-2026-07-30-web-diff-card.md: 8087ce698e65f78c7c6f51211ef00e3b0ab58ed9
-2026-07-30-web-diff-card.zh.md: d85ac1f2e13c7fb3732b327b40122076337ac538
+2026-07-30-web-diff-card.md: 396bdbc2843c1bbed5c6a913be436d8b9e96a81c
+2026-07-30-web-diff-card.zh.md: afdeafa6e94b46b4f0fbd4a065afdac8a93ac57d
diff --git a/.agents/notes/implemented/feature/2026-07-30-web-diff-card.md b/.agents/notes/implemented/feature/2026-07-30-web-diff-card.md
index 8087ce698e..396bdbc284 100644
--- a/.agents/notes/implemented/feature/2026-07-30-web-diff-card.md
+++ b/.agents/notes/implemented/feature/2026-07-30-web-diff-card.md
@@ -16,11 +16,12 @@ This is the [terminal card](2026-07-28-web-terminal-card.md) done for the `diff`
`DiffBlock` is a `ui-primitives` component that renders a file mutation as an inline diff surface, and both Web render sites for a write/edit call consume the diff render intent through it: the chat tool row's body and the details panel's Output section. `ui-conversation/src/client/contract/diff-card-model.ts` is the single place that turns the snapshot's `callView`/`resultView` pair into the component's props, so the two sites cannot disagree about a change. It returns null — the generic path — whenever neither side declares `card: 'diff'`, including a `card` value this client version does not know, and whenever a settled call's result view is generic, which is how write/edit keep their execution errors on the generic path. The result side is authoritative once the call settles: the applied hunks replace the call-time diff derived from the arguments alone. A paging window that drops the call head still renders, because the result view carries the whole change.
-The component's contract follows the TUI's `diffLines` (`packages/ui/tui/src/components/transcript.ts`) so a diff reads the same shape across front ends, with one deliberate divergence noted below (the file count):
+The component's contract follows the TUI's `diffLines` (`packages/ui/tui/src/components/transcript.ts`) so a diff reads the same shape across front ends:
-- **One path header per file.** A new file opens a bold path header; a same-file second hunk (a scattered edit, or a `replace_all`) opens with a `⋯` gap instead of repeating the path. The `N file(s)` footer counts DISTINCT paths — the divergence from the TUI, whose footer uses `diffs.length` and so reads two hunks in one file as `2 files` where this reads `1 file`.
+- **One path header per file.** A new file opens a bold path header; a same-file second hunk (a scattered edit, or a `replace_all`) opens with a `⋯` gap instead of repeating the path. The `N file(s)` footer counts DISTINCT paths on both front ends — this PR moved the TUI footer off `diffs.length` onto the distinct-path count, so two hunks in one file read as `1 file` in both.
- **The change in the diff's own colors.** A removed line is `- ` on the error token, an added line is `+ ` on the success token, drawn verbatim with `white-space: pre` inside a horizontally scrolling box — a source line is read by its indentation, so it scrolls rather than folds. A create (`oldText: null`) has no removed side.
- **Height cap with an expand control.** A diff longer than `DEFAULT_DIFF_MAX_LINES` (16) shows `ceil(max/2)` head rows plus the remaining tail rows, with a button between reporting the hidden count. The split arithmetic matches `TerminalBlock` and the TUI's collapsed card, so a long diff's head and tail slices agree across front ends.
+- **Line terminator.** A side's content splits on `\n` under the terminator rule `TerminalBlock` uses: empty text is zero lines (a full deletion's `newText`, a create's absent `oldText` side), a single trailing newline terminates its last line rather than adding a phantom empty one, and an interior blank line survives. This PR applied the same rule to the TUI diff branch, so the `+A -R` footer counts agree on both front ends for the newline-terminated content real write/edit calls carry.
- **Footer and copy.** A dim `└ +A -R · N file(s)` footer summarizes the change; `+A -R` are the added/removed line counts, the same per-side counts the TUI footer draws. The copy control copies the prefixed diff text (path headers, `- `/`+ ` lines, the `⋯` gap), so a multi-file copy stays attributable.
Geometry, radius, and fonts mirror `CodeBlock`/`TerminalBlock` so a diff card, a terminal card, and a fenced block read as one family; `white-space: pre` plus horizontal scroll is the deliberate divergence. The copy control floats in the card's top-right corner rather than on a banner row of its own, because a banner carrying only a copy button drew an empty band above the first diff line — the TUI diff card has no banner either, only the footer.
diff --git a/.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md b/.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md
index d85ac1f2e1..afdeafa6e9 100644
--- a/.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md
@@ -16,11 +16,12 @@ Web 客户端忽略了它。write/edit 调用落到 `GenericToolCard`,其行
`DiffBlock` 是一个 `ui-primitives` 组件,把文件改动渲染为内联 diff 表面,write/edit 调用的两个 Web 渲染点都通过它消费 diff 渲染意图:chat 工具行的行体和详情面板的 Output 区。`ui-conversation/src/client/contract/diff-card-model.ts` 是唯一把快照的 `callView`/`resultView` 对转成组件 props 的地方,因此两个渲染点不会对一次改动产生分歧。当两侧都未声明 `card: 'diff'` 时它返回 null —— 走通用路径 —— 包括本客户端版本不认识的 `card` 值,以及已结算调用的 result view 是 generic 的情况(write/edit 的执行错误正是这样留在通用路径上的)。调用结算后 result 侧是权威:已应用的 hunk 替换仅从参数推导的 call 时 diff。分页窗口丢弃了 call 头也仍能渲染,因为 result view 携带完整改动。
-组件的契约遵循 TUI 的 `diffLines`(`packages/ui/tui/src/components/transcript.ts`),使 diff 在两个前端读起来是同一形态,仅文件计数一处刻意分歧(见下):
+组件的契约遵循 TUI 的 `diffLines`(`packages/ui/tui/src/components/transcript.ts`),使 diff 在两个前端读起来是同一形态:
-- **每个文件一个路径头。** 新文件开启一个粗体路径头;同文件的第二个 hunk(分散编辑,或 `replace_all`)以一个 `⋯` gap 开启,而非重复路径。`N file(s)` 页脚统计**去重后的路径数** —— 这是与 TUI 的分歧:TUI 页脚用 `diffs.length`,同文件两个 hunk 在那里读作 `2 files`,此处读作 `1 file`。
+- **每个文件一个路径头。** 新文件开启一个粗体路径头;同文件的第二个 hunk(分散编辑,或 `replace_all`)以一个 `⋯` gap 开启,而非重复路径。`N file(s)` 页脚在两个前端都统计**去重后的路径数** —— 本 PR 把 TUI 页脚从 `diffs.length` 改为去重路径计数,因此同文件两个 hunk 在两端都读作 `1 file`。
- **改动用 diff 自身的颜色。** 删除行是 error token 上的 `- `,新增行是 success token 上的 `+ `,在横向滚动的盒子里以 `white-space: pre` 逐字绘制 —— 源码行靠缩进阅读,所以滚动而不折行。新建(`oldText: null`)没有删除侧。
- **高度上限带展开控件。** 长于 `DEFAULT_DIFF_MAX_LINES`(16)的 diff 显示 `ceil(max/2)` 个头部行加剩余尾部行,中间一个按钮报告隐藏行数。分割算术与 `TerminalBlock` 和 TUI 的折叠卡片一致,因此长 diff 的头尾切片在两个前端一致。
+- **行终止符。** 每一侧的内容按 `TerminalBlock` 的终止符规则在 `\n` 上切分:空文本是零行(整文件删除的 `newText`、新建缺失的 `oldText` 侧),单个结尾换行终止其最后一行而非新增一条幻影空行,内部空行保留。本 PR 把同一规则应用到了 TUI diff 分支,因此对于真实 write/edit 调用携带的以换行结尾的内容,两个前端的 `+A -R` 页脚计数一致。
- **页脚与复制。** 暗色 `└ +A -R · N file(s)` 页脚概括改动;`+A -R` 是新增/删除行数,与 TUI 页脚绘制的每侧计数相同。复制控件复制带前缀的 diff 文本(路径头、`- `/`+ ` 行、`⋯` gap),使多文件复制保持可归属。
几何、圆角、字体镜像 `CodeBlock`/`TerminalBlock`,使 diff 卡片、terminal 卡片、代码块读起来是一家;`white-space: pre` 加横向滚动是刻意的分歧。复制控件浮在卡片右上角,而非占据自己的 banner 行,因为只放一个复制按钮的 banner 会在第一行 diff 上方画出一条空带 —— TUI 的 diff 卡片也没有 banner,只有页脚。
diff --git a/apps/web/tests/built-boot.snapshot.ts b/apps/web/tests/built-boot.snapshot.ts
index 69d5d5cfae..0e1d58f965 100644
--- a/apps/web/tests/built-boot.snapshot.ts
+++ b/apps/web/tests/built-boot.snapshot.ts
@@ -108,6 +108,17 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull()
}, { timeout: 10_000 })
+ // The write/edit turns render a real diff card through the assembled graph
+ // (the keyed FileMutationRow + DiffBlock), not just the fixture's raw text.
+ // The write turn's `hello fixture\n` proves the terminator rule end to end: a
+ // trailing newline terminates its line, so the footer reads `+1` (not a
+ // phantom `+2`) and one distinct file.
+ const diffCards = document.querySelectorAll('[data-diff]')
+ expect(diffCards.length).toBeGreaterThan(0)
+ const footers = [...document.querySelectorAll('[data-diff]')]
+ .map(card => card.textContent ?? '')
+ expect(footers.some(text => text.includes('+ hello fixture') && text.includes('+1 -0 · 1 file'))).toBe(true)
+
// Every bundle injected its plugin-owned style tag (the loader's CSS path).
const styleOwners = [...document.head.querySelectorAll('style[data-plugin]')]
.map(style => style.getAttribute('data-plugin'))
diff --git a/packages/client/ui-conversation/src/client/chat/ToolRow.module.css b/packages/client/ui-conversation/src/client/chat/ToolRow.module.css
index e53b472c50..f1d3e7dd7e 100644
--- a/packages/client/ui-conversation/src/client/chat/ToolRow.module.css
+++ b/packages/client/ui-conversation/src/client/chat/ToolRow.module.css
@@ -113,14 +113,15 @@
color: var(--dsw-alias-label-tertiary);
}
-/* The two block-shaped expanded bodies: the code variant's run_code program
- through CodeBlock (shiki-highlighted TypeScript) and a terminal card's
- command output through TerminalBlock. Both are drawn by the shared
- primitive, so only the row's indentation is this file's concern — the margin
- also replaces each primitive's own standalone vertical spacing with the
- flow's row rhythm. */
+/* The block-shaped expanded bodies: the code variant's run_code program through
+ CodeBlock (shiki-highlighted TypeScript), a terminal card's command output
+ through TerminalBlock, and a write/edit diff through DiffBlock. All are drawn
+ by a shared primitive, so only the row's indentation is this file's concern —
+ the margin also replaces each primitive's own standalone vertical spacing with
+ the flow's row rhythm. Card-neutral: it carries no terminal- or diff-specific
+ value, so it fits every block body. */
.codeBody,
-.terminalBody {
+.cardBody {
margin: 4px 0 4px 22px;
}
diff --git a/packages/client/ui-conversation/src/client/chat/ToolRow.tsx b/packages/client/ui-conversation/src/client/chat/ToolRow.tsx
index 52238788d1..b89f537230 100644
--- a/packages/client/ui-conversation/src/client/chat/ToolRow.tsx
+++ b/packages/client/ui-conversation/src/client/chat/ToolRow.tsx
@@ -83,9 +83,10 @@ export function ToolRow({
// A row that names a single file keeps one interaction (open that path);
// args expand is off whether or not the open callback is wired yet. A card
// body (terminal or diff) still expands: only the file variants carry a
- // path. A write/edit row carries both a file path and a diff card, so its
- // path link and its expandable card coexist — the card expands, the summary
- // stays a link.
+ // path. A write/edit row carries both a file path and a diff card, so both
+ // the path link and the expandable card are offered — the collapsed row shows
+ // the path link, and expanding swaps it for the card body (DisclosureRow
+ // renders collapsedContent only while closed).
const singleFile = filePath !== undefined
const fileLink = singleFile && onOpenFile !== undefined
const cardBody = terminalBody !== null || diffBody !== null
@@ -138,9 +139,9 @@ export function ToolRow({
}
diff --git a/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.module.css b/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.module.css
index 143174fe42..994ae718cb 100644
--- a/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.module.css
+++ b/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.module.css
@@ -101,8 +101,9 @@
font: var(--dsw-font-xs-13);
}
-/* The terminal card sits directly under its section label, so it drops the
- primitive's standalone vertical margin; the section owns the spacing. */
-.terminal {
+/* A card body (terminal or diff) sits directly under its section label, so it
+ drops the primitive's standalone vertical margin; the section owns the
+ spacing. Card-neutral: no terminal- or diff-specific value. */
+.cardBody {
margin: 0;
}
diff --git a/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx b/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx
index d09d3ea256..5a5e19179c 100644
--- a/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx
+++ b/packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx
@@ -146,12 +146,12 @@ function OutputBody({ material, cwd }: { material: CallMaterial; cwd: string | u
{terminal.description !== undefined && (
{terminal.description}
)}
-
+
>
)
}
const diff = diffCardModel(material.block)
- if (diff !== null) return
+ if (diff !== null) return
// A settled call always carries the result node the flattened form needs;
// the running shape has no result to flatten.
if (!('kind' in material.block)) return
运行中…
diff --git a/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx b/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx
index e777c78932..323a73e77c 100644
--- a/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx
+++ b/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx
@@ -17,7 +17,7 @@ import type { Context } from 'cordis'
import { DiffBlock, IconEditOutline16, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ToolRowProps } from '../contract/slots.ts'
import { CHAT_DIFF_MAX_LINES, diffCardModel } from '../contract/diff-card-model.ts'
-import { resolveToolPath, toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
+import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
import css from './file-mutation-row.module.css'
function leadingFor(state: ToolRowState) {
@@ -63,8 +63,9 @@ function errorText(block: ToolRowProps['block']): string | null {
/**
* File-mutation row: icon + {Edit,Write} · {path} in the shared ToolRow chrome,
* with the applied diff resident below it. The summary is a path link (a file
- * tool's interaction) resolved against the session cwd and opened through the
- * host; the card's copy and expand controls are the row's only other actions.
+ * tool's interaction); the host's `openFile` resolves it against the session
+ * cwd, so this passes the tool's own path verbatim. The card's copy and expand
+ * controls are the row's only other actions.
*/
export function FileMutationRow({ toolName, block, cwd, openFile }: ToolRowProps) {
const model = toolRowModel(toolName, block, cwd)
@@ -85,7 +86,7 @@ export function FileMutationRow({ toolName, block, cwd, openFile }: ToolRowProps
diff --git a/packages/client/ui-primitives/src/DiffBlock.module.css b/packages/client/ui-primitives/src/DiffBlock.module.css
index 8794dbb87e..c5b79006a3 100644
--- a/packages/client/ui-primitives/src/DiffBlock.module.css
+++ b/packages/client/ui-primitives/src/DiffBlock.module.css
@@ -46,10 +46,14 @@
white-space: pre;
}
-/* A file header: the path in the primary tone, set apart by weight. */
+/* A file header: the path in the primary tone, set apart by weight. The copy
+ button floats over this first row's top-right corner, so reserve space at the
+ line's end for it — a long path scrolls under the button otherwise, and the
+ button's hit area would eat clicks on the path's tail. */
.path {
color: var(--dsw-alias-label-primary);
font-weight: 600;
+ padding-right: 56px;
}
/* A same-file second hunk's separator (a scattered edit), in the dim tone. */
diff --git a/packages/client/ui-primitives/src/DiffBlock.tsx b/packages/client/ui-primitives/src/DiffBlock.tsx
index 5ae28bc9b2..23c498b1d1 100644
--- a/packages/client/ui-primitives/src/DiffBlock.tsx
+++ b/packages/client/ui-primitives/src/DiffBlock.tsx
@@ -4,9 +4,10 @@
// color), with a dim `└ +A -R · N file(s)` footer. The +/- block form mirrors
// the TUI transcript's diff card (packages/ui/tui: diffLines) so a diff reads
// the same across front ends: the removed side is the old text in full, the
-// added side the new text in full. Output never soft-wraps — an aligned source
-// line keeps its indentation and scrolls horizontally instead of folding.
-// Colors resolve through --dsw-* tokens; geometry mirrors CodeBlock.
+// added side the new text in full, both split on the same terminator rule, and
+// the footer counts distinct paths on both ends. Output never soft-wraps — an
+// aligned source line keeps its indentation and scrolls horizontally instead of
+// folding. Colors resolve through --dsw-* tokens; geometry mirrors CodeBlock.
import { useCallback, useMemo, useState } from 'react'
import clsx from 'clsx'
@@ -68,9 +69,8 @@ const ROW_CLASS: Record = {
* opens each new file; a same-file second hunk (a scattered edit) opens with a
* `⋯` gap instead of repeating the path. Every old-side line counts toward
* `removed` and every new-side line toward `added`. The file count is of
- * DISTINCT paths, which is the one deliberate divergence from the TUI diff card:
- * the TUI footer uses `diffs.length`, so two hunks in one file read there as
- * `2 files`, whereas this counts the one file they belong to.
+ * DISTINCT paths, matching the TUI diff card's footer, so two hunks in one file
+ * read as `1 file` on both front ends.
* @param diffs - the hunks to render.
* @returns the body rows, the +/- totals, and the distinct-file count.
*/
diff --git a/packages/ui/tui/src/components/transcript.ts b/packages/ui/tui/src/components/transcript.ts
index 58d3d6a178..c9a505bbb2 100644
--- a/packages/ui/tui/src/components/transcript.ts
+++ b/packages/ui/tui/src/components/transcript.ts
@@ -52,15 +52,28 @@ function pretty(value: unknown): string {
return displayText(serialized ?? String(value))
}
+/**
+ * A side's content lines under the terminator rule the Web DiffBlock also
+ * applies: empty text is zero lines (a full deletion's `newText`, a create's
+ * absent `oldText`), and a single trailing newline terminates the last line
+ * rather than adding an empty one. An interior blank line survives. Keeping the
+ * two front ends on the same rule holds their `+A -R` footers in step.
+ */
+function diffContentLines(text: string): string[] {
+ if (text === '') return []
+ const body = text.endsWith('\n') ? text.slice(0, -1) : text
+ return body.split('\n')
+}
+
/** A file diff as colored `+`/`-` lines, optionally prefixed with its path. */
function diffLines(diff: FileDiff, palette: Palette): string[] {
// The card header is a fixed `Tool / ` frame that never names a file, so
// each hunk always carries its own path header (no redundancy to suppress).
const lines = [palette.bold(displayText(diff.path))]
if (diff.oldText !== null) {
- for (const line of displayText(diff.oldText).split('\n')) lines.push(palette.error(`- ${line}`))
+ for (const line of diffContentLines(displayText(diff.oldText))) lines.push(palette.error(`- ${line}`))
}
- for (const line of displayText(diff.newText).split('\n')) lines.push(palette.success(`+ ${line}`))
+ for (const line of diffContentLines(displayText(diff.newText))) lines.push(palette.success(`+ ${line}`))
return lines
}
@@ -488,15 +501,19 @@ export class ToolCardComponent implements Component {
}
if (view.card === 'diff') {
// The header no longer names the file, so each diff keeps its own path
- // header. A trailing footer summarizes the change (`+A -R · N file(s)`).
+ // header. A trailing footer summarizes the change (`+A -R · N file(s)`),
+ // on the same terminator rule and distinct-path count the Web DiffBlock
+ // uses, so the two front ends' footers agree.
let added = 0
let removed = 0
+ const paths = new Set()
const hunks = view.diffs.flatMap((diff, index) => {
- if (diff.oldText !== null) removed += displayText(diff.oldText).split('\n').length
- added += displayText(diff.newText).split('\n').length
+ paths.add(diff.path)
+ if (diff.oldText !== null) removed += diffContentLines(displayText(diff.oldText)).length
+ added += diffContentLines(displayText(diff.newText)).length
return [...index > 0 ? [''] : [], ...diffLines(diff, this.palette)]
})
- const files = view.diffs.length
+ const files = paths.size
const footer = this.palette.dim(`└ +${added} -${removed} · ${files} file${files === 1 ? '' : 's'}`)
// A diff's own `+`/`-` colors carry its meaning, so it renders verbatim
// rather than under the dim result-output color.
diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts
index d99780dedb..c03d40f098 100644
--- a/packages/ui/tui/tests/tui.spec.ts
+++ b/packages/ui/tui/tests/tui.spec.ts
@@ -4317,6 +4317,21 @@ describe('tool cards and surface replay', () => {
diffs: [{ path: 'src/only.ts', oldText: 'old', newText: 'new' }],
}),
},
+ scatteredDiff: {
+ name: 'scatteredDiff', description: '', parameters: {}, output: UNUSED_TOOL_OUTPUT, execute: async () => [],
+ // Two hunks in ONE file, each side ending in the terminator newline real
+ // write/edit content carries. The footer must read `+2 -0 · 1 file`: the
+ // trailing newline terminates its line rather than adding a phantom empty
+ // one, and the two hunks count as the single distinct path they touch.
+ presentCall: () => ({
+ card: 'diff',
+ title: 'Edit src/scatter.ts',
+ diffs: [
+ { path: 'src/scatter.ts', oldText: null, newText: 'first\n' },
+ { path: 'src/scatter.ts', oldText: null, newText: 'second\n' },
+ ],
+ }),
+ },
generic: {
name: 'generic', description: '', parameters: {}, output: UNUSED_TOOL_OUTPUT, execute: async () => [],
presentCall: () => ({ card: 'generic', title: 'Inspect value', rawInput: { alpha: 1 } }),
@@ -4621,6 +4636,29 @@ describe('tool cards and surface replay', () => {
await dispose(result)
})
+ it('counts a same-file diff once and terminates its trailing newline', async () => {
+ const result = await setup({ tools })
+ appendUser(result.session, 'scatter edits in one file')
+ appendAssistant(result.session, [
+ { type: 'text', text: 'Editing' },
+ { type: 'tool-call', id: 'scatter' as never, name: 'scatteredDiff', arguments: '{}' },
+ ])
+ result.session.append('tool/call', {
+ turn: 1, step: 1, callId: 'scatter' as never, name: 'scatteredDiff', arguments: '{}',
+ })
+ await tick()
+ const output = result.terminal.output
+ // Two hunks, one path: distinct-path count, same as the Web DiffBlock.
+ expect(output).toContain('· 1 file')
+ expect(output).not.toContain('· 2 files')
+ // The `first\n`/`second\n` sides each contribute exactly one added line —
+ // the trailing newline terminates rather than adding a phantom empty `+ `.
+ expect(output).toContain('+ first')
+ expect(output).toContain('+ second')
+ expect(output).toContain('+2 -0')
+ await dispose(result)
+ })
+
it('drops blank rows from a terminal card result that the dim styling wraps', async () => {
const blankRowTools: Record = {
trailing: {