|
|
@@ -30,12 +30,11 @@ use plugins::{PluginManager, PluginManagerConfig};
|
|
|
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
|
|
use runtime::{
|
|
|
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
|
|
- parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials,
|
|
|
- ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource,
|
|
|
- ContentBlock, ConversationMessage, ConversationRuntime, MessageRole, PromptCacheEvent,
|
|
|
- OAuthAuthorizationRequest, OAuthConfig,
|
|
|
- OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, RuntimeError,
|
|
|
- Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
|
|
+ parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, ApiClient,
|
|
|
+ ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock,
|
|
|
+ ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig,
|
|
|
+ OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent,
|
|
|
+ RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
|
|
};
|
|
|
use serde_json::json;
|
|
|
use tools::GlobalToolRegistry;
|
|
|
@@ -1036,10 +1035,12 @@ fn run_repl(
|
|
|
permission_mode: PermissionMode,
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
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());
|
|
|
|
|
|
loop {
|
|
|
+ editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
|
|
|
match editor.read_line()? {
|
|
|
input::ReadOutcome::Submit(input) => {
|
|
|
let trimmed = input.trim().to_string();
|
|
|
@@ -1200,7 +1201,7 @@ impl LiveCli {
|
|
|
\x1b[2mPermissions\x1b[0m {}\n\
|
|
|
\x1b[2mDirectory\x1b[0m {}\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.permission_mode.as_str(),
|
|
|
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(
|
|
|
&self,
|
|
|
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())
|
|
|
.map(|duration| duration.as_secs())
|
|
|
.unwrap_or_default();
|
|
|
- let (id, message_count, parent_session_id, branch_name) = Session::load_from_path(&path)
|
|
|
- .map(|session| {
|
|
|
- let parent_session_id = session
|
|
|
- .fork
|
|
|
- .as_ref()
|
|
|
- .map(|fork| fork.parent_session_id.clone());
|
|
|
- let branch_name = session
|
|
|
- .fork
|
|
|
- .as_ref()
|
|
|
- .and_then(|fork| fork.branch_name.clone());
|
|
|
- (
|
|
|
- session.session_id,
|
|
|
- session.messages.len(),
|
|
|
- parent_session_id,
|
|
|
- branch_name,
|
|
|
- )
|
|
|
- })
|
|
|
- .unwrap_or_else(|_| {
|
|
|
- (
|
|
|
+ let (id, message_count, parent_session_id, branch_name) =
|
|
|
+ match Session::load_from_path(&path) {
|
|
|
+ Ok(session) => {
|
|
|
+ let parent_session_id = session
|
|
|
+ .fork
|
|
|
+ .as_ref()
|
|
|
+ .map(|fork| fork.parent_session_id.clone());
|
|
|
+ let branch_name = session
|
|
|
+ .fork
|
|
|
+ .as_ref()
|
|
|
+ .and_then(|fork| fork.branch_name.clone());
|
|
|
+ (
|
|
|
+ session.session_id,
|
|
|
+ session.messages.len(),
|
|
|
+ parent_session_id,
|
|
|
+ branch_name,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ Err(_) => (
|
|
|
path.file_stem()
|
|
|
.and_then(|value| value.to_str())
|
|
|
.unwrap_or("unknown")
|
|
|
@@ -2083,8 +2095,8 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
|
|
|
0,
|
|
|
None,
|
|
|
None,
|
|
|
- )
|
|
|
- });
|
|
|
+ ),
|
|
|
+ };
|
|
|
sessions.push(ManagedSessionSummary {
|
|
|
id,
|
|
|
path,
|
|
|
@@ -2143,7 +2155,7 @@ fn render_repl_help() -> String {
|
|
|
" /exit Quit the REPL".to_string(),
|
|
|
" /quit Quit the REPL".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(),
|
|
|
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
|
|
|
String::new(),
|
|
|
@@ -3146,7 +3158,8 @@ fn build_runtime(
|
|
|
allowed_tools: Option<AllowedToolSet>,
|
|
|
permission_mode: PermissionMode,
|
|
|
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 mut runtime = ConversationRuntime::new_with_features(
|
|
|
session,
|
|
|
@@ -3286,7 +3299,6 @@ impl AnthropicRuntimeClient {
|
|
|
progress_reporter,
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
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()
|
|
|
}
|
|
|
|
|
|
-fn slash_command_completion_candidates() -> Vec<String> {
|
|
|
- slash_command_specs()
|
|
|
- .iter()
|
|
|
- .flat_map(|spec| {
|
|
|
- std::iter::once(spec.name)
|
|
|
- .chain(spec.aliases.iter().copied())
|
|
|
- .map(|name| format!("/{name}"))
|
|
|
- .collect::<Vec<_>>()
|
|
|
- })
|
|
|
- .collect()
|
|
|
+fn slash_command_completion_candidates_with_sessions(
|
|
|
+ model: &str,
|
|
|
+ active_session_id: Option<&str>,
|
|
|
+ recent_session_ids: Vec<String>,
|
|
|
+) -> Vec<String> {
|
|
|
+ let mut completions = BTreeSet::new();
|
|
|
+
|
|
|
+ for spec in slash_command_specs() {
|
|
|
+ completions.insert(format!("/{}", spec.name));
|
|
|
+ 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 {
|
|
|
@@ -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?;
|
|
|
Some(PromptCacheEvent {
|
|
|
unexpected: cache_break.unexpected,
|
|
|
@@ -4245,18 +4321,18 @@ fn print_help() {
|
|
|
#[cfg(test)]
|
|
|
mod tests {
|
|
|
use super::{
|
|
|
- describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report,
|
|
|
- format_internal_prompt_progress_line, format_model_report, format_model_switch_report,
|
|
|
- format_permissions_report,
|
|
|
+ create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
|
|
+ format_compact_report, format_cost_report, format_internal_prompt_progress_line,
|
|
|
+ format_model_report, 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_branch, parse_git_status_metadata_for, permission_policy,
|
|
|
- print_help_to, push_output_block, render_config_report, render_diff_report,
|
|
|
- render_memory_report, render_repl_help, resolve_model_alias, response_to_events,
|
|
|
- resume_supported_slash_commands, run_resume_command, status_context, CliAction,
|
|
|
- CliOutputFormat, InternalPromptProgressEvent,
|
|
|
- InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
|
|
- create_managed_session_handle, resolve_session_reference,
|
|
|
+ parse_git_status_branch, parse_git_status_metadata_for, permission_policy, print_help_to,
|
|
|
+ push_output_block, render_config_report, render_diff_report, render_memory_report,
|
|
|
+ render_repl_help, resolve_model_alias, resolve_session_reference, response_to_events,
|
|
|
+ resume_supported_slash_commands, run_resume_command,
|
|
|
+ slash_command_completion_candidates_with_sessions, status_context, CliAction,
|
|
|
+ CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, LiveCli,
|
|
|
+ SlashCommand, StatusUsage, DEFAULT_MODEL,
|
|
|
};
|
|
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
|
|
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
|
|
@@ -4622,6 +4698,7 @@ mod tests {
|
|
|
let help = render_repl_help();
|
|
|
assert!(help.contains("REPL"));
|
|
|
assert!(help.contains("/help"));
|
|
|
+ assert!(help.contains("Complete commands, modes, and recent sessions"));
|
|
|
assert!(help.contains("/status"));
|
|
|
assert!(help.contains("/sandbox"));
|
|
|
assert!(help.contains("/model [model]"));
|
|
|
@@ -4645,6 +4722,45 @@ mod tests {
|
|
|
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]
|
|
|
fn resume_supported_command_list_matches_expected_surface() {
|
|
|
let names = resume_supported_slash_commands()
|
|
|
@@ -5051,8 +5167,13 @@ mod tests {
|
|
|
|
|
|
let resolved = resolve_session_reference("legacy").expect("legacy session should resolve");
|
|
|
assert_eq!(
|
|
|
- resolved.path.canonicalize().expect("resolved path should exist"),
|
|
|
- legacy_path.canonicalize().expect("legacy path should exist")
|
|
|
+ resolved
|
|
|
+ .path
|
|
|
+ .canonicalize()
|
|
|
+ .expect("resolved path should exist"),
|
|
|
+ legacy_path
|
|
|
+ .canonicalize()
|
|
|
+ .expect("legacy path should exist")
|
|
|
);
|
|
|
|
|
|
std::env::set_current_dir(previous).expect("restore cwd");
|