From 7265067537f32186b78af15821ac2321dd4407f3 Mon Sep 17 00:00:00 2001 From: dal Date: Thu, 10 Apr 2025 11:46:40 -0600 Subject: [PATCH 1/2] scroll. --- cli/cli/src/commands/chat/logic.rs | 125 ++++++++++++++++------------- cli/cli/src/commands/chat/state.rs | 79 +++++++++++++++--- cli/cli/src/commands/chat/ui.rs | 62 ++++++++++---- 3 files changed, 182 insertions(+), 84 deletions(-) diff --git a/cli/cli/src/commands/chat/logic.rs b/cli/cli/src/commands/chat/logic.rs index 72246adda..c320b79fa 100644 --- a/cli/cli/src/commands/chat/logic.rs +++ b/cli/cli/src/commands/chat/logic.rs @@ -19,7 +19,8 @@ use agents::{AgentError, AgentExt, AgentThread, BusterCliAgent}; // Ratatui / Crossterm related imports use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, + MouseEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -177,69 +178,83 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> { .unwrap_or_else(|| std::time::Duration::from_secs(0)); if crossterm::event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - // Quit handlers - if key.modifiers.contains(event::KeyModifiers::CONTROL) - && key.code == KeyCode::Char('c') - { - app_state.should_quit = true; - continue; - } - if key.code == KeyCode::Esc { - app_state.should_quit = true; - continue; - } - if app_state.input == "/exit" && key.code == KeyCode::Enter { - app_state.should_quit = true; - continue; - } + match event::read()? { + Event::Key(key) => { + if key.kind == KeyEventKind::Press { + // Quit handlers + if key.modifiers.contains(event::KeyModifiers::CONTROL) + && key.code == KeyCode::Char('c') + { + app_state.should_quit = true; + continue; + } + if key.code == KeyCode::Esc { + app_state.should_quit = true; + continue; + } + if app_state.input == "/exit" && key.code == KeyCode::Enter { + app_state.should_quit = true; + continue; + } - // Check if input allowed - let can_input = - !app_state.is_agent_processing && app_state.active_tool_calls.is_empty(); + // Check if input allowed + let can_input = + !app_state.is_agent_processing && app_state.active_tool_calls.is_empty(); - if can_input { - match key.code { - KeyCode::Enter => { - // Submit message & trigger agent processing - app_state.submit_message(); - - // Get the agent receiver immediately - // The agent's run method should quickly setup the stream - // and return the receiver before heavy processing. - match cli_agent.run(&mut app_state.agent_thread, &cwd).await { - Ok(rx) => { - agent_rx = Some(rx); // Start polling this receiver - } - Err(e) => { - // Report error via AppState - app_state.process_agent_message(Err(AgentError(format!( - "Failed to start agent: {}", - e - )))); + if can_input { + match key.code { + KeyCode::Enter => { + // Submit message & trigger agent processing + app_state.submit_message(); + + // Get the agent receiver immediately + // The agent's run method should quickly setup the stream + // and return the receiver before heavy processing. + match cli_agent.run(&mut app_state.agent_thread, &cwd).await { + Ok(rx) => { + agent_rx = Some(rx); // Start polling this receiver + } + Err(e) => { + // Report error via AppState + app_state.process_agent_message(Err(AgentError(format!( + "Failed to start agent: {}", + e + )))); + } } } + KeyCode::Char(c) => { + app_state.input.push(c); + } + KeyCode::Backspace => { + app_state.input.pop(); + } + KeyCode::Up => app_state.scroll_up(), + KeyCode::Down => app_state.scroll_down(), + _ => {} // Ignore other keys when input is enabled } - KeyCode::Char(c) => { - app_state.input.push(c); + } else { + // Handle scrolling even when input is disabled + match key.code { + KeyCode::Up => app_state.scroll_up(), + KeyCode::Down => app_state.scroll_down(), + _ => {} // Ignore other input } - KeyCode::Backspace => { - app_state.input.pop(); - } - KeyCode::Up => app_state.scroll_up(), - KeyCode::Down => app_state.scroll_down(), - _ => {} // Ignore other keys when input is enabled - } - } else { - // Handle scrolling even when input is disabled - match key.code { - KeyCode::Up => app_state.scroll_up(), - KeyCode::Down => app_state.scroll_down(), - _ => {} // Ignore other input } } } + Event::Mouse(mouse_event) => { + match mouse_event.kind { + MouseEventKind::ScrollUp => { + app_state.scroll_up(); + } + MouseEventKind::ScrollDown => { + app_state.scroll_down(); + } + _ => {} // Ignore other mouse events (clicks, moves, etc.) + } + } + _ => {} // Ignore other event types (like resize for now) } } diff --git a/cli/cli/src/commands/chat/state.rs b/cli/cli/src/commands/chat/state.rs index 8c4677988..715674d6b 100644 --- a/cli/cli/src/commands/chat/state.rs +++ b/cli/cli/src/commands/chat/state.rs @@ -1,6 +1,26 @@ use agents::{AgentError, AgentThread}; use litellm::{AgentMessage, MessageProgress, ToolCall}; use uuid::Uuid; +use std::time::Instant; +use serde_json; +use serde_json::Value; +use serde::{Deserialize, Serialize}; + +// --- Structs for specific tool results (add more as needed) --- +#[derive(Serialize, Deserialize, Debug)] +struct ListDirectoryEntry { + name: String, + path: String, + is_dir: bool, + size: Option, + // modified_at: Option, // Ignoring for now +} + +#[derive(Serialize, Deserialize, Debug)] +struct ListDirectoryResult { + entries: Vec, +} + // --- Application State Structs --- #[derive(Debug, Clone)] pub struct ActiveToolCall { @@ -195,10 +215,17 @@ impl AppState { for tc in calls { if !existing_tool_ids.contains(&tc.id) { + // Format arguments nicely + let args_json = serde_json::to_string_pretty(&tc.function.arguments) + .unwrap_or_else(|_| "".to_string()); self.messages.push(AgentMessage::Developer { id: Some(tc.id.clone()), - content: format!("Executing: {}...", tc.function.name), - name: Some("Tool".to_string()), + content: format!( + "Executing: {}\nArgs:\n{}", + tc.function.name, + args_json + ), + name: Some("Tool Call".to_string()), // Change name for clarity }); } } @@ -293,10 +320,17 @@ impl AppState { for tc in calls { if !existing_tool_ids.contains(&tc.id) { + // Format arguments nicely + let args_json = serde_json::to_string_pretty(&tc.function.arguments) + .unwrap_or_else(|_| "".to_string()); self.messages.push(AgentMessage::Developer { id: Some(tc.id.clone()), - content: format!("Executing: {}...", tc.function.name), // Or maybe "Called:"? - name: Some("Tool".to_string()), + content: format!( + "Executing: {}\nArgs:\n{}", + tc.function.name, + args_json + ), + name: Some("Tool Call".to_string()), // Change name for clarity }); } } @@ -321,16 +355,37 @@ impl AppState { // Update the placeholder message in the main history for msg in self.messages.iter_mut() { - if let AgentMessage::Developer { - id: msg_id, - content: msg_content, - .. - } = msg - { + if let AgentMessage::Developer { id: msg_id, content: msg_content, name: msg_name, .. } = msg { if msg_id.as_ref() == Some(&tool_call_id) { + *msg_name = Some(format!("{} Result", name)); // Update name to indicate result *msg_content = match progress { - MessageProgress::InProgress => format!("Running {}: {}...", name, content), // Show partial content? - MessageProgress::Complete => format!("Result ({}): {}", name, content), + MessageProgress::InProgress => { + // Try to show formatted partial content if possible, else raw + format!("Running {}: {}...", name, content) + }, + MessageProgress::Complete => { + // Attempt to parse and format known tool outputs + match name.as_str() { + "list_directory" => { + match serde_json::from_str::(&content) { + Ok(parsed_result) => { + let mut formatted = String::from("Entries:\n"); + for entry in parsed_result.entries { + formatted.push_str(&format!( + " - {} ({})\n", + entry.name, + if entry.is_dir { "directory" } else { "file" } + )); + } + formatted.trim_end().to_string() // Remove trailing newline + } + Err(_) => format!("Result ({}):\n{}", name, content), // Fallback to raw JSON on parse error + } + } + // Add more known tool formatters here + _ => format!("Result ({}):\n{}", name, content), // Default: show raw JSON + } + }, }; found_message = true; break; diff --git a/cli/cli/src/commands/chat/ui.rs b/cli/cli/src/commands/chat/ui.rs index 0d9b94f46..4f6ae3e65 100644 --- a/cli/cli/src/commands/chat/ui.rs +++ b/cli/cli/src/commands/chat/ui.rs @@ -85,7 +85,9 @@ fn render_welcome(frame: &mut Frame, area: Rect, cwd: &str) { fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) { use litellm::{AgentMessage, MessageProgress}; let mut message_lines: Vec = Vec::new(); - for msg in app.messages.iter() { + let num_messages = app.messages.len(); + + for (msg_index, msg) in app.messages.iter().enumerate() { match msg { AgentMessage::User { content, .. } => { message_lines.push(Line::from(vec![ @@ -95,32 +97,54 @@ fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) { } AgentMessage::Assistant { content, progress, name, .. } => { let is_in_progress = *progress == MessageProgress::InProgress; - let prefix = Span::styled( - format!( - "{} {}: ", - if is_in_progress { "…" } else { "•" }, - name.as_deref().unwrap_or("Assistant") - ), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ); + let is_last_message = msg_index == num_messages - 1; + let prev_msg_was_tool_result = if msg_index > 0 { + matches!(app.messages.get(msg_index - 1), Some(AgentMessage::Developer { .. })) + } else { + false + }; + + // Omit prefix if it's the last message AND the previous was a tool result + let show_prefix = !(is_last_message && prev_msg_was_tool_result && !is_in_progress); + + let prefix = if show_prefix { + Some(Span::styled( + format!( + "{} {}: ", + if is_in_progress { "…" } else { "•" }, + name.as_deref().unwrap_or("Assistant") + ), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )) + } else { + None // Don't show prefix + }; if let Some(c) = content { let lines: Vec<&str> = c.split('\n').collect(); for (i, line_content) in lines.iter().enumerate() { let line_span = Span::styled(*line_content, Style::default().fg(Color::White)); if i == 0 { - message_lines.push(Line::from(vec![prefix.clone(), line_span])); + if let Some(p) = prefix.clone() { + message_lines.push(Line::from(vec![p, line_span])); + } else { + // No prefix, just the content line + message_lines.push(Line::from(line_span)); + } } else { + // Indent subsequent lines (relative to prefix or start) + let indent = if prefix.is_some() { " " } else { "" }; message_lines.push(Line::from(vec![ - Span::raw(" "), + Span::raw(indent), line_span, ])); } } - } else { - message_lines.push(Line::from(prefix)); + } else if let Some(p) = prefix { + // Show prefix even if content is None (e.g., for InProgress) + message_lines.push(Line::from(p)); } if !is_in_progress { @@ -168,7 +192,11 @@ fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) { let content_height = message_lines.len() as u16; let view_height = area.height; let max_scroll = content_height.saturating_sub(view_height); - let current_scroll = app.scroll_offset.min(max_scroll); + let current_scroll = if app.scroll_offset == 0 { + max_scroll + } else { + app.scroll_offset.min(max_scroll) + }; let messages_widget = Paragraph::new(message_lines) .scroll((current_scroll, 0)) @@ -247,7 +275,7 @@ fn render_input(frame: &mut Frame, app: &AppState, area: Rect) { // --- Cursor --- if !is_input_disabled { frame.set_cursor( - area.x + input_prefix.len() as u16 + app.input.chars().count() as u16, + area.x + 1 + input_prefix.len() as u16 + app.input.chars().count() as u16, // +1 for left border area.y + 1, ) } From c23d682514255cc75e8ae7c1502bd5a1ae2a8503 Mon Sep 17 00:00:00 2001 From: dal Date: Thu, 10 Apr 2025 11:52:12 -0600 Subject: [PATCH 2/2] git --- cli/cli/src/commands/chat/logic.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cli/cli/src/commands/chat/logic.rs b/cli/cli/src/commands/chat/logic.rs index c320b79fa..0fbadebeb 100644 --- a/cli/cli/src/commands/chat/logic.rs +++ b/cli/cli/src/commands/chat/logic.rs @@ -13,6 +13,7 @@ use std::time::Instant; use thiserror::Error; use tokio::sync::broadcast; use uuid::Uuid; +use std::process::Command; // --- Agent Imports --- use agents::{AgentError, AgentExt, AgentThread, BusterCliAgent}; @@ -106,6 +107,15 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> { .map(|p| p.display().to_string()) .unwrap_or_else(|_| "".to_string()); + // --- Git Repository Check --- + let git_check = Command::new("git") + .args(["rev-parse", "--is-inside-work-tree"]) + .output(); // Use output to capture status and stderr/stdout if needed + + if git_check.is_err() || !git_check.unwrap().status.success() { + println!("{}", colored::Colorize::yellow("Warning: Buster operates best in a git repository.")); + } + // --- Get Credentials --- let (base_url, api_key) = get_api_credentials(&args)?; if api_key.is_none() {