mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 16:11:52 +08:00
feat(cli): add resume and config inspection commands
Add in-REPL session restoration and read-only config inspection so the CLI can recover saved conversations and expose Claude settings without leaving interactive mode. /resume now reloads a session file into the live runtime, and /config shows discovered settings files plus the merged effective JSON. The new commands stay on the shared slash-command surface and rebuild runtime state using the current model, system prompt, and permission mode so existing REPL behavior remains stable. Constraint: /resume must update the live REPL session rather than only supporting top-level --resume Constraint: /config should inspect existing settings without mutating user files Rejected: Add editable /config writes in this slice | read-only inspection is safer and sufficient for immediate parity work Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep resume/config behavior on the shared slash command surface so non-REPL entrypoints can reuse it later Tested: cargo fmt; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace Not-tested: manual interactive restore against real saved session files outside automated fixtures
This commit is contained in:
@@ -15,9 +15,9 @@ use commands::{handle_slash_command, render_slash_command_help, SlashCommand};
|
||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||
use render::{Spinner, TerminalRenderer};
|
||||
use runtime::{
|
||||
load_system_prompt, ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ContentBlock,
|
||||
ConversationMessage, ConversationRuntime, MessageRole, PermissionMode, PermissionPolicy,
|
||||
RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
|
||||
load_system_prompt, ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader,
|
||||
ConfigSource, ContentBlock, ConversationMessage, ConversationRuntime, MessageRole,
|
||||
PermissionMode, PermissionPolicy, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
|
||||
};
|
||||
use tools::{execute_tool, mvp_tool_specs};
|
||||
|
||||
@@ -326,6 +326,8 @@ impl LiveCli {
|
||||
SlashCommand::Permissions { mode } => self.set_permissions(mode)?,
|
||||
SlashCommand::Clear => self.clear_session()?,
|
||||
SlashCommand::Cost => self.print_cost(),
|
||||
SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
|
||||
SlashCommand::Config => Self::print_config()?,
|
||||
SlashCommand::Unknown(name) => eprintln!("unknown slash command: /{name}"),
|
||||
}
|
||||
Ok(())
|
||||
@@ -419,6 +421,60 @@ impl LiveCli {
|
||||
);
|
||||
}
|
||||
|
||||
fn resume_session(
|
||||
&mut self,
|
||||
session_path: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(session_path) = session_path else {
|
||||
println!("Usage: /resume <session-path>");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let session = Session::load_from_path(&session_path)?;
|
||||
let message_count = session.messages.len();
|
||||
self.runtime = build_runtime_with_permission_mode(
|
||||
session,
|
||||
self.model.clone(),
|
||||
self.system_prompt.clone(),
|
||||
true,
|
||||
permission_mode_label(),
|
||||
)?;
|
||||
println!("Resumed session from {session_path} ({message_count} messages).");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
let loader = ConfigLoader::default_for(&cwd);
|
||||
let discovered = loader.discover();
|
||||
let runtime_config = loader.load()?;
|
||||
|
||||
println!(
|
||||
"config: loaded_files={} merged_keys={}",
|
||||
runtime_config.loaded_entries().len(),
|
||||
runtime_config.merged().len()
|
||||
);
|
||||
for entry in discovered {
|
||||
let source = match entry.source {
|
||||
ConfigSource::User => "user",
|
||||
ConfigSource::Project => "project",
|
||||
ConfigSource::Local => "local",
|
||||
};
|
||||
let status = if runtime_config
|
||||
.loaded_entries()
|
||||
.iter()
|
||||
.any(|loaded_entry| loaded_entry.path == entry.path)
|
||||
{
|
||||
"loaded"
|
||||
} else {
|
||||
"missing"
|
||||
};
|
||||
println!(" {source:<7} {status:<7} {}", entry.path.display());
|
||||
}
|
||||
println!(" merged {}", runtime_config.as_json().render());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compact(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let result = self.runtime.compact(CompactionConfig::default());
|
||||
let removed = result.removed_message_count;
|
||||
@@ -798,7 +854,7 @@ fn print_help() {
|
||||
mod tests {
|
||||
use super::{
|
||||
format_status_line, normalize_permission_mode, parse_args, render_repl_help, CliAction,
|
||||
DEFAULT_MODEL,
|
||||
SlashCommand, DEFAULT_MODEL,
|
||||
};
|
||||
use runtime::{ContentBlock, ConversationMessage, MessageRole};
|
||||
use std::path::PathBuf;
|
||||
@@ -872,6 +928,8 @@ mod tests {
|
||||
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
||||
assert!(help.contains("/clear"));
|
||||
assert!(help.contains("/cost"));
|
||||
assert!(help.contains("/resume <session-path>"));
|
||||
assert!(help.contains("/config"));
|
||||
assert!(help.contains("/exit"));
|
||||
}
|
||||
|
||||
@@ -917,6 +975,17 @@ mod tests {
|
||||
assert_eq!(normalize_permission_mode("unknown"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_resume_and_config_slash_commands() {
|
||||
assert_eq!(
|
||||
SlashCommand::parse("/resume saved-session.json"),
|
||||
Some(SlashCommand::Resume {
|
||||
session_path: Some("saved-session.json".to_string())
|
||||
})
|
||||
);
|
||||
assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_tool_roundtrip_messages() {
|
||||
let messages = vec![
|
||||
|
||||
Reference in New Issue
Block a user