From f3f2b35ed6359449ba9e5f2ad65b452f11242d82 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Mar 2026 12:00:37 +0000 Subject: [PATCH 1/4] feat: auto-import sessions on first use (remove explicit import command) This change implements Option 1 for removing the explicit import command: - SessionService now auto-imports when cache is empty on first query - Removed 'sessions import' and 'sessions import-from' CLI commands - Removed '/sessions import' REPL command - Updated help text to reflect new behavior - Added enable_auto_import/disable_auto_import methods for testing - Sessions are automatically imported on list, search, stats, and sessions_by_source Breaking changes: - 'terraphim-agent sessions import' command removed - 'terraphim-agent sessions import-from' command removed - '/sessions import' REPL command removed (shows deprecation message) Benefits: - Simpler UX - no manual import step needed - Transparent to users - sessions just work - Reduced cognitive load - one less command to learn --- crates/terraphim_agent/src/main.rs | 111 ++------------------ crates/terraphim_agent/src/repl/commands.rs | 75 +++---------- crates/terraphim_agent/src/repl/handler.rs | 25 +---- crates/terraphim_sessions/src/service.rs | 104 +++++++++++++++++- 4 files changed, 129 insertions(+), 186 deletions(-) diff --git a/crates/terraphim_agent/src/main.rs b/crates/terraphim_agent/src/main.rs index bfdc10fd..86816cbf 100644 --- a/crates/terraphim_agent/src/main.rs +++ b/crates/terraphim_agent/src/main.rs @@ -806,27 +806,13 @@ fn get_session_cache_path() -> std::path::PathBuf { enum SessionsSub { /// Detect available session sources (Claude Code, Cursor, etc.) Sources, - /// Import sessions from all available sources - Import { - /// Limit number of sessions to import - #[arg(long, default_value_t = 100)] - limit: usize, - }, - /// Import sessions from a specific source - ImportFrom { - /// Source ID (e.g., "claude-code-native") - source: String, - /// Limit number of sessions to import - #[arg(long, default_value_t = 100)] - limit: usize, - }, - /// List all cached sessions + /// List all cached sessions (auto-imports if cache is empty) List { /// Limit number of sessions to show #[arg(long, default_value_t = 20)] limit: usize, }, - /// Search sessions by query string + /// Search sessions by query string (auto-imports if cache is empty) Search { /// Search query query: String, @@ -834,7 +820,7 @@ enum SessionsSub { #[arg(long, default_value_t = 10)] limit: usize, }, - /// Show session statistics + /// Show session statistics (auto-imports if cache is empty) Stats, } @@ -1839,7 +1825,7 @@ async fn run_offline_command( #[cfg(feature = "repl-sessions")] Command::Sessions { sub } => { - use terraphim_sessions::{SessionService, connector::ImportOptions}; + use terraphim_sessions::SessionService; let service = SessionService::new(); @@ -1878,46 +1864,10 @@ async fn run_offline_command( } Ok(()) } - SessionsSub::Import { limit } => { - let options = ImportOptions { - limit: Some(limit), - ..Default::default() - }; - match service.import_all(&options).await { - Ok(sessions) => { - // Save to cache file - let cache_path = get_session_cache_path(); - if let Ok(data) = serde_json::to_string_pretty(&sessions) { - let _ = std::fs::write(&cache_path, data); - } - println!("Imported {} sessions.", sessions.len()); - Ok(()) - } - Err(e) => Err(anyhow::anyhow!("Import failed: {}", e)), - } - } - SessionsSub::ImportFrom { source, limit } => { - let options = ImportOptions { - limit: Some(limit), - ..Default::default() - }; - match service.import_from(&source, &options).await { - Ok(sessions) => { - // Save to cache file - let cache_path = get_session_cache_path(); - if let Ok(data) = serde_json::to_string_pretty(&sessions) { - let _ = std::fs::write(&cache_path, data); - } - println!("Imported {} sessions from {}.", sessions.len(), source); - Ok(()) - } - Err(e) => Err(anyhow::anyhow!("Import from {} failed: {}", source, e)), - } - } SessionsSub::List { limit } => { let sessions = service.list_sessions().await; if sessions.is_empty() { - println!("No sessions in cache. Import first with 'sessions import'."); + println!("No sessions found."); } else { println!("Cached sessions ({} total):", sessions.len()); for session in sessions.iter().take(limit) { @@ -1934,10 +1884,7 @@ async fn run_offline_command( SessionsSub::Search { query, limit } => { let results = service.search(&query).await; if results.is_empty() { - println!( - "No sessions matching '{}'. Import first with 'sessions import'.", - query - ); + println!("No sessions matching '{}'.", query); } else { println!("Found {} matching sessions:", results.len()); for session in results.iter().take(limit) { @@ -2613,7 +2560,7 @@ async fn run_server_command( #[cfg(feature = "repl-sessions")] Command::Sessions { sub } => { - use terraphim_sessions::{SessionService, connector::ImportOptions}; + use terraphim_sessions::SessionService; let rt = Runtime::new()?; rt.block_on(async { @@ -2641,46 +2588,11 @@ async fn run_server_command( } Ok(()) } - SessionsSub::Import { limit } => { - let options = ImportOptions { - limit: Some(limit), - ..Default::default() - }; - match service.import_all(&options).await { - Ok(sessions) => { - // Save to cache file - let cache_path = get_session_cache_path(); - if let Ok(data) = serde_json::to_string_pretty(&sessions) { - let _ = std::fs::write(&cache_path, data); - } - println!("Imported {} sessions.", sessions.len()); - Ok(()) - } - Err(e) => Err(anyhow::anyhow!("Import failed: {}", e)), - } - } - SessionsSub::ImportFrom { source, limit } => { - let options = ImportOptions { - limit: Some(limit), - ..Default::default() - }; - match service.import_from(&source, &options).await { - Ok(sessions) => { - // Save to cache file - let cache_path = get_session_cache_path(); - if let Ok(data) = serde_json::to_string_pretty(&sessions) { - let _ = std::fs::write(&cache_path, data); - } - println!("Imported {} sessions from {}.", sessions.len(), source); - Ok(()) - } - Err(e) => Err(anyhow::anyhow!("Import from {} failed: {}", source, e)), - } - } + SessionsSub::List { limit } => { let sessions = service.list_sessions().await; if sessions.is_empty() { - println!("No sessions in cache. Import first with 'sessions import'."); + println!("No sessions found."); } else { println!("Cached sessions ({} total):", sessions.len()); for session in sessions.iter().take(limit) { @@ -2697,10 +2609,7 @@ async fn run_server_command( SessionsSub::Search { query, limit } => { let results = service.search(&query).await; if results.is_empty() { - println!( - "No sessions matching '{}'. Import first with 'sessions import'.", - query - ); + println!("No sessions matching '{}'.", query); } else { println!("Found {} matching sessions:", results.len()); for session in results.iter().take(limit) { diff --git a/crates/terraphim_agent/src/repl/commands.rs b/crates/terraphim_agent/src/repl/commands.rs index c6f8ecfa..b0aeb225 100644 --- a/crates/terraphim_agent/src/repl/commands.rs +++ b/crates/terraphim_agent/src/repl/commands.rs @@ -1,6 +1,6 @@ //! Command definitions for REPL interface -use anyhow::{Result, anyhow}; +use anyhow::{anyhow, Result}; use std::str::FromStr; #[derive(Debug, Clone, PartialEq)] @@ -155,12 +155,7 @@ pub enum FileSubcommand { pub enum SessionsSubcommand { /// Detect available session sources Sources, - /// Import sessions from a source - Import { - source: Option, - limit: Option, - }, - /// List imported sessions + /// List imported sessions (auto-imports if cache is empty) List { source: Option, limit: Option, @@ -1106,45 +1101,9 @@ impl FromStr for ReplCommand { subcommand: SessionsSubcommand::Sources, }), "import" => { - let mut source = None; - let mut limit = None; - let mut i = 2; - - while i < parts.len() { - match parts[i] { - "--source" => { - if i + 1 < parts.len() { - source = Some(parts[i + 1].to_string()); - i += 2; - } else { - return Err(anyhow!("--source requires a value")); - } - } - "--limit" => { - if i + 1 < parts.len() { - limit = Some( - parts[i + 1] - .parse::() - .map_err(|_| anyhow!("Invalid limit value"))?, - ); - i += 2; - } else { - return Err(anyhow!("--limit requires a value")); - } - } - _ => { - // Treat as source if no flag prefix - if source.is_none() && !parts[i].starts_with("--") { - source = Some(parts[i].to_string()); - } - i += 1; - } - } - } - - Ok(ReplCommand::Sessions { - subcommand: SessionsSubcommand::Import { source, limit }, - }) + return Err(anyhow!( + "The 'import' command has been removed. Sessions are now automatically imported when needed. Use '/sessions list' or '/sessions search ' instead." + )); } "list" | "ls" => { let mut source = None; @@ -1345,7 +1304,7 @@ impl FromStr for ReplCommand { }) } _ => Err(anyhow!( - "Unknown sessions subcommand: {}. Use: sources, import, list, search, stats, show, concepts, related, timeline, export, enrich, files, by-file", + "Unknown sessions subcommand: {}. Use: sources, list, search, stats, show, concepts, related, timeline, export, enrich, files, by-file", parts[1] )), } @@ -1502,7 +1461,7 @@ impl ReplCommand { #[cfg(feature = "repl-sessions")] "sessions" => Some( - "/sessions - AI coding session history (sources, import, list, search, stats, show, concepts, related, timeline, export, enrich, files, by-file)", + "/sessions - AI coding session history (sources, list, search, stats, show, concepts, related, timeline, export, enrich, files, by-file)", ), _ => None, @@ -1628,22 +1587,18 @@ mod tests { // Test update without subcommand let result = "/update".parse::(); assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("requires a subcommand") - ); + assert!(result + .unwrap_err() + .to_string() + .contains("requires a subcommand")); // Test update rollback without version let result = "/update rollback".parse::(); assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("requires a version") - ); + assert!(result + .unwrap_err() + .to_string() + .contains("requires a version")); // Test unknown update subcommand let result = "/update unknown".parse::(); diff --git a/crates/terraphim_agent/src/repl/handler.rs b/crates/terraphim_agent/src/repl/handler.rs index 1862c5d3..d8c333ce 100644 --- a/crates/terraphim_agent/src/repl/handler.rs +++ b/crates/terraphim_agent/src/repl/handler.rs @@ -1766,7 +1766,7 @@ impl ReplHandler { use comfy_table::presets::UTF8_FULL; use comfy_table::{Cell, Table}; use terraphim_sessions::{ - ConnectorStatus, FileAccess, ImportOptions, MessageRole, Session, SessionService, + ConnectorStatus, FileAccess, MessageRole, Session, SessionService, }; // Get or create session service @@ -1820,24 +1820,6 @@ impl ReplHandler { println!("{}", table); } - SessionsSubcommand::Import { source, limit } => { - let options = ImportOptions::new().with_limit(limit.unwrap_or(100)); - - println!("\n{} Importing sessions...", "⏳".bold()); - - let sessions = if let Some(source_id) = source { - svc.import_from(&source_id, &options).await? - } else { - svc.import_all(&options).await? - }; - - println!( - "{} Imported {} session(s)", - "✅".bold(), - sessions.len().to_string().green() - ); - } - SessionsSubcommand::List { source, limit } => { let sessions = if let Some(source_id) = source { svc.sessions_by_source(&source_id).await @@ -1852,10 +1834,7 @@ impl ReplHandler { }; if sessions.is_empty() { - println!( - "{} No sessions found. Run '/sessions import' first.", - "ℹ".blue().bold() - ); + println!("{} No sessions found.", "ℹ".blue().bold()); return Ok(()); } diff --git a/crates/terraphim_sessions/src/service.rs b/crates/terraphim_sessions/src/service.rs index cb794f40..cdde66a6 100644 --- a/crates/terraphim_sessions/src/service.rs +++ b/crates/terraphim_sessions/src/service.rs @@ -16,15 +16,21 @@ pub struct SessionService { registry: ConnectorRegistry, /// Cached sessions (in-memory) cache: Arc>>, + /// Whether auto-import is enabled + auto_import: bool, + /// Whether auto-import has been attempted + auto_import_attempted: Arc>, } impl SessionService { - /// Create a new session service + /// Create a new session service with auto-import enabled #[must_use] pub fn new() -> Self { Self { registry: ConnectorRegistry::new(), cache: Arc::new(RwLock::new(HashMap::new())), + auto_import: true, + auto_import_attempted: Arc::new(RwLock::new(false)), } } @@ -34,9 +40,67 @@ impl SessionService { Self { registry, cache: Arc::new(RwLock::new(HashMap::new())), + auto_import: true, + auto_import_attempted: Arc::new(RwLock::new(false)), } } + /// Disable auto-import (for testing or explicit control) + pub fn disable_auto_import(&mut self) { + self.auto_import = false; + } + + /// Enable auto-import (default behavior) + pub fn enable_auto_import(&mut self) { + self.auto_import = true; + } + + /// Check if auto-import is enabled + #[must_use] + pub fn is_auto_import_enabled(&self) -> bool { + self.auto_import + } + + /// Internal method to perform auto-import if needed + async fn maybe_auto_import(&self) -> Result<()> { + if !self.auto_import { + return Ok(()); + } + + // Check if already attempted + { + let attempted = self.auto_import_attempted.read().await; + if *attempted { + return Ok(()); + } + } + + // Check if cache is empty + let cache_empty = { + let cache = self.cache.read().await; + cache.is_empty() + }; + + if cache_empty { + tracing::info!("Cache empty, auto-importing sessions..."); + let options = ImportOptions::new().with_limit(100); + match self.import_all(&options).await { + Ok(sessions) => { + tracing::info!("Auto-imported {} sessions", sessions.len()); + } + Err(e) => { + tracing::warn!("Auto-import failed: {}", e); + } + } + } + + // Mark as attempted + let mut attempted = self.auto_import_attempted.write().await; + *attempted = true; + + Ok(()) + } + /// Get the connector registry #[must_use] pub fn registry(&self) -> &ConnectorRegistry { @@ -95,7 +159,13 @@ impl SessionService { } /// List all cached sessions + /// Auto-imports from available sources if cache is empty and auto-import is enabled pub async fn list_sessions(&self) -> Vec { + // Try auto-import if needed + if let Err(e) = self.maybe_auto_import().await { + tracing::warn!("Auto-import check failed: {}", e); + } + let cache = self.cache.read().await; cache.values().cloned().collect() } @@ -107,7 +177,13 @@ impl SessionService { } /// Search sessions by query string + /// Auto-imports from available sources if cache is empty and auto-import is enabled pub async fn search(&self, query: &str) -> Vec { + // Try auto-import if needed + if let Err(e) = self.maybe_auto_import().await { + tracing::warn!("Auto-import check failed: {}", e); + } + let cache = self.cache.read().await; let query_lower = query.to_lowercase(); @@ -142,7 +218,13 @@ impl SessionService { } /// Get sessions by source + /// Auto-imports from available sources if cache is empty and auto-import is enabled pub async fn sessions_by_source(&self, source: &str) -> Vec { + // Try auto-import if needed + if let Err(e) = self.maybe_auto_import().await { + tracing::warn!("Auto-import check failed: {}", e); + } + let cache = self.cache.read().await; cache .values() @@ -172,7 +254,13 @@ impl SessionService { } /// Get summary statistics + /// Auto-imports from available sources if cache is empty and auto-import is enabled pub async fn statistics(&self) -> SessionStatistics { + // Try auto-import if needed + if let Err(e) = self.maybe_auto_import().await { + tracing::warn!("Auto-import check failed: {}", e); + } + let cache = self.cache.read().await; let mut total_messages = 0; @@ -235,6 +323,17 @@ impl Default for SessionService { } } +impl Clone for SessionService { + fn clone(&self) -> Self { + Self { + registry: ConnectorRegistry::new(), + cache: Arc::new(RwLock::new(HashMap::new())), + auto_import: self.auto_import, + auto_import_attempted: Arc::new(RwLock::new(false)), + } + } +} + /// Information about a session source #[derive(Debug, Clone)] pub struct SourceInfo { @@ -290,7 +389,8 @@ mod tests { #[tokio::test] async fn test_statistics_empty() { - let service = SessionService::new(); + let mut service = SessionService::new(); + service.disable_auto_import(); let stats = service.statistics().await; assert_eq!(stats.total_sessions, 0); From 2005c49f6208d5f10f4dadf5ddc92ea02516b7d8 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Mar 2026 15:40:36 +0000 Subject: [PATCH 2/4] ci: retrigger workflow after cancellation From 9312906cbb04a56f708479a724a662a00b09ede7 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Mar 2026 15:45:33 +0000 Subject: [PATCH 3/4] ci: trigger workflow after runner restart From c6ce1aecf33cc6c260c5a44f068e62d043df9ab5 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Mar 2026 16:20:49 +0000 Subject: [PATCH 4/4] style: fix formatting and clippy warnings - Remove unnecessary return statement in commands.rs - Apply consistent import ordering and formatting --- crates/terraphim_agent/src/repl/commands.rs | 30 +++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/terraphim_agent/src/repl/commands.rs b/crates/terraphim_agent/src/repl/commands.rs index b0aeb225..a3359d95 100644 --- a/crates/terraphim_agent/src/repl/commands.rs +++ b/crates/terraphim_agent/src/repl/commands.rs @@ -1,6 +1,6 @@ //! Command definitions for REPL interface -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use std::str::FromStr; #[derive(Debug, Clone, PartialEq)] @@ -1100,11 +1100,9 @@ impl FromStr for ReplCommand { "sources" | "detect" => Ok(ReplCommand::Sessions { subcommand: SessionsSubcommand::Sources, }), - "import" => { - return Err(anyhow!( - "The 'import' command has been removed. Sessions are now automatically imported when needed. Use '/sessions list' or '/sessions search ' instead." - )); - } + "import" => Err(anyhow!( + "The 'import' command has been removed. Sessions are now automatically imported when needed. Use '/sessions list' or '/sessions search ' instead." + )), "list" | "ls" => { let mut source = None; let mut limit = None; @@ -1587,18 +1585,22 @@ mod tests { // Test update without subcommand let result = "/update".parse::(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("requires a subcommand")); + assert!( + result + .unwrap_err() + .to_string() + .contains("requires a subcommand") + ); // Test update rollback without version let result = "/update rollback".parse::(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("requires a version")); + assert!( + result + .unwrap_err() + .to_string() + .contains("requires a version") + ); // Test unknown update subcommand let result = "/update unknown".parse::();