use super::App;
use crate::agent::context::ConversationContext;
use crate::agent::session::SessionSnapshot;
use crate::tui::state::{ChatMessage, MessageRole};
use super::utils::truncate;
impl App {
pub async fn try_resume_session(&mut self) {
if let Some(journal_id) =
crate::agent::session::detect_incomplete_journal(&self.working_dir).await
{
tracing::debug!(session_id = %journal_id, "Detected incomplete session via journal");
}
if let Some(snapshot) = self.session_store.find_incomplete().await {
let task_preview = snapshot.user_task.as_deref().unwrap_or("(unknown task)");
let msg = format!(
"Incomplete session detected: \"{}\"\n Session: {}\n Messages: {}\n Type /resume to restore, or start a new conversation.",
truncate(task_preview, 80),
&snapshot.session_id[..8],
snapshot.messages.len(),
);
self.state
.messages
.push(ChatMessage::text(MessageRole::System, msg));
self.state.pending_resume = Some(snapshot);
}
}
pub fn force_resume(&mut self, snapshot: SessionSnapshot) {
self.resume_from_snapshot(snapshot);
}
pub(super) fn resume_from_snapshot(&mut self, snapshot: SessionSnapshot) {
let msg_count = snapshot.messages.len();
let task_preview = snapshot.user_task.as_deref().unwrap_or("(unknown task)");
let session_short = snapshot.session_id[..8.min(snapshot.session_id.len())].to_string();
if let Some(ref model) = snapshot.model {
self.state.model_name = model.clone();
self.config.model = model.clone();
self.client.model = model.clone();
self.config.cli = None;
self.config.cli_args = Vec::new();
if let Ok(file) = crate::config::load_config_file()
&& let Some(entry) = file
.providers
.iter()
.find(|pe| pe.all_models().contains(&model.as_str()))
&& entry.is_cli()
{
self.config.cli = entry.cli.clone();
self.config.cli_args = entry.cli_args.clone();
self.state.provider_name = entry.name.clone();
}
}
let context = ConversationContext::restore_with_budget(
snapshot.system_prompt,
snapshot.messages,
snapshot.last_reasoning,
self.config.context_max_tokens,
self.config.compaction_threshold,
);
self.state.context_used_tokens = context.used_tokens();
self.state.context_max_tokens = context.max_context_tokens();
self.state.compaction_count = context.compaction_count();
self.context = Some(context);
self.session_id = snapshot.session_id;
if let Some(ref ui) = snapshot.ui_state {
use crate::tui::state::{
ChangedFileEntry as CFE, ChatMessage as CM, TokenStats as TS, ToolLogEntry as TLE,
};
if let Some(val) = ui.get("messages").cloned() {
match serde_json::from_value::<Vec<CM>>(val) {
Ok(msgs) => self.state.messages = msgs,
Err(e) => tracing::warn!("Session restore: failed to parse messages: {e}"),
}
}
if let Some(val) = ui.get("tool_log").cloned() {
match serde_json::from_value::<Vec<TLE>>(val) {
Ok(log) => self.state.tool_log = log,
Err(e) => tracing::warn!("Session restore: failed to parse tool_log: {e}"),
}
}
if let Some(val) = ui.get("changed_files").cloned() {
match serde_json::from_value::<Vec<CFE>>(val) {
Ok(files) => self.state.changed_files = files,
Err(e) => tracing::warn!("Session restore: failed to parse changed_files: {e}"),
}
}
if let Some(v) = ui.get("iteration").and_then(|v| v.as_u64()) {
self.state.iteration = v as u32;
}
if let Some(v) = ui.get("elapsed_secs").and_then(|v| v.as_u64()) {
self.state.elapsed_secs = v;
}
if let Some(val) = ui.get("token_stats").cloned() {
match serde_json::from_value::<TS>(val) {
Ok(ts) => self.state.token_stats = ts,
Err(e) => tracing::warn!("Session restore: failed to parse token_stats: {e}"),
}
}
if let Some(v) = ui.get("compaction_count").and_then(|v| v.as_u64()) {
self.state.compaction_count = v as usize;
}
if let Some(v) = ui.get("cache_hit_rate").and_then(|v| v.as_f64()) {
self.state.cache_hit_rate = v;
}
if let Some(v) = ui.get("agent_mode").and_then(|v| v.as_str()) {
self.state.agent_mode = v.to_string();
}
if let Some(val) = ui.get("input_history").cloned() {
match serde_json::from_value::<Vec<String>>(val) {
Ok(history) => self.input_history = history,
Err(e) => tracing::warn!("Session restore: failed to parse input_history: {e}"),
}
}
}
self.state.messages.push(ChatMessage::text(
MessageRole::System,
format!(
"Session restored ({session_short}, {msg_count} messages, {} iterations)\n Task: \"{}\"\n Continue from where you left off.",
self.state.iteration,
truncate(task_preview, 80),
)
));
tracing::info!(session_id = %self.session_id, "Session resumed with full UI state");
}
pub fn open_session_resume_popup(&mut self) {
use crate::tui::state::{PopupKind, PopupState};
if self.state.session_snapshots.is_empty() {
self.state.messages.push(ChatMessage::text(
MessageRole::System,
"No saved sessions found.",
));
return;
}
let items: Vec<(String, String, String, String)> = self
.state
.session_snapshots
.iter()
.map(|snap| {
let short_id = snap.session_id.chars().take(8).collect::<String>();
let ts = snap
.timestamp
.get(..16)
.unwrap_or(&snap.timestamp)
.to_string();
let status = if snap.completed { "done " } else { "active " }.to_string();
let task = snap.user_task.as_deref().unwrap_or("(no task)").to_string();
(short_id, ts, status, task)
})
.collect();
self.state.popup = Some(PopupState {
title: "Resume Session".to_string(),
content: String::new(),
scroll: 0,
kind: PopupKind::SessionResume { items, selected: 0 },
saved_theme: None,
select_prefix: None,
search: String::new(),
});
}
pub(super) async fn save_session(&self, completed: bool) {
let context = match self.context.as_ref() {
Some(ctx) => ctx,
None => match self.last_context_backup.as_ref() {
Some(ctx) => ctx,
None => {
tracing::warn!(
"Cannot save session: no context available (agent may have been interrupted)"
);
return;
}
},
};
let user_task = context
.messages()
.iter()
.find(|m| m.role == "user")
.and_then(|m| m.content.as_ref().map(|c| c.text_content()));
let history_tail: Vec<&String> = self
.input_history
.iter()
.rev()
.take(10)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
let ui_state = serde_json::json!({
"messages": self.state.messages,
"tool_log": self.state.tool_log,
"changed_files": self.state.changed_files,
"iteration": self.state.iteration,
"elapsed_secs": self.state.elapsed_secs,
"token_stats": self.state.token_stats,
"compaction_count": self.state.compaction_count,
"cache_hit_rate": self.state.cache_hit_rate,
"agent_mode": self.state.agent_mode,
"input_history": history_tail,
});
let snapshot = SessionSnapshot {
session_id: self.session_id.clone(),
working_dir: self.working_dir.clone(),
system_prompt: context.system_prompt().to_string(),
messages: context.messages().to_vec(),
last_reasoning: None,
timestamp: chrono::Utc::now().to_rfc3339(),
completed,
user_task,
model: Some(self.config.model.clone()),
ui_state: Some(ui_state),
};
if let Err(e) = self.session_store.save(&snapshot).await {
tracing::warn!("Failed to save session: {e}");
} else {
match self.session_store.cleanup(50).await {
Ok(removed) if removed > 0 => {
tracing::debug!(removed, "Pruned old completed sessions");
}
Err(e) => tracing::warn!("Session cleanup failed: {e}"),
_ => {}
}
}
}
}