#[allow(unused_imports)]
use crate::sync_util::LockExt;
use std::time::Instant;
use crossterm::style::Color;
use crate::ui::agent_io::{RENDER_FRAME, render_agent_stream, should_render_token};
use crate::ui::avatar;
use crate::ui::colors::c_agent;
use crate::ui::events::sanitize_output;
use crate::ui::renderer::Renderer;
use crate::ui::run_handlers::RunCtx;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
#[cfg(feature = "plugin")]
use crate::ui::streaming::TokenBatcher;
#[cfg(feature = "plugin")]
use std::sync::{Arc, Mutex};
pub(crate) fn handle_reasoning(
ctx: &mut RunCtx<'_>,
text: &str,
was_reasoning: &mut bool,
) -> anyhow::Result<()> {
let safe = sanitize_output(text);
ctx.reasoning_buf.push_str(&safe);
render_agent_stream(
ctx.reasoning_buf,
ctx.reasoning_start_line,
crate::ui::theme::thinking(),
ctx.renderer,
)?;
*ctx.agent_line_started = true;
*was_reasoning = true;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_token(
ctx: &mut RunCtx<'_>,
text: &str,
was_reasoning: &mut bool,
last_token_render: &mut Option<Instant>,
pending: usize,
#[cfg(feature = "plugin")] plugin_manager: Option<&Arc<Mutex<PluginManager>>>,
#[cfg(feature = "plugin")] token_batcher: &mut TokenBatcher,
#[cfg(feature = "plugin")] current_turn_text: &mut String,
#[cfg(feature = "plugin")] current_turn_index: u32,
) -> anyhow::Result<()> {
ctx.renderer.set_avatar_state(avatar::AvatarState::Speaking);
if *was_reasoning {
ctx.renderer.write_line("", Color::White)?;
*was_reasoning = false;
ctx.response_buf.clear();
*ctx.response_start_line = None;
ctx.end_reasoning();
*ctx.reasoning_start_line = None;
}
let safe = sanitize_output(text);
ctx.response_buf.push_str(&safe);
#[cfg(feature = "plugin")]
if let Some(pm) = plugin_manager {
current_turn_text.push_str(text);
if token_batcher.push(text).is_some() {
let mut mgr = pm.lock_ignore_poison();
let _ = mgr.dispatch(
"on-message-update",
&format!(
"@{{:index {} :partial \"{}\"}}",
current_turn_index,
crate::plugin::escape_janet_string(current_turn_text),
),
);
}
}
let since = last_token_render.map_or(RENDER_FRAME, |t| t.elapsed());
if should_render_token(pending, since, RENDER_FRAME) {
render_agent_stream(
ctx.response_buf,
ctx.response_start_line,
c_agent(),
ctx.renderer,
)?;
*last_token_render = Some(Instant::now());
}
*ctx.agent_line_started = true;
Ok(())
}
pub(crate) fn render_queued_steering(
renderer: &mut Renderer,
response_buf: &mut String,
response_start_line: &mut Option<usize>,
text: &str,
notice: &str,
notice_color: Color,
) -> anyhow::Result<()> {
if !response_buf.is_empty() {
renderer.stream(response_buf, c_agent(), true);
renderer.render_viewport()?;
}
renderer.commit_stream();
for line in text.lines() {
let safe_line = sanitize_output(line);
renderer.write_line(&format!("» {}", safe_line), crate::ui::theme::dim())?;
}
renderer.write_line(notice, notice_color)?;
response_buf.clear();
*response_start_line = None;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::renderer::Renderer;
fn screen(r: &Renderer) -> String {
r.buffer_lines().join("\n")
}
#[test]
fn queued_steering_midstream_does_not_duplicate_partial() {
let mut r = Renderer::new().expect("renderer");
r.set_test_cols(80);
let mut response_buf = String::new();
let mut start: Option<usize> = None;
response_buf.push_str("ALPHAWORD BETAWORD GAMMAWORD");
r.stream(&response_buf, c_agent(), true);
render_queued_steering(
&mut r,
&mut response_buf,
&mut start,
"please refactor",
"(queued)",
Color::White,
)
.unwrap();
assert!(response_buf.is_empty());
assert!(start.is_none());
response_buf.push_str("DELTAWORD");
r.stream(&response_buf, c_agent(), true);
r.commit_stream();
let s = screen(&r);
assert_eq!(
s.matches("GAMMAWORD").count(),
1,
"partial duplicated:\n{s}"
);
assert_eq!(
s.matches("ALPHAWORD").count(),
1,
"partial duplicated:\n{s}"
);
assert!(s.contains("please refactor"), "missing echo:\n{s}");
assert!(s.contains("(queued)"), "missing notice:\n{s}");
assert!(s.contains("DELTAWORD"), "missing continuation:\n{s}");
}
#[test]
fn queued_steering_with_empty_buffer_just_echoes() {
let mut r = Renderer::new().expect("renderer");
r.set_test_cols(80);
let mut response_buf = String::new();
let mut start: Option<usize> = None;
render_queued_steering(
&mut r,
&mut response_buf,
&mut start,
"hello",
"(queued)",
Color::White,
)
.unwrap();
let s = screen(&r);
assert!(s.contains("hello"), "missing echo:\n{s}");
assert!(s.contains("(queued)"), "missing notice:\n{s}");
}
}