Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ rmcp = "0.15"
scrape-core = "0.2.2"
subtle = "2.6"
schemars = "1.2"
similar = "2.7"
serde = "1.0"
serde_json = "1.0"
serial_test = "3.3"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ cargo build --release --features tui
| **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 | |
| **Prompt Caching** | Automatic prompt caching for Anthropic and OpenAI providers, reducing latency and cost on repeated context | |
| **Graceful Shutdown** | Ctrl-C triggers ordered teardown with MCP server cleanup and pending task draining | |
| **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) |
| **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) |
| **Multi-Channel I/O** | CLI, Discord, Slack, Telegram, and TUI with streaming support | [Channels](https://bug-ops.github.io/zeph/guide/channels.html) |
| **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) |

Expand Down
5 changes: 5 additions & 0 deletions crates/zeph-core/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,7 @@ pub(super) mod agent_tests {
summary: "tool executed successfully".to_string(),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))]);

let agent_channel = MockChannel::new(vec!["execute tool".to_string()]);
Expand Down Expand Up @@ -1356,6 +1357,7 @@ pub(super) mod agent_tests {
summary: "[error] command failed [exit code 1]".to_string(),
blocks_executed: 1,
filter_stats: None,
diff: None,
})),
Ok(None),
]);
Expand All @@ -1376,6 +1378,7 @@ pub(super) mod agent_tests {
summary: " ".to_string(),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))]);

let mut agent = Agent::new(provider, channel, registry, None, 5, executor);
Expand Down Expand Up @@ -1468,6 +1471,7 @@ pub(super) mod agent_tests {
summary: "step 1 complete".to_string(),
blocks_executed: 1,
filter_stats: None,
diff: None,
})),
Ok(None),
]);
Expand Down Expand Up @@ -1496,6 +1500,7 @@ pub(super) mod agent_tests {
summary: "continuing".to_string(),
blocks_executed: 1,
filter_stats: None,
diff: None,
})));
}
let executor = MockToolExecutor::new(outputs);
Expand Down
7 changes: 6 additions & 1 deletion crates/zeph-core/src/agent/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,12 @@ impl<C: Channel, T: ToolExecutor> Agent<C, T> {
.instrument(tracing::info_span!("tool_exec", tool_name = %tc.name))
.await;
let (output, is_error) = match tool_result {
Ok(Some(out)) => (out.summary, false),
Ok(Some(out)) => {
if let Some(diff) = out.diff {
let _ = self.channel.send_diff(diff).await;
}
(out.summary, false)
}
Ok(None) => ("(no output)".to_owned(), false),
Err(e) => (format!("[error] {e}"), true),
};
Expand Down
12 changes: 12 additions & 0 deletions crates/zeph-core/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ pub trait Channel: Send {
async { Ok(()) }
}

/// Send diff data for a tool result. No-op by default (TUI overrides).
///
/// # Errors
///
/// Returns an error if the underlying I/O fails.
fn send_diff(
&mut self,
_diff: crate::DiffData,
) -> impl Future<Output = Result<(), ChannelError>> + Send {
async { Ok(()) }
}

/// Request user confirmation for a destructive action. Returns `true` if confirmed.
/// Default: auto-confirm (for headless/test scenarios).
///
Expand Down
1 change: 1 addition & 0 deletions crates/zeph-core/src/diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub use zeph_tools::executor::DiffData;
3 changes: 3 additions & 0 deletions crates/zeph-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ pub mod project;
pub mod redact;
pub mod vault;

pub mod diff;

pub use agent::Agent;
pub use agent::error::AgentError;
pub use channel::{Channel, ChannelError, ChannelMessage};
pub use config::Config;
pub use diff::DiffData;
1 change: 1 addition & 0 deletions crates/zeph-mcp/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ impl ToolExecutor for McpToolExecutor {
summary: outputs.join("\n\n"),
blocks_executed,
filter_stats: None,
diff: None,
}))
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/zeph-tools/src/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ mod tests {
summary: "matched".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
}
}
Expand Down Expand Up @@ -91,6 +92,7 @@ mod tests {
summary: "second".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
}
}
Expand Down Expand Up @@ -167,6 +169,7 @@ mod tests {
summary: "file_handler".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
} else {
Ok(None)
Expand All @@ -190,6 +193,7 @@ mod tests {
summary: "shell_handler".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
} else {
Ok(None)
Expand Down
11 changes: 11 additions & 0 deletions crates/zeph-tools/src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
use std::collections::HashMap;
use std::fmt;

/// Data for rendering file diffs in the TUI.
#[derive(Debug, Clone)]
pub struct DiffData {
pub file_path: String,
pub old_content: String,
pub new_content: String,
}

/// Structured tool invocation from LLM.
#[derive(Debug, Clone)]
pub struct ToolCall {
Expand Down Expand Up @@ -51,6 +59,7 @@ pub struct ToolOutput {
pub summary: String,
pub blocks_executed: u32,
pub filter_stats: Option<FilterStats>,
pub diff: Option<DiffData>,
}

impl fmt::Display for ToolOutput {
Expand Down Expand Up @@ -98,6 +107,7 @@ pub enum ToolEvent {
output: String,
success: bool,
filter_stats: Option<FilterStats>,
diff: Option<DiffData>,
},
}

Expand Down Expand Up @@ -189,6 +199,7 @@ mod tests {
summary: "$ echo hello\nhello".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
};
assert_eq!(output.to_string(), "$ echo hello\nhello");
}
Expand Down
17 changes: 16 additions & 1 deletion crates/zeph-tools/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};

use schemars::JsonSchema;

use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
use crate::executor::{DiffData, ToolCall, ToolError, ToolExecutor, ToolOutput};
use crate::registry::{InvocationHint, ToolDef};

// Schema-only: fields are read by schemars derive, not by Rust code directly.
Expand Down Expand Up @@ -144,6 +144,7 @@ impl FileExecutor {
summary: selected.join("\n"),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
}

Expand All @@ -155,6 +156,8 @@ impl FileExecutor {
let content = param_str(params, "content")?;
let path = self.validate_path(Path::new(&path_str))?;

let old_content = std::fs::read_to_string(&path).unwrap_or_default();

if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
Expand All @@ -165,6 +168,11 @@ impl FileExecutor {
summary: format!("Wrote {} bytes to {path_str}", content.len()),
blocks_executed: 1,
filter_stats: None,
diff: Some(DiffData {
file_path: path_str,
old_content,
new_content: content,
}),
}))
}

Expand Down Expand Up @@ -193,6 +201,11 @@ impl FileExecutor {
summary: format!("Edited {path_str}"),
blocks_executed: 1,
filter_stats: None,
diff: Some(DiffData {
file_path: path_str,
old_content: content,
new_content,
}),
}))
}

Expand Down Expand Up @@ -225,6 +238,7 @@ impl FileExecutor {
},
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
}

Expand Down Expand Up @@ -267,6 +281,7 @@ impl FileExecutor {
},
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/zeph-tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ pub use audit::{AuditEntry, AuditLogger, AuditResult};
pub use composite::CompositeExecutor;
pub use config::{AuditConfig, ScrapeConfig, ShellConfig, ToolsConfig};
pub use executor::{
FilterStats, MAX_TOOL_OUTPUT_CHARS, ToolCall, ToolError, ToolEvent, ToolEventTx, ToolExecutor,
ToolOutput, truncate_tool_output,
DiffData, FilterStats, MAX_TOOL_OUTPUT_CHARS, ToolCall, ToolError, ToolEvent, ToolEventTx,
ToolExecutor, ToolOutput, truncate_tool_output,
};
pub use file::FileExecutor;
pub use filter::{
Expand Down
2 changes: 2 additions & 0 deletions crates/zeph-tools/src/scrape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ impl ToolExecutor for WebScrapeExecutor {
summary: outputs.join("\n\n"),
blocks_executed,
filter_stats: None,
diff: None,
}))
}

Expand Down Expand Up @@ -134,6 +135,7 @@ impl ToolExecutor for WebScrapeExecutor {
summary: result,
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
}
}
Expand Down
13 changes: 13 additions & 0 deletions crates/zeph-tools/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,17 @@ impl ShellExecutor {
};
self.log_audit(block, result, duration_ms).await;

if let Some(ref tx) = self.tool_event_tx {
let _ = tx.send(ToolEvent::Completed {
tool_name: "bash".to_owned(),
command: (*block).to_owned(),
output: out.clone(),
success: !out.contains("[error]"),
filter_stats: None,
diff: None,
});
}

let sanitized = sanitize_output(&out);
let mut per_block_stats: Option<FilterStats> = None;
let filtered = if let Some(ref registry) = self.output_filter_registry {
Expand Down Expand Up @@ -252,6 +263,7 @@ impl ShellExecutor {
output: out.clone(),
success: !out.contains("[error]"),
filter_stats: per_block_stats,
diff: None,
});
}
outputs.push(format!("$ {block}\n{filtered}"));
Expand All @@ -262,6 +274,7 @@ impl ShellExecutor {
summary: outputs.join("\n\n"),
blocks_executed,
filter_stats: cumulative_filter_stats,
diff: None,
}))
}

Expand Down
1 change: 1 addition & 0 deletions crates/zeph-tools/src/trust_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ mod tests {
summary: "ok".into(),
blocks_executed: 1,
filter_stats: None,
diff: None,
}))
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/zeph-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ crossterm.workspace = true
pulldown-cmark.workspace = true
ratatui.workspace = true
thiserror.workspace = true
similar.workspace = true
throbber-widgets-tui = "0.10"
tokio = { workspace = true, features = ["sync", "rt", "time"] }
unicode-width.workspace = true
Expand Down
Loading
Loading