mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-06 04:06:45 +00:00
Compare commits
1 Commits
27e46d7ea6
...
feat/ui-ha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a13b1c2825 |
@@ -2,7 +2,7 @@ use std::io::{self, Write};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::args::{OutputFormat, PermissionMode};
|
use crate::args::{OutputFormat, PermissionMode};
|
||||||
use crate::input::{LineEditor, ReadOutcome};
|
use crate::input::{EditorMode, LineEditor, ReadOutcome};
|
||||||
use crate::render::{Spinner, TerminalRenderer};
|
use crate::render::{Spinner, TerminalRenderer};
|
||||||
use runtime::{ConversationClient, ConversationMessage, RuntimeError, StreamEvent, UsageSummary};
|
use runtime::{ConversationClient, ConversationMessage, RuntimeError, StreamEvent, UsageSummary};
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ impl CliApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_repl(&mut self) -> io::Result<()> {
|
pub fn run_repl(&mut self) -> io::Result<()> {
|
||||||
let mut editor = LineEditor::new("› ", Vec::new());
|
let mut editor = LineEditor::new("› ", Vec::new(), EditorMode::Emacs);
|
||||||
println!("Claw Code interactive mode");
|
println!("Claw Code interactive mode");
|
||||||
println!("Type /help for commands. Shift+Enter or Ctrl+J inserts a newline.");
|
println!("Type /help for commands. Shift+Enter or Ctrl+J inserts a newline.");
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,38 @@ pub enum ReadOutcome {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum EditorMode {
|
||||||
|
Emacs,
|
||||||
|
Vim,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorMode {
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_config_value(value: Option<&str>) -> Self {
|
||||||
|
match value {
|
||||||
|
Some("vim") => Self::Vim,
|
||||||
|
Some("emacs") | Some("default") | None => Self::Emacs,
|
||||||
|
Some(_) => Self::Emacs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Emacs => "emacs",
|
||||||
|
Self::Vim => "vim",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn rustyline_mode(self) -> EditMode {
|
||||||
|
match self {
|
||||||
|
Self::Emacs => EditMode::Emacs,
|
||||||
|
Self::Vim => EditMode::Vi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SlashCommandHelper {
|
struct SlashCommandHelper {
|
||||||
completions: Vec<String>,
|
completions: Vec<String>,
|
||||||
current_line: RefCell<String>,
|
current_line: RefCell<String>,
|
||||||
@@ -100,10 +132,10 @@ pub struct LineEditor {
|
|||||||
|
|
||||||
impl LineEditor {
|
impl LineEditor {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(prompt: impl Into<String>, completions: Vec<String>) -> Self {
|
pub fn new(prompt: impl Into<String>, completions: Vec<String>, mode: EditorMode) -> Self {
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
.completion_type(CompletionType::List)
|
.completion_type(CompletionType::List)
|
||||||
.edit_mode(EditMode::Emacs)
|
.edit_mode(mode.rustyline_mode())
|
||||||
.build();
|
.build();
|
||||||
let mut editor = Editor::<SlashCommandHelper, DefaultHistory>::with_config(config)
|
let mut editor = Editor::<SlashCommandHelper, DefaultHistory>::with_config(config)
|
||||||
.expect("rustyline editor should initialize");
|
.expect("rustyline editor should initialize");
|
||||||
@@ -201,7 +233,7 @@ fn slash_command_prefix(line: &str, pos: usize) -> Option<&str> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{slash_command_prefix, LineEditor, SlashCommandHelper};
|
use super::{slash_command_prefix, EditorMode, LineEditor, SlashCommandHelper};
|
||||||
use rustyline::completion::Completer;
|
use rustyline::completion::Completer;
|
||||||
use rustyline::highlight::Highlighter;
|
use rustyline::highlight::Highlighter;
|
||||||
use rustyline::history::{DefaultHistory, History};
|
use rustyline::history::{DefaultHistory, History};
|
||||||
@@ -260,10 +292,28 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn push_history_ignores_blank_entries() {
|
fn push_history_ignores_blank_entries() {
|
||||||
let mut editor = LineEditor::new("> ", vec!["/help".to_string()]);
|
let mut editor = LineEditor::new("> ", vec!["/help".to_string()], EditorMode::Emacs);
|
||||||
editor.push_history(" ");
|
editor.push_history(" ");
|
||||||
editor.push_history("/help");
|
editor.push_history("/help");
|
||||||
|
|
||||||
assert_eq!(editor.editor.history().len(), 1);
|
assert_eq!(editor.editor.history().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_editor_mode_from_config_values() {
|
||||||
|
assert_eq!(EditorMode::from_config_value(Some("vim")), EditorMode::Vim);
|
||||||
|
assert_eq!(
|
||||||
|
EditorMode::from_config_value(Some("emacs")),
|
||||||
|
EditorMode::Emacs
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
EditorMode::from_config_value(Some("default")),
|
||||||
|
EditorMode::Emacs
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
EditorMode::from_config_value(Some("wat")),
|
||||||
|
EditorMode::Emacs
|
||||||
|
);
|
||||||
|
assert_eq!(EditorMode::from_config_value(None), EditorMode::Emacs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use std::thread;
|
|||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use api::{
|
use api::{
|
||||||
resolve_startup_auth_source, ClawApiClient, AuthSource, ContentBlockDelta, InputContentBlock,
|
resolve_startup_auth_source, AuthSource, ClawApiClient, ContentBlockDelta, InputContentBlock,
|
||||||
InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
|
InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
|
||||||
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
||||||
};
|
};
|
||||||
@@ -828,7 +828,7 @@ fn run_resume_command(
|
|||||||
match command {
|
match command {
|
||||||
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_repl_help()),
|
message: Some(render_repl_help(resolve_editor_mode())),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Compact => {
|
SlashCommand::Compact => {
|
||||||
let result = runtime::compact_session(
|
let result = runtime::compact_session(
|
||||||
@@ -881,6 +881,7 @@ fn run_resume_command(
|
|||||||
estimated_tokens: 0,
|
estimated_tokens: 0,
|
||||||
},
|
},
|
||||||
default_permission_mode().as_str(),
|
default_permission_mode().as_str(),
|
||||||
|
resolve_editor_mode().label(),
|
||||||
&status_context(Some(session_path))?,
|
&status_context(Some(session_path))?,
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
@@ -960,28 +961,29 @@ fn run_repl(
|
|||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
|
let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
|
||||||
let mut editor = input::LineEditor::new("> ", slash_command_completion_candidates());
|
let mut editor =
|
||||||
|
input::LineEditor::new("> ", slash_command_completion_candidates(), cli.editor_mode);
|
||||||
println!("{}", cli.startup_banner());
|
println!("{}", cli.startup_banner());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match editor.read_line()? {
|
match editor.read_line()? {
|
||||||
input::ReadOutcome::Submit(input) => {
|
input::ReadOutcome::Submit(input) => {
|
||||||
let trimmed = input.trim().to_string();
|
let trimmed = input.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if matches!(trimmed.as_str(), "/exit" | "/quit") {
|
if matches!(trimmed, "/exit" | "/quit") {
|
||||||
cli.persist_session()?;
|
cli.persist_session()?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Some(command) = SlashCommand::parse(&trimmed) {
|
if let Some(command) = SlashCommand::parse(trimmed) {
|
||||||
if cli.handle_repl_command(command)? {
|
if cli.handle_repl_command(command)? {
|
||||||
cli.persist_session()?;
|
cli.persist_session()?;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
editor.push_history(input);
|
editor.push_history(&input);
|
||||||
cli.run_turn(&trimmed)?;
|
cli.run_turn(&input)?;
|
||||||
}
|
}
|
||||||
input::ReadOutcome::Cancel => {}
|
input::ReadOutcome::Cancel => {}
|
||||||
input::ReadOutcome::Exit => {
|
input::ReadOutcome::Exit => {
|
||||||
@@ -1012,6 +1014,7 @@ struct LiveCli {
|
|||||||
model: String,
|
model: String,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
|
editor_mode: input::EditorMode,
|
||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
runtime: ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>,
|
runtime: ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>,
|
||||||
session: SessionHandle,
|
session: SessionHandle,
|
||||||
@@ -1025,6 +1028,7 @@ impl LiveCli {
|
|||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let system_prompt = build_system_prompt()?;
|
let system_prompt = build_system_prompt()?;
|
||||||
|
let editor_mode = resolve_editor_mode();
|
||||||
let session = create_managed_session_handle()?;
|
let session = create_managed_session_handle()?;
|
||||||
let runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
Session::new(),
|
Session::new(),
|
||||||
@@ -1040,6 +1044,7 @@ impl LiveCli {
|
|||||||
model,
|
model,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
|
editor_mode,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
runtime,
|
runtime,
|
||||||
session,
|
session,
|
||||||
@@ -1060,14 +1065,16 @@ impl LiveCli {
|
|||||||
██║ ██║ ███████║██║ █╗ ██║\n\
|
██║ ██║ ███████║██║ █╗ ██║\n\
|
||||||
██║ ██║ ██╔══██║██║███╗██║\n\
|
██║ ██║ ██╔══██║██║███╗██║\n\
|
||||||
╚██████╗███████╗██║ ██║╚███╔███╔╝\n\
|
╚██████╗███████╗██║ ██║╚███╔███╔╝\n\
|
||||||
╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\x1b[0m \x1b[38;5;208mCode\x1b[0m 🦞\n\n\
|
╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\x1b[0m \x1b[38;5;208mCode\x1b[0m 🦞\n\n\
|
||||||
\x1b[2mModel\x1b[0m {}\n\
|
\x1b[2mModel\x1b[0m {}\n\
|
||||||
\x1b[2mPermissions\x1b[0m {}\n\
|
\x1b[2mPermissions\x1b[0m {}\n\
|
||||||
|
\x1b[2mInput mode\x1b[0m {}\n\
|
||||||
\x1b[2mDirectory\x1b[0m {}\n\
|
\x1b[2mDirectory\x1b[0m {}\n\
|
||||||
\x1b[2mSession\x1b[0m {}\n\n\
|
\x1b[2mSession\x1b[0m {}\n\n\
|
||||||
Type \x1b[1m/help\x1b[0m for commands · \x1b[2mShift+Enter\x1b[0m for newline",
|
Type \x1b[1m/help\x1b[0m for commands · \x1b[1m/exit\x1b[0m to quit · \x1b[2mShift+Enter\x1b[0m for newline",
|
||||||
self.model,
|
self.model,
|
||||||
self.permission_mode.as_str(),
|
self.permission_mode.as_str(),
|
||||||
|
self.editor_mode.label(),
|
||||||
cwd,
|
cwd,
|
||||||
self.session.id,
|
self.session.id,
|
||||||
)
|
)
|
||||||
@@ -1157,7 +1164,7 @@ impl LiveCli {
|
|||||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
Ok(match command {
|
Ok(match command {
|
||||||
SlashCommand::Help => {
|
SlashCommand::Help => {
|
||||||
println!("{}", render_repl_help());
|
println!("{}", render_repl_help(self.editor_mode));
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Status => {
|
SlashCommand::Status => {
|
||||||
@@ -1243,7 +1250,7 @@ impl LiveCli {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Unknown(name) => {
|
SlashCommand::Unknown(name) => {
|
||||||
eprintln!("unknown slash command: /{name}");
|
println!("{}", render_unknown_repl_command(&name));
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1269,6 +1276,7 @@ impl LiveCli {
|
|||||||
estimated_tokens: self.runtime.estimated_tokens(),
|
estimated_tokens: self.runtime.estimated_tokens(),
|
||||||
},
|
},
|
||||||
self.permission_mode.as_str(),
|
self.permission_mode.as_str(),
|
||||||
|
self.editor_mode.label(),
|
||||||
&status_context(Some(&self.session.path)).expect("status context should load"),
|
&status_context(Some(&self.session.path)).expect("status context should load"),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -1849,22 +1857,24 @@ fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::e
|
|||||||
Ok(lines.join("\n"))
|
Ok(lines.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_repl_help() -> String {
|
fn render_repl_help(editor_mode: input::EditorMode) -> String {
|
||||||
[
|
let mut lines = vec![
|
||||||
"REPL".to_string(),
|
"REPL".to_string(),
|
||||||
|
format!(" Input mode {}", editor_mode.label()),
|
||||||
" /exit Quit the REPL".to_string(),
|
" /exit Quit the REPL".to_string(),
|
||||||
" /quit Quit the REPL".to_string(),
|
" /quit Quit the REPL".to_string(),
|
||||||
" Up/Down Navigate prompt history".to_string(),
|
" Up/Down Navigate prompt history".to_string(),
|
||||||
" Tab Complete slash commands".to_string(),
|
" Tab Complete slash commands".to_string(),
|
||||||
" Ctrl-C Clear input (or exit on empty prompt)".to_string(),
|
" Ctrl-C Clear input (or exit on empty prompt)".to_string(),
|
||||||
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
|
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
|
||||||
String::new(),
|
];
|
||||||
render_slash_command_help(),
|
if editor_mode == input::EditorMode::Vim {
|
||||||
]
|
lines.push(" Esc Switch to normal mode".to_string());
|
||||||
.join(
|
lines.push(" i / a Return to insert mode".to_string());
|
||||||
"
|
}
|
||||||
",
|
lines.push(String::new());
|
||||||
)
|
lines.push(render_slash_command_help());
|
||||||
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status_context(
|
fn status_context(
|
||||||
@@ -1892,6 +1902,7 @@ fn format_status_report(
|
|||||||
model: &str,
|
model: &str,
|
||||||
usage: StatusUsage,
|
usage: StatusUsage,
|
||||||
permission_mode: &str,
|
permission_mode: &str,
|
||||||
|
editor_mode: &str,
|
||||||
context: &StatusContext,
|
context: &StatusContext,
|
||||||
) -> String {
|
) -> String {
|
||||||
[
|
[
|
||||||
@@ -1899,6 +1910,7 @@ fn format_status_report(
|
|||||||
"Status
|
"Status
|
||||||
Model {model}
|
Model {model}
|
||||||
Permission mode {permission_mode}
|
Permission mode {permission_mode}
|
||||||
|
Input mode {editor_mode}
|
||||||
Messages {}
|
Messages {}
|
||||||
Turns {}
|
Turns {}
|
||||||
Estimated tokens {}",
|
Estimated tokens {}",
|
||||||
@@ -2037,8 +2049,7 @@ fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
|
|||||||
if project_context.instruction_files.is_empty() {
|
if project_context.instruction_files.is_empty() {
|
||||||
lines.push("Discovered files".to_string());
|
lines.push("Discovered files".to_string());
|
||||||
lines.push(
|
lines.push(
|
||||||
" No CLAW instruction files discovered in the current directory ancestry."
|
" No CLAW instruction files discovered in the current directory ancestry.".to_string(),
|
||||||
.to_string(),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
lines.push("Discovered files".to_string());
|
lines.push("Discovered files".to_string());
|
||||||
@@ -2790,7 +2801,8 @@ fn build_runtime(
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||||
) -> Result<ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>> {
|
) -> Result<ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||||
|
{
|
||||||
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
||||||
Ok(ConversationRuntime::new_with_features(
|
Ok(ConversationRuntime::new_with_features(
|
||||||
session,
|
session,
|
||||||
@@ -3101,7 +3113,7 @@ fn collect_tool_results(summary: &runtime::TurnSummary) -> Vec<serde_json::Value
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn slash_command_completion_candidates() -> Vec<String> {
|
fn slash_command_completion_candidates() -> Vec<String> {
|
||||||
slash_command_specs()
|
let mut candidates = slash_command_specs()
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|spec| {
|
.flat_map(|spec| {
|
||||||
std::iter::once(spec.name)
|
std::iter::once(spec.name)
|
||||||
@@ -3109,9 +3121,90 @@ fn slash_command_completion_candidates() -> Vec<String> {
|
|||||||
.map(|name| format!("/{name}"))
|
.map(|name| format!("/{name}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
candidates.extend([String::from("/exit"), String::from("/quit")]);
|
||||||
|
candidates.sort();
|
||||||
|
candidates.dedup();
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_editor_mode() -> input::EditorMode {
|
||||||
|
let cwd = match env::current_dir() {
|
||||||
|
Ok(cwd) => cwd,
|
||||||
|
Err(_) => return input::EditorMode::Emacs,
|
||||||
|
};
|
||||||
|
let loader = ConfigLoader::default_for(cwd);
|
||||||
|
loader
|
||||||
|
.load()
|
||||||
|
.ok()
|
||||||
|
.map(|config| input::EditorMode::from_config_value(config.get_string("editorMode")))
|
||||||
|
.unwrap_or(input::EditorMode::Emacs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_unknown_repl_command(name: &str) -> String {
|
||||||
|
let suggestions = suggest_repl_commands(name);
|
||||||
|
let mut lines = vec![format!("Unknown slash command: /{name}")];
|
||||||
|
if !suggestions.is_empty() {
|
||||||
|
lines.push(format!(" Did you mean {}?", suggestions.join(", ")));
|
||||||
|
}
|
||||||
|
lines.push(" Type /help to list available commands.".to_string());
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suggest_repl_commands(name: &str) -> Vec<String> {
|
||||||
|
let normalized = name.trim().trim_start_matches('/').to_ascii_lowercase();
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ranked = slash_command_completion_candidates()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|candidate| {
|
||||||
|
let raw = candidate.trim_start_matches('/').to_ascii_lowercase();
|
||||||
|
let distance = edit_distance(&normalized, &raw);
|
||||||
|
let prefix_match = raw.starts_with(&normalized) || normalized.starts_with(&raw);
|
||||||
|
let near_match = distance <= 2;
|
||||||
|
(prefix_match || near_match).then_some((distance, candidate))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ranked.sort();
|
||||||
|
ranked.dedup_by(|left, right| left.1 == right.1);
|
||||||
|
ranked
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, candidate)| candidate)
|
||||||
|
.take(3)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn edit_distance(left: &str, right: &str) -> usize {
|
||||||
|
if left == right {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if left.is_empty() {
|
||||||
|
return right.chars().count();
|
||||||
|
}
|
||||||
|
if right.is_empty() {
|
||||||
|
return left.chars().count();
|
||||||
|
}
|
||||||
|
|
||||||
|
let right_chars = right.chars().collect::<Vec<_>>();
|
||||||
|
let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
|
||||||
|
let mut current = vec![0; right_chars.len() + 1];
|
||||||
|
|
||||||
|
for (left_index, left_char) in left.chars().enumerate() {
|
||||||
|
current[0] = left_index + 1;
|
||||||
|
for (right_index, right_char) in right_chars.iter().enumerate() {
|
||||||
|
let substitution_cost = usize::from(left_char != *right_char);
|
||||||
|
current[right_index + 1] = (previous[right_index + 1] + 1)
|
||||||
|
.min(current[right_index] + 1)
|
||||||
|
.min(previous[right_index] + substitution_cost);
|
||||||
|
}
|
||||||
|
std::mem::swap(&mut previous, &mut current);
|
||||||
|
}
|
||||||
|
|
||||||
|
previous[right_chars.len()]
|
||||||
|
}
|
||||||
|
|
||||||
fn format_tool_call_start(name: &str, input: &str) -> String {
|
fn format_tool_call_start(name: &str, input: &str) -> String {
|
||||||
let parsed: serde_json::Value =
|
let parsed: serde_json::Value =
|
||||||
serde_json::from_str(input).unwrap_or(serde_json::Value::String(input.to_string()));
|
serde_json::from_str(input).unwrap_or(serde_json::Value::String(input.to_string()));
|
||||||
@@ -3816,10 +3909,12 @@ mod tests {
|
|||||||
format_status_report, format_tool_call_start, format_tool_result,
|
format_status_report, format_tool_call_start, format_tool_result,
|
||||||
normalize_permission_mode, parse_args, parse_git_status_metadata, permission_policy,
|
normalize_permission_mode, parse_args, parse_git_status_metadata, permission_policy,
|
||||||
print_help_to, push_output_block, render_config_report, render_memory_report,
|
print_help_to, push_output_block, render_config_report, render_memory_report,
|
||||||
render_repl_help, resolve_model_alias, response_to_events, resume_supported_slash_commands,
|
render_repl_help, render_unknown_repl_command, resolve_model_alias, response_to_events,
|
||||||
status_context, CliAction, CliOutputFormat, InternalPromptProgressEvent,
|
resume_supported_slash_commands, slash_command_completion_candidates, status_context,
|
||||||
InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
CliAction, CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState,
|
||||||
|
SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
|
use crate::input::EditorMode;
|
||||||
use api::{MessageResponse, OutputContentBlock, Usage};
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
||||||
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
||||||
use runtime::{AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode};
|
use runtime::{AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode};
|
||||||
@@ -4131,13 +4226,14 @@ mod tests {
|
|||||||
fn shared_help_uses_resume_annotation_copy() {
|
fn shared_help_uses_resume_annotation_copy() {
|
||||||
let help = commands::render_slash_command_help();
|
let help = commands::render_slash_command_help();
|
||||||
assert!(help.contains("Slash commands"));
|
assert!(help.contains("Slash commands"));
|
||||||
assert!(help.contains("works with --resume SESSION.json"));
|
assert!(help.contains("available via claw --resume SESSION.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_help_includes_shared_commands_and_exit() {
|
fn repl_help_includes_shared_commands_and_exit() {
|
||||||
let help = render_repl_help();
|
let help = render_repl_help(EditorMode::Emacs);
|
||||||
assert!(help.contains("REPL"));
|
assert!(help.contains("REPL"));
|
||||||
|
assert!(help.contains("Input mode emacs"));
|
||||||
assert!(help.contains("/help"));
|
assert!(help.contains("/help"));
|
||||||
assert!(help.contains("/status"));
|
assert!(help.contains("/status"));
|
||||||
assert!(help.contains("/model [model]"));
|
assert!(help.contains("/model [model]"));
|
||||||
@@ -4161,6 +4257,30 @@ mod tests {
|
|||||||
assert!(help.contains("/exit"));
|
assert!(help.contains("/exit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repl_help_includes_vim_key_hints_in_vim_mode() {
|
||||||
|
let help = render_repl_help(EditorMode::Vim);
|
||||||
|
assert!(help.contains("Input mode vim"));
|
||||||
|
assert!(help.contains("Esc Switch to normal mode"));
|
||||||
|
assert!(help.contains("i / a Return to insert mode"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completion_candidates_include_repl_exit_commands() {
|
||||||
|
let candidates = slash_command_completion_candidates();
|
||||||
|
assert!(candidates.contains(&"/exit".to_string()));
|
||||||
|
assert!(candidates.contains(&"/quit".to_string()));
|
||||||
|
assert!(candidates.contains(&"/help".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_repl_command_reports_helpful_suggestions() {
|
||||||
|
let rendered = render_unknown_repl_command("statu");
|
||||||
|
assert!(rendered.contains("Unknown slash command: /statu"));
|
||||||
|
assert!(rendered.contains("/status"));
|
||||||
|
assert!(rendered.contains("Type /help"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resume_supported_command_list_matches_expected_surface() {
|
fn resume_supported_command_list_matches_expected_surface() {
|
||||||
let names = resume_supported_slash_commands()
|
let names = resume_supported_slash_commands()
|
||||||
@@ -4283,6 +4403,7 @@ mod tests {
|
|||||||
estimated_tokens: 128,
|
estimated_tokens: 128,
|
||||||
},
|
},
|
||||||
"workspace-write",
|
"workspace-write",
|
||||||
|
"vim",
|
||||||
&super::StatusContext {
|
&super::StatusContext {
|
||||||
cwd: PathBuf::from("/tmp/project"),
|
cwd: PathBuf::from("/tmp/project"),
|
||||||
session_path: Some(PathBuf::from("session.json")),
|
session_path: Some(PathBuf::from("session.json")),
|
||||||
@@ -4296,6 +4417,7 @@ mod tests {
|
|||||||
assert!(status.contains("Status"));
|
assert!(status.contains("Status"));
|
||||||
assert!(status.contains("Model sonnet"));
|
assert!(status.contains("Model sonnet"));
|
||||||
assert!(status.contains("Permission mode workspace-write"));
|
assert!(status.contains("Permission mode workspace-write"));
|
||||||
|
assert!(status.contains("Input mode vim"));
|
||||||
assert!(status.contains("Messages 7"));
|
assert!(status.contains("Messages 7"));
|
||||||
assert!(status.contains("Latest total 10"));
|
assert!(status.contains("Latest total 10"));
|
||||||
assert!(status.contains("Cumulative total 31"));
|
assert!(status.contains("Cumulative total 31"));
|
||||||
@@ -4438,7 +4560,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_help_mentions_history_completion_and_multiline() {
|
fn repl_help_mentions_history_completion_and_multiline() {
|
||||||
let help = render_repl_help();
|
let help = render_repl_help(EditorMode::Emacs);
|
||||||
assert!(help.contains("Up/Down"));
|
assert!(help.contains("Up/Down"));
|
||||||
assert!(help.contains("Tab"));
|
assert!(help.contains("Tab"));
|
||||||
assert!(help.contains("Shift+Enter/Ctrl+J"));
|
assert!(help.contains("Shift+Enter/Ctrl+J"));
|
||||||
|
|||||||
@@ -389,34 +389,32 @@ pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
|
|||||||
pub fn render_slash_command_help() -> String {
|
pub fn render_slash_command_help() -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
"Slash commands".to_string(),
|
"Slash commands".to_string(),
|
||||||
" [resume] means the command also works with --resume SESSION.json".to_string(),
|
" [resume] = also available via claw --resume SESSION.json".to_string(),
|
||||||
];
|
];
|
||||||
for spec in slash_command_specs() {
|
for spec in slash_command_specs() {
|
||||||
let name = match spec.argument_hint {
|
let name = match spec.argument_hint {
|
||||||
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
||||||
None => format!("/{}", spec.name),
|
None => format!("/{}", spec.name),
|
||||||
};
|
};
|
||||||
let alias_suffix = if spec.aliases.is_empty() {
|
lines.push(format!(" {name}"));
|
||||||
String::new()
|
lines.push(format!(" {}", spec.summary));
|
||||||
} else {
|
if !spec.aliases.is_empty() || spec.resume_supported {
|
||||||
format!(
|
let mut details = Vec::new();
|
||||||
" (aliases: {})",
|
if !spec.aliases.is_empty() {
|
||||||
spec.aliases
|
details.push(format!(
|
||||||
.iter()
|
"aliases: {}",
|
||||||
.map(|alias| format!("/{alias}"))
|
spec.aliases
|
||||||
.collect::<Vec<_>>()
|
.iter()
|
||||||
.join(", ")
|
.map(|alias| format!("/{alias}"))
|
||||||
)
|
.collect::<Vec<_>>()
|
||||||
};
|
.join(", ")
|
||||||
let resume = if spec.resume_supported {
|
));
|
||||||
" [resume]"
|
}
|
||||||
} else {
|
if spec.resume_supported {
|
||||||
""
|
details.push("[resume]".to_string());
|
||||||
};
|
}
|
||||||
lines.push(format!(
|
lines.push(format!(" {}", details.join(" · ")));
|
||||||
" {name:<20} {}{alias_suffix}{resume}",
|
}
|
||||||
spec.summary
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
@@ -1413,7 +1411,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn renders_help_from_shared_specs() {
|
fn renders_help_from_shared_specs() {
|
||||||
let help = render_slash_command_help();
|
let help = render_slash_command_help();
|
||||||
assert!(help.contains("works with --resume SESSION.json"));
|
assert!(help.contains("available via claw --resume SESSION.json"));
|
||||||
assert!(help.contains("/help"));
|
assert!(help.contains("/help"));
|
||||||
assert!(help.contains("/status"));
|
assert!(help.contains("/status"));
|
||||||
assert!(help.contains("/compact"));
|
assert!(help.contains("/compact"));
|
||||||
|
|||||||
@@ -284,6 +284,11 @@ impl RuntimeConfig {
|
|||||||
self.merged.get(key)
|
self.merged.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_string(&self, key: &str) -> Option<&str> {
|
||||||
|
self.get(key).and_then(JsonValue::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn as_json(&self) -> JsonValue {
|
pub fn as_json(&self) -> JsonValue {
|
||||||
JsonValue::Object(self.merged.clone())
|
JsonValue::Object(self.merged.clone())
|
||||||
|
|||||||
Reference in New Issue
Block a user