use crossterm::style::Color;
use crate::agent::agent_loop::message::EscalationReason;
use crate::agent::agent_loop::tool_input_repair::RepairStatsSnapshot;
use crate::ui::renderer::Renderer;
use crate::ui::text_output::{
strip_leading_system_reminder, write_critic_lines, write_system_lines, write_user_lines,
};
use crate::ui::theme;
pub(crate) fn handle_user_message(renderer: &mut Renderer, content: &str) -> std::io::Result<()> {
let visible = strip_leading_system_reminder(content);
if let Some(body) = crate::ui::events::finalization_nudge_body(visible) {
write_critic_lines(renderer, body)?;
return renderer.write_line("", Color::White);
}
write_user_lines(renderer, visible)?;
renderer.write_line("", Color::White)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_user_message_after_response(
renderer: &mut Renderer,
content: &str,
response_buf: &mut String,
response_start_line: &mut Option<usize>,
reasoning_buf: &mut String,
reasoning_start_line: &mut Option<usize>,
agent_line_started: &mut bool,
) -> anyhow::Result<()> {
if !response_buf.is_empty() {
crate::ui::agent_io::render_agent_stream(
response_buf,
response_start_line,
crate::ui::colors::c_agent(),
renderer,
)?;
if *agent_line_started {
renderer.write_line("", Color::White)?;
}
}
*agent_line_started = false;
response_buf.clear();
*response_start_line = None;
reasoning_buf.clear();
*reasoning_start_line = None;
handle_user_message(renderer, content)?;
Ok(())
}
pub(crate) fn handle_system_notice(renderer: &mut Renderer, content: &str) -> std::io::Result<()> {
write_system_lines(renderer, content)?;
renderer.write_line("", Color::White)
}
pub(crate) fn handle_retry_notice(
renderer: &mut Renderer,
attempt: u32,
delay_ms: u64,
) -> std::io::Result<()> {
renderer.write_line(
&format!(" ⟳ retry {attempt} ({delay_ms}ms)…"),
theme::dim(),
)
}
pub(crate) fn handle_escalation_activated(
renderer: &mut Renderer,
provider: &str,
reason: &EscalationReason,
) -> std::io::Result<()> {
let summary = reason.summary();
renderer.write_line(
&format!(" ↑ escalating to {provider} (next turn): {summary}"),
theme::dim(),
)
}
pub(crate) fn handle_repair_stats(
renderer: &mut Renderer,
snapshot: &RepairStatsSnapshot,
) -> std::io::Result<()> {
let mut parts: Vec<String> = Vec::new();
if snapshot.md_link_unwrapped > 0 {
parts.push(format!("{} md-link", snapshot.md_link_unwrapped));
}
if snapshot.null_stripped > 0 {
parts.push(format!("{} null-strip", snapshot.null_stripped));
}
if snapshot.json_string_to_array > 0 {
parts.push(format!("{} json-array", snapshot.json_string_to_array));
}
if snapshot.object_to_array > 0 {
parts.push(format!("{} obj-to-array", snapshot.object_to_array));
}
if snapshot.bare_string_to_array > 0 {
parts.push(format!("{} bare-to-array", snapshot.bare_string_to_array));
}
let total = snapshot.total_successful();
let mut line = format!(" ⊕ repaired {total} input(s): {}", parts.join(", "));
if snapshot.invalid > 0 {
line.push_str(&format!("; {} invalid", snapshot.invalid));
}
renderer.write_line(&line, theme::dim())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::agent_loop::critic::CRITIC_TAG;
use crate::ui::agent_io::render_agent_stream;
use crate::ui::colors::c_agent;
use crate::ui::renderer::Renderer;
fn has_line_containing(r: &Renderer, needle: &str) -> bool {
r.buffer_lines()
.iter()
.any(|l| crate::ui::ansi::strip_ansi(l).contains(needle))
}
#[test]
fn critic_nudge_survives_the_next_turn_stream() {
let mut r = Renderer::new().unwrap();
let mut response_buf = String::from("here is the answer");
let mut response_start_line: Option<usize> = None;
let mut reasoning_buf = String::new();
let mut reasoning_start_line: Option<usize> = None;
let mut agent_line_started = true;
render_agent_stream(&response_buf, &mut response_start_line, c_agent(), &mut r).unwrap();
assert!(response_start_line.is_some(), "turn 1 anchored the stream");
handle_user_message_after_response(
&mut r,
&format!("{CRITIC_TAG} verify the build before finishing"),
&mut response_buf,
&mut response_start_line,
&mut reasoning_buf,
&mut reasoning_start_line,
&mut agent_line_started,
)
.unwrap();
assert_eq!(response_start_line, None);
assert!(response_buf.is_empty());
assert!(
has_line_containing(&r, "verify the build"),
"critic nudge rendered"
);
response_buf.push_str("ok, ran the tests");
render_agent_stream(&response_buf, &mut response_start_line, c_agent(), &mut r).unwrap();
assert!(
has_line_containing(&r, "verify the build"),
"critic nudge must survive the next turn's stream",
);
assert!(has_line_containing(&r, "ran the tests"));
}
#[test]
fn ordinary_user_message_renders_normally() {
let mut r = Renderer::new().unwrap();
let mut response_buf = String::new();
let mut response_start_line: Option<usize> = None;
let mut reasoning_buf = String::new();
let mut reasoning_start_line: Option<usize> = None;
let mut agent_line_started = false;
handle_user_message_after_response(
&mut r,
"what time is it?",
&mut response_buf,
&mut response_start_line,
&mut reasoning_buf,
&mut reasoning_start_line,
&mut agent_line_started,
)
.unwrap();
assert!(has_line_containing(&r, "what time is it?"));
}
}