mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 07:41:52 +08:00
Expose session details without requiring manual JSON inspection
This adds a dedicated session inspect command to the Rust CLI so users can inspect a saved session's path, timestamps, size, token totals, preview text, and latest user/assistant context without opening the underlying file by hand. It builds directly on the new session list/resume flows and keeps the UX lightweight and script-friendly. Constraint: Keep session inspection CLI-native and read-only Constraint: Reuse the existing saved-session format instead of introducing a secondary index format Rejected: Add an interactive session browser now | more overhead than needed for this inspect slice Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep session inspection output stable and grep-friendly so it remains useful in scripts Tested: cargo fmt; cargo clippy --all-targets --all-features -- -D warnings; cargo test -q Not-tested: Manual inspection against a large corpus of real saved sessions
This commit is contained in:
@@ -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![
|
||||||
|
|||||||
Reference in New Issue
Block a user