mirror of https://github.com/buster-so/buster.git
Merge branch 'evals' of https://github.com/buster-so/buster into evals
This commit is contained in:
commit
ab61b21f48
|
@ -13,13 +13,15 @@ use std::time::Instant;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
// --- Agent Imports ---
|
// --- Agent Imports ---
|
||||||
use agents::{AgentError, AgentExt, AgentThread, BusterCliAgent};
|
use agents::{AgentError, AgentExt, AgentThread, BusterCliAgent};
|
||||||
|
|
||||||
// Ratatui / Crossterm related imports
|
// Ratatui / Crossterm related imports
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
|
||||||
|
MouseEventKind},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
|
@ -105,6 +107,15 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> {
|
||||||
.map(|p| p.display().to_string())
|
.map(|p| p.display().to_string())
|
||||||
.unwrap_or_else(|_| "<unknown>".to_string());
|
.unwrap_or_else(|_| "<unknown>".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 ---
|
// --- Get Credentials ---
|
||||||
let (base_url, api_key) = get_api_credentials(&args)?;
|
let (base_url, api_key) = get_api_credentials(&args)?;
|
||||||
if api_key.is_none() {
|
if api_key.is_none() {
|
||||||
|
@ -177,69 +188,83 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> {
|
||||||
.unwrap_or_else(|| std::time::Duration::from_secs(0));
|
.unwrap_or_else(|| std::time::Duration::from_secs(0));
|
||||||
|
|
||||||
if crossterm::event::poll(timeout)? {
|
if crossterm::event::poll(timeout)? {
|
||||||
if let Event::Key(key) = event::read()? {
|
match event::read()? {
|
||||||
if key.kind == KeyEventKind::Press {
|
Event::Key(key) => {
|
||||||
// Quit handlers
|
if key.kind == KeyEventKind::Press {
|
||||||
if key.modifiers.contains(event::KeyModifiers::CONTROL)
|
// Quit handlers
|
||||||
&& key.code == KeyCode::Char('c')
|
if key.modifiers.contains(event::KeyModifiers::CONTROL)
|
||||||
{
|
&& key.code == KeyCode::Char('c')
|
||||||
app_state.should_quit = true;
|
{
|
||||||
continue;
|
app_state.should_quit = true;
|
||||||
}
|
continue;
|
||||||
if key.code == KeyCode::Esc {
|
}
|
||||||
app_state.should_quit = true;
|
if key.code == KeyCode::Esc {
|
||||||
continue;
|
app_state.should_quit = true;
|
||||||
}
|
continue;
|
||||||
if app_state.input == "/exit" && key.code == KeyCode::Enter {
|
}
|
||||||
app_state.should_quit = true;
|
if app_state.input == "/exit" && key.code == KeyCode::Enter {
|
||||||
continue;
|
app_state.should_quit = true;
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if input allowed
|
// Check if input allowed
|
||||||
let can_input =
|
let can_input =
|
||||||
!app_state.is_agent_processing && app_state.active_tool_calls.is_empty();
|
!app_state.is_agent_processing && app_state.active_tool_calls.is_empty();
|
||||||
|
|
||||||
if can_input {
|
if can_input {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
// Submit message & trigger agent processing
|
// Submit message & trigger agent processing
|
||||||
app_state.submit_message();
|
app_state.submit_message();
|
||||||
|
|
||||||
// Get the agent receiver immediately
|
// Get the agent receiver immediately
|
||||||
// The agent's run method should quickly setup the stream
|
// The agent's run method should quickly setup the stream
|
||||||
// and return the receiver before heavy processing.
|
// and return the receiver before heavy processing.
|
||||||
match cli_agent.run(&mut app_state.agent_thread, &cwd).await {
|
match cli_agent.run(&mut app_state.agent_thread, &cwd).await {
|
||||||
Ok(rx) => {
|
Ok(rx) => {
|
||||||
agent_rx = Some(rx); // Start polling this receiver
|
agent_rx = Some(rx); // Start polling this receiver
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Report error via AppState
|
// Report error via AppState
|
||||||
app_state.process_agent_message(Err(AgentError(format!(
|
app_state.process_agent_message(Err(AgentError(format!(
|
||||||
"Failed to start agent: {}",
|
"Failed to start agent: {}",
|
||||||
e
|
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) => {
|
} else {
|
||||||
app_state.input.push(c);
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,26 @@
|
||||||
use agents::{AgentError, AgentThread};
|
use agents::{AgentError, AgentThread};
|
||||||
use litellm::{AgentMessage, MessageProgress, ToolCall};
|
use litellm::{AgentMessage, MessageProgress, ToolCall};
|
||||||
use uuid::Uuid;
|
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<u64>,
|
||||||
|
// modified_at: Option<String>, // Ignoring for now
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct ListDirectoryResult {
|
||||||
|
entries: Vec<ListDirectoryEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
// --- Application State Structs ---
|
// --- Application State Structs ---
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ActiveToolCall {
|
pub struct ActiveToolCall {
|
||||||
|
@ -195,10 +215,17 @@ impl AppState {
|
||||||
|
|
||||||
for tc in calls {
|
for tc in calls {
|
||||||
if !existing_tool_ids.contains(&tc.id) {
|
if !existing_tool_ids.contains(&tc.id) {
|
||||||
|
// Format arguments nicely
|
||||||
|
let args_json = serde_json::to_string_pretty(&tc.function.arguments)
|
||||||
|
.unwrap_or_else(|_| "<failed to format args>".to_string());
|
||||||
self.messages.push(AgentMessage::Developer {
|
self.messages.push(AgentMessage::Developer {
|
||||||
id: Some(tc.id.clone()),
|
id: Some(tc.id.clone()),
|
||||||
content: format!("Executing: {}...", tc.function.name),
|
content: format!(
|
||||||
name: Some("Tool".to_string()),
|
"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 {
|
for tc in calls {
|
||||||
if !existing_tool_ids.contains(&tc.id) {
|
if !existing_tool_ids.contains(&tc.id) {
|
||||||
|
// Format arguments nicely
|
||||||
|
let args_json = serde_json::to_string_pretty(&tc.function.arguments)
|
||||||
|
.unwrap_or_else(|_| "<failed to format args>".to_string());
|
||||||
self.messages.push(AgentMessage::Developer {
|
self.messages.push(AgentMessage::Developer {
|
||||||
id: Some(tc.id.clone()),
|
id: Some(tc.id.clone()),
|
||||||
content: format!("Executing: {}...", tc.function.name), // Or maybe "Called:"?
|
content: format!(
|
||||||
name: Some("Tool".to_string()),
|
"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
|
// Update the placeholder message in the main history
|
||||||
for msg in self.messages.iter_mut() {
|
for msg in self.messages.iter_mut() {
|
||||||
if let AgentMessage::Developer {
|
if let AgentMessage::Developer { id: msg_id, content: msg_content, name: msg_name, .. } = msg {
|
||||||
id: msg_id,
|
|
||||||
content: msg_content,
|
|
||||||
..
|
|
||||||
} = msg
|
|
||||||
{
|
|
||||||
if msg_id.as_ref() == Some(&tool_call_id) {
|
if msg_id.as_ref() == Some(&tool_call_id) {
|
||||||
|
*msg_name = Some(format!("{} Result", name)); // Update name to indicate result
|
||||||
*msg_content = match progress {
|
*msg_content = match progress {
|
||||||
MessageProgress::InProgress => format!("Running {}: {}...", name, content), // Show partial content?
|
MessageProgress::InProgress => {
|
||||||
MessageProgress::Complete => format!("Result ({}): {}", name, content),
|
// 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::<ListDirectoryResult>(&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;
|
found_message = true;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -85,7 +85,9 @@ fn render_welcome(frame: &mut Frame, area: Rect, cwd: &str) {
|
||||||
fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) {
|
fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||||
use litellm::{AgentMessage, MessageProgress};
|
use litellm::{AgentMessage, MessageProgress};
|
||||||
let mut message_lines: Vec<Line> = Vec::new();
|
let mut message_lines: Vec<Line> = 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 {
|
match msg {
|
||||||
AgentMessage::User { content, .. } => {
|
AgentMessage::User { content, .. } => {
|
||||||
message_lines.push(Line::from(vec![
|
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, .. } => {
|
AgentMessage::Assistant { content, progress, name, .. } => {
|
||||||
let is_in_progress = *progress == MessageProgress::InProgress;
|
let is_in_progress = *progress == MessageProgress::InProgress;
|
||||||
let prefix = Span::styled(
|
let is_last_message = msg_index == num_messages - 1;
|
||||||
format!(
|
let prev_msg_was_tool_result = if msg_index > 0 {
|
||||||
"{} {}: ",
|
matches!(app.messages.get(msg_index - 1), Some(AgentMessage::Developer { .. }))
|
||||||
if is_in_progress { "…" } else { "•" },
|
} else {
|
||||||
name.as_deref().unwrap_or("Assistant")
|
false
|
||||||
),
|
};
|
||||||
Style::default()
|
|
||||||
.fg(Color::Cyan)
|
// Omit prefix if it's the last message AND the previous was a tool result
|
||||||
.add_modifier(Modifier::BOLD),
|
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 {
|
if let Some(c) = content {
|
||||||
let lines: Vec<&str> = c.split('\n').collect();
|
let lines: Vec<&str> = c.split('\n').collect();
|
||||||
for (i, line_content) in lines.iter().enumerate() {
|
for (i, line_content) in lines.iter().enumerate() {
|
||||||
let line_span = Span::styled(*line_content, Style::default().fg(Color::White));
|
let line_span = Span::styled(*line_content, Style::default().fg(Color::White));
|
||||||
if i == 0 {
|
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 {
|
} else {
|
||||||
|
// Indent subsequent lines (relative to prefix or start)
|
||||||
|
let indent = if prefix.is_some() { " " } else { "" };
|
||||||
message_lines.push(Line::from(vec![
|
message_lines.push(Line::from(vec![
|
||||||
Span::raw(" "),
|
Span::raw(indent),
|
||||||
line_span,
|
line_span,
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Some(p) = prefix {
|
||||||
message_lines.push(Line::from(prefix));
|
// Show prefix even if content is None (e.g., for InProgress)
|
||||||
|
message_lines.push(Line::from(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_in_progress {
|
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 content_height = message_lines.len() as u16;
|
||||||
let view_height = area.height;
|
let view_height = area.height;
|
||||||
let max_scroll = content_height.saturating_sub(view_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)
|
let messages_widget = Paragraph::new(message_lines)
|
||||||
.scroll((current_scroll, 0))
|
.scroll((current_scroll, 0))
|
||||||
|
@ -247,7 +275,7 @@ fn render_input(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||||
// --- Cursor ---
|
// --- Cursor ---
|
||||||
if !is_input_disabled {
|
if !is_input_disabled {
|
||||||
frame.set_cursor(
|
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,
|
area.y + 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue