use std::sync::mpsc::{Receiver, SyncSender};
use std::sync::{Arc, Mutex};
use crate::{
compact_session, estimate_session_tokens, CompactionConfig, ContentBlock, ConversationRuntime,
Session,
};
use crate::api::{tui_text_callback, OllamaApiClient};
use crate::executor::SecretaryToolExecutor;
use crate::memory::try_load_memory;
use crate::prompt::secretary_system_prompt_with_memory;
use crate::run::{build_permission_policy, compact_threshold, current_model, save_session};
use crate::tool_groups::{ToolGroup, ToolRegistry};
use crate::tui_events::{TuiEvent, UserInput};
use crate::tui_executor::TuiToolExecutor;
type TuiRuntime = ConversationRuntime<OllamaApiClient, TuiToolExecutor>;
fn build_tui_runtime(session: Session, tui_tx: SyncSender<TuiEvent>) -> TuiRuntime {
let reg = ToolRegistry::new();
let registry = Arc::new(Mutex::new(reg));
let api_client = OllamaApiClient::with_registry(current_model(), registry.clone())
.with_text_callback(tui_text_callback(tui_tx.clone()));
let hinter_registry = Arc::clone(®istry);
let inner = SecretaryToolExecutor::with_registry(registry);
let executor = TuiToolExecutor::new(inner, tui_tx);
let policy = build_permission_policy();
let memory = try_load_memory();
ConversationRuntime::new(
session,
api_client,
executor,
policy,
secretary_system_prompt_with_memory(memory.as_deref(), false),
)
.with_max_iterations(crate::run::max_iterations())
.with_auto_compaction_input_tokens_threshold(u32::MAX)
.with_unknown_tool_hinter(move |name: &str| {
ToolGroup::parse(name).map_or_else(Vec::new, |group| {
let reg = match hinter_registry.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
reg.group_tool_names(group)
})
})
}
fn maybe_compact(runtime: &mut TuiRuntime, tui_tx: &SyncSender<TuiEvent>) -> Option<usize> {
let estimated = estimate_session_tokens(runtime.session());
if estimated < compact_threshold() {
return None;
}
let result = compact_session(
runtime.session(),
CompactionConfig {
preserve_recent_messages: 4,
max_estimated_tokens: 0,
},
);
if result.removed_message_count == 0 {
return None;
}
let removed = result.removed_message_count;
*runtime = build_tui_runtime(result.compacted_session, tui_tx.clone());
Some(removed)
}
pub fn spawn_worker(
session: Session,
user_rx: Receiver<UserInput>,
tui_tx: SyncSender<TuiEvent>,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let mut runtime = build_tui_runtime(session, tui_tx.clone());
while let Ok(input) = user_rx.recv() {
match input {
UserInput::Quit => break,
UserInput::SlashCommand(cmd) => match cmd.trim() {
"clear" => {
runtime = build_tui_runtime(Session::default(), tui_tx.clone());
let _ = tui_tx.send(TuiEvent::SessionReset);
}
"compact" => {
if let Some(removed) = maybe_compact(&mut runtime, &tui_tx) {
let _ = tui_tx.send(TuiEvent::Compacted { removed });
} else {
let _ = tui_tx.send(TuiEvent::TurnError(
"Session is below compaction threshold — nothing to compact."
.to_string(),
));
}
}
other => {
let _ = tui_tx.send(TuiEvent::TurnError(format!(
"Unknown command: /{other} (available: /clear, /compact)"
)));
}
},
UserInput::Message { text, images } => {
let _ = tui_tx.send(TuiEvent::Working(true));
crate::tools::set_current_turn_paths(crate::tools::extract_user_prompt_paths(
&text,
));
let image_pairs: Vec<(String, String)> = images
.into_iter()
.map(|att| (att.media_type, att.data_b64))
.collect();
let turn_result = if image_pairs.is_empty() {
runtime.run_turn(&text, None)
} else {
runtime.run_turn_with_images(&text, image_pairs, None)
};
match turn_result {
Ok(summary) => {
let response = summary
.assistant_messages
.last()
.and_then(|m| {
m.blocks.iter().find_map(|b| {
if let ContentBlock::Text { text } = b {
Some(text.clone())
} else {
None
}
})
})
.unwrap_or_default();
let _ = tui_tx.send(TuiEvent::TurnComplete {
text: response,
iterations: summary.iterations as u32,
in_tok: summary.usage.input_tokens,
out_tok: summary.usage.output_tokens,
});
if let Some(removed) = maybe_compact(&mut runtime, &tui_tx) {
let _ = tui_tx.send(TuiEvent::Compacted { removed });
}
let estimated = estimate_session_tokens(runtime.session());
let _ = tui_tx.send(TuiEvent::TokensUpdate {
estimated,
threshold: compact_threshold(),
});
if let Err(e) = save_session(runtime.session()) {
eprintln!("tui worker: session save failed: {e:#}");
} else {
let _ = tui_tx.send(TuiEvent::Saved);
}
}
Err(e) => {
let _ = tui_tx.send(TuiEvent::TurnError(e.to_string()));
}
}
let _ = tui_tx.send(TuiEvent::Working(false));
}
}
}
})
}