|
6 | 6 | //! This file provides the command result handling infrastructure. |
7 | 7 |
|
8 | 8 | use anyhow::Result; |
| 9 | +use std::path::Path; |
9 | 10 |
|
10 | 11 | use crate::app::AppView; |
11 | 12 | use crate::commands::{CommandResult, FormRegistry, ModalType, ViewType}; |
12 | 13 | use crate::session::{CortexSession, ExportFormat, default_export_filename, export_session}; |
| 14 | +use cortex_engine::rollout::get_rollout_path; |
| 15 | +use cortex_engine::rollout::read_rollout; |
| 16 | +use cortex_engine::rollout::reader::{RolloutItem, SessionMetaEntry}; |
| 17 | +use cortex_protocol::{ConversationId, EventMsg}; |
13 | 18 |
|
14 | 19 | use super::core::EventLoop; |
15 | 20 |
|
| 21 | +fn legacy_session_title(meta: &SessionMetaEntry) -> String { |
| 22 | + match meta.model.as_deref() { |
| 23 | + Some(model) if !model.is_empty() && model != "unknown" => model.to_string(), |
| 24 | + _ => Path::new(&meta.cwd) |
| 25 | + .file_name() |
| 26 | + .map(|name| name.to_string_lossy().to_string()) |
| 27 | + .unwrap_or_else(|| "Session".to_string()), |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +fn legacy_session_message_count(entries: &[cortex_engine::rollout::RolloutEntry]) -> usize { |
| 32 | + entries |
| 33 | + .iter() |
| 34 | + .filter(|entry| { |
| 35 | + matches!( |
| 36 | + &entry.item, |
| 37 | + RolloutItem::EventMsg( |
| 38 | + EventMsg::UserMessage(_) |
| 39 | + | EventMsg::AgentMessage(_) |
| 40 | + | EventMsg::ExecCommandEnd(_) |
| 41 | + ) |
| 42 | + ) |
| 43 | + }) |
| 44 | + .count() |
| 45 | +} |
| 46 | + |
| 47 | +fn format_legacy_session_info( |
| 48 | + cortex_home: &std::path::PathBuf, |
| 49 | + conversation_id: &ConversationId, |
| 50 | + provider: &str, |
| 51 | +) -> Result<Option<String>> { |
| 52 | + let rollout_path = get_rollout_path(cortex_home, conversation_id); |
| 53 | + if !rollout_path.exists() { |
| 54 | + return Ok(None); |
| 55 | + } |
| 56 | + |
| 57 | + let entries = read_rollout(&rollout_path)?; |
| 58 | + let Some(meta) = entries.iter().find_map(|entry| match &entry.item { |
| 59 | + RolloutItem::SessionMeta(meta) => Some(meta), |
| 60 | + _ => None, |
| 61 | + }) else { |
| 62 | + return Ok(None); |
| 63 | + }; |
| 64 | + |
| 65 | + let mut output = String::from("Session Info:\n"); |
| 66 | + output.push_str(&format!(" ID: {}\n", meta.id)); |
| 67 | + output.push_str(&format!(" Title: {}\n", legacy_session_title(meta))); |
| 68 | + if !provider.is_empty() { |
| 69 | + output.push_str(&format!(" Provider: {}\n", provider)); |
| 70 | + } |
| 71 | + if let Some(model) = meta.model.as_deref() { |
| 72 | + output.push_str(&format!(" Model: {}\n", model)); |
| 73 | + } |
| 74 | + output.push_str(&format!( |
| 75 | + " Messages: {}\n", |
| 76 | + legacy_session_message_count(&entries) |
| 77 | + )); |
| 78 | + |
| 79 | + let created = chrono::DateTime::parse_from_rfc3339(&meta.timestamp) |
| 80 | + .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string()) |
| 81 | + .unwrap_or_else(|_| meta.timestamp.clone()); |
| 82 | + output.push_str(&format!(" Created: {}\n", created)); |
| 83 | + output.push_str(&format!(" CWD: {}\n", meta.cwd)); |
| 84 | + |
| 85 | + Ok(Some(output)) |
| 86 | +} |
| 87 | + |
16 | 88 | impl EventLoop { |
17 | 89 | /// Handles a command result from the CommandExecutor. |
18 | 90 | pub(super) async fn handle_command_result(&mut self, result: CommandResult) -> Result<()> { |
@@ -514,6 +586,18 @@ impl EventLoop { |
514 | 586 | session.meta.created_at.format("%Y-%m-%d %H:%M") |
515 | 587 | )); |
516 | 588 | self.add_system_message(&output); |
| 589 | + } else if let Some(ref bridge) = self.session_bridge { |
| 590 | + match format_legacy_session_info( |
| 591 | + &bridge.config().cortex_home, |
| 592 | + bridge.conversation_id(), |
| 593 | + &self.app_state.provider, |
| 594 | + ) { |
| 595 | + Ok(Some(output)) => self.add_system_message(&output), |
| 596 | + Ok(None) => self.add_system_message("No active session."), |
| 597 | + Err(err) => { |
| 598 | + self.add_system_message(&format!("Failed to load session info: {}", err)) |
| 599 | + } |
| 600 | + } |
517 | 601 | } else { |
518 | 602 | self.add_system_message("No active session."); |
519 | 603 | } |
@@ -654,3 +738,67 @@ impl EventLoop { |
654 | 738 | } |
655 | 739 | } |
656 | 740 | } |
| 741 | + |
| 742 | +#[cfg(test)] |
| 743 | +mod tests { |
| 744 | + use super::*; |
| 745 | + use cortex_engine::rollout::recorder::{RolloutRecorder, SessionMeta}; |
| 746 | + use cortex_protocol::{AgentMessageEvent, Event, UserMessageEvent}; |
| 747 | + use std::path::PathBuf; |
| 748 | + |
| 749 | + #[test] |
| 750 | + fn formats_legacy_session_info_from_rollout() { |
| 751 | + let temp_dir = tempfile::tempdir().unwrap(); |
| 752 | + let cortex_home = temp_dir.path().to_path_buf(); |
| 753 | + let conversation_id = ConversationId::new(); |
| 754 | + |
| 755 | + let mut recorder = RolloutRecorder::new(&cortex_home, conversation_id).unwrap(); |
| 756 | + recorder.init().unwrap(); |
| 757 | + recorder |
| 758 | + .record_meta(&SessionMeta { |
| 759 | + id: conversation_id, |
| 760 | + parent_id: None, |
| 761 | + fork_point: None, |
| 762 | + timestamp: "2026-04-10T03:00:00Z".to_string(), |
| 763 | + cwd: PathBuf::from("/tmp/imported-session"), |
| 764 | + model: "claude-3-7-sonnet".to_string(), |
| 765 | + cli_version: "test".to_string(), |
| 766 | + instructions: None, |
| 767 | + }) |
| 768 | + .unwrap(); |
| 769 | + recorder |
| 770 | + .record_event(&Event { |
| 771 | + id: "1".to_string(), |
| 772 | + msg: EventMsg::UserMessage(UserMessageEvent { |
| 773 | + id: None, |
| 774 | + parent_id: None, |
| 775 | + message: "hello".to_string(), |
| 776 | + images: None, |
| 777 | + }), |
| 778 | + }) |
| 779 | + .unwrap(); |
| 780 | + recorder |
| 781 | + .record_event(&Event { |
| 782 | + id: "1".to_string(), |
| 783 | + msg: EventMsg::AgentMessage(AgentMessageEvent { |
| 784 | + id: None, |
| 785 | + parent_id: None, |
| 786 | + message: "world".to_string(), |
| 787 | + finish_reason: None, |
| 788 | + }), |
| 789 | + }) |
| 790 | + .unwrap(); |
| 791 | + recorder.flush().unwrap(); |
| 792 | + |
| 793 | + let output = format_legacy_session_info(&cortex_home, &conversation_id, "cortex") |
| 794 | + .unwrap() |
| 795 | + .unwrap(); |
| 796 | + |
| 797 | + assert!(output.contains("Session Info:")); |
| 798 | + assert!(output.contains(&format!("ID: {}", conversation_id))); |
| 799 | + assert!(output.contains("Provider: cortex")); |
| 800 | + assert!(output.contains("Model: claude-3-7-sonnet")); |
| 801 | + assert!(output.contains("Messages: 2")); |
| 802 | + assert!(output.contains("CWD: /tmp/imported-session")); |
| 803 | + } |
| 804 | +} |
0 commit comments