mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
feat: b5-git-aware — batch 5 upstream parity
This commit is contained in:
@@ -86,6 +86,7 @@ const CLI_OPTION_SUGGESTIONS: &[&str] = &[
|
||||
"--allowed-tools",
|
||||
"--resume",
|
||||
"--print",
|
||||
"--compact",
|
||||
"-p",
|
||||
];
|
||||
|
||||
@@ -156,8 +157,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
output_format,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
compact,
|
||||
} => LiveCli::new(model, true, allowed_tools, permission_mode)?
|
||||
.run_turn_with_output(&prompt, output_format)?,
|
||||
.run_turn_with_output(&prompt, output_format, compact)?,
|
||||
CliAction::Login { output_format } => run_login(output_format)?,
|
||||
CliAction::Logout { output_format } => run_logout(output_format)?,
|
||||
CliAction::Doctor { output_format } => run_doctor(output_format)?,
|
||||
@@ -225,6 +227,7 @@ enum CliAction {
|
||||
output_format: CliOutputFormat,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
compact: bool,
|
||||
},
|
||||
Login {
|
||||
output_format: CliOutputFormat,
|
||||
@@ -283,6 +286,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let mut wants_help = false;
|
||||
let mut wants_version = false;
|
||||
let mut allowed_tool_values = Vec::new();
|
||||
let mut compact = false;
|
||||
let mut rest = Vec::new();
|
||||
let mut index = 0;
|
||||
|
||||
@@ -333,6 +337,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
permission_mode_override = Some(PermissionMode::DangerFullAccess);
|
||||
index += 1;
|
||||
}
|
||||
"--compact" => {
|
||||
compact = true;
|
||||
index += 1;
|
||||
}
|
||||
"-p" => {
|
||||
// Claw Code compat: -p "prompt" = one-shot prompt
|
||||
let prompt = args[index + 1..].join(" ");
|
||||
@@ -346,6 +354,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
|
||||
permission_mode: permission_mode_override
|
||||
.unwrap_or_else(default_permission_mode),
|
||||
compact,
|
||||
});
|
||||
}
|
||||
"--print" => {
|
||||
@@ -439,6 +448,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
output_format,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
compact,
|
||||
}),
|
||||
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||
args,
|
||||
@@ -461,6 +471,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
output_format,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
compact,
|
||||
})
|
||||
}
|
||||
other if other.starts_with('/') => parse_direct_slash_cli_action(
|
||||
@@ -469,6 +480,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
output_format,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
compact,
|
||||
),
|
||||
_other => Ok(CliAction::Prompt {
|
||||
prompt: rest.join(" "),
|
||||
@@ -476,6 +488,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
output_format,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
compact,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -565,6 +578,7 @@ fn parse_direct_slash_cli_action(
|
||||
output_format: CliOutputFormat,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
compact: bool,
|
||||
) -> Result<CliAction, String> {
|
||||
let raw = rest.join(" ");
|
||||
match SlashCommand::parse(&raw) {
|
||||
@@ -590,6 +604,7 @@ fn parse_direct_slash_cli_action(
|
||||
output_format,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
compact,
|
||||
}),
|
||||
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||
args,
|
||||
@@ -3204,13 +3219,28 @@ impl LiveCli {
|
||||
&mut self,
|
||||
input: &str,
|
||||
output_format: CliOutputFormat,
|
||||
compact: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match output_format {
|
||||
CliOutputFormat::Text if compact => self.run_prompt_compact(input),
|
||||
CliOutputFormat::Text => self.run_turn(input),
|
||||
CliOutputFormat::Json => self.run_prompt_json(input),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_prompt_compact(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(false)?;
|
||||
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
||||
hook_abort_monitor.stop();
|
||||
let summary = result?;
|
||||
self.replace_runtime(runtime)?;
|
||||
self.persist_session()?;
|
||||
let final_text = final_assistant_text(&summary);
|
||||
println!("{final_text}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_prompt_json(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(false)?;
|
||||
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||
@@ -7007,6 +7037,10 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
out,
|
||||
" --output-format FORMAT Non-interactive output format: text or json"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --compact Strip tool call details; print only the final assistant text (text mode only; useful for piping)"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
|
||||
@@ -7053,6 +7087,10 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
out,
|
||||
" claw --output-format json prompt \"explain src/main.rs\""
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw --compact \"summarize Cargo.toml\" | wc -l"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw --allowedTools read,glob \"summarize Cargo.toml\""
|
||||
@@ -7595,6 +7633,7 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
allowed_tools: None,
|
||||
permission_mode: PermissionMode::DangerFullAccess,
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -7618,10 +7657,56 @@ mod tests {
|
||||
output_format: CliOutputFormat::Json,
|
||||
allowed_tools: None,
|
||||
permission_mode: PermissionMode::DangerFullAccess,
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_compact_flag_for_prompt_mode() {
|
||||
// given a bare prompt invocation that includes the --compact flag
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
let args = vec![
|
||||
"--compact".to_string(),
|
||||
"summarize".to_string(),
|
||||
"this".to_string(),
|
||||
];
|
||||
|
||||
// when parse_args interprets the flag
|
||||
let parsed = parse_args(&args).expect("args should parse");
|
||||
|
||||
// then compact mode is propagated and other defaults stay unchanged
|
||||
assert_eq!(
|
||||
parsed,
|
||||
CliAction::Prompt {
|
||||
prompt: "summarize this".to_string(),
|
||||
model: DEFAULT_MODEL.to_string(),
|
||||
output_format: CliOutputFormat::Text,
|
||||
allowed_tools: None,
|
||||
permission_mode: PermissionMode::DangerFullAccess,
|
||||
compact: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_subcommand_defaults_compact_to_false() {
|
||||
// given a `prompt` subcommand invocation without --compact
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
let args = vec!["prompt".to_string(), "hello".to_string()];
|
||||
|
||||
// when parse_args runs
|
||||
let parsed = parse_args(&args).expect("args should parse");
|
||||
|
||||
// then compact stays false (opt-in flag)
|
||||
match parsed {
|
||||
CliAction::Prompt { compact, .. } => assert!(!compact),
|
||||
other => panic!("expected Prompt action, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_model_aliases_in_args() {
|
||||
let _guard = env_lock();
|
||||
@@ -7640,6 +7725,7 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
allowed_tools: None,
|
||||
permission_mode: PermissionMode::DangerFullAccess,
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -7791,6 +7877,7 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
allowed_tools: None,
|
||||
permission_mode: crate::default_permission_mode(),
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -7898,6 +7985,7 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
allowed_tools: None,
|
||||
permission_mode: PermissionMode::DangerFullAccess,
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -7962,6 +8050,7 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
allowed_tools: None,
|
||||
permission_mode: crate::default_permission_mode(),
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -7985,6 +8074,7 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
allowed_tools: None,
|
||||
permission_mode: crate::default_permission_mode(),
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
let error = parse_args(&["/status".to_string()])
|
||||
|
||||
159
rust/crates/rusty-claude-cli/tests/compact_output.rs
Normal file
159
rust/crates/rusty-claude-cli/tests/compact_output.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Output};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use mock_anthropic_service::{MockAnthropicService, SCENARIO_PREFIX};
|
||||
|
||||
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[test]
|
||||
fn compact_flag_prints_only_final_assistant_text_without_tool_call_details() {
|
||||
// given a workspace pointed at the mock Anthropic service and a fixture file
|
||||
// that the read_file_roundtrip scenario will fetch through a tool call
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
let base_url = server.base_url();
|
||||
|
||||
let workspace = unique_temp_dir("compact-read-file");
|
||||
let config_home = workspace.join("config-home");
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
fs::write(workspace.join("fixture.txt"), "alpha parity line\n").expect("fixture should write");
|
||||
|
||||
// when we run claw in compact text mode against a tool-using scenario
|
||||
let prompt = format!("{SCENARIO_PREFIX}read_file_roundtrip");
|
||||
let output = run_claw(
|
||||
&workspace,
|
||||
&config_home,
|
||||
&home,
|
||||
&base_url,
|
||||
&[
|
||||
"--model",
|
||||
"sonnet",
|
||||
"--permission-mode",
|
||||
"read-only",
|
||||
"--allowedTools",
|
||||
"read_file",
|
||||
"--compact",
|
||||
&prompt,
|
||||
],
|
||||
);
|
||||
|
||||
// then the command exits successfully and stdout contains exactly the final
|
||||
// assistant text with no tool call IDs, JSON envelopes, or spinner output
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"compact run should succeed\nstdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||
let trimmed = stdout.trim_end_matches('\n');
|
||||
assert_eq!(
|
||||
trimmed, "read_file roundtrip complete: alpha parity line",
|
||||
"compact stdout should contain only the final assistant text"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("toolu_"),
|
||||
"compact stdout must not leak tool_use_id ({stdout:?})"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("\"tool_uses\""),
|
||||
"compact stdout must not leak json envelopes ({stdout:?})"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("Thinking"),
|
||||
"compact stdout must not include the spinner banner ({stdout:?})"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_flag_streaming_text_only_emits_final_message_text() {
|
||||
// given a workspace pointed at the mock Anthropic service running the
|
||||
// streaming_text scenario which only emits a single assistant text block
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
let base_url = server.base_url();
|
||||
|
||||
let workspace = unique_temp_dir("compact-streaming-text");
|
||||
let config_home = workspace.join("config-home");
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
|
||||
// when we invoke claw with --compact for the streaming text scenario
|
||||
let prompt = format!("{SCENARIO_PREFIX}streaming_text");
|
||||
let output = run_claw(
|
||||
&workspace,
|
||||
&config_home,
|
||||
&home,
|
||||
&base_url,
|
||||
&[
|
||||
"--model",
|
||||
"sonnet",
|
||||
"--permission-mode",
|
||||
"read-only",
|
||||
"--compact",
|
||||
&prompt,
|
||||
],
|
||||
);
|
||||
|
||||
// then stdout should be exactly the assistant text followed by a newline
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"compact streaming run should succeed\nstdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||
assert_eq!(
|
||||
stdout, "Mock streaming says hello from the parity harness.\n",
|
||||
"compact streaming stdout should contain only the final assistant text"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
|
||||
}
|
||||
|
||||
fn run_claw(
|
||||
cwd: &std::path::Path,
|
||||
config_home: &std::path::Path,
|
||||
home: &std::path::Path,
|
||||
base_url: &str,
|
||||
args: &[&str],
|
||||
) -> Output {
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
|
||||
command
|
||||
.current_dir(cwd)
|
||||
.env_clear()
|
||||
.env("ANTHROPIC_API_KEY", "test-compact-key")
|
||||
.env("ANTHROPIC_BASE_URL", base_url)
|
||||
.env("CLAW_CONFIG_HOME", config_home)
|
||||
.env("HOME", home)
|
||||
.env("NO_COLOR", "1")
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.args(args);
|
||||
command.output().expect("claw should launch")
|
||||
}
|
||||
|
||||
fn unique_temp_dir(label: &str) -> PathBuf {
|
||||
let millis = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("clock should be after epoch")
|
||||
.as_millis();
|
||||
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
std::env::temp_dir().join(format!(
|
||||
"claw-compact-{label}-{}-{millis}-{counter}",
|
||||
std::process::id()
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user