Replace bespoke CLI line editing with rustyline and canonical model aliases

The REPL now wraps rustyline::Editor instead of maintaining a custom raw-mode
input stack. This preserves the existing LineEditor surface while delegating
history, completion, and interactive editing to a maintained library. The CLI
argument parser and /model command path also normalize shorthand model names to
our current canonical Anthropic identifiers.

Constraint: User requested rustyline 15 specifically for the CLI editor rewrite
Constraint: Existing LineEditor constructor and read_line API had to remain stable
Rejected: Keep extending the crossterm-based editor | custom key handling and history logic were redundant with rustyline
Rejected: Resolve aliases only for --model flags | /model would still diverge from CLI startup behavior
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep model alias normalization centralized in main.rs so CLI flag parsing and /model stay in sync
Tested: cargo check --workspace
Tested: cargo test --workspace
Tested: cargo build --workspace
Tested: cargo clippy --workspace --all-targets -- -D warnings
Not-tested: Interactive manual terminal validation of Shift+Enter behavior across terminal emulators
This commit is contained in:
Yeachan-Heo
2026-04-01 02:04:12 +00:00
parent 9a86aa6444
commit e2753f055a
5 changed files with 374 additions and 581 deletions

View File

@@ -157,11 +157,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let value = args
.get(index + 1)
.ok_or_else(|| "missing value for --model".to_string())?;
model.clone_from(value);
model = resolve_model_alias(value).to_string();
index += 2;
}
flag if flag.starts_with("--model=") => {
model = flag[8..].to_string();
model = resolve_model_alias(&flag[8..]).to_string();
index += 1;
}
"--output-format" => {
@@ -259,6 +259,15 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
}
fn resolve_model_alias(model: &str) -> &str {
match model {
"opus" => "claude-opus-4-6",
"sonnet" => "claude-sonnet-4-6",
"haiku" => "claude-haiku-3-5-20241022",
_ => model,
}
}
fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
if values.is_empty() {
return Ok(None);
@@ -1033,7 +1042,8 @@ impl LiveCli {
}
fn run_prompt_json(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = AnthropicClient::from_auth(resolve_cli_auth_source()?).with_base_url(api::read_base_url());
let client = AnthropicClient::from_auth(resolve_cli_auth_source()?)
.with_base_url(api::read_base_url());
let request = MessageRequest {
model: self.model.clone(),
max_tokens: DEFAULT_MAX_TOKENS,
@@ -1172,6 +1182,8 @@ impl LiveCli {
return Ok(false);
};
let model = resolve_model_alias(&model).to_string();
if model == self.model {
println!(
"{}",
@@ -1934,7 +1946,8 @@ impl AnthropicRuntimeClient {
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
runtime: tokio::runtime::Runtime::new()?,
client: AnthropicClient::from_auth(resolve_cli_auth_source()?).with_base_url(api::read_base_url()),
client: AnthropicClient::from_auth(resolve_cli_auth_source()?)
.with_base_url(api::read_base_url()),
model,
enable_tools,
allowed_tools,
@@ -2307,10 +2320,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
)?;
writeln!(out, " claw dump-manifests")?;
writeln!(out, " claw bootstrap-plan")?;
writeln!(
out,
" claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]"
)?;
writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
writeln!(out, " claw login")?;
writeln!(out, " claw logout")?;
writeln!(out, " claw init")?;
@@ -2347,10 +2357,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
.join(", ");
writeln!(out, "Resume-safe commands: {resume_commands}")?;
writeln!(out, "Examples:")?;
writeln!(
out,
" claw --model claude-opus \"summarize this repo\""
)?;
writeln!(out, " claw --model claude-opus \"summarize this repo\"")?;
writeln!(
out,
" claw --output-format json prompt \"explain src/main.rs\""
@@ -2379,7 +2386,7 @@ mod tests {
format_model_switch_report, format_permissions_report, format_permissions_switch_report,
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
normalize_permission_mode, parse_args, parse_git_status_metadata, print_help_to,
render_config_report, render_memory_report, render_repl_help,
render_config_report, render_memory_report, render_repl_help, resolve_model_alias,
resume_supported_slash_commands, status_context, CliAction, CliOutputFormat, SlashCommand,
StatusUsage, DEFAULT_MODEL,
};
@@ -2438,6 +2445,34 @@ mod tests {
);
}
#[test]
fn resolves_model_aliases_in_args() {
let args = vec![
"--model".to_string(),
"opus".to_string(),
"explain".to_string(),
"this".to_string(),
];
assert_eq!(
parse_args(&args).expect("args should parse"),
CliAction::Prompt {
prompt: "explain this".to_string(),
model: "claude-opus-4-6".to_string(),
output_format: CliOutputFormat::Text,
allowed_tools: None,
permission_mode: PermissionMode::WorkspaceWrite,
}
);
}
#[test]
fn resolves_known_model_aliases() {
assert_eq!(resolve_model_alias("opus"), "claude-opus-4-6");
assert_eq!(resolve_model_alias("sonnet"), "claude-sonnet-4-6");
assert_eq!(resolve_model_alias("haiku"), "claude-haiku-3-5-20241022");
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
}
#[test]
fn parses_version_flags_without_initializing_prompt_mode() {
assert_eq!(