|
@@ -47,6 +47,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
command,
|
|
command,
|
|
|
} => resume_session(&session_path, command),
|
|
} => resume_session(&session_path, command),
|
|
|
CliAction::ResumeNamed { target, command } => resume_named_session(&target, command),
|
|
CliAction::ResumeNamed { target, command } => resume_named_session(&target, command),
|
|
|
|
|
+ CliAction::InspectSession { target } => inspect_session(&target),
|
|
|
CliAction::ListSessions { query, limit } => list_sessions(query.as_deref(), limit),
|
|
CliAction::ListSessions { query, limit } => list_sessions(query.as_deref(), limit),
|
|
|
CliAction::Prompt { prompt, model } => LiveCli::new(model, false)?.run_turn(&prompt)?,
|
|
CliAction::Prompt { prompt, model } => LiveCli::new(model, false)?.run_turn(&prompt)?,
|
|
|
CliAction::Repl { model } => run_repl(model)?,
|
|
CliAction::Repl { model } => run_repl(model)?,
|
|
@@ -71,6 +72,9 @@ enum CliAction {
|
|
|
target: String,
|
|
target: String,
|
|
|
command: Option<String>,
|
|
command: Option<String>,
|
|
|
},
|
|
},
|
|
|
|
|
+ InspectSession {
|
|
|
|
|
+ target: String,
|
|
|
|
|
+ },
|
|
|
ListSessions {
|
|
ListSessions {
|
|
|
query: Option<String>,
|
|
query: Option<String>,
|
|
|
limit: usize,
|
|
limit: usize,
|
|
@@ -124,6 +128,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|
|
"dump-manifests" => Ok(CliAction::DumpManifests),
|
|
"dump-manifests" => Ok(CliAction::DumpManifests),
|
|
|
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
|
|
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
|
|
|
"resume" => parse_named_resume_args(&rest[1..]),
|
|
"resume" => parse_named_resume_args(&rest[1..]),
|
|
|
|
|
+ "session" => parse_session_inspect_args(&rest[1..]),
|
|
|
"sessions" => parse_sessions_args(&rest[1..]),
|
|
"sessions" => parse_sessions_args(&rest[1..]),
|
|
|
"system-prompt" => parse_system_prompt_args(&rest[1..]),
|
|
"system-prompt" => parse_system_prompt_args(&rest[1..]),
|
|
|
"prompt" => {
|
|
"prompt" => {
|
|
@@ -177,6 +182,17 @@ fn parse_named_resume_args(args: &[String]) -> Result<CliAction, String> {
|
|
|
Ok(CliAction::ResumeNamed { target, command })
|
|
Ok(CliAction::ResumeNamed { target, command })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn parse_session_inspect_args(args: &[String]) -> Result<CliAction, String> {
|
|
|
|
|
+ let target = args
|
|
|
|
|
+ .first()
|
|
|
|
|
+ .ok_or_else(|| "missing session id, path, or 'latest' for session".to_string())?
|
|
|
|
|
+ .clone();
|
|
|
|
|
+ if args.len() > 1 {
|
|
|
|
|
+ return Err("session accepts exactly one target argument".to_string());
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(CliAction::InspectSession { target })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn parse_sessions_args(args: &[String]) -> Result<CliAction, String> {
|
|
fn parse_sessions_args(args: &[String]) -> Result<CliAction, String> {
|
|
|
let mut query = None;
|
|
let mut query = None;
|
|
|
let mut limit = DEFAULT_SESSION_LIMIT;
|
|
let mut limit = DEFAULT_SESSION_LIMIT;
|
|
@@ -333,6 +349,53 @@ fn list_sessions(query: Option<&str>, limit: usize) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn inspect_session(target: &str) {
|
|
|
|
|
+ let path = match resolve_session_target(target) {
|
|
|
|
|
+ Ok(path) => path,
|
|
|
|
|
+ Err(error) => {
|
|
|
|
|
+ eprintln!("{error}");
|
|
|
|
|
+ std::process::exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let session = match Session::load_from_path(&path) {
|
|
|
|
|
+ Ok(session) => session,
|
|
|
|
|
+ Err(error) => {
|
|
|
|
|
+ eprintln!("failed to load session: {error}");
|
|
|
|
|
+ std::process::exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let metadata = fs::metadata(&path).ok();
|
|
|
|
|
+ let updated_unix = metadata
|
|
|
|
|
+ .as_ref()
|
|
|
|
|
+ .and_then(|meta| meta.modified().ok())
|
|
|
|
|
+ .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
|
|
|
|
|
+ .map_or(0, |duration| duration.as_secs());
|
|
|
|
|
+ let bytes = metadata.as_ref().map_or(0, std::fs::Metadata::len);
|
|
|
|
|
+ let usage = runtime::UsageTracker::from_session(&session).cumulative_usage();
|
|
|
|
|
+
|
|
|
|
|
+ println!("Session details:");
|
|
|
|
|
+ println!(
|
|
|
|
|
+ "- id: {}",
|
|
|
|
|
+ path.file_stem()
|
|
|
|
|
+ .map_or_else(String::new, |stem| stem.to_string_lossy().into_owned())
|
|
|
|
|
+ );
|
|
|
|
|
+ println!("- path: {}", path.display());
|
|
|
|
|
+ println!("- updated: {updated_unix}");
|
|
|
|
|
+ println!("- size_bytes: {bytes}");
|
|
|
|
|
+ println!("- messages: {}", session.messages.len());
|
|
|
|
|
+ println!("- total_tokens: {}", usage.total_tokens());
|
|
|
|
|
+ println!("- preview: {}", session_preview(&session));
|
|
|
|
|
+
|
|
|
|
|
+ if let Some(user_text) = latest_text_for_role(&session, MessageRole::User) {
|
|
|
|
|
+ println!("- latest_user: {user_text}");
|
|
|
|
|
+ }
|
|
|
|
|
+ if let Some(assistant_text) = latest_text_for_role(&session, MessageRole::Assistant) {
|
|
|
|
|
+ println!("- latest_assistant: {assistant_text}");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn run_repl(model: String) -> Result<(), Box<dyn std::error::Error>> {
|
|
fn run_repl(model: String) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
let mut cli = LiveCli::new(model, true)?;
|
|
let mut cli = LiveCli::new(model, true)?;
|
|
|
let editor = input::LineEditor::new("› ");
|
|
let editor = input::LineEditor::new("› ");
|
|
@@ -647,6 +710,21 @@ fn session_preview(session: &Session) -> String {
|
|
|
"No text preview available".to_string()
|
|
"No text preview available".to_string()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn latest_text_for_role(session: &Session, role: MessageRole) -> Option<String> {
|
|
|
|
|
+ session.messages.iter().rev().find_map(|message| {
|
|
|
|
|
+ if message.role != role {
|
|
|
|
|
+ return None;
|
|
|
|
|
+ }
|
|
|
|
|
+ message.blocks.iter().find_map(|block| match block {
|
|
|
|
|
+ ContentBlock::Text { text } => {
|
|
|
|
|
+ let trimmed = text.trim();
|
|
|
|
|
+ (!trimmed.is_empty()).then(|| truncate_preview(trimmed, 120))
|
|
|
|
|
+ }
|
|
|
|
|
+ ContentBlock::ToolUse { .. } | ContentBlock::ToolResult { .. } => None,
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn truncate_preview(text: &str, max_chars: usize) -> String {
|
|
fn truncate_preview(text: &str, max_chars: usize) -> String {
|
|
|
if text.chars().count() <= max_chars {
|
|
if text.chars().count() <= max_chars {
|
|
|
return text.to_string();
|
|
return text.to_string();
|
|
@@ -1033,6 +1111,7 @@ fn print_help() {
|
|
|
println!(" rusty-claude-cli dump-manifests");
|
|
println!(" rusty-claude-cli dump-manifests");
|
|
|
println!(" rusty-claude-cli bootstrap-plan");
|
|
println!(" rusty-claude-cli bootstrap-plan");
|
|
|
println!(" rusty-claude-cli sessions [--query TEXT] [--limit N]");
|
|
println!(" rusty-claude-cli sessions [--query TEXT] [--limit N]");
|
|
|
|
|
+ println!(" rusty-claude-cli session <latest|SESSION|PATH>");
|
|
|
println!(" rusty-claude-cli resume <latest|SESSION|PATH> [/compact]");
|
|
println!(" rusty-claude-cli resume <latest|SESSION|PATH> [/compact]");
|
|
|
println!(" env RUSTY_CLAUDE_PERMISSION_MODE=prompt enables interactive tool approval");
|
|
println!(" env RUSTY_CLAUDE_PERMISSION_MODE=prompt enables interactive tool approval");
|
|
|
println!(" rusty-claude-cli system-prompt [--cwd PATH] [--date YYYY-MM-DD]");
|
|
println!(" rusty-claude-cli system-prompt [--cwd PATH] [--date YYYY-MM-DD]");
|
|
@@ -1107,6 +1186,17 @@ mod tests {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn parses_session_inspect_subcommand() {
|
|
|
|
|
+ let args = vec!["session".to_string(), "latest".to_string()];
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ parse_args(&args).expect("args should parse"),
|
|
|
|
|
+ CliAction::InspectSession {
|
|
|
|
|
+ target: "latest".to_string(),
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
#[test]
|
|
#[test]
|
|
|
fn parses_sessions_subcommand() {
|
|
fn parses_sessions_subcommand() {
|
|
|
let args = vec![
|
|
let args = vec![
|