Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ pub async fn run(update_notification: Option<crate::update::UpdateNotification>)
}

if args.history_subcommand {
if let Some(ref target) = args.history_target {
return ContextManager::show_specific_history(&config, target);
}
return ContextManager::list_global(&config);
}

Expand Down
23 changes: 22 additions & 1 deletion src/cli/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Global flag (used with history subcommand)
pub global: bool,

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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());
}
Expand All @@ -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());
}

Expand Down
84 changes: 82 additions & 2 deletions src/context/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
};
Expand All @@ -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();
Expand Down
Loading