Merge subagent implementation with intelligent completion system
# Conflicts: # src/services/claude.ts
This commit is contained in:
commit
5bf4bb2182
8
.claude/agents/a-agent-like-linus-keep-it-sim.md
Normal file
8
.claude/agents/a-agent-like-linus-keep-it-sim.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: a-agent-like-linus-keep-it-sim
|
||||
description: "Use this agent when you need assistance with: a agent like linus, keep it simaple and stupid "
|
||||
model: glm-4.5
|
||||
color: pink
|
||||
---
|
||||
|
||||
You are a specialized assistant focused on helping with a agent like linus, keep it simaple and stupid . Provide expert-level assistance in this domain.
|
||||
8
.claude/agents/dao-qi-harmony-designer.md
Normal file
8
.claude/agents/dao-qi-harmony-designer.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: dao-qi-harmony-designer
|
||||
description: "This agent should be used when designing, evaluating, or refining software products to ensure harmony between core principles (Dao) and user interface (Qi). It's particularly valuable during the conceptual phase of product development, when redesigning existing systems, or when trying to improve user adoption by making complex systems more intuitive. Use it when you want to create products where users can naturally grasp the underlying logic through the interface, or when you need to identify disconnects between your system's conceptual foundation and its implementation."
|
||||
tools: "*"
|
||||
color: red
|
||||
---
|
||||
|
||||
You are the Dao-Qi Harmony Designer, an AI agent specialized in applying the Chinese philosophical concepts of Dao (道) and Qi (器) to software design. Your purpose is to help achieve perfect harmony between a system's underlying principles (Dao) and its concrete implementation (Qi).When analyzing a software product or design:1. First identify the Dao - the core concepts, mental models, information architecture, logic, and data flows that form the invisible foundation of the system.2. Then examine the Qi - the UI elements, interactions, visual designs, and user experiences that give form to these concepts.3. Evaluate how well the Qi expresses the Dao, looking for both strengths and misalignments.4. Provide specific recommendations to achieve '道器统一' (unity of Dao and Qi), ensuring the interface makes the underlying principles intuitive and accessible.Always remember that '道生器,器载道' (Dao gives birth to Qi, Qi carries Dao). The best designs emerge naturally from a clear understanding of fundamental principles. Guide users to first establish a strong Dao, then let the Qi emerge organically from it.Avoid solutions that focus only on beautiful interfaces without solid logic (Qi without Dao) or excellent concepts trapped in poor implementations (Dao without Qi). Instead, strive for designs where users can intuitively grasp the system's essence through its interface.
|
||||
9
.claude/agents/simplicity-auditor.md
Normal file
9
.claude/agents/simplicity-auditor.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
name: simplicity-auditor
|
||||
description: "Use this agent when you need a thorough code review focused on identifying over-engineering, unnecessary abstractions, and unused logic. This agent is particularly valuable during code refactoring, architecture design phases, or when reviewing pull requests to ensure the codebase remains simple, maintainable, and practical. It's especially helpful for teams that tend to over-engineer solutions or create complex frameworks for problems that don't require them. The agent will challenge design decisions that add complexity without providing proportional value."
|
||||
tools: "*"
|
||||
model: glm-4.5
|
||||
color: yellow
|
||||
---
|
||||
|
||||
You are a code reviewer with the philosophy of Linus Torvalds - you value simplicity, practicality, and direct solutions over complex abstractions and over-engineering. Your primary goal is to identify and eliminate unnecessary complexity in code.When reviewing code, focus on:1. Identifying over-engineered solutions that could be simplified2. Pointing out unnecessary abstractions that don't provide clear value3. Flagging unused code, functions, or logic that should be removed4. Challenging complex designs when simpler alternatives would work5. Ensuring code only solves actual problems, not hypothetical future onesAdopt the 'Keep it simple & stupid' (KISS) principle in all your feedback. Be direct, sometimes blunt, but always constructive. Question the necessity of each component and prefer straightforward implementations over clever ones.Remember: Good code solves the problem at hand with minimal complexity. Extra features, abstractions, or flexibility 'just in case' they're needed later are usually a waste of time and make the code harder to maintain. Only design what you need to use right now.
|
||||
9
.claude/agents/test-agent.md
Normal file
9
.claude/agents/test-agent.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
name: test-agent
|
||||
description: Test agent for validation
|
||||
tools: '*'
|
||||
model: claude-3-5-sonnet-20241022
|
||||
color: cyan
|
||||
---
|
||||
|
||||
You are a test agent.
|
||||
32
.claude/agents/test-writer.md
Normal file
32
.claude/agents/test-writer.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: test-writer
|
||||
description: "Specialized in writing comprehensive test suites. Use for creating unit tests, integration tests, and test documentation."
|
||||
tools: ["FileRead", "FileWrite", "FileEdit", "Bash", "Grep"]
|
||||
model: glm-4.5
|
||||
---
|
||||
|
||||
You are a test writing specialist. Your role is to create comprehensive, well-structured test suites.
|
||||
|
||||
Your testing expertise includes:
|
||||
- Writing unit tests with proper mocking and assertions
|
||||
- Creating integration tests that verify component interactions
|
||||
- Developing end-to-end tests for critical user workflows
|
||||
- Generating test fixtures and test data
|
||||
- Writing test documentation and coverage reports
|
||||
|
||||
Testing guidelines:
|
||||
- Follow the project's existing test patterns and conventions
|
||||
- Ensure high code coverage while avoiding redundant tests
|
||||
- Write clear test descriptions that explain what is being tested and why
|
||||
- Include edge cases and error scenarios
|
||||
- Use appropriate assertion methods and matchers
|
||||
- Mock external dependencies appropriately
|
||||
- Keep tests isolated and independent
|
||||
|
||||
When writing tests:
|
||||
1. First understand the code being tested
|
||||
2. Identify key behaviors and edge cases
|
||||
3. Structure tests using describe/it blocks or equivalent
|
||||
4. Write clear, descriptive test names
|
||||
5. Include setup and teardown when needed
|
||||
6. Verify the tests pass by running them
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,7 +9,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
KODE.md
|
||||
AGENTS.md
|
||||
|
||||
# Caches
|
||||
|
||||
|
||||
28
.kode/agents/code-writer.md
Normal file
28
.kode/agents/code-writer.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: code-writer
|
||||
description: Specialized in writing and modifying code, implementing features, fixing bugs, and refactoring
|
||||
tools: ["Read", "Write", "Edit", "MultiEdit", "Bash"]
|
||||
color: blue
|
||||
---
|
||||
|
||||
You are a code writing specialist focused on implementing features, fixing bugs, and refactoring code.
|
||||
|
||||
Your primary responsibilities:
|
||||
1. Write clean, maintainable, and well-tested code
|
||||
2. Follow existing project conventions and patterns
|
||||
3. Implement features according to specifications
|
||||
4. Fix bugs with minimal side effects
|
||||
5. Refactor code to improve quality and maintainability
|
||||
|
||||
Guidelines:
|
||||
- Always understand the existing code structure before making changes
|
||||
- Write code that fits naturally with the surrounding codebase
|
||||
- Consider edge cases and error handling
|
||||
- Keep changes focused and avoid scope creep
|
||||
- Test your changes when possible
|
||||
|
||||
When implementing features:
|
||||
- Start by understanding the requirements fully
|
||||
- Review existing similar code for patterns to follow
|
||||
- Implement incrementally with clear commits
|
||||
- Ensure backward compatibility where needed
|
||||
27
.kode/agents/dao-qi-harmony-designer.md
Normal file
27
.kode/agents/dao-qi-harmony-designer.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
name: dao-qi-harmony-designer
|
||||
description: Architecture and design harmony specialist that evaluates system coherence, design patterns, and architectural decisions
|
||||
tools: ["Read", "Grep", "Glob", "LS"]
|
||||
color: red
|
||||
---
|
||||
|
||||
You are the Dao-Qi Harmony Designer, an architecture evaluation specialist focused on system coherence and design harmony.
|
||||
|
||||
Your role is to evaluate and improve architectural designs based on principles of simplicity, clarity, and system-wide harmony. You examine codebases to identify architectural patterns, potential improvements, and ensure design consistency.
|
||||
|
||||
When evaluating architecture:
|
||||
1. Start by understanding the overall system structure
|
||||
2. Identify key architectural patterns and design decisions
|
||||
3. Look for inconsistencies or areas that break the harmony
|
||||
4. Suggest improvements that enhance simplicity and maintainability
|
||||
5. Consider both technical excellence and practical constraints
|
||||
|
||||
Key focus areas:
|
||||
- Component boundaries and responsibilities
|
||||
- Data flow and state management patterns
|
||||
- Separation of concerns
|
||||
- Code organization and module structure
|
||||
- Dependency management and coupling
|
||||
- Interface design and API consistency
|
||||
|
||||
Always provide specific examples from the codebase and concrete suggestions for improvement.
|
||||
33
.kode/agents/docs-writer.md
Normal file
33
.kode/agents/docs-writer.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
name: docs-writer
|
||||
description: "Documentation specialist for creating and updating technical documentation, README files, and API docs."
|
||||
tools: ["FileRead", "FileWrite", "FileEdit", "Grep", "Glob"]
|
||||
model: main
|
||||
---
|
||||
|
||||
You are a documentation specialist. Your role is to create clear, comprehensive, and maintainable documentation.
|
||||
|
||||
Your documentation expertise includes:
|
||||
- Writing clear README files with installation and usage instructions
|
||||
- Creating API documentation with examples
|
||||
- Developing architecture and design documents
|
||||
- Writing user guides and tutorials
|
||||
- Creating inline code documentation and comments
|
||||
- Generating changelog entries
|
||||
|
||||
Documentation guidelines:
|
||||
- Write for your target audience (developers, users, or both)
|
||||
- Use clear, concise language avoiding unnecessary jargon
|
||||
- Include practical examples and code snippets
|
||||
- Structure documents with clear headings and sections
|
||||
- Keep documentation in sync with the actual code
|
||||
- Use diagrams and visuals where helpful
|
||||
- Follow the project's documentation standards
|
||||
|
||||
When creating documentation:
|
||||
1. Understand the system or feature being documented
|
||||
2. Identify the target audience and their needs
|
||||
3. Organize information logically
|
||||
4. Include all necessary details without overwhelming
|
||||
5. Provide examples and use cases
|
||||
6. Review for clarity and completeness
|
||||
24
.kode/agents/search-specialist.md
Normal file
24
.kode/agents/search-specialist.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: search-specialist
|
||||
description: Specialized in finding files and code patterns quickly using targeted searches
|
||||
tools: ["Grep", "Glob", "Read", "LS"]
|
||||
color: green
|
||||
---
|
||||
|
||||
You are a search specialist optimized for quickly finding files, code patterns, and information in codebases.
|
||||
|
||||
Your expertise:
|
||||
1. Efficient pattern matching and search strategies
|
||||
2. Finding code references and dependencies
|
||||
3. Locating configuration files and documentation
|
||||
4. Tracing function calls and data flow
|
||||
5. Discovering hidden or hard-to-find code
|
||||
|
||||
Search strategies:
|
||||
- Start with broad searches and narrow down
|
||||
- Use multiple search patterns if the first doesn't work
|
||||
- Consider different naming conventions and variations
|
||||
- Check common locations for specific file types
|
||||
- Use context clues to refine searches
|
||||
|
||||
Always aim to find all relevant occurrences, not just the first match.
|
||||
32
.kode/agents/test-writer.md
Normal file
32
.kode/agents/test-writer.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: test-writer
|
||||
description: "Specialized in writing comprehensive test suites. Use for creating unit tests, integration tests, and test documentation."
|
||||
tools: ["FileRead", "FileWrite", "FileEdit", "Bash", "Grep"]
|
||||
model: main
|
||||
---
|
||||
|
||||
You are a test writing specialist. Your role is to create comprehensive, well-structured test suites.
|
||||
|
||||
Your testing expertise includes:
|
||||
- Writing unit tests with proper mocking and assertions
|
||||
- Creating integration tests that verify component interactions
|
||||
- Developing end-to-end tests for critical user workflows
|
||||
- Generating test fixtures and test data
|
||||
- Writing test documentation and coverage reports
|
||||
|
||||
Testing guidelines:
|
||||
- Follow the project's existing test patterns and conventions
|
||||
- Ensure high code coverage while avoiding redundant tests
|
||||
- Write clear test descriptions that explain what is being tested and why
|
||||
- Include edge cases and error scenarios
|
||||
- Use appropriate assertion methods and matchers
|
||||
- Mock external dependencies appropriately
|
||||
- Keep tests isolated and independent
|
||||
|
||||
When writing tests:
|
||||
1. First understand the code being tested
|
||||
2. Identify key behaviors and edge cases
|
||||
3. Structure tests using describe/it blocks or equivalent
|
||||
4. Write clear, descriptive test names
|
||||
5. Include setup and teardown when needed
|
||||
6. Verify the tests pass by running them
|
||||
316
CONTEXT.md
Normal file
316
CONTEXT.md
Normal file
@ -0,0 +1,316 @@
|
||||
# Kode Subagent Implementation - Project Context
|
||||
|
||||
## Project Overview
|
||||
|
||||
### Mission
|
||||
Implement a complete subagent system for Kode that achieves **100% alignment** with Claude Code's Task tool functionality, enabling dynamic agent configuration loading from markdown files with YAML frontmatter.
|
||||
|
||||
### Core Architecture
|
||||
Based on Claude Code's three-layer parallel architecture:
|
||||
1. **User Interaction Layer** - REPL interface and commands
|
||||
2. **Task Tool Layer** - Dynamic agent orchestration
|
||||
3. **Tool Layer** - Individual tools (FileRead, Bash, etc.)
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
### Key Components Implemented
|
||||
|
||||
#### 1. Dynamic Agent Loader (`src/utils/agentLoader.ts`)
|
||||
- **Purpose**: Load agent configurations from markdown files with YAML frontmatter
|
||||
- **Five-tier Priority System**: Built-in < ~/.claude < ~/.kode < ./.claude < ./.kode
|
||||
- **Features**:
|
||||
- Memoized loading for performance
|
||||
- Hot reload with file system watching
|
||||
- Tool permission filtering
|
||||
- Model override capabilities
|
||||
|
||||
#### 2. TaskTool Integration (`src/tools/TaskTool/TaskTool.tsx`)
|
||||
- **Purpose**: Modified TaskTool to use dynamic agent configurations
|
||||
- **Key Changes**:
|
||||
- Removed hardcoded `SUBAGENT_CONFIGS`
|
||||
- Added dynamic `subagent_type` parameter validation
|
||||
- Integrated with agent loader for real-time agent discovery
|
||||
- Support for async tool description generation
|
||||
|
||||
#### 3. Agent Management UI (`src/commands/agents.tsx`)
|
||||
- **Purpose**: Interactive `/agents` command for agent CRUD operations
|
||||
- **Features**:
|
||||
- List all available agents with location indicators
|
||||
- Create new agents with step-by-step wizard
|
||||
- View agent details and system prompts
|
||||
- Delete custom agents (preserves built-ins)
|
||||
- Support for saving to all 4 directory locations
|
||||
|
||||
#### 4. Claude Service Fix (`src/services/claude.ts`)
|
||||
- **Critical Fix**: Modified tool description processing to support async functions
|
||||
- **Problem**: `tool.description` was used directly instead of `await tool.description()`
|
||||
- **Solution**: Added async handling with function type checking
|
||||
|
||||
### Agent Configuration Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: agent-name
|
||||
description: "When to use this agent description"
|
||||
tools: ["ToolName1", "ToolName2"] # or "*" for all tools
|
||||
model: model-name # optional
|
||||
---
|
||||
|
||||
System prompt content here...
|
||||
Multi-line prompts supported.
|
||||
```
|
||||
|
||||
### Directory Structure & Priority
|
||||
|
||||
```
|
||||
Priority Order (later overrides earlier):
|
||||
1. Built-in (code-embedded)
|
||||
2. ~/.claude/agents/ (Claude user)
|
||||
3. ~/.kode/agents/ (Kode user)
|
||||
4. ./.claude/agents/ (Claude project)
|
||||
5. ./.kode/agents/ (Kode project)
|
||||
```
|
||||
|
||||
### Available Built-in Agents
|
||||
|
||||
```typescript
|
||||
// User-level agents (in ~/.kode/agents/)
|
||||
- general-purpose: Multi-step tasks, research, complex questions
|
||||
- search-specialist: File/code pattern finding (tools: Grep, Glob, FileRead, LS)
|
||||
- code-writer: Implementation, debugging (tools: FileRead, FileWrite, FileEdit, MultiEdit, Bash)
|
||||
- reviewer: Code quality analysis (tools: FileRead, Grep, Glob)
|
||||
- architect: System design decisions (tools: FileRead, FileWrite, Grep, Glob)
|
||||
|
||||
// Project-level agents (in ./.kode/agents/)
|
||||
- test-writer: Test suite creation (tools: FileRead, FileWrite, FileEdit, Bash, Grep)
|
||||
- docs-writer: Technical documentation (tools: FileRead, FileWrite, FileEdit, Grep, Glob)
|
||||
```
|
||||
|
||||
## Critical Technical Details
|
||||
|
||||
### 1. Async Description Pattern
|
||||
```typescript
|
||||
// WRONG (old pattern)
|
||||
const toolSchemas = tools.map(tool => ({
|
||||
description: tool.description, // Function reference
|
||||
}))
|
||||
|
||||
// CORRECT (fixed pattern)
|
||||
const toolSchemas = await Promise.all(
|
||||
tools.map(async tool => ({
|
||||
description: typeof tool.description === 'function'
|
||||
? await tool.description()
|
||||
: tool.description,
|
||||
}))
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Agent Loading Flow
|
||||
```typescript
|
||||
1. scanAgentDirectory() -> Parse .md files with gray-matter
|
||||
2. loadAllAgents() -> Parallel scanning of all 4 directories
|
||||
3. Priority override -> Map-based deduplication by agentType
|
||||
4. Memoization -> LRU cache for performance
|
||||
5. Hot reload -> FSWatcher on all directories
|
||||
```
|
||||
|
||||
### 3. Tool Permission Filtering
|
||||
```typescript
|
||||
// In TaskTool.tsx
|
||||
if (toolFilter && toolFilter !== '*') {
|
||||
if (Array.isArray(toolFilter)) {
|
||||
tools = tools.filter(tool => toolFilter.includes(tool.name))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Model Override Logic
|
||||
```typescript
|
||||
// Priority: CLI model param > agent config > default
|
||||
let effectiveModel = model || 'task' // CLI param
|
||||
if (!model && agentConfig.model) {
|
||||
effectiveModel = agentConfig.model // Agent config
|
||||
}
|
||||
```
|
||||
|
||||
## Standard Operating Procedures
|
||||
|
||||
### SOP 1: Adding New Built-in Agent
|
||||
1. Create `.md` file in appropriate directory
|
||||
2. Use proper YAML frontmatter format
|
||||
3. Test with `getActiveAgents()` function
|
||||
4. Verify priority system works correctly
|
||||
5. Update documentation if needed
|
||||
|
||||
### SOP 2: Debugging Agent Loading Issues
|
||||
```bash
|
||||
# 1. Test agent loader directly
|
||||
bun -e "import {getActiveAgents} from './src/utils/agentLoader'; console.log(await getActiveAgents())"
|
||||
|
||||
# 2. Clear cache and reload
|
||||
bun -e "import {clearAgentCache} from './src/utils/agentLoader'; clearAgentCache()"
|
||||
|
||||
# 3. Check TaskTool description generation
|
||||
bun -e "import {TaskTool} from './src/tools/TaskTool/TaskTool'; console.log(await TaskTool.description())"
|
||||
|
||||
# 4. Verify directory structure
|
||||
ls -la ~/.claude/agents/ ~/.kode/agents/ ./.claude/agents/ ./.kode/agents/
|
||||
```
|
||||
|
||||
### SOP 3: Testing Subagent System
|
||||
```typescript
|
||||
// Comprehensive test pattern
|
||||
async function testSubagentSystem() {
|
||||
// 1. Clear cache
|
||||
clearAgentCache()
|
||||
|
||||
// 2. Load agents
|
||||
const agents = await getActiveAgents()
|
||||
|
||||
// 3. Verify count and types
|
||||
const types = await getAvailableAgentTypes()
|
||||
|
||||
// 4. Test TaskTool integration
|
||||
const description = await TaskTool.description()
|
||||
|
||||
// 5. Verify priority system
|
||||
const duplicates = findDuplicateAgentTypes(agents)
|
||||
|
||||
return { agents, types, description, duplicates }
|
||||
}
|
||||
```
|
||||
|
||||
### SOP 4: Agent Management Best Practices
|
||||
1. **Agent Naming**: Use kebab-case (`search-specialist`, not `SearchSpecialist`)
|
||||
2. **Tool Selection**: Be specific about tool permissions for security
|
||||
3. **Model Selection**: Only specify if different from default
|
||||
4. **Description**: Clear, concise "when to use" guidance
|
||||
5. **System Prompt**: Focus on capabilities and constraints
|
||||
|
||||
## Key Learnings & Insights
|
||||
|
||||
### 1. Claude Code Alignment Requirements
|
||||
- **100% format compatibility**: YAML frontmatter + markdown body
|
||||
- **Directory structure**: Support both `.claude` and `.kode` directories
|
||||
- **Priority system**: Complex 5-tier hierarchy with proper override logic
|
||||
- **Hot reload**: Real-time configuration updates without restart
|
||||
- **Tool permissions**: Security through capability restriction
|
||||
|
||||
### 2. Performance Considerations
|
||||
- **Memoization**: Critical for avoiding repeated file I/O
|
||||
- **Parallel loading**: All directories scanned concurrently
|
||||
- **Caching strategy**: LRU cache with manual invalidation
|
||||
- **File watching**: Efficient hot reload with minimal overhead
|
||||
|
||||
### 3. Error Handling Patterns
|
||||
```typescript
|
||||
// Graceful degradation pattern
|
||||
try {
|
||||
const agents = await loadAllAgents()
|
||||
return { activeAgents: agents.activeAgents, allAgents: agents.allAgents }
|
||||
} catch (error) {
|
||||
console.error('Failed to load agents, falling back to built-in:', error)
|
||||
return {
|
||||
activeAgents: [BUILTIN_GENERAL_PURPOSE],
|
||||
allAgents: [BUILTIN_GENERAL_PURPOSE]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. TypeScript Integration Points
|
||||
```typescript
|
||||
export interface AgentConfig {
|
||||
agentType: string // Matches subagent_type parameter
|
||||
whenToUse: string // User-facing description
|
||||
tools: string[] | '*' // Tool permission filtering
|
||||
systemPrompt: string // Injected into task prompt
|
||||
location: 'built-in' | 'user' | 'project'
|
||||
color?: string // Optional UI theming
|
||||
model?: string // Optional model override
|
||||
}
|
||||
```
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue 1: "Agent type 'X' not found"
|
||||
**Cause**: Agent not loaded or wrong agentType in frontmatter
|
||||
**Solution**:
|
||||
1. Check file exists in expected directory
|
||||
2. Verify `name:` field in YAML frontmatter
|
||||
3. Clear cache with `clearAgentCache()`
|
||||
4. Check file permissions
|
||||
|
||||
### Issue 2: Tool description not showing subagent types
|
||||
**Cause**: Async description function not being awaited
|
||||
**Solution**: Ensure Claude service uses `await tool.description()` pattern
|
||||
|
||||
### Issue 3: Priority system not working
|
||||
**Cause**: Map iteration order or incorrect directory scanning
|
||||
**Solution**: Verify loading order matches priority specification
|
||||
|
||||
### Issue 4: Hot reload not triggering
|
||||
**Cause**: File watcher not set up or wrong directory
|
||||
**Solution**: Check `startAgentWatcher()` covers all 4 directories
|
||||
|
||||
## Future Enhancement Opportunities
|
||||
|
||||
### 1. Advanced Agent Features
|
||||
- **Agent inheritance**: Base agents with specialized variants
|
||||
- **Conditional logic**: Dynamic tool selection based on context
|
||||
- **Agent composition**: Chaining agents for complex workflows
|
||||
- **Performance metrics**: Track agent usage and effectiveness
|
||||
|
||||
### 2. UI/UX Improvements
|
||||
- **Visual agent editor**: Rich text editing for system prompts
|
||||
- **Agent marketplace**: Share and discover community agents
|
||||
- **Configuration validation**: Real-time feedback on agent configs
|
||||
- **Usage analytics**: Show which agents are most effective
|
||||
|
||||
### 3. Integration Enhancements
|
||||
- **IDE integration**: VS Code extension for agent management
|
||||
- **API endpoints**: REST API for external agent management
|
||||
- **Version control**: Git integration for agent configuration history
|
||||
- **Cloud sync**: Cross-device agent synchronization
|
||||
|
||||
## Testing & Validation
|
||||
|
||||
### Test Coverage Areas
|
||||
1. **Agent Loading**: All directory combinations and priority scenarios
|
||||
2. **Tool Filtering**: Security boundary enforcement
|
||||
3. **Model Override**: CLI param vs agent config vs default
|
||||
4. **Hot Reload**: File change detection and cache invalidation
|
||||
5. **Error Handling**: Graceful degradation and recovery
|
||||
6. **TaskTool Integration**: Dynamic description generation
|
||||
7. **UI Components**: Agent management command workflows
|
||||
|
||||
### Validation Checklist
|
||||
- [ ] All 5 priority levels load correctly
|
||||
- [ ] Duplicate agent names resolve to highest priority
|
||||
- [ ] Tool permissions filter correctly
|
||||
- [ ] Model overrides work in correct precedence
|
||||
- [ ] Hot reload detects changes in all directories
|
||||
- [ ] TaskTool description includes all available agents
|
||||
- [ ] `/agents` command CRUD operations work
|
||||
- [ ] Error states handled gracefully
|
||||
- [ ] TypeScript types are correct and complete
|
||||
- [ ] Documentation is accurate and comprehensive
|
||||
|
||||
## Project Metrics & Success Criteria
|
||||
|
||||
### Quantitative Metrics
|
||||
- **Agent Load Performance**: < 100ms for typical configurations
|
||||
- **Hot Reload Latency**: < 500ms from file change to cache update
|
||||
- **Memory Usage**: < 50MB additional overhead for agent system
|
||||
- **Test Coverage**: > 90% for core agent functionality
|
||||
- **TypeScript Compliance**: 0 type errors in agent-related code
|
||||
|
||||
### Qualitative Success Criteria
|
||||
- **100% Claude Code compatibility**: All agent formats work identically
|
||||
- **Seamless user experience**: No learning curve for existing Claude Code users
|
||||
- **Robust error handling**: System degrades gracefully under all failure modes
|
||||
- **Maintainable architecture**: Code is clean, documented, and extensible
|
||||
- **Performance excellence**: No noticeable impact on Kode startup or operation
|
||||
|
||||
---
|
||||
|
||||
*This document serves as the single source of truth for the Kode subagent implementation project. All team members should refer to this context when working on agent-related features or debugging issues.*
|
||||
90
README.md
90
README.md
@ -2,22 +2,65 @@
|
||||
|
||||
[](https://www.npmjs.com/package/@shareai-lab/kode)
|
||||
[](https://opensource.org/licenses/ISC)
|
||||
[](https://agents.md)
|
||||
|
||||
[中文文档](README.zh-CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
|
||||
|
||||
## 🤝 AGENTS.md Standard Support
|
||||
|
||||
**Kode proudly supports the [AGENTS.md standard protocol](https://agents.md) initiated by OpenAI** - a simple, open format for guiding programming agents that's used by 20k+ open source projects.
|
||||
|
||||
### Full Compatibility with Multiple Standards
|
||||
|
||||
- ✅ **AGENTS.md** - Native support for the OpenAI-initiated standard format
|
||||
- ✅ **CLAUDE.md** - Full backward compatibility with Claude Code configurations
|
||||
- ✅ **Subagent System** - Advanced agent delegation and task orchestration
|
||||
- ✅ **Cross-platform** - Works with 20+ AI models and providers
|
||||
|
||||
Use `# Your documentation request` to generate and maintain your AGENTS.md file automatically, while maintaining full compatibility with existing Claude Code workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
Kode is a powerful AI assistant that lives in your terminal. It can understand your codebase, edit files, run commands, and handle entire workflows for you.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Capabilities
|
||||
- 🤖 **AI-Powered Assistance** - Uses advanced AI models to understand and respond to your requests
|
||||
- 🔄 **Multi-Model Collaboration** - Flexibly switch and combine multiple AI models to leverage their unique strengths
|
||||
- 🦜 **Expert Model Consultation** - Use `@ask-model-name` to consult specific AI models for specialized analysis
|
||||
- 👤 **Intelligent Agent System** - Use `@run-agent-name` to delegate tasks to specialized subagents
|
||||
- 📝 **Code Editing** - Directly edit files with intelligent suggestions and improvements
|
||||
- 🔍 **Codebase Understanding** - Analyzes your project structure and code relationships
|
||||
- 🚀 **Command Execution** - Run shell commands and see results in real-time
|
||||
- 🛠️ **Workflow Automation** - Handle complex development tasks with simple prompts
|
||||
|
||||
### 🎯 Advanced Intelligent Completion System
|
||||
Our state-of-the-art completion system provides unparalleled coding assistance:
|
||||
|
||||
#### Smart Fuzzy Matching
|
||||
- **Hyphen-Aware Matching** - Type `dao` to match `run-agent-dao-qi-harmony-designer`
|
||||
- **Abbreviation Support** - `dq` matches `dao-qi`, `nde` matches `node`
|
||||
- **Numeric Suffix Handling** - `py3` intelligently matches `python3`
|
||||
- **Multi-Algorithm Fusion** - Combines 7+ matching algorithms for best results
|
||||
|
||||
#### Intelligent Context Detection
|
||||
- **No @ Required** - Type `gp5` directly to match `@ask-gpt-5`
|
||||
- **Auto-Prefix Addition** - Tab/Enter automatically adds `@` for agents and models
|
||||
- **Mixed Completion** - Seamlessly switch between commands, files, agents, and models
|
||||
- **Smart Prioritization** - Results ranked by relevance and usage frequency
|
||||
|
||||
#### Unix Command Optimization
|
||||
- **500+ Common Commands** - Curated database of frequently used Unix/Linux commands
|
||||
- **System Intersection** - Only shows commands that actually exist on your system
|
||||
- **Priority Scoring** - Common commands appear first (git, npm, docker, etc.)
|
||||
- **Real-time Loading** - Dynamic command discovery from system PATH
|
||||
|
||||
### User Experience
|
||||
- 🎨 **Interactive UI** - Beautiful terminal interface with syntax highlighting
|
||||
- 🔌 **Tool System** - Extensible architecture with specialized tools for different tasks
|
||||
- 💾 **Context Management** - Smart context handling to maintain conversation continuity
|
||||
- 📋 **AGENTS.md Integration** - Use `# documentation requests` to auto-generate and maintain project documentation
|
||||
|
||||
## Installation
|
||||
|
||||
@ -50,6 +93,53 @@ kode -p "explain this function" main.js
|
||||
kwa -p "explain this function" main.js
|
||||
```
|
||||
|
||||
### Using the @ Mention System
|
||||
|
||||
Kode supports a powerful @ mention system for intelligent completions:
|
||||
|
||||
#### 🦜 Expert Model Consultation
|
||||
```bash
|
||||
# Consult specific AI models for expert opinions
|
||||
@ask-claude-sonnet-4 How should I optimize this React component for performance?
|
||||
@ask-gpt-5 What are the security implications of this authentication method?
|
||||
@ask-o1-preview Analyze the complexity of this algorithm
|
||||
```
|
||||
|
||||
#### 👤 Specialized Agent Delegation
|
||||
```bash
|
||||
# Delegate tasks to specialized subagents
|
||||
@run-agent-simplicity-auditor Review this code for over-engineering
|
||||
@run-agent-architect Design a microservices architecture for this system
|
||||
@run-agent-test-writer Create comprehensive tests for these modules
|
||||
```
|
||||
|
||||
#### 📁 Smart File References
|
||||
```bash
|
||||
# Reference files and directories with auto-completion
|
||||
@src/components/Button.tsx
|
||||
@docs/api-reference.md
|
||||
@.env.example
|
||||
```
|
||||
|
||||
The @ mention system provides intelligent completions as you type, showing available models, agents, and files.
|
||||
|
||||
### AGENTS.md Documentation Mode
|
||||
|
||||
Use the `#` prefix to generate and maintain your AGENTS.md documentation:
|
||||
|
||||
```bash
|
||||
# Generate setup instructions
|
||||
# How do I set up the development environment?
|
||||
|
||||
# Create testing documentation
|
||||
# What are the testing procedures for this project?
|
||||
|
||||
# Document deployment process
|
||||
# Explain the deployment pipeline and requirements
|
||||
```
|
||||
|
||||
This mode automatically formats responses as structured documentation and appends them to your AGENTS.md file.
|
||||
|
||||
### Commands
|
||||
|
||||
- `/help` - Show available commands
|
||||
|
||||
270
docs/ELEGANT_TAB_IMPROVEMENT_PLAN.md
Normal file
270
docs/ELEGANT_TAB_IMPROVEMENT_PLAN.md
Normal file
@ -0,0 +1,270 @@
|
||||
# 优雅的Tab补全改进计划
|
||||
|
||||
## 一、当前架构分析
|
||||
|
||||
### 核心数据结构(保持不变)
|
||||
```typescript
|
||||
interface UnifiedSuggestion // ✅ 完美,不需要改动
|
||||
interface CompletionContext // ✅ 完美,不需要改动
|
||||
```
|
||||
|
||||
### 状态管理(需要增强)
|
||||
```typescript
|
||||
// 当前状态
|
||||
const [suggestions, setSuggestions] // ✅ 保持
|
||||
const [selectedIndex, setSelectedIndex] // ✅ 保持
|
||||
const [isActive, setIsActive] // ✅ 保持
|
||||
const lastTabContext = useRef() // ✅ 保持
|
||||
|
||||
// 需要添加的状态(最小化)
|
||||
const tabState = useRef<TabState>() // 🆕 Tab按键状态
|
||||
```
|
||||
|
||||
### 关键函数(大部分保持)
|
||||
- `getWordAtCursor()` ✅ 完美,不改
|
||||
- `generateCommandSuggestions()` ✅ 完美,不改
|
||||
- `generateAgentSuggestions()` ✅ 完美,不改
|
||||
- `generateFileSuggestions()` ✅ 完美,不改
|
||||
- `generateSuggestions()` ✅ 完美,不改
|
||||
- Tab处理逻辑 ❌ 需要重构
|
||||
|
||||
## 二、最小化改动方案
|
||||
|
||||
### 1. 添加Tab状态跟踪(新增数据结构)
|
||||
```typescript
|
||||
// 添加到文件顶部,与其他interface并列
|
||||
interface TabState {
|
||||
lastTabTime: number
|
||||
consecutiveTabCount: number
|
||||
lastPrefix: string
|
||||
lastSuggestions: UnifiedSuggestion[]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 添加公共前缀计算(纯函数,无副作用)
|
||||
```typescript
|
||||
// 添加为独立的utility函数
|
||||
const findCommonPrefix = (suggestions: UnifiedSuggestion[]): string => {
|
||||
if (suggestions.length === 0) return ''
|
||||
if (suggestions.length === 1) return suggestions[0].value
|
||||
|
||||
const values = suggestions.map(s => s.value)
|
||||
let prefix = values[0]
|
||||
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
while (prefix && !values[i].startsWith(prefix)) {
|
||||
prefix = prefix.slice(0, -1)
|
||||
}
|
||||
if (!prefix) break
|
||||
}
|
||||
|
||||
return prefix
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 重构Tab处理逻辑(核心改动)
|
||||
|
||||
将现有的Tab处理(185-237行)替换为新的智能处理:
|
||||
|
||||
```typescript
|
||||
// Handle Tab key - Terminal-compliant behavior
|
||||
useInput(async (_, key) => {
|
||||
if (!key.tab || key.shift) return false
|
||||
|
||||
const context = getWordAtCursor()
|
||||
if (!context) return false
|
||||
|
||||
const now = Date.now()
|
||||
const isDoubleTab = tabState.current &&
|
||||
(now - tabState.current.lastTabTime) < 500 &&
|
||||
tabState.current.lastPrefix === context.prefix
|
||||
|
||||
// 如果菜单已显示,Tab选择下一个
|
||||
if (isActive && suggestions.length > 0) {
|
||||
// 保持原有逻辑
|
||||
const selected = suggestions[selectedIndex]
|
||||
// ... 完成逻辑
|
||||
return true
|
||||
}
|
||||
|
||||
// 生成建议(只在需要时)
|
||||
let currentSuggestions = suggestions
|
||||
if (!isDoubleTab || suggestions.length === 0) {
|
||||
currentSuggestions = await generateSuggestions(context)
|
||||
}
|
||||
|
||||
// 决策树 - 完全符合终端行为
|
||||
if (currentSuggestions.length === 0) {
|
||||
// 无匹配:蜂鸣
|
||||
return false
|
||||
|
||||
} else if (currentSuggestions.length === 1) {
|
||||
// 唯一匹配:立即完成
|
||||
completeWith(currentSuggestions[0], context)
|
||||
resetTabState()
|
||||
return true
|
||||
|
||||
} else {
|
||||
// 多个匹配
|
||||
const commonPrefix = findCommonPrefix(currentSuggestions)
|
||||
|
||||
if (commonPrefix.length > context.prefix.length) {
|
||||
// 可以补全到公共前缀
|
||||
partialComplete(commonPrefix, context)
|
||||
updateTabState(now, context.prefix, currentSuggestions)
|
||||
return true
|
||||
|
||||
} else if (isDoubleTab) {
|
||||
// 第二次Tab:显示菜单
|
||||
setSuggestions(currentSuggestions)
|
||||
setIsActive(true)
|
||||
setSelectedIndex(0)
|
||||
return true
|
||||
|
||||
} else {
|
||||
// 第一次Tab但无法补全:记录状态
|
||||
updateTabState(now, context.prefix, currentSuggestions)
|
||||
return false // 蜂鸣
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 4. 添加辅助函数(与现有风格一致)
|
||||
|
||||
```typescript
|
||||
// 完成补全
|
||||
const completeWith = useCallback((suggestion: UnifiedSuggestion, context: CompletionContext) => {
|
||||
const completion = context.type === 'command' ? `/${suggestion.value} ` :
|
||||
context.type === 'agent' ? `@${suggestion.value} ` :
|
||||
suggestion.value
|
||||
|
||||
const newInput = input.slice(0, context.startPos) + completion + input.slice(context.endPos)
|
||||
onInputChange(newInput)
|
||||
setCursorOffset(context.startPos + completion.length)
|
||||
}, [input, onInputChange, setCursorOffset])
|
||||
|
||||
// 部分补全
|
||||
const partialComplete = useCallback((prefix: string, context: CompletionContext) => {
|
||||
const newInput = input.slice(0, context.startPos) + prefix + input.slice(context.endPos)
|
||||
onInputChange(newInput)
|
||||
setCursorOffset(context.startPos + prefix.length)
|
||||
}, [input, onInputChange, setCursorOffset])
|
||||
|
||||
// Tab状态管理
|
||||
const updateTabState = useCallback((time: number, prefix: string, suggestions: UnifiedSuggestion[]) => {
|
||||
tabState.current = {
|
||||
lastTabTime: time,
|
||||
consecutiveTabCount: (tabState.current?.consecutiveTabCount || 0) + 1,
|
||||
lastPrefix: prefix,
|
||||
lastSuggestions: suggestions
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resetTabState = useCallback(() => {
|
||||
tabState.current = null
|
||||
}, [])
|
||||
```
|
||||
|
||||
## 三、实施步骤
|
||||
|
||||
### Phase 1: 基础设施(不影响现有功能)
|
||||
1. 添加 `TabState` interface
|
||||
2. 添加 `tabState` useRef
|
||||
3. 添加 `findCommonPrefix` 函数
|
||||
4. 添加辅助函数
|
||||
|
||||
### Phase 2: 核心逻辑替换(原子操作)
|
||||
1. 备份现有Tab处理代码
|
||||
2. 替换为新的决策树逻辑
|
||||
3. 测试所有场景
|
||||
|
||||
### Phase 3: 细节优化
|
||||
1. 调整超时时间(500ms vs 300ms)
|
||||
2. 优化菜单显示格式
|
||||
3. 添加蜂鸣反馈(可选)
|
||||
|
||||
## 四、影响评估
|
||||
|
||||
### 不变的部分(90%)
|
||||
- 所有数据结构
|
||||
- 所有生成函数
|
||||
- 箭头键处理
|
||||
- Effect清理逻辑
|
||||
- 与PromptInput的接口
|
||||
|
||||
### 改变的部分(10%)
|
||||
- Tab按键处理逻辑
|
||||
- 新增4个小函数
|
||||
- 新增1个状态ref
|
||||
|
||||
### 风险评估
|
||||
- **低风险**:改动集中在一处
|
||||
- **可回滚**:逻辑独立,易于回滚
|
||||
- **向后兼容**:接口不变
|
||||
|
||||
## 五、测试场景
|
||||
|
||||
### 场景1: 多个文件补全
|
||||
```bash
|
||||
# 文件: package.json, package-lock.json
|
||||
输入: p[Tab]
|
||||
期望: 补全到 "package"
|
||||
输入: package[Tab][Tab]
|
||||
期望: 显示菜单
|
||||
```
|
||||
|
||||
### 场景2: 唯一匹配
|
||||
```bash
|
||||
输入: READ[Tab]
|
||||
期望: 补全到 "README.md"
|
||||
```
|
||||
|
||||
### 场景3: 连续补全
|
||||
```bash
|
||||
输入: src/[Tab]
|
||||
期望: 可以继续Tab补全
|
||||
```
|
||||
|
||||
## 六、代码风格指南
|
||||
|
||||
### 保持一致性
|
||||
- 使用 `useCallback` 包装所有函数
|
||||
- 使用 `as const` 断言类型
|
||||
- 保持简洁的注释风格
|
||||
|
||||
### 命名规范
|
||||
- 动词开头:`completeWith`, `updateTabState`
|
||||
- 布尔值:`isDoubleTab`, `isActive`
|
||||
- 常量大写:`TAB_TIMEOUT`
|
||||
|
||||
### 错误处理
|
||||
- 保持静默失败(符合现有风格)
|
||||
- 使用 try-catch 包装文件操作
|
||||
|
||||
## 七、预期效果
|
||||
|
||||
### Before
|
||||
```
|
||||
cat p[Tab]
|
||||
▸ package.json # 立即显示菜单 ❌
|
||||
package-lock.json
|
||||
```
|
||||
|
||||
### After
|
||||
```
|
||||
cat p[Tab]
|
||||
cat package # 补全公共前缀 ✅
|
||||
cat package[Tab][Tab]
|
||||
package.json package-lock.json # 双Tab显示 ✅
|
||||
```
|
||||
|
||||
## 八、总结
|
||||
|
||||
这个方案:
|
||||
1. **最小化改动** - 90%代码不变
|
||||
2. **原子操作** - 可以一次性替换
|
||||
3. **风格一致** - 像原生代码
|
||||
4. **100%终端兼容** - 完全匹配bash行为
|
||||
|
||||
准备好实施了吗?
|
||||
@ -30,7 +30,12 @@ Clean, modern TypeScript CLI project using Bun for development and building.
|
||||
│ ├── tools/ # AI tool implementations
|
||||
│ ├── services/ # Core services
|
||||
│ ├── hooks/ # React hooks
|
||||
│ │ └── useUnifiedCompletion.ts # Advanced completion system
|
||||
│ ├── utils/ # Utility functions
|
||||
│ │ ├── advancedFuzzyMatcher.ts # 7+ algorithm fuzzy matcher
|
||||
│ │ ├── fuzzyMatcher.ts # Matcher integration layer
|
||||
│ │ ├── commonUnixCommands.ts # 500+ command database
|
||||
│ │ └── agentLoader.ts # Agent configuration loader
|
||||
│ └── constants/ # Constants and configurations
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
@ -45,7 +50,7 @@ Clean, modern TypeScript CLI project using Bun for development and building.
|
||||
├── README.md # English documentation
|
||||
├── README.zh-CN.md # Chinese documentation
|
||||
├── PUBLISH.md # Publishing guide
|
||||
├── KODE.md # Project context (generated)
|
||||
├── AGENTS.md # Project context (generated)
|
||||
└── system-design.md # System architecture doc (Chinese)
|
||||
```
|
||||
|
||||
|
||||
186
docs/TAB_BEHAVIOR_DEMO.md
Normal file
186
docs/TAB_BEHAVIOR_DEMO.md
Normal file
@ -0,0 +1,186 @@
|
||||
# Tab 补全行为演示
|
||||
|
||||
## 🎯 核心洞察:Tab的两个职责
|
||||
|
||||
### 1️⃣ Tab = "尽可能补全"
|
||||
### 2️⃣ Tab Tab = "显示所有选项"
|
||||
|
||||
---
|
||||
|
||||
## 📱 实际例子(假设有文件:package.json, package-lock.json)
|
||||
|
||||
### ✅ 标准终端的智慧
|
||||
```bash
|
||||
cat p[Tab]
|
||||
# 🤔 思考:有 package.json, package-lock.json, public/
|
||||
# 💡 决策:补全到公共前缀
|
||||
cat package█
|
||||
|
||||
cat package[Tab]
|
||||
# 🤔 思考:还是有歧义
|
||||
# 💡 决策:不动(或蜂鸣)
|
||||
|
||||
cat package[Tab][Tab]
|
||||
# 🤔 思考:用户需要看选项了
|
||||
# 💡 决策:显示所有
|
||||
package.json package-lock.json
|
||||
|
||||
cat package.j[Tab]
|
||||
# 🤔 思考:唯一匹配!
|
||||
# 💡 决策:直接补全
|
||||
cat package.json█
|
||||
```
|
||||
|
||||
### ❌ 我们现在的问题
|
||||
```bash
|
||||
cat p[Tab]
|
||||
# 😵 立即显示菜单
|
||||
▸ package.json
|
||||
package-lock.json
|
||||
public/
|
||||
# 用户:我只是想补全啊,不是要选择!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 终端的补全决策树
|
||||
|
||||
```
|
||||
按下 Tab
|
||||
↓
|
||||
有几个匹配?
|
||||
↓
|
||||
┌────────┼────────┐
|
||||
0个 1个 多个
|
||||
↓ ↓ ↓
|
||||
蜂鸣 补全完成 有公共前缀?
|
||||
↓
|
||||
┌────┴────┐
|
||||
有 无
|
||||
↓ ↓
|
||||
补全前缀 是第二次Tab?
|
||||
↓
|
||||
┌────┴────┐
|
||||
是 否
|
||||
↓ ↓
|
||||
显示菜单 蜂鸣
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💭 为什么这样设计?
|
||||
|
||||
### 效率原则
|
||||
- **最少按键**: 能补全就补全,不要问
|
||||
- **渐进显示**: 只在需要时才显示选项
|
||||
- **智能判断**: 根据上下文做最合理的事
|
||||
|
||||
### 用户心智模型
|
||||
```
|
||||
Tab = "帮我补全"
|
||||
Tab Tab = "我需要看看有什么选项"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔨 我们需要的改动
|
||||
|
||||
### 当前代码(过于简单)
|
||||
```typescript
|
||||
if (key.tab) {
|
||||
if (suggestions.length > 1) {
|
||||
showMenu() // ❌ 太早了!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 应该的代码(智能判断)
|
||||
```typescript
|
||||
if (key.tab) {
|
||||
const now = Date.now()
|
||||
const isDoubleTab = (now - lastTabTime) < 300
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
beep()
|
||||
} else if (suggestions.length === 1) {
|
||||
complete(suggestions[0])
|
||||
} else {
|
||||
// 多个匹配
|
||||
const commonPrefix = findCommonPrefix(suggestions)
|
||||
const currentWord = getCurrentWord()
|
||||
|
||||
if (commonPrefix.length > currentWord.length) {
|
||||
// 可以补全到公共前缀
|
||||
complete(commonPrefix)
|
||||
} else if (isDoubleTab) {
|
||||
// 第二次Tab,显示菜单
|
||||
showMenu()
|
||||
} else {
|
||||
// 第一次Tab,但没有可补全的
|
||||
beep()
|
||||
}
|
||||
}
|
||||
|
||||
lastTabTime = now
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 交互示例
|
||||
|
||||
### 场景:输入命令参数
|
||||
```bash
|
||||
# 文件: README.md, README.txt, package.json
|
||||
|
||||
$ cat R[Tab]
|
||||
$ cat README # 补全公共前缀
|
||||
|
||||
$ cat README[Tab]
|
||||
*beep* # 无法继续补全
|
||||
|
||||
$ cat README[Tab][Tab]
|
||||
README.md README.txt # 显示选项
|
||||
|
||||
$ cat README.m[Tab]
|
||||
$ cat README.md # 唯一匹配,完成
|
||||
```
|
||||
|
||||
### 场景:路径导航
|
||||
```bash
|
||||
$ cd s[Tab]
|
||||
$ cd src/ # 唯一匹配,补全+加斜杠
|
||||
|
||||
$ cd src/[Tab][Tab]
|
||||
components/ hooks/ utils/ # 继续显示下级
|
||||
|
||||
$ cd src/c[Tab]
|
||||
$ cd src/components/ # 继续补全
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 影响分析
|
||||
|
||||
| 操作 | 按键次数(现在) | 按键次数(改进后) | 节省 |
|
||||
|------|-----------------|-------------------|------|
|
||||
| 输入 package.json | 5次(p+Tab+↓+↓+Enter) | 3次(pa+Tab+.j+Tab) | 40% |
|
||||
| 进入 src/components | 4次(s+Tab+Enter+c+Tab) | 2次(s+Tab+c+Tab) | 50% |
|
||||
| 选择多个选项之一 | 3次(Tab+↓+Enter) | 4次(Tab+Tab+↓+Enter) | -33% |
|
||||
|
||||
**平均效率提升:~30%**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 结论
|
||||
|
||||
**一个原则**:Tab应该"尽力而为",而不是"立即放弃"
|
||||
|
||||
**两个规则**:
|
||||
1. 能补全就补全
|
||||
2. 不能补全才显示选项(且需要双击)
|
||||
|
||||
**三个好处**:
|
||||
1. 符合用户期望
|
||||
2. 减少操作次数
|
||||
3. 保持专注流程
|
||||
251
docs/TERMINAL_BEHAVIOR_ANALYSIS.md
Normal file
251
docs/TERMINAL_BEHAVIOR_ANALYSIS.md
Normal file
@ -0,0 +1,251 @@
|
||||
# 终端Tab补全行为深度分析
|
||||
|
||||
## 一、标准终端(bash/zsh)的Tab补全行为
|
||||
|
||||
### 1. **单次Tab行为**
|
||||
```bash
|
||||
$ cat pa[Tab]
|
||||
# 场景A:唯一匹配
|
||||
$ cat package.json # 立即补全,光标在末尾
|
||||
|
||||
# 场景B:多个匹配
|
||||
$ cat p[Tab]
|
||||
# 无反应,需要按第二次Tab
|
||||
```
|
||||
|
||||
### 2. **双击Tab行为**
|
||||
```bash
|
||||
$ cat p[Tab][Tab]
|
||||
package.json package-lock.json public/
|
||||
$ cat p█ # 光标保持原位,显示所有可能选项
|
||||
```
|
||||
|
||||
### 3. **公共前缀补全**
|
||||
```bash
|
||||
$ cat pac[Tab]
|
||||
$ cat package # 补全到公共前缀 "package"
|
||||
$ cat package[Tab][Tab]
|
||||
package.json package-lock.json
|
||||
```
|
||||
|
||||
### 4. **路径补全特性**
|
||||
```bash
|
||||
# 自动添加斜杠
|
||||
$ cd src[Tab]
|
||||
$ cd src/ # 目录自动加斜杠
|
||||
|
||||
# 连续补全
|
||||
$ cd src/[Tab]
|
||||
components/ hooks/ utils/ # 显示目录内容
|
||||
|
||||
# 隐藏文件
|
||||
$ ls .[Tab][Tab] # 需要以.开头才显示隐藏文件
|
||||
.env .gitignore .npmrc
|
||||
```
|
||||
|
||||
### 5. **上下文感知**
|
||||
```bash
|
||||
# 命令后的第一个参数
|
||||
$ git [Tab][Tab]
|
||||
add commit push pull status # 显示git子命令
|
||||
|
||||
# 不同命令的不同行为
|
||||
$ cd [Tab] # 只显示目录
|
||||
$ cat [Tab] # 显示文件
|
||||
$ chmod [Tab] # 显示可执行文件
|
||||
```
|
||||
|
||||
### 6. **特殊字符处理**
|
||||
```bash
|
||||
# 空格转义
|
||||
$ cat My\ Documents/[Tab]
|
||||
$ cat "My Documents/"[Tab]
|
||||
|
||||
# 通配符
|
||||
$ cat *.js[Tab] # 展开所有.js文件
|
||||
$ cat test*[Tab] # 展开所有test开头的文件
|
||||
```
|
||||
|
||||
## 二、现代终端增强功能(fish/zsh with plugins)
|
||||
|
||||
### 1. **实时建议(灰色文本)**
|
||||
```bash
|
||||
$ cat p
|
||||
$ cat package.json # 灰色显示建议
|
||||
# 右箭头接受,Tab完成
|
||||
```
|
||||
|
||||
### 2. **智能历史**
|
||||
```bash
|
||||
$ npm run
|
||||
$ npm run dev # 基于历史的建议
|
||||
```
|
||||
|
||||
### 3. **模糊匹配**
|
||||
```bash
|
||||
$ cat pjs[Tab]
|
||||
$ cat package.json # 模糊匹配p...j...s
|
||||
```
|
||||
|
||||
### 4. **语法高亮**
|
||||
```bash
|
||||
$ cat existing.txt # 绿色,文件存在
|
||||
$ cat missing.txt # 红色,文件不存在
|
||||
```
|
||||
|
||||
## 三、我们当前实现的差距
|
||||
|
||||
### ❌ 缺失的核心功能
|
||||
|
||||
1. **双击Tab显示所有选项**
|
||||
- 当前:第一次Tab就显示菜单
|
||||
- 应该:第一次Tab尝试补全,第二次显示选项
|
||||
|
||||
2. **公共前缀补全**
|
||||
- 当前:直接显示菜单
|
||||
- 应该:先补全到公共前缀
|
||||
|
||||
3. **无需显式触发**
|
||||
- 当前:必须Tab才触发
|
||||
- 应该:输入时就准备好建议
|
||||
|
||||
4. **连续路径补全**
|
||||
- 当前:补全后停止
|
||||
- 应该:目录补全后继续等待下一次Tab
|
||||
|
||||
5. **通配符展开**
|
||||
- 当前:不支持
|
||||
- 应该:*.js展开为所有js文件
|
||||
|
||||
### ✅ 已有但需优化
|
||||
|
||||
1. **上下文检测**
|
||||
- 有基础实现,但不够智能
|
||||
|
||||
2. **文件类型区分**
|
||||
- 有图标,但行为未区分
|
||||
|
||||
3. **即时响应**
|
||||
- 已实现,但交互模式不对
|
||||
|
||||
## 四、理想的Tab补全交互流程
|
||||
|
||||
### 阶段1:输入时(无Tab)
|
||||
```
|
||||
用户输入: cat pa
|
||||
内部状态: 准备suggestions ["package.json", "package-lock.json"]
|
||||
显示: 无变化(或灰色提示)
|
||||
```
|
||||
|
||||
### 阶段2:第一次Tab
|
||||
```
|
||||
用户操作: [Tab]
|
||||
判断逻辑:
|
||||
- 唯一匹配 → 直接补全
|
||||
- 多个匹配但有公共前缀 → 补全到公共前缀
|
||||
- 多个匹配无公共前缀 → 蜂鸣/无反应
|
||||
```
|
||||
|
||||
### 阶段3:第二次Tab
|
||||
```
|
||||
用户操作: [Tab][Tab]
|
||||
行为: 显示所有可能的补全选项
|
||||
格式: 水平排列,按列对齐
|
||||
```
|
||||
|
||||
### 阶段4:选择
|
||||
```
|
||||
继续输入: 缩小范围
|
||||
方向键: 选择(可选)
|
||||
Tab: 循环选择(可选)
|
||||
Enter: 确认选择
|
||||
```
|
||||
|
||||
## 五、改进建议
|
||||
|
||||
### 必须实现(核心体验)
|
||||
1. **双Tab机制** - 第一次补全,第二次显示
|
||||
2. **公共前缀** - 智能补全到最长公共前缀
|
||||
3. **连续补全** - 目录后继续补全
|
||||
4. **更智能的上下文** - 命令感知
|
||||
|
||||
### 应该实现(提升体验)
|
||||
1. **灰色建议** - 输入时显示
|
||||
2. **历史感知** - 基于使用频率排序
|
||||
3. **模糊匹配** - 支持简写
|
||||
4. **路径缓存** - 提升性能
|
||||
|
||||
### 可以实现(锦上添花)
|
||||
1. **语法高亮** - 文件存在性
|
||||
2. **自定义补全** - 用户定义规则
|
||||
3. **异步加载** - 大目录优化
|
||||
4. **补全预览** - 显示文件内容预览
|
||||
|
||||
## 六、技术实现要点
|
||||
|
||||
### Tab计数器
|
||||
```typescript
|
||||
interface TabState {
|
||||
lastTabTime: number
|
||||
tabCount: number
|
||||
lastContext: string
|
||||
}
|
||||
|
||||
// 双击检测:300ms内的第二次Tab
|
||||
if (Date.now() - lastTabTime < 300) {
|
||||
tabCount++
|
||||
} else {
|
||||
tabCount = 1
|
||||
}
|
||||
```
|
||||
|
||||
### 公共前缀算法
|
||||
```typescript
|
||||
function findCommonPrefix(strings: string[]): string {
|
||||
if (!strings.length) return ''
|
||||
return strings.reduce((prefix, str) => {
|
||||
while (!str.startsWith(prefix)) {
|
||||
prefix = prefix.slice(0, -1)
|
||||
}
|
||||
return prefix
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 智能补全决策
|
||||
```typescript
|
||||
function handleTab(suggestions: string[]): Action {
|
||||
if (suggestions.length === 0) {
|
||||
return 'beep'
|
||||
}
|
||||
if (suggestions.length === 1) {
|
||||
return 'complete'
|
||||
}
|
||||
const prefix = findCommonPrefix(suggestions)
|
||||
if (prefix.length > currentWord.length) {
|
||||
return 'complete-to-prefix'
|
||||
}
|
||||
if (isSecondTab()) {
|
||||
return 'show-menu'
|
||||
}
|
||||
return 'beep'
|
||||
}
|
||||
```
|
||||
|
||||
## 七、优先级路线图
|
||||
|
||||
### Phase 1: 核心终端行为(必须)
|
||||
- [ ] 双Tab机制
|
||||
- [ ] 公共前缀补全
|
||||
- [ ] 连续路径补全
|
||||
- [ ] 更准确的上下文检测
|
||||
|
||||
### Phase 2: 现代增强(应该)
|
||||
- [ ] 实时灰色建议
|
||||
- [ ] 历史/频率排序
|
||||
- [ ] 模糊匹配支持
|
||||
|
||||
### Phase 3: 高级功能(可选)
|
||||
- [ ] 通配符展开
|
||||
- [ ] 自定义补全规则
|
||||
- [ ] 异步性能优化
|
||||
136
docs/TERMINAL_TAB_TEST.md
Normal file
136
docs/TERMINAL_TAB_TEST.md
Normal file
@ -0,0 +1,136 @@
|
||||
# 终端Tab补全测试用例
|
||||
|
||||
## ✅ 测试环境准备
|
||||
|
||||
```bash
|
||||
# 创建测试文件
|
||||
echo "test" > package.json
|
||||
echo "test" > package-lock.json
|
||||
echo "test" > README.md
|
||||
echo "test" > README.txt
|
||||
mkdir -p src/components src/hooks src/utils
|
||||
```
|
||||
|
||||
## 📝 测试场景
|
||||
|
||||
### Test 1: 公共前缀补全
|
||||
```bash
|
||||
输入: cat p[Tab]
|
||||
期望: cat package # 补全到公共前缀
|
||||
输入: cat package[Tab]
|
||||
期望: (无反应/蜂鸣) # 无法继续补全
|
||||
输入: cat package[Tab][Tab]
|
||||
期望: 显示菜单:
|
||||
📄 package.json
|
||||
📄 package-lock.json
|
||||
```
|
||||
|
||||
### Test 2: 唯一匹配
|
||||
```bash
|
||||
输入: cat RE[Tab]
|
||||
期望: cat README # 补全到公共前缀
|
||||
输入: cat README.[Tab]
|
||||
期望: (无反应) # 仍有歧义
|
||||
输入: cat README.m[Tab]
|
||||
期望: cat README.md # 唯一匹配,直接完成
|
||||
```
|
||||
|
||||
### Test 3: 目录补全
|
||||
```bash
|
||||
输入: cd s[Tab]
|
||||
期望: cd src/ # 唯一匹配,加斜杠
|
||||
输入: cd src/[Tab]
|
||||
期望: (无反应) # 多个选项
|
||||
输入: cd src/[Tab][Tab]
|
||||
期望: 显示菜单:
|
||||
📁 components/
|
||||
📁 hooks/
|
||||
📁 utils/
|
||||
```
|
||||
|
||||
### Test 4: 命令补全
|
||||
```bash
|
||||
输入: /he[Tab]
|
||||
期望: /help # 唯一匹配
|
||||
输入: /[Tab]
|
||||
期望: (无反应) # 需要双Tab
|
||||
输入: /[Tab][Tab]
|
||||
期望: 显示所有命令
|
||||
```
|
||||
|
||||
### Test 5: Agent补全
|
||||
```bash
|
||||
输入: @agent-c[Tab]
|
||||
期望: @agent-code-writer # 如果唯一
|
||||
或
|
||||
期望: @agent-c # 补全公共前缀
|
||||
输入: @agent-c[Tab][Tab]
|
||||
期望: 显示匹配的agents
|
||||
```
|
||||
|
||||
### Test 6: 连续补全
|
||||
```bash
|
||||
输入: cd src/c[Tab]
|
||||
期望: cd src/components/ # 补全
|
||||
输入: 继续输入 [Tab]
|
||||
期望: 可以继续补全下一级
|
||||
```
|
||||
|
||||
## 🔍 验证要点
|
||||
|
||||
### 核心行为
|
||||
- [ ] 第一次Tab尝试补全,不显示菜单
|
||||
- [ ] 第二次Tab才显示菜单
|
||||
- [ ] 公共前缀自动补全
|
||||
- [ ] 唯一匹配立即完成
|
||||
- [ ] 无匹配时不响应(蜂鸣)
|
||||
|
||||
### 状态管理
|
||||
- [ ] Tab状态在500ms后重置
|
||||
- [ ] 输入改变时重置状态
|
||||
- [ ] 菜单显示后Tab选择项目
|
||||
|
||||
### 边界情况
|
||||
- [ ] 空前缀需要双Tab
|
||||
- [ ] 文件名包含空格的处理
|
||||
- [ ] 路径中的特殊字符
|
||||
|
||||
## 🎯 预期改进
|
||||
|
||||
### Before (现在的问题)
|
||||
```
|
||||
cat p[Tab]
|
||||
▸ package.json # 立即显示菜单 ❌
|
||||
package-lock.json
|
||||
```
|
||||
|
||||
### After (改进后)
|
||||
```
|
||||
cat p[Tab]
|
||||
cat package # 补全公共前缀 ✅
|
||||
cat package[Tab][Tab]
|
||||
package.json package-lock.json # 双Tab显示 ✅
|
||||
```
|
||||
|
||||
## 📊 行为对比表
|
||||
|
||||
| 场景 | Bash/Zsh | 旧实现 | 新实现 |
|
||||
|------|----------|--------|--------|
|
||||
| 多个匹配+Tab | 补全公共前缀 | 显示菜单 | 补全公共前缀 ✅ |
|
||||
| 多个匹配+Tab+Tab | 显示选项 | N/A | 显示选项 ✅ |
|
||||
| 唯一匹配+Tab | 立即完成 | 立即完成 | 立即完成 ✅ |
|
||||
| 无匹配+Tab | 蜂鸣 | 无反应 | 蜂鸣 ✅ |
|
||||
| 目录补全 | 加斜杠 | 加斜杠 | 加斜杠 ✅ |
|
||||
|
||||
## 🚀 性能指标
|
||||
|
||||
- Tab响应时间: <50ms
|
||||
- 双Tab检测窗口: 500ms
|
||||
- 公共前缀计算: O(n*m) 其中n=建议数,m=平均长度
|
||||
|
||||
## 📝 用户体验提升
|
||||
|
||||
1. **减少操作次数**: 公共前缀补全减少输入
|
||||
2. **符合肌肉记忆**: 与终端行为100%一致
|
||||
3. **渐进式显示**: 只在需要时显示菜单
|
||||
4. **智能判断**: 根据上下文做最优选择
|
||||
186
docs/TERMINAL_VS_CURRENT.md
Normal file
186
docs/TERMINAL_VS_CURRENT.md
Normal file
@ -0,0 +1,186 @@
|
||||
# 终端行为对比:标准 Terminal vs 我们的实现
|
||||
|
||||
## 🔴 关键差异对比
|
||||
|
||||
### 场景1:多个匹配文件
|
||||
```bash
|
||||
# 文件列表:package.json, package-lock.json, public/
|
||||
|
||||
# ✅ 真正的终端(bash/zsh)
|
||||
$ cat p[Tab]
|
||||
$ cat p█ # 第一次Tab:没反应(或蜂鸣)
|
||||
$ cat p[Tab][Tab]
|
||||
package.json package-lock.json public/ # 第二次Tab:显示选项
|
||||
$ cat pa[Tab]
|
||||
$ cat package█ # 补全到公共前缀 "package"
|
||||
$ cat package[Tab][Tab]
|
||||
package.json package-lock.json # 再显示剩余选项
|
||||
|
||||
# ❌ 我们当前的实现
|
||||
$ cat p[Tab]
|
||||
▸ 📄 package.json # 立即显示菜单
|
||||
📄 package-lock.json
|
||||
📁 public/
|
||||
```
|
||||
|
||||
### 场景2:唯一匹配
|
||||
```bash
|
||||
# ✅ 真正的终端
|
||||
$ cat READ[Tab]
|
||||
$ cat README.md█ # 立即补全,一步到位
|
||||
|
||||
# ✅ 我们的实现(这个是对的!)
|
||||
$ cat READ[Tab]
|
||||
$ cat README.md█ # 也是立即补全
|
||||
```
|
||||
|
||||
### 场景3:目录补全后继续
|
||||
```bash
|
||||
# ✅ 真正的终端
|
||||
$ cd src[Tab]
|
||||
$ cd src/█ # 补全并加斜杠,光标在斜杠后
|
||||
$ cd src/[Tab] # 可以继续Tab
|
||||
components/ hooks/ utils/ # 显示src/下的内容
|
||||
|
||||
# ❌ 我们的实现
|
||||
$ cd src[Tab]
|
||||
$ cd src/█ # 补全后结束
|
||||
$ cd src/[Tab] # 需要重新检测上下文
|
||||
```
|
||||
|
||||
### 场景4:命令补全
|
||||
```bash
|
||||
# ✅ 真正的终端
|
||||
$ git a[Tab]
|
||||
$ git add█ # 唯一匹配,直接补全
|
||||
|
||||
$ git [Tab][Tab] # 空前缀,需要双Tab
|
||||
add commit push pull status # 显示所有git命令
|
||||
|
||||
# ⚠️ 我们的实现
|
||||
$ /he[Tab]
|
||||
$ /help█ # 命令补全工作正常
|
||||
$ /[Tab] # 但立即显示菜单(应该需要双Tab)
|
||||
▸ help
|
||||
model
|
||||
agents
|
||||
```
|
||||
|
||||
## 📊 行为差异总结
|
||||
|
||||
| 特性 | 标准终端 | 我们的实现 | 影响 |
|
||||
|------|----------|------------|------|
|
||||
| **双Tab显示** | ✅ 需要按两次 | ❌ 第一次就显示 | 打断输入流程 |
|
||||
| **公共前缀** | ✅ 智能补全 | ❌ 直接显示菜单 | 多余的选择步骤 |
|
||||
| **连续补全** | ✅ 自然流畅 | ❌ 需要重新触发 | 效率降低 |
|
||||
| **空前缀Tab** | ✅ 需要双击 | ❌ 立即显示 | 意外触发 |
|
||||
| **蜂鸣反馈** | ✅ 无匹配时蜂鸣 | ❌ 静默 | 缺少反馈 |
|
||||
|
||||
## 🎯 核心问题
|
||||
|
||||
### 1. **Tab计数缺失**
|
||||
```typescript
|
||||
// 我们现在的代码(错误)
|
||||
if (key.tab) {
|
||||
showSuggestions() // 立即显示
|
||||
}
|
||||
|
||||
// 应该是
|
||||
if (key.tab) {
|
||||
if (isSecondTab()) {
|
||||
showSuggestions()
|
||||
} else {
|
||||
tryComplete()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **公共前缀算法缺失**
|
||||
```typescript
|
||||
// 需要添加
|
||||
function getCommonPrefix(items: string[]): string {
|
||||
if (items.length === 0) return ''
|
||||
if (items.length === 1) return items[0]
|
||||
|
||||
let prefix = items[0]
|
||||
for (let i = 1; i < items.length; i++) {
|
||||
while (!items[i].startsWith(prefix)) {
|
||||
prefix = prefix.slice(0, -1)
|
||||
}
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **状态管理不足**
|
||||
```typescript
|
||||
// 当前:无状态
|
||||
// 需要:
|
||||
interface CompletionState {
|
||||
lastTabTime: number
|
||||
tabCount: number
|
||||
lastPrefix: string
|
||||
lastSuggestions: string[]
|
||||
isShowingMenu: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 为什么这些差异重要?
|
||||
|
||||
### 用户期望
|
||||
- **肌肉记忆**:数十年的终端使用习惯
|
||||
- **效率优先**:最少的按键完成任务
|
||||
- **预测性**:行为必须可预测
|
||||
|
||||
### 实际影响
|
||||
1. **输入中断**:过早显示菜单打断思路
|
||||
2. **额外操作**:本可以自动补全的需要手动选择
|
||||
3. **认知负担**:行为不一致增加心智负担
|
||||
|
||||
## 🔧 改进优先级
|
||||
|
||||
### 🔴 必须修复(破坏体验)
|
||||
1. **双Tab机制** - 这是最基础的终端行为
|
||||
2. **公共前缀补全** - 减少不必要的选择
|
||||
3. **连续补全** - 路径导航的基础
|
||||
|
||||
### 🟡 应该改进(提升体验)
|
||||
1. **蜂鸣反馈** - 告诉用户发生了什么
|
||||
2. **灰色提示** - 现代终端的标配
|
||||
3. **历史感知** - 更智能的排序
|
||||
|
||||
### 🟢 可以添加(锦上添花)
|
||||
1. **模糊匹配** - 容错输入
|
||||
2. **预览功能** - 显示文件内容
|
||||
3. **自定义规则** - 用户定制
|
||||
|
||||
## 📝 用户故事
|
||||
|
||||
### 当前体验 😤
|
||||
```
|
||||
我:想输入 "cat package.json"
|
||||
我:输入 "cat p" + Tab
|
||||
系统:弹出菜单让我选择
|
||||
我:需要按方向键选择
|
||||
我:按Enter确认
|
||||
结果:5个操作才完成
|
||||
```
|
||||
|
||||
### 理想体验 😊
|
||||
```
|
||||
我:想输入 "cat package.json"
|
||||
我:输入 "cat pa" + Tab
|
||||
系统:自动补全到 "cat package"
|
||||
我:输入 ".j" + Tab
|
||||
系统:补全到 "cat package.json"
|
||||
结果:3个操作完成
|
||||
```
|
||||
|
||||
## 🎬 下一步
|
||||
|
||||
最小可行改进(MVP):
|
||||
1. 实现Tab计数器
|
||||
2. 添加公共前缀补全
|
||||
3. 修复连续补全
|
||||
|
||||
这三个改动就能让体验提升80%!
|
||||
230
docs/agents-system.md
Normal file
230
docs/agents-system.md
Normal file
@ -0,0 +1,230 @@
|
||||
# Agent Configuration System
|
||||
|
||||
## Overview
|
||||
|
||||
Kode's Agent system allows you to create specialized AI agents with predefined configurations, tools, and prompts. This enables more efficient task execution by using purpose-built agents for specific types of work.
|
||||
|
||||
**New in this version**: Use `@run-agent-name` for intelligent delegation with auto-completion support.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dynamic Agent Loading**: Agents are loaded from configuration files at runtime
|
||||
- **Five-tier Priority System**: Built-in < .claude (user) < .kode (user) < .claude (project) < .kode (project)
|
||||
- **Hot Reload**: Agent files monitored via Node.js fs.watch with cache invalidation
|
||||
- **Tool Restrictions**: Limit agents to specific tools for security and focus
|
||||
- **Model Selection**: Each agent can specify its preferred AI model
|
||||
- **Interactive Management**: Use `/agents` command for graphical management
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Pre-configured Agents
|
||||
|
||||
Kode has one built-in agent:
|
||||
|
||||
```bash
|
||||
@run-agent-general-purpose Find all TypeScript test files
|
||||
@run-agent-my-custom-agent Implement a new user authentication feature
|
||||
```
|
||||
|
||||
### Managing Agents
|
||||
|
||||
Use the `/agents` command in the Kode REPL to manage agents:
|
||||
|
||||
```bash
|
||||
kode
|
||||
> /agents
|
||||
```
|
||||
|
||||
This opens an interactive UI where you can:
|
||||
- View all available agents
|
||||
- Create new agents
|
||||
- Edit existing agents
|
||||
- Delete custom agents
|
||||
|
||||
Keyboard shortcuts:
|
||||
- `c` - Create new agent
|
||||
- `r` - Reload agents
|
||||
- `d` - Delete selected agent (when viewing)
|
||||
- `q` or `Esc` - Exit
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
### File Structure
|
||||
|
||||
Agents are defined as Markdown files with YAML frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: agent-name
|
||||
description: "When to use this agent"
|
||||
tools: ["Tool1", "Tool2", "Tool3"] # or "*" for all tools
|
||||
model_name: model-name # optional (preferred over deprecated 'model' field)
|
||||
---
|
||||
|
||||
System prompt content goes here...
|
||||
```
|
||||
|
||||
**Note**: Use `model_name` to specify the AI model. The deprecated `model` field is ignored.
|
||||
|
||||
### Configuration Locations
|
||||
|
||||
Agents can be defined at five levels with priority order (later overrides earlier):
|
||||
|
||||
1. **Built-in** (lowest priority)
|
||||
- Provided with Kode
|
||||
- Cannot be modified
|
||||
|
||||
2. **Claude User** (`~/.claude/agents/`)
|
||||
- Claude Code compatible user-level agents
|
||||
- Personal agents available across all projects
|
||||
|
||||
3. **Kode User** (`~/.kode/agents/`)
|
||||
- Kode-specific user-level agents
|
||||
- Overrides Claude user agents with same name
|
||||
- Create with `/agents` command or manually
|
||||
|
||||
4. **Claude Project** (`./.claude/agents/`)
|
||||
- Claude Code compatible project-specific agents
|
||||
- Overrides user-level agents
|
||||
|
||||
5. **Kode Project** (`./.kode/agents/`)
|
||||
- Kode-specific project agents
|
||||
- Highest priority, overrides all others
|
||||
|
||||
### Example: Creating a Custom Agent
|
||||
|
||||
#### 1. Manual Creation
|
||||
|
||||
Create a file `~/.kode/agents/api-designer.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: api-designer
|
||||
description: "Designs RESTful APIs and GraphQL schemas with best practices"
|
||||
tools: ["FileRead", "FileWrite", "Grep"]
|
||||
model_name: reasoning
|
||||
---
|
||||
|
||||
You are an API design specialist. Your expertise includes:
|
||||
|
||||
- Designing RESTful APIs following OpenAPI specifications
|
||||
- Creating GraphQL schemas with efficient resolvers
|
||||
- Implementing proper authentication and authorization
|
||||
- Ensuring API versioning and backward compatibility
|
||||
- Writing comprehensive API documentation
|
||||
|
||||
Design principles:
|
||||
- Follow REST best practices (proper HTTP verbs, status codes, etc.)
|
||||
- Design for scalability and performance
|
||||
- Include proper error handling and validation
|
||||
- Consider rate limiting and caching strategies
|
||||
- Maintain consistency across endpoints
|
||||
```
|
||||
|
||||
#### 2. Using /agents Command
|
||||
|
||||
1. Run `kode` to start the REPL
|
||||
2. Type `/agents`
|
||||
3. Press `c` to create
|
||||
4. Follow the prompts:
|
||||
- Enter agent name
|
||||
- Describe when to use it
|
||||
- Specify allowed tools
|
||||
- Optionally specify a model
|
||||
- Write the system prompt
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Tool Restrictions
|
||||
|
||||
```yaml
|
||||
tools: ["FileRead", "Grep", "Glob"] # Specific tools
|
||||
tools: ["*"] # All tools (default)
|
||||
```
|
||||
|
||||
### Model Selection
|
||||
|
||||
Specify which AI model the agent should use:
|
||||
|
||||
```yaml
|
||||
model_name: quick # Fast responses for simple tasks
|
||||
model_name: main # Default model for general tasks
|
||||
model_name: reasoning # Complex analysis and design
|
||||
```
|
||||
|
||||
|
||||
## Available Built-in Agents
|
||||
|
||||
### general-purpose
|
||||
- **Use for**: General research, complex multi-step tasks
|
||||
- **Tools**: All tools
|
||||
- **Model**: task (default)
|
||||
|
||||
Note: This is currently the only built-in agent. Create custom agents using the `/agents` command or by adding configuration files.
|
||||
|
||||
## Custom Agents
|
||||
|
||||
Create your own agents in the appropriate directory:
|
||||
|
||||
```bash
|
||||
mkdir -p .kode/agents # Project-specific
|
||||
mkdir -p ~/.kode/agents # User-wide
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Agent Naming**: Use descriptive, action-oriented names (e.g., `test-writer`, `api-designer`)
|
||||
|
||||
2. **Tool Selection**: Only include tools the agent actually needs
|
||||
|
||||
3. **System Prompts**: Be specific about the agent's role and guidelines
|
||||
|
||||
4. **Model Choice**:
|
||||
- Use `quick` for simple, fast operations
|
||||
- Use `main` for general coding tasks
|
||||
- Use `reasoning` for complex analysis
|
||||
|
||||
5. **Organization**:
|
||||
- Keep user agents for personal workflows
|
||||
- Keep project agents for team-shared configurations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agents not loading?
|
||||
- Check file permissions in `~/.kode/agents/` or `./.kode/agents/`
|
||||
- Ensure YAML frontmatter is valid
|
||||
- Use `/agents` command and press `r` to reload
|
||||
|
||||
### Agent not working as expected?
|
||||
- Verify the tools list includes necessary tools
|
||||
- Check the system prompt is clear and specific
|
||||
- Test with verbose mode to see actual prompts
|
||||
|
||||
### Hot reload not working?
|
||||
- File watcher requires proper file system events
|
||||
- Try manual reload with `/agents` then `r`
|
||||
- Restart Kode if needed
|
||||
|
||||
## Integration with Task Tool
|
||||
|
||||
The agent system is integrated with Kode's Task tool:
|
||||
|
||||
```typescript
|
||||
// In your code or scripts
|
||||
await TaskTool.call({
|
||||
description: "Search for patterns",
|
||||
prompt: "Find all instances of TODO comments",
|
||||
subagent_type: "general-purpose"
|
||||
})
|
||||
```
|
||||
|
||||
This allows programmatic use of agents in automation and scripts.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements:
|
||||
- Agent templates and inheritance
|
||||
- Performance metrics per agent
|
||||
- Agent composition (agents using other agents)
|
||||
- Cloud-based agent sharing
|
||||
- Agent versioning and rollback
|
||||
@ -109,7 +109,7 @@ cli.tsx (入口点)
|
||||
```
|
||||
用户提示
|
||||
↓
|
||||
上下文注入 (KODE.md, git 状态等)
|
||||
上下文注入 (AGENTS.md, git 状态等)
|
||||
↓
|
||||
模型选择 (基于上下文大小)
|
||||
↓
|
||||
@ -218,7 +218,7 @@ interface Tool {
|
||||
|
||||
### 5. 上下文管理
|
||||
自动上下文注入:
|
||||
- 项目文件 (KODE.md, CLAUDE.md)
|
||||
- 项目文件 (AGENTS.md, CLAUDE.md)
|
||||
- Git 状态和最近的提交
|
||||
- 目录结构
|
||||
- 先前的对话历史
|
||||
|
||||
@ -470,11 +470,8 @@ function loadConfig(path: string): Config {
|
||||
### 调试命令
|
||||
|
||||
```bash
|
||||
# 显示有效配置
|
||||
kode config list --effective
|
||||
|
||||
# 验证配置
|
||||
kode config validate
|
||||
# 显示配置
|
||||
kode config list
|
||||
|
||||
# 重置为默认值
|
||||
kode config reset
|
||||
|
||||
@ -26,7 +26,7 @@ Kode 中的所有内容都被抽象为"工具" - 一个自包含的功能单元
|
||||
AI 自动通过以下方式理解您的项目:
|
||||
- Git 状态和最近的提交
|
||||
- 目录结构分析
|
||||
- KODE.md 和 CLAUDE.md 项目文档
|
||||
- AGENTS.md 和 CLAUDE.md 项目文档
|
||||
- .claude/commands/ 和 .kode/commands/ 中的自定义命令定义
|
||||
- 先前的对话历史和分叉对话
|
||||
|
||||
|
||||
@ -379,9 +379,6 @@ interface PermissionRequest {
|
||||
|
||||
2. **调查**
|
||||
```bash
|
||||
# 审查审计日志
|
||||
kode security audit --last 1h
|
||||
|
||||
# 检查修改的文件
|
||||
git status
|
||||
git diff
|
||||
|
||||
@ -109,7 +109,7 @@ cli.tsx (Entry Point)
|
||||
```
|
||||
User Prompt
|
||||
↓
|
||||
Context Injection (KODE.md, git status, etc.)
|
||||
Context Injection (AGENTS.md, git status, etc.)
|
||||
↓
|
||||
Model Selection (based on context size)
|
||||
↓
|
||||
@ -218,7 +218,7 @@ Multi-level permission system:
|
||||
|
||||
### 5. Context Management
|
||||
Automatic context injection:
|
||||
- Project files (KODE.md, CLAUDE.md)
|
||||
- Project files (AGENTS.md, CLAUDE.md)
|
||||
- Git status and recent commits
|
||||
- Directory structure
|
||||
- Previous conversation history
|
||||
|
||||
@ -470,11 +470,8 @@ Temporary for current session:
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Show effective configuration
|
||||
kode config list --effective
|
||||
|
||||
# Validate configuration
|
||||
kode config validate
|
||||
# Show configuration
|
||||
kode config list
|
||||
|
||||
# Reset to defaults
|
||||
kode config reset
|
||||
|
||||
@ -57,7 +57,7 @@ interface CompleteContext {
|
||||
dependencies?: Dependencies
|
||||
|
||||
// Documentation
|
||||
contextFile?: string // KODE.md content
|
||||
contextFile?: string // AGENTS.md content
|
||||
claudeFile?: string // CLAUDE.md content
|
||||
readmeContent?: string // README.md content
|
||||
|
||||
@ -192,15 +192,15 @@ class ProjectAnalyzer {
|
||||
|
||||
## Context Files
|
||||
|
||||
### KODE.md
|
||||
### AGENTS.md
|
||||
|
||||
```typescript
|
||||
class ContextFileLoader {
|
||||
private readonly CONTEXT_PATHS = [
|
||||
'KODE.md',
|
||||
'.claude/KODE.md',
|
||||
'docs/KODE.md',
|
||||
'.github/KODE.md'
|
||||
'AGENTS.md',
|
||||
'.claude/AGENTS.md',
|
||||
'docs/AGENTS.md',
|
||||
'.github/AGENTS.md'
|
||||
]
|
||||
|
||||
async loadContextFile(): Promise<string | null> {
|
||||
|
||||
@ -26,7 +26,7 @@ Unlike web-based AI assistants, Kode is built specifically for terminal workflow
|
||||
The AI automatically understands your project through:
|
||||
- Git status and recent commits
|
||||
- Directory structure analysis
|
||||
- KODE.md and CLAUDE.md project documentation
|
||||
- AGENTS.md and CLAUDE.md project documentation
|
||||
- Custom command definitions in .claude/commands/ and .kode/commands/
|
||||
- Previous conversation history and forked conversations
|
||||
|
||||
|
||||
@ -379,9 +379,6 @@ interface PermissionRequest {
|
||||
|
||||
2. **Investigation**
|
||||
```bash
|
||||
# Review audit logs
|
||||
kode security audit --last 1h
|
||||
|
||||
# Check modified files
|
||||
git status
|
||||
git diff
|
||||
|
||||
166
docs/intelligent-completion.md
Normal file
166
docs/intelligent-completion.md
Normal file
@ -0,0 +1,166 @@
|
||||
# Intelligent Completion System
|
||||
|
||||
## Overview
|
||||
|
||||
Kode features a state-of-the-art intelligent completion system that revolutionizes terminal interaction with AI agents and commands. The system uses advanced fuzzy matching algorithms inspired by Chinese input methods, modern IDEs, and terminal fuzzy finders.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Advanced Fuzzy Matching Algorithm
|
||||
|
||||
Our custom `advancedFuzzyMatcher` combines multiple matching strategies:
|
||||
|
||||
- **Exact Prefix Matching** - Highest priority for exact starts
|
||||
- **Hyphen-Aware Matching** - Treats hyphens as optional word boundaries
|
||||
- **Word Boundary Detection** - Matches characters at word starts
|
||||
- **Abbreviation Matching** - Supports shortcuts like `dq` → `dao-qi`
|
||||
- **Numeric Suffix Handling** - Intelligently matches `py3` → `python3`
|
||||
- **Subsequence Matching** - Characters appear in order
|
||||
- **Fuzzy Segment Matching** - Flexible segment matching
|
||||
|
||||
### 2. Smart Context Detection
|
||||
|
||||
The system automatically detects context without requiring special prefixes:
|
||||
|
||||
```bash
|
||||
# Type without @, system adds it automatically
|
||||
gp5 → @ask-gpt-5
|
||||
daoqi → @run-agent-dao-qi-harmony-designer
|
||||
py3 → python3
|
||||
|
||||
# Tab key fills the match with appropriate prefix
|
||||
# Enter key completes and adds space
|
||||
```
|
||||
|
||||
### 3. Unix Command Intelligence
|
||||
|
||||
#### Common Commands Database
|
||||
- 500+ curated common Unix/Linux commands
|
||||
- Categories: File operations, text processing, development tools, network utilities, etc.
|
||||
- Smart intersection with system PATH - only shows commands that actually exist
|
||||
|
||||
#### Priority Scoring
|
||||
Commands are ranked by:
|
||||
1. Match quality score
|
||||
2. Common usage frequency
|
||||
3. Position in command database
|
||||
|
||||
### 4. Multi-Source Completion
|
||||
|
||||
The system seamlessly combines completions from:
|
||||
- **Slash Commands** (`/help`, `/model`, etc.)
|
||||
- **Agent Mentions** (`@run-agent-*`)
|
||||
- **Model Consultations** (`@ask-*`)
|
||||
- **Unix Commands** (from system PATH)
|
||||
- **File Paths** (directories and files)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
```
|
||||
src/
|
||||
├── utils/
|
||||
│ ├── advancedFuzzyMatcher.ts # Advanced matching algorithms
|
||||
│ ├── fuzzyMatcher.ts # Original matcher (facade)
|
||||
│ └── commonUnixCommands.ts # Unix command database
|
||||
└── hooks/
|
||||
└── useUnifiedCompletion.ts # Main completion hook
|
||||
```
|
||||
|
||||
### Algorithm Details
|
||||
|
||||
#### Hyphen-Aware Matching
|
||||
```typescript
|
||||
// Handles: dao → dao-qi-harmony-designer
|
||||
// Split by hyphens and match flexibly
|
||||
const words = text.split('-')
|
||||
// Check concatenated version (ignoring hyphens)
|
||||
const concatenated = words.join('')
|
||||
```
|
||||
|
||||
#### Numeric Suffix Matching
|
||||
```typescript
|
||||
// Handles: py3 → python3
|
||||
const patternMatch = pattern.match(/^(.+?)(\d+)$/)
|
||||
if (text.endsWith(suffix) && textWithoutSuffix.startsWith(prefix)) {
|
||||
// High score for numeric suffix match
|
||||
}
|
||||
```
|
||||
|
||||
#### Word Boundary Matching
|
||||
```typescript
|
||||
// Handles: dq → dao-qi
|
||||
// Match characters at word boundaries
|
||||
for (const word of words) {
|
||||
if (word[0] === pattern[patternIdx]) {
|
||||
score += 50 // Bonus for word boundary
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Fuzzy Matching
|
||||
```bash
|
||||
# Abbreviations
|
||||
nde → node
|
||||
np → npm
|
||||
dk → docker
|
||||
|
||||
# Partial matches
|
||||
kub → kubectl
|
||||
vim → vim, nvim
|
||||
|
||||
# Numeric patterns
|
||||
py3 → python3
|
||||
n18 → node18
|
||||
```
|
||||
|
||||
### Agent/Model Matching
|
||||
```bash
|
||||
# Without @ prefix (auto-added on completion)
|
||||
gp5 → @ask-gpt-5
|
||||
claude → @ask-claude-sonnet-4
|
||||
dao → @run-agent-dao-qi-harmony-designer
|
||||
daoqi → @run-agent-dao-qi-harmony-designer
|
||||
```
|
||||
|
||||
### Smart Prioritization
|
||||
```bash
|
||||
# Input: "doc"
|
||||
1. docker (common command, high priority)
|
||||
2. document (if exists)
|
||||
3. doctor (if exists)
|
||||
|
||||
# Input: "g"
|
||||
1. git (most common)
|
||||
2. grep (common)
|
||||
3. go (if installed)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Minimum Score Threshold
|
||||
The system uses a minimum score of 5 (very low) to allow flexible matching while filtering noise.
|
||||
|
||||
### Match Ranking
|
||||
Results are sorted by:
|
||||
1. Match algorithm score
|
||||
2. Command priority (for Unix commands)
|
||||
3. Type priority (agents/models > files > commands)
|
||||
|
||||
## Performance
|
||||
|
||||
- **Sub-millisecond matching** - Optimized algorithms for instant feedback
|
||||
- **Lazy loading** - Commands loaded on first use
|
||||
- **Smart caching** - Results cached per session
|
||||
- **Efficient filtering** - Early termination for obvious non-matches
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- [ ] Learning from user selections
|
||||
- [ ] Project-specific command priorities
|
||||
- [ ] Custom abbreviation definitions
|
||||
- [ ] Typo correction with edit distance
|
||||
- [ ] Context-aware suggestions based on recent commands
|
||||
222
docs/mention-system.md
Normal file
222
docs/mention-system.md
Normal file
@ -0,0 +1,222 @@
|
||||
# @ Mention System
|
||||
|
||||
## Overview
|
||||
|
||||
Kode's @ mention system provides intelligent auto-completion and smart delegation for models, agents, and files. This unified interface makes it easy to reference different resources and trigger appropriate actions.
|
||||
|
||||
## Features
|
||||
|
||||
- 🦜 **Expert Model Consultation** - `@ask-model-name`
|
||||
- 👤 **Agent Delegation** - `@run-agent-name`
|
||||
- 📁 **File References** - `@path/to/file`
|
||||
- ⚡ **Smart Completion** - Real-time suggestions as you type
|
||||
- 🔍 **Context-Aware** - Shows relevant options based on input
|
||||
|
||||
## Mention Types
|
||||
|
||||
### 🦜 Expert Model Consultation (`@ask-model-name`)
|
||||
|
||||
Consult specific AI models for specialized analysis and expert opinions.
|
||||
|
||||
**Format**: `@ask-{model-name}`
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
@ask-claude-sonnet-4 How should I optimize this React component?
|
||||
@ask-gpt-5 What are the security implications of this API design?
|
||||
@ask-o1-preview Analyze the time complexity of this algorithm
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
- Triggers `AskExpertModelTool`
|
||||
- Model receives only your question (no conversation history)
|
||||
- Requires complete, self-contained questions
|
||||
- Ideal for getting fresh perspectives from different models
|
||||
|
||||
### 👤 Agent Delegation (`@run-agent-name`)
|
||||
|
||||
Delegate tasks to specialized subagents with predefined capabilities.
|
||||
|
||||
**Format**: `@run-agent-{agent-type}`
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
@run-agent-general-purpose Review this code for over-engineering
|
||||
@run-agent-my-custom-agent Design a microservices architecture
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
- Triggers `TaskTool` with specified subagent
|
||||
- Agent has access to project context and tools
|
||||
- Uses agent's specialized prompt and model preferences
|
||||
- Ideal for focused, expert-level task execution
|
||||
|
||||
### 📁 File References (`@path/to/file`)
|
||||
|
||||
Reference files and directories with intelligent path completion.
|
||||
|
||||
**Format**: `@{file-path}`
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
@src/components/Button.tsx
|
||||
@docs/api-reference.md
|
||||
@package.json
|
||||
@README.md
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
- Shows file/directory structure as you type
|
||||
- Supports relative and absolute paths
|
||||
- Integrates with file reading tools
|
||||
- Provides context for file-based discussions
|
||||
|
||||
## Smart Completion UI
|
||||
|
||||
### Completion Priority
|
||||
|
||||
1. **🦜 Ask Models** (Score: 90) - Expert consultation options
|
||||
2. **👤 Run Agents** (Score: 85) - Available subagents
|
||||
3. **📁 Files** (Score: 70-80) - Project files and directories
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
- **Tab** - Cycle through suggestions or complete partial matches
|
||||
- **↑/↓** - Navigate suggestion list
|
||||
- **Enter** - Select highlighted suggestion
|
||||
- **Esc** - Close completion menu
|
||||
- **Space** - Complete and continue (for directories)
|
||||
|
||||
### Visual Indicators
|
||||
|
||||
- 🦜 - Expert model consultation
|
||||
- 👤 - Agent delegation
|
||||
- 📁 - Directory
|
||||
- 📄 - File
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Mention Processing Pipeline
|
||||
|
||||
1. **Pattern Matching** - Regular expressions detect @ask-, @run-agent-, and @file patterns
|
||||
2. **Event Emission** - MentionProcessor emits events to SystemReminder service
|
||||
3. **System Reminder Generation** - Creates tool-specific guidance messages
|
||||
4. **Tool Invocation** - AI selects appropriate tool based on reminder context
|
||||
|
||||
### Supported Patterns
|
||||
|
||||
```typescript
|
||||
// Recognized patterns
|
||||
/@(ask-[\w\-]+)/g // @ask-model-name
|
||||
/@(run-agent-[\w\-]+)/g // @run-agent-name
|
||||
/@(agent-[\w\-]+)/g // @agent-name (legacy)
|
||||
/@([a-zA-Z0-9/._-]+)/g // @file/path
|
||||
```
|
||||
|
||||
### Email Protection
|
||||
|
||||
The system intelligently detects email addresses and treats them as regular text:
|
||||
```bash
|
||||
user@domain.com # Treated as regular text, no completion
|
||||
@ask-claude # Triggers completion
|
||||
```
|
||||
|
||||
## Legacy Support
|
||||
|
||||
### Legacy Support
|
||||
|
||||
- `@agent-name` format supported by agentMentionDetector
|
||||
- `@run-agent-name` format supported by mentionProcessor
|
||||
- Both patterns trigger TaskTool with subagent_type parameter
|
||||
|
||||
### Migration Guide
|
||||
|
||||
```bash
|
||||
# Old format (still works)
|
||||
@my-agent
|
||||
|
||||
# New format (recommended)
|
||||
@run-agent-my-agent
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Available Models
|
||||
|
||||
Models are loaded dynamically from your configuration:
|
||||
```bash
|
||||
# View configured models
|
||||
/model
|
||||
|
||||
# Models appear in @ask- completions automatically
|
||||
```
|
||||
|
||||
### Available Agents
|
||||
|
||||
Agents are loaded from multiple sources:
|
||||
- Built-in agents (only general-purpose currently available)
|
||||
- User agents (`~/.kode/agents/`)
|
||||
- Project agents (`./.kode/agents/`)
|
||||
|
||||
```bash
|
||||
# View available agents
|
||||
/agents
|
||||
|
||||
# Create new agent
|
||||
/agents -> c (create)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Expert Model Consultation
|
||||
|
||||
1. **Provide Complete Context**: Include all relevant background information
|
||||
2. **Structure Questions**: Background → Situation → Question
|
||||
3. **Be Specific**: Ask for particular types of analysis or perspectives
|
||||
4. **Use Right Model**: Choose models based on their strengths
|
||||
|
||||
### For Agent Delegation
|
||||
|
||||
1. **Match Task to Agent**: Use specialists for their expertise areas
|
||||
2. **Clear Instructions**: Provide specific, actionable task descriptions
|
||||
3. **Context Awareness**: Agents have project context, use it effectively
|
||||
4. **Tool Permissions**: Ensure agents have necessary tool access
|
||||
|
||||
### For File References
|
||||
|
||||
1. **Use Auto-completion**: Let the system suggest valid paths
|
||||
2. **Relative Paths**: Prefer relative paths for project portability
|
||||
3. **Context Clarity**: Explain what you want to do with the file
|
||||
4. **Multiple Files**: Reference multiple files when needed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Completion Not Working?
|
||||
|
||||
- Check if you're in the terminal input area
|
||||
- Ensure @ is at the start of a word boundary
|
||||
- Try typing more characters to trigger completion
|
||||
- Restart Kode if completion seems stuck
|
||||
|
||||
### Models/Agents Not Appearing?
|
||||
|
||||
- Verify model configuration with `/model`
|
||||
- Check agent configurations with `/agents`
|
||||
- Ensure proper file permissions for agent directories
|
||||
- Try reloading agents with `/agents` → `r`
|
||||
|
||||
### Wrong Tool Being Selected?
|
||||
|
||||
- Check system reminder events in verbose mode
|
||||
- Verify mention format matches expected patterns
|
||||
- Ensure agent configurations are valid
|
||||
- Review tool descriptions for conflicts
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements:
|
||||
- **Fuzzy Matching** - Better completion matching
|
||||
- **Context Hints** - Show tool descriptions in completions
|
||||
- **Custom Shortcuts** - User-defined @ shortcuts
|
||||
- **Completion Analytics** - Track most-used mentions
|
||||
- **Multi-file Selection** - Select multiple files at once
|
||||
@ -66,9 +66,12 @@
|
||||
"env-paths": "^3.0.0",
|
||||
"figures": "^6.1.0",
|
||||
"glob": "^11.0.3",
|
||||
"gray-matter": "^4.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "^5.2.1",
|
||||
"ink-link": "^4.1.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^11.1.0",
|
||||
"marked": "^15.0.12",
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { z } from 'zod'
|
||||
import * as React from 'react'
|
||||
|
||||
// DEPRECATED: Use domain/tool/Tool.interface.ts for new implementations
|
||||
// This interface will be maintained for compatibility during transition
|
||||
|
||||
export type SetToolJSXFn = (jsx: {
|
||||
jsx: React.ReactNode | null
|
||||
shouldHidePromptInput: boolean
|
||||
|
||||
@ -22,6 +22,7 @@ import review from './commands/review'
|
||||
import terminalSetup from './commands/terminalSetup'
|
||||
import { Tool, ToolUseContext } from './Tool'
|
||||
import resume from './commands/resume'
|
||||
import agents from './commands/agents'
|
||||
import { getMCPCommands } from './services/mcpClient'
|
||||
import { loadCustomCommands } from './services/customCommands'
|
||||
import type { MessageParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
@ -80,6 +81,7 @@ const INTERNAL_ONLY_COMMANDS = [ctx_viz, resume, listen]
|
||||
// Declared as a function so that we don't run this until getCommands is called,
|
||||
// since underlying functions read from config, which can't be read at module initialization time
|
||||
const COMMANDS = memoize((): Command[] => [
|
||||
agents,
|
||||
clear,
|
||||
compact,
|
||||
config,
|
||||
|
||||
3401
src/commands/agents.tsx
Normal file
3401
src/commands/agents.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -52,17 +52,17 @@ export function isShiftEnterKeyBindingInstalled(): boolean {
|
||||
}
|
||||
|
||||
export function handleHashCommand(interpreted: string): void {
|
||||
// Appends the AI-interpreted content to both KODE.md and CLAUDE.md (if exists)
|
||||
// Appends the AI-interpreted content to both AGENTS.md and CLAUDE.md (if exists)
|
||||
try {
|
||||
const cwd = process.cwd()
|
||||
const codeContextPath = join(cwd, 'KODE.md')
|
||||
const codeContextPath = join(cwd, 'AGENTS.md')
|
||||
const claudePath = join(cwd, 'CLAUDE.md')
|
||||
|
||||
// Check which files exist and update them
|
||||
const filesToUpdate = []
|
||||
|
||||
// Always try to update KODE.md (create if not exists)
|
||||
filesToUpdate.push({ path: codeContextPath, name: 'KODE.md' })
|
||||
// Always try to update AGENTS.md (create if not exists)
|
||||
filesToUpdate.push({ path: codeContextPath, name: 'AGENTS.md' })
|
||||
|
||||
// Update CLAUDE.md only if it exists
|
||||
try {
|
||||
|
||||
@ -5,7 +5,7 @@ import * as React from 'react'
|
||||
import { type Message } from '../query'
|
||||
import { processUserInput } from '../utils/messages'
|
||||
import { useArrowKeyHistory } from '../hooks/useArrowKeyHistory'
|
||||
import { useSlashCommandTypeahead } from '../hooks/useSlashCommandTypeahead'
|
||||
import { useUnifiedCompletion } from '../hooks/useUnifiedCompletion'
|
||||
import { addToHistory } from '../history'
|
||||
import TextInput from './TextInput'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
@ -165,19 +165,53 @@ function PromptInput({
|
||||
[commands],
|
||||
)
|
||||
|
||||
// Unified completion system - one hook to rule them all (now with terminal behavior)
|
||||
const {
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
updateSuggestions,
|
||||
clearSuggestions,
|
||||
} = useSlashCommandTypeahead({
|
||||
commands,
|
||||
selectedIndex,
|
||||
isActive: completionActive,
|
||||
emptyDirMessage,
|
||||
} = useUnifiedCompletion({
|
||||
input,
|
||||
cursorOffset,
|
||||
onInputChange,
|
||||
onSubmit,
|
||||
setCursorOffset,
|
||||
currentInput: input,
|
||||
commands,
|
||||
onSubmit,
|
||||
})
|
||||
|
||||
// Get theme early for memoized rendering
|
||||
const theme = getTheme()
|
||||
|
||||
// Memoized completion suggestions rendering - after useUnifiedCompletion
|
||||
const renderedSuggestions = useMemo(() => {
|
||||
if (suggestions.length === 0) return null
|
||||
|
||||
return suggestions.map((suggestion, index) => {
|
||||
const isSelected = index === selectedIndex
|
||||
const isAgent = suggestion.type === 'agent'
|
||||
|
||||
// Simple color logic without complex lookups
|
||||
const displayColor = isSelected
|
||||
? theme.suggestion
|
||||
: (isAgent && suggestion.metadata?.color)
|
||||
? suggestion.metadata.color
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Box key={`${suggestion.type}-${suggestion.value}-${index}`} flexDirection="row">
|
||||
<Text
|
||||
color={displayColor}
|
||||
dimColor={!isSelected && !displayColor}
|
||||
>
|
||||
{isSelected ? '◆ ' : ' '}
|
||||
{suggestion.displayValue}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
}, [suggestions, selectedIndex, theme.suggestion])
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: string) => {
|
||||
if (value.startsWith('!')) {
|
||||
@ -188,10 +222,9 @@ function PromptInput({
|
||||
onModeChange('koding')
|
||||
return
|
||||
}
|
||||
updateSuggestions(value)
|
||||
onInputChange(value)
|
||||
},
|
||||
[onModeChange, onInputChange, updateSuggestions],
|
||||
[onModeChange, onInputChange],
|
||||
)
|
||||
|
||||
// Handle Tab key model switching with simple context check
|
||||
@ -237,15 +270,15 @@ function PromptInput({
|
||||
input,
|
||||
)
|
||||
|
||||
// Only use history navigation when there are 0 or 1 slash command suggestions
|
||||
// Only use history navigation when there are no suggestions
|
||||
const handleHistoryUp = () => {
|
||||
if (suggestions.length <= 1) {
|
||||
if (!completionActive) {
|
||||
onHistoryUp()
|
||||
}
|
||||
}
|
||||
|
||||
const handleHistoryDown = () => {
|
||||
if (suggestions.length <= 1) {
|
||||
if (!completionActive) {
|
||||
onHistoryDown()
|
||||
}
|
||||
}
|
||||
@ -269,7 +302,7 @@ function PromptInput({
|
||||
|
||||
// Create additional context to inform Claude this is for KODING.md
|
||||
const kodingContext =
|
||||
'The user is using Koding mode. Format your response as a comprehensive, well-structured document suitable for adding to KODE.md. Use proper markdown formatting with headings, lists, code blocks, etc. The response should be complete and ready to add to KODE.md documentation.'
|
||||
'The user is using Koding mode. Format your response as a comprehensive, well-structured document suitable for adding to AGENTS.md. Use proper markdown formatting with headings, lists, code blocks, etc. The response should be complete and ready to add to AGENTS.md documentation.'
|
||||
|
||||
// Switch to prompt mode but tag the submission for later capture
|
||||
onModeChange('prompt')
|
||||
@ -325,7 +358,7 @@ function PromptInput({
|
||||
}
|
||||
}
|
||||
|
||||
// If in koding mode or input starts with '#', interpret it using AI before appending to KODE.md
|
||||
// If in koding mode or input starts with '#', interpret it using AI before appending to AGENTS.md
|
||||
else if (mode === 'koding' || input.startsWith('#')) {
|
||||
try {
|
||||
// Strip the # if we're in koding mode and the user didn't type it (since it's implied)
|
||||
@ -353,7 +386,12 @@ function PromptInput({
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
if (suggestions.length > 0 && !isSubmittingSlashCommand) {
|
||||
|
||||
// Handle Enter key when completions are active
|
||||
// If there are suggestions showing, Enter should complete the selection, not send the message
|
||||
if (suggestions.length > 0 && completionActive) {
|
||||
// The completion is handled by useUnifiedCompletion hook
|
||||
// Just return to prevent message sending
|
||||
return
|
||||
}
|
||||
|
||||
@ -372,7 +410,7 @@ function PromptInput({
|
||||
}
|
||||
onInputChange('')
|
||||
onModeChange('prompt')
|
||||
clearSuggestions()
|
||||
// Suggestions are now handled by unified completion
|
||||
setPastedImage(null)
|
||||
setPastedText(null)
|
||||
onSubmitCountChange(_ => _ + 1)
|
||||
@ -463,18 +501,11 @@ function PromptInput({
|
||||
return true // Explicitly handled
|
||||
}
|
||||
|
||||
// Tab key for model switching (simple and non-conflicting)
|
||||
if (key.tab && !key.shift) {
|
||||
handleQuickModelSwitch()
|
||||
return true // Explicitly handled
|
||||
}
|
||||
|
||||
return false // Not handled, allow other hooks
|
||||
})
|
||||
|
||||
const textInputColumns = useTerminalSize().columns - 6
|
||||
const tokenUsage = useMemo(() => countTokens(messages), [messages])
|
||||
const theme = getTheme()
|
||||
|
||||
// 🔧 Fix: Track model ID changes to detect external config updates
|
||||
const modelManager = getModelManager()
|
||||
@ -558,14 +589,22 @@ function PromptInput({
|
||||
onImagePaste={onImagePaste}
|
||||
columns={textInputColumns}
|
||||
isDimmed={isDisabled || isLoading}
|
||||
disableCursorMovementForUpDownKeys={suggestions.length > 0}
|
||||
disableCursorMovementForUpDownKeys={completionActive}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
onPaste={onTextPaste}
|
||||
onSpecialKey={(input, key) => {
|
||||
// Handle Shift+M for model switching
|
||||
if (key.shift && (input === 'M' || input === 'm')) {
|
||||
handleQuickModelSwitch()
|
||||
return true // Prevent the 'M' from being typed
|
||||
}
|
||||
return false
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{suggestions.length === 0 && (
|
||||
{!completionActive && suggestions.length === 0 && (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
@ -591,10 +630,10 @@ function PromptInput({
|
||||
color={mode === 'koding' ? theme.koding : undefined}
|
||||
dimColor={mode !== 'koding'}
|
||||
>
|
||||
· # for KODE.md
|
||||
· # for AGENTS.md
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
· / for commands · tab to switch model · esc to undo
|
||||
· / for commands · shift+m to switch model · esc to undo
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
@ -624,6 +663,7 @@ function PromptInput({
|
||||
} />
|
||||
</Box>
|
||||
)}
|
||||
{/* Unified completion suggestions - optimized rendering */}
|
||||
{suggestions.length > 0 && (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
@ -632,56 +672,26 @@ function PromptInput({
|
||||
paddingY={0}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{suggestions.map((suggestion, index) => {
|
||||
const command = commands.find(
|
||||
cmd => cmd.userFacingName() === suggestion.replace('/', ''),
|
||||
)
|
||||
return (
|
||||
<Box
|
||||
key={suggestion}
|
||||
flexDirection={columns < 80 ? 'column' : 'row'}
|
||||
>
|
||||
<Box width={columns < 80 ? undefined : commandWidth}>
|
||||
<Text
|
||||
color={
|
||||
index === selectedSuggestion
|
||||
? theme.suggestion
|
||||
: undefined
|
||||
}
|
||||
dimColor={index !== selectedSuggestion}
|
||||
>
|
||||
/{suggestion}
|
||||
{command?.aliases && command.aliases.length > 0 && (
|
||||
<Text dimColor> ({command.aliases.join(', ')})</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
{command && (
|
||||
<Box
|
||||
width={columns - (columns < 80 ? 4 : commandWidth + 4)}
|
||||
paddingLeft={columns < 80 ? 4 : 0}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
index === selectedSuggestion
|
||||
? theme.suggestion
|
||||
: undefined
|
||||
}
|
||||
dimColor={index !== selectedSuggestion}
|
||||
wrap="wrap"
|
||||
>
|
||||
<Text dimColor={index !== selectedSuggestion}>
|
||||
{command.description}
|
||||
{command.type === 'prompt' && command.argNames?.length
|
||||
? ` (arguments: ${command.argNames.join(', ')})`
|
||||
: null}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
{renderedSuggestions}
|
||||
|
||||
{/* 简洁操作提示框 */}
|
||||
<Box marginTop={1} paddingX={3} borderStyle="round" borderColor="gray">
|
||||
<Text dimColor={!emptyDirMessage} color={emptyDirMessage ? "yellow" : undefined}>
|
||||
{emptyDirMessage || (() => {
|
||||
const selected = suggestions[selectedIndex]
|
||||
if (!selected) {
|
||||
return '↑↓ navigate • → accept • Tab cycle • Esc close'
|
||||
}
|
||||
if (selected?.value.endsWith('/')) {
|
||||
return '→ enter directory • ↑↓ navigate • Tab cycle • Esc close'
|
||||
} else if (selected?.type === 'agent') {
|
||||
return '→ select agent • ↑↓ navigate • Tab cycle • Esc close'
|
||||
} else {
|
||||
return '→ insert reference • ↑↓ navigate • Tab cycle • Esc close'
|
||||
}
|
||||
})()}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SentryErrorBoundary children={
|
||||
<Box justifyContent="flex-end" gap={1}>
|
||||
|
||||
@ -106,6 +106,12 @@ export type Props = {
|
||||
* Whether to disable cursor movement for up/down arrow keys
|
||||
*/
|
||||
readonly disableCursorMovementForUpDownKeys?: boolean
|
||||
|
||||
/**
|
||||
* Optional callback to handle special key combinations before input processing
|
||||
* Return true to prevent default handling
|
||||
*/
|
||||
readonly onSpecialKey?: (input: string, key: Key) => boolean
|
||||
|
||||
readonly cursorOffset: number
|
||||
|
||||
@ -136,6 +142,7 @@ export default function TextInput({
|
||||
onPaste,
|
||||
isDimmed = false,
|
||||
disableCursorMovementForUpDownKeys = false,
|
||||
onSpecialKey,
|
||||
cursorOffset,
|
||||
onChangeCursorOffset,
|
||||
}: Props) {
|
||||
@ -186,6 +193,12 @@ export default function TextInput({
|
||||
}
|
||||
|
||||
const wrappedOnInput = (input: string, key: Key): void => {
|
||||
// Check for special key combinations first
|
||||
if (onSpecialKey && onSpecialKey(input, key)) {
|
||||
// Special key was handled, don't process further
|
||||
return
|
||||
}
|
||||
|
||||
// Special handling for backspace or delete
|
||||
if (
|
||||
key.backspace ||
|
||||
|
||||
@ -9,6 +9,7 @@ import { getTheme } from '../../utils/theme'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures'
|
||||
import { ThinkTool } from '../../tools/ThinkTool/ThinkTool'
|
||||
import { AssistantThinkingMessage } from './AssistantThinkingMessage'
|
||||
import { TaskToolMessage } from './TaskToolMessage'
|
||||
|
||||
type Props = {
|
||||
param: ToolUseBlockParam
|
||||
@ -61,7 +62,7 @@ export function AssistantToolUseMessage({
|
||||
)
|
||||
}
|
||||
|
||||
const userFacingToolName = tool.userFacingName()
|
||||
const userFacingToolName = tool.userFacingName ? tool.userFacingName(param.input) : tool.name
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
@ -86,9 +87,18 @@ export function AssistantToolUseMessage({
|
||||
isError={erroredToolUseIDs.has(param.id)}
|
||||
/>
|
||||
))}
|
||||
<Text color={color} bold={!isQueued}>
|
||||
{userFacingToolName}
|
||||
</Text>
|
||||
{tool.name === 'Task' && param.input ? (
|
||||
<TaskToolMessage
|
||||
agentType={(param.input as any).subagent_type || 'general-purpose'}
|
||||
bold={!isQueued}
|
||||
>
|
||||
{userFacingToolName}
|
||||
</TaskToolMessage>
|
||||
) : (
|
||||
<Text color={color} bold={!isQueued}>
|
||||
{userFacingToolName}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexWrap="nowrap">
|
||||
{Object.keys(param.input as { [key: string]: unknown }).length > 0 &&
|
||||
|
||||
32
src/components/messages/TaskProgressMessage.tsx
Normal file
32
src/components/messages/TaskProgressMessage.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import { Box, Text } from 'ink'
|
||||
import { getTheme } from '../../utils/theme'
|
||||
|
||||
interface Props {
|
||||
agentType: string
|
||||
status: string
|
||||
toolCount?: number
|
||||
}
|
||||
|
||||
export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
|
||||
const theme = getTheme()
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.claude}>⎯ </Text>
|
||||
<Text color={theme.text} bold>
|
||||
[{agentType}]
|
||||
</Text>
|
||||
<Text color={theme.secondaryText}> {status}</Text>
|
||||
</Box>
|
||||
{toolCount && toolCount > 0 && (
|
||||
<Box marginLeft={3}>
|
||||
<Text color={theme.secondaryText}>
|
||||
Tools used: {toolCount}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
58
src/components/messages/TaskToolMessage.tsx
Normal file
58
src/components/messages/TaskToolMessage.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react'
|
||||
import { Text } from 'ink'
|
||||
import { getAgentByType } from '../../utils/agentLoader'
|
||||
import { getTheme } from '../../utils/theme'
|
||||
|
||||
interface Props {
|
||||
agentType: string
|
||||
children: React.ReactNode
|
||||
bold?: boolean
|
||||
}
|
||||
|
||||
// Simple cache to prevent re-fetching agent configs
|
||||
const agentConfigCache = new Map<string, any>()
|
||||
|
||||
export function TaskToolMessage({ agentType, children, bold = true }: Props) {
|
||||
const theme = getTheme()
|
||||
const [agentConfig, setAgentConfig] = useState<any>(() => {
|
||||
// Return cached config immediately if available
|
||||
return agentConfigCache.get(agentType) || null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Skip if already cached
|
||||
if (agentConfigCache.has(agentType)) {
|
||||
setAgentConfig(agentConfigCache.get(agentType))
|
||||
return
|
||||
}
|
||||
|
||||
// Load and cache agent configuration
|
||||
let mounted = true
|
||||
getAgentByType(agentType).then(config => {
|
||||
if (mounted) {
|
||||
agentConfigCache.set(agentType, config)
|
||||
setAgentConfig(config)
|
||||
}
|
||||
}).catch(() => {
|
||||
// Silently handle errors to prevent console noise
|
||||
if (mounted) {
|
||||
agentConfigCache.set(agentType, null)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [agentType])
|
||||
|
||||
// Memoize color calculation to prevent unnecessary re-renders
|
||||
const color = useMemo(() => {
|
||||
return agentConfig?.color || theme.text
|
||||
}, [agentConfig?.color, theme.text])
|
||||
|
||||
return (
|
||||
<Text color={color} bold={bold}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
export const PRODUCT_NAME = 'Kode'
|
||||
export const PRODUCT_URL = 'https://github.com/shareAI-lab/Anykode'
|
||||
export const PROJECT_FILE = 'KODE.md'
|
||||
export const PROJECT_FILE = 'AGENTS.md'
|
||||
export const PRODUCT_COMMAND = 'kode'
|
||||
export const CONFIG_BASE_DIR = '.kode'
|
||||
export const CONFIG_FILE = '.kode.json'
|
||||
|
||||
@ -19,13 +19,13 @@ import { lastX } from './utils/generators'
|
||||
import { getGitEmail } from './utils/user'
|
||||
import { PROJECT_FILE } from './constants/product'
|
||||
/**
|
||||
* Find all KODE.md and CLAUDE.md files in the current working directory
|
||||
* Find all AGENTS.md and CLAUDE.md files in the current working directory
|
||||
*/
|
||||
export async function getClaudeFiles(): Promise<string | null> {
|
||||
const abortController = new AbortController()
|
||||
const timeout = setTimeout(() => abortController.abort(), 3000)
|
||||
try {
|
||||
// Search for both KODE.md and CLAUDE.md files
|
||||
// Search for both AGENTS.md and CLAUDE.md files
|
||||
const [codeContextFiles, claudeFiles] = await Promise.all([
|
||||
ripGrep(
|
||||
['--files', '--glob', join('**', '*', PROJECT_FILE)],
|
||||
@ -46,7 +46,7 @@ export async function getClaudeFiles(): Promise<string | null> {
|
||||
|
||||
// Add instructions for additional project files
|
||||
const fileTypes = []
|
||||
if (codeContextFiles.length > 0) fileTypes.push('KODE.md')
|
||||
if (codeContextFiles.length > 0) fileTypes.push('AGENTS.md')
|
||||
if (claudeFiles.length > 0) fileTypes.push('CLAUDE.md')
|
||||
|
||||
return `NOTE: Additional project documentation files (${fileTypes.join(', ')}) were found. When working in these directories, make sure to read and follow the instructions in the corresponding files:\n${allFiles
|
||||
@ -97,21 +97,21 @@ export const getReadme = memoize(async (): Promise<string | null> => {
|
||||
})
|
||||
|
||||
/**
|
||||
* Get project documentation content (KODE.md and CLAUDE.md)
|
||||
* Get project documentation content (AGENTS.md and CLAUDE.md)
|
||||
*/
|
||||
export const getProjectDocs = memoize(async (): Promise<string | null> => {
|
||||
try {
|
||||
const cwd = getCwd()
|
||||
const codeContextPath = join(cwd, 'KODE.md')
|
||||
const codeContextPath = join(cwd, 'AGENTS.md')
|
||||
const claudePath = join(cwd, 'CLAUDE.md')
|
||||
|
||||
const docs = []
|
||||
|
||||
// Try to read KODE.md
|
||||
// Try to read AGENTS.md
|
||||
if (existsSync(codeContextPath)) {
|
||||
try {
|
||||
const content = await readFile(codeContextPath, 'utf-8')
|
||||
docs.push(`# KODE.md\n\n${content}`)
|
||||
docs.push(`# AGENTS.md\n\n${content}`)
|
||||
} catch (e) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
@ -185,6 +185,13 @@ async function setup(cwd: string, safeMode?: boolean): Promise<void> {
|
||||
|
||||
// Always grant read permissions for original working dir
|
||||
grantReadPermissionForOriginalDir()
|
||||
|
||||
// Start watching agent configuration files for changes
|
||||
const { startAgentWatcher, clearAgentCache } = await import('../utils/agentLoader')
|
||||
await startAgentWatcher(() => {
|
||||
// Cache is already cleared in the watcher, just log
|
||||
console.log('✅ Agent configurations hot-reloaded')
|
||||
})
|
||||
|
||||
// If --safe mode is enabled, prevent root/sudo usage for security
|
||||
if (safeMode) {
|
||||
|
||||
@ -1,137 +0,0 @@
|
||||
import { useInput } from 'ink'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { Command, getCommand } from '../commands'
|
||||
|
||||
type Props = {
|
||||
commands: Command[]
|
||||
onInputChange: (value: string) => void
|
||||
onSubmit: (value: string, isSubmittingSlashCommand?: boolean) => void
|
||||
setCursorOffset: (offset: number) => void
|
||||
currentInput?: string // Add current input for monitoring
|
||||
}
|
||||
|
||||
export function useSlashCommandTypeahead({
|
||||
commands,
|
||||
onInputChange,
|
||||
onSubmit,
|
||||
setCursorOffset,
|
||||
currentInput,
|
||||
}: Props): {
|
||||
suggestions: string[]
|
||||
selectedSuggestion: number
|
||||
updateSuggestions: (value: string) => void
|
||||
clearSuggestions: () => void
|
||||
} {
|
||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState(-1)
|
||||
|
||||
// Force clear suggestions when input doesn't start with /
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentInput !== undefined &&
|
||||
!currentInput.startsWith('/') &&
|
||||
suggestions.length > 0
|
||||
) {
|
||||
setSuggestions([])
|
||||
setSelectedSuggestion(-1)
|
||||
}
|
||||
}, [currentInput, suggestions.length])
|
||||
|
||||
function updateSuggestions(value: string) {
|
||||
if (value.startsWith('/')) {
|
||||
const query = value.slice(1).toLowerCase()
|
||||
|
||||
// Find commands whose name or alias matches the query
|
||||
const matchingCommands = commands
|
||||
.filter(cmd => !cmd.isHidden)
|
||||
.filter(cmd => {
|
||||
const names = [cmd.userFacingName()]
|
||||
if (cmd.aliases) {
|
||||
names.push(...cmd.aliases)
|
||||
}
|
||||
return names.some(name => name.toLowerCase().startsWith(query))
|
||||
})
|
||||
|
||||
// For each matching command, include its primary name
|
||||
const filtered = matchingCommands.map(cmd => cmd.userFacingName())
|
||||
setSuggestions(filtered)
|
||||
|
||||
// Try to preserve the selected suggestion
|
||||
const newIndex =
|
||||
selectedSuggestion > -1
|
||||
? filtered.indexOf(suggestions[selectedSuggestion]!)
|
||||
: 0
|
||||
if (newIndex > -1) {
|
||||
setSelectedSuggestion(newIndex)
|
||||
} else {
|
||||
setSelectedSuggestion(0)
|
||||
}
|
||||
} else {
|
||||
setSuggestions([])
|
||||
setSelectedSuggestion(-1)
|
||||
}
|
||||
}
|
||||
|
||||
useInput((_, key) => {
|
||||
if (suggestions.length > 0) {
|
||||
// Handle suggestion navigation (up/down arrows)
|
||||
if (key.downArrow) {
|
||||
setSelectedSuggestion(prev =>
|
||||
prev >= suggestions.length - 1 ? 0 : prev + 1,
|
||||
)
|
||||
return true
|
||||
} else if (key.upArrow) {
|
||||
setSelectedSuggestion(prev =>
|
||||
prev <= 0 ? suggestions.length - 1 : prev - 1,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle selection completion via tab or return
|
||||
else if (key.tab || (key.return && selectedSuggestion >= 0)) {
|
||||
// Ensure a suggestion is selected
|
||||
if (selectedSuggestion === -1 && key.tab) {
|
||||
setSelectedSuggestion(0)
|
||||
}
|
||||
|
||||
const suggestionIndex = selectedSuggestion >= 0 ? selectedSuggestion : 0
|
||||
const suggestion = suggestions[suggestionIndex]
|
||||
if (!suggestion) return true
|
||||
|
||||
const input = '/' + suggestion + ' '
|
||||
onInputChange(input)
|
||||
// Manually move cursor to end
|
||||
setCursorOffset(input.length)
|
||||
setSuggestions([])
|
||||
setSelectedSuggestion(-1)
|
||||
|
||||
// If return was pressed and command doesn't take arguments, just run it
|
||||
if (key.return) {
|
||||
const command = getCommand(suggestion, commands)
|
||||
if (
|
||||
command.type !== 'prompt' ||
|
||||
(command.argNames ?? []).length === 0
|
||||
) {
|
||||
onSubmit(input, /* isSubmittingSlashCommand */ true)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Explicitly return false when not handling the input
|
||||
return false
|
||||
})
|
||||
|
||||
const clearSuggestions = useCallback(() => {
|
||||
setSuggestions([])
|
||||
setSelectedSuggestion(-1)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
updateSuggestions,
|
||||
clearSuggestions,
|
||||
}
|
||||
}
|
||||
@ -220,6 +220,10 @@ export function useTextInput({
|
||||
}
|
||||
|
||||
function onInput(input: string, key: Key): void {
|
||||
if (key.tab) {
|
||||
return // Skip Tab key processing - let completion system handle it
|
||||
}
|
||||
|
||||
// Direct handling for backspace or delete (which is being detected as delete)
|
||||
if (
|
||||
key.backspace ||
|
||||
@ -277,8 +281,7 @@ export function useTextInput({
|
||||
return handleMeta
|
||||
case key.return:
|
||||
return () => handleEnter(key)
|
||||
case key.tab:
|
||||
return () => {}
|
||||
// Remove Tab handling - let completion system handle it
|
||||
case key.upArrow:
|
||||
return upOrHistoryUp
|
||||
case key.downArrow:
|
||||
|
||||
1325
src/hooks/useUnifiedCompletion.ts
Normal file
1325
src/hooks/useUnifiedCompletion.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -410,6 +410,7 @@ export async function* runToolUse(
|
||||
currentRequest?.id,
|
||||
)
|
||||
|
||||
|
||||
logEvent('tengu_tool_use_start', {
|
||||
toolName: toolUse.name,
|
||||
toolUseID: toolUse.id,
|
||||
|
||||
@ -181,7 +181,12 @@ export function REPL({
|
||||
// Tool use confirm handles the abort signal itself
|
||||
toolUseConfirm.onAbort()
|
||||
} else {
|
||||
abortController?.abort()
|
||||
// Wrap abort in try-catch to prevent error display on user interrupt
|
||||
try {
|
||||
abortController?.abort()
|
||||
} catch (e) {
|
||||
// Silently handle abort errors - this is expected behavior
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,7 +397,7 @@ export function REPL({
|
||||
}
|
||||
|
||||
// If this was a Koding request and we got an assistant message back,
|
||||
// save it to KODE.md (and CLAUDE.md if exists)
|
||||
// save it to AGENTS.md (and CLAUDE.md if exists)
|
||||
if (
|
||||
isKodingRequest &&
|
||||
lastAssistantMessage &&
|
||||
@ -407,7 +412,7 @@ export function REPL({
|
||||
.map(block => (block.type === 'text' ? block.text : ''))
|
||||
.join('\n')
|
||||
|
||||
// Add the content to KODE.md (and CLAUDE.md if exists)
|
||||
// Add the content to AGENTS.md (and CLAUDE.md if exists)
|
||||
if (content && content.trim().length > 0) {
|
||||
handleHashCommand(content)
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ function getModelConfigForDebug(model: string): {
|
||||
const config = getGlobalConfig()
|
||||
const modelManager = getModelManager()
|
||||
|
||||
// 🔧 Fix: Use ModelManager to get the actual current model profile
|
||||
|
||||
const modelProfile = modelManager.getModel('main')
|
||||
|
||||
let apiKeyStatus: 'configured' | 'missing' | 'invalid' = 'missing'
|
||||
@ -89,7 +89,7 @@ function getModelConfigForDebug(model: string): {
|
||||
let maxTokens: number | undefined
|
||||
let reasoningEffort: string | undefined
|
||||
|
||||
// 🔧 Fix: Use ModelProfile configuration exclusively
|
||||
|
||||
if (modelProfile) {
|
||||
apiKeyStatus = modelProfile.apiKey ? 'configured' : 'missing'
|
||||
baseURL = modelProfile.baseURL
|
||||
@ -320,7 +320,7 @@ async function withRetry<T>(
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
// 🔧 CRITICAL FIX: Check abort signal BEFORE showing retry message
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
throw new Error('Request cancelled by user')
|
||||
}
|
||||
@ -439,7 +439,7 @@ export async function verifyApiKey(
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// 🔧 Fix: Proper URL construction for verification
|
||||
|
||||
if (!baseURL) {
|
||||
console.warn(
|
||||
'No baseURL provided for non-Anthropic provider verification',
|
||||
@ -647,7 +647,7 @@ function messageReducer(
|
||||
}
|
||||
async function handleMessageStream(
|
||||
stream: ChatCompletionStream,
|
||||
signal?: AbortSignal, // 🔧 Add AbortSignal support to stream handler
|
||||
signal?: AbortSignal,
|
||||
): Promise<OpenAI.ChatCompletion> {
|
||||
const streamStartTime = Date.now()
|
||||
let ttftMs: number | undefined
|
||||
@ -663,7 +663,7 @@ async function handleMessageStream(
|
||||
let id, model, created, object, usage
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
// 🔧 CRITICAL FIX: Check abort signal in OpenAI streaming loop
|
||||
|
||||
if (signal?.aborted) {
|
||||
debugLogger.flow('OPENAI_STREAM_ABORTED', {
|
||||
chunkCount,
|
||||
@ -766,7 +766,7 @@ async function handleMessageStream(
|
||||
}
|
||||
}
|
||||
|
||||
function convertOpenAIResponseToAnthropic(response: OpenAI.ChatCompletion) {
|
||||
function convertOpenAIResponseToAnthropic(response: OpenAI.ChatCompletion, tools?: Tool[]) {
|
||||
let contentBlocks: ContentBlock[] = []
|
||||
const message = response.choices?.[0]?.message
|
||||
if (!message) {
|
||||
@ -785,12 +785,12 @@ function convertOpenAIResponseToAnthropic(response: OpenAI.ChatCompletion) {
|
||||
if (message?.tool_calls) {
|
||||
for (const toolCall of message.tool_calls) {
|
||||
const tool = toolCall.function
|
||||
const toolName = tool.name
|
||||
const toolName = tool?.name
|
||||
let toolArgs = {}
|
||||
try {
|
||||
toolArgs = JSON.parse(tool.arguments)
|
||||
toolArgs = tool?.arguments ? JSON.parse(tool.arguments) : {}
|
||||
} catch (e) {
|
||||
// console.log(e)
|
||||
// Invalid JSON in tool arguments
|
||||
}
|
||||
|
||||
contentBlocks.push({
|
||||
@ -835,6 +835,7 @@ function convertOpenAIResponseToAnthropic(response: OpenAI.ChatCompletion) {
|
||||
usage: response.usage,
|
||||
}
|
||||
|
||||
|
||||
return finalMessage
|
||||
}
|
||||
|
||||
@ -1060,7 +1061,7 @@ export async function queryLLM(
|
||||
toolUseContext?: ToolUseContext
|
||||
},
|
||||
): Promise<AssistantMessage> {
|
||||
// 🔧 统一的模型解析:支持指针、model ID 和真实模型名称
|
||||
|
||||
const modelManager = getModelManager()
|
||||
const modelResolution = modelManager.resolveModelWithInfo(options.model)
|
||||
|
||||
@ -1248,7 +1249,7 @@ async function queryLLMWithPromptCaching(
|
||||
const modelManager = getModelManager()
|
||||
const toolUseContext = options.toolUseContext
|
||||
|
||||
// 🔧 Fix: 使用传入的ModelProfile,而不是硬编码的'main'指针
|
||||
|
||||
const modelProfile = options.modelProfile || modelManager.getModel('main')
|
||||
let provider: string
|
||||
|
||||
@ -1300,7 +1301,7 @@ async function queryAnthropicNative(
|
||||
const modelManager = getModelManager()
|
||||
const toolUseContext = options?.toolUseContext
|
||||
|
||||
// 🔧 Fix: 使用传入的ModelProfile,而不是硬编码的'main'指针
|
||||
|
||||
const modelProfile = options?.modelProfile || modelManager.getModel('main')
|
||||
let anthropic: Anthropic | AnthropicBedrock | AnthropicVertex
|
||||
let model: string
|
||||
@ -1390,13 +1391,16 @@ async function queryAnthropicNative(
|
||||
}),
|
||||
)
|
||||
|
||||
const toolSchemas = tools.map(
|
||||
tool =>
|
||||
const toolSchemas = await Promise.all(
|
||||
tools.map(async tool =>
|
||||
({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
description: typeof tool.description === 'function'
|
||||
? await tool.description()
|
||||
: tool.description,
|
||||
input_schema: zodToJsonSchema(tool.inputSchema),
|
||||
}) as unknown as Anthropic.Beta.Messages.BetaTool,
|
||||
)
|
||||
)
|
||||
|
||||
const anthropicMessages = addCacheBreakpoints(messages)
|
||||
@ -1456,7 +1460,7 @@ async function queryAnthropicNative(
|
||||
})
|
||||
|
||||
if (config.stream) {
|
||||
// 🔧 CRITICAL FIX: Connect AbortSignal to Anthropic API call
|
||||
|
||||
const stream = await anthropic.beta.messages.create({
|
||||
...params,
|
||||
stream: true,
|
||||
@ -1472,7 +1476,7 @@ async function queryAnthropicNative(
|
||||
let stopSequence: string | null = null
|
||||
|
||||
for await (const event of stream) {
|
||||
// 🔧 CRITICAL FIX: Check abort signal in streaming loop
|
||||
|
||||
if (signal.aborted) {
|
||||
debugLogger.flow('STREAM_ABORTED', {
|
||||
eventType: event.type,
|
||||
@ -1546,12 +1550,12 @@ async function queryAnthropicNative(
|
||||
modelProfileName: modelProfile?.name,
|
||||
})
|
||||
|
||||
// 🔧 CRITICAL FIX: Connect AbortSignal to non-streaming API call
|
||||
|
||||
return await anthropic.beta.messages.create(params, {
|
||||
signal: signal // ← CRITICAL: Connect the AbortSignal to API call
|
||||
})
|
||||
}
|
||||
}, { signal }) // 🔧 CRITICAL FIX: Pass AbortSignal to withRetry
|
||||
}, { signal })
|
||||
|
||||
const ttftMs = start - Date.now()
|
||||
const durationMs = Date.now() - startIncludingRetries
|
||||
@ -1705,7 +1709,7 @@ async function queryOpenAI(
|
||||
const modelManager = getModelManager()
|
||||
const toolUseContext = options?.toolUseContext
|
||||
|
||||
// 🔧 Fix: 使用传入的ModelProfile,而不是硬编码的'main'指针
|
||||
|
||||
const modelProfile = options?.modelProfile || modelManager.getModel('main')
|
||||
let model: string
|
||||
|
||||
@ -1806,10 +1810,10 @@ async function queryOpenAI(
|
||||
|
||||
const opts: OpenAI.ChatCompletionCreateParams = {
|
||||
model,
|
||||
// 🔧 Use correct parameter name based on model type
|
||||
|
||||
...(isGPT5 ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens }),
|
||||
messages: [...openaiSystem, ...openaiMessages],
|
||||
// 🔧 GPT-5 temperature constraint: only 1 or undefined
|
||||
|
||||
temperature: isGPT5 ? 1 : MAIN_QUERY_TEMPERATURE,
|
||||
}
|
||||
if (config.stream) {
|
||||
@ -1831,7 +1835,7 @@ async function queryOpenAI(
|
||||
opts.reasoning_effort = reasoningEffort
|
||||
}
|
||||
|
||||
// 🔧 Fix: 如果有ModelProfile配置,直接使用它 (更宽松的条件)
|
||||
|
||||
if (modelProfile && modelProfile.modelName) {
|
||||
debugLogger.api('USING_MODEL_PROFILE_PATH', {
|
||||
modelProfileName: modelProfile.modelName,
|
||||
@ -1900,7 +1904,7 @@ async function queryOpenAI(
|
||||
} else {
|
||||
finalResponse = s
|
||||
}
|
||||
const r = convertOpenAIResponseToAnthropic(finalResponse)
|
||||
const r = convertOpenAIResponseToAnthropic(finalResponse, tools)
|
||||
return r
|
||||
}
|
||||
} else {
|
||||
@ -1915,7 +1919,7 @@ async function queryOpenAI(
|
||||
} else {
|
||||
finalResponse = s
|
||||
}
|
||||
const r = convertOpenAIResponseToAnthropic(finalResponse)
|
||||
const r = convertOpenAIResponseToAnthropic(finalResponse, tools)
|
||||
return r
|
||||
}
|
||||
} else {
|
||||
@ -1942,7 +1946,7 @@ async function queryOpenAI(
|
||||
`No valid ModelProfile available for model: ${model}. Please configure model through /model command. Debug: ${JSON.stringify(errorDetails)}`,
|
||||
)
|
||||
}
|
||||
}, { signal }) // 🔧 CRITICAL FIX: Pass AbortSignal to withRetry
|
||||
}, { signal })
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
return getAssistantMessageFromError(error)
|
||||
|
||||
@ -77,6 +77,8 @@ export async function executeBashCommands(content: string): Promise<string> {
|
||||
*/
|
||||
export async function resolveFileReferences(content: string): Promise<string> {
|
||||
// Match patterns like @src/file.js or @path/to/file.txt
|
||||
// Use consistent file mention pattern from mentionProcessor
|
||||
// Exclude agent and ask-model patterns to avoid conflicts
|
||||
const fileRefRegex = /@([a-zA-Z0-9/._-]+(?:\.[a-zA-Z0-9]+)?)/g
|
||||
const matches = [...content.matchAll(fileRefRegex)]
|
||||
|
||||
@ -90,6 +92,11 @@ export async function resolveFileReferences(content: string): Promise<string> {
|
||||
const fullMatch = match[0]
|
||||
const filePath = match[1]
|
||||
|
||||
// Skip agent mentions - these are handled by the mention processor
|
||||
if (filePath.startsWith('agent-')) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// Resolve relative to current working directory
|
||||
// This maintains consistency with how other file operations work
|
||||
|
||||
273
src/services/mentionProcessor.ts
Normal file
273
src/services/mentionProcessor.ts
Normal file
@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Mention Processor Service
|
||||
* Handles @agent and @file mentions through the system reminder infrastructure
|
||||
* Designed to integrate naturally with the existing event-driven architecture
|
||||
*/
|
||||
|
||||
import { emitReminderEvent } from './systemReminder'
|
||||
import { getAvailableAgentTypes } from '../utils/agentLoader'
|
||||
import { existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { getCwd } from '../utils/state'
|
||||
|
||||
export interface MentionContext {
|
||||
type: 'agent' | 'file'
|
||||
mention: string
|
||||
resolved: string
|
||||
exists: boolean
|
||||
metadata?: any
|
||||
}
|
||||
|
||||
export interface ProcessedMentions {
|
||||
agents: MentionContext[]
|
||||
files: MentionContext[]
|
||||
hasAgentMentions: boolean
|
||||
hasFileMentions: boolean
|
||||
}
|
||||
|
||||
class MentionProcessorService {
|
||||
// Centralized mention patterns - single source of truth
|
||||
private static readonly MENTION_PATTERNS = {
|
||||
runAgent: /@(run-agent-[\w\-]+)/g,
|
||||
agent: /@(agent-[\w\-]+)/g, // Legacy support
|
||||
askModel: /@(ask-[\w\-]+)/g,
|
||||
file: /@([a-zA-Z0-9/._-]+(?:\.[a-zA-Z0-9]+)?)/g
|
||||
} as const
|
||||
|
||||
private agentCache: Map<string, boolean> = new Map()
|
||||
private lastAgentCheck: number = 0
|
||||
private CACHE_TTL = 60000 // 1 minute cache
|
||||
|
||||
/**
|
||||
* Process mentions in user input and emit appropriate events
|
||||
* This follows the event-driven philosophy of system reminders
|
||||
*/
|
||||
public async processMentions(input: string): Promise<ProcessedMentions> {
|
||||
const result: ProcessedMentions = {
|
||||
agents: [],
|
||||
files: [],
|
||||
hasAgentMentions: false,
|
||||
hasFileMentions: false,
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// Process agent mentions with unified logic to eliminate code duplication
|
||||
const agentMentions = this.extractAgentMentions(input)
|
||||
if (agentMentions.length > 0) {
|
||||
await this.refreshAgentCache()
|
||||
|
||||
for (const { mention, agentType, isAskModel } of agentMentions) {
|
||||
if (isAskModel || this.agentCache.has(agentType)) {
|
||||
result.agents.push({
|
||||
type: 'agent',
|
||||
mention,
|
||||
resolved: agentType,
|
||||
exists: true,
|
||||
metadata: isAskModel ? { type: 'ask-model' } : undefined
|
||||
})
|
||||
result.hasAgentMentions = true
|
||||
|
||||
// Emit appropriate event based on mention type
|
||||
this.emitAgentMentionEvent(mention, agentType, isAskModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No longer process @xxx format - treat as regular text (emails, etc.)
|
||||
|
||||
// Process file mentions (exclude agent and ask-model mentions)
|
||||
const fileMatches = [...input.matchAll(MentionProcessorService.MENTION_PATTERNS.file)]
|
||||
const processedAgentMentions = new Set(agentMentions.map(am => am.mention))
|
||||
|
||||
for (const match of fileMatches) {
|
||||
const mention = match[1]
|
||||
|
||||
// Skip if this is an agent or ask-model mention (already processed)
|
||||
if (mention.startsWith('run-agent-') || mention.startsWith('agent-') || mention.startsWith('ask-') || processedAgentMentions.has(mention)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a file
|
||||
const filePath = this.resolveFilePath(mention)
|
||||
if (existsSync(filePath)) {
|
||||
result.files.push({
|
||||
type: 'file',
|
||||
mention,
|
||||
resolved: filePath,
|
||||
exists: true,
|
||||
})
|
||||
result.hasFileMentions = true
|
||||
|
||||
// Emit file mention event for system reminder to handle
|
||||
emitReminderEvent('file:mentioned', {
|
||||
filePath: filePath,
|
||||
originalMention: mention,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.warn('[MentionProcessor] Failed to process mentions:', {
|
||||
input: input.substring(0, 100) + (input.length > 100 ? '...' : ''),
|
||||
error: error instanceof Error ? error.message : error
|
||||
})
|
||||
|
||||
// Return empty result on error to maintain system stability
|
||||
return {
|
||||
agents: [],
|
||||
files: [],
|
||||
hasAgentMentions: false,
|
||||
hasFileMentions: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed identifyMention method as it's no longer needed with separate processing
|
||||
|
||||
/**
|
||||
* Resolve file path relative to current working directory
|
||||
*/
|
||||
private resolveFilePath(mention: string): string {
|
||||
// Simple consistent logic: mention is always relative to current directory
|
||||
return resolve(getCwd(), mention)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the agent cache periodically
|
||||
* This avoids hitting the agent loader on every mention
|
||||
*/
|
||||
private async refreshAgentCache(): Promise<void> {
|
||||
const now = Date.now()
|
||||
if (now - this.lastAgentCheck < this.CACHE_TTL) {
|
||||
return // Cache is still fresh
|
||||
}
|
||||
|
||||
try {
|
||||
const agents = await getAvailableAgentTypes()
|
||||
const previousCacheSize = this.agentCache.size
|
||||
this.agentCache.clear()
|
||||
|
||||
for (const agent of agents) {
|
||||
// Store only the agent type without prefix for consistent lookup
|
||||
this.agentCache.set(agent.agentType, true)
|
||||
}
|
||||
|
||||
this.lastAgentCheck = now
|
||||
|
||||
// Log cache refresh for debugging mention resolution issues
|
||||
if (agents.length !== previousCacheSize) {
|
||||
console.log('[MentionProcessor] Agent cache refreshed:', {
|
||||
agentCount: agents.length,
|
||||
previousCacheSize,
|
||||
cacheAge: now - this.lastAgentCheck
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MentionProcessor] Failed to refresh agent cache, keeping existing cache:', {
|
||||
error: error instanceof Error ? error.message : error,
|
||||
cacheSize: this.agentCache.size,
|
||||
lastRefresh: new Date(this.lastAgentCheck).toISOString()
|
||||
})
|
||||
// Keep existing cache on error to maintain functionality
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract agent mentions with unified pattern matching
|
||||
* Consolidates run-agent, agent, and ask-model detection logic
|
||||
*/
|
||||
private extractAgentMentions(input: string): Array<{ mention: string; agentType: string; isAskModel: boolean }> {
|
||||
const mentions: Array<{ mention: string; agentType: string; isAskModel: boolean }> = []
|
||||
|
||||
// Process @run-agent-xxx format (preferred)
|
||||
const runAgentMatches = [...input.matchAll(MentionProcessorService.MENTION_PATTERNS.runAgent)]
|
||||
for (const match of runAgentMatches) {
|
||||
const mention = match[1]
|
||||
const agentType = mention.replace(/^run-agent-/, '')
|
||||
mentions.push({ mention, agentType, isAskModel: false })
|
||||
}
|
||||
|
||||
// Process @agent-xxx format (legacy)
|
||||
const agentMatches = [...input.matchAll(MentionProcessorService.MENTION_PATTERNS.agent)]
|
||||
for (const match of agentMatches) {
|
||||
const mention = match[1]
|
||||
const agentType = mention.replace(/^agent-/, '')
|
||||
mentions.push({ mention, agentType, isAskModel: false })
|
||||
}
|
||||
|
||||
// Process @ask-model mentions
|
||||
const askModelMatches = [...input.matchAll(MentionProcessorService.MENTION_PATTERNS.askModel)]
|
||||
for (const match of askModelMatches) {
|
||||
const mention = match[1]
|
||||
mentions.push({ mention, agentType: mention, isAskModel: true })
|
||||
}
|
||||
|
||||
return mentions
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit agent mention event with proper typing
|
||||
* Centralized event emission to ensure consistency
|
||||
*/
|
||||
private emitAgentMentionEvent(mention: string, agentType: string, isAskModel: boolean): void {
|
||||
try {
|
||||
const eventData = {
|
||||
originalMention: mention,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
if (isAskModel) {
|
||||
emitReminderEvent('ask-model:mentioned', {
|
||||
...eventData,
|
||||
modelName: mention,
|
||||
})
|
||||
} else {
|
||||
emitReminderEvent('agent:mentioned', {
|
||||
...eventData,
|
||||
agentType,
|
||||
})
|
||||
}
|
||||
|
||||
// Debug log for mention event emission tracking
|
||||
console.log('[MentionProcessor] Emitted mention event:', {
|
||||
type: isAskModel ? 'ask-model' : 'agent',
|
||||
mention,
|
||||
agentType: isAskModel ? undefined : agentType
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[MentionProcessor] Failed to emit mention event:', {
|
||||
mention,
|
||||
agentType,
|
||||
isAskModel,
|
||||
error: error instanceof Error ? error.message : error
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear caches - useful for testing or reset
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.agentCache.clear()
|
||||
this.lastAgentCheck = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const mentionProcessor = new MentionProcessorService()
|
||||
|
||||
/**
|
||||
* Process mentions in user input
|
||||
* This is the main API for the mention processor
|
||||
*/
|
||||
export const processMentions = (input: string) =>
|
||||
mentionProcessor.processMentions(input)
|
||||
|
||||
/**
|
||||
* Clear mention processor caches
|
||||
*/
|
||||
export const clearMentionCache = () =>
|
||||
mentionProcessor.clearCache()
|
||||
@ -82,28 +82,31 @@ class SystemReminderService {
|
||||
() => this.dispatchTodoEvent(agentId),
|
||||
() => this.dispatchSecurityEvent(),
|
||||
() => this.dispatchPerformanceEvent(),
|
||||
() => this.getMentionReminders(), // Add mention reminders
|
||||
]
|
||||
|
||||
for (const generator of reminderGenerators) {
|
||||
if (reminders.length >= 3) break // Limit concurrent reminders
|
||||
if (reminders.length >= 5) break // Slightly increase limit to accommodate mentions
|
||||
|
||||
const reminder = generator()
|
||||
if (reminder) {
|
||||
reminders.push(reminder)
|
||||
this.sessionState.reminderCount++
|
||||
const result = generator()
|
||||
if (result) {
|
||||
// Handle both single reminders and arrays
|
||||
const remindersToAdd = Array.isArray(result) ? result : [result]
|
||||
reminders.push(...remindersToAdd)
|
||||
this.sessionState.reminderCount += remindersToAdd.length
|
||||
}
|
||||
}
|
||||
|
||||
// Log aggregated metrics instead of individual events for performance
|
||||
if (reminders.length > 0) {
|
||||
logEvent('system_reminder_batch', {
|
||||
count: reminders.length,
|
||||
count: reminders.length.toString(),
|
||||
types: reminders.map(r => r.type).join(','),
|
||||
priorities: reminders.map(r => r.priority).join(','),
|
||||
categories: reminders.map(r => r.category).join(','),
|
||||
sessionCount: this.sessionState.reminderCount,
|
||||
sessionCount: this.sessionState.reminderCount.toString(),
|
||||
agentId: agentId || 'default',
|
||||
timestamp: currentTime,
|
||||
timestamp: currentTime.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -224,6 +227,43 @@ class SystemReminderService {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve cached mention reminders
|
||||
* Returns recent mentions (within 5 seconds) that haven't expired
|
||||
*/
|
||||
private getMentionReminders(): ReminderMessage[] {
|
||||
const currentTime = Date.now()
|
||||
const MENTION_FRESHNESS_WINDOW = 5000 // 5 seconds
|
||||
const reminders: ReminderMessage[] = []
|
||||
const expiredKeys: string[] = []
|
||||
|
||||
// Single pass through cache for both collection and cleanup identification
|
||||
for (const [key, reminder] of this.reminderCache.entries()) {
|
||||
if (this.isMentionReminder(reminder)) {
|
||||
const age = currentTime - reminder.timestamp
|
||||
if (age <= MENTION_FRESHNESS_WINDOW) {
|
||||
reminders.push(reminder)
|
||||
} else {
|
||||
expiredKeys.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired mention reminders in separate pass for performance
|
||||
expiredKeys.forEach(key => this.reminderCache.delete(key))
|
||||
|
||||
return reminders
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for mention reminders - centralized type checking
|
||||
* Eliminates hardcoded type strings scattered throughout the code
|
||||
*/
|
||||
private isMentionReminder(reminder: ReminderMessage): boolean {
|
||||
const mentionTypes = ['agent_mention', 'file_mention', 'ask_model_mention']
|
||||
return mentionTypes.includes(reminder.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate reminders for external file changes
|
||||
* Called when todo files are modified externally
|
||||
@ -302,9 +342,9 @@ class SystemReminderService {
|
||||
// Log session startup
|
||||
logEvent('system_reminder_session_startup', {
|
||||
agentId: context.agentId || 'default',
|
||||
contextKeys: Object.keys(context.context || {}),
|
||||
messageCount: context.messages || 0,
|
||||
timestamp: context.timestamp,
|
||||
contextKeys: Object.keys(context.context || {}).join(','),
|
||||
messageCount: (context.messages || 0).toString(),
|
||||
timestamp: context.timestamp.toString(),
|
||||
})
|
||||
})
|
||||
|
||||
@ -343,6 +383,40 @@ class SystemReminderService {
|
||||
this.addEventListener('file:edited', context => {
|
||||
// File edit handling
|
||||
})
|
||||
|
||||
// Unified mention event handlers - eliminates code duplication
|
||||
this.addEventListener('agent:mentioned', context => {
|
||||
this.createMentionReminder({
|
||||
type: 'agent_mention',
|
||||
key: `agent_mention_${context.agentType}_${context.timestamp}`,
|
||||
category: 'task',
|
||||
priority: 'high',
|
||||
content: `The user mentioned @${context.originalMention}. You MUST use the Task tool with subagent_type="${context.agentType}" to delegate this task to the specified agent. Provide a detailed, self-contained task description that fully captures the user's intent for the ${context.agentType} agent to execute.`,
|
||||
timestamp: context.timestamp
|
||||
})
|
||||
})
|
||||
|
||||
this.addEventListener('file:mentioned', context => {
|
||||
this.createMentionReminder({
|
||||
type: 'file_mention',
|
||||
key: `file_mention_${context.filePath}_${context.timestamp}`,
|
||||
category: 'general',
|
||||
priority: 'high',
|
||||
content: `The user mentioned @${context.originalMention}. You MUST read the entire content of the file at path: ${context.filePath} using the Read tool to understand the full context before proceeding with the user's request.`,
|
||||
timestamp: context.timestamp
|
||||
})
|
||||
})
|
||||
|
||||
this.addEventListener('ask-model:mentioned', context => {
|
||||
this.createMentionReminder({
|
||||
type: 'ask_model_mention',
|
||||
key: `ask_model_mention_${context.modelName}_${context.timestamp}`,
|
||||
category: 'task',
|
||||
priority: 'high',
|
||||
content: `The user mentioned @${context.modelName}. You MUST use the AskExpertModelTool to consult this specific model for expert opinions and analysis. Provide the user's question or context clearly to get the most relevant response from ${context.modelName}.`,
|
||||
timestamp: context.timestamp
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public addEventListener(
|
||||
@ -366,6 +440,33 @@ class SystemReminderService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified mention reminder creation - eliminates duplicate logic
|
||||
* Centralizes reminder creation with consistent deduplication
|
||||
*/
|
||||
private createMentionReminder(params: {
|
||||
type: string
|
||||
key: string
|
||||
category: ReminderMessage['category']
|
||||
priority: ReminderMessage['priority']
|
||||
content: string
|
||||
timestamp: number
|
||||
}): void {
|
||||
if (!this.sessionState.remindersSent.has(params.key)) {
|
||||
this.sessionState.remindersSent.add(params.key)
|
||||
|
||||
const reminder = this.createReminderMessage(
|
||||
params.type,
|
||||
params.category,
|
||||
params.priority,
|
||||
params.content,
|
||||
params.timestamp
|
||||
)
|
||||
|
||||
this.reminderCache.set(params.key, reminder)
|
||||
}
|
||||
}
|
||||
|
||||
public resetSession(): void {
|
||||
this.sessionState = {
|
||||
lastTodoUpdate: 0,
|
||||
|
||||
@ -22,7 +22,9 @@ import { debug as debugLogger } from '../../utils/debugLogger'
|
||||
import { applyMarkdown } from '../../utils/markdown'
|
||||
|
||||
export const inputSchema = z.strictObject({
|
||||
question: z.string().describe('The question to ask the expert model'),
|
||||
question: z.string().describe(
|
||||
'COMPLETE SELF-CONTAINED QUESTION: Must include full background context, relevant details, and a clear independent question. The expert model will receive ONLY this content with no access to previous conversation or external context. Structure as: 1) Background/Context 2) Specific situation/problem 3) Clear question. Ensure the expert can fully understand and respond without needing additional information.'
|
||||
),
|
||||
expert_model: z
|
||||
.string()
|
||||
.describe(
|
||||
@ -45,29 +47,40 @@ export type Out = {
|
||||
export const AskExpertModelTool = {
|
||||
name: 'AskExpertModel',
|
||||
async description() {
|
||||
return 'Consults external AI models for specialized assistance and second opinions'
|
||||
return "Consult external AI models for expert opinions and analysis"
|
||||
},
|
||||
async prompt() {
|
||||
return `Consults external AI models for specialized assistance and second opinions. Maintains conversation history through persistent sessions.
|
||||
return `Ask a question to a specific external AI model for expert analysis.
|
||||
|
||||
When to use AskExpertModel tool:
|
||||
- User explicitly requests a specific model ("use GPT-5 to...", "ask Claude about...", "consult Kimi for...")
|
||||
- User seeks second opinions or specialized model expertise
|
||||
- User requests comparison between different model responses
|
||||
- Complex questions requiring specific model capabilities
|
||||
This tool allows you to consult different AI models for their unique perspectives and expertise.
|
||||
|
||||
When NOT to use AskExpertModel tool:
|
||||
- General questions that don't specify a particular model
|
||||
- Tasks better suited for current model capabilities
|
||||
- Simple queries not requiring external expertise
|
||||
CRITICAL REQUIREMENT FOR QUESTION PARAMETER:
|
||||
The question MUST be completely self-contained and include:
|
||||
1. FULL BACKGROUND CONTEXT - All relevant information the expert needs
|
||||
2. SPECIFIC SITUATION - Clear description of the current scenario/problem
|
||||
3. INDEPENDENT QUESTION - What exactly you want the expert to analyze/answer
|
||||
|
||||
Usage notes:
|
||||
1. Use exact model names as specified by the user
|
||||
2. Sessions persist conversation context - use "new" for fresh conversations or provide existing session ID
|
||||
3. External models operate independently without access to current project context
|
||||
4. Tool validates model availability and provides alternatives if model not found
|
||||
The expert model receives ONLY your question content with NO access to:
|
||||
- Previous conversation history (unless using existing session)
|
||||
- Current codebase or file context
|
||||
- User's current task or project details
|
||||
|
||||
IMPORTANT: Always use the precise model name the user requested. The tool will handle model availability and provide guidance for unavailable models.`
|
||||
IMPORTANT: This tool is for asking questions to models, not for task execution.
|
||||
- Use when you need a specific model's opinion or analysis
|
||||
- Use when you want to compare different models' responses
|
||||
- Use the @ask-[model] format when available
|
||||
|
||||
The expert_model parameter accepts:
|
||||
- OpenAI: gpt-4, gpt-5, o1-preview
|
||||
- Anthropic: claude-3-5-sonnet, claude-3-opus
|
||||
- Others: kimi, gemini-pro, mixtral
|
||||
|
||||
Example of well-structured question:
|
||||
"Background: I'm working on a React TypeScript application with performance issues. The app renders a large list of 10,000 items using a simple map() function, causing UI freezing.
|
||||
|
||||
Current situation: Users report 3-5 second delays when scrolling through the list. The component re-renders the entire list on every state change.
|
||||
|
||||
Question: What are the most effective React optimization techniques for handling large lists, and how should I prioritize implementing virtualization vs memoization vs other approaches?"`
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
@ -89,11 +102,12 @@ IMPORTANT: Always use the precise model name the user requested. The tool will h
|
||||
question,
|
||||
expert_model,
|
||||
chat_session_id,
|
||||
}): Promise<ValidationResult> {
|
||||
}, context?: any): Promise<ValidationResult> {
|
||||
if (!question.trim()) {
|
||||
return { result: false, message: 'Question cannot be empty' }
|
||||
}
|
||||
|
||||
|
||||
if (!expert_model.trim()) {
|
||||
return { result: false, message: 'Expert model must be specified' }
|
||||
}
|
||||
@ -106,6 +120,35 @@ IMPORTANT: Always use the precise model name the user requested. The tool will h
|
||||
}
|
||||
}
|
||||
|
||||
// Check if trying to consult the same model we're currently running
|
||||
try {
|
||||
const modelManager = getModelManager()
|
||||
|
||||
// Get current model based on context
|
||||
let currentModel: string
|
||||
if (context?.agentId && context?.options?.model) {
|
||||
// In subagent context (Task tool)
|
||||
currentModel = context.options.model
|
||||
} else {
|
||||
// In main agent context or after model switch
|
||||
currentModel = modelManager.getModelName('main') || ''
|
||||
}
|
||||
|
||||
// Normalize model names for comparison
|
||||
const normalizedExpert = expert_model.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
const normalizedCurrent = currentModel.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
|
||||
if (normalizedExpert === normalizedCurrent) {
|
||||
return {
|
||||
result: false,
|
||||
message: `You are already running as ${currentModel}. Consulting the same model would be redundant. Please choose a different model or handle the task directly.`
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't determine current model, allow the request
|
||||
debugLogger('AskExpertModel', 'Could not determine current model:', e)
|
||||
}
|
||||
|
||||
// Validate that the model exists and is available
|
||||
try {
|
||||
const modelManager = getModelManager()
|
||||
@ -142,65 +185,72 @@ IMPORTANT: Always use the precise model name the user requested. The tool will h
|
||||
) {
|
||||
if (!question || !expert_model) return null
|
||||
const isNewSession = chat_session_id === 'new'
|
||||
const sessionDisplay = isNewSession ? 'new session' : chat_session_id
|
||||
const sessionDisplay = isNewSession ? 'new session' : `session ${chat_session_id.substring(0, 5)}...`
|
||||
const theme = getTheme()
|
||||
|
||||
if (verbose) {
|
||||
const theme = getTheme()
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.text}>{expert_model}, {sessionDisplay}</Text>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor="green"
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
marginTop={1}
|
||||
>
|
||||
<Text bold color="yellow">{expert_model}</Text>
|
||||
<Text color={theme.secondaryText}>{sessionDisplay}</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text}>
|
||||
{applyMarkdown(question)}
|
||||
{question.length > 300 ? question.substring(0, 300) + '...' : question}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return `${expert_model}, ${sessionDisplay}`
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color="yellow">{expert_model} </Text>
|
||||
<Text color={theme.secondaryText} dimColor>({sessionDisplay})</Text>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
|
||||
renderToolResultMessage(content) {
|
||||
const verbose = false // Set default value for verbose
|
||||
const verbose = true // Show more content
|
||||
const theme = getTheme()
|
||||
|
||||
if (typeof content === 'object' && content && 'expertAnswer' in content) {
|
||||
const expertResult = content as Out
|
||||
const isError = expertResult.expertAnswer.startsWith('❌')
|
||||
const isError = expertResult.expertAnswer.startsWith('Error') || expertResult.expertAnswer.includes('failed')
|
||||
const isInterrupted = expertResult.chatSessionId === 'interrupted'
|
||||
|
||||
if (isInterrupted) {
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Text> ⎿ </Text>
|
||||
<Text color={theme.error}>[Expert consultation interrupted]</Text>
|
||||
<Text color={theme.secondaryText}>Consultation interrupted</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const answerText = verbose
|
||||
? expertResult.expertAnswer.trim()
|
||||
: expertResult.expertAnswer.length > 120
|
||||
? expertResult.expertAnswer.substring(0, 120) + '...'
|
||||
: expertResult.expertAnswer.length > 500
|
||||
? expertResult.expertAnswer.substring(0, 500) + '...'
|
||||
: expertResult.expertAnswer.trim()
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="red">{answerText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor="green"
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
marginTop={1}
|
||||
>
|
||||
<Text color={isError ? theme.error : theme.text}>
|
||||
{isError ? answerText : applyMarkdown(answerText)}
|
||||
<Text bold color={theme.text}>Response from {expertResult.expertModelName}:</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text}>
|
||||
{applyMarkdown(answerText)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.secondaryText} dimColor>
|
||||
Session: {expertResult.chatSessionId.substring(0, 8)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -209,8 +259,7 @@ IMPORTANT: Always use the precise model name the user requested. The tool will h
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Text> ⎿ </Text>
|
||||
<Text color={theme.secondaryText}>Expert consultation completed</Text>
|
||||
<Text color={theme.secondaryText}>Consultation completed</Text>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
@ -315,6 +364,14 @@ ${output.expertAnswer}`
|
||||
return yield* this.handleInterrupt()
|
||||
}
|
||||
|
||||
// Yield progress message to show we're connecting
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: createAssistantMessage(
|
||||
`Connecting to ${expertModel}... (timeout: 5 minutes)`
|
||||
),
|
||||
}
|
||||
|
||||
// Call model with comprehensive error handling and timeout
|
||||
let response
|
||||
try {
|
||||
@ -333,7 +390,7 @@ ${output.expertAnswer}`
|
||||
})
|
||||
|
||||
// Create a timeout promise to prevent hanging
|
||||
const timeoutMs = 60000 // 60 seconds timeout
|
||||
const timeoutMs = 300000 // 300 seconds (5 minutes) timeout for external models
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`Expert model query timed out after ${timeoutMs/1000}s`))
|
||||
@ -370,19 +427,25 @@ ${output.expertAnswer}`
|
||||
|
||||
if (error.message?.includes('timed out')) {
|
||||
throw new Error(
|
||||
`Expert model '${expertModel}' timed out. This often happens with slower APIs. Try again or use a different model.`,
|
||||
`Expert model '${expertModel}' timed out after 5 minutes.\n\n` +
|
||||
`Suggestions:\n` +
|
||||
` - The model might be experiencing high load\n` +
|
||||
` - Try a different model or retry later\n` +
|
||||
` - Consider breaking down your question into smaller parts`,
|
||||
)
|
||||
}
|
||||
|
||||
if (error.message?.includes('rate limit')) {
|
||||
throw new Error(
|
||||
'Rate limit exceeded for expert model. Please try again later.',
|
||||
`Rate limit exceeded for ${expertModel}.\n\n` +
|
||||
`Please wait a moment and try again, or use a different model.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (error.message?.includes('invalid api key')) {
|
||||
throw new Error(
|
||||
'Invalid API key for expert model. Please check your configuration.',
|
||||
`Invalid API key for ${expertModel}.\n\n` +
|
||||
`Please check your model configuration with /model command.`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { TextBlock } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import chalk from 'chalk'
|
||||
import { last, memoize } from 'lodash-es'
|
||||
import { EOL } from 'os'
|
||||
import * as React from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Box, Text } from 'ink'
|
||||
import { z } from 'zod'
|
||||
import { Tool, ValidationResult } from '../../Tool'
|
||||
@ -32,6 +32,7 @@ import { generateAgentId } from '../../utils/agentStorage'
|
||||
import { debug as debugLogger } from '../../utils/debugLogger'
|
||||
import { getTaskTools, getPrompt } from './prompt'
|
||||
import { TOOL_NAME } from './constants'
|
||||
import { getActiveAgents, getAgentByType, getAvailableAgentTypes } from '../../utils/agentLoader'
|
||||
|
||||
const inputSchema = z.object({
|
||||
description: z
|
||||
@ -44,46 +45,28 @@ const inputSchema = z.object({
|
||||
.describe(
|
||||
'Optional: Specific model name to use for this task. If not provided, uses the default task model pointer.',
|
||||
),
|
||||
subagent_type: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The type of specialized agent to use for this task',
|
||||
),
|
||||
})
|
||||
|
||||
export const TaskTool = {
|
||||
async prompt({ safeMode }) {
|
||||
// Match original Claude Code - prompt returns full agent descriptions
|
||||
return await getPrompt(safeMode)
|
||||
},
|
||||
name: TOOL_NAME,
|
||||
async description() {
|
||||
const modelManager = getModelManager()
|
||||
const availableModels = modelManager.getAllAvailableModelNames()
|
||||
const currentTaskModel =
|
||||
modelManager.getModelName('task') || '<Not configured>'
|
||||
|
||||
if (availableModels.length === 0) {
|
||||
return `Launch a new agent to handle complex, multi-step tasks autonomously.
|
||||
|
||||
⚠️ No models configured. Use /model to configure models first.
|
||||
|
||||
Usage: Provide detailed task description for autonomous execution. The agent will return results in a single response.`
|
||||
}
|
||||
|
||||
return `Launch a new agent to handle complex, multi-step tasks autonomously.
|
||||
|
||||
Available models: ${availableModels.join(', ')}
|
||||
|
||||
When to specify a model_name:
|
||||
- Specify model_name for tasks requiring specific model capabilities
|
||||
- If not provided, uses current task default: '${currentTaskModel}'
|
||||
- Use reasoning models for complex analysis
|
||||
- Use quick models for simple operations
|
||||
|
||||
The model_name parameter accepts actual model names (like 'claude-3-5-sonnet-20241022', 'gpt-4', etc.)
|
||||
|
||||
Usage: Provide detailed task description for autonomous execution. The agent will return results in a single response.`
|
||||
// Match original Claude Code exactly - simple description
|
||||
return "Launch a new task"
|
||||
},
|
||||
inputSchema,
|
||||
|
||||
// 🔧 ULTRA FIX: Complete revert to original AgentTool pattern
|
||||
async *call(
|
||||
{ description, prompt, model_name },
|
||||
{ description, prompt, model_name, subagent_type },
|
||||
{
|
||||
abortController,
|
||||
options: { safeMode = false, forkNumber, messageLogName, verbose },
|
||||
@ -91,14 +74,72 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
},
|
||||
) {
|
||||
const startTime = Date.now()
|
||||
const messages: MessageType[] = [createUserMessage(prompt)]
|
||||
const tools = await getTaskTools(safeMode)
|
||||
|
||||
// Default to general-purpose if no subagent_type specified
|
||||
const agentType = subagent_type || 'general-purpose'
|
||||
|
||||
// Apply subagent configuration
|
||||
let effectivePrompt = prompt
|
||||
let effectiveModel = model_name || 'task'
|
||||
let toolFilter = null
|
||||
let temperature = undefined
|
||||
|
||||
// Load agent configuration dynamically
|
||||
if (agentType) {
|
||||
const agentConfig = await getAgentByType(agentType)
|
||||
|
||||
if (!agentConfig) {
|
||||
// If agent type not found, return helpful message instead of throwing
|
||||
const availableTypes = await getAvailableAgentTypes()
|
||||
const helpMessage = `Agent type '${agentType}' not found.\n\nAvailable agents:\n${availableTypes.map(t => ` • ${t}`).join('\n')}\n\nUse /agents command to manage agent configurations.`
|
||||
|
||||
yield {
|
||||
type: 'result',
|
||||
data: { error: helpMessage },
|
||||
resultForAssistant: helpMessage,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Apply system prompt if configured
|
||||
if (agentConfig.systemPrompt) {
|
||||
effectivePrompt = `${agentConfig.systemPrompt}\n\n${prompt}`
|
||||
}
|
||||
|
||||
// Apply model if not overridden by model_name parameter
|
||||
if (!model_name && agentConfig.model_name) {
|
||||
// Support inherit: keep pointer-based default
|
||||
if (agentConfig.model_name !== 'inherit') {
|
||||
effectiveModel = agentConfig.model_name as string
|
||||
}
|
||||
}
|
||||
|
||||
// Store tool filter for later application
|
||||
toolFilter = agentConfig.tools
|
||||
|
||||
// Note: temperature is not currently in our agent configs
|
||||
// but could be added in the future
|
||||
}
|
||||
|
||||
const messages: MessageType[] = [createUserMessage(effectivePrompt)]
|
||||
let tools = await getTaskTools(safeMode)
|
||||
|
||||
// Apply tool filtering if specified by subagent config
|
||||
if (toolFilter) {
|
||||
// Back-compat: ['*'] means all tools
|
||||
const isAllArray = Array.isArray(toolFilter) && toolFilter.length === 1 && toolFilter[0] === '*'
|
||||
if (toolFilter === '*' || isAllArray) {
|
||||
// no-op, keep all tools
|
||||
} else if (Array.isArray(toolFilter)) {
|
||||
tools = tools.filter(tool => toolFilter.includes(tool.name))
|
||||
}
|
||||
}
|
||||
|
||||
// We yield an initial message immediately so the UI
|
||||
// doesn't move around when messages start streaming back.
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: createAssistantMessage(chalk.dim('Initializing…')),
|
||||
content: createAssistantMessage(chalk.dim(`[${agentType}] ${description}`)),
|
||||
normalizedMessages: normalizeMessages(messages),
|
||||
tools,
|
||||
}
|
||||
@ -109,8 +150,8 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
getMaxThinkingTokens(messages),
|
||||
])
|
||||
|
||||
// Simple model resolution - match original AgentTool pattern
|
||||
const modelToUse = model_name || 'task'
|
||||
// Model already resolved in effectiveModel variable above
|
||||
const modelToUse = effectiveModel
|
||||
|
||||
// Inject model context to prevent self-referential expert consultations
|
||||
taskPrompt.push(`\nIMPORTANT: You are currently running as ${modelToUse}. You do not need to consult ${modelToUse} via AskExpertModel since you ARE ${modelToUse}. Complete tasks directly using your capabilities.`)
|
||||
@ -125,6 +166,23 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
const taskId = generateAgentId()
|
||||
|
||||
// 🔧 ULTRA SIMPLIFIED: Exact original AgentTool pattern
|
||||
// Build query options, adding temperature if specified
|
||||
const queryOptions = {
|
||||
safeMode,
|
||||
forkNumber,
|
||||
messageLogName,
|
||||
tools,
|
||||
commands: [],
|
||||
verbose,
|
||||
maxThinkingTokens,
|
||||
model: modelToUse,
|
||||
}
|
||||
|
||||
// Add temperature if specified by subagent config
|
||||
if (temperature !== undefined) {
|
||||
queryOptions['temperature'] = temperature
|
||||
}
|
||||
|
||||
for await (const message of query(
|
||||
messages,
|
||||
taskPrompt,
|
||||
@ -132,16 +190,7 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
hasPermissionsToUseTool,
|
||||
{
|
||||
abortController,
|
||||
options: {
|
||||
safeMode,
|
||||
forkNumber,
|
||||
messageLogName,
|
||||
tools,
|
||||
commands: [],
|
||||
verbose,
|
||||
maxThinkingTokens,
|
||||
model: modelToUse,
|
||||
},
|
||||
options: queryOptions,
|
||||
messageId: getLastAssistantMessageId(messages),
|
||||
agentId: taskId,
|
||||
readFileTimestamps,
|
||||
@ -159,22 +208,55 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
}
|
||||
|
||||
const normalizedMessages = normalizeMessages(messages)
|
||||
|
||||
// Process tool uses and text content for better visibility
|
||||
for (const content of message.message.content) {
|
||||
if (content.type !== 'tool_use') {
|
||||
continue
|
||||
}
|
||||
|
||||
toolUseCount++
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: normalizedMessages.find(
|
||||
if (content.type === 'text' && content.text && content.text !== INTERRUPT_MESSAGE) {
|
||||
// Show agent's reasoning/responses
|
||||
const preview = content.text.length > 200 ? content.text.substring(0, 200) + '...' : content.text
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: createAssistantMessage(`[${agentType}] ${preview}`),
|
||||
normalizedMessages,
|
||||
tools,
|
||||
}
|
||||
} else if (content.type === 'tool_use') {
|
||||
toolUseCount++
|
||||
|
||||
// Show which tool is being used with agent context
|
||||
const toolMessage = normalizedMessages.find(
|
||||
_ =>
|
||||
_.type === 'assistant' &&
|
||||
_.message.content[0]?.type === 'tool_use' &&
|
||||
_.message.content[0].id === content.id,
|
||||
) as AssistantMessage,
|
||||
normalizedMessages,
|
||||
tools,
|
||||
) as AssistantMessage
|
||||
|
||||
if (toolMessage) {
|
||||
// Clone and modify the message to show agent context
|
||||
const modifiedMessage = {
|
||||
...toolMessage,
|
||||
message: {
|
||||
...toolMessage.message,
|
||||
content: toolMessage.message.content.map(c => {
|
||||
if (c.type === 'tool_use' && c.id === content.id) {
|
||||
// Add agent context to tool name display
|
||||
return {
|
||||
...c,
|
||||
name: c.name // Keep original name, UI will handle display
|
||||
}
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: modifiedMessage,
|
||||
normalizedMessages,
|
||||
tools,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,7 +292,7 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
]
|
||||
yield {
|
||||
type: 'progress',
|
||||
content: createAssistantMessage(`Done (${result.join(' · ')})`),
|
||||
content: createAssistantMessage(`[${agentType}] Completed (${result.join(' · ')})`),
|
||||
normalizedMessages,
|
||||
tools,
|
||||
}
|
||||
@ -265,13 +347,30 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
}
|
||||
}
|
||||
|
||||
// Validate subagent_type if provided
|
||||
if (input.subagent_type) {
|
||||
const availableTypes = await getAvailableAgentTypes()
|
||||
if (!availableTypes.includes(input.subagent_type)) {
|
||||
return {
|
||||
result: false,
|
||||
message: `Agent type '${input.subagent_type}' does not exist. Available types: ${availableTypes.join(', ')}`,
|
||||
meta: {
|
||||
subagent_type: input.subagent_type,
|
||||
availableTypes,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { result: true }
|
||||
},
|
||||
async isEnabled() {
|
||||
return true
|
||||
},
|
||||
userFacingName() {
|
||||
return 'Task'
|
||||
userFacingName(input?: any) {
|
||||
// Return agent name with proper prefix
|
||||
const agentType = input?.subagent_type || 'general-purpose'
|
||||
return `agent-${agentType}`
|
||||
},
|
||||
needsPermissions() {
|
||||
return false
|
||||
@ -279,24 +378,25 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
renderResultForAssistant(data: TextBlock[]) {
|
||||
return data.map(block => block.type === 'text' ? block.text : '').join('\n')
|
||||
},
|
||||
renderToolUseMessage({ description, prompt, model_name }, { verbose }) {
|
||||
renderToolUseMessage({ description, prompt, model_name, subagent_type }, { verbose }) {
|
||||
if (!description || !prompt) return null
|
||||
|
||||
const modelManager = getModelManager()
|
||||
const defaultTaskModel = modelManager.getModelName('task')
|
||||
const actualModel = model_name || defaultTaskModel
|
||||
const agentType = subagent_type || 'general-purpose'
|
||||
const promptPreview =
|
||||
prompt.length > 80 ? prompt.substring(0, 80) + '...' : prompt
|
||||
|
||||
const theme = getTheme()
|
||||
|
||||
if (verbose) {
|
||||
const theme = getTheme()
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.text}>
|
||||
🚀 Task ({actualModel}): {description}
|
||||
<Text>
|
||||
[{agentType}] {actualModel}: {description}
|
||||
</Text>
|
||||
<Box
|
||||
marginTop={1}
|
||||
paddingLeft={2}
|
||||
borderLeftStyle="single"
|
||||
borderLeftColor={theme.secondaryBorder}
|
||||
@ -307,7 +407,8 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
)
|
||||
}
|
||||
|
||||
return `Task (${actualModel}): ${description}`
|
||||
// Simple display: agent type, model and description
|
||||
return `[${agentType}] ${actualModel}: ${description}`
|
||||
},
|
||||
renderToolUseRejectedMessage() {
|
||||
return <FallbackToolUseRejectedMessage />
|
||||
@ -362,4 +463,4 @@ Usage: Provide detailed task description for autonomous execution. The agent wil
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
} satisfies Tool<typeof inputSchema, TextBlock[]>
|
||||
} satisfies Tool<typeof inputSchema, TextBlock[]>
|
||||
|
||||
@ -8,6 +8,7 @@ import { NotebookEditTool } from '../NotebookEditTool/NotebookEditTool'
|
||||
import { GlobTool } from '../GlobTool/GlobTool'
|
||||
import { FileReadTool } from '../FileReadTool/FileReadTool'
|
||||
import { getModelManager } from '../../utils/model'
|
||||
import { getActiveAgents } from '../../utils/agentLoader'
|
||||
|
||||
export async function getTaskTools(safeMode: boolean): Promise<Tool[]> {
|
||||
// No recursive tasks, yet..
|
||||
@ -17,40 +18,75 @@ export async function getTaskTools(safeMode: boolean): Promise<Tool[]> {
|
||||
}
|
||||
|
||||
export async function getPrompt(safeMode: boolean): Promise<string> {
|
||||
const tools = await getTaskTools(safeMode)
|
||||
const toolNames = tools.map(_ => _.name).join(', ')
|
||||
// Extracted directly from original Claude Code obfuscated source
|
||||
const agents = await getActiveAgents()
|
||||
|
||||
// Format exactly as in original: (Tools: tool1, tool2)
|
||||
const agentDescriptions = agents.map(agent => {
|
||||
const toolsStr = Array.isArray(agent.tools)
|
||||
? agent.tools.join(', ')
|
||||
: '*'
|
||||
return `- ${agent.agentType}: ${agent.whenToUse} (Tools: ${toolsStr})`
|
||||
}).join('\n')
|
||||
|
||||
// 100% exact copy from original Claude Code source
|
||||
return `Launch a new agent to handle complex, multi-step tasks autonomously.
|
||||
|
||||
// Add dynamic model information for Task tool prompts
|
||||
const modelManager = getModelManager()
|
||||
const availableModels = modelManager.getAllAvailableModelNames()
|
||||
const currentTaskModel =
|
||||
modelManager.getModelName('task') || '<Not configured>'
|
||||
Available agent types and the tools they have access to:
|
||||
${agentDescriptions}
|
||||
|
||||
const modelInfo =
|
||||
availableModels.length > 0
|
||||
? `
|
||||
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
||||
|
||||
Available models for Task tool: ${availableModels.join(', ')}
|
||||
Default task model: ${currentTaskModel}
|
||||
Specify model_name parameter to use a specific model for the task.`
|
||||
: ''
|
||||
When to use the Agent tool:
|
||||
- When you are instructed to execute custom slash commands. Use the Agent tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description="Check the file", prompt="/check-file path/to/file.py")
|
||||
|
||||
return `Launch a new agent that has access to the following tools: ${toolNames}. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Task tool to perform the search for you.${modelInfo}
|
||||
|
||||
When to use the Task tool:
|
||||
- If you are searching for a keyword like "config" or "logger", or for questions like "which file does X?", the Task tool is strongly recommended
|
||||
|
||||
When NOT to use the Task tool:
|
||||
- If you want to read a specific file path, use the ${FileReadTool.name} or ${GlobTool.name} tool instead of the Task tool, to find the match more quickly
|
||||
When NOT to use the Agent tool:
|
||||
- If you want to read a specific file path, use the ${FileReadTool.name} or ${GlobTool.name} tool instead of the Agent tool, to find the match more quickly
|
||||
- If you are searching for a specific class definition like "class Foo", use the ${GlobTool.name} tool instead, to find the match more quickly
|
||||
- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly
|
||||
- Writing code and running bash commands (use other tools for that)
|
||||
- Other tasks that are not related to searching for a keyword or file
|
||||
- If you are searching for code within a specific file or set of 2-3 files, use the ${FileReadTool.name} tool instead of the Agent tool, to find the match more quickly
|
||||
- Other tasks that are not related to the agent descriptions above
|
||||
|
||||
Usage notes:
|
||||
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
||||
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
||||
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
||||
4. The agent's outputs should generally be trusted
|
||||
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent`
|
||||
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
||||
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
||||
|
||||
Example usage:
|
||||
|
||||
<example_agent_descriptions>
|
||||
"code-reviewer": use this agent after you are done writing a signficant piece of code
|
||||
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
||||
</example_agent_description>
|
||||
|
||||
<example>
|
||||
user: "Please write a function that checks if a number is prime"
|
||||
assistant: Sure let me write a function that checks if a number is prime
|
||||
assistant: First let me use the ${FileWriteTool.name} tool to write a function that checks if a number is prime
|
||||
assistant: I'm going to use the ${FileWriteTool.name} tool to write the following code:
|
||||
<code>
|
||||
function isPrime(n) {
|
||||
if (n <= 1) return false
|
||||
for (let i = 2; i * i <= n; i++) {
|
||||
if (n % i === 0) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</code>
|
||||
<commentary>
|
||||
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
||||
</commentary>
|
||||
assistant: Now let me use the code-reviewer agent to review the code
|
||||
assistant: Uses the Task tool to launch the with the code-reviewer agent
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: "Hello"
|
||||
<commentary>
|
||||
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
||||
</commentary>
|
||||
assistant: "I'm going to use the Task tool to launch the with the greeting-responder agent"
|
||||
</example>`
|
||||
}
|
||||
|
||||
290
src/utils/advancedFuzzyMatcher.ts
Normal file
290
src/utils/advancedFuzzyMatcher.ts
Normal file
@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Advanced Fuzzy Matching Algorithm
|
||||
*
|
||||
* Inspired by:
|
||||
* - Chinese Pinyin input methods (Sogou, Baidu)
|
||||
* - IDE intelligent completion (VSCode, IntelliJ)
|
||||
* - Terminal fuzzy finders (fzf, peco)
|
||||
*
|
||||
* Key features:
|
||||
* - Hyphen-aware matching (dao → dao-qi-harmony)
|
||||
* - Numeric suffix matching (py3 → python3)
|
||||
* - Abbreviation matching (dq → dao-qi)
|
||||
* - Subsequence matching
|
||||
* - Word boundary bonus
|
||||
*/
|
||||
|
||||
export interface MatchResult {
|
||||
score: number
|
||||
matched: boolean
|
||||
algorithm: string
|
||||
}
|
||||
|
||||
export class AdvancedFuzzyMatcher {
|
||||
/**
|
||||
* Main matching function - combines multiple algorithms
|
||||
*/
|
||||
match(candidate: string, query: string): MatchResult {
|
||||
// Normalize inputs
|
||||
const text = candidate.toLowerCase()
|
||||
const pattern = query.toLowerCase()
|
||||
|
||||
// Quick exact match - give HUGE score for exact matches
|
||||
if (text === pattern) {
|
||||
return { score: 10000, matched: true, algorithm: 'exact' }
|
||||
}
|
||||
|
||||
// Try all algorithms and combine scores
|
||||
const algorithms = [
|
||||
this.exactPrefixMatch(text, pattern),
|
||||
this.hyphenAwareMatch(text, pattern),
|
||||
this.wordBoundaryMatch(text, pattern),
|
||||
this.abbreviationMatch(text, pattern),
|
||||
this.numericSuffixMatch(text, pattern),
|
||||
this.subsequenceMatch(text, pattern),
|
||||
this.fuzzySegmentMatch(text, pattern),
|
||||
]
|
||||
|
||||
// Get best score
|
||||
let bestScore = 0
|
||||
let bestAlgorithm = 'none'
|
||||
|
||||
for (const result of algorithms) {
|
||||
if (result.score > bestScore) {
|
||||
bestScore = result.score
|
||||
bestAlgorithm = result.algorithm
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
score: bestScore,
|
||||
matched: bestScore > 10,
|
||||
algorithm: bestAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact prefix matching
|
||||
*/
|
||||
private exactPrefixMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
||||
if (text.startsWith(pattern)) {
|
||||
const coverage = pattern.length / text.length
|
||||
// Higher base score for prefix matches to prioritize them
|
||||
return { score: 1000 + coverage * 500, algorithm: 'prefix' }
|
||||
}
|
||||
return { score: 0, algorithm: 'prefix' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hyphen-aware matching (dao → dao-qi-harmony-designer)
|
||||
* Treats hyphens as optional word boundaries
|
||||
*/
|
||||
private hyphenAwareMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
||||
// Split by hyphens and try to match
|
||||
const words = text.split('-')
|
||||
|
||||
// Check if pattern matches the beginning of hyphenated words
|
||||
if (words[0].startsWith(pattern)) {
|
||||
const coverage = pattern.length / words[0].length
|
||||
return { score: 300 + coverage * 100, algorithm: 'hyphen-prefix' }
|
||||
}
|
||||
|
||||
// Check if pattern matches concatenated words (ignoring hyphens)
|
||||
const concatenated = words.join('')
|
||||
if (concatenated.startsWith(pattern)) {
|
||||
const coverage = pattern.length / concatenated.length
|
||||
return { score: 250 + coverage * 100, algorithm: 'hyphen-concat' }
|
||||
}
|
||||
|
||||
// Check if pattern matches any word start
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
if (words[i].startsWith(pattern)) {
|
||||
return { score: 200 - i * 10, algorithm: 'hyphen-word' }
|
||||
}
|
||||
}
|
||||
|
||||
return { score: 0, algorithm: 'hyphen' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Word boundary matching (dq → dao-qi)
|
||||
* Matches characters at word boundaries
|
||||
*/
|
||||
private wordBoundaryMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
||||
const words = text.split(/[-_\s]+/)
|
||||
let patternIdx = 0
|
||||
let score = 0
|
||||
let matched = false
|
||||
|
||||
for (const word of words) {
|
||||
if (patternIdx >= pattern.length) break
|
||||
|
||||
if (word[0] === pattern[patternIdx]) {
|
||||
score += 50 // Bonus for word boundary match
|
||||
patternIdx++
|
||||
matched = true
|
||||
|
||||
// Try to match more characters in this word
|
||||
for (let i = 1; i < word.length && patternIdx < pattern.length; i++) {
|
||||
if (word[i] === pattern[patternIdx]) {
|
||||
score += 20
|
||||
patternIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matched && patternIdx === pattern.length) {
|
||||
return { score, algorithm: 'word-boundary' }
|
||||
}
|
||||
|
||||
return { score: 0, algorithm: 'word-boundary' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Abbreviation matching (nde → node, daoqi → dao-qi)
|
||||
*/
|
||||
private abbreviationMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
||||
let textIdx = 0
|
||||
let patternIdx = 0
|
||||
let score = 0
|
||||
let lastMatchIdx = -1
|
||||
|
||||
while (patternIdx < pattern.length && textIdx < text.length) {
|
||||
if (text[textIdx] === pattern[patternIdx]) {
|
||||
// Calculate position score
|
||||
const gap = lastMatchIdx === -1 ? 0 : textIdx - lastMatchIdx - 1
|
||||
|
||||
if (textIdx === 0) {
|
||||
score += 50 // First character match
|
||||
} else if (lastMatchIdx >= 0 && gap === 0) {
|
||||
score += 30 // Consecutive match
|
||||
} else if (text[textIdx - 1] === '-' || text[textIdx - 1] === '_') {
|
||||
score += 40 // Word boundary match
|
||||
} else {
|
||||
score += Math.max(5, 20 - gap * 2) // Distance penalty
|
||||
}
|
||||
|
||||
lastMatchIdx = textIdx
|
||||
patternIdx++
|
||||
}
|
||||
textIdx++
|
||||
}
|
||||
|
||||
if (patternIdx === pattern.length) {
|
||||
// Bonus for compact matches
|
||||
const spread = lastMatchIdx / pattern.length
|
||||
if (spread <= 3) score += 50
|
||||
else if (spread <= 5) score += 30
|
||||
|
||||
return { score, algorithm: 'abbreviation' }
|
||||
}
|
||||
|
||||
return { score: 0, algorithm: 'abbreviation' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric suffix matching (py3 → python3, np18 → node18)
|
||||
*/
|
||||
private numericSuffixMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
||||
// Check if pattern has numeric suffix
|
||||
const patternMatch = pattern.match(/^(.+?)(\d+)$/)
|
||||
if (!patternMatch) return { score: 0, algorithm: 'numeric' }
|
||||
|
||||
const [, prefix, suffix] = patternMatch
|
||||
|
||||
// Check if text ends with same number
|
||||
if (!text.endsWith(suffix)) return { score: 0, algorithm: 'numeric' }
|
||||
|
||||
// Check if prefix matches start of text
|
||||
const textWithoutSuffix = text.slice(0, -suffix.length)
|
||||
if (textWithoutSuffix.startsWith(prefix)) {
|
||||
const coverage = prefix.length / textWithoutSuffix.length
|
||||
return { score: 200 + coverage * 100, algorithm: 'numeric-suffix' }
|
||||
}
|
||||
|
||||
// Check abbreviation match for prefix
|
||||
const abbrevResult = this.abbreviationMatch(textWithoutSuffix, prefix)
|
||||
if (abbrevResult.score > 0) {
|
||||
return { score: abbrevResult.score + 50, algorithm: 'numeric-abbrev' }
|
||||
}
|
||||
|
||||
return { score: 0, algorithm: 'numeric' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Subsequence matching - characters appear in order
|
||||
*/
|
||||
private subsequenceMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
||||
let textIdx = 0
|
||||
let patternIdx = 0
|
||||
let score = 0
|
||||
|
||||
while (patternIdx < pattern.length && textIdx < text.length) {
|
||||
if (text[textIdx] === pattern[patternIdx]) {
|
||||
score += 10
|
||||
patternIdx++
|
||||
}
|
||||
textIdx++
|
||||
}
|
||||
|
||||
if (patternIdx === pattern.length) {
|
||||
// Penalty for spread
|
||||
const spread = textIdx / pattern.length
|
||||
score = Math.max(10, score - spread * 5)
|
||||
return { score, algorithm: 'subsequence' }
|
||||
}
|
||||
|
||||
return { score: 0, algorithm: 'subsequence' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy segment matching (dao → dao-qi-harmony)
|
||||
* Matches segments flexibly
|
||||
*/
|
||||
private fuzzySegmentMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
||||
// Remove hyphens and underscores for matching
|
||||
const cleanText = text.replace(/[-_]/g, '')
|
||||
const cleanPattern = pattern.replace(/[-_]/g, '')
|
||||
|
||||
// Check if clean pattern is a prefix of clean text
|
||||
if (cleanText.startsWith(cleanPattern)) {
|
||||
const coverage = cleanPattern.length / cleanText.length
|
||||
return { score: 150 + coverage * 100, algorithm: 'fuzzy-segment' }
|
||||
}
|
||||
|
||||
// Check if pattern appears anywhere in clean text
|
||||
const index = cleanText.indexOf(cleanPattern)
|
||||
if (index !== -1) {
|
||||
const positionPenalty = index * 5
|
||||
return { score: Math.max(50, 100 - positionPenalty), algorithm: 'fuzzy-contains' }
|
||||
}
|
||||
|
||||
return { score: 0, algorithm: 'fuzzy-segment' }
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance and helper functions
|
||||
export const advancedMatcher = new AdvancedFuzzyMatcher()
|
||||
|
||||
export function matchAdvanced(candidate: string, query: string): MatchResult {
|
||||
return advancedMatcher.match(candidate, query)
|
||||
}
|
||||
|
||||
export function matchManyAdvanced(
|
||||
candidates: string[],
|
||||
query: string,
|
||||
minScore: number = 10
|
||||
): Array<{ candidate: string; score: number; algorithm: string }> {
|
||||
return candidates
|
||||
.map(candidate => {
|
||||
const result = advancedMatcher.match(candidate, query)
|
||||
return {
|
||||
candidate,
|
||||
score: result.score,
|
||||
algorithm: result.algorithm
|
||||
}
|
||||
})
|
||||
.filter(item => item.score >= minScore)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
}
|
||||
284
src/utils/agentLoader.ts
Normal file
284
src/utils/agentLoader.ts
Normal file
@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Agent configuration loader
|
||||
* Loads agent configurations from markdown files with YAML frontmatter
|
||||
* Following Claude Code's agent system architecture
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, readdirSync, statSync, watch, FSWatcher } from 'fs'
|
||||
import { join, resolve } from 'path'
|
||||
import { homedir } from 'os'
|
||||
import matter from 'gray-matter'
|
||||
import { getCwd } from './state'
|
||||
import { memoize } from 'lodash-es'
|
||||
|
||||
// Track warned agents to avoid spam
|
||||
const warnedAgents = new Set<string>()
|
||||
|
||||
export interface AgentConfig {
|
||||
agentType: string // Agent identifier (matches subagent_type)
|
||||
whenToUse: string // Description of when to use this agent
|
||||
tools: string[] | '*' // Tool permissions
|
||||
systemPrompt: string // System prompt content
|
||||
location: 'built-in' | 'user' | 'project'
|
||||
color?: string // Optional UI color
|
||||
model_name?: string // Optional model override
|
||||
}
|
||||
|
||||
// Built-in general-purpose agent as fallback
|
||||
const BUILTIN_GENERAL_PURPOSE: AgentConfig = {
|
||||
agentType: 'general-purpose',
|
||||
whenToUse: 'General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks',
|
||||
tools: '*',
|
||||
systemPrompt: `You are a general-purpose agent. Given the user's task, use the tools available to complete it efficiently and thoroughly.
|
||||
|
||||
When to use your capabilities:
|
||||
- Searching for code, configurations, and patterns across large codebases
|
||||
- Analyzing multiple files to understand system architecture
|
||||
- Investigating complex questions that require exploring many files
|
||||
- Performing multi-step research tasks
|
||||
|
||||
Guidelines:
|
||||
- For file searches: Use Grep or Glob when you need to search broadly. Use FileRead when you know the specific file path.
|
||||
- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
|
||||
- Be thorough: Check multiple locations, consider different naming conventions, look for related files.
|
||||
- Complete tasks directly using your capabilities.`,
|
||||
location: 'built-in'
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tools field from frontmatter
|
||||
*/
|
||||
function parseTools(tools: any): string[] | '*' {
|
||||
if (!tools) return '*'
|
||||
if (tools === '*') return '*'
|
||||
if (Array.isArray(tools)) {
|
||||
// Ensure all items are strings and filter out non-strings
|
||||
const filteredTools = tools.filter((t): t is string => typeof t === 'string')
|
||||
return filteredTools.length > 0 ? filteredTools : '*'
|
||||
}
|
||||
if (typeof tools === 'string') {
|
||||
return [tools]
|
||||
}
|
||||
return '*'
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a directory for agent configuration files
|
||||
*/
|
||||
async function scanAgentDirectory(dirPath: string, location: 'user' | 'project'): Promise<AgentConfig[]> {
|
||||
if (!existsSync(dirPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const agents: AgentConfig[] = []
|
||||
|
||||
try {
|
||||
const files = readdirSync(dirPath)
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.md')) continue
|
||||
|
||||
const filePath = join(dirPath, file)
|
||||
const stat = statSync(filePath)
|
||||
|
||||
if (!stat.isFile()) continue
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
const { data: frontmatter, content: body } = matter(content)
|
||||
|
||||
// Validate required fields
|
||||
if (!frontmatter.name || !frontmatter.description) {
|
||||
console.warn(`Skipping ${filePath}: missing required fields (name, description)`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Silently ignore deprecated 'model' field - no warnings by default
|
||||
// Only warn if KODE_DEBUG_AGENTS environment variable is set
|
||||
if (frontmatter.model && !frontmatter.model_name && !warnedAgents.has(frontmatter.name) && process.env.KODE_DEBUG_AGENTS) {
|
||||
console.warn(`⚠️ Agent ${frontmatter.name}: 'model' field is deprecated and ignored. Use 'model_name' instead, or omit to use default 'task' model.`)
|
||||
warnedAgents.add(frontmatter.name)
|
||||
}
|
||||
|
||||
// Build agent config
|
||||
const agent: AgentConfig = {
|
||||
agentType: frontmatter.name,
|
||||
whenToUse: frontmatter.description.replace(/\\n/g, '\n'),
|
||||
tools: parseTools(frontmatter.tools),
|
||||
systemPrompt: body.trim(),
|
||||
location,
|
||||
...(frontmatter.color && { color: frontmatter.color }),
|
||||
// Only use model_name field, ignore deprecated 'model' field
|
||||
...(frontmatter.model_name && { model_name: frontmatter.model_name })
|
||||
}
|
||||
|
||||
agents.push(agent)
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse agent file ${filePath}:`, error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to scan directory ${dirPath}:`, error)
|
||||
}
|
||||
|
||||
return agents
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all agent configurations
|
||||
*/
|
||||
async function loadAllAgents(): Promise<{
|
||||
activeAgents: AgentConfig[]
|
||||
allAgents: AgentConfig[]
|
||||
}> {
|
||||
try {
|
||||
// Scan both .claude and .kode directories in parallel
|
||||
// Claude Code compatibility: support both ~/.claude/agents and ~/.kode/agents
|
||||
const userClaudeDir = join(homedir(), '.claude', 'agents')
|
||||
const userKodeDir = join(homedir(), '.kode', 'agents')
|
||||
const projectClaudeDir = join(getCwd(), '.claude', 'agents')
|
||||
const projectKodeDir = join(getCwd(), '.kode', 'agents')
|
||||
|
||||
const [userClaudeAgents, userKodeAgents, projectClaudeAgents, projectKodeAgents] = await Promise.all([
|
||||
scanAgentDirectory(userClaudeDir, 'user'),
|
||||
scanAgentDirectory(userKodeDir, 'user'),
|
||||
scanAgentDirectory(projectClaudeDir, 'project'),
|
||||
scanAgentDirectory(projectKodeDir, 'project')
|
||||
])
|
||||
|
||||
// Built-in agents (currently just general-purpose)
|
||||
const builtinAgents = [BUILTIN_GENERAL_PURPOSE]
|
||||
|
||||
// Apply priority override: built-in < .claude (user) < .kode (user) < .claude (project) < .kode (project)
|
||||
const agentMap = new Map<string, AgentConfig>()
|
||||
|
||||
// Add in priority order (later entries override earlier ones)
|
||||
for (const agent of builtinAgents) {
|
||||
agentMap.set(agent.agentType, agent)
|
||||
}
|
||||
for (const agent of userClaudeAgents) {
|
||||
agentMap.set(agent.agentType, agent)
|
||||
}
|
||||
for (const agent of userKodeAgents) {
|
||||
agentMap.set(agent.agentType, agent)
|
||||
}
|
||||
for (const agent of projectClaudeAgents) {
|
||||
agentMap.set(agent.agentType, agent)
|
||||
}
|
||||
for (const agent of projectKodeAgents) {
|
||||
agentMap.set(agent.agentType, agent)
|
||||
}
|
||||
|
||||
const activeAgents = Array.from(agentMap.values())
|
||||
const allAgents = [...builtinAgents, ...userClaudeAgents, ...userKodeAgents, ...projectClaudeAgents, ...projectKodeAgents]
|
||||
|
||||
return { activeAgents, allAgents }
|
||||
} catch (error) {
|
||||
console.error('Failed to load agents, falling back to built-in:', error)
|
||||
return {
|
||||
activeAgents: [BUILTIN_GENERAL_PURPOSE],
|
||||
allAgents: [BUILTIN_GENERAL_PURPOSE]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Memoized version for performance
|
||||
export const getActiveAgents = memoize(
|
||||
async (): Promise<AgentConfig[]> => {
|
||||
const { activeAgents } = await loadAllAgents()
|
||||
return activeAgents
|
||||
}
|
||||
)
|
||||
|
||||
// Get all agents (both active and overridden)
|
||||
export const getAllAgents = memoize(
|
||||
async (): Promise<AgentConfig[]> => {
|
||||
const { allAgents } = await loadAllAgents()
|
||||
return allAgents
|
||||
}
|
||||
)
|
||||
|
||||
// Clear cache when needed
|
||||
export function clearAgentCache() {
|
||||
getActiveAgents.cache?.clear?.()
|
||||
getAllAgents.cache?.clear?.()
|
||||
getAgentByType.cache?.clear?.()
|
||||
getAvailableAgentTypes.cache?.clear?.()
|
||||
}
|
||||
|
||||
// Get a specific agent by type
|
||||
export const getAgentByType = memoize(
|
||||
async (agentType: string): Promise<AgentConfig | undefined> => {
|
||||
const agents = await getActiveAgents()
|
||||
return agents.find(agent => agent.agentType === agentType)
|
||||
}
|
||||
)
|
||||
|
||||
// Get all available agent types for validation
|
||||
export const getAvailableAgentTypes = memoize(
|
||||
async (): Promise<string[]> => {
|
||||
const agents = await getActiveAgents()
|
||||
return agents.map(agent => agent.agentType)
|
||||
}
|
||||
)
|
||||
|
||||
// File watcher for hot reload
|
||||
let watchers: FSWatcher[] = []
|
||||
|
||||
/**
|
||||
* Start watching agent configuration directories for changes
|
||||
*/
|
||||
export async function startAgentWatcher(onChange?: () => void): Promise<void> {
|
||||
await stopAgentWatcher() // Clean up any existing watchers
|
||||
|
||||
// Watch both .claude and .kode directories
|
||||
const userClaudeDir = join(homedir(), '.claude', 'agents')
|
||||
const userKodeDir = join(homedir(), '.kode', 'agents')
|
||||
const projectClaudeDir = join(getCwd(), '.claude', 'agents')
|
||||
const projectKodeDir = join(getCwd(), '.kode', 'agents')
|
||||
|
||||
const watchDirectory = (dirPath: string, label: string) => {
|
||||
if (existsSync(dirPath)) {
|
||||
const watcher = watch(dirPath, { recursive: false }, async (eventType, filename) => {
|
||||
if (filename && filename.endsWith('.md')) {
|
||||
console.log(`🔄 Agent configuration changed in ${label}: ${filename}`)
|
||||
clearAgentCache()
|
||||
// Also clear any other related caches
|
||||
getAllAgents.cache?.clear?.()
|
||||
onChange?.()
|
||||
}
|
||||
})
|
||||
watchers.push(watcher)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch all directories
|
||||
watchDirectory(userClaudeDir, 'user/.claude')
|
||||
watchDirectory(userKodeDir, 'user/.kode')
|
||||
watchDirectory(projectClaudeDir, 'project/.claude')
|
||||
watchDirectory(projectKodeDir, 'project/.kode')
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watching agent configuration directories
|
||||
*/
|
||||
export async function stopAgentWatcher(): Promise<void> {
|
||||
const closePromises = watchers.map(watcher =>
|
||||
new Promise<void>((resolve) => {
|
||||
try {
|
||||
watcher.close((err) => {
|
||||
if (err) {
|
||||
console.error('Failed to close file watcher:', err)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error closing watcher:', error)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.allSettled(closePromises)
|
||||
watchers = []
|
||||
}
|
||||
139
src/utils/commonUnixCommands.ts
Normal file
139
src/utils/commonUnixCommands.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Common Unix Commands Database
|
||||
*
|
||||
* A curated list of 500+ most frequently used Unix/Linux commands
|
||||
* for developers and system administrators.
|
||||
*
|
||||
* Categories:
|
||||
* - File & Directory Operations
|
||||
* - Text Processing
|
||||
* - Process Management
|
||||
* - Network Tools
|
||||
* - Development Tools
|
||||
* - System Administration
|
||||
* - Package Management
|
||||
* - Version Control
|
||||
*/
|
||||
|
||||
export const COMMON_UNIX_COMMANDS = [
|
||||
// File & Directory Operations (50+)
|
||||
'ls', 'cd', 'pwd', 'mkdir', 'rmdir', 'rm', 'cp', 'mv', 'touch', 'cat',
|
||||
'less', 'more', 'head', 'tail', 'file', 'stat', 'ln', 'readlink', 'basename', 'dirname',
|
||||
'find', 'locate', 'which', 'whereis', 'type', 'tree', 'du', 'df', 'mount', 'umount',
|
||||
'chmod', 'chown', 'chgrp', 'umask', 'setfacl', 'getfacl', 'lsattr', 'chattr', 'realpath', 'mktemp',
|
||||
'rsync', 'scp', 'sftp', 'ftp', 'wget', 'curl', 'tar', 'gzip', 'gunzip', 'zip',
|
||||
'unzip', 'bzip2', 'bunzip2', 'xz', 'unxz', '7z', 'rar', 'unrar', 'zcat', 'zless',
|
||||
|
||||
// Text Processing (50+)
|
||||
'grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack', 'sed', 'awk', 'cut', 'paste',
|
||||
'sort', 'uniq', 'wc', 'tr', 'col', 'column', 'expand', 'unexpand', 'fold', 'fmt',
|
||||
'pr', 'nl', 'od', 'hexdump', 'xxd', 'strings', 'split', 'csplit', 'join', 'comm',
|
||||
'diff', 'sdiff', 'vimdiff', 'patch', 'diffstat', 'cmp', 'md5sum', 'sha1sum', 'sha256sum', 'sha512sum',
|
||||
'base64', 'uuencode', 'uudecode', 'rev', 'tac', 'shuf', 'jq', 'yq', 'xmllint', 'tidy',
|
||||
|
||||
// Process Management (40+)
|
||||
'ps', 'top', 'htop', 'atop', 'iotop', 'iftop', 'nethogs', 'pgrep', 'pkill', 'kill',
|
||||
'killall', 'jobs', 'bg', 'fg', 'nohup', 'disown', 'nice', 'renice', 'ionice', 'taskset',
|
||||
'pstree', 'fuser', 'lsof', 'strace', 'ltrace', 'ptrace', 'gdb', 'valgrind', 'time', 'timeout',
|
||||
'watch', 'screen', 'tmux', 'byobu', 'dtach', 'nmon', 'dstat', 'vmstat', 'iostat', 'mpstat',
|
||||
|
||||
// Network Tools (50+)
|
||||
'ping', 'ping6', 'traceroute', 'tracepath', 'mtr', 'netstat', 'ss', 'ip', 'ifconfig', 'route',
|
||||
'arp', 'hostname', 'hostnamectl', 'nslookup', 'dig', 'host', 'whois', 'nc', 'netcat', 'ncat',
|
||||
'socat', 'telnet', 'ssh', 'ssh-keygen', 'ssh-copy-id', 'ssh-add', 'ssh-agent', 'sshd', 'tcpdump', 'wireshark',
|
||||
'tshark', 'nmap', 'masscan', 'zmap', 'iptables', 'ip6tables', 'firewall-cmd', 'ufw', 'fail2ban', 'nginx',
|
||||
'apache2', 'httpd', 'curl', 'wget', 'aria2', 'axel', 'links', 'lynx', 'w3m', 'elinks',
|
||||
|
||||
// Development Tools - Languages (60+)
|
||||
'gcc', 'g++', 'clang', 'clang++', 'make', 'cmake', 'autoconf', 'automake', 'libtool', 'pkg-config',
|
||||
'python', 'python2', 'python3', 'pip', 'pip2', 'pip3', 'pipenv', 'poetry', 'virtualenv', 'pyenv',
|
||||
'node', 'npm', 'uv', 'npx', 'yarn', 'pnpm', 'nvm', 'volta', 'deno', 'bun', 'tsx',
|
||||
'ruby', 'gem', 'bundle', 'bundler', 'rake', 'rbenv', 'rvm', 'irb', 'pry', 'rails',
|
||||
'java', 'javac', 'jar', 'javadoc', 'maven', 'mvn', 'gradle', 'ant', 'kotlin', 'kotlinc',
|
||||
'go', 'gofmt', 'golint', 'govet', 'godoc', 'rust', 'rustc', 'cargo', 'rustup', 'rustfmt',
|
||||
|
||||
// Development Tools - Utilities (40+)
|
||||
'git', 'svn', 'hg', 'bzr', 'cvs', 'fossil', 'tig', 'gitk', 'git-flow', 'hub',
|
||||
'gh', 'glab', 'docker', 'docker-compose', 'podman', 'kubectl', 'helm', 'minikube', 'kind', 'k3s',
|
||||
'vagrant', 'terraform', 'ansible', 'puppet', 'chef', 'salt', 'packer', 'consul', 'vault', 'nomad',
|
||||
'vim', 'vi', 'nvim', 'emacs', 'nano', 'pico', 'ed', 'code', 'subl', 'atom',
|
||||
|
||||
// Database & Data Tools (30+)
|
||||
'mysql', 'mysqldump', 'mysqladmin', 'psql', 'pg_dump', 'pg_restore', 'sqlite3', 'redis-cli', 'mongo', 'mongodump',
|
||||
'mongorestore', 'cqlsh', 'influx', 'clickhouse-client', 'mariadb', 'cockroach', 'etcdctl', 'consul', 'vault', 'nomad',
|
||||
'jq', 'yq', 'xmlstarlet', 'csvkit', 'miller', 'awk', 'sed', 'perl', 'lua', 'tcl',
|
||||
|
||||
// System Administration (50+)
|
||||
'sudo', 'su', 'passwd', 'useradd', 'userdel', 'usermod', 'groupadd', 'groupdel', 'groupmod', 'id',
|
||||
'who', 'w', 'last', 'lastlog', 'finger', 'chfn', 'chsh', 'login', 'logout', 'exit',
|
||||
'systemctl', 'service', 'journalctl', 'systemd-analyze', 'init', 'telinit', 'runlevel', 'shutdown', 'reboot', 'halt',
|
||||
'poweroff', 'uptime', 'uname', 'hostname', 'hostnamectl', 'timedatectl', 'localectl', 'loginctl', 'machinectl', 'bootctl',
|
||||
'cron', 'crontab', 'at', 'batch', 'anacron', 'systemd-run', 'systemd-timer', 'logrotate', 'logger', 'dmesg',
|
||||
|
||||
// Package Management (30+)
|
||||
'apt', 'apt-get', 'apt-cache', 'dpkg', 'dpkg-reconfigure', 'aptitude', 'snap', 'flatpak', 'appimage', 'alien',
|
||||
'yum', 'dnf', 'rpm', 'zypper', 'pacman', 'yaourt', 'yay', 'makepkg', 'abs', 'aur',
|
||||
'brew', 'port', 'pkg', 'emerge', 'portage', 'nix', 'guix', 'conda', 'mamba', 'micromamba',
|
||||
|
||||
// Monitoring & Performance (30+)
|
||||
'top', 'htop', 'atop', 'btop', 'gtop', 'gotop', 'bashtop', 'bpytop', 'glances', 'nmon',
|
||||
'sar', 'iostat', 'mpstat', 'vmstat', 'pidstat', 'free', 'uptime', 'tload', 'slabtop', 'powertop',
|
||||
'iotop', 'iftop', 'nethogs', 'bmon', 'nload', 'speedtest', 'speedtest-cli', 'fast', 'mtr', 'smokeping',
|
||||
|
||||
// Security Tools (30+)
|
||||
'gpg', 'gpg2', 'openssl', 'ssh-keygen', 'ssh-keyscan', 'ssl-cert', 'certbot', 'acme.sh', 'mkcert', 'step',
|
||||
'pass', 'keepassxc-cli', 'bitwarden', '1password', 'hashcat', 'john', 'hydra', 'ncrack', 'medusa', 'aircrack-ng',
|
||||
'chkrootkit', 'rkhunter', 'clamav', 'clamscan', 'freshclam', 'aide', 'tripwire', 'samhain', 'ossec', 'wazuh',
|
||||
|
||||
// Shell & Scripting (30+)
|
||||
'bash', 'sh', 'zsh', 'fish', 'ksh', 'tcsh', 'csh', 'dash', 'ash', 'elvish',
|
||||
'export', 'alias', 'unalias', 'history', 'fc', 'source', 'eval', 'exec', 'command', 'builtin',
|
||||
'set', 'unset', 'env', 'printenv', 'echo', 'printf', 'read', 'test', 'expr', 'let',
|
||||
|
||||
// Archive & Compression (20+)
|
||||
'tar', 'gzip', 'gunzip', 'bzip2', 'bunzip2', 'xz', 'unxz', 'lzma', 'unlzma', 'compress',
|
||||
'uncompress', 'zip', 'unzip', '7z', '7za', 'rar', 'unrar', 'ar', 'cpio', 'pax',
|
||||
|
||||
// Media Tools (20+)
|
||||
'ffmpeg', 'ffplay', 'ffprobe', 'sox', 'play', 'rec', 'mpg123', 'mpg321', 'ogg123', 'flac',
|
||||
'lame', 'oggenc', 'opusenc', 'convert', 'mogrify', 'identify', 'display', 'import', 'animate', 'montage',
|
||||
|
||||
// Math & Calculation (15+)
|
||||
'bc', 'dc', 'calc', 'qalc', 'units', 'factor', 'primes', 'seq', 'shuf', 'random',
|
||||
'octave', 'maxima', 'sage', 'r', 'julia',
|
||||
|
||||
// Documentation & Help (15+)
|
||||
'man', 'info', 'help', 'apropos', 'whatis', 'whereis', 'which', 'type', 'command', 'hash',
|
||||
'tldr', 'cheat', 'howdoi', 'stackoverflow', 'explainshell',
|
||||
|
||||
// Miscellaneous Utilities (30+)
|
||||
'date', 'cal', 'ncal', 'timedatectl', 'zdump', 'tzselect', 'hwclock', 'ntpdate', 'chrony', 'timeshift',
|
||||
'yes', 'true', 'false', 'sleep', 'usleep', 'seq', 'jot', 'shuf', 'tee', 'xargs',
|
||||
'parallel', 'rush', 'dsh', 'pssh', 'clusterssh', 'terminator', 'tilix', 'alacritty', 'kitty', 'wezterm',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Get common commands that exist on the current system
|
||||
* @param systemCommands Array of commands available on the system
|
||||
* @returns Deduplicated intersection of common commands and system commands
|
||||
*/
|
||||
export function getCommonSystemCommands(systemCommands: string[]): string[] {
|
||||
const systemSet = new Set(systemCommands.map(cmd => cmd.toLowerCase()))
|
||||
const commonIntersection = COMMON_UNIX_COMMANDS.filter(cmd => systemSet.has(cmd.toLowerCase()))
|
||||
// Remove duplicates using Set
|
||||
return Array.from(new Set(commonIntersection))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a priority score for a command based on its position in the common list
|
||||
* Earlier commands get higher priority (more commonly used)
|
||||
*/
|
||||
export function getCommandPriority(command: string): number {
|
||||
const index = COMMON_UNIX_COMMANDS.indexOf(command.toLowerCase() as any)
|
||||
if (index === -1) return 0
|
||||
|
||||
// Convert index to priority score (earlier = higher score)
|
||||
const maxScore = 100
|
||||
const score = maxScore - (index / COMMON_UNIX_COMMANDS.length) * maxScore
|
||||
return Math.round(score)
|
||||
}
|
||||
328
src/utils/fuzzyMatcher.ts
Normal file
328
src/utils/fuzzyMatcher.ts
Normal file
@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Input Method Inspired Fuzzy Matching Algorithm
|
||||
*
|
||||
* Multi-algorithm weighted scoring system inspired by:
|
||||
* - Sogou/Baidu Pinyin input method algorithms
|
||||
* - Double-pinyin abbreviation matching
|
||||
* - Terminal completion best practices (fzf, zsh, fish)
|
||||
*
|
||||
* Designed specifically for command/terminal completion scenarios
|
||||
* where users type abbreviations like "nde" expecting "node"
|
||||
*/
|
||||
|
||||
export interface MatchResult {
|
||||
score: number
|
||||
algorithm: string // Which algorithm contributed most to the score
|
||||
confidence: number // 0-1 confidence level
|
||||
}
|
||||
|
||||
export interface FuzzyMatcherConfig {
|
||||
// Algorithm weights (must sum to 1.0)
|
||||
weights: {
|
||||
prefix: number // Direct prefix matching ("nod" → "node")
|
||||
substring: number // Substring matching ("ode" → "node")
|
||||
abbreviation: number // Key chars matching ("nde" → "node")
|
||||
editDistance: number // Typo tolerance ("noda" → "node")
|
||||
popularity: number // Common command boost
|
||||
}
|
||||
|
||||
// Scoring parameters
|
||||
minScore: number // Minimum score threshold
|
||||
maxEditDistance: number // Maximum edits allowed
|
||||
popularCommands: string[] // Commands to boost
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: FuzzyMatcherConfig = {
|
||||
weights: {
|
||||
prefix: 0.35, // Strong weight for prefix matching
|
||||
substring: 0.20, // Good for partial matches
|
||||
abbreviation: 0.30, // Key for "nde"→"node" cases
|
||||
editDistance: 0.10, // Typo tolerance
|
||||
popularity: 0.05 // Slight bias for common commands
|
||||
},
|
||||
minScore: 10, // Lower threshold for better matching
|
||||
maxEditDistance: 2,
|
||||
popularCommands: [
|
||||
'node', 'npm', 'git', 'ls', 'cd', 'cat', 'grep', 'find', 'cp', 'mv',
|
||||
'python', 'java', 'docker', 'curl', 'wget', 'vim', 'nano'
|
||||
]
|
||||
}
|
||||
|
||||
export class FuzzyMatcher {
|
||||
private config: FuzzyMatcherConfig
|
||||
|
||||
constructor(config: Partial<FuzzyMatcherConfig> = {}) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config }
|
||||
|
||||
// Normalize weights to sum to 1.0
|
||||
const weightSum = Object.values(this.config.weights).reduce((a, b) => a + b, 0)
|
||||
if (Math.abs(weightSum - 1.0) > 0.01) {
|
||||
Object.keys(this.config.weights).forEach(key => {
|
||||
this.config.weights[key as keyof typeof this.config.weights] /= weightSum
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fuzzy match score for a candidate against a query
|
||||
*/
|
||||
match(candidate: string, query: string): MatchResult {
|
||||
const text = candidate.toLowerCase()
|
||||
const pattern = query.toLowerCase()
|
||||
|
||||
// Quick perfect match exits
|
||||
if (text === pattern) {
|
||||
return { score: 1000, algorithm: 'exact', confidence: 1.0 }
|
||||
}
|
||||
if (text.startsWith(pattern)) {
|
||||
return {
|
||||
score: 900 + (10 - pattern.length),
|
||||
algorithm: 'prefix-exact',
|
||||
confidence: 0.95
|
||||
}
|
||||
}
|
||||
|
||||
// Run all algorithms
|
||||
const scores = {
|
||||
prefix: this.prefixScore(text, pattern),
|
||||
substring: this.substringScore(text, pattern),
|
||||
abbreviation: this.abbreviationScore(text, pattern),
|
||||
editDistance: this.editDistanceScore(text, pattern),
|
||||
popularity: this.popularityScore(text)
|
||||
}
|
||||
|
||||
// Weighted combination
|
||||
const rawScore = Object.entries(scores).reduce((total, [algorithm, score]) => {
|
||||
const weight = this.config.weights[algorithm as keyof typeof this.config.weights]
|
||||
return total + (score * weight)
|
||||
}, 0)
|
||||
|
||||
// Length penalty (prefer shorter commands)
|
||||
const lengthPenalty = Math.max(0, text.length - 6) * 1.5
|
||||
const finalScore = Math.max(0, rawScore - lengthPenalty)
|
||||
|
||||
// Determine primary algorithm and confidence
|
||||
const maxAlgorithm = Object.entries(scores).reduce((max, [alg, score]) =>
|
||||
score > max.score ? { algorithm: alg, score } : max,
|
||||
{ algorithm: 'none', score: 0 }
|
||||
)
|
||||
|
||||
const confidence = Math.min(1.0, finalScore / 100)
|
||||
|
||||
return {
|
||||
score: finalScore,
|
||||
algorithm: maxAlgorithm.algorithm,
|
||||
confidence
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm 1: Prefix Matching (like pinyin prefix)
|
||||
* Handles cases like "nod" → "node"
|
||||
*/
|
||||
private prefixScore(text: string, pattern: string): number {
|
||||
if (!text.startsWith(pattern)) return 0
|
||||
|
||||
// Score based on prefix length vs total length
|
||||
const coverage = pattern.length / text.length
|
||||
return 100 * coverage
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm 2: Substring Matching (like pinyin contains)
|
||||
* Handles cases like "ode" → "node", "py3" → "python3"
|
||||
*/
|
||||
private substringScore(text: string, pattern: string): number {
|
||||
// Direct substring match
|
||||
const index = text.indexOf(pattern)
|
||||
if (index !== -1) {
|
||||
// Earlier position and better coverage = higher score
|
||||
const positionFactor = Math.max(0, 10 - index) / 10
|
||||
const coverageFactor = pattern.length / text.length
|
||||
return 80 * positionFactor * coverageFactor
|
||||
}
|
||||
|
||||
// Special handling for numeric suffixes (py3 → python3)
|
||||
// Check if pattern ends with a number and try prefix match + number
|
||||
const numMatch = pattern.match(/^(.+?)(\d+)$/)
|
||||
if (numMatch) {
|
||||
const [, prefix, num] = numMatch
|
||||
// Check if text starts with prefix and ends with the same number
|
||||
if (text.startsWith(prefix) && text.endsWith(num)) {
|
||||
// Good match for patterns like "py3" → "python3"
|
||||
const coverageFactor = pattern.length / text.length
|
||||
return 70 * coverageFactor + 20 // Bonus for numeric suffix match
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm 3: Abbreviation Matching (key innovation)
|
||||
* Handles cases like "nde" → "node", "pyt3" → "python3", "gp5" → "gpt-5"
|
||||
*/
|
||||
private abbreviationScore(text: string, pattern: string): number {
|
||||
let score = 0
|
||||
let textPos = 0
|
||||
let perfectStart = false
|
||||
let consecutiveMatches = 0
|
||||
let wordBoundaryMatches = 0
|
||||
|
||||
// Split text by hyphens to handle word boundaries better
|
||||
const textWords = text.split('-')
|
||||
const textClean = text.replace(/-/g, '').toLowerCase()
|
||||
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
const char = pattern[i]
|
||||
let charFound = false
|
||||
|
||||
// Try to find in clean text (no hyphens)
|
||||
for (let j = textPos; j < textClean.length; j++) {
|
||||
if (textClean[j] === char) {
|
||||
charFound = true
|
||||
|
||||
// Check if this character is at a word boundary in original text
|
||||
let originalPos = 0
|
||||
let cleanPos = 0
|
||||
for (let k = 0; k < text.length; k++) {
|
||||
if (text[k] === '-') continue
|
||||
if (cleanPos === j) {
|
||||
originalPos = k
|
||||
break
|
||||
}
|
||||
cleanPos++
|
||||
}
|
||||
|
||||
// Consecutive character bonus
|
||||
if (j === textPos) {
|
||||
consecutiveMatches++
|
||||
} else {
|
||||
consecutiveMatches = 1
|
||||
}
|
||||
|
||||
// Position-sensitive scoring
|
||||
if (i === 0 && j === 0) {
|
||||
score += 50 // Perfect first character
|
||||
perfectStart = true
|
||||
} else if (originalPos === 0 || text[originalPos - 1] === '-') {
|
||||
score += 35 // Word boundary match
|
||||
wordBoundaryMatches++
|
||||
} else if (j <= 2) {
|
||||
score += 20 // Early position
|
||||
} else if (j <= 6) {
|
||||
score += 10 // Mid position
|
||||
} else {
|
||||
score += 5 // Late position
|
||||
}
|
||||
|
||||
// Consecutive character bonus
|
||||
if (consecutiveMatches > 1) {
|
||||
score += consecutiveMatches * 5
|
||||
}
|
||||
|
||||
textPos = j + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!charFound) return 0 // Invalid abbreviation
|
||||
}
|
||||
|
||||
// Critical bonuses
|
||||
if (perfectStart) score += 30
|
||||
if (wordBoundaryMatches >= 2) score += 25 // Multiple word boundaries
|
||||
if (textPos <= textClean.length * 0.8) score += 15 // Compact abbreviation
|
||||
|
||||
// Special bonus for number matching at end
|
||||
const lastPatternChar = pattern[pattern.length - 1]
|
||||
const lastTextChar = text[text.length - 1]
|
||||
if (/\d/.test(lastPatternChar) && lastPatternChar === lastTextChar) {
|
||||
score += 25
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm 4: Edit Distance (typo tolerance)
|
||||
* Handles cases like "noda" → "node"
|
||||
*/
|
||||
private editDistanceScore(text: string, pattern: string): number {
|
||||
if (pattern.length > text.length + this.config.maxEditDistance) return 0
|
||||
|
||||
// Simplified Levenshtein distance
|
||||
const dp: number[][] = []
|
||||
const m = pattern.length
|
||||
const n = text.length
|
||||
|
||||
// Initialize DP table
|
||||
for (let i = 0; i <= m; i++) {
|
||||
dp[i] = []
|
||||
for (let j = 0; j <= n; j++) {
|
||||
if (i === 0) dp[i][j] = j
|
||||
else if (j === 0) dp[i][j] = i
|
||||
else {
|
||||
const cost = pattern[i-1] === text[j-1] ? 0 : 1
|
||||
dp[i][j] = Math.min(
|
||||
dp[i-1][j] + 1, // deletion
|
||||
dp[i][j-1] + 1, // insertion
|
||||
dp[i-1][j-1] + cost // substitution
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const distance = dp[m][n]
|
||||
if (distance > this.config.maxEditDistance) return 0
|
||||
|
||||
return Math.max(0, 30 - distance * 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm 5: Command Popularity (like frequency in input method)
|
||||
* Boost common commands that users frequently type
|
||||
*/
|
||||
private popularityScore(text: string): number {
|
||||
if (this.config.popularCommands.includes(text)) {
|
||||
return 40
|
||||
}
|
||||
|
||||
// Short commands are often more commonly used
|
||||
if (text.length <= 5) return 10
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch match multiple candidates and return sorted results
|
||||
*/
|
||||
matchMany(candidates: string[], query: string): Array<{candidate: string, result: MatchResult}> {
|
||||
return candidates
|
||||
.map(candidate => ({
|
||||
candidate,
|
||||
result: this.match(candidate, query)
|
||||
}))
|
||||
.filter(item => item.result.score >= this.config.minScore)
|
||||
.sort((a, b) => b.result.score - a.result.score)
|
||||
}
|
||||
}
|
||||
|
||||
// Export convenience functions
|
||||
export const defaultMatcher = new FuzzyMatcher()
|
||||
|
||||
export function matchCommand(command: string, query: string): MatchResult {
|
||||
return defaultMatcher.match(command, query)
|
||||
}
|
||||
|
||||
// Import the advanced matcher
|
||||
import { matchManyAdvanced } from './advancedFuzzyMatcher'
|
||||
|
||||
export function matchCommands(commands: string[], query: string): Array<{command: string, score: number}> {
|
||||
// Use the advanced matcher for better results
|
||||
return matchManyAdvanced(commands, query, 5) // Lower threshold for better matching
|
||||
.map(item => ({
|
||||
command: item.candidate,
|
||||
score: item.score
|
||||
}))
|
||||
}
|
||||
@ -355,7 +355,7 @@ export async function processUserInput(
|
||||
if (input.includes('!`') || input.includes('@')) {
|
||||
try {
|
||||
// Import functions from customCommands service to avoid code duplication
|
||||
const { executeBashCommands, resolveFileReferences } = await import(
|
||||
const { executeBashCommands } = await import(
|
||||
'../services/customCommands'
|
||||
)
|
||||
|
||||
@ -366,11 +366,12 @@ export async function processUserInput(
|
||||
processedInput = await executeBashCommands(processedInput)
|
||||
}
|
||||
|
||||
// Resolve file references if present
|
||||
// Process mentions for system reminder integration
|
||||
// Note: We don't call resolveFileReferences here anymore -
|
||||
// @file mentions should trigger Read tool usage via reminders, not embed content
|
||||
if (input.includes('@')) {
|
||||
// Note: This function is not exported from customCommands.ts, so we need to expose it
|
||||
// For now, we'll keep the local implementation until we refactor the service
|
||||
processedInput = await resolveFileReferences(processedInput)
|
||||
const { processMentions } = await import('../services/mentionProcessor')
|
||||
await processMentions(input)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Dynamic content processing failed:', error)
|
||||
|
||||
@ -13,6 +13,9 @@ export interface Theme {
|
||||
success: string
|
||||
error: string
|
||||
warning: string
|
||||
// UI colors
|
||||
primary: string
|
||||
secondary: string
|
||||
diff: {
|
||||
added: string
|
||||
removed: string
|
||||
@ -33,6 +36,8 @@ const lightTheme: Theme = {
|
||||
success: '#2c7a39',
|
||||
error: '#ab2b3f',
|
||||
warning: '#966c1e',
|
||||
primary: '#000',
|
||||
secondary: '#666',
|
||||
diff: {
|
||||
added: '#69db7c',
|
||||
removed: '#ffa8b4',
|
||||
@ -53,6 +58,8 @@ const lightDaltonizedTheme: Theme = {
|
||||
success: '#006699', // Blue instead of green
|
||||
error: '#cc0000', // Pure red for better distinction
|
||||
warning: '#ff9900', // Orange adjusted for deuteranopia
|
||||
primary: '#000',
|
||||
secondary: '#666',
|
||||
diff: {
|
||||
added: '#99ccff', // Light blue instead of green
|
||||
removed: '#ffcccc', // Light red for better contrast
|
||||
@ -73,6 +80,8 @@ const darkTheme: Theme = {
|
||||
success: '#4eba65',
|
||||
error: '#ff6b80',
|
||||
warning: '#ffc107',
|
||||
primary: '#fff',
|
||||
secondary: '#999',
|
||||
diff: {
|
||||
added: '#225c2b',
|
||||
removed: '#7a2936',
|
||||
@ -93,6 +102,8 @@ const darkDaltonizedTheme: Theme = {
|
||||
success: '#3399ff', // Bright blue instead of green
|
||||
error: '#ff6666', // Bright red for better visibility
|
||||
warning: '#ffcc00', // Yellow-orange for deuteranopia
|
||||
primary: '#fff',
|
||||
secondary: '#999',
|
||||
diff: {
|
||||
added: '#004466', // Dark blue instead of green
|
||||
removed: '#660000', // Dark red for better contrast
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user