use crate::sink::UiEvent;
use crate::tui_context::TuiContext;
use crate::tui_types::TuiState;
use koda_core::engine::{EngineCommand, EngineEvent};
use koda_core::persistence::Persistence;
use ratatui::{
style::{Color, Style},
text::{Line, Span},
};
use tokio::sync::mpsc;
impl TuiContext {
pub(super) async fn post_turn_cleanup(
&mut self,
ui_rx: &mut mpsc::UnboundedReceiver<UiEvent>,
cmd_rx: &mut mpsc::Receiver<EngineCommand>,
) {
self.tui_state = TuiState::Idle;
self.inference_start = None;
while let Ok(UiEvent::Engine(e)) = ui_rx.try_recv() {
if let EngineEvent::ContextUsage { used, max } = &e {
self.context_pct = if *max > 0 {
(used * 100 / max) as u32
} else {
0
};
}
if let EngineEvent::ContextWindowChanged { tokens } = &e {
self.learned_context_window = Some(*tokens);
}
if let EngineEvent::ChildAgentActivity { task_id, kind, .. } = &e {
self.child_activity.record_activity(*task_id, kind);
}
self.renderer.render_to_buffer(e, &mut self.scroll_buffer);
}
super::crossterm_handler::close_dialog(
&mut self.menu,
&mut self.prompt_mode,
&mut self.pending_approval_id,
&mut self.textarea,
);
let unsent = self.session.take_queued_steers(cmd_rx);
if !unsent.is_empty() {
let has_draft = !self.textarea.text().is_empty();
let draft = unsent_steers_as_draft(&unsent, has_draft);
self.textarea.insert_str_at(0, &draft);
self.scroll_buffer.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"\u{21a9} Not sent to the model. It's back in the input box.",
Style::default().fg(Color::Yellow),
),
]));
}
if let Some(tokens) = self.learned_context_window.take() {
self.config.max_context_tokens = tokens;
self.config.model_settings.max_context_tokens = tokens;
}
self.maybe_auto_compact().await;
}
async fn maybe_auto_compact(&mut self) {
let ctx_pct = self.context_pct as usize;
if ctx_pct < koda_core::inference_helpers::AUTO_COMPACT_THRESHOLD {
return;
}
let pending = self
.session
.db
.has_pending_tool_calls(&self.session.id)
.await
.unwrap_or(false);
if pending {
if !self.silent_compact_deferred {
self.scroll_buffer.push(
Line::from(vec![
Span::raw(" "),
Span::styled(
format!(
"\u{1f43b} Context at {ctx_pct}% \u{2014} deferring compact (tool calls pending)"
),
Style::default().fg(Color::Yellow),
),
]),
);
self.silent_compact_deferred = true;
}
return;
}
self.silent_compact_deferred = false;
self.scroll_buffer.push(Line::from(vec![
Span::raw(" "),
Span::styled(
format!("\u{1f43b} Context at {ctx_pct}% \u{2014} auto-compacting..."),
Style::default().fg(Color::Cyan),
),
]));
match koda_core::compact::compact_session(
&self.session.db,
&self.session.id,
self.config.max_context_tokens,
&self.config.model_settings,
&self.provider,
)
.await
{
Ok(Ok(result)) => {
self.scroll_buffer.push(Line::styled(
format!(
" \u{2713} Compacted {} messages \u{2192} ~{} tokens",
result.deleted, result.summary_tokens
),
Style::default().fg(Color::Green),
));
}
Ok(Err(_skip)) => {} Err(e) => {
self.scroll_buffer.push(Line::styled(
format!(" \u{2717} Auto-compact failed: {e:#}"),
Style::default().fg(Color::Red),
));
}
}
}
}
fn unsent_steers_as_draft(steers: &[String], has_draft: bool) -> String {
let joined = steers.join("\n\n");
if has_draft {
format!("{joined}\n\n")
} else {
joined
}
}
#[cfg(test)]
mod tests {
use super::unsent_steers_as_draft;
#[test]
fn unsent_steers_keep_their_order() {
let steers = ["one".to_string(), "two".to_string()];
assert_eq!(unsent_steers_as_draft(&steers, false), "one\n\ntwo");
}
#[test]
fn unsent_steers_leave_room_for_the_draft() {
let steers = ["one".to_string()];
assert_eq!(unsent_steers_as_draft(&steers, true), "one\n\n");
}
}