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

@@ -0,0 +1,154 @@
// Integration test for plugin hooks with HookManager
use color_eyre::eyre::Result;
use hooks::{HookEvent, HookManager, HookResult};
use tempfile::TempDir;
#[tokio::test]
async fn test_register_and_execute_plugin_hooks() -> Result<()> {
// Create temporary directory to act as project root
let temp_dir = TempDir::new()?;
// Create hook manager
let mut hook_mgr = HookManager::new(temp_dir.path().to_str().unwrap());
// Register a hook that matches Edit|Write tools
hook_mgr.register_hook(
"PreToolUse".to_string(),
"echo 'Hook executed' && exit 0".to_string(),
Some("Edit|Write".to_string()),
Some(5000),
);
// Test that the hook executes for Edit tool
let event = HookEvent::PreToolUse {
tool: "Edit".to_string(),
args: serde_json::json!({"path": "/tmp/test.txt"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Allow);
// Test that the hook executes for Write tool
let event = HookEvent::PreToolUse {
tool: "Write".to_string(),
args: serde_json::json!({"path": "/tmp/test.txt"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Allow);
// Test that the hook does NOT execute for Read tool (doesn't match pattern)
let event = HookEvent::PreToolUse {
tool: "Read".to_string(),
args: serde_json::json!({"path": "/tmp/test.txt"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Allow);
Ok(())
}
#[tokio::test]
async fn test_deny_hook() -> Result<()> {
// Create temporary directory to act as project root
let temp_dir = TempDir::new()?;
// Create hook manager
let mut hook_mgr = HookManager::new(temp_dir.path().to_str().unwrap());
// Register a hook that denies Write operations
hook_mgr.register_hook(
"PreToolUse".to_string(),
"exit 2".to_string(), // Exit code 2 means deny
Some("Write".to_string()),
Some(5000),
);
// Test that the hook denies Write tool
let event = HookEvent::PreToolUse {
tool: "Write".to_string(),
args: serde_json::json!({"path": "/tmp/test.txt"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Deny);
Ok(())
}
#[tokio::test]
async fn test_multiple_hooks_same_event() -> Result<()> {
// Create temporary directory to act as project root
let temp_dir = TempDir::new()?;
// Create hook manager
let mut hook_mgr = HookManager::new(temp_dir.path().to_str().unwrap());
// Register multiple hooks for the same event
hook_mgr.register_hook(
"PreToolUse".to_string(),
"echo 'Hook 1' && exit 0".to_string(),
Some("Edit".to_string()),
Some(5000),
);
hook_mgr.register_hook(
"PreToolUse".to_string(),
"echo 'Hook 2' && exit 0".to_string(),
Some("Edit".to_string()),
Some(5000),
);
// Test that both hooks execute
let event = HookEvent::PreToolUse {
tool: "Edit".to_string(),
args: serde_json::json!({"path": "/tmp/test.txt"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Allow);
Ok(())
}
#[tokio::test]
async fn test_hook_with_no_pattern_matches_all() -> Result<()> {
// Create temporary directory to act as project root
let temp_dir = TempDir::new()?;
// Create hook manager
let mut hook_mgr = HookManager::new(temp_dir.path().to_str().unwrap());
// Register a hook with no pattern (matches all tools)
hook_mgr.register_hook(
"PreToolUse".to_string(),
"echo 'Hook for all tools' && exit 0".to_string(),
None, // No pattern = match all
Some(5000),
);
// Test that the hook executes for any tool
let event = HookEvent::PreToolUse {
tool: "Read".to_string(),
args: serde_json::json!({"path": "/tmp/test.txt"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Allow);
let event = HookEvent::PreToolUse {
tool: "Write".to_string(),
args: serde_json::json!({"path": "/tmp/test.txt"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Allow);
let event = HookEvent::PreToolUse {
tool: "Bash".to_string(),
args: serde_json::json!({"command": "ls"}),
};
let result = hook_mgr.execute(&event, Some(5000)).await?;
assert_eq!(result, HookResult::Allow);
Ok(())
}