use crossterm::{
execute,
style::{Color as CtColor, Print, ResetColor, SetForegroundColor},
};
use std::io::{self, Write};
pub use crate::tty::NEWT_ORANGE_CT;
pub(crate) use crate::tty::{term_cols, FADE_CT};
pub(crate) use crate::tty::width::wrap_line as wrap_to_width;
pub fn print_newt(msg: &str, color: bool, verbose: bool) {
println!("{}", newt_line(msg, color, verbose));
}
pub fn newt_line(msg: &str, color: bool, verbose: bool) -> String {
let prefix = if color {
if verbose {
"newt ▸ "
} else {
"▸ "
}
} else if verbose {
"newt > "
} else {
"> "
};
format!("{prefix}{msg}")
}
pub fn print_list_item(label: &str, active: bool, color: bool) {
if !active {
println!(" {label}");
return;
}
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::Red),
Print("▸ "),
ResetColor,
Print(label),
Print(" "),
SetForegroundColor(CtColor::Red),
Print("◀ "),
SetForegroundColor(CtColor::Green),
Print("active"),
ResetColor,
Print("\n"),
)
.ok();
} else {
println!("> {label} <- active");
}
}
pub fn print_harness_notice(msg: &str, color: bool) {
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkYellow),
Print(format!("⚠ newt: {msg}\n")),
ResetColor,
)
.ok();
} else {
println!("⚠ newt: {msg}");
}
io::stdout().flush().ok();
}
pub(crate) fn print_debug(msg: &str, color: bool) {
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print(format!("[debug] {msg}\n")),
ResetColor,
)
.ok();
} else {
println!("[debug] {msg}");
}
io::stdout().flush().ok();
}
pub(crate) fn print_trace(msg: &str, color: bool) {
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print(format!("[trace] {msg}\n")),
ResetColor,
)
.ok();
} else {
println!("[trace] {msg}");
}
io::stdout().flush().ok();
}
pub(crate) fn fmt_tokens(n: u32) -> String {
let s = n.to_string();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
out.push(',');
}
out.push(c);
}
out.chars().rev().collect()
}
pub(crate) fn fmt_tokens_k(n: u32) -> String {
format!("{}k", (n + 500) / 1000)
}
pub fn fmt_tokens_compact(n: u32) -> String {
let k = (n + 500) / 1000;
if k >= 1024 {
let m = k as f64 / 1024.0;
if (m - m.round()).abs() < 0.05 {
format!("{}M", m.round() as u64)
} else {
format!("{m:.1}M")
}
} else {
format!("{k}k")
}
}
pub fn fmt_token_gauge(used: u32, budget: u32) -> String {
format!("{}/{}", fmt_tokens_k(used), fmt_tokens_k(budget))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GaugeLevel {
Ok,
Warn,
Critical,
}
pub fn gauge_level(used: u32, budget: u32) -> GaugeLevel {
let pct = if budget == 0 {
0
} else {
(used as u64 * 100 / budget as u64) as u32
};
if pct >= 90 {
GaugeLevel::Critical
} else if pct >= 75 {
GaugeLevel::Warn
} else {
GaugeLevel::Ok
}
}
pub(crate) fn emit_overflow_notice(
color: bool,
usage: Option<&crate::TokenUsage>,
safe_context: Option<u32>,
model: &str,
attempt: u32,
) {
let token_str = usage
.map(|u| format!("{} tokens", fmt_tokens(u.input_tokens)))
.unwrap_or_else(|| "unknown tokens".to_string());
let safe_str = safe_context
.map(|s| format!(" > {} safe window for {model}", fmt_tokens(s)))
.unwrap_or_default();
let msg = format!(
"⚠ context overflow likely ({token_str}{safe_str})\n⟳ trimming context and retrying (attempt {attempt}/2)…"
);
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkYellow),
Print(format!("{msg}\n")),
ResetColor,
)
.ok();
} else {
println!("{msg}");
}
io::stdout().flush().ok();
}
pub(crate) fn compression_notice_text(
action: super::compress::CompressAction,
before: usize,
after: usize,
suffix: &str,
) -> (String, bool) {
use super::compress::CompressAction;
let b = fmt_tokens(before.min(u32::MAX as usize) as u32);
let a = fmt_tokens(after.min(u32::MAX as usize) as u32);
match action {
CompressAction::StaticFallback => (
format!(
"⛔ summary unavailable — context compacted to a marker \
(~{b} → ~{a} est. tokens{suffix}). Re-read files if needed."
),
true,
),
CompressAction::Summarized => (
format!("✓ context summarized: ~{b} → ~{a} est. tokens{suffix}"),
false,
),
other => (
format!(
"⧉ context compressed: ~{b} → ~{a} est. tokens ({}{suffix})",
other.describe()
),
false,
),
}
}
pub(crate) fn emit_compression_notice(
color: bool,
before: usize,
after: usize,
action: super::compress::CompressAction,
suffix: &str,
) {
let (msg, loud) = compression_notice_text(action, before, after, suffix);
let hue = if loud {
CtColor::Red
} else {
CtColor::DarkYellow
};
if color {
execute!(
io::stdout(),
SetForegroundColor(hue),
Print(format!("{msg}\n")),
ResetColor,
)
.ok();
} else {
println!("{msg}");
}
io::stdout().flush().ok();
}
pub(crate) fn print_retry_indicator(attempt: u32, delay: std::time::Duration, color: bool) {
let delay_s = delay.as_secs_f32();
let msg = format!(" ↻ connection lost — retrying in {delay_s:.1}s (attempt {attempt})…\n");
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::Rgb {
r: 200,
g: 140,
b: 0
}),
Print(&msg),
ResetColor,
)
.ok();
} else {
print!("{msg}");
}
io::stdout().flush().ok();
}
fn tool_call_lines(name: &str, detail: &str, cols: usize) -> Vec<String> {
let prefix_w = 3 + name.chars().count() + 2; let detail_w = cols.saturating_sub(prefix_w).max(8);
let wrapped = wrap_to_width(detail, detail_w);
let indent = " ".repeat(prefix_w);
wrapped
.into_iter()
.enumerate()
.map(|(i, line)| {
if i == 0 {
format!("⚙ {name}: {line}")
} else {
format!("{indent}{line}")
}
})
.collect()
}
static SPILL_LINES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(3);
pub fn set_spill_lines(n: usize) {
SPILL_LINES.store(n, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn spill_lines() -> usize {
SPILL_LINES.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) const SPILL_RECOVERY_HINT: &str = "/spill N raises this view";
pub(crate) fn spill_view_lines(output: &str, view: usize, columns: usize) -> Vec<String> {
let content_width = columns.saturating_sub(2).max(1);
let rows_of = |l: &str| wrap_to_width(l, content_width).len().max(1);
let lines: Vec<&str> = output.lines().collect();
if lines.is_empty() {
return Vec::new();
}
if view == 0 {
return lines.iter().map(|l| (*l).to_string()).collect();
}
let mut kept = 0usize;
let mut used = 0usize;
for l in lines.iter().rev() {
let r = rows_of(l);
if kept > 0 && used + r > view {
break;
}
kept += 1;
used += r;
if used >= view {
break;
}
}
let start = lines.len() - kept;
let tail = &lines[start..];
if used > view && tail.len() == 1 {
let wrapped = wrap_to_width(tail[0], content_width);
let dropped = wrapped.len() - view;
let mut out = vec![format!(
"▲ {dropped} more wrapped rows above · {SPILL_RECOVERY_HINT}"
)];
for (i, row) in wrapped[dropped..].iter().enumerate() {
let glyph = if i + 1 == view { '▓' } else { '▒' };
out.push(format!("{glyph} {row}"));
}
out.push("…".to_string());
return out;
}
let lines: Vec<String> = tail.iter().map(|l| (*l).to_string()).collect();
let hidden_logical = start;
let mut out = Vec::new();
if hidden_logical == 0 {
for l in &lines {
out.push(format!("▒ {l}"));
}
out.push("…".to_string());
return out;
}
let hidden = hidden_logical;
out.push(format!(
"▲ {hidden} more lines above · {SPILL_RECOVERY_HINT}"
));
let tail = &lines[..];
for (i, l) in tail.iter().enumerate() {
let glyph = if i + 1 == tail.len() { '▓' } else { '▒' };
out.push(format!("{glyph} {l}"));
}
out.push("…".to_string());
out
}
pub(crate) struct ToolDisplay<W: Write> {
writer: W,
color: bool,
cols: usize,
spill_lines: usize,
result_override: Option<String>,
}
impl<W: Write> ToolDisplay<W> {
pub(crate) fn new(writer: W, color: bool, cols: usize, spill_lines: usize) -> Self {
Self {
writer,
color,
cols,
spill_lines,
result_override: None,
}
}
pub(crate) fn call(&mut self, name: &str, detail: &str) {
let lines = tool_call_lines(name, detail, self.cols);
for (i, line) in lines.iter().enumerate() {
if self.color {
if i == 0 {
let prefix = format!("⚙ {name}");
let suffix = line.strip_prefix(&prefix).unwrap_or(line);
execute!(
&mut self.writer,
SetForegroundColor(NEWT_ORANGE_CT),
Print(prefix),
ResetColor,
SetForegroundColor(CtColor::DarkGrey),
Print(suffix),
ResetColor,
Print("\n"),
)
.ok();
} else {
execute!(
&mut self.writer,
SetForegroundColor(CtColor::DarkGrey),
Print(line),
ResetColor,
Print("\n"),
)
.ok();
}
} else {
writeln!(&mut self.writer, "{line}").ok();
}
}
self.writer.flush().ok();
}
pub(crate) fn result(&mut self, output: &str) {
let overridden = self.result_override.take();
let output = overridden.as_deref().unwrap_or(output);
let output = if output.trim().is_empty() {
"(no output)"
} else {
output
};
let rendered = spill_view_lines(output, self.spill_lines, self.cols).join("\n");
if self.color {
execute!(
&mut self.writer,
SetForegroundColor(CtColor::DarkGrey),
Print(format!("{rendered}\n")),
ResetColor,
)
.ok();
} else {
writeln!(&mut self.writer, "{rendered}").ok();
}
self.writer.flush().ok();
}
#[cfg(test)]
pub(crate) fn into_inner(self) -> W {
self.writer
}
}
pub(crate) trait ToolPresentation: Send {
fn preview(&mut self, output: &str, max_lines: usize);
fn document(&mut self, output: &str);
fn override_result(&mut self, output: String);
}
impl<W: Write + Send> ToolPresentation for ToolDisplay<W> {
fn preview(&mut self, output: &str, max_lines: usize) {
let lines: Vec<&str> = output.lines().collect();
let shown = if max_lines == 0 {
lines.len()
} else {
lines.len().min(max_lines)
};
let mut rendered = lines[..shown].join("\n");
let hidden = lines.len().saturating_sub(shown);
if hidden > 0 {
if !rendered.is_empty() {
rendered.push('\n');
}
rendered.push_str(&format!(
" … ({hidden} more lines hidden · {SPILL_RECOVERY_HINT})"
));
}
if rendered.is_empty() {
return;
}
if self.color {
execute!(
&mut self.writer,
SetForegroundColor(CtColor::DarkGrey),
Print(format!("{rendered}\n")),
ResetColor,
)
.ok();
} else {
writeln!(&mut self.writer, "{rendered}").ok();
}
self.writer.flush().ok();
}
fn document(&mut self, output: &str) {
writeln!(&mut self.writer, "{output}").ok();
self.writer.flush().ok();
}
fn override_result(&mut self, output: String) {
self.result_override = Some(output);
}
}
#[cfg(test)]
pub(crate) fn print_tool_call(name: &str, detail: &str, color: bool) {
ToolDisplay::new(io::stdout(), color, term_cols(), spill_lines()).call(name, detail);
}
#[cfg(test)]
pub(crate) fn print_tool_output(output: &str, _tool_output_lines: usize, color: bool) {
ToolDisplay::new(io::stdout(), color, term_cols(), spill_lines()).result(output);
}
#[cfg(test)]
mod tests {
use super::{
fmt_tokens, print_harness_notice, print_list_item, print_newt, spill_view_lines,
tool_call_lines, wrap_to_width, SPILL_RECOVERY_HINT,
};
#[test]
fn one_long_line_cannot_flood_the_row_budget() {
const COLS: usize = 80;
let long = "x".repeat(400);
let out = spill_view_lines(&long, 3, COLS);
let rendered_rows: usize = out.iter().map(|l| wrap_to_width(l, COLS).len()).sum();
assert!(
rendered_rows <= 5,
"a single {}-char line rendered {rendered_rows} rows at {COLS} columns \
against a 3-row budget:\n{out:#?}",
long.len()
);
}
#[test]
fn a_line_wider_than_the_terminal_is_measured_but_not_rewrapped() {
const COLS: usize = 40;
let wide = "artifact:abc: returned 44 of 44 body characters at offset 0 (complete)";
assert!(wide.len() > COLS, "the fixture must exceed the width");
let out = spill_view_lines(&format!("first\n{wide}"), 3, COLS);
let joined = out.join("\n");
assert!(
joined.contains("returned 44 of 44 body characters"),
"the phrase was split across a wrap — copy-paste and search are \
broken by rewrapping the canonical block:\n{joined}"
);
}
#[test]
fn a_wide_line_spends_the_row_budget_it_actually_occupies() {
const COLS: usize = 20;
let wide = "w".repeat(50);
let out = spill_view_lines(&format!("a\nb\nc\n{wide}"), 3, COLS);
let body: Vec<&String> = out
.iter()
.filter(|l| l.starts_with('▒') || l.starts_with('▓'))
.collect();
assert_eq!(
body.len(),
1,
"the wide line alone should consume the 3-row budget, leaving no \
room for a/b/c:\n{out:#?}"
);
assert!(out[0].contains("more lines above"), "{out:#?}");
assert!(
out[0].contains(SPILL_RECOVERY_HINT),
"every truncation marker names the way out:\n{out:#?}"
);
}
#[test]
fn spill_view_is_tail_biased_with_gutter_glyphs() {
let small = spill_view_lines("a\nb\nc", 3, 80);
assert_eq!(small, vec!["▒ a", "▒ b", "▒ c", "…"]);
let big = spill_view_lines("l1\nl2\nl3\nl4\nl5", 3, 80);
assert_eq!(
big,
vec![
"▲ 2 more lines above · /spill N raises this view",
"▒ l3",
"▒ l4",
"▓ l5",
"…"
]
);
assert_eq!(spill_view_lines("x\ny", 0, 80), vec!["x", "y"]);
assert!(spill_view_lines("", 3, 80).is_empty());
}
#[test]
fn completed_tool_output_uses_the_spill_view() {
let output = "l1\nl2\nl3\nl4\nl5";
assert_eq!(
spill_view_lines(output, 3, 80),
vec![
"▲ 2 more lines above · /spill N raises this view",
"▒ l3",
"▒ l4",
"▓ l5",
"…"
]
);
let raw: Vec<String> = output.lines().map(str::to_string).collect();
assert_eq!(spill_view_lines(output, 0, 80), raw);
}
#[test]
fn completed_excerpt_names_its_recovery_path() {
let lines = spill_view_lines("l1\nl2\nl3\nl4\nl5", 3, 80);
assert!(
lines[0].contains("/spill N raises this view"),
"the ▲ boundary must carry the recovery hint: {:?}",
lines[0]
);
assert_eq!(lines.last().map(String::as_str), Some("…"));
let small = spill_view_lines("a\nb", 3, 80);
assert_eq!(small.last().map(String::as_str), Some("…"));
}
#[test]
fn tool_call_lines_wrap_without_losing_the_command() {
assert_eq!(
tool_call_lines("find", ". (name=*.rs, type=f)", 80),
vec!["⚙ find: . (name=*.rs, type=f)"]
);
}
#[test]
fn wrap_to_width_never_drops_and_hard_splits_long_tokens() {
let cmd = "grep -rn \"NudgerProfile|resolve.*knob|KNOWN_KNOBS\" --include=*.rs newt-core/src newt-cli/src";
let wrapped = super::wrap_to_width(cmd, 20);
assert_eq!(wrapped.join(""), cmd, "no characters lost");
assert!(
wrapped.iter().all(|l| l.chars().count() <= 20),
"each line fits"
);
assert!(wrapped.len() > 1, "long command actually wraps");
let long = "a".repeat(50);
let w = super::wrap_to_width(&long, 10);
assert_eq!(w.join(""), long);
assert_eq!(w.len(), 5);
assert_eq!(super::wrap_to_width("cargo build", 40), vec!["cargo build"]);
assert_eq!(super::wrap_to_width("a\nb", 40), vec!["a", "b"]);
}
#[test]
fn fmt_tokens_inserts_thousands_separators() {
assert_eq!(fmt_tokens(0), "0");
assert_eq!(fmt_tokens(999), "999");
assert_eq!(fmt_tokens(1_000), "1,000");
assert_eq!(fmt_tokens(1_234_567), "1,234,567");
}
#[test]
fn gauge_formatting_k_compact_fraction_and_level() {
use super::{fmt_token_gauge, fmt_tokens_compact, fmt_tokens_k, gauge_level, GaugeLevel};
assert_eq!(fmt_tokens_k(899_000), "899k");
assert_eq!(fmt_tokens_k(1_024_000), "1024k");
assert_eq!(fmt_tokens_k(512_400), "512k");
assert_eq!(fmt_tokens_compact(899_000), "899k");
assert_eq!(fmt_tokens_compact(1_024_000), "1M");
assert_eq!(fmt_tokens_compact(2_048_000), "2M");
assert_eq!(fmt_tokens_compact(1_536_000), "1.5M");
assert_eq!(fmt_token_gauge(899_000, 1_024_000), "899k/1024k");
assert_eq!(gauge_level(100, 1000), GaugeLevel::Ok);
assert_eq!(gauge_level(740, 1000), GaugeLevel::Ok);
assert_eq!(gauge_level(750, 1000), GaugeLevel::Warn);
assert_eq!(gauge_level(890, 1000), GaugeLevel::Warn);
assert_eq!(gauge_level(900, 1000), GaugeLevel::Critical);
assert_eq!(gauge_level(0, 0), GaugeLevel::Ok); }
#[test]
fn compression_notice_text_registers_per_outcome() {
use super::compression_notice_text;
use crate::agentic::compress::CompressAction;
let (msg, loud) =
compression_notice_text(CompressAction::StaticFallback, 10_000, 6_000, "");
assert!(loud, "static marker is the loud last resort");
assert!(msg.starts_with("⛔"), "{msg}");
assert!(msg.contains("summary unavailable"), "{msg}");
assert!(msg.contains("Re-read files"), "{msg}");
let (msg, loud) = compression_notice_text(CompressAction::Summarized, 10_000, 6_000, "");
assert!(!loud);
assert!(msg.starts_with("✓") && msg.contains("summarized"), "{msg}");
let (msg, loud) = compression_notice_text(CompressAction::Pruned, 10_000, 6_000, "");
assert!(!loud);
assert!(
msg.starts_with("⧉") && msg.contains("structural prune"),
"{msg}"
);
let (msg, _) = compression_notice_text(
CompressAction::Summarized,
10_000,
6_000,
", still over budget",
);
assert!(msg.contains(", still over budget"), "{msg}");
}
#[test]
fn notice_reproduces_the_compression_notice_bytes() {
use super::compression_notice_text;
use crate::agentic::compress::CompressAction;
use crate::tty::{Level, Notice};
let cases = [
(CompressAction::StaticFallback, Level::Loud, "⛔"),
(CompressAction::Summarized, Level::Ok, "✓"),
(CompressAction::Pruned, Level::Info, "⧉"),
];
for (action, level, glyph) in cases {
let (msg, _) = compression_notice_text(action, 10_000, 6_000, "");
let body = msg
.strip_prefix(glyph)
.and_then(|r| r.strip_prefix(" "))
.unwrap_or_else(|| panic!("{action:?} is not `{glyph}` + two spaces: {msg:?}"));
assert_eq!(
Notice::new(level, glyph, body).gap(2).line(),
msg,
"Notice must reproduce {action:?}'s bytes exactly"
);
}
}
#[test]
#[ignore = "visual preview; run with --ignored --nocapture"]
fn compression_notice_visual_preview() {
use super::compression_notice_text;
use crate::agentic::compress::CompressAction;
let paint = |loud: bool, s: &str| {
if loud {
format!("\x1b[31m{s}\x1b[0m") } else {
format!("\x1b[33m{s}\x1b[0m") }
};
println!("\n compression-notice registers (24.7):");
for action in [
CompressAction::Pruned,
CompressAction::Summarized,
CompressAction::StaticFallback,
] {
let (msg, loud) = compression_notice_text(action, 1_024_000, 600_000, "");
println!(" {}", paint(loud, &msg));
}
println!();
}
#[test]
#[ignore = "visual preview; run with --ignored --nocapture"]
fn gauge_visual_preview() {
use super::{fmt_token_gauge, fmt_tokens_compact, gauge_level, GaugeLevel};
use crossterm::style::Color;
let color = |lvl: GaugeLevel| match lvl {
GaugeLevel::Ok => Color::Green,
GaugeLevel::Warn => Color::DarkYellow,
GaugeLevel::Critical => Color::Red,
};
let paint = |c: Color, s: &str| match c {
Color::Green => format!("\x1b[32m{s}\x1b[0m"),
Color::DarkYellow => format!("\x1b[33m{s}\x1b[0m"),
Color::Red => format!("\x1b[31m{s}\x1b[0m"),
_ => s.to_string(),
};
let budget = 1_024_000;
println!("\n context-budget gauge — fraction form (live header):");
for used in [102_000u32, 512_000, 800_000, 972_000, 1_010_000] {
let lvl = gauge_level(used, budget);
let g = fmt_token_gauge(used, budget);
println!(" {:<14} {:?}", paint(color(lvl), &g), lvl);
}
println!("\n compact budget form (1M = 1024k):");
for n in [899_000u32, 1_024_000, 1_536_000, 2_048_000] {
println!(" {n:>9} → {}", fmt_tokens_compact(n));
}
println!(
"\n mock header:\n [2026-06-22 14:32:01] vi --INSERT-- nemotron @ REDACTED-HOST {}\n",
paint(color(gauge_level(972_000, budget)), &fmt_token_gauge(972_000, budget)),
);
}
#[test]
fn printers_cover_every_branch_without_panicking() {
for color in [true, false] {
for verbose in [true, false] {
print_newt("narrator line", color, verbose);
}
print_list_item("name · ollama · model @ url", true, color);
print_list_item("name · ollama · model @ url", false, color);
print_harness_notice(
"over budget — dispatching and letting the backend decide",
color,
);
}
}
}