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

@@ -56,17 +56,38 @@ pub enum HookResult {
Deny,
}
/// A registered hook that can be executed
#[derive(Debug, Clone)]
struct Hook {
event: String, // Event name like "PreToolUse", "PostToolUse", etc.
command: String,
pattern: Option<String>, // Optional regex pattern for matching tool names
timeout: Option<u64>,
}
pub struct HookManager {
project_root: PathBuf,
hooks: Vec<Hook>,
}
impl HookManager {
pub fn new(project_root: &str) -> Self {
Self {
project_root: PathBuf::from(project_root),
hooks: Vec::new(),
}
}
/// Register a single hook
pub fn register_hook(&mut self, event: String, command: String, pattern: Option<String>, timeout: Option<u64>) {
self.hooks.push(Hook {
event,
command,
pattern,
timeout,
});
}
/// Execute a hook for the given event
///
/// Returns:
@@ -74,18 +95,66 @@ impl HookManager {
/// - Ok(HookResult::Deny) if hook denies (exit code 2)
/// - Err if hook fails (other exit codes) or times out
pub async fn execute(&self, event: &HookEvent, timeout_ms: Option<u64>) -> Result<HookResult> {
// First check for legacy file-based hooks
let hook_path = self.get_hook_path(event);
let has_file_hook = hook_path.exists();
// If hook doesn't exist, allow by default
if !hook_path.exists() {
// Get registered hooks for this event
let event_name = event.hook_name();
let mut matching_hooks: Vec<&Hook> = self.hooks.iter()
.filter(|h| h.event == event_name)
.collect();
// If we need to filter by pattern (for PreToolUse events)
if let HookEvent::PreToolUse { tool, .. } = event {
matching_hooks.retain(|h| {
if let Some(pattern) = &h.pattern {
// Use regex to match tool name
if let Ok(re) = regex::Regex::new(pattern) {
re.is_match(tool)
} else {
false
}
} else {
true // No pattern means match all
}
});
}
// If no hooks at all, allow by default
if !has_file_hook && matching_hooks.is_empty() {
return Ok(HookResult::Allow);
}
// Execute file-based hook first (if exists)
if has_file_hook {
let result = self.execute_hook_command(&hook_path.to_string_lossy(), event, timeout_ms).await?;
if result == HookResult::Deny {
return Ok(HookResult::Deny);
}
}
// Execute registered hooks
for hook in matching_hooks {
let hook_timeout = hook.timeout.or(timeout_ms);
let result = self.execute_hook_command(&hook.command, event, hook_timeout).await?;
if result == HookResult::Deny {
return Ok(HookResult::Deny);
}
}
Ok(HookResult::Allow)
}
/// Execute a single hook command
async fn execute_hook_command(&self, command: &str, event: &HookEvent, timeout_ms: Option<u64>) -> Result<HookResult> {
// Serialize event to JSON
let input_json = serde_json::to_string(event)?;
// Spawn the hook process
let mut child = Command::new(&hook_path)
let mut child = Command::new("sh")
.arg("-c")
.arg(command)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())