feat(v2): complete multi-LLM providers, TUI redesign, and advanced agent features
Multi-LLM Provider Support: - Add llm-core crate with LlmProvider trait abstraction - Implement Anthropic Claude API client with streaming - Implement OpenAI API client with streaming - Add token counting with SimpleTokenCounter and ClaudeTokenCounter - Add retry logic with exponential backoff and jitter Borderless TUI Redesign: - Rewrite theme system with terminal capability detection (Full/Unicode256/Basic) - Add provider tabs component with keybind switching [1]/[2]/[3] - Implement vim-modal input (Normal/Insert/Visual/Command modes) - Redesign chat panel with timestamps and streaming indicators - Add multi-provider status bar with cost tracking - Add Nerd Font icons with graceful ASCII fallbacks - Add syntax highlighting (syntect) and markdown rendering (pulldown-cmark) Advanced Agent Features: - Add system prompt builder with configurable components - Enhance subagent orchestration with parallel execution - Add git integration module for safe command detection - Add streaming tool results via channels - Expand tool set: AskUserQuestion, TodoWrite, LS, MultiEdit, BashOutput, KillShell - Add WebSearch with provider abstraction Plugin System Enhancement: - Add full agent definition parsing from YAML frontmatter - Add skill system with progressive disclosure - Wire plugin hooks into HookManager 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use std::collections::HashMap;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::Mutex;
|
||||
@@ -19,6 +22,7 @@ pub struct CommandOutput {
|
||||
|
||||
pub struct BashSession {
|
||||
child: Mutex<Child>,
|
||||
last_output: Option<String>,
|
||||
}
|
||||
|
||||
impl BashSession {
|
||||
@@ -40,6 +44,7 @@ impl BashSession {
|
||||
|
||||
Ok(Self {
|
||||
child: Mutex::new(child),
|
||||
last_output: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,7 +59,13 @@ impl BashSession {
|
||||
let result = timeout(timeout_duration, self.execute_internal(command)).await;
|
||||
|
||||
match result {
|
||||
Ok(output) => output,
|
||||
Ok(output) => {
|
||||
// Store the output for potential retrieval via BashOutput tool
|
||||
let combined = format!("{}{}", output.as_ref().map(|o| o.stdout.as_str()).unwrap_or(""),
|
||||
output.as_ref().map(|o| o.stderr.as_str()).unwrap_or(""));
|
||||
self.last_output = Some(combined);
|
||||
output
|
||||
},
|
||||
Err(_) => Err(eyre!("Command timed out after {}ms", timeout_duration.as_millis())),
|
||||
}
|
||||
}
|
||||
@@ -158,6 +169,106 @@ impl BashSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages background bash shells by ID
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ShellManager {
|
||||
shells: Arc<RwLock<HashMap<String, BashSession>>>,
|
||||
}
|
||||
|
||||
impl ShellManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Start a new background shell, returns shell ID
|
||||
pub async fn start_shell(&self) -> Result<String> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let session = BashSession::new().await?;
|
||||
self.shells.write().insert(id.clone(), session);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Execute command in background shell
|
||||
pub async fn execute(&self, shell_id: &str, command: &str, timeout: Option<Duration>) -> Result<CommandOutput> {
|
||||
// We need to handle this carefully to avoid holding the lock across await
|
||||
// First check if the shell exists and clone what we need
|
||||
let exists = self.shells.read().contains_key(shell_id);
|
||||
if !exists {
|
||||
return Err(eyre!("Shell not found: {}", shell_id));
|
||||
}
|
||||
|
||||
// For now, we need to use a more complex approach since BashSession contains async operations
|
||||
// We'll execute and then update in a separate critical section
|
||||
let timeout_ms = timeout.map(|d| d.as_millis() as u64);
|
||||
|
||||
// Take temporary ownership for execution
|
||||
let mut session = {
|
||||
let mut shells = self.shells.write();
|
||||
shells.remove(shell_id)
|
||||
.ok_or_else(|| eyre!("Shell not found: {}", shell_id))?
|
||||
};
|
||||
|
||||
// Execute without holding the lock
|
||||
let result = session.execute(command, timeout_ms).await;
|
||||
|
||||
// Put the session back
|
||||
self.shells.write().insert(shell_id.to_string(), session);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Get output from a shell (BashOutput tool)
|
||||
pub fn get_output(&self, shell_id: &str) -> Result<Option<String>> {
|
||||
let shells = self.shells.read();
|
||||
let session = shells.get(shell_id)
|
||||
.ok_or_else(|| eyre!("Shell not found: {}", shell_id))?;
|
||||
// Return any buffered output
|
||||
Ok(session.last_output.clone())
|
||||
}
|
||||
|
||||
/// Kill a shell (KillShell tool)
|
||||
pub fn kill_shell(&self, shell_id: &str) -> Result<()> {
|
||||
let mut shells = self.shells.write();
|
||||
if shells.remove(shell_id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(eyre!("Shell not found: {}", shell_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// List active shells
|
||||
pub fn list_shells(&self) -> Vec<String> {
|
||||
self.shells.read().keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a background bash command, returns shell ID
|
||||
pub async fn run_background(manager: &ShellManager, command: &str) -> Result<String> {
|
||||
let shell_id = manager.start_shell().await?;
|
||||
// Execute in background (non-blocking)
|
||||
tokio::spawn({
|
||||
let manager = manager.clone();
|
||||
let command = command.to_string();
|
||||
let shell_id = shell_id.clone();
|
||||
async move {
|
||||
let _ = manager.execute(&shell_id, &command, None).await;
|
||||
}
|
||||
});
|
||||
Ok(shell_id)
|
||||
}
|
||||
|
||||
/// Get output from background shell (BashOutput tool)
|
||||
pub fn bash_output(manager: &ShellManager, shell_id: &str) -> Result<String> {
|
||||
manager.get_output(shell_id)?
|
||||
.ok_or_else(|| eyre!("No output available"))
|
||||
}
|
||||
|
||||
/// Kill a background shell (KillShell tool)
|
||||
pub fn kill_shell(manager: &ShellManager, shell_id: &str) -> Result<String> {
|
||||
manager.kill_shell(shell_id)?;
|
||||
Ok(format!("Shell {} terminated", shell_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user