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:
2025-12-02 17:24:14 +01:00
parent 09c8c9d83e
commit 10c8e2baae
67 changed files with 11444 additions and 626 deletions

View File

@@ -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::*;