#[allow(unused_imports)]
use crate::sync_util::LockExt;
use compact_str::CompactString;
use crossterm::style::Color;
use tokio::sync::mpsc;
use crate::context::ContextFiles;
use crate::event::AgentEvent;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
use crate::provider::AnyAgent;
use crate::session::MessageRole;
use crate::ui::agent_io::persist_turn_to_db;
use crate::ui::avatar;
use crate::ui::colors::{c_agent, c_error};
use crate::ui::run_handlers::{AgentBuildDeps, RunCtx};
use crate::ui::slash::handle_compress;
use crate::ui::theme;
use crate::ui::tool_display::{chamber_bottom, chamber_widths};
#[cfg(feature = "loop")]
pub(crate) struct LoopBits<'a> {
pub state: &'a mut Option<crate::extras::r#loop::LoopState>,
pub label: &'a mut Option<String>,
}
#[allow(clippy::too_many_arguments, clippy::await_holding_lock, unused_mut)]
pub(crate) async fn handle_done(
ctx: &mut RunCtx<'_>,
mut response: CompactString,
tokens: u64,
cost: f64,
was_reasoning: &mut bool,
is_running: &mut bool,
agent: &mut AnyAgent,
context: &mut ContextFiles,
deps: &AgentBuildDeps<'_>,
agent_rx: &mut Option<mpsc::Receiver<AgentEvent>>,
agent_abort: &mut Option<tokio::task::JoinHandle<()>>,
agent_interject: &mut Option<mpsc::Sender<()>>,
agent_cancel: &mut Option<mpsc::Sender<()>>,
interjection_queue: &std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
#[cfg(feature = "plugin")] plugin_manager: Option<
&std::sync::Arc<std::sync::Mutex<PluginManager>>,
>,
#[cfg(feature = "loop")] loop_bits: LoopBits<'_>,
) -> anyhow::Result<()> {
let client = deps.client;
let permission = deps.permission;
let ask_tx = deps.ask_tx;
let question_tx = deps.question_tx;
let plan_tx = deps.plan_tx;
let user_tx = deps.user_tx;
let bg_store = deps.bg_store;
let sandbox = deps.sandbox;
#[cfg(feature = "mcp")]
let mcp_manager = deps.mcp_manager;
#[cfg(feature = "semantic")]
let semantic_manager = deps.semantic_manager;
#[cfg(feature = "lsp")]
let lsp_manager = deps.lsp_manager;
*was_reasoning = false;
if *ctx.tool_chamber_open {
let drop_chamber = match (*ctx.chamber_top_start, *ctx.chamber_top_end) {
(Some(_), Some(end)) => ctx.renderer.buffer_len() == end,
_ => false,
};
if drop_chamber {
if let Some(start) = *ctx.chamber_top_start {
ctx.renderer.replace_from(start, Vec::new());
}
} else {
let (frame_w, _) = chamber_widths(ctx.renderer);
ctx.renderer
.write_line_raw(&chamber_bottom(frame_w), theme::dim())?;
}
*ctx.tool_chamber_open = false;
*ctx.chamber_top_start = None;
*ctx.chamber_top_end = None;
}
*ctx.last_tool_name = None;
ctx.renderer.set_avatar_state(avatar::AvatarState::Done);
#[cfg(feature = "experimental-ui-terminal-tab")]
ctx.renderer.set_last_tool_name("");
#[allow(unused_mut, unused_variables)]
let mut plugin_followup: Option<String> = None;
#[cfg(feature = "plugin")]
if let Some(pm) = plugin_manager {
let mut mgr = pm.lock_ignore_poison();
match mgr.dispatch(
"on-response",
&format!(
"@{{:response \"{}\"}}",
crate::plugin::escape_janet_string(&response)
),
) {
Ok(results) if !results.is_empty() => {
for line in &results {
let safe = crate::ui::events::sanitize_output(line);
ctx.renderer
.write_line(&format!("[plugin] {}", safe), theme::dim())?;
}
plugin_followup = Some(results.join("\n"));
}
Ok(_) => {}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] on-response error: {e}"), c_error())?;
}
}
if let Some(pending) = mgr.take_pending_prompt() {
plugin_followup = Some(pending);
}
match mgr.dispatch(
"message-end",
&format!(
"@{{:message \"{}\"}}",
crate::plugin::escape_janet_string(&response)
),
) {
Ok(_) => {
if let Some(rewritten) = mgr.take_message_rewrite() {
response = compact_str::CompactString::new(&rewritten);
}
}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] message-end error: {e}"), c_error())?;
}
}
mgr.store_response(&response);
match mgr.dispatch("on-complete", "@{}") {
Ok(_) => {}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] on-complete error: {e}"), c_error())?;
}
}
match mgr.dispatch("prepare-next-run", "@{}") {
Ok(_) => {}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] prepare-next-run error: {e}"), c_error())?;
}
}
let pending_next_model = mgr.take_pending_next_model();
drop(mgr);
if let Some(next_model) = pending_next_model {
let trimmed = next_model.trim();
if !trimmed.is_empty() && trimmed != ctx.session.model.as_str() {
let new_model_compact = CompactString::new(trimmed);
let model_obj = client.completion_model(new_model_compact.to_string());
*agent = crate::provider::build_agent(
model_obj,
ctx.cli,
ctx.cfg,
context,
permission.clone(),
ask_tx.clone(),
question_tx.clone(),
plan_tx.clone(),
bg_store.clone(),
#[cfg(feature = "lsp")]
lsp_manager.cloned(),
sandbox.clone(),
#[cfg(feature = "mcp")]
mcp_manager,
#[cfg(feature = "semantic")]
semantic_manager,
Some(ctx.session.id.to_string()),
)
.await;
let old_model = ctx.session.model.clone();
ctx.session.model = new_model_compact.clone();
ctx.session.provider = ctx.cli.resolve_provider(ctx.cfg);
let new_ctx = ctx.cfg.resolve_context_window(new_model_compact.as_str());
if new_ctx != ctx.session.context_window {
ctx.session.context_window = new_ctx;
}
ctx.renderer.write_line(
&format!(
"[plugin] swapped model: {} → {}",
old_model, new_model_compact,
),
c_agent(),
)?;
}
}
{
let mut mgr = pm.lock_ignore_poison();
let _ = mgr.eval("(set harness-response nil)");
}
}
if !ctx.response_buf.is_empty() {
ctx.renderer.stream(ctx.response_buf, c_agent(), true);
ctx.renderer.render_viewport()?;
} else if !*ctx.agent_line_started {
ctx.renderer.write("<dirge> ", c_agent())?;
}
ctx.renderer.commit_stream();
ctx.renderer.write_line("", Color::White)?;
ctx.renderer.write_line("", Color::White)?;
ctx.session.add_message_with_tool_calls(
MessageRole::Assistant,
&response,
std::mem::take(ctx.tool_calls_buf),
);
ctx.session.total_tokens = ctx.session.total_tokens.saturating_add(tokens);
ctx.session.total_cost += cost;
*ctx.tool_calls_this_run = 0;
*ctx.agent_line_started = false;
ctx.response_buf.clear();
*ctx.response_start_line = None;
ctx.end_reasoning();
*ctx.reasoning_start_line = None;
#[cfg(feature = "loop")]
let loop_running = loop_bits.state.as_ref().is_some_and(|ls| ls.active);
#[cfg(not(feature = "loop"))]
let loop_running = false;
if !loop_running
&& ctx.cfg.resolve_compact_enabled()
&& ctx
.session
.needs_compaction(ctx.cfg.resolve_reserve_tokens())
&& !ctx.cli.no_session
{
ctx.renderer
.write_line("▒░ auto-compacting context ░▒", theme::accent())?;
let compress_result = handle_compress(
None,
false, agent,
client,
ctx.renderer,
ctx.session,
ctx.cli,
ctx.cfg,
context,
permission,
ask_tx,
question_tx,
plan_tx,
user_tx,
bg_store,
sandbox,
#[cfg(feature = "mcp")]
mcp_manager,
#[cfg(feature = "semantic")]
semantic_manager,
#[cfg(feature = "lsp")]
lsp_manager,
)
.await;
if let Err(e) = compress_result {
ctx.renderer.write_line(
"╭─ ⚠ AUTO-COMPACT FAILED ─────────────────────────────╮",
c_error(),
)?;
let cause = {
let s = e.to_string().replace('\n', " ");
if s.chars().count() > 64 {
let mut out: String = s.chars().take(63).collect();
out.push('…');
out
} else {
s
}
};
ctx.renderer
.write_line(&format!("│ cause: {}", cause), c_error())?;
ctx.renderer.write_line(
"│ context is over the threshold — replies may start",
c_error(),
)?;
ctx.renderer
.write_line("│ hitting context-length errors. Try /compress", c_error())?;
ctx.renderer.write_line(
"│ manually, /clear to start fresh, or restart with",
c_error(),
)?;
ctx.renderer
.write_line("│ a larger context_window in config.", c_error())?;
ctx.renderer.write_line(
"╰─────────────────────────────────────────────────────╯",
c_error(),
)?;
}
}
if !ctx.cli.no_session
&& let Err(e) = crate::session::storage::save_session(ctx.session)
{
ctx.renderer.write_line(
&format!("warning: failed to save session: {}", e),
c_error(),
)?;
}
*is_running = false;
if let Some(h) = agent_abort.take() {
h.abort();
}
*agent_rx = None;
*agent_interject = None;
*agent_cancel = None;
#[cfg(feature = "plugin")]
let followup_for_decision = plugin_followup.clone();
#[cfg(not(feature = "plugin"))]
let followup_for_decision: Option<String> = None;
#[cfg(feature = "loop")]
let (loop_active, loop_should_stop) = loop_bits
.state
.as_ref()
.map(|ls| (ls.active, ls.active && ls.should_stop()))
.unwrap_or((false, false));
#[cfg(not(feature = "loop"))]
let (loop_active, loop_should_stop) = (false, false);
let action = crate::plugin::decide_post_done_action(
followup_for_decision,
loop_active,
loop_should_stop,
);
match action {
crate::plugin::PostDoneAction::Followup(text) => {
let followup_prompt = text + "\n\nContinue.";
ctx.last_user_prompt.clone_from(&followup_prompt);
let runner = agent.clone().spawn_runner(
crate::agent::tools::background::prepend_pending_notifications(
&followup_prompt,
bg_store.as_ref(),
),
crate::agent::runner::convert_history(ctx.session),
Some(interjection_queue.clone()),
);
runner.install_into(
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
);
}
crate::plugin::PostDoneAction::LoopStop =>
{
#[cfg(feature = "loop")]
if let Some(ls) = loop_bits.state.as_mut() {
ctx.renderer.write_line(
&format!("[loop] max iterations ({}) reached, stopping", ls.iteration),
c_agent(),
)?;
ls.active = false;
*loop_bits.label = None;
}
}
crate::plugin::PostDoneAction::LoopIter =>
{
#[cfg(feature = "loop")]
if let Some(ls) = loop_bits.state.as_mut() {
let summary: String = response.chars().take(200).collect();
ls.last_summary = Some(summary);
ls.iteration += 1;
let prompt = ls.build_prompt();
ctx.last_user_prompt.clone_from(&prompt);
let runner = agent.clone().spawn_runner(
crate::agent::tools::background::prepend_pending_notifications(
&prompt,
bg_store.as_ref(),
),
Vec::new(),
Some(interjection_queue.clone()),
);
runner.install_into(
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
);
*loop_bits.label = Some(ls.iteration_label());
ctx.renderer.write_line(
&format!("[loop] launching {}", ls.iteration_label()),
c_agent(),
)?;
}
}
crate::plugin::PostDoneAction::Idle => {}
}
super::plan_review::drive_plan_review(
ctx,
agent,
bg_store,
interjection_queue,
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
)
.await?;
if !*is_running {
let cwd = std::env::current_dir().unwrap_or_else(|_| ".".into());
let paths = crate::extras::dirge_paths::ProjectPaths::new(&cwd);
persist_turn_to_db(
ctx.session,
ctx.last_user_prompt,
&response,
ctx.tool_calls_buf,
);
let base = crate::agent::review::build_transcript(ctx.session);
let transcript =
crate::agent::session_digest::review_transcript(ctx.session, Some(&paths.root), base);
crate::agent::post_session::spawn_post_session(agent.clone(), paths, transcript);
}
if !*is_running && !interjection_queue.lock().unwrap().is_empty() {
let queued: Vec<String> = interjection_queue.lock().unwrap().drain(..).collect();
let combined = queued.join("\n\n");
ctx.last_user_prompt.clone_from(&combined);
let history = crate::agent::runner::convert_history(ctx.session);
ctx.session.add_message(MessageRole::User, &combined);
let runner = agent.clone().spawn_runner(
crate::agent::tools::background::prepend_pending_notifications(
&combined,
bg_store.as_ref(),
),
history,
Some(interjection_queue.clone()),
);
runner.install_into(
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
);
}
Ok(())
}