# Diff View, @File-Mention & Clipboard Copy Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Give `edit`/`write` real unified diffs (with colored rendering in chat), add fuzzy `@file` mention autocomplete to the chat input, and add an OSC52 clipboard-copy hotkey for the last assistant message — three independent, small-to-medium features bundled into one plan per user request. **Architecture:** Diff view wraps `similar`'s unified-diff output in a ` ```diff ` fenced block returned by `edit`/`write`, rendered through an extended `markdown.rs` that keeps diff-line colors even in the "dim" tool-output mode. File mention reuses `ignore::Walk` (already used by `search.rs`) to build a background-populated file index, fuzzy-matched via `nucleo-matcher`, spliced into the input buffer at the `@` trigger. Clipboard copy sets a `pending_clipboard_copy` state field in `handle_key` (which may run daemon-side) and the actual OSC52 terminal write happens wherever the real terminal lives (single-process loop, or the attach client after a new `DaemonFrame::ClipboardCopy`). **Tech Stack:** Rust, ratatui/crossterm TUI, `similar` (new), `nucleo-matcher` (new), `ignore` (existing), `base64` (existing). ## Global Constraints - New dependencies: `similar = "3"`, `nucleo-matcher = "0.3"`. No other new dependencies (clipboard uses the existing `base64` crate). - `MAX_DIFF_LINES = 200` — diffs longer than this are truncated with `"... ({N} more lines truncated)"`. - Mention index cap: 50,000 entries across all workspace roots. - Every `pub fn`/`pub struct`/`pub enum` needs a doc comment (CLAUDE.md convention); non-trivial private functions (≥10 lines) too. - Tests are inline `#[cfg(test)] mod tests` blocks in the same file, per CLAUDE.md — no separate `tests/` directory. - No compiler/clippy bypass attributes (`#[allow(dead_code)]` etc.) to silence warnings — fix the underlying issue instead. - Specs: `docs/superpowers/specs/2026-07-15-diff-view-design.md`, `docs/superpowers/specs/2026-07-15-file-mention-design.md`, `docs/superpowers/specs/2026-07-15-clipboard-osc52-design.md`. --- ## Part A — Diff view for `edit`/`write` ### Task 1: Add `similar` dependency **Files:** - Modify: `Cargo.toml` **Interfaces:** - Produces: `similar::TextDiff`, `similar::udiff::UnifiedDiff` available to `src/tool/fs/*.rs`. - [ ] **Step 1: Add the dependency** In `Cargo.toml`, in the `[dependencies]` block, add this line right after `pulldown-cmark = { version = "0.13", default-features = false }`: ```toml similar = "3" ``` - [ ] **Step 2: Verify it builds** Run: `cargo check` Expected: compiles with no errors (a new `similar` entry appears in `Cargo.lock`). - [ ] **Step 3: Commit** ```bash git add Cargo.toml Cargo.lock git commit -m "chore: Tambah dependency similar untuk diff computation" ``` --- ### Task 2: Shared diff-truncation helper **Files:** - Modify: `src/tool/fs/helpers.rs` **Interfaces:** - Produces: `pub const MAX_DIFF_LINES: usize`, `pub fn truncate_diff(diff: &str) -> String`. - [ ] **Step 1: Write the failing tests** Add to the existing `#[cfg(test)] mod tests` block in `src/tool/fs/helpers.rs` (after the existing `test_arg_str_null` test, before the closing `}`): ```rust #[test] fn test_truncate_diff_under_limit_unchanged() { let diff = "line1\nline2\nline3"; assert_eq!(truncate_diff(diff), diff); } #[test] fn test_truncate_diff_over_limit_truncates() { let diff = (0..250).map(|i| format!("line{i}")).collect::>().join("\n"); let result = truncate_diff(&diff); assert!(result.contains("... (50 more lines truncated)")); assert_eq!(result.lines().count(), MAX_DIFF_LINES + 1); } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `cargo test tool::fs::helpers::tests -- truncate_diff` Expected: FAIL with "cannot find function `truncate_diff`" / "cannot find value `MAX_DIFF_LINES`" - [ ] **Step 3: Implement `truncate_diff`** Add this above the `#[cfg(test)]` line in `src/tool/fs/helpers.rs` (after the existing `not_found_help` function): ```rust /// Maximum number of lines a diff block may contain before being truncated. pub const MAX_DIFF_LINES: usize = 200; /// Cap a unified diff at `MAX_DIFF_LINES` lines, appending a truncation note. /// /// Return: `diff` unchanged if it's within the limit; otherwise the first /// `MAX_DIFF_LINES` lines followed by `"... ({N} more lines truncated)"`. pub fn truncate_diff(diff: &str) -> String { let lines: Vec<&str> = diff.lines().collect(); if lines.len() <= MAX_DIFF_LINES { return diff.to_string(); } let remaining = lines.len() - MAX_DIFF_LINES; format!("{}\n... ({remaining} more lines truncated)", lines[..MAX_DIFF_LINES].join("\n")) } ``` - [ ] **Step 4: Run tests to verify they pass** Run: `cargo test tool::fs::helpers::tests` Expected: PASS (all tests in the module, old and new) - [ ] **Step 5: Commit** ```bash git add src/tool/fs/helpers.rs git commit -m "feat: Tambah helper truncate_diff untuk membatasi panjang diff" ``` --- ### Task 3: Embed diff in `edit` tool **Files:** - Modify: `src/tool/fs/edit.rs` **Interfaces:** - Consumes: `helpers::truncate_diff` (Task 2), `similar::TextDiff::from_lines`, `.unified_diff().context_radius(3).header(a, b)` (Display). - Produces: `edit`'s success message now contains a ` ```diff ` fenced block instead of a byte-delta note. - [ ] **Step 1: Write the failing tests** Add at the end of `src/tool/fs/edit.rs` (there is no existing test module in this file): ```rust #[cfg(test)] mod tests { use super::*; fn test_ctx(workspace: std::path::PathBuf) -> crate::tool::ToolCtx { crate::tool::ToolCtx::builder().workspaces(vec![workspace]).build() } fn temp_workspace() -> std::path::PathBuf { let dir = std::env::temp_dir().join(format!("zesdex-edit-test-{}", uuid::Uuid::new_v4())); fs::create_dir_all(&dir).unwrap(); dir } #[test] fn edit_returns_a_diff_block_for_a_single_replace() { let workspace = temp_workspace(); fs::write(workspace.join("a.txt"), "line1\nline2\nline3\n").unwrap(); let ctx = test_ctx(workspace.clone()); let args = json!({ "path": "a.txt", "old": "line2", "new": "changed", "reason": "test edit" }); let result = Edit.run(&ctx, &args).unwrap(); assert!(result.contains("```diff")); assert!(result.contains("-line2")); assert!(result.contains("+changed")); fs::remove_dir_all(&workspace).ok(); } #[test] fn edit_truncates_a_very_large_diff() { let workspace = temp_workspace(); let old_content: String = (0..300).map(|i| format!("line{i}\n")).collect(); let new_content: String = (0..300).map(|i| format!("changed{i}\n")).collect(); fs::write(workspace.join("big.txt"), &old_content).unwrap(); let ctx = test_ctx(workspace.clone()); let args = json!({ "path": "big.txt", "old": &old_content, "new": &new_content, "reason": "test large replace" }); let result = Edit.run(&ctx, &args).unwrap(); assert!(result.contains("more lines truncated")); fs::remove_dir_all(&workspace).ok(); } } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `cargo test tool::fs::edit::tests` Expected: FAIL — `edit_returns_a_diff_block_for_a_single_replace` fails because the current message contains "byte delta" not "```diff"; `edit_truncates_a_very_large_diff` fails the same way. - [ ] **Step 3: Add the `similar` import and `helpers` module import** In `src/tool/fs/edit.rs`, change line 12 from: ```rust use super::helpers::arg_str; ``` to: ```rust use super::helpers::{self, arg_str}; use similar::TextDiff; ``` - [ ] **Step 4: Replace the byte-delta message with a diff block** Replace this block (currently lines 100-121): ```rust fs::write(&path, &new_content) .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; let bytes_diff = if new_content.len() > content.len() { new_content.len() - content.len() } else { content.len() - new_content.len() }; // Notify the LSP server of the on-disk change so diagnostics stay fresh. // Never fail the edit because of this — LSP errors are surfaced as a // trailing annotation on the success message instead. let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() { lsp.did_change_file(&path); String::new() } else { String::new() }; if check_matches.is_empty() { Ok(format!("edited {} ({} byte delta){}", rel, bytes_diff as isize, lsp_note)) } else { Ok(format!("edited {} ({} byte delta). Graduated checks matched: {}{}", rel, bytes_diff as isize, check_matches.join(", "), lsp_note)) } } } ``` with: ```rust fs::write(&path, &new_content) .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; let text_diff = TextDiff::from_lines(content.as_str(), new_content.as_str()); let diff_text = format!( "{}", text_diff.unified_diff().context_radius(3).header(&rel, &rel) ); let diff_block = format!("```diff\n{}\n```", helpers::truncate_diff(&diff_text)); // Notify the LSP server of the on-disk change so diagnostics stay fresh. // Never fail the edit because of this — LSP errors are surfaced as a // trailing annotation on the success message instead. let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() { lsp.did_change_file(&path); String::new() } else { String::new() }; if check_matches.is_empty() { Ok(format!("edited {rel}\n{diff_block}{lsp_note}")) } else { Ok(format!("edited {rel}. Graduated checks matched: {}\n{diff_block}{lsp_note}", check_matches.join(", "))) } } } ``` - [ ] **Step 5: Run tests to verify they pass** Run: `cargo test tool::fs::edit::tests` Expected: PASS - [ ] **Step 6: Commit** ```bash git add src/tool/fs/edit.rs git commit -m "feat: Tampilkan unified diff pada hasil tool edit" ``` --- ### Task 4: Embed diff in `write` tool (overwrite case) **Files:** - Modify: `src/tool/fs/write.rs` **Interfaces:** - Consumes: `helpers::truncate_diff` (Task 2), `similar::TextDiff` (same API as Task 3). - Produces: `write`'s success message gets an appended ` ```diff ` block when overwriting an existing UTF-8 file; unchanged for new files or non-UTF-8 overwrites. - [ ] **Step 1: Write the failing tests** Add at the end of `src/tool/fs/write.rs`: ```rust #[cfg(test)] mod tests { use super::*; fn test_ctx(workspace: std::path::PathBuf) -> crate::tool::ToolCtx { crate::tool::ToolCtx::builder().workspaces(vec![workspace]).build() } fn temp_workspace() -> std::path::PathBuf { let dir = std::env::temp_dir().join(format!("zesdex-write-test-{}", uuid::Uuid::new_v4())); fs::create_dir_all(&dir).unwrap(); dir } #[test] fn write_to_a_new_file_has_no_diff_block() { let workspace = temp_workspace(); let ctx = test_ctx(workspace.clone()); let args = json!({"path": "new.txt", "content": "hello\n", "reason": "test new file"}); let result = Write.run(&ctx, &args).unwrap(); assert!(result.contains("wrote 6 bytes")); assert!(!result.contains("```diff")); fs::remove_dir_all(&workspace).ok(); } #[test] fn write_overwriting_an_existing_utf8_file_includes_a_diff_block() { let workspace = temp_workspace(); fs::write(workspace.join("existing.txt"), "old content\n").unwrap(); let ctx = test_ctx(workspace.clone()); let args = json!({"path": "existing.txt", "content": "new content\n", "reason": "test overwrite"}); let result = Write.run(&ctx, &args).unwrap(); assert!(result.contains("```diff")); assert!(result.contains("-old content")); assert!(result.contains("+new content")); fs::remove_dir_all(&workspace).ok(); } #[test] fn write_overwriting_a_non_utf8_file_has_no_diff_block() { let workspace = temp_workspace(); fs::write(workspace.join("binary.dat"), [0xFFu8, 0xFE, 0xFD]).unwrap(); let ctx = test_ctx(workspace.clone()); let args = json!({"path": "binary.dat", "content": "now text\n", "reason": "test binary overwrite"}); let result = Write.run(&ctx, &args).unwrap(); assert!(!result.contains("```diff")); assert!(result.contains("wrote")); fs::remove_dir_all(&workspace).ok(); } } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `cargo test tool::fs::write::tests` Expected: FAIL — `write_overwriting_an_existing_utf8_file_includes_a_diff_block` fails (no diff block exists yet); the other two pass already (current behavior already matches them), which is fine. - [ ] **Step 3: Add the `similar` import** In `src/tool/fs/write.rs`, change line 10 from: ```rust use super::helpers::arg_str; ``` to: ```rust use super::helpers::{self, arg_str}; use similar::TextDiff; ``` - [ ] **Step 4: Capture old content and append a diff block** Replace this block (currently lines 60-81): ```rust let path = resolve_path(&ctx.workspaces, &rel)?; if let Some(parent) = path.parent() { fs::create_dir_all(parent) .map_err(|e| anyhow!("failed to create parent directories for '{rel}': {e}"))?; } fs::write(&path, &content) .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; // Notify the LSP server of the on-disk change so diagnostics stay in // sync. Never fails the write itself: a lock failure or LSP error is // folded into the returned message instead of propagated as an Err. let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() { lsp.did_change_file(&path); String::new() } else { String::new() }; if check_matches.is_empty() { Ok(format!("wrote {} bytes to {}{}", content.len(), rel, lsp_note)) } else { Ok(format!("wrote {} bytes to {}{}. Graduated checks matched: {}", content.len(), rel, lsp_note, check_matches.join(", "))) } } } ``` with: ```rust let path = resolve_path(&ctx.workspaces, &rel)?; let old_content = fs::read_to_string(&path).ok(); if let Some(parent) = path.parent() { fs::create_dir_all(parent) .map_err(|e| anyhow!("failed to create parent directories for '{rel}': {e}"))?; } fs::write(&path, &content) .map_err(|e| anyhow!("failed to write '{rel}': {e}"))?; // Notify the LSP server of the on-disk change so diagnostics stay in // sync. Never fails the write itself: a lock failure or LSP error is // folded into the returned message instead of propagated as an Err. let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() { lsp.did_change_file(&path); String::new() } else { String::new() }; // Only emit a diff when the file existed before and was valid UTF-8; // new files and binary overwrites fall back to the byte-count message. let diff_note = if let Some(old) = old_content { let text_diff = TextDiff::from_lines(old.as_str(), content.as_str()); let diff_text = format!( "{}", text_diff.unified_diff().context_radius(3).header(&rel, &rel) ); format!("\n```diff\n{}\n```", helpers::truncate_diff(&diff_text)) } else { String::new() }; if check_matches.is_empty() { Ok(format!("wrote {} bytes to {}{}{}", content.len(), rel, lsp_note, diff_note)) } else { Ok(format!("wrote {} bytes to {}{}. Graduated checks matched: {}{}", content.len(), rel, lsp_note, check_matches.join(", "), diff_note)) } } } ``` - [ ] **Step 5: Run tests to verify they pass** Run: `cargo test tool::fs::write::tests` Expected: PASS - [ ] **Step 6: Commit** ```bash git add src/tool/fs/write.rs git commit -m "feat: Tampilkan unified diff saat tool write menimpa file yang sudah ada" ``` --- ### Task 5: Diff-aware coloring in `markdown.rs` **Files:** - Modify: `src/view/markdown.rs` **Interfaces:** - Consumes: `Theme::SUCCESS`, `Theme::ERROR`, `Theme::INFO`, `Theme::TEXT_DIM`, `Theme::ACCENT_TEAL`, `Theme::CODE_BG` (all exist in `src/view/theme.rs`). - Produces: `pub fn render_markdown(text: &str, width: u16, dim: bool) -> Vec>` (signature change — was `(text: &str, width: u16)`). Diff lines (` ```diff ` fenced blocks) keep their color even when `dim = true`; every other span falls back to `Theme::TEXT_DIM` + italic when `dim = true`, and is unchanged from today's behavior when `dim = false`. - [ ] **Step 1: Write the failing tests** Add at the end of `src/view/markdown.rs` (there is no existing test module in this file): ```rust #[cfg(test)] mod tests { use super::*; fn span_text(spans: &[Span<'static>]) -> String { spans.iter().map(|s| s.content.as_ref()).collect() } #[test] fn dim_false_plain_text_has_no_color() { let spans = render_markdown("hello world", 0, false); assert_eq!(span_text(&spans), "hello world"); assert_eq!(spans[0].style, Style::default()); } #[test] fn dim_true_plain_text_is_dim_italic() { let spans = render_markdown("hello", 0, true); let expected = Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC); assert_eq!(spans[0].style, expected); } #[test] fn dim_true_diff_lines_keep_their_own_color() { let md = "```diff\n@@ -1,2 +1,2 @@\n-old line\n+new line\n context line\n```"; let spans = render_markdown(md, 0, true); let plus_span = spans.iter().find(|s| s.content.contains("+new line")).expect("plus span present"); assert_eq!(plus_span.style.fg, Some(Theme::SUCCESS)); let minus_span = spans.iter().find(|s| s.content.contains("-old line")).expect("minus span present"); assert_eq!(minus_span.style.fg, Some(Theme::ERROR)); let hunk_span = spans.iter().find(|s| s.content.contains("@@")).expect("hunk header span present"); assert_eq!(hunk_span.style.fg, Some(Theme::INFO)); } #[test] fn dim_true_non_diff_code_block_is_dimmed() { let md = "```rust\nfn main() {}\n```"; let spans = render_markdown(md, 0, true); let code_span = spans.iter().find(|s| s.content.contains("fn main")).expect("code span present"); assert_eq!(code_span.style, Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)); } } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `cargo test view::markdown::tests` Expected: FAIL to compile — `render_markdown` takes 2 arguments, not 3, at every call site in the new tests. - [ ] **Step 3: Add the `apply_dim` and `diff_line_style` helpers** Add these two functions right above `pub fn render_markdown` in `src/view/markdown.rs`: ```rust /// Apply the "tool output" dim/italic style, or pass `style` through /// unchanged, depending on `dim`. fn apply_dim(style: Style, dim: bool) -> Style { if dim { Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC) } else { style } } /// Classify a single line inside a ` ```diff ` fenced block by its unified-diff /// prefix, returning the color it should always render with (even when the /// surrounding tool output is dimmed) — or `None` for context lines and the /// `+++`/`---` file-header lines, which use the normal code-block color. fn diff_line_style(line: &str) -> Option