Skip to content

Commit 3ce3f34

Browse files
authored
TUI: syntax-highlighted diff view for write/edit tool output (#455)
* feat: syntax-highlighted diff view for write/edit tool output in TUI Add diff rendering to TUI chat panel for write and edit tool results. FileExecutor captures old content before modifications, propagates DiffData through agent events to TUI where it renders with green/red backgrounds for added/removed lines, word-level change highlighting, and syntax coloring via tree-sitter. Compact/expanded toggle reuses existing 'e' key binding. Closes #451 * docs: update documentation, changelog, and readme for TUI diff view
1 parent 0b2debe commit 3ce3f34

File tree

29 files changed

+395
-6
lines changed

29 files changed

+395
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
77
## [Unreleased]
88

99
### Added
10+
- Syntax-highlighted diff view for write/edit tool output in TUI (#451)
11+
- Diff rendering with green/red backgrounds for added/removed lines
12+
- Word-level change highlighting within modified lines
13+
- Syntax highlighting via tree-sitter
14+
- Compact/expanded toggle with existing 'e' key binding
15+
- New dependency: `similar` 2.7.0
1016
- Per-tool inline filter stats in CLI chat: `[shell] cargo test (342 lines -> 28 lines, 91.8% filtered)` (#449)
1117
- Filter metrics in TUI Resources panel: confidence distribution, command hit rate, token savings (#448)
1218
- Periodic 250ms tick in TUI event loop for real-time metrics refresh (#447)

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ rmcp = "0.15"
4040
scrape-core = "0.2.2"
4141
subtle = "2.6"
4242
schemars = "1.2"
43+
similar = "2.7"
4344
serde = "1.0"
4445
serde_json = "1.0"
4546
serial_test = "3.3"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ cargo build --release --features tui
118118
| **Skill Trust & Quarantine** | 4-tier trust model (Trusted/Verified/Quarantined/Blocked) with blake3 integrity verification, anomaly detection with automatic blocking, and restricted tool access for untrusted skills | |
119119
| **Prompt Caching** | Automatic prompt caching for Anthropic and OpenAI providers, reducing latency and cost on repeated context | |
120120
| **Graceful Shutdown** | Ctrl-C triggers ordered teardown with MCP server cleanup and pending task draining | |
121-
| **TUI Dashboard** | ratatui terminal UI with tree-sitter syntax highlighting, markdown rendering, deferred model warmup, scrollbar, mouse scroll, thinking blocks, conversation history, splash screen, live metrics (including filter savings), message queueing (max 10, FIFO with Ctrl+K clear) | [TUI](https://bug-ops.github.io/zeph/guide/tui.html) |
121+
| **TUI Dashboard** | ratatui terminal UI with tree-sitter syntax highlighting, markdown rendering, syntax-highlighted diff view for write/edit tool output (compact/expanded toggle), deferred model warmup, scrollbar, mouse scroll, thinking blocks, conversation history, splash screen, live metrics (including filter savings), message queueing (max 10, FIFO with Ctrl+K clear) | [TUI](https://bug-ops.github.io/zeph/guide/tui.html) |
122122
| **Multi-Channel I/O** | CLI, Discord, Slack, Telegram, and TUI with streaming support | [Channels](https://bug-ops.github.io/zeph/guide/channels.html) |
123123
| **Defense-in-Depth** | Shell sandbox with relative path traversal detection, file sandbox, command filter, secret redaction (Google/GitLab patterns), audit log, SSRF protection (agent + MCP), rate limiter TTL eviction, doom-loop detection, skill trust quarantine | [Security](https://bug-ops.github.io/zeph/security.html) |
124124

crates/zeph-core/src/agent/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,7 @@ pub(super) mod agent_tests {
11581158
summary: "tool executed successfully".to_string(),
11591159
blocks_executed: 1,
11601160
filter_stats: None,
1161+
diff: None,
11611162
}))]);
11621163

11631164
let agent_channel = MockChannel::new(vec!["execute tool".to_string()]);
@@ -1356,6 +1357,7 @@ pub(super) mod agent_tests {
13561357
summary: "[error] command failed [exit code 1]".to_string(),
13571358
blocks_executed: 1,
13581359
filter_stats: None,
1360+
diff: None,
13591361
})),
13601362
Ok(None),
13611363
]);
@@ -1376,6 +1378,7 @@ pub(super) mod agent_tests {
13761378
summary: " ".to_string(),
13771379
blocks_executed: 1,
13781380
filter_stats: None,
1381+
diff: None,
13791382
}))]);
13801383

13811384
let mut agent = Agent::new(provider, channel, registry, None, 5, executor);
@@ -1468,6 +1471,7 @@ pub(super) mod agent_tests {
14681471
summary: "step 1 complete".to_string(),
14691472
blocks_executed: 1,
14701473
filter_stats: None,
1474+
diff: None,
14711475
})),
14721476
Ok(None),
14731477
]);
@@ -1496,6 +1500,7 @@ pub(super) mod agent_tests {
14961500
summary: "continuing".to_string(),
14971501
blocks_executed: 1,
14981502
filter_stats: None,
1503+
diff: None,
14991504
})));
15001505
}
15011506
let executor = MockToolExecutor::new(outputs);

crates/zeph-core/src/agent/streaming.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,12 @@ impl<C: Channel, T: ToolExecutor> Agent<C, T> {
619619
.instrument(tracing::info_span!("tool_exec", tool_name = %tc.name))
620620
.await;
621621
let (output, is_error) = match tool_result {
622-
Ok(Some(out)) => (out.summary, false),
622+
Ok(Some(out)) => {
623+
if let Some(diff) = out.diff {
624+
let _ = self.channel.send_diff(diff).await;
625+
}
626+
(out.summary, false)
627+
}
623628
Ok(None) => ("(no output)".to_owned(), false),
624629
Err(e) => (format!("[error] {e}"), true),
625630
};

crates/zeph-core/src/channel.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ pub trait Channel: Send {
9393
async { Ok(()) }
9494
}
9595

96+
/// Send diff data for a tool result. No-op by default (TUI overrides).
97+
///
98+
/// # Errors
99+
///
100+
/// Returns an error if the underlying I/O fails.
101+
fn send_diff(
102+
&mut self,
103+
_diff: crate::DiffData,
104+
) -> impl Future<Output = Result<(), ChannelError>> + Send {
105+
async { Ok(()) }
106+
}
107+
96108
/// Request user confirmation for a destructive action. Returns `true` if confirmed.
97109
/// Default: auto-confirm (for headless/test scenarios).
98110
///

crates/zeph-core/src/diff.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub use zeph_tools::executor::DiffData;

crates/zeph-core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ pub mod project;
1515
pub mod redact;
1616
pub mod vault;
1717

18+
pub mod diff;
19+
1820
pub use agent::Agent;
1921
pub use agent::error::AgentError;
2022
pub use channel::{Channel, ChannelError, ChannelMessage};
2123
pub use config::Config;
24+
pub use diff::DiffData;

crates/zeph-mcp/src/executor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ impl ToolExecutor for McpToolExecutor {
6868
summary: outputs.join("\n\n"),
6969
blocks_executed,
7070
filter_stats: None,
71+
diff: None,
7172
}))
7273
}
7374
}

0 commit comments

Comments
 (0)