# Colored diff output for edit_file

## Current state

- `_edit_file` returns just `"Edited {file_path}"` on success
- `fmt.tool_result()` shows a one-line summary (checkmark, tool name, elapsed time, preview)
- The user has no visibility into *what* changed — only that something changed

## Goal

When `edit_file` succeeds, show a colored unified-diff-style output on stderr so the user can see exactly what was replaced.

## Plan

### 1. Add `fmt.tool_diff()` to `fmt.py`

```python
def tool_diff(file_path: str, old: str, new: str) -> None
```

Uses `difflib.unified_diff` to generate the diff, then prints it to the Rich stderr console with coloring:

- `---`/`+++` header lines: bold
- `@@` hunk headers: cyan
- `-` (removed) lines: red
- `+` (added) lines: green
- Context lines: dim

Build output using `rich.text.Text` objects (not raw strings) to avoid bracket-as-markup injection. Every line appended via `Text(line, style=...)` or `Text(escape(line))`.

**Truncation policy:** cap at both 50 lines *and* 4 KB of diff text, whichever hits first. If truncated, append a dim `"... N more lines"` note.

### 2. Capture before/after in `_edit_file` (`tools.py`)

`_edit_file` already reads the file content before calling `replace()`. Save that as `old_content`. `replace()` returns the new content — use that directly instead of re-reading the file (avoids extra I/O and a race window).

If `replace()` currently doesn't return the new content, change it to do so. The return is internal (`edit.py` → `tools.py`), no public contract affected.

### 3. Call the formatter from `_edit_file`

In `tools.py:_edit_file`, after a successful `replace()` call:

```python
if verbose:
    fmt.tool_diff(file_path, old_content, new_content)
```

This lives in `_edit_file` itself — not in `agent.py`. Reason: `dispatch()` returns only a `str`, so there's no clean way to pass old/new content across the dispatch boundary without new plumbing. Doing it in `_edit_file` is self-contained.

### 4. Thread `verbose` through the call chain

The full propagation path:

1. **`agent.py:~1045`** — where `dispatch()` is called. Pass `verbose=verbose` as an extra kwarg.
2. **`tools.py:dispatch()`** — accept `verbose=False` parameter. When `name == "edit_file"`, forward it to `_edit_file(..., verbose=verbose)`.
3. **`tools.py:_edit_file()`** — accept `verbose=False` parameter. Only call `fmt.tool_diff()` when `verbose is True`.

Default is `False` at every level, so:

- Direct calls to `_edit_file` (e.g., from tests) never emit stderr unless explicitly opted in
- If `dispatch()` is called without `verbose` (e.g., from a future code path), diff is suppressed — safe default
- Quiet mode in agent.py simply doesn't pass `verbose=True`, so no diff output

### 5. Tests

| Test | File | What it covers |
|------|------|----------------|
| `tool_diff` formatting | `tests/test_fmt.py` | Correct coloring, line classification, output structure |
| `tool_diff` truncation | `tests/test_fmt.py` | Both line-count and byte-size caps, "N more lines" note |
| `tool_diff` markup safety | `tests/test_fmt.py` | Input containing `[brackets]` doesn't break Rich |
| No diff in quiet mode | `tests/test_tools.py` | `_edit_file(verbose=False)` produces no stderr |
| Return value unchanged | `tests/test_tools.py` | `_edit_file` still returns `"Edited {path}"` |
| Quiet-mode integration | `tests/test_tools.py` | End-to-end: `dispatch("edit_file", ..., verbose=False)` does not call `fmt.tool_diff` (mock-assert) |
| Verbose-mode integration | `tests/test_tools.py` | `dispatch("edit_file", ..., verbose=True)` *does* call `fmt.tool_diff` with correct old/new args |
| Quiet-mode e2e | `tests/test_agent.py` | Full agent loop with `verbose=False`: run an edit_file tool call and assert zero diff-related bytes on captured stderr |

## Files to change

| File | Change |
|------|--------|
| `swival/edit.py` | Make `replace()` return the new content (if it doesn't already) |
| `swival/fmt.py` | Add `tool_diff(file_path, old, new)` with Rich `Text` objects |
| `swival/tools.py` | Add `verbose` param to `_edit_file`, capture old content, call `fmt.tool_diff` |
| `swival/agent.py` | Pass `verbose` through to `_edit_file` via dispatch |
| `tests/test_fmt.py` | Tests for `tool_diff` |
| `tests/test_tools.py` | Quiet-mode and return-value tests |

## Resolved questions

- Diff shows for `replace_all=True` too — same treatment.
- No `--no-diff` flag — gated on `verbose` is sufficient.
- Diffing happens in `_edit_file`, not `agent.py` — avoids dispatch boundary issues.
- Use `replace()` return value for new content — no second file read.
- Use `Text` objects — no markup injection.
