Skip to content

feat(app): native diff renderer, replacing the Pierre-based one - #1728

Open
Scoteezy wants to merge 8 commits into
slopus:mainfrom
Scoteezy:new-file-diff-mobile
Open

feat(app): native diff renderer, replacing the Pierre-based one#1728
Scoteezy wants to merge 8 commits into
slopus:mainfrom
Scoteezy:new-file-diff-mobile

Conversation

@Scoteezy

@Scoteezy Scoteezy commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Diffs were rendered two different ways: a React Native renderer for most surfaces and @pierre/diffs for the web split view, with a second, older line-diff module feeding the +N/−M counts. Stats and rendering could therefore disagree, and the Codex patch detail screen had no renderer registered at all, so it fell back to dumping the tool payload as raw JSON.

Everything now goes through one engine that parses a patch, tokenizes and word-diffs it once, and hands the render layer immutable rows. @pierre/diffs is dropped.

What changed

  • Split (side-by-side) layout in the native renderer, so the Pierre dependency can go. Both cells share one flex row, so a wrapped line grows the row and the two sides cannot drift apart.
  • Full-screen views for CodexPatch/CodexDiff, and the collapse toggle that hid every Codex diff behind an extra tap is gone.
  • Added and deleted files render correctly. Codex reuses the diff field for the whole file body on add/delete, which parsed as zero hunks and showed as "no changes".
  • Changed-line counts for new files. git status reports none for untracked files, so every new file claimed zero.
  • A trailing newline no longer becomes a blank row at the end of every diff.
  • Long files mount over a few frames instead of all at once. Measured on an iPhone 17 simulator: 2017 rows go from 414 ms to 179 ms to first paint, and the UI thread holds 60 fps while the rest arrives. Chunks use setTimeout, not requestAnimationFrame, because a backgrounded tab stops firing frames and leaves the file silently truncated.
  • Collapse never cuts between removals and the additions that replaced them, which side by side reads as deleted code.
  • A session Changes screen, and chat diff cards open the full-screen diff for that file. Both were previously reachable only from the wide-screen sidebar.
  • Expandable context, an ignore-whitespace toggle, and image diffs. Tapping an "N unchanged lines" separator re-fetches with full context; whitespace-only changes fold away via git diff -w; images render as before/after pictures instead of "binary file". SVG is deliberately left to the text renderer.
  • The changes screen opens with every file collapsed, as a list of what changed.
  • The +N/−M badges count staged and untracked lines too. They read a bare git diff --numstat, so the count collapsed the moment an agent ran git add, and files the agent created counted as zero — the same gap already fixed inside the changes view. Untracked lines now come from wc -l over git ls-files --others, and every surface reads one shared helper.
  • Happy Agent SDK tool calls render as one-line rows. TaskCreate, create_agent, web_fetch and their siblings had no entry in knownTools, so each fell back to a half-screen card of raw input JSON. Unknown tools now take that compact row on every flavor, not just Gemini; failed ones still expand.

Shell paths are now escaped as well — a filename containing a quote or a backtick previously broke the command apart.

Testing

pnpm typecheck is clean and vitest passes (115 files, 1216 tests). Coverage includes patch parsing, split pairing, collapse and gutter geometry, the Codex payload shapes, untracked line counting, tool activity labels, and render tests over a mocked React Native.

Verified by hand on an iPhone 17 simulator against a live Codex session — the patch detail screen, inline chat diffs, the changes screen, context expansion, the whitespace toggle and image diffs were each exercised on real data.

Diffs were rendered two different ways: a React Native renderer for most
surfaces and @pierre/diffs for the web split view, with a second, older
line-diff module feeding the +N/-M counts. Stats and rendering could
therefore disagree, and the Codex patch detail screen had no renderer at
all, so it fell back to dumping the tool payload as raw JSON.

Everything now goes through one engine that parses a patch, tokenizes and
word-diffs it once, and hands the render layer immutable rows.

- Add split (side-by-side) layout so the Pierre dependency can go. Both
  cells share one flex row, so a wrapped line grows the row and the two
  sides cannot drift apart.
- Register full-screen views for CodexPatch/CodexDiff and drop the
  collapse toggle that hid every Codex diff behind an extra tap.
- Read added and deleted files correctly: Codex reuses the `diff` field
  for the whole file body on add/delete, which parsed as zero hunks and
  rendered as "no changes".
- Stop a trailing newline from becoming a blank row at the end of a diff.
- Mount long files over a few frames instead of all at once. Measured on
  an iPhone 17 simulator: 2017 rows go from 414ms to 179ms to first
  paint, and the UI thread holds 60fps while the rest arrives. Chunks
  are scheduled with setTimeout, not rAF, because a backgrounded tab
  stops firing frames and leaves the file silently truncated.
- Never cut a collapse boundary between removals and the additions that
  replaced them, which side by side reads as deleted code.
- Add a session Changes screen and make chat diff cards open the
  full-screen diff for that file; both were previously reachable only
  from the wide-screen sidebar.

Covered by pure tests for patch parsing, split pairing, collapse and
gutter geometry, plus render tests over a mocked React Native.
A patch tool card carried a header naming the file, and then each diff
inside repeated the same name in its own header — a box inside a box
saying the same thing twice. The web build already hid the outer header;
do it everywhere, for Gemini patches too.
`git status` carries no line counts for untracked files, so every new
file claimed zero changed lines: no +N in its header, and a collapsed
section offering to expand "0 changed lines". Recount from the content
once it arrives — from the patch for tracked files, from the blob for
untracked ones — so the header always matches the diff under it.
Three things the removed Pierre renderer or GitHub could do and this one
could not. All three come down to asking git a different question, so the
command now has one builder instead of being inlined at each call site.

- Tapping an "N unchanged lines" separator re-fetches the file with full
  context. A patch does not carry the lines it skipped, so the separator
  is only tappable where the caller can ask for a wider diff — in chat,
  where the file has already moved on, it stays inert rather than lying.
- A header toggle folds whitespace-only changes away via `git diff -w`,
  which turns a reformatted file from a wall of red and green into the
  handful of lines that actually changed.
- Images render as before/after pictures instead of "binary file", read
  from HEAD and from disk. SVG is deliberately left to the text renderer,
  where it diffs properly.

Paths are now escaped for the shell as well; a filename containing a
quote or a backtick previously broke the command apart.
A changeset of thirty files opened as thirty diffs stacked end to end,
which is a lot of scrolling before you know what is even in it. It now
opens as a list of what changed — path, kind and counts — and a file
opens when you ask for it.

The "N changed lines — tap to expand" row is kept only where the size
forced the collapse; when everything starts closed it explains nothing
and doubles the height of the list, so the header's own chevron carries
it instead.
@Scoteezy
Scoteezy force-pushed the new-file-diff-mobile branch from 85696a6 to 10938dc Compare August 26, 2026 12:50
TaskCreate, create_agent, web_fetch and their siblings had no entry in knownTools and no view of their own, so each one fell back to a half-screen card of raw input JSON.

Describe the SDK tools in knownTools so they render as one-line activity rows, and let getToolSummaryDetail read the subject-like fields they use (subject, title, query, name) instead of only paths and commands.

Unknown tools now take the same compact row on every flavor rather than only on Gemini: providers keep adding tools faster than we describe them. Tools that failed still expand, since the payload is the only thing explaining the error.
The badges read unstagedLinesAdded/Removed, which come from a bare 'git diff --numstat'. That misses two things: staged hunks, so the count collapsed the moment an agent ran 'git add', and untracked files, which git diff never reports at all, so every file the agent created counted as zero lines.

Count untracked lines with 'git ls-files --others --exclude-standard -z | xargs -0 wc -l' and fold them into the unstaged additions, skipping wc's per-batch total line and binary-looking paths whose newline counts mean nothing. The pipeline needs a POSIX shell, so on Windows it fails and untracked lines stay uncounted, as they are everywhere today.

Show staged + unstaged everywhere through one helper, so the session status bar, the badges and the project rows agree on a single number.
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.

1 participant