diff --git a/crates/app/cli/src/main.rs b/crates/app/cli/src/main.rs index 75e52c6..2d56753 100644 --- a/crates/app/cli/src/main.rs +++ b/crates/app/cli/src/main.rs @@ -458,26 +458,131 @@ async fn main() -> Result<()> { if args.prompt.is_empty() { println!("šŸ¤– Owlen Interactive Mode"); println!("Model: {}", opts.model); - println!("Type your message and press Enter. Press Ctrl+C to exit.\n"); + println!("Mode: {:?}", settings.mode); + println!("Type your message or /help for commands. Press Ctrl+C to exit.\n"); use std::io::{stdin, BufRead}; let stdin = stdin(); let mut lines = stdin.lock().lines(); + let mut stats = agent_core::SessionStats::new(); + let mut history = agent_core::SessionHistory::new(); loop { - print!("\n> "); + print!("> "); std::io::stdout().flush().ok(); if let Some(Ok(line)) = lines.next() { - let prompt = line.trim(); - if prompt.is_empty() { + let input = line.trim(); + if input.is_empty() { continue; } - // Run agent loop for this prompt - match agent_core::run_agent_loop(&client, prompt, &opts, &perms).await { + // Handle slash commands + if input.starts_with('/') { + match input { + "/help" => { + println!("\nšŸ“– Available Commands:"); + println!(" /help - Show this help message"); + println!(" /status - Show session status"); + println!(" /permissions - Show permission settings"); + println!(" /cost - Show token usage and timing"); + println!(" /history - Show conversation history"); + println!(" /clear - Clear conversation history"); + println!(" /exit - Exit interactive mode"); + } + "/status" => { + println!("\nšŸ“Š Session Status:"); + println!(" Model: {}", opts.model); + println!(" Mode: {:?}", settings.mode); + println!(" Messages: {}", stats.total_messages); + println!(" Tools: {} calls", stats.total_tool_calls); + let elapsed = stats.start_time.elapsed().unwrap_or_default(); + println!(" Uptime: {}", agent_core::SessionStats::format_duration(elapsed)); + } + "/permissions" => { + println!("\nšŸ”’ Permission Settings:"); + println!(" Mode: {:?}", perms.mode()); + println!("\n Read-only tools: Read, Grep, Glob, NotebookRead"); + match perms.mode() { + permissions::Mode::Plan => { + println!(" āœ… Allowed (plan mode)"); + println!("\n Write tools: Write, Edit, NotebookEdit"); + println!(" ā“ Ask permission"); + println!("\n System tools: Bash"); + println!(" ā“ Ask permission"); + } + permissions::Mode::AcceptEdits => { + println!(" āœ… Allowed"); + println!("\n Write tools: Write, Edit, NotebookEdit"); + println!(" āœ… Allowed (acceptEdits mode)"); + println!("\n System tools: Bash"); + println!(" ā“ Ask permission"); + } + permissions::Mode::Code => { + println!(" āœ… Allowed"); + println!("\n Write tools: Write, Edit, NotebookEdit"); + println!(" āœ… Allowed (code mode)"); + println!("\n System tools: Bash"); + println!(" āœ… Allowed (code mode)"); + } + } + } + "/cost" => { + println!("\nšŸ’° Token Usage & Timing:"); + println!(" Est. Tokens: ~{}", stats.estimated_tokens); + println!(" Total Time: {}", agent_core::SessionStats::format_duration(stats.total_duration)); + if stats.total_messages > 0 { + let avg_time = stats.total_duration / stats.total_messages as u32; + println!(" Avg/Message: {}", agent_core::SessionStats::format_duration(avg_time)); + } + println!("\n Note: Ollama is free - no cost incurred!"); + } + "/history" => { + println!("\nšŸ“œ Conversation History:"); + if history.user_prompts.is_empty() { + println!(" (No messages yet)"); + } else { + for (i, (user, assistant)) in history.user_prompts.iter() + .zip(history.assistant_responses.iter()).enumerate() { + println!("\n [{}] User: {}", i + 1, user); + println!(" Assistant: {}...", + assistant.chars().take(100).collect::()); + } + } + if !history.tool_calls.is_empty() { + println!("\n Tool Calls: {}", history.tool_calls.len()); + } + } + "/clear" => { + history.clear(); + stats = agent_core::SessionStats::new(); + println!("\nšŸ—‘ļø Session history cleared!"); + } + "/exit" => { + println!("\nšŸ‘‹ Goodbye!"); + break; + } + _ => { + println!("\nāŒ Unknown command: {}", input); + println!(" Type /help for available commands"); + } + } + continue; + } + + // Regular message - run through agent loop + history.add_user_message(input.to_string()); + let start = SystemTime::now(); + + match agent_core::run_agent_loop(&client, input, &opts, &perms).await { Ok(response) => { println!("\n{}", response); + history.add_assistant_message(response.clone()); + + // Update stats + let duration = start.elapsed().unwrap_or_default(); + let tokens = (input.len() + response.len()) / 4; // Rough estimate + stats.record_message(tokens, duration); } Err(e) => { eprintln!("\nāŒ Error: {}", e); diff --git a/crates/core/agent/src/lib.rs b/crates/core/agent/src/lib.rs index 43ebb64..06b5057 100644 --- a/crates/core/agent/src/lib.rs +++ b/crates/core/agent/src/lib.rs @@ -1,9 +1,13 @@ +pub mod session; + use color_eyre::eyre::{Result, eyre}; use futures_util::TryStreamExt; use llm_ollama::{ChatMessage, OllamaClient, OllamaOptions, Tool, ToolFunction, ToolParameters}; use permissions::{PermissionDecision, PermissionManager, Tool as PermTool}; use serde_json::{json, Value}; +pub use session::{SessionStats, SessionHistory, ToolCallRecord}; + /// Define all available tools for the LLM pub fn get_tool_definitions() -> Vec { vec![ diff --git a/crates/core/agent/src/session.rs b/crates/core/agent/src/session.rs new file mode 100644 index 0000000..7f894b1 --- /dev/null +++ b/crates/core/agent/src/session.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionStats { + pub start_time: SystemTime, + pub total_messages: usize, + pub total_tool_calls: usize, + pub total_duration: Duration, + pub estimated_tokens: usize, +} + +impl SessionStats { + pub fn new() -> Self { + Self { + start_time: SystemTime::now(), + total_messages: 0, + total_tool_calls: 0, + total_duration: Duration::ZERO, + estimated_tokens: 0, + } + } + + pub fn record_message(&mut self, tokens: usize, duration: Duration) { + self.total_messages += 1; + self.estimated_tokens += tokens; + self.total_duration += duration; + } + + pub fn record_tool_call(&mut self) { + self.total_tool_calls += 1; + } + + pub fn format_duration(d: Duration) -> String { + let secs = d.as_secs(); + if secs < 60 { + format!("{}s", secs) + } else if secs < 3600 { + format!("{}m {}s", secs / 60, secs % 60) + } else { + format!("{}h {}m", secs / 3600, (secs % 3600) / 60) + } + } +} + +impl Default for SessionStats { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone)] +pub struct SessionHistory { + pub user_prompts: Vec, + pub assistant_responses: Vec, + pub tool_calls: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolCallRecord { + pub tool_name: String, + pub arguments: String, + pub result: String, + pub success: bool, +} + +impl SessionHistory { + pub fn new() -> Self { + Self { + user_prompts: Vec::new(), + assistant_responses: Vec::new(), + tool_calls: Vec::new(), + } + } + + pub fn add_user_message(&mut self, message: String) { + self.user_prompts.push(message); + } + + pub fn add_assistant_message(&mut self, message: String) { + self.assistant_responses.push(message); + } + + pub fn add_tool_call(&mut self, record: ToolCallRecord) { + self.tool_calls.push(record); + } + + pub fn clear(&mut self) { + self.user_prompts.clear(); + self.assistant_responses.clear(); + self.tool_calls.clear(); + } +} + +impl Default for SessionHistory { + fn default() -> Self { + Self::new() + } +}