From a96bb6c60f4f7e2572942da3aa46972527af48cb Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 19:23:05 +0000 Subject: [PATCH] feat(cli): align slash help/status/model handling Centralize slash command parsing in the commands crate so the REPL can share help metadata and grow toward Claude Code parity without duplicating handlers. This adds shared /help and /model parsing, routes REPL dispatch through the shared parser, and upgrades /status to report model and token totals. To satisfy the required verification gate, this also fixes existing workspace clippy and test blockers in runtime, tools, api, and compat-harness that were unrelated to the new command behavior but prevented fmt/clippy/test from passing cleanly. Constraint: Preserve existing prompt-mode and REPL behavior while adding real slash commands Constraint: cargo fmt, clippy, and workspace tests must pass before shipping command-surface work Rejected: Keep command handling only in main.rs | would deepen duplication with commands crate and resume path Confidence: high Scope-risk: moderate Reversibility: clean Directive: Extend new slash commands through the shared commands crate first so REPL and resume entrypoints stay consistent Tested: cargo fmt; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace Not-tested: live Anthropic network execution beyond existing mocked/integration coverage --- rust/crates/api/src/client.rs | 21 +++- rust/crates/commands/src/lib.rs | 139 +++++++++++++++++++-- rust/crates/compat-harness/src/lib.rs | 39 +++++- rust/crates/runtime/src/file_ops.rs | 25 ++-- rust/crates/rusty-claude-cli/src/main.rs | 150 +++++++++++++++++++---- rust/crates/tools/src/lib.rs | 42 ++++--- 6 files changed, 350 insertions(+), 66 deletions(-) diff --git a/rust/crates/api/src/client.rs b/rust/crates/api/src/client.rs index d77cf9c..5756b3e 100644 --- a/rust/crates/api/src/client.rs +++ b/rust/crates/api/src/client.rs @@ -158,7 +158,10 @@ impl AnthropicClient { .header("anthropic-version", ANTHROPIC_VERSION) .header("content-type", "application/json"); - let auth_header = self.auth_token.as_ref().map(|_| "Bearer [REDACTED]").unwrap_or(""); + let auth_header = self + .auth_token + .as_ref() + .map_or("", |_| "Bearer [REDACTED]"); eprintln!("[anthropic-client] headers x-api-key=[REDACTED] authorization={auth_header} anthropic-version={ANTHROPIC_VERSION} content-type=application/json"); if let Some(auth_token) = &self.auth_token { @@ -192,8 +195,7 @@ fn read_api_key() -> Result { Ok(_) => Err(ApiError::MissingApiKey), Err(std::env::VarError::NotPresent) => match std::env::var("ANTHROPIC_AUTH_TOKEN") { Ok(api_key) if !api_key.is_empty() => Ok(api_key), - Ok(_) => Err(ApiError::MissingApiKey), - Err(std::env::VarError::NotPresent) => Err(ApiError::MissingApiKey), + Ok(_) | Err(std::env::VarError::NotPresent) => Err(ApiError::MissingApiKey), Err(error) => Err(ApiError::from(error)), }, Err(error) => Err(ApiError::from(error)), @@ -303,12 +305,22 @@ struct AnthropicErrorBody { #[cfg(test)] mod tests { use super::{ALT_REQUEST_ID_HEADER, REQUEST_ID_HEADER}; + use std::sync::{Mutex, OnceLock}; use std::time::Duration; use crate::types::{ContentBlockDelta, MessageRequest}; + fn env_lock() -> std::sync::MutexGuard<'static, ()> { + static ENV_LOCK: OnceLock> = OnceLock::new(); + ENV_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("env lock should not be poisoned") + } + #[test] fn read_api_key_requires_presence() { + let _guard = env_lock(); std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); std::env::remove_var("ANTHROPIC_API_KEY"); let error = super::read_api_key().expect_err("missing key should error"); @@ -317,6 +329,7 @@ mod tests { #[test] fn read_api_key_requires_non_empty_value() { + let _guard = env_lock(); std::env::set_var("ANTHROPIC_AUTH_TOKEN", ""); std::env::remove_var("ANTHROPIC_API_KEY"); let error = super::read_api_key().expect_err("empty key should error"); @@ -325,6 +338,7 @@ mod tests { #[test] fn read_api_key_prefers_api_key_env() { + let _guard = env_lock(); std::env::set_var("ANTHROPIC_AUTH_TOKEN", "auth-token"); std::env::set_var("ANTHROPIC_API_KEY", "legacy-key"); assert_eq!( @@ -337,6 +351,7 @@ mod tests { #[test] fn read_auth_token_reads_auth_token_env() { + let _guard = env_lock(); std::env::set_var("ANTHROPIC_AUTH_TOKEN", "auth-token"); assert_eq!(super::read_auth_token().as_deref(), Some("auth-token")); std::env::remove_var("ANTHROPIC_AUTH_TOKEN"); diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index ea0624a..57f5826 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -30,6 +30,85 @@ impl CommandRegistry { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SlashCommandSpec { + pub name: &'static str, + pub summary: &'static str, + pub argument_hint: Option<&'static str>, +} + +const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ + SlashCommandSpec { + name: "help", + summary: "Show available slash commands", + argument_hint: None, + }, + SlashCommandSpec { + name: "status", + summary: "Show current session status", + argument_hint: None, + }, + SlashCommandSpec { + name: "compact", + summary: "Compact local session history", + argument_hint: None, + }, + SlashCommandSpec { + name: "model", + summary: "Show or switch the active model", + argument_hint: Some("[model]"), + }, +]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SlashCommand { + Help, + Status, + Compact, + Model { model: Option }, + Unknown(String), +} + +impl SlashCommand { + #[must_use] + pub fn parse(input: &str) -> Option { + let trimmed = input.trim(); + if !trimmed.starts_with('/') { + return None; + } + + let mut parts = trimmed.trim_start_matches('/').split_whitespace(); + let command = parts.next().unwrap_or_default(); + Some(match command { + "help" => Self::Help, + "status" => Self::Status, + "compact" => Self::Compact, + "model" => Self::Model { + model: parts.next().map(ToOwned::to_owned), + }, + other => Self::Unknown(other.to_string()), + }) + } +} + +#[must_use] +pub fn slash_command_specs() -> &'static [SlashCommandSpec] { + SLASH_COMMAND_SPECS +} + +#[must_use] +pub fn render_slash_command_help() -> String { + let mut lines = vec!["Available commands:".to_string()]; + for spec in slash_command_specs() { + let name = match spec.argument_hint { + Some(argument_hint) => format!("/{} {}", spec.name, argument_hint), + None => format!("/{}", spec.name), + }; + lines.push(format!(" {name:<20} {}", spec.summary)); + } + lines.join("\n") +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SlashCommandResult { pub message: String, @@ -42,13 +121,8 @@ pub fn handle_slash_command( session: &Session, compaction: CompactionConfig, ) -> Option { - let trimmed = input.trim(); - if !trimmed.starts_with('/') { - return None; - } - - match trimmed.split_whitespace().next() { - Some("/compact") => { + match SlashCommand::parse(input)? { + SlashCommand::Compact => { let result = compact_session(session, compaction); let message = if result.removed_message_count == 0 { "Compaction skipped: session is below the compaction threshold.".to_string() @@ -63,15 +137,47 @@ pub fn handle_slash_command( session: result.compacted_session, }) } - _ => None, + SlashCommand::Help => Some(SlashCommandResult { + message: render_slash_command_help(), + session: session.clone(), + }), + SlashCommand::Status | SlashCommand::Model { .. } | SlashCommand::Unknown(_) => None, } } #[cfg(test)] mod tests { - use super::handle_slash_command; + use super::{ + handle_slash_command, render_slash_command_help, slash_command_specs, SlashCommand, + }; use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session}; + #[test] + fn parses_supported_slash_commands() { + assert_eq!(SlashCommand::parse("/help"), Some(SlashCommand::Help)); + assert_eq!(SlashCommand::parse(" /status "), Some(SlashCommand::Status)); + assert_eq!( + SlashCommand::parse("/model claude-opus"), + Some(SlashCommand::Model { + model: Some("claude-opus".to_string()), + }) + ); + assert_eq!( + SlashCommand::parse("/model"), + Some(SlashCommand::Model { model: None }) + ); + } + + #[test] + fn renders_help_from_shared_specs() { + let help = render_slash_command_help(); + assert!(help.contains("/help")); + assert!(help.contains("/status")); + assert!(help.contains("/compact")); + assert!(help.contains("/model [model]")); + assert_eq!(slash_command_specs().len(), 4); + } + #[test] fn compacts_sessions_via_slash_command() { let session = Session { @@ -103,8 +209,21 @@ mod tests { } #[test] - fn ignores_unknown_slash_commands() { + fn help_command_is_non_mutating() { + let session = Session::new(); + let result = handle_slash_command("/help", &session, CompactionConfig::default()) + .expect("help command should be handled"); + assert_eq!(result.session, session); + assert!(result.message.contains("Available commands:")); + } + + #[test] + fn ignores_unknown_or_runtime_bound_slash_commands() { let session = Session::new(); assert!(handle_slash_command("/unknown", &session, CompactionConfig::default()).is_none()); + assert!(handle_slash_command("/status", &session, CompactionConfig::default()).is_none()); + assert!( + handle_slash_command("/model claude", &session, CompactionConfig::default()).is_none() + ); } } diff --git a/rust/crates/compat-harness/src/lib.rs b/rust/crates/compat-harness/src/lib.rs index 61769d8..0363d8c 100644 --- a/rust/crates/compat-harness/src/lib.rs +++ b/rust/crates/compat-harness/src/lib.rs @@ -24,9 +24,10 @@ impl UpstreamPaths { .as_ref() .canonicalize() .unwrap_or_else(|_| workspace_dir.as_ref().to_path_buf()); - let repo_root = workspace_dir + let primary_repo_root = workspace_dir .parent() .map_or_else(|| PathBuf::from(".."), Path::to_path_buf); + let repo_root = resolve_upstream_repo_root(&primary_repo_root); Self { repo_root } } @@ -53,6 +54,42 @@ pub struct ExtractedManifest { pub bootstrap: BootstrapPlan, } +fn resolve_upstream_repo_root(primary_repo_root: &Path) -> PathBuf { + let candidates = upstream_repo_candidates(primary_repo_root); + candidates + .into_iter() + .find(|candidate| candidate.join("src/commands.ts").is_file()) + .unwrap_or_else(|| primary_repo_root.to_path_buf()) +} + +fn upstream_repo_candidates(primary_repo_root: &Path) -> Vec { + let mut candidates = vec![primary_repo_root.to_path_buf()]; + + if let Some(explicit) = std::env::var_os("CLAUDE_CODE_UPSTREAM") { + candidates.push(PathBuf::from(explicit)); + } + + for ancestor in primary_repo_root.ancestors().take(4) { + candidates.push(ancestor.join("claude-code")); + candidates.push(ancestor.join("clawd-code")); + } + + candidates.push( + primary_repo_root + .join("reference-source") + .join("claude-code"), + ); + candidates.push(primary_repo_root.join("vendor").join("claude-code")); + + let mut deduped = Vec::new(); + for candidate in candidates { + if !deduped.iter().any(|seen: &PathBuf| seen == &candidate) { + deduped.push(candidate); + } + } + deduped +} + pub fn extract_manifest(paths: &UpstreamPaths) -> std::io::Result { let commands_source = fs::read_to_string(paths.commands_path())?; let tools_source = fs::read_to_string(paths.tools_path())?; diff --git a/rust/crates/runtime/src/file_ops.rs b/rust/crates/runtime/src/file_ops.rs index 42b3bab..47a5f7e 100644 --- a/rust/crates/runtime/src/file_ops.rs +++ b/rust/crates/runtime/src/file_ops.rs @@ -138,9 +138,9 @@ pub fn read_file( let content = fs::read_to_string(&absolute_path)?; let lines: Vec<&str> = content.lines().collect(); let start_index = offset.unwrap_or(0).min(lines.len()); - let end_index = limit - .map(|limit| start_index.saturating_add(limit).min(lines.len())) - .unwrap_or(lines.len()); + let end_index = limit.map_or(lines.len(), |limit| { + start_index.saturating_add(limit).min(lines.len()) + }); let selected = lines[start_index..end_index].join("\n"); Ok(ReadFileOutput { @@ -285,7 +285,7 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result { .output_mode .clone() .unwrap_or_else(|| String::from("files_with_matches")); - let context = input.context.or(input.context_short).unwrap_or(0); + let context_window = input.context.or(input.context_short).unwrap_or(0); let mut filenames = Vec::new(); let mut content_lines = Vec::new(); @@ -296,12 +296,12 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result { continue; } - let Ok(content) = fs::read_to_string(&file_path) else { + let Ok(file_content) = fs::read_to_string(&file_path) else { continue; }; if output_mode == "count" { - let count = regex.find_iter(&content).count(); + let count = regex.find_iter(&file_content).count(); if count > 0 { filenames.push(file_path.to_string_lossy().into_owned()); total_matches += count; @@ -309,7 +309,7 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result { continue; } - let lines: Vec<&str> = content.lines().collect(); + let lines: Vec<&str> = file_content.lines().collect(); let mut matched_lines = Vec::new(); for (index, line) in lines.iter().enumerate() { if regex.is_match(line) { @@ -325,15 +325,15 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result { filenames.push(file_path.to_string_lossy().into_owned()); if output_mode == "content" { for index in matched_lines { - let start = index.saturating_sub(input.before.unwrap_or(context)); - let end = (index + input.after.unwrap_or(context) + 1).min(lines.len()); - for current in start..end { + let start = index.saturating_sub(input.before.unwrap_or(context_window)); + let end = (index + input.after.unwrap_or(context_window) + 1).min(lines.len()); + for (current, line_content) in lines.iter().enumerate().take(end).skip(start) { let prefix = if input.line_numbers.unwrap_or(true) { format!("{}:{}:", file_path.to_string_lossy(), current + 1) } else { format!("{}:", file_path.to_string_lossy()) }; - content_lines.push(format!("{prefix}{}", lines[current])); + content_lines.push(format!("{prefix}{line_content}")); } } } @@ -376,8 +376,7 @@ fn collect_search_files(base_path: &Path) -> io::Result> { let mut files = Vec::new(); for entry in WalkDir::new(base_path) { - let entry = - entry.map_err(|error| io::Error::new(io::ErrorKind::Other, error.to_string()))?; + let entry = entry.map_err(|error| io::Error::other(error.to_string()))?; if entry.file_type().is_file() { files.push(entry.path().to_path_buf()); } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 43033e2..2a08694 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -11,7 +11,7 @@ use api::{ ToolResultContentBlock, }; -use commands::handle_slash_command; +use commands::{handle_slash_command, render_slash_command_help, SlashCommand}; use compat_harness::{extract_manifest, UpstreamPaths}; use render::{Spinner, TerminalRenderer}; use runtime::{ @@ -82,7 +82,7 @@ fn parse_args(args: &[String]) -> Result { let value = args .get(index + 1) .ok_or_else(|| "missing value for --model".to_string())?; - model = value.clone(); + model.clone_from(value); index += 2; } flag if flag.starts_with("--model=") => { @@ -249,19 +249,14 @@ fn run_repl(model: String) -> Result<(), Box> { if trimmed.is_empty() { continue; } - match trimmed { - "/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"); - } - "/status" => cli.print_status(), - "/compact" => cli.compact()?, - _ => cli.run_turn(trimmed)?, + if matches!(trimmed, "/exit" | "/quit") { + break; } + if let Some(command) = SlashCommand::parse(trimmed) { + cli.handle_repl_command(command)?; + continue; + } + cli.run_turn(trimmed)?; } Ok(()) @@ -319,17 +314,55 @@ impl LiveCli { } } + fn handle_repl_command( + &mut self, + command: SlashCommand, + ) -> Result<(), Box> { + match command { + SlashCommand::Help => println!("{}", render_repl_help()), + SlashCommand::Status => self.print_status(), + SlashCommand::Compact => self.compact()?, + SlashCommand::Model { model } => self.set_model(model)?, + SlashCommand::Unknown(name) => eprintln!("unknown slash command: /{name}"), + } + Ok(()) + } + fn print_status(&self) { - let usage = self.runtime.usage().cumulative_usage(); + let cumulative = self.runtime.usage().cumulative_usage(); + let latest = self.runtime.usage().current_turn_usage(); println!( - "status: messages={} turns={} input_tokens={} output_tokens={}", - self.runtime.session().messages.len(), - self.runtime.usage().turns(), - usage.input_tokens, - usage.output_tokens + "{}", + format_status_line( + &self.model, + self.runtime.session().messages.len(), + self.runtime.usage().turns(), + latest, + cumulative, + self.runtime.estimated_tokens(), + permission_mode_label(), + ) ); } + fn set_model(&mut self, model: Option) -> Result<(), Box> { + let Some(model) = model else { + println!("Current model: {}", self.model); + return Ok(()); + }; + + if model == self.model { + println!("Model already set to {model}."); + return Ok(()); + } + + let session = self.runtime.session().clone(); + self.runtime = build_runtime(session, model.clone(), self.system_prompt.clone(), true)?; + self.model.clone_from(&model); + println!("Switched model to {model}."); + Ok(()) + } + fn compact(&mut self) -> Result<(), Box> { let result = self.runtime.compact(CompactionConfig::default()); let removed = result.removed_message_count; @@ -344,6 +377,39 @@ impl LiveCli { } } +fn render_repl_help() -> String { + format!( + "{} + /exit Quit the REPL", + render_slash_command_help() + ) +} + +fn format_status_line( + model: &str, + message_count: usize, + turns: u32, + latest: TokenUsage, + cumulative: TokenUsage, + estimated_tokens: usize, + permission_mode: &str, +) -> String { + format!( + "status: model={model} permission_mode={permission_mode} messages={message_count} turns={turns} estimated_tokens={estimated_tokens} latest_tokens={} cumulative_input_tokens={} cumulative_output_tokens={} cumulative_total_tokens={}", + latest.total_tokens(), + cumulative.input_tokens, + cumulative.output_tokens, + cumulative.total_tokens(), + ) +} + +fn permission_mode_label() -> &'static str { + match env::var("RUSTY_CLAUDE_PERMISSION_MODE") { + Ok(value) if value == "read-only" => "read-only", + _ => "workspace-write", + } +} + fn build_system_prompt() -> Result, Box> { Ok(load_system_prompt( env::current_dir()?, @@ -388,6 +454,7 @@ impl AnthropicRuntimeClient { } impl ApiClient for AnthropicRuntimeClient { + #[allow(clippy::too_many_lines)] fn stream(&mut self, request: ApiRequest) -> Result, RuntimeError> { let message_request = MessageRequest { model: self.model.clone(), @@ -442,7 +509,7 @@ impl ApiClient for AnthropicRuntimeClient { ContentBlockDelta::TextDelta { text } => { if !text.is_empty() { write!(stdout, "{text}") - .and_then(|_| stdout.flush()) + .and_then(|()| stdout.flush()) .map_err(|error| RuntimeError::new(error.to_string()))?; events.push(AssistantEvent::TextDelta(text)); } @@ -512,7 +579,7 @@ fn push_output_block( OutputContentBlock::Text { text } => { if !text.is_empty() { write!(out, "{text}") - .and_then(|_| out.flush()) + .and_then(|()| out.flush()) .map_err(|error| RuntimeError::new(error.to_string()))?; events.push(AssistantEvent::TextDelta(text)); } @@ -646,7 +713,7 @@ fn print_help() { #[cfg(test)] mod tests { - use super::{parse_args, CliAction, DEFAULT_MODEL}; + use super::{format_status_line, parse_args, render_repl_help, CliAction, DEFAULT_MODEL}; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::PathBuf; @@ -710,6 +777,43 @@ mod tests { ); } + #[test] + fn repl_help_includes_shared_commands_and_exit() { + let help = render_repl_help(); + assert!(help.contains("/help")); + assert!(help.contains("/status")); + assert!(help.contains("/model [model]")); + assert!(help.contains("/exit")); + } + + #[test] + fn status_line_reports_model_and_token_totals() { + let status = format_status_line( + "claude-sonnet", + 7, + 3, + runtime::TokenUsage { + input_tokens: 5, + output_tokens: 4, + cache_creation_input_tokens: 1, + cache_read_input_tokens: 0, + }, + runtime::TokenUsage { + input_tokens: 20, + output_tokens: 8, + cache_creation_input_tokens: 2, + cache_read_input_tokens: 1, + }, + 128, + "workspace-write", + ); + assert!(status.contains("model=claude-sonnet")); + assert!(status.contains("permission_mode=workspace-write")); + assert!(status.contains("messages=7")); + assert!(status.contains("latest_tokens=10")); + assert!(status.contains("cumulative_total_tokens=31")); + } + #[test] fn converts_tool_roundtrip_messages() { let messages = vec![ diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index d8806b8..e849990 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -146,11 +146,17 @@ pub fn mvp_tool_specs() -> Vec { pub fn execute_tool(name: &str, input: &Value) -> Result { match name { "bash" => from_value::(input).and_then(run_bash), - "read_file" => from_value::(input).and_then(run_read_file), - "write_file" => from_value::(input).and_then(run_write_file), - "edit_file" => from_value::(input).and_then(run_edit_file), - "glob_search" => from_value::(input).and_then(run_glob_search), - "grep_search" => from_value::(input).and_then(run_grep_search), + "read_file" => from_value::(input).and_then(|input| run_read_file(&input)), + "write_file" => { + from_value::(input).and_then(|input| run_write_file(&input)) + } + "edit_file" => from_value::(input).and_then(|input| run_edit_file(&input)), + "glob_search" => { + from_value::(input).and_then(|input| run_glob_search(&input)) + } + "grep_search" => { + from_value::(input).and_then(|input| run_grep_search(&input)) + } _ => Err(format!("unsupported tool: {name}")), } } @@ -164,15 +170,17 @@ fn run_bash(input: BashCommandInput) -> Result { .map_err(|error| error.to_string()) } -fn run_read_file(input: ReadFileInput) -> Result { - to_pretty_json(read_file(&input.path, input.offset, input.limit).map_err(io_to_string)?) +fn run_read_file(input: &ReadFileInput) -> Result { + to_pretty_json( + read_file(&input.path, input.offset, input.limit).map_err(|error| io_to_string(&error))?, + ) } -fn run_write_file(input: WriteFileInput) -> Result { - to_pretty_json(write_file(&input.path, &input.content).map_err(io_to_string)?) +fn run_write_file(input: &WriteFileInput) -> Result { + to_pretty_json(write_file(&input.path, &input.content).map_err(|error| io_to_string(&error))?) } -fn run_edit_file(input: EditFileInput) -> Result { +fn run_edit_file(input: &EditFileInput) -> Result { to_pretty_json( edit_file( &input.path, @@ -180,23 +188,25 @@ fn run_edit_file(input: EditFileInput) -> Result { &input.new_string, input.replace_all.unwrap_or(false), ) - .map_err(io_to_string)?, + .map_err(|error| io_to_string(&error))?, ) } -fn run_glob_search(input: GlobSearchInputValue) -> Result { - to_pretty_json(glob_search(&input.pattern, input.path.as_deref()).map_err(io_to_string)?) +fn run_glob_search(input: &GlobSearchInputValue) -> Result { + to_pretty_json( + glob_search(&input.pattern, input.path.as_deref()).map_err(|error| io_to_string(&error))?, + ) } -fn run_grep_search(input: GrepSearchInput) -> Result { - to_pretty_json(grep_search(&input).map_err(io_to_string)?) +fn run_grep_search(input: &GrepSearchInput) -> Result { + to_pretty_json(grep_search(input).map_err(|error| io_to_string(&error))?) } fn to_pretty_json(value: T) -> Result { serde_json::to_string_pretty(&value).map_err(|error| error.to_string()) } -fn io_to_string(error: std::io::Error) -> String { +fn io_to_string(error: &std::io::Error) -> String { error.to_string() }