feat(cli): add tab based slash command completion and prompt info completion (#1436)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
mod builder;
|
||||
mod completion;
|
||||
mod input;
|
||||
mod output;
|
||||
mod prompt;
|
||||
@@ -9,6 +10,7 @@ pub use builder::build_session;
|
||||
pub use storage::Identifier;
|
||||
|
||||
use anyhow::Result;
|
||||
use completion::GooseCompleter;
|
||||
use etcetera::choose_app_strategy;
|
||||
use goose::agents::extension::{Envs, ExtensionConfig};
|
||||
use goose::agents::Agent;
|
||||
@@ -20,6 +22,8 @@ use rand::{distributions::Alphanumeric, Rng};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio;
|
||||
|
||||
use crate::log_usage::log_usage;
|
||||
@@ -28,6 +32,25 @@ pub struct Session {
|
||||
agent: Box<dyn Agent>,
|
||||
messages: Vec<Message>,
|
||||
session_file: PathBuf,
|
||||
// Cache for completion data - using std::sync for thread safety without async
|
||||
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
|
||||
}
|
||||
|
||||
// Cache structure for completion data
|
||||
struct CompletionCache {
|
||||
prompts: HashMap<String, Vec<String>>,
|
||||
prompt_info: HashMap<String, output::PromptInfo>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl CompletionCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prompts: HashMap::new(),
|
||||
prompt_info: HashMap::new(),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -44,6 +67,7 @@ impl Session {
|
||||
agent,
|
||||
messages,
|
||||
session_file,
|
||||
completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +112,12 @@ impl Session {
|
||||
self.agent
|
||||
.add_extension(config)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?;
|
||||
|
||||
// Invalidate the completion cache when a new extension is added
|
||||
self.invalidate_completion_cache().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a builtin extension to the session
|
||||
@@ -105,18 +134,35 @@ impl Session {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to start builtin extension: {}", e))?;
|
||||
}
|
||||
|
||||
// Invalidate the completion cache when a new extension is added
|
||||
self.invalidate_completion_cache().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_prompts(&mut self) -> HashMap<String, Vec<String>> {
|
||||
pub async fn list_prompts(
|
||||
&mut self,
|
||||
extension: Option<String>,
|
||||
) -> Result<HashMap<String, Vec<String>>> {
|
||||
let prompts = self.agent.list_extension_prompts().await;
|
||||
prompts
|
||||
|
||||
// Early validation if filtering by extension
|
||||
if let Some(filter) = &extension {
|
||||
if !prompts.contains_key(filter) {
|
||||
return Err(anyhow::anyhow!("Extension '{}' not found", filter));
|
||||
}
|
||||
}
|
||||
|
||||
// Convert prompts into filtered map of extension names to prompt names
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.filter(|(ext, _)| extension.as_ref().is_none_or(|f| f == ext))
|
||||
.map(|(extension, prompt_list)| {
|
||||
let names = prompt_list.into_iter().map(|p| p.name).collect();
|
||||
(extension, names)
|
||||
})
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_prompt_info(&mut self, name: &str) -> Result<Option<output::PromptInfo>> {
|
||||
@@ -157,7 +203,21 @@ impl Session {
|
||||
self.process_message(msg).await?;
|
||||
}
|
||||
|
||||
let mut editor = rustyline::Editor::<(), rustyline::history::DefaultHistory>::new()?;
|
||||
// Initialize the completion cache
|
||||
self.update_completion_cache().await?;
|
||||
|
||||
// Create a new editor with our custom completer
|
||||
let config = rustyline::Config::builder()
|
||||
.completion_type(rustyline::CompletionType::Circular)
|
||||
.build();
|
||||
let mut editor =
|
||||
rustyline::Editor::<GooseCompleter, rustyline::history::DefaultHistory>::with_config(
|
||||
config,
|
||||
)?;
|
||||
|
||||
// Set up the completer with a reference to the completion cache
|
||||
let completer = GooseCompleter::new(self.completion_cache.clone());
|
||||
editor.set_helper(Some(completer));
|
||||
|
||||
// Load history from messages
|
||||
for msg in self
|
||||
@@ -217,8 +277,11 @@ impl Session {
|
||||
continue;
|
||||
}
|
||||
input::InputResult::Retry => continue,
|
||||
input::InputResult::ListPrompts => {
|
||||
output::render_prompts(&self.list_prompts().await)
|
||||
input::InputResult::ListPrompts(extension) => {
|
||||
match self.list_prompts(extension).await {
|
||||
Ok(prompts) => output::render_prompts(&prompts),
|
||||
Err(e) => output::render_error(&e.to_string()),
|
||||
}
|
||||
}
|
||||
input::InputResult::PromptCommand(opts) => {
|
||||
// name is required
|
||||
@@ -431,4 +494,45 @@ impl Session {
|
||||
pub fn session_file(&self) -> PathBuf {
|
||||
self.session_file.clone()
|
||||
}
|
||||
|
||||
/// Update the completion cache with fresh data
|
||||
/// This should be called before the interactive session starts
|
||||
pub async fn update_completion_cache(&mut self) -> Result<()> {
|
||||
// Get fresh data
|
||||
let prompts = self.agent.list_extension_prompts().await;
|
||||
|
||||
// Update the cache with write lock
|
||||
let mut cache = self.completion_cache.write().unwrap();
|
||||
cache.prompts.clear();
|
||||
cache.prompt_info.clear();
|
||||
|
||||
for (extension, prompt_list) in prompts {
|
||||
let names: Vec<String> = prompt_list.iter().map(|p| p.name.clone()).collect();
|
||||
cache.prompts.insert(extension.clone(), names);
|
||||
|
||||
for prompt in prompt_list {
|
||||
cache.prompt_info.insert(
|
||||
prompt.name.clone(),
|
||||
output::PromptInfo {
|
||||
name: prompt.name.clone(),
|
||||
description: prompt.description.clone(),
|
||||
arguments: prompt.arguments.clone(),
|
||||
extension: Some(extension.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cache.last_updated = Instant::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Invalidate the completion cache
|
||||
/// This should be called when extensions are added or removed
|
||||
async fn invalidate_completion_cache(&self) {
|
||||
let mut cache = self.completion_cache.write().unwrap();
|
||||
cache.prompts.clear();
|
||||
cache.prompt_info.clear();
|
||||
cache.last_updated = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user