mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 16:21:52 +08:00
Prevent accidental session clears in REPL and resume flows
Require an explicit /clear --confirm flag before wiping live or resumed session state. This keeps the command genuinely useful while adding the minimal safety check needed for a destructive command in a chatty terminal workflow. Constraint: /clear must remain a real functional command without introducing interactive prompt machinery that would complicate REPL input handling Rejected: Add y/n interactive confirmation prompt | extra stateful prompting would be slower to ship and more fragile inside the line editor loop Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep destructive slash commands opt-in via explicit flags unless the CLI gains a dedicated confirmation subsystem 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 keyboard-driven UX pass for accidental /clear entry in interactive REPL
This commit is contained in:
@@ -72,7 +72,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "clear",
|
name: "clear",
|
||||||
summary: "Start a fresh local session",
|
summary: "Start a fresh local session",
|
||||||
argument_hint: None,
|
argument_hint: Some("[--confirm]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
@@ -114,7 +114,7 @@ pub enum SlashCommand {
|
|||||||
Compact,
|
Compact,
|
||||||
Model { model: Option<String> },
|
Model { model: Option<String> },
|
||||||
Permissions { mode: Option<String> },
|
Permissions { mode: Option<String> },
|
||||||
Clear,
|
Clear { confirm: bool },
|
||||||
Cost,
|
Cost,
|
||||||
Resume { session_path: Option<String> },
|
Resume { session_path: Option<String> },
|
||||||
Config,
|
Config,
|
||||||
@@ -143,7 +143,9 @@ impl SlashCommand {
|
|||||||
"permissions" => Self::Permissions {
|
"permissions" => Self::Permissions {
|
||||||
mode: parts.next().map(ToOwned::to_owned),
|
mode: parts.next().map(ToOwned::to_owned),
|
||||||
},
|
},
|
||||||
"clear" => Self::Clear,
|
"clear" => Self::Clear {
|
||||||
|
confirm: parts.next() == Some("--confirm"),
|
||||||
|
},
|
||||||
"cost" => Self::Cost,
|
"cost" => Self::Cost,
|
||||||
"resume" => Self::Resume {
|
"resume" => Self::Resume {
|
||||||
session_path: parts.next().map(ToOwned::to_owned),
|
session_path: parts.next().map(ToOwned::to_owned),
|
||||||
@@ -225,7 +227,7 @@ pub fn handle_slash_command(
|
|||||||
SlashCommand::Status
|
SlashCommand::Status
|
||||||
| SlashCommand::Model { .. }
|
| SlashCommand::Model { .. }
|
||||||
| SlashCommand::Permissions { .. }
|
| SlashCommand::Permissions { .. }
|
||||||
| SlashCommand::Clear
|
| SlashCommand::Clear { .. }
|
||||||
| SlashCommand::Cost
|
| SlashCommand::Cost
|
||||||
| SlashCommand::Resume { .. }
|
| SlashCommand::Resume { .. }
|
||||||
| SlashCommand::Config
|
| SlashCommand::Config
|
||||||
@@ -263,7 +265,14 @@ mod tests {
|
|||||||
mode: Some("read-only".to_string()),
|
mode: Some("read-only".to_string()),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(SlashCommand::parse("/clear"), Some(SlashCommand::Clear));
|
assert_eq!(
|
||||||
|
SlashCommand::parse("/clear"),
|
||||||
|
Some(SlashCommand::Clear { confirm: false })
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SlashCommand::parse("/clear --confirm"),
|
||||||
|
Some(SlashCommand::Clear { confirm: true })
|
||||||
|
);
|
||||||
assert_eq!(SlashCommand::parse("/cost"), Some(SlashCommand::Cost));
|
assert_eq!(SlashCommand::parse("/cost"), Some(SlashCommand::Cost));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SlashCommand::parse("/resume session.json"),
|
SlashCommand::parse("/resume session.json"),
|
||||||
@@ -285,7 +294,7 @@ mod tests {
|
|||||||
assert!(help.contains("/compact"));
|
assert!(help.contains("/compact"));
|
||||||
assert!(help.contains("/model [model]"));
|
assert!(help.contains("/model [model]"));
|
||||||
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
||||||
assert!(help.contains("/clear"));
|
assert!(help.contains("/clear [--confirm]"));
|
||||||
assert!(help.contains("/cost"));
|
assert!(help.contains("/cost"));
|
||||||
assert!(help.contains("/resume <session-path>"));
|
assert!(help.contains("/resume <session-path>"));
|
||||||
assert!(help.contains("/config"));
|
assert!(help.contains("/config"));
|
||||||
@@ -349,6 +358,10 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
assert!(handle_slash_command("/clear", &session, CompactionConfig::default()).is_none());
|
assert!(handle_slash_command("/clear", &session, CompactionConfig::default()).is_none());
|
||||||
|
assert!(
|
||||||
|
handle_slash_command("/clear --confirm", &session, CompactionConfig::default())
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
assert!(handle_slash_command("/cost", &session, CompactionConfig::default()).is_none());
|
assert!(handle_slash_command("/cost", &session, CompactionConfig::default()).is_none());
|
||||||
assert!(handle_slash_command(
|
assert!(handle_slash_command(
|
||||||
"/resume session.json",
|
"/resume session.json",
|
||||||
|
|||||||
@@ -299,7 +299,15 @@ fn run_resume_command(
|
|||||||
message: Some(result.message),
|
message: Some(result.message),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Clear => {
|
SlashCommand::Clear { confirm } => {
|
||||||
|
if !confirm {
|
||||||
|
return Ok(ResumeCommandOutcome {
|
||||||
|
session: session.clone(),
|
||||||
|
message: Some(
|
||||||
|
"clear: confirmation required; rerun with /clear --confirm".to_string(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
let cleared = Session::new();
|
let cleared = Session::new();
|
||||||
cleared.save_to_path(session_path)?;
|
cleared.save_to_path(session_path)?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
@@ -448,7 +456,7 @@ impl LiveCli {
|
|||||||
SlashCommand::Compact => self.compact()?,
|
SlashCommand::Compact => self.compact()?,
|
||||||
SlashCommand::Model { model } => self.set_model(model)?,
|
SlashCommand::Model { model } => self.set_model(model)?,
|
||||||
SlashCommand::Permissions { mode } => self.set_permissions(mode)?,
|
SlashCommand::Permissions { mode } => self.set_permissions(mode)?,
|
||||||
SlashCommand::Clear => self.clear_session()?,
|
SlashCommand::Clear { confirm } => self.clear_session(confirm)?,
|
||||||
SlashCommand::Cost => self.print_cost(),
|
SlashCommand::Cost => self.print_cost(),
|
||||||
SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
|
SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
|
||||||
SlashCommand::Config => Self::print_config()?,
|
SlashCommand::Config => Self::print_config()?,
|
||||||
@@ -526,7 +534,14 @@ impl LiveCli {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_session(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
fn clear_session(&mut self, confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if !confirm {
|
||||||
|
println!(
|
||||||
|
"clear: confirmation required; run /clear --confirm to start a fresh session."
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
self.runtime = build_runtime_with_permission_mode(
|
self.runtime = build_runtime_with_permission_mode(
|
||||||
Session::new(),
|
Session::new(),
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -1274,7 +1289,7 @@ mod tests {
|
|||||||
assert!(help.contains("/status"));
|
assert!(help.contains("/status"));
|
||||||
assert!(help.contains("/model [model]"));
|
assert!(help.contains("/model [model]"));
|
||||||
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
||||||
assert!(help.contains("/clear"));
|
assert!(help.contains("/clear [--confirm]"));
|
||||||
assert!(help.contains("/cost"));
|
assert!(help.contains("/cost"));
|
||||||
assert!(help.contains("/resume <session-path>"));
|
assert!(help.contains("/resume <session-path>"));
|
||||||
assert!(help.contains("/config"));
|
assert!(help.contains("/config"));
|
||||||
@@ -1367,6 +1382,18 @@ mod tests {
|
|||||||
assert_eq!(normalize_permission_mode("unknown"), None);
|
assert_eq!(normalize_permission_mode("unknown"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_command_requires_explicit_confirmation_flag() {
|
||||||
|
assert_eq!(
|
||||||
|
SlashCommand::parse("/clear"),
|
||||||
|
Some(SlashCommand::Clear { confirm: false })
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SlashCommand::parse("/clear --confirm"),
|
||||||
|
Some(SlashCommand::Clear { confirm: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_resume_and_config_slash_commands() {
|
fn parses_resume_and_config_slash_commands() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1375,6 +1402,10 @@ mod tests {
|
|||||||
session_path: Some("saved-session.json".to_string())
|
session_path: Some("saved-session.json".to_string())
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SlashCommand::parse("/clear --confirm"),
|
||||||
|
Some(SlashCommand::Clear { confirm: true })
|
||||||
|
);
|
||||||
assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config));
|
assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config));
|
||||||
assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
|
assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
|
||||||
assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
|
assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
|
||||||
|
|||||||
Reference in New Issue
Block a user