use super::history;
use crate::{
commands::BuiltInCommand,
sessions::{
Session, SessionEvent, SessionEventKind, SessionManager, parse_prune_sessions_days,
resolve_session_scope_path,
},
shell::{ShellState, handle_builtin},
tui::{
PADDED_INLINE_SEPARATOR, TuiEvent,
events::delivery::{EventDelivery, tui_event_delivery},
send_critical, state,
},
};
use crossbeam_channel::{Receiver, Sender};
use std::{
fs,
io::{BufRead, BufReader, Read, Seek, SeekFrom},
path::{Path, PathBuf},
};
pub(crate) fn preserve_critical_tui_events(
receiver: &Receiver<TuiEvent>,
sender: &Sender<TuiEvent>,
) -> anyhow::Result<()> {
let mut critical = Vec::new();
while let Ok(event) = receiver.try_recv() {
if tui_event_delivery(&event) == EventDelivery::Critical {
critical.push(event);
}
}
for event in critical {
send_critical(sender, event)?;
}
Ok(())
}
pub(crate) fn session_picker_rows(
manager: &SessionManager,
active_session_id: Option<&str>,
current_cwd: &Path,
) -> anyhow::Result<Vec<state::SessionPickerRow>> {
let current_session_path = resolve_session_scope_path(current_cwd);
let mut sessions = manager.list_metadata_summaries()?;
sessions.reverse();
Ok(sessions
.into_iter()
.filter(|summary| summary.session_path.as_deref() == Some(current_session_path.as_path()))
.map(|summary| {
let id = summary.session.id().to_string();
state::SessionPickerRow {
label: session_title_label(summary.session.id(), summary.latest_title.as_deref()),
is_current: active_session_id == Some(id.as_str()),
preview: None,
id,
}
})
.collect())
}
pub(crate) const SESSION_PREVIEW_MESSAGE_MAX_CHARS: usize = 2_000;
pub(crate) const SESSION_PREVIEW_MAX_FORWARD_EVENTS: usize = 256;
pub(crate) const SESSION_PREVIEW_MAX_RECENT_EVENTS: usize = 256;
pub(crate) const SESSION_PREVIEW_MAX_SCAN_BYTES: u64 = 256 * 1024;
pub(crate) fn load_session_preview(session: &Session) -> state::SessionPreviewState {
match load_session_preview_inner(session) {
Ok(preview) => preview,
Err(error) => state::SessionPreviewState::Error(compact_preview_error(&error.to_string())),
}
}
fn load_session_preview_inner(session: &Session) -> anyhow::Result<state::SessionPreviewState> {
if !session.path().exists() {
return Ok(state::SessionPreviewState::Empty);
}
let first = scan_first_preview_message(session)?;
let recent = scan_recent_preview_message(session)?;
let limited = first.limited || recent.limited;
match (first.message, recent.message) {
(Some(first), Some(recent)) if first == recent => {
if limited {
Ok(state::SessionPreviewState::Partial(
state::SessionPreviewPartial {
first: Some(first),
most_recent: None,
reason: "scan bounds reached",
},
))
} else {
Ok(state::SessionPreviewState::Ready(state::SessionPreview {
first,
most_recent: None,
}))
}
}
(Some(first), Some(recent)) => {
if limited {
Ok(state::SessionPreviewState::Partial(
state::SessionPreviewPartial {
first: Some(first),
most_recent: Some(recent),
reason: "scan bounds reached",
},
))
} else {
Ok(state::SessionPreviewState::Ready(state::SessionPreview {
first,
most_recent: Some(recent),
}))
}
}
(Some(first), None) => Ok(if limited {
state::SessionPreviewState::Partial(state::SessionPreviewPartial {
first: Some(first),
most_recent: None,
reason: "scan bounds reached",
})
} else {
state::SessionPreviewState::Ready(state::SessionPreview {
first,
most_recent: None,
})
}),
(None, Some(recent)) => Ok(state::SessionPreviewState::Partial(
state::SessionPreviewPartial {
first: None,
most_recent: Some(recent),
reason: "scan bounds reached before first message",
},
)),
(None, None) => Ok(if limited {
state::SessionPreviewState::Partial(state::SessionPreviewPartial {
first: None,
most_recent: None,
reason: "scan bounds reached",
})
} else {
state::SessionPreviewState::Empty
}),
}
}
#[derive(Debug)]
struct PreviewScanResult {
message: Option<state::PreviewMessage>,
limited: bool,
}
fn scan_first_preview_message(session: &Session) -> anyhow::Result<PreviewScanResult> {
let file = fs::File::open(session.path())?;
let mut bytes_read = 0u64;
let mut events_read = 0usize;
for (index, line) in BufReader::new(file).lines().enumerate() {
if events_read >= SESSION_PREVIEW_MAX_FORWARD_EVENTS {
return Ok(PreviewScanResult {
message: None,
limited: true,
});
}
let line = line?;
let line_bytes = line.len() as u64 + 1;
if bytes_read.saturating_add(line_bytes) > SESSION_PREVIEW_MAX_SCAN_BYTES {
return Ok(PreviewScanResult {
message: None,
limited: true,
});
}
bytes_read = bytes_read.saturating_add(line_bytes);
if line.trim().is_empty() {
continue;
}
events_read = events_read.saturating_add(1);
let event = parse_preview_event(&line, index + 1)?;
if let Some(message) = preview_message_from_event(&event)
&& message.role == "user"
{
return Ok(PreviewScanResult {
message: Some(message),
limited: false,
});
}
}
Ok(PreviewScanResult {
message: None,
limited: false,
})
}
fn scan_recent_preview_message(session: &Session) -> anyhow::Result<PreviewScanResult> {
let mut file = fs::File::open(session.path())?;
let len = file.metadata()?.len();
let start = len.saturating_sub(SESSION_PREVIEW_MAX_SCAN_BYTES);
file.seek(SeekFrom::Start(start))?;
let mut buffer = Vec::new();
file.take(SESSION_PREVIEW_MAX_SCAN_BYTES)
.read_to_end(&mut buffer)?;
if start > 0 {
if let Some(newline) = buffer.iter().position(|byte| *byte == b'\n') {
buffer.drain(..=newline);
} else {
buffer.clear();
}
}
let text = String::from_utf8_lossy(&buffer);
let lines = text.lines().collect::<Vec<_>>();
let mut assistant_chunks = Vec::new();
let mut events_read = 0usize;
for (reverse_index, line) in lines.iter().rev().enumerate() {
if line.trim().is_empty() {
continue;
}
if events_read >= SESSION_PREVIEW_MAX_RECENT_EVENTS {
return Ok(PreviewScanResult {
message: assistant_chunks_preview(&assistant_chunks),
limited: true,
});
}
events_read = events_read.saturating_add(1);
let event = parse_preview_event(line, lines.len().saturating_sub(reverse_index))?;
if let Some(message) = preview_message_from_event(&event) {
if message.role == "assistant" && event.kind() != Some(SessionEventKind::AssistantChunk)
{
return Ok(PreviewScanResult {
message: Some(message),
limited: start > 0,
});
}
if message.role == "assistant" {
assistant_chunks.push(message.text);
continue;
}
}
if event.kind() == Some(SessionEventKind::UserInput) {
return Ok(PreviewScanResult {
message: assistant_chunks_preview(&assistant_chunks),
limited: start > 0,
});
}
}
if let Some(message) = assistant_chunks_preview(&assistant_chunks) {
return Ok(PreviewScanResult {
message: Some(message),
limited: start > 0,
});
}
Ok(PreviewScanResult {
message: None,
limited: start > 0,
})
}
fn assistant_chunks_preview(chunks: &[String]) -> Option<state::PreviewMessage> {
if chunks.is_empty() {
return None;
}
let text = chunks.iter().rev().map(String::as_str).collect::<String>();
Some(state::PreviewMessage {
role: "assistant",
text: bounded_preview_text(&text),
})
}
fn parse_preview_event(line: &str, line_number: usize) -> anyhow::Result<SessionEvent> {
serde_json::from_str::<SessionEvent>(line)
.map_err(|error| anyhow::anyhow!("malformed session event at line {line_number}: {error}"))
}
fn preview_message_from_event(event: &SessionEvent) -> Option<state::PreviewMessage> {
let kind = event.kind()?;
let role = match kind {
SessionEventKind::UserInput => "user",
SessionEventKind::AssistantOutput | SessionEventKind::AssistantChunk => "assistant",
SessionEventKind::TurnStatus => "assistant",
_ => return None,
};
let field = if kind == SessionEventKind::TurnStatus {
"assistant_text"
} else {
"text"
};
let raw_text = event.payload.get(field)?.as_str()?;
let text = if kind == SessionEventKind::AssistantChunk {
if raw_text.trim().is_empty() {
return None;
}
raw_text
} else {
let trimmed = raw_text.trim();
if trimmed.is_empty() {
return None;
}
trimmed
};
Some(state::PreviewMessage {
role,
text: if kind == SessionEventKind::AssistantChunk {
text.to_string()
} else {
bounded_preview_text(text)
},
})
}
fn bounded_preview_text(text: &str) -> String {
let sanitized = crate::tui::transcript::sanitize_preview(text);
let mut chars = sanitized.chars();
let mut bounded = chars
.by_ref()
.take(SESSION_PREVIEW_MESSAGE_MAX_CHARS)
.collect::<String>();
if chars.next().is_some() {
bounded.push('…');
}
bounded
}
fn compact_preview_error(error: &str) -> String {
crate::tui::transcript::sanitize_preview(error)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.take(1)
.collect::<Vec<_>>()
.join(PADDED_INLINE_SEPARATOR)
}
pub(crate) const SESSION_SWITCH_MAX_EVENTS: usize = 20_000;
pub(crate) const SESSION_SWITCH_MAX_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone)]
pub(crate) struct SessionHydrationSnapshot {
pub(crate) transcript: std::collections::VecDeque<String>,
pub(crate) transcript_activity_links:
std::collections::HashMap<usize, crate::tui::transcript_cards::TranscriptActivityLink>,
pub(crate) nodes:
std::collections::HashMap<crate::output::ActivityId, crate::tui::activity::ActivityNode>,
pub(crate) roots: Vec<crate::output::ActivityId>,
pub(crate) children:
std::collections::HashMap<crate::output::ActivityId, Vec<crate::output::ActivityId>>,
pub(crate) expanded: std::collections::HashSet<crate::output::ActivityId>,
pub(crate) active_tool_transcripts:
std::collections::HashMap<crate::output::ActivityId, state::ActiveToolTranscript>,
pub(crate) context_usage: Option<state::ContextUsageState>,
pub(crate) selected: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct SessionSwitchLoad {
pub(crate) session: Session,
pub(crate) latest_title: Option<String>,
pub(crate) snapshot: SessionHydrationSnapshot,
pub(crate) diagnostics: Vec<String>,
pub(crate) status: String,
}
pub(crate) fn hydrate_session_history_snapshot(
events: &[SessionEvent],
injected_content_settings: &crate::config::InjectedContentSettings,
) -> SessionHydrationSnapshot {
let mut ui_state = state::MissionControlState::default();
history::hydrate_session_history(&mut ui_state, events, injected_content_settings);
SessionHydrationSnapshot {
transcript: ui_state.transcript,
transcript_activity_links: ui_state.transcript_activity_links,
nodes: ui_state.nodes,
roots: ui_state.roots,
children: ui_state.children,
expanded: ui_state.expanded,
active_tool_transcripts: ui_state.active_tool_transcripts,
context_usage: ui_state.context_usage,
selected: ui_state.selected,
}
}
pub(crate) fn apply_session_hydration_snapshot(
ui_state: &mut state::MissionControlState,
snapshot: SessionHydrationSnapshot,
) {
ui_state.transcript = snapshot.transcript;
ui_state.transcript_activity_links = snapshot.transcript_activity_links;
ui_state.nodes = snapshot.nodes;
ui_state.roots = snapshot.roots;
ui_state.children = snapshot.children;
ui_state.expanded = snapshot.expanded;
ui_state.active_tool_transcripts = snapshot.active_tool_transcripts;
ui_state.context_usage = snapshot.context_usage;
ui_state.selected = snapshot.selected;
ui_state.invalidate_transcript_cache();
ui_state.invalidate_activity_render_cache();
}
pub(crate) fn start_session_switch_load(
manager: &SessionManager,
config: Option<&crate::config::EffectiveConfig>,
session_id: &str,
) -> Result<SessionSwitchLoad, String> {
load_session_switch(manager, config, session_id).map_err(|error| error.to_string())
}
fn load_session_switch(
manager: &SessionManager,
config: Option<&crate::config::EffectiveConfig>,
session_id: &str,
) -> anyhow::Result<SessionSwitchLoad> {
let sessions = manager.list_metadata_summaries()?;
let target_summary = sessions
.iter()
.find(|summary| summary.session.id() == session_id && summary.session.path().exists());
let Some(target_summary) = target_summary else {
anyhow::bail!("session no longer exists: {}", short_session_id(session_id));
};
let latest_title = target_summary.latest_title.clone();
let session = manager.open(session_id)?;
let tolerant = session
.read_events_tolerant_bounded(SESSION_SWITCH_MAX_EVENTS, SESSION_SWITCH_MAX_BYTES)?;
let injected_content_settings = config
.and_then(|config| crate::config::read_settings(&config.paths).ok())
.map(|settings| settings.hooks.injected_content)
.unwrap_or_default();
let snapshot = hydrate_session_history_snapshot(&tolerant.events, &injected_content_settings);
Ok(SessionSwitchLoad {
session,
latest_title,
snapshot,
diagnostics: tolerant
.diagnostics
.into_iter()
.map(|diagnostic| diagnostic.message)
.collect(),
status: format!("switched session: {}", short_session_id(session_id)),
})
}
#[cfg(test)]
pub(crate) fn execute_switch_session_command(
shell_state: &mut ShellState,
ui_state: &mut state::MissionControlState,
session_id: &str,
receiver: &Receiver<TuiEvent>,
sender: &Sender<TuiEvent>,
) -> anyhow::Result<String> {
let sessions = shell_state.session_manager.list_metadata_summaries()?;
let target_summary = sessions
.iter()
.find(|summary| summary.session.id() == session_id && summary.session.path().exists());
let Some(target_summary) = target_summary else {
anyhow::bail!("session no longer exists: {}", short_session_id(session_id));
};
let target_latest_title = target_summary.latest_title.clone();
if shell_state.active_session_id() == Some(session_id) {
ui_state.close_session_picker();
return Ok(format!(
"session unchanged: {}",
short_session_id(session_id)
));
}
let session = shell_state.session_manager.open(session_id)?;
let tolerant = session
.read_events_tolerant_bounded(SESSION_SWITCH_MAX_EVENTS, SESSION_SWITCH_MAX_BYTES)?;
let events = tolerant.events;
preserve_critical_tui_events(receiver, sender)?;
shell_state.current_session = Some(session);
ui_state.reset_for_new_session();
let injected_content_settings = shell_state
.config
.as_ref()
.and_then(|config| crate::config::read_settings(&config.paths).ok())
.map(|settings| settings.hooks.injected_content)
.unwrap_or_default();
history::hydrate_session_history(ui_state, &events, &injected_content_settings);
for diagnostic in tolerant.diagnostics {
ui_state.apply_output_event(&crate::output::OutputEvent::Diagnostic {
level: "warning".to_string(),
message: diagnostic.message,
});
}
apply_footer_context(
ui_state,
shell_state.current_session.as_ref(),
target_latest_title.as_deref(),
&shell_state.cwd,
);
Ok(format!(
"switched session: {}",
short_session_id(session_id)
))
}
pub(crate) fn execute_new_session_command(
shell_state: &mut ShellState,
ui_state: &mut state::MissionControlState,
) -> anyhow::Result<String> {
let status = handle_builtin(BuiltInCommand::New, None, shell_state)?;
ui_state.reset_for_new_session();
apply_footer_context(
ui_state,
shell_state.current_session.as_ref(),
None,
&shell_state.cwd,
);
Ok(status)
}
pub(crate) fn execute_prune_sessions_command(
shell_state: &mut ShellState,
ui_state: &mut state::MissionControlState,
arg: Option<&str>,
) -> anyhow::Result<String> {
let retention_days = parse_prune_sessions_days(arg).map_err(|usage| anyhow::anyhow!(usage))?;
let report = shell_state
.session_manager
.prune_sessions(retention_days, shell_state.active_session_id())?;
if ui_state.modals.session_picker.is_some() {
let rows = session_picker_rows(
&shell_state.session_manager,
shell_state.active_session_id(),
&shell_state.cwd,
)?;
let selected = ui_state
.modals
.session_picker
.as_ref()
.map(|picker| picker.selected.min(rows.len().saturating_sub(1)))
.unwrap_or(0);
if let Some(picker) = &mut ui_state.modals.session_picker {
picker.rows = rows;
picker.selected = selected;
}
ui_state.set_scroll_offset(&ui_state.scroll_views.session_list, 0);
ui_state.set_scroll_offset(&ui_state.scroll_views.session_preview, 0);
}
let active_latest_title = shell_state.active_session_id().and_then(|active_id| {
shell_state
.session_manager
.list_metadata_summaries()
.ok()?
.into_iter()
.find(|summary| summary.session.id() == active_id)
.and_then(|summary| summary.latest_title)
});
apply_footer_context(
ui_state,
shell_state.current_session.as_ref(),
active_latest_title.as_deref(),
&shell_state.cwd,
);
Ok(report.summary())
}
pub(crate) fn apply_footer_context(
ui_state: &mut state::MissionControlState,
session: Option<&Session>,
latest_title: Option<&str>,
cwd: &Path,
) {
ui_state.footer_session_id = session.map(|session| session.id().to_string());
ui_state.footer_session = session
.map(|session| session_title_label(session.id(), latest_title))
.unwrap_or_else(|| "<disabled>".to_string());
ui_state.footer_cwd = cwd.display().to_string();
}
#[cfg(test)]
pub(crate) fn refresh_footer_git_branch(
ui_state: &mut state::MissionControlState,
cwd: &Path,
) -> bool {
let git_branch = current_git_branch(cwd);
if ui_state.footer_git_branch == git_branch {
return false;
}
ui_state.footer_git_branch = git_branch;
true
}
fn session_title_label(session_id: &str, latest_title: Option<&str>) -> String {
match latest_title {
Some(title) => format!("{} [{}]", title, short_session_id(session_id)),
None => short_session_id(session_id),
}
}
pub(crate) fn short_session_id(session_id: &str) -> String {
let char_count = session_id.chars().count();
if char_count <= 8 {
return session_id.to_string();
}
let prefix: String = session_id.chars().take(4).collect();
let suffix: String = session_id
.chars()
.rev()
.take(4)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
format!("{prefix}...{suffix}")
}
pub(crate) fn current_git_branch(cwd: &Path) -> Option<String> {
let git_dir = find_git_dir(cwd)?;
let head = fs::read_to_string(git_dir.join("HEAD")).ok()?;
let head = head.trim();
if let Some(branch) = head.strip_prefix("ref: refs/heads/") {
return Some(branch.to_string());
}
None
}
fn find_git_dir(cwd: &Path) -> Option<PathBuf> {
for dir in cwd.ancestors() {
let dot_git = dir.join(".git");
if dot_git.is_dir() {
return Some(dot_git);
}
if dot_git.is_file() {
let contents = fs::read_to_string(&dot_git).ok()?;
if let Some(path) = contents.trim().strip_prefix("gitdir: ") {
let git_dir = PathBuf::from(path);
return Some(if git_dir.is_absolute() {
git_dir
} else {
dir.join(git_dir)
});
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::{ActivityEvent, OutputEvent};
use crossbeam_channel::bounded;
#[test]
fn apply_footer_context_does_not_read_git_branch() {
let temp = tempfile::TempDir::new().unwrap();
let git_dir = temp.path().join(".git");
fs::create_dir(&git_dir).unwrap();
fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("footer-session").unwrap();
let mut ui_state = state::MissionControlState {
footer_git_branch: Some("old".to_string()),
..Default::default()
};
apply_footer_context(&mut ui_state, Some(&session), Some("Title"), temp.path());
assert_eq!(ui_state.footer_git_branch.as_deref(), Some("old"));
assert_eq!(ui_state.footer_session, "Title [foot...sion]");
assert_eq!(ui_state.footer_cwd, temp.path().display().to_string());
}
#[test]
fn refresh_footer_git_branch_detects_branch_change() {
let temp = tempfile::TempDir::new().unwrap();
let git_dir = temp.path().join(".git");
fs::create_dir(&git_dir).unwrap();
fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let mut ui_state = state::MissionControlState::default();
assert!(refresh_footer_git_branch(&mut ui_state, temp.path()));
assert_eq!(ui_state.footer_git_branch.as_deref(), Some("main"));
fs::write(git_dir.join("HEAD"), "ref: refs/heads/other\n").unwrap();
assert!(refresh_footer_git_branch(&mut ui_state, temp.path()));
assert_eq!(ui_state.footer_git_branch.as_deref(), Some("other"));
assert!(!refresh_footer_git_branch(&mut ui_state, temp.path()));
}
#[test]
fn drain_stale_tui_events_preserves_critical_run_finished() {
let (sender, receiver) = bounded::<TuiEvent>(4);
sender
.send(TuiEvent::Output(OutputEvent::AssistantDelta {
text: "streaming".to_string(),
}))
.unwrap();
sender
.send(TuiEvent::RunFinished { final_event: None })
.unwrap();
preserve_critical_tui_events(&receiver, &sender).unwrap();
assert!(matches!(
receiver.try_recv(),
Ok(TuiEvent::RunFinished { final_event: None })
));
assert!(receiver.try_recv().is_err());
}
#[test]
fn execute_new_session_command_switches_session_and_resets_ui() {
let temp = tempfile::TempDir::new().unwrap();
let mut shell_state = ShellState::new(
SessionManager::new(temp.path().join("sessions")),
None,
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState {
..Default::default()
};
ui_state.apply_output_event(&OutputEvent::UserPrompt {
text: "old".to_string(),
});
ui_state.apply_activity_event(ActivityEvent::Started {
id: crate::output::ActivityId::new("old-tool"),
parent_id: None,
kind: crate::output::ActivityKind::Tool,
status: crate::output::ActivityStatus::Running,
metadata: crate::output::ActivityMetadata::new("old tool"),
});
let status = execute_new_session_command(&mut shell_state, &mut ui_state).unwrap();
let session_id = shell_state.active_session_id().unwrap();
assert!(status.starts_with("new session: "));
assert!(status.ends_with(session_id));
assert!(ui_state.transcript.is_empty());
assert_eq!(ui_state.selected_activity_text(), "No activity selected.");
assert!(ui_state.nodes.is_empty());
assert_eq!(ui_state.help_offset(), 0);
}
#[test]
fn session_picker_rows_are_newest_first_and_mark_active() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let old = manager.open("old-session").unwrap();
let new = manager.open("new-session").unwrap();
let mut old_event = crate::sessions::SessionEvent::new(
"diagnostic",
old.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
);
old_event.timestamp =
chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2024, 1, 1, 0, 0, 0).unwrap();
old.append(&old_event).unwrap();
let mut new_event = crate::sessions::SessionEvent::new(
"diagnostic",
new.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
);
new_event.timestamp =
chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2024, 1, 2, 0, 0, 0).unwrap();
new.append(&new_event).unwrap();
let rows = session_picker_rows(&manager, Some(old.id()), temp.path()).unwrap();
assert_eq!(
rows.iter().map(|row| row.id.as_str()).collect::<Vec<_>>(),
vec!["new-session", "old-session"]
);
assert!(!rows[0].is_current);
assert!(rows[1].is_current);
}
#[test]
fn session_picker_rows_filter_by_current_session_path_and_hide_missing_path() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let current = temp.path().join("current");
let other = temp.path().join("other");
fs::create_dir_all(¤t).unwrap();
fs::create_dir_all(&other).unwrap();
let matching = manager.open("matching-session").unwrap();
matching
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
matching.id().to_string(),
current.clone(),
serde_json::json!({}),
))
.unwrap();
let hidden = manager.open("hidden-session").unwrap();
hidden
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
hidden.id().to_string(),
other,
serde_json::json!({}),
))
.unwrap();
let legacy = manager.open("legacy-session").unwrap();
let mut legacy_event = crate::sessions::SessionEvent::new(
"diagnostic",
legacy.id().to_string(),
current.clone(),
serde_json::json!({}),
);
legacy_event.session_path = None;
fs::create_dir_all(legacy.path().parent().unwrap()).unwrap();
fs::write(
legacy.path(),
format!("{}\n", serde_json::to_string(&legacy_event).unwrap()),
)
.unwrap();
let rows = session_picker_rows(&manager, None, ¤t).unwrap();
assert_eq!(
rows.iter().map(|row| row.id.as_str()).collect::<Vec<_>>(),
vec!["matching-session"]
);
}
#[test]
fn session_picker_rows_use_indexed_session_title_lookup() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("titled-session").unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"session_title",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"title":"Old durable title"}),
))
.unwrap();
for index in 0..140 {
session
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"message": format!("{}-{index}", "x".repeat(2048))}),
))
.unwrap();
}
let rows = session_picker_rows(&manager, None, temp.path()).unwrap();
assert_eq!(rows.len(), 1);
assert!(rows[0].label.contains("Old durable title"));
assert!(rows[0].label.contains("titl...sion"));
}
#[test]
fn session_preview_extracts_first_and_most_recent_messages_with_redaction() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("preview-session").unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"session_title",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"title":"Ignore me"}),
))
.unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"user_input",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"first prompt sk-1234567890abcdef1234567890abcdef"}),
))
.unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"tool_result",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"content":"ignore tool"}),
))
.unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"assistant_output",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"latest answer bearer secret-token"}),
))
.unwrap();
let preview = load_session_preview(&session);
let state::SessionPreviewState::Ready(preview) = preview else {
panic!("expected ready preview");
};
assert_eq!(preview.first.role, "user");
assert!(preview.first.text.contains("first prompt"));
assert!(
!preview
.first
.text
.contains("sk-1234567890abcdef1234567890abcdef")
);
let recent = preview.most_recent.unwrap();
assert_eq!(recent.role, "assistant");
assert!(recent.text.contains("latest answer"));
assert!(!recent.text.contains("secret-token"));
}
#[test]
fn session_preview_single_message_is_not_duplicated() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("single-preview-session").unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"user_input",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"only message"}),
))
.unwrap();
let state::SessionPreviewState::Ready(preview) = load_session_preview(&session) else {
panic!("expected ready preview");
};
assert_eq!(preview.first.text, "only message");
assert!(preview.most_recent.is_none());
}
#[test]
fn session_preview_many_irrelevant_events_is_bounded() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("bounded-preview-session").unwrap();
for index in 0..(SESSION_PREVIEW_MAX_FORWARD_EVENTS + 20) {
session
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"message": format!("ignore {index}")}),
))
.unwrap();
}
let preview = load_session_preview(&session);
assert!(matches!(preview, state::SessionPreviewState::Partial(_)));
}
#[test]
fn session_preview_large_session_is_bounded() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("large-preview-session").unwrap();
for index in 0..(SESSION_PREVIEW_MAX_FORWARD_EVENTS + 1) {
session
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"message": "x".repeat(32), "index": index}),
))
.unwrap();
}
session
.append(&crate::sessions::SessionEvent::new(
"assistant_output",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"tail answer"}),
))
.unwrap();
let preview = load_session_preview(&session);
let state::SessionPreviewState::Partial(partial) = preview else {
panic!("expected partial preview");
};
assert!(partial.first.is_none());
assert_eq!(partial.most_recent.unwrap().text, "tail answer");
}
#[test]
fn session_preview_malformed_payload_is_sanitized() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("malformed-preview-session").unwrap();
fs::create_dir_all(session.path().parent().unwrap()).unwrap();
fs::write(
session.path(),
"not json with /tmp/private/path and sk-1234567890abcdef\n",
)
.unwrap();
let preview = load_session_preview(&session);
let state::SessionPreviewState::Error(error) = preview else {
panic!("expected preview error");
};
assert!(error.contains("malformed session event"));
assert!(!error.contains("/tmp/private/path"));
assert!(!error.contains("sk-1234567890abcdef"));
}
#[test]
fn session_preview_empty_when_no_conversational_messages() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("empty-preview-session").unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"session_title",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"title":"Only title"}),
))
.unwrap();
assert!(matches!(
load_session_preview(&session),
state::SessionPreviewState::Empty
));
}
#[test]
fn session_picker_rows_do_not_preload_every_preview() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("no-eager-preview-session").unwrap();
session
.append(&crate::sessions::SessionEvent::new(
"user_input",
session.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"not loaded yet"}),
))
.unwrap();
let rows = session_picker_rows(&manager, None, temp.path()).unwrap();
assert_eq!(rows.len(), 1);
assert!(rows[0].preview.is_none());
}
#[test]
fn session_picker_rows_empty_when_no_sessions() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
assert!(
session_picker_rows(&manager, None, temp.path())
.unwrap()
.is_empty()
);
}
#[test]
fn selecting_session_tolerates_malformed_jsonl_lines() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let active = manager.open("active-session").unwrap();
let target = manager.open("target-session").unwrap();
active
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
active.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
))
.unwrap();
let prompt = crate::sessions::SessionEvent::new(
"user_input",
target.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"target prompt"}),
);
fs::create_dir_all(target.path().parent().unwrap()).unwrap();
fs::write(
target.path(),
format!("not json\n{}\n", serde_json::to_string(&prompt).unwrap()),
)
.unwrap();
crate::sessions::secure_test_session_root(target.path().parent().unwrap());
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut shell_state = ShellState::new(
manager,
Some(active.clone()),
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState::default();
let status = execute_switch_session_command(
&mut shell_state,
&mut ui_state,
target.id(),
&receiver,
&sender,
)
.unwrap();
assert!(status.contains("switched session"));
assert_eq!(shell_state.active_session_id(), Some(target.id()));
assert!(
ui_state
.transcript
.contains(&"you: target prompt".to_string())
);
assert!(
ui_state
.transcript
.iter()
.any(|line| line.contains("failed to parse session JSONL"))
);
}
#[test]
fn selecting_session_caps_malformed_jsonl_warnings() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let active = manager.open("active-session").unwrap();
let target = manager.open("target-session").unwrap();
active
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
active.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
))
.unwrap();
let prompt = crate::sessions::SessionEvent::new(
"user_input",
target.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"target prompt"}),
);
fs::create_dir_all(target.path().parent().unwrap()).unwrap();
let mut lines = (0..100)
.map(|index| format!("not json {index}"))
.collect::<Vec<_>>();
lines.push(serde_json::to_string(&prompt).unwrap());
fs::write(target.path(), format!("{}\n", lines.join("\n"))).unwrap();
crate::sessions::secure_test_session_root(target.path().parent().unwrap());
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut shell_state = ShellState::new(
manager,
Some(active.clone()),
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState::default();
execute_switch_session_command(
&mut shell_state,
&mut ui_state,
target.id(),
&receiver,
&sender,
)
.unwrap();
assert_eq!(shell_state.active_session_id(), Some(target.id()));
assert!(
ui_state
.transcript
.contains(&"you: target prompt".to_string())
);
let warning_count = ui_state
.transcript
.iter()
.filter(|line| line.contains("failed to parse session JSONL"))
.count();
assert_eq!(warning_count, 63);
assert!(
ui_state
.transcript
.iter()
.any(|line| line.contains("omitted 36 additional"))
);
}
#[test]
fn selecting_oversized_session_fails_before_hydration() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let active = manager.open("active-session").unwrap();
let target = manager.open("target-session").unwrap();
active
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
active.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
))
.unwrap();
fs::write(target.path(), "x".repeat(SESSION_SWITCH_MAX_BYTES + 1)).unwrap();
crate::sessions::secure_test_session_root(target.path().parent().unwrap());
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut shell_state = ShellState::new(
manager,
Some(active.clone()),
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState::default();
let error = execute_switch_session_command(
&mut shell_state,
&mut ui_state,
target.id(),
&receiver,
&sender,
)
.unwrap_err();
assert!(
error
.to_string()
.contains("session JSONL tolerant read limit exceeded")
);
assert_eq!(shell_state.active_session_id(), Some(active.id()));
assert!(ui_state.transcript.is_empty());
}
#[test]
fn round_five_session_switch_hydrates_completed_tool_row() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let active = manager.open("active-session").unwrap();
let target = manager.open("target-session").unwrap();
active
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
active.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
))
.unwrap();
target
.append(&crate::sessions::SessionEvent::new(
"session_title",
target.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"title":"Target durable title"}),
))
.unwrap();
target
.append(&crate::sessions::SessionEvent::new(
"user_input",
target.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"target prompt"}),
))
.unwrap();
target
.append(&crate::sessions::SessionEvent::new(
"assistant_output",
target.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"text":"target answer"}),
))
.unwrap();
target
.append(&crate::sessions::SessionEvent::new(
"tool_call",
target.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"id":"call_1","name":"read","arguments":{"path":"file.txt"}}),
))
.unwrap();
target
.append(&crate::sessions::SessionEvent::new(
"tool_result",
target.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({"call_id":"call_1","result":{"tool_name":"read","success":true,"content":"file body","metadata":{"bytes":9}}}),
))
.unwrap();
let (sender, receiver) = bounded::<TuiEvent>(8);
let mut shell_state = ShellState::new(
manager,
Some(active.clone()),
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState {
transcript: vec!["old".into()].into(),
..Default::default()
};
sender
.send(TuiEvent::Output(OutputEvent::AssistantDelta {
text: "queued".into(),
}))
.unwrap();
let status = execute_switch_session_command(
&mut shell_state,
&mut ui_state,
target.id(),
&receiver,
&sender,
)
.unwrap();
assert_eq!(shell_state.active_session_id(), Some(target.id()));
assert_eq!(ui_state.footer_session_id.as_deref(), Some(target.id()));
assert_eq!(
ui_state.footer_session,
"Target durable title [targ...sion]"
);
assert!(status.contains("switched session"));
assert_eq!(
ui_state.transcript,
vec![
"you: target prompt".to_string(),
"assistant: target answer".to_string(),
"tool: ✓ read file.txt • 1 line".to_string(),
]
);
assert_eq!(ui_state.visible_nodes().len(), 1);
assert_eq!(ui_state.visible_nodes()[0].1.as_str(), "call_1");
assert!(ui_state.selected_activity_text().contains("file body"));
assert!(receiver.try_recv().is_err());
}
#[test]
fn selecting_deleted_session_reports_error_and_preserves_active_state() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let active = manager.open("active-session").unwrap();
active
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
active.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
))
.unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut shell_state = ShellState::new(
manager,
Some(active.clone()),
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState {
transcript: vec!["keep".into()].into(),
..Default::default()
};
let error = execute_switch_session_command(
&mut shell_state,
&mut ui_state,
"deleted-session",
&receiver,
&sender,
)
.unwrap_err()
.to_string();
assert!(error.contains("session no longer exists"));
assert_eq!(shell_state.active_session_id(), Some(active.id()));
assert_eq!(ui_state.transcript, vec!["keep".to_string()]);
}
#[test]
fn execute_prune_sessions_command_refreshes_picker_and_preserves_display_state() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let active = manager.open("active-session").unwrap();
let old = manager.open("old-session").unwrap();
append_session_event_at(&active, temp.path(), 2020, 1, 1);
append_session_event_at(&old, temp.path(), 2020, 1, 1);
let mut shell_state = ShellState::new(
manager,
Some(active.clone()),
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState {
transcript: vec!["keep transcript".into()].into(),
..Default::default()
};
ui_state.open_session_picker(
vec![
state::SessionPickerRow {
id: old.id().into(),
label: old.id().into(),
is_current: false,
preview: None,
},
state::SessionPickerRow {
id: active.id().into(),
label: active.id().into(),
is_current: true,
preview: None,
},
],
4,
);
let status =
execute_prune_sessions_command(&mut shell_state, &mut ui_state, Some("1")).unwrap();
assert!(status.contains("pruned 1 sessions older than 1 days"));
assert!(status.contains("skipped active session"));
assert!(!old.path().exists());
assert!(active.path().exists());
let picker = ui_state.modals.session_picker.as_ref().unwrap();
assert_eq!(picker.rows.len(), 1);
assert_eq!(picker.rows[0].id, active.id());
assert!(picker.rows[0].is_current);
assert_eq!(ui_state.transcript, vec!["keep transcript".to_string()]);
assert_eq!(ui_state.help_offset(), 0);
}
#[test]
fn execute_prune_sessions_command_rejects_invalid_args_without_deleting() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let old = manager.open("old-session").unwrap();
append_session_event_at(&old, temp.path(), 2020, 1, 1);
let mut shell_state = ShellState::new(
manager,
None,
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState::default();
let error = execute_prune_sessions_command(&mut shell_state, &mut ui_state, Some("7 now"))
.unwrap_err()
.to_string();
assert!(error.contains("usage: /prune-sessions [days]"));
assert!(old.path().exists());
}
fn append_session_event_at(session: &Session, cwd: &Path, year: i32, month: u32, day: u32) {
let mut event = crate::sessions::SessionEvent::new(
"diagnostic",
session.id().to_string(),
cwd.to_path_buf(),
serde_json::json!({}),
);
event.timestamp =
chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, year, month, day, 0, 0, 0).unwrap();
session.append(&event).unwrap();
}
#[test]
fn selecting_active_session_closes_modal_without_mutating_history() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let active = manager.open("active-session").unwrap();
active
.append(&crate::sessions::SessionEvent::new(
"diagnostic",
active.id().to_string(),
temp.path().to_path_buf(),
serde_json::json!({}),
))
.unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut shell_state = ShellState::new(
manager,
Some(active.clone()),
temp.path().to_path_buf(),
"test-model".to_string(),
crate::config::AuthState::Missing {
provider: "openai".to_string(),
},
);
let mut ui_state = state::MissionControlState::default();
ui_state.open_session_picker(
vec![state::SessionPickerRow {
id: active.id().into(),
label: active.id().into(),
is_current: true,
preview: None,
}],
1,
);
let before = active.read_events().unwrap().len();
let status = execute_switch_session_command(
&mut shell_state,
&mut ui_state,
active.id(),
&receiver,
&sender,
)
.unwrap();
assert!(status.contains("session unchanged"));
assert!(!ui_state.session_picker_visible());
assert_eq!(active.read_events().unwrap().len(), before);
}
#[test]
fn session_preview_prefers_failed_turn_assistant_text() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("failed-preview-session").unwrap();
for (kind, payload) in [
("user_input", serde_json::json!({"text":"prompt"})),
(
"assistant_chunk",
serde_json::json!({"text":"partial answer"}),
),
(
"turn_status",
serde_json::json!({"status":"failed","assistant_text":"authoritative failure text"}),
),
] {
session
.append(&crate::sessions::SessionEvent::new(
kind,
session.id().to_string(),
temp.path().to_path_buf(),
payload,
))
.unwrap();
}
let state::SessionPreviewState::Ready(preview) = load_session_preview(&session) else {
panic!("expected ready preview");
};
assert_eq!(preview.first.text, "prompt");
assert_eq!(
preview.most_recent.unwrap().text,
"authoritative failure text"
);
}
#[test]
fn session_preview_does_not_fall_back_past_unanswered_latest_prompt() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("unanswered-preview-session").unwrap();
for (kind, payload) in [
("user_input", serde_json::json!({"text":"old prompt"})),
("assistant_output", serde_json::json!({"text":"old answer"})),
("user_input", serde_json::json!({"text":"latest prompt"})),
] {
session
.append(&crate::sessions::SessionEvent::new(
kind,
session.id().to_string(),
temp.path().to_path_buf(),
payload,
))
.unwrap();
}
let state::SessionPreviewState::Ready(preview) = load_session_preview(&session) else {
panic!("expected ready preview");
};
assert_eq!(preview.first.text, "old prompt");
assert!(preview.most_recent.is_none());
}
#[test]
fn session_preview_handles_chunk_only_and_empty_terminal_fallbacks() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("chunk-preview-session").unwrap();
for (kind, payload) in [
("user_input", serde_json::json!({"text":"prompt"})),
("assistant_chunk", serde_json::json!({"text":"partial "})),
(
"assistant_chunk",
serde_json::json!({"text":"answer\nnext"}),
),
("assistant_chunk", serde_json::json!({"text":"\n"})),
(
"turn_status",
serde_json::json!({"status":"canceled","assistant_text":" "}),
),
] {
session
.append(&crate::sessions::SessionEvent::new(
kind,
session.id().to_string(),
temp.path().to_path_buf(),
payload,
))
.unwrap();
}
let state::SessionPreviewState::Ready(preview) = load_session_preview(&session) else {
panic!("expected ready preview");
};
assert_eq!(preview.most_recent.unwrap().text, "partial answer\nnext");
}
#[test]
fn session_preview_completed_output_replaces_chunk_fallback() {
let temp = tempfile::TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.open("completed-preview-session").unwrap();
for (kind, payload) in [
("user_input", serde_json::json!({"text":"prompt"})),
("assistant_chunk", serde_json::json!({"text":"partial"})),
(
"assistant_output",
serde_json::json!({"text":"completed answer"}),
),
] {
session
.append(&crate::sessions::SessionEvent::new(
kind,
session.id().to_string(),
temp.path().to_path_buf(),
payload,
))
.unwrap();
}
let state::SessionPreviewState::Ready(preview) = load_session_preview(&session) else {
panic!("expected ready preview");
};
assert_eq!(preview.most_recent.unwrap().text, "completed answer");
}
}