Merge subagent implementation with intelligent completion system

# Conflicts:
#	src/services/claude.ts
This commit is contained in:
CrazyBoyM 2025-08-22 13:24:28 +08:00
commit 5bf4bb2182
63 changed files with 9082 additions and 451 deletions

View 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.

View 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.

View 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.

View 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.

View 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
View File

@ -9,7 +9,7 @@ yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
KODE.md
AGENTS.md
# Caches

View 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

View 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.

View 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

View 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.

View 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
View 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.*

View File

@ -2,22 +2,65 @@
[![npm version](https://badge.fury.io/js/@shareai-lab%2Fkode.svg)](https://www.npmjs.com/package/@shareai-lab/kode)
[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
[![AGENTS.md](https://img.shields.io/badge/AGENTS.md-Compatible-brightgreen)](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

View 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行为
准备好实施了吗?

View File

@ -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
View 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. 保持专注流程

View 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
View 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
View 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
View 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

View File

@ -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 状态和最近的提交
- 目录结构
- 先前的对话历史

View File

@ -470,11 +470,8 @@ function loadConfig(path: string): Config {
### 调试命令
```bash
# 显示有效配置
kode config list --effective
# 验证配置
kode config validate
# 显示配置
kode config list
# 重置为默认值
kode config reset

View File

@ -26,7 +26,7 @@ Kode 中的所有内容都被抽象为"工具" - 一个自包含的功能单元
AI 自动通过以下方式理解您的项目:
- Git 状态和最近的提交
- 目录结构分析
- KODE.md 和 CLAUDE.md 项目文档
- AGENTS.md 和 CLAUDE.md 项目文档
- .claude/commands/ 和 .kode/commands/ 中的自定义命令定义
- 先前的对话历史和分叉对话

View File

@ -379,9 +379,6 @@ interface PermissionRequest {
2. **调查**
```bash
# 审查审计日志
kode security audit --last 1h
# 检查修改的文件
git status
git diff

View File

@ -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

View File

@ -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

View File

@ -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> {

View File

@ -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

View File

@ -379,9 +379,6 @@ interface PermissionRequest {
2. **Investigation**
```bash
# Review audit logs
kode security audit --last 1h
# Check modified files
git status
git diff

View 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
View 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

1
main.js Normal file
View File

@ -0,0 +1 @@
Testing file 2

View File

@ -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",

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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 {

View File

@ -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}>

View File

@ -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 ||

View File

@ -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 &&

View 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>
)
}

View 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>
)
}

View File

@ -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'

View File

@ -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)
}

View File

@ -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) {

View File

@ -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,
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -410,6 +410,7 @@ export async function* runToolUse(
currentRequest?.id,
)
logEvent('tengu_tool_use_start', {
toolName: toolUse.name,
toolUseID: toolUse.id,

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View 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()

View File

@ -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,

View File

@ -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>&nbsp;&nbsp; &nbsp;</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>&nbsp;&nbsp; &nbsp;</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.`,
)
}

View File

@ -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[]>

View File

@ -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>`
}

View 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
View 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 = []
}

View 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
View 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
}))
}

View File

@ -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)

View File

@ -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