mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-05 23:54:50 +08:00
Reduce REPL overhead for orchestration-heavy workflows
Claw already exposes useful orchestration primitives such as session forking, resume, ultraplan, agents, and skills, but compared with OmO/OMX they were still high-friction to discover and re-type during live operator loops. This change makes the REPL act more like an orchestration console by refreshing context-aware tab completions before each prompt, allowing completion after slash-command arguments, and surfacing common workflow paths such as model aliases, permission modes, and recent session IDs. The startup banner and REPL help now advertise that guidance so the capability is visible instead of hidden. Constraint: Keep the improvement low-risk and REPL-local without adding dependencies or new command semantics Rejected: Add a brand new orchestration slash command | higher UX surface area and more docs burden than a discoverability fix Rejected: Implement a persistent HUD/status bar first | higher implementation risk than improving existing command ergonomics Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep dynamic completion candidates aligned with slash-command behavior and session management semantics Tested: cargo test -p rusty-claude-cli Not-tested: Interactive TTY tab-completion behavior in a live terminal session; full clippy remains blocked by pre-existing runtime crate lints
This commit is contained in:
@@ -96,6 +96,8 @@ Commands:
|
|||||||
|
|
||||||
## Slash Commands (REPL)
|
## Slash Commands (REPL)
|
||||||
|
|
||||||
|
Tab completion now expands not just slash command names, but also common workflow arguments like model aliases, permission modes, and recent session IDs.
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `/help` | Show help |
|
| `/help` | Show help |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::io::{self, IsTerminal, Write};
|
use std::io::{self, IsTerminal, Write};
|
||||||
|
|
||||||
use rustyline::completion::{Completer, Pair};
|
use rustyline::completion::{Completer, Pair};
|
||||||
@@ -27,7 +28,7 @@ struct SlashCommandHelper {
|
|||||||
impl SlashCommandHelper {
|
impl SlashCommandHelper {
|
||||||
fn new(completions: Vec<String>) -> Self {
|
fn new(completions: Vec<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
completions,
|
completions: normalize_completions(completions),
|
||||||
current_line: RefCell::new(String::new()),
|
current_line: RefCell::new(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +46,10 @@ impl SlashCommandHelper {
|
|||||||
current.clear();
|
current.clear();
|
||||||
current.push_str(line);
|
current.push_str(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_completions(&mut self, completions: Vec<String>) {
|
||||||
|
self.completions = normalize_completions(completions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for SlashCommandHelper {
|
impl Completer for SlashCommandHelper {
|
||||||
@@ -126,6 +131,12 @@ impl LineEditor {
|
|||||||
let _ = self.editor.add_history_entry(entry);
|
let _ = self.editor.add_history_entry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_completions(&mut self, completions: Vec<String>) {
|
||||||
|
if let Some(helper) = self.editor.helper_mut() {
|
||||||
|
helper.set_completions(completions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_line(&mut self) -> io::Result<ReadOutcome> {
|
pub fn read_line(&mut self) -> io::Result<ReadOutcome> {
|
||||||
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
|
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
|
||||||
return self.read_line_fallback();
|
return self.read_line_fallback();
|
||||||
@@ -192,13 +203,22 @@ fn slash_command_prefix(line: &str, pos: usize) -> Option<&str> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let prefix = &line[..pos];
|
let prefix = &line[..pos];
|
||||||
if prefix.contains(char::is_whitespace) || !prefix.starts_with('/') {
|
if !prefix.starts_with('/') {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(prefix)
|
Some(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_completions(completions: Vec<String>) -> Vec<String> {
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
completions
|
||||||
|
.into_iter()
|
||||||
|
.filter(|candidate| candidate.starts_with('/'))
|
||||||
|
.filter(|candidate| seen.insert(candidate.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{slash_command_prefix, LineEditor, SlashCommandHelper};
|
use super::{slash_command_prefix, LineEditor, SlashCommandHelper};
|
||||||
@@ -208,9 +228,13 @@ mod tests {
|
|||||||
use rustyline::Context;
|
use rustyline::Context;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extracts_only_terminal_slash_command_prefixes() {
|
fn extracts_terminal_slash_command_prefixes_with_arguments() {
|
||||||
assert_eq!(slash_command_prefix("/he", 3), Some("/he"));
|
assert_eq!(slash_command_prefix("/he", 3), Some("/he"));
|
||||||
assert_eq!(slash_command_prefix("/help me", 5), None);
|
assert_eq!(slash_command_prefix("/help me", 8), Some("/help me"));
|
||||||
|
assert_eq!(
|
||||||
|
slash_command_prefix("/session switch ses", 19),
|
||||||
|
Some("/session switch ses")
|
||||||
|
);
|
||||||
assert_eq!(slash_command_prefix("hello", 5), None);
|
assert_eq!(slash_command_prefix("hello", 5), None);
|
||||||
assert_eq!(slash_command_prefix("/help", 2), None);
|
assert_eq!(slash_command_prefix("/help", 2), None);
|
||||||
}
|
}
|
||||||
@@ -238,6 +262,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_matching_slash_command_arguments() {
|
||||||
|
let helper = SlashCommandHelper::new(vec![
|
||||||
|
"/model".to_string(),
|
||||||
|
"/model opus".to_string(),
|
||||||
|
"/model sonnet".to_string(),
|
||||||
|
"/session switch alpha".to_string(),
|
||||||
|
]);
|
||||||
|
let history = DefaultHistory::new();
|
||||||
|
let ctx = Context::new(&history);
|
||||||
|
let (start, matches) = helper
|
||||||
|
.complete("/model o", 8, &ctx)
|
||||||
|
.expect("completion should work");
|
||||||
|
|
||||||
|
assert_eq!(start, 0);
|
||||||
|
assert_eq!(
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|candidate| candidate.replacement)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["/model opus".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ignores_non_slash_command_completion_requests() {
|
fn ignores_non_slash_command_completion_requests() {
|
||||||
let helper = SlashCommandHelper::new(vec!["/help".to_string()]);
|
let helper = SlashCommandHelper::new(vec!["/help".to_string()]);
|
||||||
@@ -266,4 +314,17 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(editor.editor.history().len(), 1);
|
assert_eq!(editor.editor.history().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_completions_replaces_and_normalizes_candidates() {
|
||||||
|
let mut editor = LineEditor::new("> ", vec!["/help".to_string()]);
|
||||||
|
editor.set_completions(vec![
|
||||||
|
"/model opus".to_string(),
|
||||||
|
"/model opus".to_string(),
|
||||||
|
"status".to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let helper = editor.editor.helper().expect("helper should exist");
|
||||||
|
assert_eq!(helper.completions, vec!["/model opus".to_string()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,11 @@ use plugins::{PluginManager, PluginManagerConfig};
|
|||||||
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
||||||
use runtime::{
|
use runtime::{
|
||||||
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
||||||
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials,
|
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, ApiClient,
|
||||||
ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource,
|
ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock,
|
||||||
ContentBlock, ConversationMessage, ConversationRuntime, MessageRole, PromptCacheEvent,
|
ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig,
|
||||||
OAuthAuthorizationRequest, OAuthConfig,
|
OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent,
|
||||||
OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, RuntimeError,
|
RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
||||||
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tools::GlobalToolRegistry;
|
use tools::GlobalToolRegistry;
|
||||||
@@ -1036,10 +1035,12 @@ 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("> ", cli.repl_completion_candidates().unwrap_or_default());
|
||||||
println!("{}", cli.startup_banner());
|
println!("{}", cli.startup_banner());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
|
||||||
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().to_string();
|
||||||
@@ -1200,7 +1201,7 @@ impl LiveCli {
|
|||||||
\x1b[2mPermissions\x1b[0m {}\n\
|
\x1b[2mPermissions\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[2mTab\x1b[0m for workflow completions · \x1b[2mShift+Enter\x1b[0m for newline",
|
||||||
self.model,
|
self.model,
|
||||||
self.permission_mode.as_str(),
|
self.permission_mode.as_str(),
|
||||||
cwd,
|
cwd,
|
||||||
@@ -1208,6 +1209,17 @@ impl LiveCli {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn repl_completion_candidates(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||||
|
Ok(slash_command_completion_candidates_with_sessions(
|
||||||
|
&self.model,
|
||||||
|
Some(&self.session.id),
|
||||||
|
list_managed_sessions()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|session| session.id)
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_turn_runtime(
|
fn prepare_turn_runtime(
|
||||||
&self,
|
&self,
|
||||||
emit_output: bool,
|
emit_output: bool,
|
||||||
@@ -2057,25 +2069,25 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
|
|||||||
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
|
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
|
||||||
.map(|duration| duration.as_secs())
|
.map(|duration| duration.as_secs())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let (id, message_count, parent_session_id, branch_name) = Session::load_from_path(&path)
|
let (id, message_count, parent_session_id, branch_name) =
|
||||||
.map(|session| {
|
match Session::load_from_path(&path) {
|
||||||
let parent_session_id = session
|
Ok(session) => {
|
||||||
.fork
|
let parent_session_id = session
|
||||||
.as_ref()
|
.fork
|
||||||
.map(|fork| fork.parent_session_id.clone());
|
.as_ref()
|
||||||
let branch_name = session
|
.map(|fork| fork.parent_session_id.clone());
|
||||||
.fork
|
let branch_name = session
|
||||||
.as_ref()
|
.fork
|
||||||
.and_then(|fork| fork.branch_name.clone());
|
.as_ref()
|
||||||
(
|
.and_then(|fork| fork.branch_name.clone());
|
||||||
session.session_id,
|
(
|
||||||
session.messages.len(),
|
session.session_id,
|
||||||
parent_session_id,
|
session.messages.len(),
|
||||||
branch_name,
|
parent_session_id,
|
||||||
)
|
branch_name,
|
||||||
})
|
)
|
||||||
.unwrap_or_else(|_| {
|
}
|
||||||
(
|
Err(_) => (
|
||||||
path.file_stem()
|
path.file_stem()
|
||||||
.and_then(|value| value.to_str())
|
.and_then(|value| value.to_str())
|
||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
@@ -2083,8 +2095,8 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
)
|
),
|
||||||
});
|
};
|
||||||
sessions.push(ManagedSessionSummary {
|
sessions.push(ManagedSessionSummary {
|
||||||
id,
|
id,
|
||||||
path,
|
path,
|
||||||
@@ -2143,7 +2155,7 @@ fn render_repl_help() -> String {
|
|||||||
" /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 commands, modes, and recent sessions".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(),
|
String::new(),
|
||||||
@@ -3146,7 +3158,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<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>> {
|
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||||
|
{
|
||||||
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
||||||
let mut runtime = ConversationRuntime::new_with_features(
|
let mut runtime = ConversationRuntime::new_with_features(
|
||||||
session,
|
session,
|
||||||
@@ -3286,7 +3299,6 @@ impl AnthropicRuntimeClient {
|
|||||||
progress_reporter,
|
progress_reporter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
|
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
|
||||||
@@ -3515,16 +3527,78 @@ fn collect_prompt_cache_events(summary: &runtime::TurnSummary) -> Vec<serde_json
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn slash_command_completion_candidates() -> Vec<String> {
|
fn slash_command_completion_candidates_with_sessions(
|
||||||
slash_command_specs()
|
model: &str,
|
||||||
.iter()
|
active_session_id: Option<&str>,
|
||||||
.flat_map(|spec| {
|
recent_session_ids: Vec<String>,
|
||||||
std::iter::once(spec.name)
|
) -> Vec<String> {
|
||||||
.chain(spec.aliases.iter().copied())
|
let mut completions = BTreeSet::new();
|
||||||
.map(|name| format!("/{name}"))
|
|
||||||
.collect::<Vec<_>>()
|
for spec in slash_command_specs() {
|
||||||
})
|
completions.insert(format!("/{}", spec.name));
|
||||||
.collect()
|
for alias in spec.aliases {
|
||||||
|
completions.insert(format!("/{alias}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for candidate in [
|
||||||
|
"/bughunter ",
|
||||||
|
"/clear --confirm",
|
||||||
|
"/config ",
|
||||||
|
"/config env",
|
||||||
|
"/config hooks",
|
||||||
|
"/config model",
|
||||||
|
"/config plugins",
|
||||||
|
"/export ",
|
||||||
|
"/issue ",
|
||||||
|
"/model ",
|
||||||
|
"/model opus",
|
||||||
|
"/model sonnet",
|
||||||
|
"/model haiku",
|
||||||
|
"/permissions ",
|
||||||
|
"/permissions read-only",
|
||||||
|
"/permissions workspace-write",
|
||||||
|
"/permissions danger-full-access",
|
||||||
|
"/plugin list",
|
||||||
|
"/plugin install ",
|
||||||
|
"/plugin enable ",
|
||||||
|
"/plugin disable ",
|
||||||
|
"/plugin uninstall ",
|
||||||
|
"/plugin update ",
|
||||||
|
"/plugins list",
|
||||||
|
"/pr ",
|
||||||
|
"/resume ",
|
||||||
|
"/session list",
|
||||||
|
"/session switch ",
|
||||||
|
"/session fork ",
|
||||||
|
"/teleport ",
|
||||||
|
"/ultraplan ",
|
||||||
|
"/agents help",
|
||||||
|
"/skills help",
|
||||||
|
] {
|
||||||
|
completions.insert(candidate.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.trim().is_empty() {
|
||||||
|
completions.insert(format!("/model {}", resolve_model_alias(model)));
|
||||||
|
completions.insert(format!("/model {model}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(active_session_id) = active_session_id.filter(|value| !value.trim().is_empty()) {
|
||||||
|
completions.insert(format!("/resume {active_session_id}"));
|
||||||
|
completions.insert(format!("/session switch {active_session_id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for session_id in recent_session_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.take(10)
|
||||||
|
{
|
||||||
|
completions.insert(format!("/resume {session_id}"));
|
||||||
|
completions.insert(format!("/session switch {session_id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
completions.into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_tool_call_start(name: &str, input: &str) -> String {
|
fn format_tool_call_start(name: &str, input: &str) -> String {
|
||||||
@@ -4023,7 +4097,9 @@ fn push_prompt_cache_record(client: &AnthropicClient, events: &mut Vec<Assistant
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_cache_record_to_runtime_event(record: api::PromptCacheRecord) -> Option<PromptCacheEvent> {
|
fn prompt_cache_record_to_runtime_event(
|
||||||
|
record: api::PromptCacheRecord,
|
||||||
|
) -> Option<PromptCacheEvent> {
|
||||||
let cache_break = record.cache_break?;
|
let cache_break = record.cache_break?;
|
||||||
Some(PromptCacheEvent {
|
Some(PromptCacheEvent {
|
||||||
unexpected: cache_break.unexpected,
|
unexpected: cache_break.unexpected,
|
||||||
@@ -4245,18 +4321,18 @@ fn print_help() {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report,
|
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
||||||
format_internal_prompt_progress_line, format_model_report, format_model_switch_report,
|
format_compact_report, format_cost_report, format_internal_prompt_progress_line,
|
||||||
format_permissions_report,
|
format_model_report, format_model_switch_report, format_permissions_report,
|
||||||
format_permissions_switch_report, format_resume_report, format_status_report,
|
format_permissions_switch_report, format_resume_report, format_status_report,
|
||||||
format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args,
|
format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args,
|
||||||
parse_git_status_branch, parse_git_status_metadata_for, permission_policy,
|
parse_git_status_branch, parse_git_status_metadata_for, permission_policy, print_help_to,
|
||||||
print_help_to, push_output_block, render_config_report, render_diff_report,
|
push_output_block, render_config_report, render_diff_report, render_memory_report,
|
||||||
render_memory_report, render_repl_help, resolve_model_alias, response_to_events,
|
render_repl_help, resolve_model_alias, resolve_session_reference, response_to_events,
|
||||||
resume_supported_slash_commands, run_resume_command, status_context, CliAction,
|
resume_supported_slash_commands, run_resume_command,
|
||||||
CliOutputFormat, InternalPromptProgressEvent,
|
slash_command_completion_candidates_with_sessions, status_context, CliAction,
|
||||||
InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, LiveCli,
|
||||||
create_managed_session_handle, resolve_session_reference,
|
SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
use api::{MessageResponse, OutputContentBlock, Usage};
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
||||||
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
||||||
@@ -4622,6 +4698,7 @@ mod tests {
|
|||||||
let help = render_repl_help();
|
let help = render_repl_help();
|
||||||
assert!(help.contains("REPL"));
|
assert!(help.contains("REPL"));
|
||||||
assert!(help.contains("/help"));
|
assert!(help.contains("/help"));
|
||||||
|
assert!(help.contains("Complete commands, modes, and recent sessions"));
|
||||||
assert!(help.contains("/status"));
|
assert!(help.contains("/status"));
|
||||||
assert!(help.contains("/sandbox"));
|
assert!(help.contains("/sandbox"));
|
||||||
assert!(help.contains("/model [model]"));
|
assert!(help.contains("/model [model]"));
|
||||||
@@ -4645,6 +4722,45 @@ mod tests {
|
|||||||
assert!(help.contains("/exit"));
|
assert!(help.contains("/exit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completion_candidates_include_workflow_shortcuts_and_dynamic_sessions() {
|
||||||
|
let completions = slash_command_completion_candidates_with_sessions(
|
||||||
|
"sonnet",
|
||||||
|
Some("session-current"),
|
||||||
|
vec!["session-old".to_string()],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(completions.contains(&"/model claude-sonnet-4-6".to_string()));
|
||||||
|
assert!(completions.contains(&"/permissions workspace-write".to_string()));
|
||||||
|
assert!(completions.contains(&"/session list".to_string()));
|
||||||
|
assert!(completions.contains(&"/session switch session-current".to_string()));
|
||||||
|
assert!(completions.contains(&"/resume session-old".to_string()));
|
||||||
|
assert!(completions.contains(&"/ultraplan ".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startup_banner_mentions_workflow_completions() {
|
||||||
|
let _guard = env_lock();
|
||||||
|
let root = temp_dir();
|
||||||
|
fs::create_dir_all(&root).expect("root dir");
|
||||||
|
|
||||||
|
let banner = with_current_dir(&root, || {
|
||||||
|
LiveCli::new(
|
||||||
|
"claude-sonnet-4-6".to_string(),
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
PermissionMode::DangerFullAccess,
|
||||||
|
)
|
||||||
|
.expect("cli should initialize")
|
||||||
|
.startup_banner()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(banner.contains("Tab"));
|
||||||
|
assert!(banner.contains("workflow completions"));
|
||||||
|
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
#[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()
|
||||||
@@ -5051,8 +5167,13 @@ mod tests {
|
|||||||
|
|
||||||
let resolved = resolve_session_reference("legacy").expect("legacy session should resolve");
|
let resolved = resolve_session_reference("legacy").expect("legacy session should resolve");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved.path.canonicalize().expect("resolved path should exist"),
|
resolved
|
||||||
legacy_path.canonicalize().expect("legacy path should exist")
|
.path
|
||||||
|
.canonicalize()
|
||||||
|
.expect("resolved path should exist"),
|
||||||
|
legacy_path
|
||||||
|
.canonicalize()
|
||||||
|
.expect("legacy path should exist")
|
||||||
);
|
);
|
||||||
|
|
||||||
std::env::set_current_dir(previous).expect("restore cwd");
|
std::env::set_current_dir(previous).expect("restore cwd");
|
||||||
|
|||||||
Reference in New Issue
Block a user