feat(web): show edit diffs inline in tool call cards - #1103
Conversation
Render a line-by-line diff inside Edit/Write tool cards in the web chat, reusing the existing diff-line style from the changes panel. The diff is built client-side from the tool input, so no protocol or server changes are needed; the header chip now shows real +/- counts.
🦋 Changeset detectedLatest commit: cc92e55 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7a2665d66b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| <template v-if="editDiff"> | ||
| <div v-if="editDiff.length > 0" class="bb-diff"><DiffLines :lines="editDiff" /></div> | ||
| <div v-else class="bb-empty">No changes.</div> | ||
| </template> |
There was a problem hiding this comment.
Preserve failed edit/write output
When an Edit/Write call fails (for example Edit returns old_string not found), editDiff is still derived from the requested input, so this branch renders the proposed diff and suppresses tool.output. The card shows an error mark but hides the actual diagnostic, making a failed edit look like a successful change preview; gate the diff view on successful status or fall back to the output on errors.
Useful? React with 👍 / 👎.
Diff rows were only as wide as the viewport, so the add/del background stopped where long lines overflowed and the area revealed by horizontal scroll had no color. Size each row to its content so the background paints the full line.
Size the diff container to the longest line and have every row fill it, so add/del backgrounds form one continuous band across the whole horizontal scroll instead of stopping at each line's own length.
Use Array.at(-1) and Array.from instead of index access and new Array(length), which oxlint flags as errors.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78ff1e1f27
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| <template v-if="editDiff"> | ||
| <div v-if="editDiff.length > 0" class="bb-diff"><DiffLines :lines="editDiff" /></div> |
There was a problem hiding this comment.
Preserve tool output when edit/write calls fail
For failed Edit/Write calls with normal JSON args, editDiff is still truthy, so the expanded card takes this branch and never renders tool.output, which is where the actual failure reason lives (for example old_string not found or a write error). This makes an error card show the requested diff instead of why no change was applied; gate the diff view to successful calls or render the error output for tool.status === 'error'.
Useful? React with 👍 / 👎.
# Conflicts: # apps/kimi-web/src/components/chat/ToolCall.vue
When an Edit/Write call fails, render the tool output (the failure reason) instead of the requested diff, so error cards explain why no change was applied.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e0ef1959d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (kind === 'edit') { | ||
| const before = typeof d.old_string === 'string' ? d.old_string : undefined; | ||
| const after = typeof d.new_string === 'string' ? d.new_string : undefined; | ||
| if (before === undefined || after === undefined) return null; | ||
| return buildDiffLines(before, after); |
There was a problem hiding this comment.
Handle replace_all before showing a single-edit diff
When an Edit call includes replace_all: true, the core tool replaces every occurrence and reports the actual occurrence count, but this path builds a preview from only one old_string/new_string pair and the expanded card then suppresses tool.output. In sessions where the agent intentionally replaces multiple occurrences, the web card will show a one-occurrence diff and chip counts such as +1 −1, hiding the real Replaced N occurrences... result.
Useful? React with 👍 / 👎.
| const content = typeof d.content === 'string' ? d.content : undefined; | ||
| if (content === undefined) return null; | ||
| return buildDiffLines('', content); |
There was a problem hiding this comment.
Do not render append writes as new-file diffs
For Write calls with mode: 'append', this always diffs from an empty file, so appended chunks are displayed as if they start at line 1 of a newly written file while the actual Appended ... bytes output is hidden by the diff branch. This makes append writes to existing files misleading; skip the synthetic diff for append mode or preserve the tool output in that case.
Useful? React with 👍 / 👎.
A single-pair diff misrepresents replace_all edits (which change many occurrences) and from-empty diffs misrepresent append writes (which extend an existing file). Skip the synthetic diff in those cases and render the truthful tool output instead.
Move the Edit/Write diff out of the inline tool-card body into the shared right-side detail layer, opened by clicking the card (like the subagent detail panel). The panel shows the line diff when it faithfully represents the operation, otherwise the tool output (replace_all, append, errors), so failed calls explain why no change was applied.
|
❌ Nix build failed |
…again Clicking the same Edit/Write card, file link, or git-status area a second time now collapses the right-side detail panel, matching the existing thinking / compaction / subagent toggle behavior.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b23ebdb5af
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return; | ||
| } | ||
| detailTarget.value = 'toolDiff'; | ||
| toolDiffTarget.value = target; |
There was a problem hiding this comment.
Keep open tool diffs tied to live tool state
When an Edit/Write card is opened before the tool finishes, this stores the current payload as a snapshot, so later status/output changes from the same tool call are not reflected in the already-open panel. In that scenario append/replace_all calls stay on “No diff”, and a tool that later errors can continue showing the attempted diff instead of the failure output until the user closes and reopens it; keep only the live tool id/lookup or refresh the stored target when the tool updates.
Useful? React with 👍 / 👎.
| const m = newLines.length; | ||
| if (n === 0 && m === 0) return []; | ||
|
|
||
| const dp: number[][] = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }, () => 0)); |
There was a problem hiding this comment.
Cap client-side LCS for large edit payloads
Rendering an Edit card now computes chip() from editDiff, which calls this LCS builder even before the user opens the panel. For an Edit that replaces a large block or whole generated file, this allocates an (oldLines + 1) * (newLines + 1) matrix (for example, 5k changed lines is 25M cells) and can freeze or exhaust the web chat/history view; add a line/size cap or use a cheaper diff/fallback for large inputs.
Useful? React with 👍 / 👎.
The line-level LCS allocates an (oldLines+1) x (newLines+1) matrix and runs eagerly when an Edit/Write card renders. For large edits (e.g. a 5k x 5k block, ~25M cells) this can freeze or exhaust the chat. Cap the matrix at 1M cells and fall back to the raw tool output beyond that.
Store only the tool id and re-derive the diff panel payload from the live tool call in the session turns, so a panel opened while the tool is still running reflects later status/output changes (e.g. an eventual error shows the failure output instead of the stale attempted diff).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5950c7c9bf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (d.mode === 'append') return null; | ||
| const content = typeof d.content === 'string' ? d.content : undefined; | ||
| if (content === undefined) return null; | ||
| return buildDiffLines('', content); |
There was a problem hiding this comment.
Avoid treating Write overwrites as new-file diffs
When a Write call targets an existing file, the tool defaults to overwrite mode, but this builds the preview from an empty before-state, so the card/panel reports every resulting line as added and no removed lines. In the common complete-replacement case the +/- chip and diff are therefore materially inaccurate; if the client cannot know the previous contents, fall back to the tool output for Write overwrites unless the operation is known to be a new file.
Useful? React with 👍 / 👎.
Write only reports the new content, so the client cannot tell a new file from an overwrite of an existing one. A from-empty diff showed overwrites as all additions and no deletions, which is misleading. Fall back to the tool output for every Write (append already did).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c66ad2179f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const n = oldLines.length; | ||
| const m = newLines.length; | ||
| if (n === 0 && m === 0) return []; | ||
| if ((n + 1) * (m + 1) > MAX_DIFF_CELLS) return null; |
There was a problem hiding this comment.
Cap diff rows as well as LCS cells
When an Edit replaces a short old_string with a very large new_string (for example one line to hundreds of thousands of lines), this product check still passes, so buildDiffLines constructs a huge row array; ToolCall computes that diff just to render the chip, so the chat can freeze before the user even opens the panel. Add a total old/new line or output-row cap in addition to the matrix-size cap.
Useful? React with 👍 / 👎.
The matrix-size cap alone let through asymmetric edits (e.g. one line replaced by hundreds of thousands) whose small matrix still produced a huge row array that the chip computed on render. Cap each side's line count too, so the diff output is bounded.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0975c95194
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (isEditWrite.value) { | ||
| emit('openToolDiff', props.tool.id); | ||
| return; |
There was a problem hiding this comment.
Preserve expansion when no diff panel handler exists
When an Edit/Write tool card is rendered inside SideChatPanel, the nested ChatPane is not wired with @open-tool-diff (checked apps/kimi-web/src/components/chat/SideChatPanel.vue, where the ChatPane only receives turns/approvals/running/sending). This early return therefore makes those BTW side-chat cards do nothing on click, so users can no longer expand the card to see the tool output or error; only the main conversation path handles the new event.
Useful? React with 👍 / 👎.
…ired Clicking an Edit/Write card opens the right-side diff panel, but the nested ChatPane in the side chat does not wire that event, so the click became a no-op and the card could no longer expand to show its output. Gate the panel behavior behind a toolDiffPanel prop (enabled only in the main conversation); elsewhere the cards expand inline as before.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ae9e0d65a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (detailTarget.value === 'thinking' && thinkingVisible.value) { closeThinkingPanel(); return true; } | ||
| if (detailTarget.value === 'compaction' && compactionPanelVisible.value) { closeCompactionPanel(); return true; } | ||
| if (detailTarget.value === 'agent' && agentPanelVisible.value) { closeAgentPanel(); return true; } | ||
| if (detailTarget.value === 'toolDiff' && toolDiffVisible.value) { closeToolDiff(); return true; } |
There was a problem hiding this comment.
Gate the tool diff panel on a live target
The new toolDiffVisible is only used here, but sidePanelVisible does not gate detailTarget === 'toolDiff' on it. In a same-session resync (the WebSocket resync path replaces messagesBySession[sessionId] from a fresh snapshot), an opened diff for a tool call that is no longer in the latest snapshot makes toolDiffTarget null while detailTarget stays 'toolDiff'; the aside remains open with no ToolDiffPanel/close button, and this Escape branch refuses to close it because toolDiffVisible is false. Please either include the tool-diff visibility check in sidePanelVisible or clear the tool-diff target when it disappears.
Useful? React with 👍 / 👎.
When a session resync removes the tool call an open diff panel is showing, toolDiffTarget becomes null but detailTarget stayed 'toolDiff', leaving an empty aside that Escape would not close. Gate sidePanelVisible on toolDiffVisible so the panel collapses once its target is gone.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc92e55b89
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const lines = s.split('\n'); | ||
| // A trailing newline produces a trailing empty element that is not a real | ||
| // content line — drop exactly one of them. | ||
| if (lines.at(-1) === '') lines.pop(); |
There was a problem hiding this comment.
Preserve terminal newline-only edits
When an Edit changes only the final newline, dropping the trailing empty split entry makes the old and new line arrays identical (for example old_string: "a", new_string: "a\n"). The new tool diff panel then renders only context and the +/- chip reports no change, even though the file content did change; keep enough information to show EOF-newline additions/removals or a no-newline marker.
Useful? React with 👍 / 👎.
Related Issue
N/A — no linked issue.
Problem
When the agent edits or writes a file in the web chat, the tool card only showed a one-line summary (e.g. "Replaced 1 occurrence(s) in path") as plain text. Users could not see what actually changed without opening the file, unlike the CLI/TUI which renders the diff inline.
What changed
Render a line-by-line diff inside Edit/Write tool cards in the web chat, reusing the existing diff-line visual style from the changes panel.
Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.