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
8 changes: 7 additions & 1 deletion src/agent/tools/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,15 @@ impl Tool for EditTool {
result.push_str(&format!(" ({} replacements)", match_positions.len()));
}

// Always emit a diff. The earlier 20-line cap was meant to
// keep LLM context lean, but in practice it silently hid
// useful diffs for any non-trivial edit. Bump to 200 lines
// per side which covers the vast majority of real edits;
// edits larger than that are likely refactors where the
// "edit + diff" pattern isn't the right tool anyway.
let old_lines = args.old_text.lines().count();
let new_lines = args.new_text.lines().count();
if old_lines <= 20 && new_lines <= 20 {
if old_lines <= 200 && new_lines <= 200 {
result.push_str(&Self::show_diff(
&args.path,
&content,
Expand Down
75 changes: 70 additions & 5 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2030,19 +2030,37 @@ pub async fn run_interactive(
// independent of terminal width; the chat area
// requires at least 60 cols anyway.
const BOX_W: usize = 64;
let inner = BOX_W.saturating_sub(2);
let pre = "╭─ ⚠ ALERT · PERMISSION ";
let pre_len = pre.chars().count();
let top_fill = BOX_W.saturating_sub(pre_len + 1);
let bot_bar = "─".repeat(BOX_W.saturating_sub(2));
let bot_bar = "─".repeat(inner);
// Helper: format one row as `│ content (padded) │`
// so every row of the alert closes on the right edge.
let row = |content: &str| -> String {
let chars: Vec<char> = content.chars().collect();
let trimmed: String = if chars.len() <= inner.saturating_sub(2) {
chars.iter().collect()
} else if inner <= 2 {
String::new()
} else {
let cap = inner.saturating_sub(3);
let mut out: String = chars[..cap].iter().collect();
out.push('…');
out
};
let pad = inner.saturating_sub(trimmed.chars().count() + 1);
format!("│ {}{}│", trimmed, " ".repeat(pad))
};
renderer.write_line(
&format!("{}{}╮", pre, "─".repeat(top_fill)),
c_perm(),
)?;
renderer.write_line(&format!("tool : {}", ask_req.tool), c_perm())?;
renderer.write_line(&format!("args : {}", ask_req.input), c_perm())?;
renderer.write_line(&row(&format!("tool : {}", ask_req.tool)), c_perm())?;
renderer.write_line(&row(&format!("args : {}", ask_req.input)), c_perm())?;
renderer.write_line(&format!("├{}┤", bot_bar), c_perm())?;
renderer.write_line(
"│ [y] allow once [a] allow always [n] deny [ESC] abort",
&row("[y] allow once [a] allow always [n] deny [ESC] abort"),
c_perm(),
)?;
renderer.write_line(&format!("╰{}╯", bot_bar), c_perm())?;
Expand Down Expand Up @@ -2665,13 +2683,60 @@ fn close_tool_chamber_if_open(
last_tool_name: &mut Option<String>,
) -> anyhow::Result<()> {
if last_tool_name.is_some() {
let (frame_w, _) = chamber_widths(renderer);
let (frame_w, inner) = chamber_widths(renderer);
// Abnormal close: this helper is only called when the tool's
// chamber is closing without a `ToolResult` (permission
// denied, interjected mid-execution, agent error, fresh tool
// call before the previous one finished). Surface that with
// a CRT-static "no signal" row so the empty chamber isn't a
// mute box. Two textured rows + one labelled row inside the
// chamber give it a distinct shape from a normal output
// chamber.
renderer.write_line(&static_row(inner, 0), theme::dim())?;
renderer.write_line(
&chamber_row_centered("░▒▓ NO OUTPUT ▓▒░", inner),
theme::dim(),
)?;
renderer.write_line(
&chamber_row_centered("tool denied · aborted · no result", inner),
theme::dim(),
)?;
renderer.write_line(&static_row(inner, 1), theme::dim())?;
renderer.write_line(&chamber_bottom(frame_w), theme::dim())?;
*last_tool_name = None;
}
Ok(())
}

/// Produce a "CRT signal noise" row inside a chamber: a deterministic
/// `░▒▓` glyph mix padded to inner width. The `seed` selects between
/// two pre-baked patterns so top vs bottom static rows differ slightly
/// — the eye reads it as continuous noise rather than a duplicated row.
fn static_row(inner: usize, seed: usize) -> String {
let glyphs = [
['░', '▒', '░', '▓', '░', '▒', '▒', '░', '▓', '▒'],
['▒', '░', '▓', '░', '▒', '▓', '░', '▒', '░', '▓'],
];
let pattern = &glyphs[seed % 2];
// Body fills `inner` chars (the chamber inner is already the
// padded content width); the `│ ` and ` │` borders sit outside.
let body: String = (0..inner).map(|i| pattern[i % pattern.len()]).collect();
format!("│{}│", body)
}

/// `│ <content centered to inner> │` — pad text on both sides so
/// it sits horizontally centered within the chamber inner width.
fn chamber_row_centered(content: &str, inner: usize) -> String {
let len = content.chars().count();
if len + 2 >= inner {
return chamber_row(content, inner);
}
let pad = inner.saturating_sub(len + 2);
let left = pad / 2;
let right = pad - left;
format!("│ {}{}{} │", " ".repeat(left), content, " ".repeat(right))
}

fn render_tool_output(
renderer: &mut Renderer,
output: &str,
Expand Down
28 changes: 27 additions & 1 deletion src/ui/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1042,10 +1042,16 @@ impl Renderer {
.collect();
push_section(&mut out, "TODOS", todo_items);

// Modified-files panel section: filename matters more than the
// path prefix, so truncate from the *left* (`…foo/bar.rs`)
// rather than the right. Available content width inside the
// chamber row is roughly `inner - 2` (the ` ` indent inside
// the `│ … │` borders).
let mod_max = inner.saturating_sub(2);
let mod_items: Vec<(String, Color)> = d
.modified
.iter()
.map(|p| (format!(" {}", p), Color::White))
.map(|p| (format!(" {}", left_truncate(p, mod_max)), Color::White))
.collect();
push_section(&mut out, "MODIFIED", mod_items);

Expand Down Expand Up @@ -1123,6 +1129,26 @@ pub(crate) fn wrap_input(
(rows, cursor_visual_row, cursor_visual_col)
}

/// Truncate a string from the LEFT so the tail survives when content
/// overflows. Useful for paths where the filename matters more than
/// the prefix: `…clj/yourname/foo.rs` reads better than `src/clj/…`.
/// Returns the input verbatim when `s` fits in `max` chars.
fn left_truncate(s: &str, max: usize) -> String {
let chars: Vec<char> = s.chars().collect();
if chars.len() <= max {
return s.to_string();
}
if max <= 1 {
return "…".to_string();
}
// Reserve 1 char for the leading `…`; keep the last `max-1` chars.
let start = chars.len() - (max - 1);
let mut out = String::with_capacity(max);
out.push('…');
out.extend(&chars[start..]);
out
}

pub fn copy_to_clipboard(text: &str) {
let cmds: &[(&str, &[&str])] = &[
("wl-copy", &[]),
Expand Down
Loading