feat: simplify CLI sessions (#1168)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
use console::style;
|
||||
use goose::agents::extension::ExtensionError;
|
||||
use goose::agents::AgentFactory;
|
||||
use goose::config::{Config, ExtensionManager};
|
||||
use mcp_client::transport::Error as McpClientError;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use super::output;
|
||||
use super::storage;
|
||||
use super::Session;
|
||||
|
||||
pub async fn build_session(
|
||||
name: Option<String>,
|
||||
resume: bool,
|
||||
extensions: Vec<String>,
|
||||
builtins: Vec<String>,
|
||||
) -> Session {
|
||||
// Load config and get provider/model
|
||||
let config = Config::global();
|
||||
|
||||
let provider_name: String = config
|
||||
.get("GOOSE_PROVIDER")
|
||||
.expect("No provider configured. Run 'goose configure' first");
|
||||
let session_dir = storage::ensure_session_dir().expect("Failed to create session directory");
|
||||
|
||||
let model: String = config
|
||||
.get("GOOSE_MODEL")
|
||||
.expect("No model configured. Run 'goose configure' first");
|
||||
let model_config = goose::model::ModelConfig::new(model.clone());
|
||||
let provider =
|
||||
goose::providers::create(&provider_name, model_config).expect("Failed to create provider");
|
||||
|
||||
// Create the agent
|
||||
let agent_version: Option<String> = config.get("GOOSE_AGENT").ok();
|
||||
let mut agent = match agent_version {
|
||||
Some(version) => AgentFactory::create(&version, provider),
|
||||
None => AgentFactory::create(AgentFactory::default_version(), provider),
|
||||
}
|
||||
.expect("Failed to create agent");
|
||||
|
||||
// Setup extensions for the agent
|
||||
for extension in ExtensionManager::get_all().expect("should load extensions") {
|
||||
if extension.enabled {
|
||||
let config = extension.config.clone();
|
||||
agent
|
||||
.add_extension(config.clone())
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
let err = match e {
|
||||
ExtensionError::Transport(McpClientError::StdioProcessError(inner)) => {
|
||||
inner
|
||||
}
|
||||
_ => e.to_string(),
|
||||
};
|
||||
println!("Failed to start extension: {}, {:?}", config.name(), err);
|
||||
println!(
|
||||
"Please check extension configuration for {}.",
|
||||
config.name()
|
||||
);
|
||||
process::exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle session file resolution and resuming
|
||||
let session_file = if resume {
|
||||
if let Some(ref session_name) = name {
|
||||
// Try to resume specific named session
|
||||
let session_file = session_dir.join(format!("{}.jsonl", session_name));
|
||||
if !session_file.exists() {
|
||||
output::render_error(&format!(
|
||||
"Cannot resume session {} - no such session exists",
|
||||
style(session_name).cyan()
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
session_file
|
||||
} else {
|
||||
// Try to resume most recent session
|
||||
match storage::get_most_recent_session() {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
output::render_error("Cannot resume - no previous sessions found");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create new session with provided or generated name
|
||||
let session_name = name.unwrap_or_else(generate_session_name);
|
||||
create_new_session_file(&session_dir, &session_name)
|
||||
};
|
||||
|
||||
// Create new session
|
||||
let mut session = Session::new(agent, session_file.clone());
|
||||
|
||||
// Add extensions if provided
|
||||
for extension_str in extensions {
|
||||
if let Err(e) = session.add_extension(extension_str).await {
|
||||
eprintln!("Failed to start extension: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add builtin extensions
|
||||
for builtin in builtins {
|
||||
if let Err(e) = session.add_builtin(builtin).await {
|
||||
eprintln!("Failed to start builtin extension: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add CLI-specific system prompt extension
|
||||
session
|
||||
.agent
|
||||
.extend_system_prompt(super::prompt::get_cli_prompt())
|
||||
.await;
|
||||
|
||||
output::display_session_info(resume, &provider_name, &model, &session_file);
|
||||
session
|
||||
}
|
||||
|
||||
fn generate_session_name() -> String {
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn create_new_session_file(session_dir: &std::path::Path, name: &str) -> PathBuf {
|
||||
let session_file = session_dir.join(format!("{}.jsonl", name));
|
||||
if session_file.exists() {
|
||||
eprintln!("Session '{}' already exists", name);
|
||||
process::exit(1);
|
||||
}
|
||||
session_file
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
use anyhow::Result;
|
||||
use rustyline::Editor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputResult {
|
||||
Message(String),
|
||||
Exit,
|
||||
AddExtension(String),
|
||||
AddBuiltin(String),
|
||||
ToggleTheme,
|
||||
Retry,
|
||||
}
|
||||
|
||||
pub fn get_input(
|
||||
editor: &mut Editor<(), rustyline::history::DefaultHistory>,
|
||||
) -> Result<InputResult> {
|
||||
// Ensure Ctrl-J binding is set for newlines
|
||||
editor.bind_sequence(
|
||||
rustyline::KeyEvent(rustyline::KeyCode::Char('j'), rustyline::Modifiers::CTRL),
|
||||
rustyline::EventHandler::Simple(rustyline::Cmd::Newline),
|
||||
);
|
||||
|
||||
let prompt = format!("{} ", console::style("( O)>").cyan().bold());
|
||||
let input = match editor.readline(&prompt) {
|
||||
Ok(text) => text,
|
||||
Err(e) => match e {
|
||||
rustyline::error::ReadlineError::Interrupted => return Ok(InputResult::Exit),
|
||||
_ => return Err(e.into()),
|
||||
},
|
||||
};
|
||||
|
||||
// Add valid input to history
|
||||
if !input.trim().is_empty() {
|
||||
editor.add_history_entry(input.as_str())?;
|
||||
}
|
||||
|
||||
// Handle non-slash commands first
|
||||
if !input.starts_with('/') {
|
||||
if input.eq_ignore_ascii_case("exit") || input.eq_ignore_ascii_case("quit") {
|
||||
return Ok(InputResult::Exit);
|
||||
}
|
||||
return Ok(InputResult::Message(input.trim().to_string()));
|
||||
}
|
||||
|
||||
// Handle slash commands
|
||||
match handle_slash_command(&input) {
|
||||
Some(result) => Ok(result),
|
||||
None => Ok(InputResult::Message(input.trim().to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_slash_command(input: &str) -> Option<InputResult> {
|
||||
let input = input.trim();
|
||||
|
||||
match input {
|
||||
"/exit" | "/quit" => Some(InputResult::Exit),
|
||||
"/?" | "/help" => {
|
||||
print_help();
|
||||
Some(InputResult::Retry)
|
||||
}
|
||||
"/t" => Some(InputResult::ToggleTheme),
|
||||
s if s.starts_with("/extension ") => Some(InputResult::AddExtension(s[11..].to_string())),
|
||||
s if s.starts_with("/builtin ") => Some(InputResult::AddBuiltin(s[9..].to_string())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"Available commands:
|
||||
/exit or /quit - Exit the session
|
||||
/t - Toggle Light/Dark/Ansi theme
|
||||
/extension <command> - Add a stdio extension (format: ENV1=val1 command args...)
|
||||
/builtin <names> - Add builtin extensions by name (comma-separated)
|
||||
/? or /help - Display this help message
|
||||
|
||||
Navigation:
|
||||
Ctrl+C - Interrupt goose (resets the interaction to before the interrupted user request)
|
||||
Ctrl+J - Add a newline
|
||||
Up/Down arrows - Navigate through command history"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_handle_slash_command() {
|
||||
// Test exit commands
|
||||
assert!(matches!(
|
||||
handle_slash_command("/exit"),
|
||||
Some(InputResult::Exit)
|
||||
));
|
||||
assert!(matches!(
|
||||
handle_slash_command("/quit"),
|
||||
Some(InputResult::Exit)
|
||||
));
|
||||
|
||||
// Test help commands
|
||||
assert!(matches!(
|
||||
handle_slash_command("/help"),
|
||||
Some(InputResult::Retry)
|
||||
));
|
||||
assert!(matches!(
|
||||
handle_slash_command("/?"),
|
||||
Some(InputResult::Retry)
|
||||
));
|
||||
|
||||
// Test theme toggle
|
||||
assert!(matches!(
|
||||
handle_slash_command("/t"),
|
||||
Some(InputResult::ToggleTheme)
|
||||
));
|
||||
|
||||
// Test extension command
|
||||
if let Some(InputResult::AddExtension(cmd)) = handle_slash_command("/extension foo bar") {
|
||||
assert_eq!(cmd, "foo bar");
|
||||
} else {
|
||||
panic!("Expected AddExtension");
|
||||
}
|
||||
|
||||
// Test builtin command
|
||||
if let Some(InputResult::AddBuiltin(names)) = handle_slash_command("/builtin dev,git") {
|
||||
assert_eq!(names, "dev,git");
|
||||
} else {
|
||||
panic!("Expected AddBuiltin");
|
||||
}
|
||||
|
||||
// Test unknown commands
|
||||
assert!(handle_slash_command("/unknown").is_none());
|
||||
}
|
||||
|
||||
// Test whitespace handling
|
||||
#[test]
|
||||
fn test_whitespace_handling() {
|
||||
// Leading/trailing whitespace in extension command
|
||||
if let Some(InputResult::AddExtension(cmd)) = handle_slash_command(" /extension foo bar ")
|
||||
{
|
||||
assert_eq!(cmd, "foo bar");
|
||||
} else {
|
||||
panic!("Expected AddExtension");
|
||||
}
|
||||
|
||||
// Leading/trailing whitespace in builtin command
|
||||
if let Some(InputResult::AddBuiltin(names)) = handle_slash_command(" /builtin dev,git ") {
|
||||
assert_eq!(names, "dev,git");
|
||||
} else {
|
||||
panic!("Expected AddBuiltin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
mod builder;
|
||||
mod input;
|
||||
mod output;
|
||||
mod prompt;
|
||||
mod storage;
|
||||
mod thinking;
|
||||
|
||||
pub use builder::build_session;
|
||||
|
||||
use anyhow::Result;
|
||||
use goose::agents::extension::{Envs, ExtensionConfig};
|
||||
use goose::agents::Agent;
|
||||
use goose::message::{Message, MessageContent};
|
||||
use mcp_core::handler::ToolError;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use std::path::PathBuf;
|
||||
use tokio;
|
||||
|
||||
use crate::log_usage::log_usage;
|
||||
|
||||
pub struct Session {
|
||||
agent: Box<dyn Agent>,
|
||||
messages: Vec<Message>,
|
||||
session_file: PathBuf,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(agent: Box<dyn Agent>, session_file: PathBuf) -> Self {
|
||||
let messages = match storage::read_messages(&session_file) {
|
||||
Ok(msgs) => msgs,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to load message history: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
Session {
|
||||
agent,
|
||||
messages,
|
||||
session_file,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a stdio extension to the session
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `extension_command` - Full command string including environment variables
|
||||
/// Format: "ENV1=val1 ENV2=val2 command args..."
|
||||
pub async fn add_extension(&mut self, extension_command: String) -> Result<()> {
|
||||
let mut parts: Vec<&str> = extension_command.split_whitespace().collect();
|
||||
let mut envs = std::collections::HashMap::new();
|
||||
|
||||
// Parse environment variables (format: KEY=value)
|
||||
while let Some(part) = parts.first() {
|
||||
if !part.contains('=') {
|
||||
break;
|
||||
}
|
||||
let env_part = parts.remove(0);
|
||||
let (key, value) = env_part.split_once('=').unwrap();
|
||||
envs.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
return Err(anyhow::anyhow!("No command provided in extension string"));
|
||||
}
|
||||
|
||||
let cmd = parts.remove(0).to_string();
|
||||
// Generate a random name for the ephemeral extension
|
||||
let name: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let config = ExtensionConfig::Stdio {
|
||||
name,
|
||||
cmd,
|
||||
args: parts.iter().map(|s| s.to_string()).collect(),
|
||||
envs: Envs::new(envs),
|
||||
};
|
||||
|
||||
self.agent
|
||||
.add_extension(config)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))
|
||||
}
|
||||
|
||||
/// Add a builtin extension to the session
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `builtin_name` - Name of the builtin extension(s), comma separated
|
||||
pub async fn add_builtin(&mut self, builtin_name: String) -> Result<()> {
|
||||
for name in builtin_name.split(',') {
|
||||
let config = ExtensionConfig::Builtin {
|
||||
name: name.trim().to_string(),
|
||||
};
|
||||
self.agent
|
||||
.add_extension(config)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to start builtin extension: {}", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) -> Result<()> {
|
||||
let mut editor = rustyline::Editor::<(), rustyline::history::DefaultHistory>::new()?;
|
||||
|
||||
// Load history from messages
|
||||
for msg in self
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|m| m.role == mcp_core::role::Role::User)
|
||||
{
|
||||
for content in msg.content.iter() {
|
||||
if let Some(text) = content.as_text() {
|
||||
if let Err(e) = editor.add_history_entry(text) {
|
||||
eprintln!("Warning: Failed to add history entry: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output::display_greeting();
|
||||
loop {
|
||||
match input::get_input(&mut editor)? {
|
||||
input::InputResult::Message(content) => {
|
||||
self.messages.push(Message::user().with_text(&content));
|
||||
storage::persist_messages(&self.session_file, &self.messages)?;
|
||||
|
||||
output::show_thinking();
|
||||
self.process_agent_response().await?;
|
||||
output::hide_thinking();
|
||||
}
|
||||
input::InputResult::Exit => break,
|
||||
input::InputResult::AddExtension(cmd) => {
|
||||
match self.add_extension(cmd.clone()).await {
|
||||
Ok(_) => output::render_extension_success(&cmd),
|
||||
Err(e) => output::render_extension_error(&cmd, &e.to_string()),
|
||||
}
|
||||
}
|
||||
input::InputResult::AddBuiltin(names) => {
|
||||
match self.add_builtin(names.clone()).await {
|
||||
Ok(_) => output::render_builtin_success(&names),
|
||||
Err(e) => output::render_builtin_error(&names, &e.to_string()),
|
||||
}
|
||||
}
|
||||
input::InputResult::ToggleTheme => {
|
||||
let current = output::get_theme();
|
||||
let new_theme = match current {
|
||||
output::Theme::Light => {
|
||||
println!("Switching to Dark theme");
|
||||
output::Theme::Dark
|
||||
}
|
||||
output::Theme::Dark => {
|
||||
println!("Switching to Ansi theme");
|
||||
output::Theme::Ansi
|
||||
}
|
||||
output::Theme::Ansi => {
|
||||
println!("Switching to Light theme");
|
||||
output::Theme::Light
|
||||
}
|
||||
};
|
||||
output::set_theme(new_theme);
|
||||
continue;
|
||||
}
|
||||
input::InputResult::Retry => continue,
|
||||
}
|
||||
}
|
||||
|
||||
// Log usage and cleanup
|
||||
let usage = self.agent.usage().await;
|
||||
log_usage(self.session_file.to_string_lossy().to_string(), usage);
|
||||
println!(
|
||||
"\nClosing session. Recorded to {}",
|
||||
self.session_file.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn headless_start(&mut self, initial_message: String) -> Result<()> {
|
||||
self.messages
|
||||
.push(Message::user().with_text(&initial_message));
|
||||
storage::persist_messages(&self.session_file, &self.messages)?;
|
||||
self.process_agent_response().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_agent_response(&mut self) -> Result<()> {
|
||||
let mut stream = self.agent.reply(&self.messages).await?;
|
||||
|
||||
use futures::StreamExt;
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = stream.next() => {
|
||||
match result {
|
||||
Some(Ok(message)) => {
|
||||
self.messages.push(message.clone());
|
||||
storage::persist_messages(&self.session_file, &self.messages)?;
|
||||
output::hide_thinking();
|
||||
output::render_message(&message);
|
||||
output::show_thinking();
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
eprintln!("Error: {}", e);
|
||||
drop(stream);
|
||||
self.handle_interrupted_messages(false);
|
||||
output::render_error(
|
||||
"The error above was an exception we were not able to handle.\n\
|
||||
These errors are often related to connection or authentication\n\
|
||||
We've removed the conversation up to the most recent user message\n\
|
||||
- depending on the error you may be able to continue",
|
||||
);
|
||||
break;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
drop(stream);
|
||||
self.handle_interrupted_messages(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_interrupted_messages(&mut self, interrupt: bool) {
|
||||
// First, get any tool requests from the last message if it exists
|
||||
let tool_requests = self
|
||||
.messages
|
||||
.last()
|
||||
.filter(|msg| msg.role == mcp_core::role::Role::Assistant)
|
||||
.map_or(Vec::new(), |msg| {
|
||||
msg.content
|
||||
.iter()
|
||||
.filter_map(|content| {
|
||||
if let MessageContent::ToolRequest(req) = content {
|
||||
Some((req.id.clone(), req.tool_call.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
if !tool_requests.is_empty() {
|
||||
// Interrupted during a tool request
|
||||
// Create tool responses for all interrupted tool requests
|
||||
let mut response_message = Message::user();
|
||||
let last_tool_name = tool_requests
|
||||
.last()
|
||||
.and_then(|(_, tool_call)| tool_call.as_ref().ok().map(|tool| tool.name.clone()))
|
||||
.unwrap_or_else(|| "tool".to_string());
|
||||
|
||||
let notification = if interrupt {
|
||||
"Interrupted by the user to make a correction".to_string()
|
||||
} else {
|
||||
"An uncaught error happened during tool use".to_string()
|
||||
};
|
||||
for (req_id, _) in &tool_requests {
|
||||
response_message.content.push(MessageContent::tool_response(
|
||||
req_id.clone(),
|
||||
Err(ToolError::ExecutionError(notification.clone())),
|
||||
));
|
||||
}
|
||||
self.messages.push(response_message);
|
||||
|
||||
let prompt = format!(
|
||||
"The existing call to {} was interrupted. How would you like to proceed?",
|
||||
last_tool_name
|
||||
);
|
||||
self.messages.push(Message::assistant().with_text(&prompt));
|
||||
output::render_message(&Message::assistant().with_text(&prompt));
|
||||
} else {
|
||||
// An interruption occurred outside of a tool request-response.
|
||||
if let Some(last_msg) = self.messages.last() {
|
||||
if last_msg.role == mcp_core::role::Role::User {
|
||||
match last_msg.content.first() {
|
||||
Some(MessageContent::ToolResponse(_)) => {
|
||||
// Interruption occurred after a tool had completed but not assistant reply
|
||||
let prompt = "The tool calling loop was interrupted. How would you like to proceed?";
|
||||
self.messages.push(Message::assistant().with_text(prompt));
|
||||
output::render_message(&Message::assistant().with_text(prompt));
|
||||
}
|
||||
Some(_) => {
|
||||
// A real users message
|
||||
self.messages.pop();
|
||||
let prompt = "Interrupted before the model replied and removed the last message.";
|
||||
output::render_message(&Message::assistant().with_text(prompt));
|
||||
}
|
||||
None => panic!("No content in last message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_file(&self) -> PathBuf {
|
||||
self.session_file.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
use bat::WrappingMode;
|
||||
use console::style;
|
||||
use goose::message::{Message, MessageContent, ToolRequest, ToolResponse};
|
||||
use mcp_core::tool::ToolCall;
|
||||
use serde_json::Value;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
|
||||
// Re-export theme for use in main
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Theme {
|
||||
Light,
|
||||
Dark,
|
||||
Ansi,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Theme::Light => "GitHub",
|
||||
Theme::Dark => "zenburn",
|
||||
Theme::Ansi => "base16",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_THEME: RefCell<Theme> = RefCell::new(
|
||||
std::env::var("GOOSE_CLI_THEME")
|
||||
.ok()
|
||||
.map(|val| {
|
||||
if val.eq_ignore_ascii_case("light") {
|
||||
Theme::Light
|
||||
} else if val.eq_ignore_ascii_case("ansi") {
|
||||
Theme::Ansi
|
||||
} else {
|
||||
Theme::Dark
|
||||
}
|
||||
})
|
||||
.unwrap_or(Theme::Dark)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_theme(theme: Theme) {
|
||||
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
|
||||
}
|
||||
|
||||
pub fn get_theme() -> Theme {
|
||||
CURRENT_THEME.with(|t| *t.borrow())
|
||||
}
|
||||
|
||||
// Simple wrapper around spinner to manage its state
|
||||
#[derive(Default)]
|
||||
pub struct ThinkingIndicator {
|
||||
spinner: Option<cliclack::ProgressBar>,
|
||||
}
|
||||
|
||||
impl ThinkingIndicator {
|
||||
pub fn show(&mut self) {
|
||||
let spinner = cliclack::spinner();
|
||||
spinner.start(format!(
|
||||
"{}...",
|
||||
super::thinking::get_random_thinking_message()
|
||||
));
|
||||
self.spinner = Some(spinner);
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
if let Some(spinner) = self.spinner.take() {
|
||||
spinner.stop("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global thinking indicator
|
||||
thread_local! {
|
||||
static THINKING: RefCell<ThinkingIndicator> = RefCell::new(ThinkingIndicator::default());
|
||||
}
|
||||
|
||||
pub fn show_thinking() {
|
||||
THINKING.with(|t| t.borrow_mut().show());
|
||||
}
|
||||
|
||||
pub fn hide_thinking() {
|
||||
THINKING.with(|t| t.borrow_mut().hide());
|
||||
}
|
||||
|
||||
pub fn render_message(message: &Message) {
|
||||
let theme = get_theme();
|
||||
|
||||
for content in &message.content {
|
||||
match content {
|
||||
MessageContent::Text(text) => print_markdown(&text.text, theme),
|
||||
MessageContent::ToolRequest(req) => render_tool_request(req, theme),
|
||||
MessageContent::ToolResponse(resp) => render_tool_response(resp, theme),
|
||||
MessageContent::Image(image) => {
|
||||
println!("Image: [data: {}, type: {}]", image.data, image.mime_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn render_tool_request(req: &ToolRequest, theme: Theme) {
|
||||
match &req.tool_call {
|
||||
Ok(call) => match call.name.as_str() {
|
||||
"developer__text_editor" => render_text_editor_request(call),
|
||||
"developer__shell" => render_shell_request(call),
|
||||
_ => render_default_request(call),
|
||||
},
|
||||
Err(e) => print_markdown(&e.to_string(), theme),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tool_response(resp: &ToolResponse, theme: Theme) {
|
||||
match &resp.tool_result {
|
||||
Ok(contents) => {
|
||||
for content in contents {
|
||||
if let Some(audience) = content.audience() {
|
||||
if !audience.contains(&mcp_core::role::Role::User) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let min_priority = std::env::var("GOOSE_CLI_MIN_PRIORITY")
|
||||
.ok()
|
||||
.and_then(|val| val.parse::<f32>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
if content
|
||||
.priority()
|
||||
.is_some_and(|priority| priority <= min_priority)
|
||||
|| content.priority().is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if let mcp_core::content::Content::Text(text) = content {
|
||||
print_markdown(&text.text, theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => print_markdown(&e.to_string(), theme),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_error(message: &str) {
|
||||
println!("\n {} {}\n", style("error:").red().bold(), message);
|
||||
}
|
||||
|
||||
pub fn render_extension_success(name: &str) {
|
||||
println!();
|
||||
println!(
|
||||
" {} extension `{}`",
|
||||
style("added").green(),
|
||||
style(name).cyan(),
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn render_extension_error(name: &str, error: &str) {
|
||||
println!();
|
||||
println!(
|
||||
" {} to add extension {}",
|
||||
style("failed").red(),
|
||||
style(name).red()
|
||||
);
|
||||
println!();
|
||||
println!("{}", style(error).dim());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn render_builtin_success(names: &str) {
|
||||
println!();
|
||||
println!(
|
||||
" {} builtin{}: {}",
|
||||
style("added").green(),
|
||||
if names.contains(',') { "s" } else { "" },
|
||||
style(names).cyan()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn render_builtin_error(names: &str, error: &str) {
|
||||
println!();
|
||||
println!(
|
||||
" {} to add builtin{}: {}",
|
||||
style("failed").red(),
|
||||
if names.contains(',') { "s" } else { "" },
|
||||
style(names).red()
|
||||
);
|
||||
println!();
|
||||
println!("{}", style(error).dim());
|
||||
println!();
|
||||
}
|
||||
|
||||
fn render_text_editor_request(call: &ToolCall) {
|
||||
print_tool_header(call);
|
||||
|
||||
// Print path first with special formatting
|
||||
if let Some(Value::String(path)) = call.arguments.get("path") {
|
||||
println!(
|
||||
"{}: {}",
|
||||
style("path").dim(),
|
||||
style(shorten_path(path)).green()
|
||||
);
|
||||
}
|
||||
|
||||
// Print other arguments normally, excluding path
|
||||
if let Some(args) = call.arguments.as_object() {
|
||||
let mut other_args = serde_json::Map::new();
|
||||
for (k, v) in args {
|
||||
if k != "path" {
|
||||
other_args.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
print_params(&Value::Object(other_args), 0);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn render_shell_request(call: &ToolCall) {
|
||||
print_tool_header(call);
|
||||
|
||||
match call.arguments.get("command") {
|
||||
Some(Value::String(s)) => {
|
||||
println!("{}: {}", style("command").dim(), style(s).green());
|
||||
}
|
||||
_ => print_params(&call.arguments, 0),
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn render_default_request(call: &ToolCall) {
|
||||
print_tool_header(call);
|
||||
print_params(&call.arguments, 0);
|
||||
println!();
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
fn print_tool_header(call: &ToolCall) {
|
||||
let parts: Vec<_> = call.name.rsplit("__").collect();
|
||||
let tool_header = format!(
|
||||
"─── {} | {} ──────────────────────────",
|
||||
style(parts.first().unwrap_or(&"unknown")),
|
||||
style(
|
||||
parts
|
||||
.split_first()
|
||||
.map(|(_, s)| s.iter().rev().copied().collect::<Vec<_>>().join("__"))
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
)
|
||||
.magenta()
|
||||
.dim(),
|
||||
);
|
||||
println!();
|
||||
println!("{}", tool_header);
|
||||
}
|
||||
|
||||
fn print_markdown(content: &str, theme: Theme) {
|
||||
bat::PrettyPrinter::new()
|
||||
.input(bat::Input::from_bytes(content.as_bytes()))
|
||||
.theme(theme.as_str())
|
||||
.language("Markdown")
|
||||
.wrapping_mode(WrappingMode::Character)
|
||||
.print()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
const MAX_STRING_LENGTH: usize = 40;
|
||||
const INDENT: &str = " ";
|
||||
|
||||
fn print_params(value: &Value, depth: usize) {
|
||||
let indent = INDENT.repeat(depth);
|
||||
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
for (key, val) in map {
|
||||
match val {
|
||||
Value::Object(_) => {
|
||||
println!("{}{}:", indent, style(key).dim());
|
||||
print_params(val, depth + 1);
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
println!("{}{}:", indent, style(key).dim());
|
||||
for item in arr.iter() {
|
||||
println!("{}{}- ", indent, INDENT);
|
||||
print_params(item, depth + 2);
|
||||
}
|
||||
}
|
||||
Value::String(s) => {
|
||||
if s.len() > MAX_STRING_LENGTH {
|
||||
println!("{}{}: {}", indent, style(key).dim(), style("...").dim());
|
||||
} else {
|
||||
println!("{}{}: {}", indent, style(key).dim(), style(s).green());
|
||||
}
|
||||
}
|
||||
Value::Number(n) => {
|
||||
println!("{}{}: {}", indent, style(key).dim(), style(n).blue());
|
||||
}
|
||||
Value::Bool(b) => {
|
||||
println!("{}{}: {}", indent, style(key).dim(), style(b).blue());
|
||||
}
|
||||
Value::Null => {
|
||||
println!("{}{}: {}", indent, style(key).dim(), style("null").dim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for (i, item) in arr.iter().enumerate() {
|
||||
println!("{}{}.", indent, i + 1);
|
||||
print_params(item, depth + 1);
|
||||
}
|
||||
}
|
||||
Value::String(s) => {
|
||||
if s.len() > MAX_STRING_LENGTH {
|
||||
println!(
|
||||
"{}{}",
|
||||
indent,
|
||||
style(format!("[REDACTED: {} chars]", s.len())).yellow()
|
||||
);
|
||||
} else {
|
||||
println!("{}{}", indent, style(s).green());
|
||||
}
|
||||
}
|
||||
Value::Number(n) => {
|
||||
println!("{}{}", indent, style(n).yellow());
|
||||
}
|
||||
Value::Bool(b) => {
|
||||
println!("{}{}", indent, style(b).yellow());
|
||||
}
|
||||
Value::Null => {
|
||||
println!("{}{}", indent, style("null").dim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shorten_path(path: &str) -> String {
|
||||
let path = Path::new(path);
|
||||
|
||||
// First try to convert to ~ if it's in home directory
|
||||
let home = etcetera::home_dir().ok();
|
||||
let path_str = if let Some(home) = home {
|
||||
if let Ok(stripped) = path.strip_prefix(home) {
|
||||
format!("~/{}", stripped.display())
|
||||
} else {
|
||||
path.display().to_string()
|
||||
}
|
||||
} else {
|
||||
path.display().to_string()
|
||||
};
|
||||
|
||||
// If path is already short enough, return as is
|
||||
if path_str.len() <= 60 {
|
||||
return path_str;
|
||||
}
|
||||
|
||||
let parts: Vec<_> = path_str.split('/').collect();
|
||||
|
||||
// If we have 3 or fewer parts, return as is
|
||||
if parts.len() <= 3 {
|
||||
return path_str;
|
||||
}
|
||||
|
||||
// Keep the first component (empty string before root / or ~) and last two components intact
|
||||
let mut shortened = vec![parts[0].to_string()];
|
||||
|
||||
// Shorten middle components to their first letter
|
||||
for component in &parts[1..parts.len() - 2] {
|
||||
if !component.is_empty() {
|
||||
shortened.push(component.chars().next().unwrap_or('?').to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last two components
|
||||
shortened.push(parts[parts.len() - 2].to_string());
|
||||
shortened.push(parts[parts.len() - 1].to_string());
|
||||
|
||||
shortened.join("/")
|
||||
}
|
||||
|
||||
// Session display functions
|
||||
pub fn display_session_info(resume: bool, provider: &str, model: &str, session_file: &Path) {
|
||||
let start_session_msg = if resume {
|
||||
"resuming session |"
|
||||
} else {
|
||||
"starting session |"
|
||||
};
|
||||
println!(
|
||||
"{} {} {} {} {}",
|
||||
style(start_session_msg).dim(),
|
||||
style("provider:").dim(),
|
||||
style(provider).cyan().dim(),
|
||||
style("model:").dim(),
|
||||
style(model).cyan().dim(),
|
||||
);
|
||||
println!(
|
||||
" {} {}",
|
||||
style("logging to").dim(),
|
||||
style(session_file.display()).dim().cyan(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn display_greeting() {
|
||||
println!("\nGoose is running! Enter your instructions, or try asking what goose can do.\n");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_short_paths_unchanged() {
|
||||
assert_eq!(shorten_path("/usr/bin"), "/usr/bin");
|
||||
assert_eq!(shorten_path("/a/b/c"), "/a/b/c");
|
||||
assert_eq!(shorten_path("file.txt"), "file.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_home_directory_conversion() {
|
||||
// Save the current home dir
|
||||
let original_home = env::var("HOME").ok();
|
||||
|
||||
// Set a test home directory
|
||||
env::set_var("HOME", "/Users/testuser");
|
||||
|
||||
assert_eq!(
|
||||
shorten_path("/Users/testuser/documents/file.txt"),
|
||||
"~/documents/file.txt"
|
||||
);
|
||||
|
||||
// A path that starts similarly to home but isn't in home
|
||||
assert_eq!(
|
||||
shorten_path("/Users/testuser2/documents/file.txt"),
|
||||
"/Users/testuser2/documents/file.txt"
|
||||
);
|
||||
|
||||
// Restore the original home dir
|
||||
if let Some(home) = original_home {
|
||||
env::set_var("HOME", home);
|
||||
} else {
|
||||
env::remove_var("HOME");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long_path_shortening() {
|
||||
assert_eq!(
|
||||
shorten_path(
|
||||
"/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv/long/path/with/many/components/file.txt"
|
||||
),
|
||||
"/v/l/p/w/m/components/file.txt"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/// Returns a system prompt extension that explains CLI-specific functionality
|
||||
pub fn get_cli_prompt() -> String {
|
||||
String::from(
|
||||
"You are being accessed through a command-line interface. The following slash commands are available
|
||||
- you can let the user know about them if they need help:
|
||||
|
||||
- /exit or /quit - Exit the session
|
||||
- /t - Toggle between Light/Dark/Ansi themes
|
||||
- /? or /help - Display help message
|
||||
|
||||
Additional keyboard shortcuts:
|
||||
- Ctrl+C - Interrupt the current interaction (resets to before the interrupted request)
|
||||
- Ctrl+J - Add a newline
|
||||
- Up/Down arrows - Navigate command history"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use anyhow::Result;
|
||||
use etcetera::{choose_app_strategy, AppStrategy};
|
||||
use goose::message::Message;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, BufRead, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Ensure the session directory exists and return its path
|
||||
pub fn ensure_session_dir() -> Result<PathBuf> {
|
||||
let data_dir = choose_app_strategy(crate::APP_STRATEGY.clone())
|
||||
.expect("goose requires a home dir")
|
||||
.data_dir()
|
||||
.join("sessions");
|
||||
|
||||
if !data_dir.exists() {
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
}
|
||||
|
||||
Ok(data_dir)
|
||||
}
|
||||
|
||||
/// Get the path to the most recently modified session file
|
||||
pub fn get_most_recent_session() -> Result<PathBuf> {
|
||||
let session_dir = ensure_session_dir()?;
|
||||
let mut entries = fs::read_dir(&session_dir)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "jsonl"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if entries.is_empty() {
|
||||
return Err(anyhow::anyhow!("No session files found"));
|
||||
}
|
||||
|
||||
// Sort by modification time, most recent first
|
||||
entries.sort_by(|a, b| {
|
||||
b.metadata()
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
.cmp(
|
||||
&a.metadata()
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH),
|
||||
)
|
||||
});
|
||||
|
||||
Ok(entries[0].path())
|
||||
}
|
||||
|
||||
/// Read messages from a session file
|
||||
///
|
||||
/// Creates the file if it doesn't exist, reads and deserializes all messages if it does.
|
||||
pub fn read_messages(session_file: &Path) -> Result<Vec<Message>> {
|
||||
let file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(session_file)?;
|
||||
|
||||
let reader = io::BufReader::new(file);
|
||||
let mut messages = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
messages.push(serde_json::from_str::<Message>(&line?)?);
|
||||
}
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Write messages to a session file
|
||||
///
|
||||
/// Overwrites the file with all messages in JSONL format.
|
||||
pub fn persist_messages(session_file: &Path, messages: &[Message]) -> Result<()> {
|
||||
let file = File::create(session_file)?;
|
||||
let mut writer = io::BufWriter::new(file);
|
||||
|
||||
for message in messages {
|
||||
serde_json::to_writer(&mut writer, &message)?;
|
||||
writeln!(writer)?;
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use goose::message::MessageContent;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_read_write_messages() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let file_path = dir.path().join("test.jsonl");
|
||||
|
||||
// Create some test messages
|
||||
let messages = vec![
|
||||
Message::user().with_text("Hello"),
|
||||
Message::assistant().with_text("Hi there"),
|
||||
];
|
||||
|
||||
// Write messages
|
||||
persist_messages(&file_path, &messages)?;
|
||||
|
||||
// Read them back
|
||||
let read_messages = read_messages(&file_path)?;
|
||||
|
||||
// Compare
|
||||
assert_eq!(messages.len(), read_messages.len());
|
||||
for (orig, read) in messages.iter().zip(read_messages.iter()) {
|
||||
assert_eq!(orig.role, read.role);
|
||||
assert_eq!(orig.content.len(), read.content.len());
|
||||
|
||||
// Compare first text content
|
||||
if let (Some(MessageContent::Text(orig_text)), Some(MessageContent::Text(read_text))) =
|
||||
(orig.content.first(), read.content.first())
|
||||
{
|
||||
assert_eq!(orig_text.text, read_text.text);
|
||||
} else {
|
||||
panic!("Messages don't match expected structure");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_file() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let file_path = dir.path().join("empty.jsonl");
|
||||
|
||||
// Reading an empty file should return empty vec
|
||||
let messages = read_messages(&file_path)?;
|
||||
assert!(messages.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_most_recent() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let base_path = dir.path().join("sessions");
|
||||
fs::create_dir_all(&base_path)?;
|
||||
|
||||
// Create a few session files with different timestamps
|
||||
let old_file = base_path.join("old.jsonl");
|
||||
let new_file = base_path.join("new.jsonl");
|
||||
|
||||
// Create files with some delay to ensure different timestamps
|
||||
fs::write(&old_file, "dummy content")?;
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
fs::write(&new_file, "dummy content")?;
|
||||
|
||||
// Override the home directory for testing
|
||||
// This is a bit hacky but works for testing
|
||||
std::env::set_var("HOME", dir.path());
|
||||
|
||||
if let Ok(most_recent) = get_most_recent_session() {
|
||||
assert_eq!(most_recent.file_name().unwrap(), "new.jsonl");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
/// Extended list of playful thinking messages including both goose and general AI actions
|
||||
const THINKING_MESSAGES: &[&str] = &[
|
||||
"Spreading wings",
|
||||
"Honking thoughtfully",
|
||||
"Waddling to conclusions",
|
||||
"Flapping wings excitedly",
|
||||
"Preening code feathers",
|
||||
"Gathering digital breadcrumbs",
|
||||
"Paddling through data",
|
||||
"Migrating thoughts",
|
||||
"Nesting ideas",
|
||||
"Squawking calculations",
|
||||
"Ruffling algorithmic feathers",
|
||||
"Pecking at problems",
|
||||
"Stretching webbed feet",
|
||||
"Foraging for solutions",
|
||||
"Grooming syntax",
|
||||
"Building digital nest",
|
||||
"Patrolling the codebase",
|
||||
"Gosling about",
|
||||
"Strutting with purpose",
|
||||
"Diving for answers",
|
||||
"Herding bytes",
|
||||
"Molting old code",
|
||||
"Swimming through streams",
|
||||
"Synchronizing flock algorithms",
|
||||
"Navigating code marshes",
|
||||
"Incubating brilliant ideas",
|
||||
"Arranging feathers recursively",
|
||||
"Gliding through branches",
|
||||
"Migrating to better solutions",
|
||||
"Nesting functions carefully",
|
||||
"Hatching clever solutions",
|
||||
"Preening parse trees",
|
||||
"Flying through functions",
|
||||
"Gathering syntax seeds",
|
||||
"Webbing connections",
|
||||
"Flocking to optimizations",
|
||||
"Paddling through protocols",
|
||||
"Honking success signals",
|
||||
"Waddling through workflows",
|
||||
"Nesting in neural networks",
|
||||
"Consulting the digital oracle",
|
||||
"Summoning binary spirits",
|
||||
"Reticulating splines",
|
||||
"Calculating meaning of life",
|
||||
"Traversing neural pathways",
|
||||
"Untangling spaghetti code",
|
||||
"Mining thought gems",
|
||||
"Defragmenting brain bits",
|
||||
"Compiling wisdom",
|
||||
"Debugging reality",
|
||||
"Optimizing thought processes",
|
||||
"Scanning parallel universes",
|
||||
"Reorganizing bits and bytes",
|
||||
"Calibrating neural networks",
|
||||
"Charging creativity cells",
|
||||
"Indexing imagination",
|
||||
"Parsing possibilities",
|
||||
"Buffering brilliance",
|
||||
"Loading clever responses",
|
||||
"Generating witty remarks",
|
||||
"Synthesizing solutions",
|
||||
"Applying machine learning",
|
||||
"Calculating quantum states",
|
||||
"Analyzing algorithms",
|
||||
"Decoding human intent",
|
||||
"Exploring solution space",
|
||||
"Gathering computational momentum",
|
||||
"Initializing clever mode",
|
||||
"Juggling variables",
|
||||
"Knitting neural networks",
|
||||
"Learning at light speed",
|
||||
"Navigating knowledge graphs",
|
||||
"Orchestrating outputs",
|
||||
"Pondering possibilities",
|
||||
"Reading between the lines",
|
||||
"Searching solution space",
|
||||
"Training thought vectors",
|
||||
"Unfolding understanding",
|
||||
"Validating variables",
|
||||
"Weaving wisdom web",
|
||||
"Yielding insights",
|
||||
"Zooming through zettabytes",
|
||||
"Baking fresh ideas",
|
||||
"Charging creativity crystals",
|
||||
"Dancing with data",
|
||||
"Enchanting electrons",
|
||||
"Folding thought origami",
|
||||
"Growing solution trees",
|
||||
"Harmonizing heuristics",
|
||||
"Inspiring innovations",
|
||||
"Jazzing up algorithms",
|
||||
"Kindling knowledge",
|
||||
"Levitating logic gates",
|
||||
"Manifesting solutions",
|
||||
"Nurturing neural nets",
|
||||
"Optimizing outcomes",
|
||||
"Painting with pixels",
|
||||
"Questioning bits",
|
||||
"Recycling random thoughts",
|
||||
"Serenading semiconductors",
|
||||
"Taming tensors",
|
||||
"Unlocking understanding",
|
||||
"Visualizing vectors",
|
||||
"Wrangling widgets",
|
||||
"Yodeling yaml",
|
||||
"Aligning artificial awarenesses",
|
||||
"Bootstrapping brain bytes",
|
||||
"Contemplating code conundrums",
|
||||
"Distilling digital dreams",
|
||||
"Energizing electron engines",
|
||||
"Fabricating future frameworks",
|
||||
"Generating genius guidelines",
|
||||
"Harmonizing hardware helpers",
|
||||
"Illuminating input insights",
|
||||
"Kindling knowledge kernels",
|
||||
"Linking logical lattices",
|
||||
"Materializing memory maps",
|
||||
"Navigating neural nodes",
|
||||
"Orchestrating output oracles",
|
||||
"Pioneering program paths",
|
||||
"Quantifying quantum queries",
|
||||
"Refactoring reality routines",
|
||||
"Synchronizing system states",
|
||||
"Transforming thought threads",
|
||||
"Unifying understanding units",
|
||||
"Vectorizing virtual visions",
|
||||
"Weaving wisdom wavelengths",
|
||||
"Yielding yaml yearnings",
|
||||
"Brewing binary brilliance",
|
||||
"Crafting code crystals",
|
||||
"Designing data dreams",
|
||||
"Encoding ethereal elements",
|
||||
"Filtering function flows",
|
||||
"Gathering gigabyte galaxies",
|
||||
"Hashing hope hypotheses",
|
||||
"Igniting innovation ions",
|
||||
"Joining joy journals",
|
||||
"Knitting knowledge knots",
|
||||
"Launching logic loops",
|
||||
"Merging memory matrices",
|
||||
"Nourishing neural networks",
|
||||
"Ordering output orbits",
|
||||
"Processing pattern particles",
|
||||
"Rendering reality rays",
|
||||
"Streaming syntax stars",
|
||||
"Threading thought theories",
|
||||
"Updating understanding units",
|
||||
"Validating virtual vectors",
|
||||
"Warming wisdom waves",
|
||||
"Examining electron echoes",
|
||||
"Yoking yesterday yields",
|
||||
"Assembling algorithm arrays",
|
||||
"Balancing binary bridges",
|
||||
"Calculating cosmic codes",
|
||||
"Debugging dream drivers",
|
||||
"Encrypting ethereal edges",
|
||||
"Formatting future frames",
|
||||
"Growing gradient gardens",
|
||||
"Harvesting hash harmonies",
|
||||
"Importing insight ions",
|
||||
"Keeping kernel keys",
|
||||
"Linking lambda loops",
|
||||
"Mapping memory mazes",
|
||||
"Normalizing neural nodes",
|
||||
"Organizing output oceans",
|
||||
"Parsing pattern paths",
|
||||
"Sampling syntax streams",
|
||||
"Testing thought threads",
|
||||
"Validating virtual vectors",
|
||||
"Examining electron echoes",
|
||||
"Accelerating abstract algebras",
|
||||
"Buffering binary bubbles",
|
||||
"Caching cosmic calculations",
|
||||
"Deploying digital dreams",
|
||||
"Evolving ethereal entities",
|
||||
"Calculating response probabilities",
|
||||
"Updating knowledge graphs",
|
||||
"Processing neural feedback",
|
||||
"Exploring decision trees",
|
||||
"Measuring semantic distance",
|
||||
"Connecting synaptic pathways",
|
||||
"Evaluating response options",
|
||||
"Scanning memory banks",
|
||||
"Simulating future outcomes",
|
||||
"Adjusting confidence weights",
|
||||
"Mapping context vectors",
|
||||
"Balancing response parameters",
|
||||
"Running inference engines",
|
||||
"Optimizing memory usage",
|
||||
"Merging knowledge streams",
|
||||
"Calibrating response tone",
|
||||
"Analyzing input patterns",
|
||||
"Processing feedback loops",
|
||||
"Measuring response quality",
|
||||
"Scanning information matrices",
|
||||
"Processing user intent",
|
||||
"Measuring response coherence",
|
||||
"Exploring solution paths",
|
||||
"Processing context clues",
|
||||
"Scanning memory circuits",
|
||||
"Building response chains",
|
||||
"Analyzing conversation flow",
|
||||
"Processing temporal data",
|
||||
"Exploring concept spaces",
|
||||
"Processing memory streams",
|
||||
"Evaluating logical paths",
|
||||
"Building thought graphs",
|
||||
"Scanning neural pathways",
|
||||
];
|
||||
|
||||
/// Returns a random thinking message from the extended list
|
||||
pub fn get_random_thinking_message() -> &'static str {
|
||||
THINKING_MESSAGES
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap_or(&THINKING_MESSAGES[0])
|
||||
}
|
||||
Reference in New Issue
Block a user