From 3f284cbb9a30c459b7a7021fd1c37d11e0c2b0e2 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 15 Jul 2026 04:44:51 +0700 Subject: [PATCH] docs: Tambah spec desain diff view untuk tool edit/write --- .../specs/2026-07-15-diff-view-design.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-15-diff-view-design.md diff --git a/docs/superpowers/specs/2026-07-15-diff-view-design.md b/docs/superpowers/specs/2026-07-15-diff-view-design.md new file mode 100644 index 0000000..44e7eae --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-diff-view-design.md @@ -0,0 +1,115 @@ +# Diff View for edit/write Tools — Design + +**Status:** Approved, pending implementation plan +**Date:** 2026-07-15 +**Scope:** `src/tool/fs/edit.rs`, `src/tool/fs/write.rs`, `src/view/markdown.rs`, `src/view/chat.rs` + +## Context + +`edit` currently reports only a byte-delta (`"edited {rel} ({N} byte delta)"`), and `write` +reports only a byte count. Neither the model nor the user sees what actually changed — +just a number. This makes it hard for the model to self-verify an edit landed correctly, +and hard for the user to review a change without opening the file. No diff-computing +library exists in the dependency tree today. + +## Goals + +- `edit` returns a real unified diff (git-style, 3 lines of context) of the change it just + made, in place of the byte-delta note. +- `write` returns the same kind of diff when it overwrites a file that already existed + with valid UTF-8 content; falls back to the current "wrote N bytes" message for new + files or non-UTF-8 (binary) overwrites. +- Diffs render in the chat view with real color (green add / red remove / cyan hunk + header) instead of being flattened to dim/italic like other tool output. +- Large diffs are truncated with a trailing count, matching the existing pattern in + `read.rs` (`"... ({N} more lines, total {total})"`). + +## Non-goals + +- No diff view for any tool besides `edit`/`write` (e.g. no retroactive diffing of + `bash_tools.rs` shell edits). +- No side-by-side diff layout — unified format only, matching how every other tool + output already renders as a single text stream. +- No persistence of diff history; each diff is only the delta of the single tool call + that produced it, not a cumulative session diff. +- No changes to non-tool (assistant/user/system) message rendering or coloring. + +## Dependency + +Add `similar = "3"` (line/word diff crate; permissive MIT/Apache-2.0, no heavy +transitive deps). Use `TextDiff::from_lines(old, new).unified_diff().context_radius(3)`, +which produces standard `@@ -a,b +c,d @@` hunk headers and `-`/`+`/` `-prefixed lines — +no custom diff algorithm needed. + +## Tool changes + +### `edit.rs` + +After computing `new_content` and writing it to disk: + +1. Compute `similar::TextDiff::from_lines(&content, &new_content).unified_diff().context_radius(3).to_string()`. +2. Split into lines; if `> MAX_DIFF_LINES` (200), keep the first 200 and append + `"... ({N} more lines truncated)"`. +3. Wrap the (possibly truncated) diff text in a fenced ` ```diff ` block. +4. Replace the byte-delta note in the returned message with this block; keep the + existing "Graduated checks matched" / LSP note suffixes in their current position + (after the diff block). + +### `write.rs` + +Before overwriting: + +1. If `path.exists()` and `fs::read_to_string(&path)` succeeds (valid UTF-8), capture it + as `old_content` and note `is_overwrite = true`. +2. If the file doesn't exist, or reading it fails (binary/non-UTF-8), `is_overwrite = false` + — no error, just skip the diff path silently. +3. After writing, if `is_overwrite`, compute and truncate the diff exactly as in `edit.rs` + and append the fenced block to the return message (in addition to the existing + "wrote N bytes" line, not instead of it — for `write`, unlike `edit`, the byte count is + still useful since it can be a full-file rewrite). +4. If not `is_overwrite`, return message is unchanged from today. + +The truncation constant (`MAX_DIFF_LINES = 200`) and truncation message format are +shared — factor into a small helper in `tool/fs/helpers.rs` used by both tools. + +## Rendering changes + +### `markdown.rs` + +- `render_markdown` gains a `dim: bool` parameter: `render_markdown(text, width, dim)`. +- Capture the fence language from `Tag::CodeBlock(CodeBlockKind::Fenced(lang))` (today + matched as `CodeBlock(_)`, discarding the language). Track `in_diff_block: bool` when + `lang == "diff"`. +- Inside a diff block, process text line-by-line instead of as one blob: a line starting + with `+` (not `+++`) is styled green, `-` (not `---`) red, `@@` cyan/muted, everything + else (context lines, `+++`/`---` file headers) uses the existing code-block teal. +- When `dim` is `true`: every span keeps its assigned color as computed above, but + non-diff spans (headings, links, plain text, non-diff code blocks, table cells) fall + back to `Theme::TEXT_DIM` + `Modifier::ITALIC` instead of their normal palette color — + this replicates today's "tool output is always dim" behavior for everything except + diff lines. +- When `dim` is `false`: behavior is unchanged from today (full color, used for + assistant/user/system messages). + +### `chat.rs` + +- `Role::Tool` branch: replace the two manual span-remapping loops (that force every + span to `dim_italic`) with a direct call to `render_markdown(&content, content_width, true)` + and use the returned spans as-is. +- All other roles: call `render_markdown(&content_str, content_width, false)` — same + call as today, just with the new explicit `false` argument. + +## Testing + +Inline `#[cfg(test)] mod tests` per CLAUDE.md convention: + +- `edit.rs`: a normal single-replace edit produces a diff block with matching + `-`/`+` lines; a `replace_all` across 250+ lines truncates at 200 with the correct + trailing count. +- `write.rs`: writing a brand-new file keeps the old "wrote N bytes" message with no + diff block; overwriting an existing UTF-8 file produces a diff block; overwriting + a path that reads as invalid UTF-8 (simulate via non-UTF-8 bytes) falls back to the + byte-count message without erroring. +- `markdown.rs`: a fenced ` ```diff ` block with `+`/`-`/`@@` lines produces spans with + the expected fg colors under `dim=true` (diff lines colored) and confirms non-diff + text in the same call falls back to `TEXT_DIM` + italic.