Show mixed or multi-file diff tool output as text instead of a blank "Invalid or empty patch" block
iamnbutler opened ·
In Canary 0.0.11, a bash tool result that contains a multi-file `git diff` mixed with other output renders as **"Invalid or empty patch"** followed by several screens of blank space. There is no raw-text fallback, so the result can't be audited in the chat.
## Observed
Channel `image-paste-review`, agent response at 11:33 AM. Expand the command group, then expand the bash call:
```sh git rev-parse origin/main HEAD; git diff origin/main; gh issue view 91 --repo githubnext/ace2 | head -40 ```
The expanded result shows only "Invalid or empty patch" followed by blank space. Nate confirmed this from both the native Accessibility text and a screenshot.
## Cause
The shell didn't fail. The tool result saved in the channel's database is intact: `isError: false`, 4,105 characters, 110 lines. It contains two commit SHAs, a valid two-file diff (`chat-composer.tsx` and `rich-text.tsx`), and then the `gh issue view` output.
The renderer treats the entire result as a single-file patch (all paths are at `origin/main` = `1ab91be`, canary 0.0.11):
1. `isPatch()` in `packages/ui/src/components/blocks/tool-layout.ts` matches any result that contains `diff --git`/`---`, `+++`, and `@@` lines anywhere, so the whole mixed output gets routed to `diffBlock(data.result)`. 2. `parseDiff()` in `packages/ui/src/lib/highlighter.ts` calls `@pierre/diffs` `getSingularPatch`. That function throws `FileDiff: Provided patch must contain exactly 1 file diff` for this result. `parsePatchFiles` sees 1 patch with 2 files. `parseDiff` catches the error and returns `null`. 3. With `parsed === null`, `diff()` in `packages/ui/src/components/blocks/diff.tsx` still sizes the block from the raw line count: 110 × 20 px + 12 ≈ **2,212 px**. `Diff` (`packages/ui/src/components/diff/diff.tsx`) then renders the "Invalid or empty patch" box at that height, which produces the blank space. The raw text is never shown.
I reproduced this against the saved result with the bundled `@pierre/diffs` 1.2.10. Each file's diff parses on its own. A related silent loss: if the output has a *single*-file diff plus surrounding text, `getSingularPatch` succeeds but renders only the hunk lines. In that case the SHAs and the `gh` output disappear without any error.
## Expected
- Tool results are always readable. If rich diff parsing fails, show the result as plain (diff-highlighted) text, the same way other results are shown. - Non-diff output around a diff (command output, SHAs, issue text) is never dropped. Either render diff segments richly and keep the surrounding text in order, or fall back to text for the whole result. - Multi-file diffs either render every file or fall back to text. A diff must never be treated as invalid just because it contains more than one file. - The block height matches the content that is actually rendered. A failed parse never reserves a large blank height. - The full raw output can still be audited even when a rich rendering is shown.
Related: #54 (tool results and full output in the channel), #99 (plain-language account with an auditable trail). Dogfooding core/QOL: #5.
iamnbutlerparent-issue-addedlast month on GitHub
iamnbutlerreferenced this in [Meta] Dogfood Ace end to end #5last month on GitHub
iamnbutlerreferenced this in Show a live, plain-language account of agent progress, outcomes, and blockers #99last month on GitHub
iamnbutlerreferenced this in Show live tool progress in the channel #54last month on GitHub
iamnbutleradded label buglast month on GitHub