feat(cli): add tab based slash command completion and prompt info completion (#1436)

This commit is contained in:
Kalvin C
2025-02-28 10:09:27 -08:00
committed by GitHub
parent 314f567f95
commit 0d3d9ca5ae
4 changed files with 711 additions and 13 deletions
+111 -7
View File
@@ -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();
}
}