Skip to content

feat(runtime): Edit/Write/FormatJson emit file_diff results for both renderers - #2264

Merged
Astro-Han merged 14 commits into
mainfrom
feat/tool-result-diff
Aug 5, 2026
Merged

feat(runtime): Edit/Write/FormatJson emit file_diff results for both renderers#2264
Astro-Han merged 14 commits into
mainfrom
feat/tool-result-diff

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #2232.

Edit, Write, and FormatJson now produce a unified diff at the write site — where both file contents are already in memory — and return it as the canonical file_diff tool-result kind. That kind has had consumers on every surface (desktop FileDiffPreview, TUI renderDiffResult/compactDiffSummary, storage schema) but no producer outside fixtures; this PR is the producer, which also makes the #2227 line-level diff rendering reachable in the product.

Decisions (from the issue's open questions):

  • Per call, not per turn. The diff answers "what did this call do".
  • The model gets a bounded summary, not the diff — everywhere. toModelOutput returns one line live (Edited a.ts (+1 -1), Created new.md (+2)), and the same summary is what the model sees on every later re-read: a replay-plan projection (the projectBashToolResultForModel precedent) keeps prior turns, compaction, and resume from materializing the stored full diff into the prompt.
  • Write on a new file renders the whole content as additions under --- /dev/null; overwrites read the previous content first (inside the per-file write lock). Only ENOENT/ENOTDIR counts as "new file" — an unreadable or binary previous state yields no diff rather than a false "Created".
  • Degradation: content over 800 lines / 32 KB or containing NUL bytes yields no diff; Write then falls back to a file_write descriptor (Wrote N bytes to <path> on every surface).
  • FormatJson is covered by the same seam and never leaks its diff through the stripped diagnostic json.

Rendering, on top of the raw diff becoming reachable:

  • All diff reading — desktop gutter panel, row +N/-N badges, TUI transcript, model summaries — goes through one structural parser in @maka/core, driven by the declared hunk counts: inside a hunk body the first character is the marker, so a deleted SQL -- a comment (--- a in the diff) or an added ++i (+++i) can no longer be hidden as a file header, desync line numbers, or escape the +/- counts. This replaced three divergent heuristic classifiers.
  • The desktop panel numbers every line in a left gutter (old-side for deletions, new-side otherwise); hunk headers feed the counters instead of rendering. The copyable body keeps the full standard diff. The TUI keeps @@ as its line-number anchor.
  • New Storybook variants (Product/Tool Activity → EditWriteDiffRows/EditWriteDiffDetails) cover the newly reachable states with self-consistent fixtures.

Deferred from review: matchedVia is no longer surfaced to the model when a diff exists (the file_diff contract is exact-shape; fuzzy matches are rare and unambiguous; the no-diff fallback path still returns it). Headless keeps its own result shapes (separate harness contract, no rendering surface).

Verification

  • Worker: edit/new-file/overwrite/format diffs, no-diff-when-unchanged, oversized degradation, unreadable-overwrite and image-overwrite reporting no diff.
  • Builtin tools: file_diff mapping, file_write fallback, no diff leaking as json, bounded toModelOutput incl. ++-prefixed content.
  • Replay: an Edit file_diff materializes as Edited a.ts (+1 -1) for the model while the durable event keeps the full diff.
  • Core parser: standard hunks, -- /++-prefixed content, \\ No newline markers, multi-file/multi-hunk numbering, foreign-diff degradation, header/hunk exclusion from counts.
  • Renderers: gutter numbering incl. cross-hunk resume, header hiding, -0 suppression, file_write descriptor; TUI expanded diff drops headers, keeps @@.
  • Suites green: core 787, runtime 3099, ui 386, cli 470, headless 1410; desktop typecheck + stories typecheck + story-annotations check; biome clean. CI: all checks pass.
  • Visual: EditWriteDiffRows/EditWriteDiffDetails reviewed against the live Storybook dev server after the parser migration.
  • Not run: desktop e2e (no e2e covers this path; branches are covered by contract tests and stories).

Edit, Write, and FormatJson now carry an optional unified diff on their
worker results, computed where both contents are already in memory. Write
reads the previous content before overwriting so an overwrite shows what
was lost; a new file renders as pure additions. Oversized or binary-ish
content degrades to no diff, preserving the existing summary fallback.
…tent

The three writing tools now return the canonical file_diff result kind
when the executor reports a diff, so both renderers (desktop FileDiffPreview
and the TUI diff view) finally receive real data. Write degrades to the
existing file_write descriptor when no diff is available. The model gets a
bounded one-line summary via toModelOutput — path plus addition/deletion
counts — instead of the full diff or the old anonymous json blob.
ToolTrow counts +/- lines from a file_diff result and passes them to the
Astryx ChatToolCalls row, which already knew how to paint green +N and red
-N but never received them.
The ---/+++ file markers and index hashes repeat what the diff panel's
heading and the tool card already say, so both renderers now drop them
visually; the @@ hunk header stays as the only line-number anchor, and
diff --git stays as the in-body boundary between files. The durable
result and the desktop copyable body keep the full standard unified diff.
A pure-additions write painted a red -0 next to its green +N; zero counts
now stay unpainted.
Covers the states #2232 makes reachable in the product: an Edit row, a
new-file Write (pure additions), and an overwrite Write, as collapsed
rows with +/- stats and as expanded diff panels.
Each rendered diff line now carries its line number in a left gutter —
old-side for deletions, new-side for additions and context — counted from
the @@ hunk headers, which no longer render as rows of their own; a jump
in the numbering is what marks a hunk boundary. A foreign diff with no
hunk headers degrades to unnumbered rows, and the copyable body still
holds the full standard unified diff.
Parsing driven by the declared hunk counts, not per-line prefix heuristics:
inside a hunk body the first character is the marker, so a deletion of a
SQL -- comment (--- a in the diff) or an added ++i (+++i) can no longer be
mistaken for a file header — the bug class that hid real change lines,
desynced line numbers, and undercounted +/- stats across three heuristic
copies.
… replay

The durable ledger keeps the full diff for the UI, but every model re-read
of history — prior turns, compaction, resume — materializes the stored
result, so after the first editing turn the model received the entire diff
as JSON on each request despite seeing a bounded summary live. Project
file_diff/file_write results to the same one-line summary at the replay
plan seam, following the projectBashToolResultForModel precedent. The
summary counting now comes from the shared structural parse in @maka/core,
so an added ++i line is counted rather than skipped as a false header.
…erwrites

Read-before-write treated every read failure as a missing file, so a
write-only file (or an image on the executor path) overwritten by Write
was reported as --- /dev/null with the model told 'Created <path>'. Only
ENOENT/ENOTDIR now means new file; any other unreadable or binary previous
state yields no diff.
The chat diff panel and row badges now consume parseUnifiedDiffRows /
countDiffLineStats from @maka/core, deleting the local heuristic parser
and counter. Hunk-body classification can no longer hide a deleted
-- -prefixed line as a file header or desync the gutter after one, and
zero-count badges stay unpainted. A file_write result renders as
'Wrote N bytes to <path>' instead of a bare [file_write] tag.
…rser

tui-diff shrinks to coloring over parseUnifiedDiffRows; the transcript's
expanded diff drops file headers as a parse result rather than a separate
heuristic filter, and compactDiffSummary counts via countDiffLineStats.
The CLI's bare-prefix classifier (which dimmed a deleted YAML --- line as
a header) is gone.
…ation

The edit fixture's hunk header now matches its own body (7/8) and no
longer shows a removal line that never existed in the repo; the two other
fixtures were already consistent. EditWriteDiffDetails names its real
path, fixing the story-annotations CI check.
@Astro-Han
Astro-Han marked this pull request as ready for review August 5, 2026 17:42
@Astro-Han
Astro-Han merged commit deaf122 into main Aug 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Edit and Write produce no diff — neither for the reader nor for the model

1 participant