diff --git a/src/cli/mod.rs b/src/cli/mod.rs index be205cf..b747dd6 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -129,6 +129,9 @@ pub async fn run(update_notification: Option) } if args.history_subcommand { + if let Some(ref target) = args.history_target { + return ContextManager::show_specific_history(&config, target); + } return ContextManager::list_global(&config); } diff --git a/src/cli/parser.rs b/src/cli/parser.rs index 09399ae..f5fc186 100644 --- a/src/cli/parser.rs +++ b/src/cli/parser.rs @@ -100,6 +100,9 @@ pub struct Args { /// List all global history pub history_subcommand: bool, + /// View or act on specific history (ID or path) + pub history_target: Option, + /// Global flag (used with history subcommand) pub global: bool, @@ -246,7 +249,14 @@ impl Args { // Subcommands "init" | "config" if query_parts.is_empty() => result.init = true, "profiles" if query_parts.is_empty() => result.list_profiles = true, - "history" if query_parts.is_empty() => result.history_subcommand = true, + "history" if query_parts.is_empty() => { + result.history_subcommand = true; + // Check if there's a target argument next (not a flag) + if i + 1 < args.len() && !args[i + 1].starts_with('-') { + i += 1; + result.history_target = Some(args[i].clone()); + } + } "--clear" => result.clear_context = true, "--history" => result.show_history = true, "--global" => result.global = true, @@ -678,6 +688,7 @@ mod tests { fn test_parse_history_subcommand() { let args = Args::parse_args(vec!["history".into()]); assert!(args.history_subcommand); + assert!(args.history_target.is_none()); assert!(!args.global); assert!(args.query.is_empty()); } @@ -687,6 +698,16 @@ mod tests { let args = Args::parse_args(vec!["history".into(), "--global".into()]); assert!(args.history_subcommand); assert!(args.global); + assert!(args.history_target.is_none()); + assert!(args.query.is_empty()); + } + + #[test] + fn test_parse_history_with_target() { + let args = Args::parse_args(vec!["history".into(), "1234abcd".into()]); + assert!(args.history_subcommand); + assert_eq!(args.history_target, Some("1234abcd".to_string())); + assert!(!args.global); assert!(args.query.is_empty()); } diff --git a/src/context/manager.rs b/src/context/manager.rs index a39228c..2933b2a 100644 --- a/src/context/manager.rs +++ b/src/context/manager.rs @@ -181,8 +181,9 @@ impl ContextManager { println!("[{}] {}", role_color, msg.timestamp.format("%H:%M:%S")); // Truncate long messages - let content = if msg.content.len() > 200 { - format!("{}...", &msg.content[..200]) + let content = if msg.content.chars().count() > 200 { + let truncated: String = msg.content.chars().take(200).collect(); + format!("{}...", truncated) } else { msg.content.clone() }; @@ -203,6 +204,85 @@ impl ContextManager { Ok(()) } + /// Show specific history by ID or path + pub fn show_specific_history(config: &Config, target: &str) -> Result<()> { + let storage_path = config.context_storage_path(); + let storage = ContextStorage::new(storage_path)?; + let contexts = storage.list()?; + + if contexts.is_empty() { + println!("{}", "No global context history found.".yellow()); + return Ok(()); + } + + let current_dir = std::env::current_dir() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Resolve absolute path if target looks like a path + let search_target = if target == "." { + current_dir.clone() + } else if let Ok(abs_path) = std::fs::canonicalize(target) { + abs_path.to_string_lossy().to_string() + } else { + target.to_string() + }; + + // Find by exact path or prefix of ID + let matching_ctx = contexts.into_iter().find(|ctx| { + ctx.id.starts_with(&search_target) || ctx.pwd == search_target + }); + + match matching_ctx { + Some(ctx) => { + println!("{} {}", "Context for:".cyan(), ctx.pwd.bright_white()); + println!("{} {}", "ID:".cyan(), ctx.id.bright_black()); + println!( + "{} {}", + "Created:".cyan(), + ctx.created_at.format("%Y-%m-%d %H:%M:%S") + ); + println!( + "{} {}", + "Last used:".cyan(), + ctx.last_used.format("%Y-%m-%d %H:%M:%S") + ); + println!("{} {}", "Messages:".cyan(), ctx.messages.len()); + println!(); + + for msg in &ctx.messages { + let role_color = match msg.role.as_str() { + "user" => msg.role.green(), + "assistant" => msg.role.blue(), + _ => msg.role.normal(), + }; + + println!("[{}] {}", role_color, msg.timestamp.format("%H:%M:%S")); + + let content = if msg.content.chars().count() > 200 { + let truncated: String = msg.content.chars().take(200).collect(); + format!("{}...", truncated) + } else { + msg.content.clone() + }; + + println!("{}", content.bright_black()); + println!(); + } + } + None => { + println!( + "{} '{}'", + "No context found matching:".yellow(), + search_target.bright_white() + ); + } + } + + Ok(()) + } + /// List all global context history pub fn list_global(config: &Config) -> Result<()> { let storage_path = config.context_storage_path();