Skip to content

Commit d1921c5

Browse files
committed
feat: show write_file diff preview
1 parent f8dd8f1 commit d1921c5

File tree

5 files changed

+324
-11
lines changed

5 files changed

+324
-11
lines changed

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.

src/agent/runloop/tool_output.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ pub(crate) fn render_tool_output(
4343
let git_styles = GitStyles::new();
4444
let ls_styles = LsStyles::from_env();
4545

46+
if tool_name == Some(tools::WRITE_FILE) {
47+
render_write_file_preview(renderer, val, &git_styles, &ls_styles)?;
48+
}
49+
4650
if let Some(stdout) = val.get("stdout").and_then(|value| value.as_str()) {
4751
render_stream_section(
4852
renderer,
@@ -510,6 +514,112 @@ fn select_stream_lines<'a>(
510514
(tail, total, truncated)
511515
}
512516

517+
fn render_write_file_preview(
518+
renderer: &mut AnsiRenderer,
519+
payload: &Value,
520+
git_styles: &GitStyles,
521+
ls_styles: &LsStyles,
522+
) -> Result<()> {
523+
let path = payload
524+
.get("path")
525+
.and_then(|value| value.as_str())
526+
.unwrap_or("(unknown path)");
527+
let mode = payload
528+
.get("mode")
529+
.and_then(|value| value.as_str())
530+
.unwrap_or("overwrite");
531+
let bytes_written = payload
532+
.get("bytes_written")
533+
.and_then(|value| value.as_u64())
534+
.unwrap_or(0);
535+
536+
renderer.line(MessageStyle::Tool, &format!("[write_file] {path}"))?;
537+
renderer.line(
538+
MessageStyle::Info,
539+
&format!(" mode={mode} | bytes={bytes_written}"),
540+
)?;
541+
542+
let diff_value = match payload.get("diff_preview") {
543+
Some(value) => value,
544+
None => return Ok(()),
545+
};
546+
547+
if diff_value
548+
.get("skipped")
549+
.and_then(|value| value.as_bool())
550+
.unwrap_or(false)
551+
{
552+
let reason = diff_value
553+
.get("reason")
554+
.and_then(|value| value.as_str())
555+
.unwrap_or("diff preview skipped");
556+
renderer.line(
557+
MessageStyle::Info,
558+
&format!(" diff preview skipped: {reason}"),
559+
)?;
560+
561+
if let Some(detail) = diff_value.get("detail").and_then(|value| value.as_str()) {
562+
renderer.line(MessageStyle::Info, &format!(" detail: {detail}"))?;
563+
}
564+
565+
if let Some(max_bytes) = diff_value.get("max_bytes").and_then(|value| value.as_u64()) {
566+
renderer.line(
567+
MessageStyle::Info,
568+
&format!(" preview limit: {max_bytes} bytes"),
569+
)?;
570+
}
571+
return Ok(());
572+
}
573+
574+
let diff_content = diff_value
575+
.get("content")
576+
.and_then(|value| value.as_str())
577+
.unwrap_or("");
578+
579+
if diff_content.is_empty()
580+
&& diff_value
581+
.get("is_empty")
582+
.and_then(|value| value.as_bool())
583+
.unwrap_or(false)
584+
{
585+
renderer.line(MessageStyle::Info, " No diff changes to display.")?;
586+
}
587+
588+
if !diff_content.is_empty() {
589+
renderer.line(MessageStyle::Tool, "[diff]")?;
590+
for line in diff_content.lines() {
591+
let display = format!(" {line}");
592+
if let Some(style) =
593+
select_line_style(Some(tools::WRITE_FILE), line, git_styles, ls_styles)
594+
{
595+
renderer.line_with_style(style, &display)?;
596+
} else {
597+
renderer.line(MessageStyle::Output, &display)?;
598+
}
599+
}
600+
}
601+
602+
if diff_value
603+
.get("truncated")
604+
.and_then(|value| value.as_bool())
605+
.unwrap_or(false)
606+
{
607+
if let Some(omitted) = diff_value
608+
.get("omitted_line_count")
609+
.and_then(|value| value.as_u64())
610+
{
611+
renderer.line(
612+
MessageStyle::Info,
613+
&format!(" … diff truncated ({omitted} lines omitted)"),
614+
)?;
615+
} else {
616+
renderer.line(MessageStyle::Info, " … diff truncated")?;
617+
}
618+
}
619+
620+
Ok(())
621+
}
622+
513623
fn render_stream_section(
514624
renderer: &mut AnsiRenderer,
515625
title: &str,
@@ -842,7 +952,17 @@ fn select_line_style(
842952
ls: &LsStyles,
843953
) -> Option<Style> {
844954
match tool_name {
845-
Some("run_terminal_cmd") | Some("bash") => {
955+
Some(name)
956+
if matches!(
957+
name,
958+
tools::RUN_TERMINAL_CMD
959+
| tools::BASH
960+
| tools::WRITE_FILE
961+
| tools::EDIT_FILE
962+
| tools::APPLY_PATCH
963+
| tools::SRGN
964+
) =>
965+
{
846966
let trimmed = line.trim_start();
847967
if trimmed.starts_with("diff --")
848968
|| trimmed.starts_with("index ")

vtcode-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pulldown-cmark = { version = "0.9", default-features = false, features = [
9797
"simd",
9898
] }
9999
catppuccin = { version = "2.5", default-features = false }
100+
similar = "2.4"
100101
rig = { package = "rig-core", version = "0.21", default-features = false, features = ["reqwest-rustls"] }
101102

102103
# MCP (Model Context Protocol) support

vtcode-core/src/config/constants.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,21 @@ pub mod chunking {
479479
/// Chunk size for write operations (in bytes)
480480
pub const WRITE_CHUNK_SIZE: usize = 50_000; // 50KB chunks
481481
}
482+
483+
/// Diff preview controls for file operations
484+
pub mod diff {
485+
/// Maximum number of bytes allowed in diff preview inputs
486+
pub const MAX_PREVIEW_BYTES: usize = 200_000;
487+
488+
/// Number of context lines to include around changes in unified diff output
489+
pub const CONTEXT_RADIUS: usize = 3;
490+
491+
/// Maximum number of diff lines to keep in preview output before condensation
492+
pub const MAX_PREVIEW_LINES: usize = 160;
493+
494+
/// Number of leading diff lines to retain when condensing previews
495+
pub const HEAD_LINE_COUNT: usize = 96;
496+
497+
/// Number of trailing diff lines to retain when condensing previews
498+
pub const TAIL_LINE_COUNT: usize = 32;
499+
}

0 commit comments

Comments
 (0)