mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 16:01:52 +08:00
Make tool approvals and summaries easier to understand
This adds a prompt-mode permission flow for the Rust CLI, surfaces permission policy details in the REPL, and improves tool output rendering with concise human-readable summaries before the raw JSON payload. The goal is to make tool execution feel safer and more legible without changing the underlying runtime loop or adding a heavyweight UI layer. Constraint: Keep the permission UX terminal-native and incremental Constraint: Preserve existing allow and read-only behavior while adding prompt mode Rejected: Build a full-screen interactive approval UI now | unnecessary complexity for this parity slice Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep raw tool JSON available even when adding richer summaries so debugging fidelity remains intact Tested: cargo fmt; cargo clippy --all-targets --all-features -- -D warnings; cargo test -q Not-tested: Manual prompt-mode approvals against live API-driven tool calls
This commit is contained in:
@@ -19,7 +19,8 @@ use render::{Spinner, TerminalRenderer};
|
||||
use runtime::{
|
||||
estimate_session_tokens, load_system_prompt, ApiClient, ApiRequest, AssistantEvent,
|
||||
CompactionConfig, ContentBlock, ConversationMessage, ConversationRuntime, MessageRole,
|
||||
PermissionMode, PermissionPolicy, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
|
||||
PermissionMode, PermissionPolicy, PermissionPromptDecision, PermissionPrompter,
|
||||
PermissionRequest, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
|
||||
};
|
||||
use tools::{execute_tool, mvp_tool_specs};
|
||||
|
||||
@@ -347,12 +348,16 @@ fn run_repl(model: String) -> Result<(), Box<dyn std::error::Error>> {
|
||||
"/exit" | "/quit" => break,
|
||||
"/help" => {
|
||||
println!("Available commands:");
|
||||
println!(" /help Show help");
|
||||
println!(" /status Show session status");
|
||||
println!(" /compact Compact session history");
|
||||
println!(" /exit Quit the REPL");
|
||||
println!(" /help Show help");
|
||||
println!(" /status Show session status");
|
||||
println!(" /tools Show tool catalog and permission policy");
|
||||
println!(" /permissions Show permission mode details");
|
||||
println!(" /compact Compact session history");
|
||||
println!(" /exit Quit the REPL");
|
||||
}
|
||||
"/status" => cli.print_status(),
|
||||
"/tools" => cli.print_tools(),
|
||||
"/permissions" => cli.print_permissions(),
|
||||
"/compact" => cli.compact()?,
|
||||
_ => cli.run_turn(trimmed)?,
|
||||
}
|
||||
@@ -366,23 +371,27 @@ struct LiveCli {
|
||||
system_prompt: Vec<String>,
|
||||
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
||||
session_path: PathBuf,
|
||||
permission_policy: PermissionPolicy,
|
||||
}
|
||||
|
||||
impl LiveCli {
|
||||
fn new(model: String, enable_tools: bool) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let system_prompt = build_system_prompt()?;
|
||||
let session_path = new_session_path()?;
|
||||
let permission_policy = permission_policy_from_env();
|
||||
let runtime = build_runtime(
|
||||
Session::new(),
|
||||
model.clone(),
|
||||
system_prompt.clone(),
|
||||
enable_tools,
|
||||
permission_policy.clone(),
|
||||
)?;
|
||||
Ok(Self {
|
||||
model,
|
||||
system_prompt,
|
||||
runtime,
|
||||
session_path,
|
||||
permission_policy,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -394,7 +403,8 @@ impl LiveCli {
|
||||
TerminalRenderer::new().color_theme(),
|
||||
&mut stdout,
|
||||
)?;
|
||||
let result = self.runtime.run_turn(input, None);
|
||||
let mut permission_prompter = CliPermissionPrompter::new();
|
||||
let result = self.runtime.run_turn(input, Some(&mut permission_prompter));
|
||||
match result {
|
||||
Ok(turn) => {
|
||||
spinner.finish(
|
||||
@@ -443,6 +453,37 @@ impl LiveCli {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_permissions(&self) {
|
||||
let mode = env::var("RUSTY_CLAUDE_PERMISSION_MODE")
|
||||
.unwrap_or_else(|_| "workspace-write".to_string());
|
||||
println!("Permission mode: {mode}");
|
||||
println!(
|
||||
"Default policy: {}",
|
||||
permission_mode_label(self.permission_policy.mode_for("bash"))
|
||||
);
|
||||
println!("Read-only safe tools stay auto-allowed when read-only mode is active.");
|
||||
println!("Interactive approvals appear when permission mode is set to prompt.");
|
||||
}
|
||||
|
||||
fn print_tools(&self) {
|
||||
println!("Tool catalog:");
|
||||
for spec in mvp_tool_specs() {
|
||||
let mode = self.permission_policy.mode_for(spec.name);
|
||||
let summary = summarize_tool_schema(&spec.input_schema);
|
||||
println!(
|
||||
"- {} [{}] — {}{}",
|
||||
spec.name,
|
||||
permission_mode_label(mode),
|
||||
spec.description,
|
||||
if summary.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" | args: {summary}")
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn compact(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let estimated_before = self.runtime.estimated_tokens();
|
||||
let result = self.runtime.compact(CompactionConfig::default());
|
||||
@@ -456,6 +497,7 @@ impl LiveCli {
|
||||
self.model.clone(),
|
||||
self.system_prompt.clone(),
|
||||
true,
|
||||
self.permission_policy.clone(),
|
||||
)?;
|
||||
|
||||
if removed == 0 {
|
||||
@@ -628,13 +670,14 @@ fn build_runtime(
|
||||
model: String,
|
||||
system_prompt: Vec<String>,
|
||||
enable_tools: bool,
|
||||
permission_policy: PermissionPolicy,
|
||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||
{
|
||||
Ok(ConversationRuntime::new(
|
||||
session,
|
||||
AnthropicRuntimeClient::new(model, enable_tools)?,
|
||||
CliToolExecutor::new(),
|
||||
permission_policy_from_env(),
|
||||
permission_policy,
|
||||
system_prompt,
|
||||
))
|
||||
}
|
||||
@@ -819,6 +862,77 @@ fn response_to_events(
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
fn permission_mode_label(mode: PermissionMode) -> &'static str {
|
||||
match mode {
|
||||
PermissionMode::Allow => "allow",
|
||||
PermissionMode::Deny => "deny",
|
||||
PermissionMode::Prompt => "prompt",
|
||||
}
|
||||
}
|
||||
|
||||
fn summarize_tool_schema(schema: &serde_json::Value) -> String {
|
||||
let Some(properties) = schema
|
||||
.get("properties")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
else {
|
||||
return String::new();
|
||||
};
|
||||
let mut keys = properties.keys().cloned().collect::<Vec<_>>();
|
||||
keys.sort();
|
||||
keys.join(", ")
|
||||
}
|
||||
|
||||
fn summarize_tool_output(tool_name: &str, output: &str) -> String {
|
||||
let compact = output.replace('\n', " ");
|
||||
let preview = truncate_preview(compact.trim(), 120);
|
||||
if preview.is_empty() {
|
||||
format!("{tool_name} completed with no textual output")
|
||||
} else {
|
||||
format!("{tool_name} → {preview}")
|
||||
}
|
||||
}
|
||||
|
||||
struct CliPermissionPrompter {
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
impl CliPermissionPrompter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prompt: "Allow tool? [y]es / [n]o / [a]lways deny this run: ".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionPrompter for CliPermissionPrompter {
|
||||
fn decide(&mut self, request: &PermissionRequest) -> PermissionPromptDecision {
|
||||
println!(
|
||||
"
|
||||
Tool permission request:"
|
||||
);
|
||||
println!("- tool: {}", request.tool_name);
|
||||
println!("- input: {}", truncate_preview(request.input.trim(), 200));
|
||||
print!("{}", self.prompt);
|
||||
let _ = io::stdout().flush();
|
||||
|
||||
let mut response = String::new();
|
||||
match io::stdin().read_line(&mut response) {
|
||||
Ok(_) => match response.trim().to_ascii_lowercase().as_str() {
|
||||
"y" | "yes" => PermissionPromptDecision::Allow,
|
||||
"a" | "always" => PermissionPromptDecision::Deny {
|
||||
reason: "tool denied for this run by user".to_string(),
|
||||
},
|
||||
_ => PermissionPromptDecision::Deny {
|
||||
reason: "tool denied by user".to_string(),
|
||||
},
|
||||
},
|
||||
Err(error) => PermissionPromptDecision::Deny {
|
||||
reason: format!("tool approval failed: {error}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CliToolExecutor {
|
||||
renderer: TerminalRenderer,
|
||||
}
|
||||
@@ -837,7 +951,10 @@ impl ToolExecutor for CliToolExecutor {
|
||||
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
||||
match execute_tool(tool_name, &value) {
|
||||
Ok(output) => {
|
||||
let markdown = format!("### Tool `{tool_name}`\n\n```json\n{output}\n```\n");
|
||||
let summary = summarize_tool_output(tool_name, &output);
|
||||
let markdown = format!(
|
||||
"### Tool `{tool_name}`\n\n- Summary: {summary}\n\n```json\n{output}\n```\n"
|
||||
);
|
||||
self.renderer
|
||||
.stream_markdown(&markdown, &mut io::stdout())
|
||||
.map_err(|error| ToolError::new(error.to_string()))?;
|
||||
@@ -856,6 +973,10 @@ fn permission_policy_from_env() -> PermissionPolicy {
|
||||
.with_tool_mode("read_file", PermissionMode::Allow)
|
||||
.with_tool_mode("glob_search", PermissionMode::Allow)
|
||||
.with_tool_mode("grep_search", PermissionMode::Allow),
|
||||
"prompt" => PermissionPolicy::new(PermissionMode::Prompt)
|
||||
.with_tool_mode("read_file", PermissionMode::Allow)
|
||||
.with_tool_mode("glob_search", PermissionMode::Allow)
|
||||
.with_tool_mode("grep_search", PermissionMode::Allow),
|
||||
_ => PermissionPolicy::new(PermissionMode::Allow),
|
||||
}
|
||||
}
|
||||
@@ -913,6 +1034,7 @@ fn print_help() {
|
||||
println!(" rusty-claude-cli bootstrap-plan");
|
||||
println!(" rusty-claude-cli sessions [--query TEXT] [--limit N]");
|
||||
println!(" rusty-claude-cli resume <latest|SESSION|PATH> [/compact]");
|
||||
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 --resume SESSION.json [/compact]");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user