mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 19:21:51 +08:00
Improve resumed CLI workflows beyond one-shot inspection
Extend --resume so operators can run multiple safe slash commands in sequence against a saved session file, including mutating maintenance actions like /compact and /clear plus useful local /init scaffolding. This brings resumed sessions closer to the live REPL command surface without pretending unsupported runtime-bound commands work offline. Constraint: Resumed sessions only have serialized session state, not a live model client or interactive runtime Rejected: Support every slash command under --resume | model and permission changes do not affect offline saved-session inspection meaningfully Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep --resume limited to commands that can operate purely from session files or local filesystem context Tested: cargo fmt --manifest-path ./rust/Cargo.toml --all; cargo clippy --manifest-path ./rust/Cargo.toml --workspace --all-targets -- -D warnings; cargo test --manifest-path ./rust/Cargo.toml --workspace Not-tested: Manual interactive smoke test of chained --resume commands in a shell session
This commit is contained in:
@@ -42,8 +42,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
|
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
|
||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path,
|
session_path,
|
||||||
command,
|
commands,
|
||||||
} => resume_session(&session_path, command),
|
} => resume_session(&session_path, &commands),
|
||||||
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)?,
|
||||||
CliAction::Help => print_help(),
|
CliAction::Help => print_help(),
|
||||||
@@ -61,7 +61,7 @@ enum CliAction {
|
|||||||
},
|
},
|
||||||
ResumeSession {
|
ResumeSession {
|
||||||
session_path: PathBuf,
|
session_path: PathBuf,
|
||||||
command: Option<String>,
|
commands: Vec<String>,
|
||||||
},
|
},
|
||||||
Prompt {
|
Prompt {
|
||||||
prompt: String,
|
prompt: String,
|
||||||
@@ -156,13 +156,16 @@ fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| "missing session path for --resume".to_string())
|
.ok_or_else(|| "missing session path for --resume".to_string())
|
||||||
.map(PathBuf::from)?;
|
.map(PathBuf::from)?;
|
||||||
let command = args.get(1).cloned();
|
let commands = args[1..].to_vec();
|
||||||
if args.len() > 2 {
|
if commands
|
||||||
return Err("--resume accepts at most one trailing slash command".to_string());
|
.iter()
|
||||||
|
.any(|command| !command.trim_start().starts_with('/'))
|
||||||
|
{
|
||||||
|
return Err("--resume trailing arguments must be slash commands".to_string());
|
||||||
}
|
}
|
||||||
Ok(CliAction::ResumeSession {
|
Ok(CliAction::ResumeSession {
|
||||||
session_path,
|
session_path,
|
||||||
command,
|
commands,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +201,7 @@ fn print_system_prompt(cwd: PathBuf, date: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resume_session(session_path: &Path, command: Option<String>) {
|
fn resume_session(session_path: &Path, commands: &[String]) {
|
||||||
let session = match Session::load_from_path(session_path) {
|
let session = match Session::load_from_path(session_path) {
|
||||||
Ok(session) => session,
|
Ok(session) => session,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -207,39 +210,55 @@ fn resume_session(session_path: &Path, command: Option<String>) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match command.as_deref().and_then(SlashCommand::parse) {
|
if commands.is_empty() {
|
||||||
Some(command) => match run_resume_command(session_path, &session, &command) {
|
|
||||||
Ok(Some(message)) => println!("{message}"),
|
|
||||||
Ok(None) => {}
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("{error}");
|
|
||||||
std::process::exit(2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None if command.is_some() => {
|
|
||||||
eprintln!(
|
|
||||||
"unsupported resumed command: {}",
|
|
||||||
command.unwrap_or_default()
|
|
||||||
);
|
|
||||||
std::process::exit(2);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
println!(
|
println!(
|
||||||
"Restored session from {} ({} messages).",
|
"Restored session from {} ({} messages).",
|
||||||
session_path.display(),
|
session_path.display(),
|
||||||
session.messages.len()
|
session.messages.len()
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut session = session;
|
||||||
|
for raw_command in commands {
|
||||||
|
let Some(command) = SlashCommand::parse(raw_command) else {
|
||||||
|
eprintln!("unsupported resumed command: {raw_command}");
|
||||||
|
std::process::exit(2);
|
||||||
|
};
|
||||||
|
match run_resume_command(session_path, &session, &command) {
|
||||||
|
Ok(ResumeCommandOutcome {
|
||||||
|
session: next_session,
|
||||||
|
message,
|
||||||
|
}) => {
|
||||||
|
session = next_session;
|
||||||
|
if let Some(message) = message {
|
||||||
|
println!("{message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("{error}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ResumeCommandOutcome {
|
||||||
|
session: Session,
|
||||||
|
message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_resume_command(
|
fn run_resume_command(
|
||||||
session_path: &Path,
|
session_path: &Path,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
command: &SlashCommand,
|
command: &SlashCommand,
|
||||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
) -> Result<ResumeCommandOutcome, Box<dyn std::error::Error>> {
|
||||||
match command {
|
match command {
|
||||||
SlashCommand::Help => Ok(Some(render_repl_help())),
|
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: Some(render_repl_help()),
|
||||||
|
}),
|
||||||
SlashCommand::Compact => {
|
SlashCommand::Compact => {
|
||||||
let Some(result) = handle_slash_command(
|
let Some(result) = handle_slash_command(
|
||||||
"/compact",
|
"/compact",
|
||||||
@@ -249,41 +268,73 @@ fn run_resume_command(
|
|||||||
..CompactionConfig::default()
|
..CompactionConfig::default()
|
||||||
},
|
},
|
||||||
) else {
|
) else {
|
||||||
return Ok(None);
|
return Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: None,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
result.session.save_to_path(session_path)?;
|
result.session.save_to_path(session_path)?;
|
||||||
Ok(Some(result.message))
|
Ok(ResumeCommandOutcome {
|
||||||
|
session: result.session,
|
||||||
|
message: Some(result.message),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SlashCommand::Clear => {
|
||||||
|
let cleared = Session::new();
|
||||||
|
cleared.save_to_path(session_path)?;
|
||||||
|
Ok(ResumeCommandOutcome {
|
||||||
|
session: cleared,
|
||||||
|
message: Some(format!(
|
||||||
|
"Cleared resumed session file {}.",
|
||||||
|
session_path.display()
|
||||||
|
)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Status => {
|
SlashCommand::Status => {
|
||||||
let usage = UsageTracker::from_session(session).cumulative_usage();
|
let tracker = UsageTracker::from_session(session);
|
||||||
Ok(Some(format_status_line(
|
let usage = tracker.cumulative_usage();
|
||||||
|
Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: Some(format_status_line(
|
||||||
"restored-session",
|
"restored-session",
|
||||||
session.messages.len(),
|
session.messages.len(),
|
||||||
UsageTracker::from_session(session).turns(),
|
tracker.turns(),
|
||||||
UsageTracker::from_session(session).current_turn_usage(),
|
tracker.current_turn_usage(),
|
||||||
usage,
|
usage,
|
||||||
0,
|
0,
|
||||||
permission_mode_label(),
|
permission_mode_label(),
|
||||||
)))
|
)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Cost => {
|
SlashCommand::Cost => {
|
||||||
let usage = UsageTracker::from_session(session).cumulative_usage();
|
let usage = UsageTracker::from_session(session).cumulative_usage();
|
||||||
Ok(Some(format!(
|
Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: Some(format!(
|
||||||
"cost: input_tokens={} output_tokens={} cache_creation_tokens={} cache_read_tokens={} total_tokens={}",
|
"cost: input_tokens={} output_tokens={} cache_creation_tokens={} cache_read_tokens={} total_tokens={}",
|
||||||
usage.input_tokens,
|
usage.input_tokens,
|
||||||
usage.output_tokens,
|
usage.output_tokens,
|
||||||
usage.cache_creation_input_tokens,
|
usage.cache_creation_input_tokens,
|
||||||
usage.cache_read_input_tokens,
|
usage.cache_read_input_tokens,
|
||||||
usage.total_tokens(),
|
usage.total_tokens(),
|
||||||
)))
|
)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Config => Ok(Some(render_config_report()?)),
|
SlashCommand::Config => Ok(ResumeCommandOutcome {
|
||||||
SlashCommand::Memory => Ok(Some(render_memory_report()?)),
|
session: session.clone(),
|
||||||
|
message: Some(render_config_report()?),
|
||||||
|
}),
|
||||||
|
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: Some(render_memory_report()?),
|
||||||
|
}),
|
||||||
|
SlashCommand::Init => Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: Some(init_claude_md()?),
|
||||||
|
}),
|
||||||
SlashCommand::Resume { .. }
|
SlashCommand::Resume { .. }
|
||||||
| SlashCommand::Model { .. }
|
| SlashCommand::Model { .. }
|
||||||
| SlashCommand::Permissions { .. }
|
| SlashCommand::Permissions { .. }
|
||||||
| SlashCommand::Clear
|
|
||||||
| SlashCommand::Init
|
|
||||||
| SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),
|
| SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1021,7 +1072,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 system-prompt [--cwd PATH] [--date YYYY-MM-DD]");
|
println!(" rusty-claude-cli system-prompt [--cwd PATH] [--date YYYY-MM-DD]");
|
||||||
println!(" rusty-claude-cli --resume SESSION.json [/compact]");
|
println!(" rusty-claude-cli --resume SESSION.json [/status] [/compact] [...]");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1088,7 +1139,29 @@ mod tests {
|
|||||||
parse_args(&args).expect("args should parse"),
|
parse_args(&args).expect("args should parse"),
|
||||||
CliAction::ResumeSession {
|
CliAction::ResumeSession {
|
||||||
session_path: PathBuf::from("session.json"),
|
session_path: PathBuf::from("session.json"),
|
||||||
command: Some("/compact".to_string()),
|
commands: vec!["/compact".to_string()],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_resume_flag_with_multiple_slash_commands() {
|
||||||
|
let args = vec![
|
||||||
|
"--resume".to_string(),
|
||||||
|
"session.json".to_string(),
|
||||||
|
"/status".to_string(),
|
||||||
|
"/compact".to_string(),
|
||||||
|
"/cost".to_string(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&args).expect("args should parse"),
|
||||||
|
CliAction::ResumeSession {
|
||||||
|
session_path: PathBuf::from("session.json"),
|
||||||
|
commands: vec![
|
||||||
|
"/status".to_string(),
|
||||||
|
"/compact".to_string(),
|
||||||
|
"/cost".to_string(),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user