Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 258b29f5f0 | |||
| 6a69a524ea | |||
| 10c1de8843 | |||
| 4fb52596e3 | |||
| 9dfb52ac8d | |||
| c9be0cf804 | |||
| 60577f8951 |
173
CHANGELOG.md
173
CHANGELOG.md
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.0] - 2026-01-16
|
||||
|
||||
### Added
|
||||
|
||||
- **Conflict Resolution System** - Git-style diff visualization for code review
|
||||
- New `conflict.lua` module with full conflict management
|
||||
- Git-style markers: `<<<<<<< CURRENT`, `=======`, `>>>>>>> INCOMING`
|
||||
- Visual highlighting: green for original, blue for AI suggestions
|
||||
- Buffer-local keymaps: `co` (ours), `ct` (theirs), `cb` (both), `cn` (none)
|
||||
- Navigation keymaps: `]x` (next), `[x` (previous)
|
||||
- Floating menu with `cm` or `<CR>` on conflict
|
||||
- Number keys `1-4` for quick selection in menu
|
||||
- Auto-show menu after code injection
|
||||
- Auto-show menu for next conflict after resolution
|
||||
- Commands: `:CoderConflictToggle`, `:CoderConflictMenu`, `:CoderConflictNext`, `:CoderConflictPrev`, `:CoderConflictStatus`, `:CoderConflictResolveAll`, `:CoderConflictAcceptCurrent`, `:CoderConflictAcceptIncoming`, `:CoderConflictAcceptBoth`, `:CoderConflictAcceptNone`, `:CoderConflictAutoMenu`
|
||||
|
||||
- **Linter Validation System** - Auto-check and fix lint errors after code injection
|
||||
- New `linter.lua` module for LSP diagnostics integration
|
||||
- Auto-saves file after code injection
|
||||
- Waits for LSP diagnostics to update
|
||||
- Detects errors and warnings in injected code region
|
||||
- Auto-queues AI fix prompts for lint errors
|
||||
- Shows errors in quickfix list
|
||||
- Commands: `:CoderLintCheck`, `:CoderLintFix`, `:CoderLintQuickfix`, `:CoderLintToggleAuto`
|
||||
|
||||
- **SEARCH/REPLACE Block System** - Reliable code editing with fuzzy matching
|
||||
- New `search_replace.lua` module for reliable code editing
|
||||
- Parses SEARCH/REPLACE blocks from LLM responses
|
||||
- Fuzzy matching with configurable thresholds
|
||||
- Whitespace normalization for better matching
|
||||
- Multiple matching strategies: exact, normalized, line-by-line
|
||||
- Automatic fallback to line-based injection
|
||||
|
||||
- **Process and Show Menu Function** - Streamlined conflict handling
|
||||
- New `process_and_show_menu()` function combines processing and menu display
|
||||
- Ensures highlights and keymaps are set up before showing menu
|
||||
|
||||
### Changed
|
||||
|
||||
- Unified automatic and manual tag processing to use same code path
|
||||
- `insert_conflict()` now only inserts markers, callers handle processing
|
||||
- Added `nowait = true` to conflict keymaps to prevent delay from built-in `c` command
|
||||
- Improved patch application flow with conflict mode integration
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `string.gsub` returning two values causing `table.insert` errors
|
||||
- Fixed keymaps not triggering due to Neovim's `c` command intercepting first character
|
||||
- Fixed menu not showing after code injection
|
||||
- Fixed diff highlighting not appearing
|
||||
|
||||
---
|
||||
|
||||
## [0.5.0] - 2026-01-15
|
||||
|
||||
### Added
|
||||
@@ -25,14 +78,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Notifies user of provider switch
|
||||
|
||||
- **Enhanced Error Handling** - Better error messages for API failures
|
||||
- Shows actual API response on parse errors (not generic "failed to parse")
|
||||
- Shows actual API response on parse errors
|
||||
- Improved rate limit detection and messaging
|
||||
- Sanitized newlines in error notifications to prevent UI crashes
|
||||
- Sanitized newlines in error notifications
|
||||
|
||||
- **Agent Tools System Improvements**
|
||||
- New `to_openai_format()` and `to_claude_format()` functions
|
||||
- `get_definitions()` for generic tool access
|
||||
- Fixed tool call argument serialization (JSON strings vs tables)
|
||||
- Fixed tool call argument serialization
|
||||
|
||||
- **Credentials Management System** - Store API keys outside of config files
|
||||
- New `:CoderAddApiKey` command for interactive credential setup
|
||||
@@ -40,8 +93,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `:CoderCredentials` to view credential status
|
||||
- `:CoderSwitchProvider` to switch active LLM provider
|
||||
- Credentials stored in `~/.local/share/nvim/codetyper/configuration.json`
|
||||
- Priority: stored credentials > config > environment variables
|
||||
- Supports all providers: Claude, OpenAI, Gemini, Copilot, Ollama
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -54,7 +105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fixed "Failed to parse Copilot response" error showing instead of actual error
|
||||
- Fixed `nvim_buf_set_lines` crash from newlines in error messages
|
||||
- Fixed `tools.definitions` nil error in agent initialization
|
||||
- Fixed tool name mismatch in agent prompts (write_file vs write)
|
||||
- Fixed tool name mismatch in agent prompts
|
||||
|
||||
---
|
||||
|
||||
@@ -63,7 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- **Event-Driven Architecture** - Complete rewrite of prompt processing system
|
||||
- Prompts are now treated as events with metadata (buffer state, priority, timestamps)
|
||||
- Prompts are now treated as events with metadata
|
||||
- New modules: `queue.lua`, `patch.lua`, `confidence.lua`, `worker.lua`, `scheduler.lua`
|
||||
- Priority-based event queue with observer pattern
|
||||
- Buffer snapshots for staleness detection
|
||||
@@ -74,42 +125,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Configurable escalation threshold (default: 0.7)
|
||||
|
||||
- **Confidence Scoring** - Response quality heuristics
|
||||
- 5 weighted heuristics: length, uncertainty phrases, syntax completeness, repetition, truncation
|
||||
- 5 weighted heuristics: length, uncertainty, syntax, repetition, truncation
|
||||
- Scores range from 0.0-1.0
|
||||
- Determines whether to escalate to more capable LLM
|
||||
|
||||
- **Staleness Detection** - Safe patch application
|
||||
- Track `vim.b.changedtick` and content hash at prompt time
|
||||
- Discard patches if buffer changed during generation
|
||||
- Prevents stale code injection
|
||||
|
||||
- **Completion-Aware Injection** - No fighting with autocomplete
|
||||
- Defer code injection while completion popup visible
|
||||
- Works with native popup, nvim-cmp, and coq_nvim
|
||||
- Configurable delay after popup closes (default: 100ms)
|
||||
|
||||
- **Tree-sitter Scope Resolution** - Smart context extraction
|
||||
- Automatically resolves prompts to enclosing function/method/class
|
||||
- Falls back to heuristics when Tree-sitter unavailable
|
||||
- Scope types: function, method, class, block, file
|
||||
|
||||
- **Intent Detection** - Understands what you want
|
||||
- Parses prompts to detect: complete, refactor, fix, add, document, test, optimize, explain
|
||||
- Intent determines injection strategy (replace vs insert vs append)
|
||||
- Priority adjustment based on intent type
|
||||
|
||||
- **Tag Precedence Rules** - Multiple tags handled cleanly
|
||||
- First tag in scope wins (FIFO ordering)
|
||||
- Later tags in same scope skipped with warning
|
||||
- Different scopes process independently
|
||||
- Intent determines injection strategy
|
||||
|
||||
### Configuration
|
||||
|
||||
New `scheduler` configuration block:
|
||||
```lua
|
||||
scheduler = {
|
||||
enabled = true, -- Enable event-driven mode
|
||||
ollama_scout = true, -- Use Ollama first
|
||||
enabled = true,
|
||||
ollama_scout = true,
|
||||
escalation_threshold = 0.7,
|
||||
max_concurrent = 2,
|
||||
completion_delay_ms = 100,
|
||||
@@ -122,50 +164,32 @@ scheduler = {
|
||||
|
||||
### Added
|
||||
|
||||
- **Multiple LLM Providers** - Support for additional providers beyond Claude and Ollama
|
||||
- OpenAI API with custom endpoint support (Azure, OpenRouter, etc.)
|
||||
- **Multiple LLM Providers** - Support for additional providers
|
||||
- OpenAI API with custom endpoint support
|
||||
- Google Gemini API
|
||||
- GitHub Copilot (uses existing copilot.lua/copilot.vim authentication)
|
||||
- GitHub Copilot
|
||||
|
||||
- **Agent Mode** - Autonomous coding assistant with tool use
|
||||
- `read_file` - Read file contents
|
||||
- `edit_file` - Edit files with find/replace
|
||||
- `write_file` - Create or overwrite files
|
||||
- `bash` - Execute shell commands
|
||||
- `read_file`, `edit_file`, `write_file`, `bash` tools
|
||||
- Real-time logging of agent actions
|
||||
- `:CoderAgent`, `:CoderAgentToggle`, `:CoderAgentStop` commands
|
||||
|
||||
- **Transform Commands** - Transform /@ @/ tags inline without split view
|
||||
- `:CoderTransform` - Transform all tags in file
|
||||
- `:CoderTransformCursor` - Transform tag at cursor
|
||||
- `:CoderTransformVisual` - Transform selected tags
|
||||
- Default keymaps: `<leader>ctt` (cursor/visual), `<leader>ctT` (all)
|
||||
- **Transform Commands** - Transform /@ @/ tags inline
|
||||
- `:CoderTransform`, `:CoderTransformCursor`, `:CoderTransformVisual`
|
||||
- Default keymaps: `<leader>ctt`, `<leader>ctT`
|
||||
|
||||
- **Auto-Index Feature** - Automatically create coder companion files
|
||||
- Creates `.coder.` companion files when opening source files
|
||||
- Language-aware templates with correct comment syntax
|
||||
- `:CoderIndex` command to manually open companion
|
||||
- `<leader>ci` keymap
|
||||
- Configurable via `auto_index` option (disabled by default)
|
||||
- Language-aware templates
|
||||
|
||||
- **Logs Panel** - Real-time visibility into LLM operations
|
||||
- Token usage tracking (prompt and completion tokens)
|
||||
- "Thinking" process visibility
|
||||
- Request/response logging
|
||||
- `:CoderLogs` command to toggle panel
|
||||
|
||||
- **Mode Switcher** - Switch between Ask and Agent modes
|
||||
- `:CoderType` command shows mode selection UI
|
||||
|
||||
### Changed
|
||||
|
||||
- Window width configuration now uses percentage as whole number (e.g., `25` for 25%)
|
||||
- Window width configuration now uses percentage as whole number
|
||||
- Improved code extraction from LLM responses
|
||||
- Better prompt templates for code generation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Window width calculation consistency across modules
|
||||
|
||||
---
|
||||
|
||||
@@ -174,31 +198,23 @@ scheduler = {
|
||||
### Added
|
||||
|
||||
- **Ask Panel** - Chat interface for asking questions about code
|
||||
- Fixed at 1/4 (25%) screen width for consistent layout
|
||||
- File attachment with `@` key (uses Telescope if available)
|
||||
- `Ctrl+n` to start a new chat (clears input and history)
|
||||
- Fixed at 1/4 (25%) screen width
|
||||
- File attachment with `@` key
|
||||
- `Ctrl+n` to start a new chat
|
||||
- `Ctrl+Enter` to submit questions
|
||||
- `Ctrl+f` to add current file as context
|
||||
- `Ctrl+h/j/k/l` for window navigation
|
||||
- `K/J` to jump between output and input windows
|
||||
- `Y` to copy last response to clipboard
|
||||
- `q` to close panel (closes both windows together)
|
||||
- Auto-open Ask panel on startup (configurable via `auto_open_ask`)
|
||||
- File content is now sent to LLM when attaching files with `@`
|
||||
- `Y` to copy last response
|
||||
|
||||
### Changed
|
||||
|
||||
- Ask panel width is now fixed at 25% (1/4 of screen)
|
||||
- Improved close behavior - closing either Ask window closes both
|
||||
- Proper focus management after closing Ask panel
|
||||
- Compact UI elements to fit 1/4 width layout
|
||||
- Changed "Assistant" label to "AI" in chat messages
|
||||
- Ask panel width is now fixed at 25%
|
||||
- Improved close behavior
|
||||
- Changed "Assistant" label to "AI"
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ask panel window state sync issues
|
||||
- Window focus returning to code after closing Ask panel
|
||||
- NerdTree/nvim-tree causing Ask panel to resize incorrectly
|
||||
- Window focus returning to code after closing
|
||||
|
||||
---
|
||||
|
||||
@@ -210,27 +226,13 @@ scheduler = {
|
||||
- Core plugin architecture with modular Lua structure
|
||||
- Split window view for coder and target files
|
||||
- Tag-based prompt system (`/@` to open, `@/` to close)
|
||||
- Claude API integration for code generation
|
||||
- Ollama API integration for local LLM support
|
||||
- Automatic `.gitignore` management for coder files and `.coder/` folder
|
||||
- Smart prompt type detection (refactor, add, document, explain)
|
||||
- Code injection system with multiple strategies
|
||||
- User commands: `Coder`, `CoderOpen`, `CoderClose`, `CoderToggle`, `CoderProcess`, `CoderTree`, `CoderTreeView`
|
||||
- Health check module (`:checkhealth codetyper`)
|
||||
- Comprehensive documentation and help files
|
||||
- Telescope integration for file selection (optional)
|
||||
- **Project tree logging**: Automatic `.coder/tree.log` maintenance
|
||||
- Updates on file create, save, delete
|
||||
- Debounced updates (1 second) for performance
|
||||
- File type icons for visual clarity
|
||||
- Ignores common build/dependency folders
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- LLM provider selection (Claude/Ollama)
|
||||
- Window position and width customization
|
||||
- Custom prompt tag patterns
|
||||
- Auto gitignore toggle
|
||||
- Claude API integration
|
||||
- Ollama API integration
|
||||
- Automatic `.gitignore` management
|
||||
- Smart prompt type detection
|
||||
- Code injection system
|
||||
- Health check module
|
||||
- Project tree logging
|
||||
|
||||
---
|
||||
|
||||
@@ -245,7 +247,8 @@ scheduler = {
|
||||
- **Fixed** - Bug fixes
|
||||
- **Security** - Vulnerability fixes
|
||||
|
||||
[Unreleased]: https://github.com/cargdev/codetyper.nvim/compare/v0.5.0...HEAD
|
||||
[Unreleased]: https://github.com/cargdev/codetyper.nvim/compare/v0.6.0...HEAD
|
||||
[0.6.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.5.0...v0.6.0
|
||||
[0.5.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.4.0...v0.5.0
|
||||
[0.4.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.2.0...v0.3.0
|
||||
|
||||
@@ -120,7 +120,7 @@ Default configuration: >lua
|
||||
5. LLM PROVIDERS *codetyper-providers*
|
||||
|
||||
*codetyper-claude*
|
||||
Claude (Anthropic)~
|
||||
Claude~
|
||||
Best for complex reasoning and code generation.
|
||||
>lua
|
||||
llm = {
|
||||
|
||||
561
llms.txt
561
llms.txt
@@ -14,68 +14,70 @@ Instead of having an AI generate entire files, Codetyper lets developers maintai
|
||||
2. A companion "coder file" is created (`index.coder.ts`)
|
||||
3. Developer writes prompts using special tags: `/@ prompt @/`
|
||||
4. When the closing tag is typed, the LLM generates code
|
||||
5. Generated code is injected into the target file
|
||||
5. Generated code is shown as a conflict for review
|
||||
6. Developer accepts/rejects changes using keymaps
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
```
|
||||
lua/codetyper/
|
||||
├── init.lua # Main entry, setup function, module initialization
|
||||
├── config.lua # Configuration management, defaults, validation
|
||||
├── types.lua # Lua type definitions for LSP/documentation
|
||||
├── utils.lua # Utility functions (file ops, notifications)
|
||||
├── commands.lua # Vim command definitions (:Coder, :CoderOpen, etc.)
|
||||
├── window.lua # Split window management (open, close, toggle)
|
||||
├── parser.lua # Parses /@ @/ tags from buffer content
|
||||
├── gitignore.lua # Manages .gitignore entries for coder files
|
||||
├── autocmds.lua # Autocommands for tag detection, filetype, auto-index
|
||||
├── inject.lua # Code injection strategies
|
||||
├── health.lua # Health check for :checkhealth
|
||||
├── tree.lua # Project tree logging (.coder/tree.log)
|
||||
├── logs_panel.lua # Standalone logs panel UI
|
||||
├── cost.lua # LLM cost tracking with persistent history
|
||||
├── credentials.lua # Secure credential storage (API keys, models)
|
||||
├── init.lua # Main entry, setup function
|
||||
├── config.lua # Configuration management
|
||||
├── types.lua # Lua type definitions
|
||||
├── utils.lua # Utility functions
|
||||
├── commands.lua # Vim command definitions
|
||||
├── window.lua # Split window management
|
||||
├── parser.lua # Parses /@ @/ tags
|
||||
├── gitignore.lua # Manages .gitignore entries
|
||||
├── autocmds.lua # Autocommands for tag detection
|
||||
├── inject.lua # Code injection strategies
|
||||
├── health.lua # Health check for :checkhealth
|
||||
├── tree.lua # Project tree logging
|
||||
├── logs_panel.lua # Standalone logs panel UI
|
||||
├── cost.lua # LLM cost tracking
|
||||
├── credentials.lua # Secure credential storage
|
||||
├── llm/
|
||||
│ ├── init.lua # LLM interface, provider selection
|
||||
│ ├── claude.lua # Claude API client (Anthropic)
|
||||
│ ├── openai.lua # OpenAI API client (with custom endpoint support)
|
||||
│ ├── gemini.lua # Google Gemini API client
|
||||
│ ├── copilot.lua # GitHub Copilot client (uses OAuth from copilot.lua/vim)
|
||||
│ └── ollama.lua # Ollama API client (local LLMs)
|
||||
│ ├── init.lua # LLM interface, provider selection
|
||||
│ ├── claude.lua # Claude API client
|
||||
│ ├── openai.lua # OpenAI API client
|
||||
│ ├── gemini.lua # Google Gemini API client
|
||||
│ ├── copilot.lua # GitHub Copilot client
|
||||
│ └── ollama.lua # Ollama API client (local)
|
||||
├── agent/
|
||||
│ ├── init.lua # Agent system entry point
|
||||
│ ├── ui.lua # Agent panel UI
|
||||
│ ├── logs.lua # Logging system with listeners
|
||||
│ ├── tools.lua # Tool definitions (read_file, edit_file, write_file, bash)
|
||||
│ ├── executor.lua # Tool execution logic
|
||||
│ ├── parser.lua # Parse tool calls from LLM responses
|
||||
│ ├── queue.lua # Event queue with priority heap
|
||||
│ ├── patch.lua # Patch candidates with staleness detection
|
||||
│ ├── confidence.lua # Response confidence scoring heuristics
|
||||
│ ├── worker.lua # Async LLM worker wrapper
|
||||
│ ├── scheduler.lua # Event scheduler with completion-awareness
|
||||
│ ├── scope.lua # Tree-sitter scope resolution
|
||||
│ └── intent.lua # Intent detection from prompts
|
||||
│ ├── init.lua # Agent system entry point
|
||||
│ ├── ui.lua # Agent panel UI
|
||||
│ ├── logs.lua # Logging system
|
||||
│ ├── tools.lua # Tool definitions (read, edit, write, bash)
|
||||
│ ├── executor.lua # Tool execution logic
|
||||
│ ├── parser.lua # Parse tool calls from responses
|
||||
│ ├── queue.lua # Event queue with priority heap
|
||||
│ ├── patch.lua # Patch candidates with staleness detection
|
||||
│ ├── confidence.lua # Response confidence scoring
|
||||
│ ├── worker.lua # Async LLM worker
|
||||
│ ├── scheduler.lua # Event scheduler
|
||||
│ ├── scope.lua # Tree-sitter scope resolution
|
||||
│ ├── intent.lua # Intent detection from prompts
|
||||
│ ├── conflict.lua # Git-style conflict resolution
|
||||
│ ├── linter.lua # LSP diagnostics validation
|
||||
│ └── search_replace.lua # SEARCH/REPLACE block parsing
|
||||
├── ask/
|
||||
│ ├── init.lua # Ask panel entry point
|
||||
│ └── ui.lua # Ask panel UI (chat interface)
|
||||
│ ├── init.lua # Ask panel entry point
|
||||
│ └── ui.lua # Ask panel UI (chat interface)
|
||||
└── prompts/
|
||||
├── init.lua # System prompts for code generation
|
||||
└── agent.lua # Agent-specific prompts and tool instructions
|
||||
├── init.lua # System prompts for code generation
|
||||
└── agent.lua # Agent-specific prompts
|
||||
```
|
||||
|
||||
## .coder/ Folder
|
||||
|
||||
The plugin automatically creates and maintains a `.coder/` folder in your project:
|
||||
|
||||
```
|
||||
.coder/
|
||||
├── tree.log # Project structure, auto-updated on file changes
|
||||
├── cost_history.json # LLM cost tracking history (persistent)
|
||||
├── tree.log # Project structure, auto-updated
|
||||
├── cost_history.json # LLM cost tracking history
|
||||
├── brain/ # Knowledge graph storage
|
||||
│ ├── nodes/ # Learning nodes by type
|
||||
│ ├── indices/ # Search indices
|
||||
│ └── deltas/ # Version history
|
||||
│ ├── nodes/
|
||||
│ ├── indices/
|
||||
│ └── deltas/
|
||||
├── agents/ # Custom agent definitions
|
||||
└── rules/ # Project-specific rules
|
||||
```
|
||||
@@ -95,108 +97,122 @@ llm = {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Agent Mode
|
||||
### 2. Conflict Resolution System
|
||||
|
||||
Git-style diff visualization for code review:
|
||||
|
||||
```
|
||||
<<<<<<< CURRENT
|
||||
// Original code
|
||||
=======
|
||||
// AI-generated code
|
||||
>>>>>>> INCOMING
|
||||
```
|
||||
|
||||
**Keymaps (buffer-local when conflicts exist):**
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `co` | Accept CURRENT (original) code |
|
||||
| `ct` | Accept INCOMING (AI suggestion) |
|
||||
| `cb` | Accept BOTH versions |
|
||||
| `cn` | Delete conflict (accept NONE) |
|
||||
| `cm` | Show conflict resolution menu |
|
||||
| `]x` | Go to next conflict |
|
||||
| `[x` | Go to previous conflict |
|
||||
| `<CR>` | Show menu when on conflict |
|
||||
|
||||
**Menu keymaps:**
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `1` | Accept current |
|
||||
| `2` | Accept incoming |
|
||||
| `3` | Accept both |
|
||||
| `4` | Accept none |
|
||||
| `q`/`<Esc>` | Close menu |
|
||||
|
||||
**Configuration:**
|
||||
```lua
|
||||
-- In conflict.lua
|
||||
config = {
|
||||
lint_after_accept = true, -- Check linter after accepting
|
||||
auto_fix_lint_errors = true, -- Auto-queue fix
|
||||
auto_show_menu = true, -- Show menu after injection
|
||||
auto_show_next_menu = true, -- Show menu for next conflict
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Linter Validation
|
||||
|
||||
Auto-check and fix lint errors after code injection:
|
||||
|
||||
```lua
|
||||
-- In linter.lua
|
||||
config = {
|
||||
auto_save = true, -- Save file after injection
|
||||
diagnostic_delay_ms = 500, -- Wait for LSP
|
||||
min_severity = vim.diagnostic.severity.WARN,
|
||||
auto_offer_fix = true, -- Offer to fix errors
|
||||
}
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
- `:CoderLintCheck` - Check buffer for lint errors
|
||||
- `:CoderLintFix` - Request AI to fix lint errors
|
||||
- `:CoderLintQuickfix` - Show errors in quickfix
|
||||
- `:CoderLintToggleAuto` - Toggle auto lint checking
|
||||
|
||||
### 4. SEARCH/REPLACE Block System
|
||||
|
||||
Reliable code editing with fuzzy matching:
|
||||
|
||||
```
|
||||
<<<<<<< SEARCH
|
||||
function oldCode() {
|
||||
// original
|
||||
}
|
||||
=======
|
||||
function newCode() {
|
||||
// replacement
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```lua
|
||||
-- In search_replace.lua
|
||||
config = {
|
||||
fuzzy_threshold = 0.8, -- Minimum similarity
|
||||
normalize_whitespace = true, -- Ignore whitespace differences
|
||||
context_lines = 3, -- Lines for context matching
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Agent Mode
|
||||
|
||||
Autonomous coding assistant with tool access:
|
||||
|
||||
**Available Tools:**
|
||||
- `read_file` - Read file contents
|
||||
- `edit_file` - Edit files with find/replace
|
||||
- `write_file` - Create or overwrite files
|
||||
- `bash` - Execute shell commands
|
||||
|
||||
### 3. Transform Commands
|
||||
|
||||
Transform `/@ @/` tags inline without split view:
|
||||
|
||||
- `:CoderTransform` - Transform all tags in file
|
||||
- `:CoderTransformCursor` - Transform tag at cursor
|
||||
- `:CoderTransformVisual` - Transform selected tags
|
||||
|
||||
### 4. Auto-Index
|
||||
|
||||
Automatically create coder companion files when opening source files:
|
||||
|
||||
```lua
|
||||
auto_index = true -- disabled by default
|
||||
```
|
||||
|
||||
### 5. Logs Panel
|
||||
|
||||
Real-time visibility into LLM operations with token usage tracking.
|
||||
|
||||
### 6. Cost Tracking
|
||||
|
||||
Track LLM API costs across sessions:
|
||||
|
||||
- **Session tracking**: Monitor current session costs in real-time
|
||||
- **All-time tracking**: Persistent history in `.coder/cost_history.json`
|
||||
- **Per-model breakdown**: See costs by individual model
|
||||
- **50+ models**: Built-in pricing for GPT, Claude, O-series, Gemini
|
||||
|
||||
Cost window keymaps:
|
||||
- `q`/`<Esc>` - Close window
|
||||
- `r` - Refresh display
|
||||
- `c` - Clear session costs
|
||||
- `C` - Clear all history
|
||||
|
||||
### 7. Automatic Ollama Fallback
|
||||
|
||||
When API rate limits are hit (e.g., Copilot free tier), the plugin:
|
||||
1. Detects the rate limit error
|
||||
2. Checks if local Ollama is available
|
||||
3. Automatically switches provider to Ollama
|
||||
4. Notifies user of the provider change
|
||||
|
||||
### 8. Credentials Management
|
||||
|
||||
Store API keys securely outside of config files:
|
||||
|
||||
```vim
|
||||
:CoderAddApiKey
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Interactive prompts for provider, API key, model, endpoint
|
||||
- Stored in `~/.local/share/nvim/codetyper/configuration.json`
|
||||
- Supports all providers: Claude, OpenAI, Gemini, Copilot, Ollama
|
||||
- Switch providers at runtime with `:CoderSwitchProvider`
|
||||
|
||||
**Credential priority:**
|
||||
1. Stored credentials (via `:CoderAddApiKey`)
|
||||
2. Config file settings (`require("codetyper").setup({...})`)
|
||||
3. Environment variables (`OPENAI_API_KEY`, etc.)
|
||||
|
||||
### 9. Event-Driven Scheduler
|
||||
|
||||
Prompts are treated as events, not commands:
|
||||
### 6. Event-Driven Scheduler
|
||||
|
||||
```
|
||||
User types /@...@/ → Event queued → Scheduler dispatches → Worker processes → Patch created → Safe injection
|
||||
User types /@...@/ → Event queued → Scheduler dispatches → Worker processes → Patch created → Conflict shown
|
||||
```
|
||||
|
||||
**Key concepts:**
|
||||
- **PromptEvent**: Captures buffer state at prompt time
|
||||
- **Optimistic Execution**: Ollama as fast scout
|
||||
- **Confidence Scoring**: 5 heuristics
|
||||
- **Staleness Detection**: Discard if buffer changed
|
||||
- **Completion Safety**: Defer while autocomplete visible
|
||||
|
||||
- **PromptEvent**: Captures buffer state (changedtick, content hash) at prompt time
|
||||
- **Optimistic Execution**: Ollama as fast scout, escalate to remote LLMs if confidence low
|
||||
- **Confidence Scoring**: 5 heuristics (length, uncertainty, syntax, repetition, truncation)
|
||||
- **Staleness Detection**: Discard patches if buffer changed during generation
|
||||
- **Completion Safety**: Defer injection while autocomplete popup visible
|
||||
### 7. Tree-sitter Scope Resolution
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```lua
|
||||
scheduler = {
|
||||
enabled = true, -- Enable event-driven mode
|
||||
ollama_scout = true, -- Use Ollama first
|
||||
escalation_threshold = 0.7, -- Below this → escalate
|
||||
max_concurrent = 2, -- Parallel workers
|
||||
completion_delay_ms = 100, -- Wait after popup closes
|
||||
}
|
||||
```
|
||||
|
||||
### 10. Tree-sitter Scope Resolution
|
||||
|
||||
Prompts automatically resolve to their enclosing function/method/class:
|
||||
Prompts automatically resolve to enclosing scope:
|
||||
|
||||
```lua
|
||||
function foo()
|
||||
@@ -206,12 +222,7 @@ end
|
||||
|
||||
**Scope types:** `function`, `method`, `class`, `block`, `file`
|
||||
|
||||
For replacement intents (complete, refactor, fix), the entire scope is extracted
|
||||
and sent to the LLM, then replaced with the transformed version.
|
||||
|
||||
### 11. Intent Detection
|
||||
|
||||
The system parses prompts to detect user intent:
|
||||
### 8. Intent Detection
|
||||
|
||||
| Intent | Keywords | Action |
|
||||
|--------|----------|--------|
|
||||
@@ -221,157 +232,174 @@ The system parses prompts to detect user intent:
|
||||
| add | add, create, insert, new | insert |
|
||||
| document | document, comment, jsdoc | replace |
|
||||
| test | test, spec, unit test | append |
|
||||
| optimize | optimize, performance, faster | replace |
|
||||
| explain | explain, what, how, why | none |
|
||||
| optimize | optimize, performance | replace |
|
||||
| explain | explain, what, how | none |
|
||||
|
||||
### 12. Tag Precedence
|
||||
### 9. Cost Tracking
|
||||
|
||||
Multiple tags in the same scope follow "first tag wins" rule:
|
||||
- Earlier (by line number) unresolved tag processes first
|
||||
- Later tags in same scope are skipped with warning
|
||||
- Different scopes process independently
|
||||
Track LLM API costs:
|
||||
- Session costs tracked in real-time
|
||||
- All-time costs in `.coder/cost_history.json`
|
||||
- Pricing for 50+ models
|
||||
|
||||
## Commands
|
||||
### 10. Credentials Management
|
||||
|
||||
All commands can be invoked via `:Coder {subcommand}` or dedicated aliases.
|
||||
```vim
|
||||
:CoderAddApiKey
|
||||
```
|
||||
|
||||
Stored in `~/.local/share/nvim/codetyper/configuration.json`
|
||||
|
||||
**Priority:** stored credentials > config > environment variables
|
||||
|
||||
## Commands Reference
|
||||
|
||||
### Core Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder open` | `:CoderOpen` | Open coder split view |
|
||||
| `:Coder close` | `:CoderClose` | Close coder split view |
|
||||
| `:Coder toggle` | `:CoderToggle` | Toggle coder split view |
|
||||
| `:Coder process` | `:CoderProcess` | Process last prompt in coder file |
|
||||
| `:Coder status` | - | Show plugin status and configuration |
|
||||
| `:Coder focus` | - | Switch focus between coder/target windows |
|
||||
| `:Coder open` | `:CoderOpen` | Open coder split |
|
||||
| `:Coder close` | `:CoderClose` | Close coder split |
|
||||
| `:Coder toggle` | `:CoderToggle` | Toggle coder split |
|
||||
| `:Coder process` | `:CoderProcess` | Process last prompt |
|
||||
| `:Coder status` | - | Show status |
|
||||
| `:Coder focus` | - | Switch focus |
|
||||
| `:Coder reset` | - | Reset processed prompts |
|
||||
| `:Coder gitignore` | - | Force update .gitignore |
|
||||
|
||||
### Ask Panel (Chat Interface)
|
||||
### Ask Panel
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder ask` | `:CoderAsk` | Open Ask panel |
|
||||
| `:Coder ask-toggle` | `:CoderAskToggle` | Toggle Ask panel |
|
||||
| `:Coder ask-close` | - | Close Ask panel |
|
||||
| `:Coder ask-clear` | `:CoderAskClear` | Clear chat history |
|
||||
| `:Coder ask-clear` | `:CoderAskClear` | Clear chat |
|
||||
|
||||
### Agent Mode (Autonomous Coding)
|
||||
### Agent Mode
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder agent` | `:CoderAgent` | Open Agent panel |
|
||||
| `:Coder agent-toggle` | `:CoderAgentToggle` | Toggle Agent panel |
|
||||
| `:Coder agent-close` | - | Close Agent panel |
|
||||
| `:Coder agent-stop` | `:CoderAgentStop` | Stop running agent |
|
||||
| `:Coder agent-stop` | `:CoderAgentStop` | Stop agent |
|
||||
|
||||
### Agentic Mode (IDE-like Multi-file Agent)
|
||||
### Transform Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder agentic-run <task>` | `:CoderAgenticRun <task>` | Run agentic task |
|
||||
| `:Coder agentic-list` | `:CoderAgenticList` | List available agents |
|
||||
| `:Coder agentic-init` | `:CoderAgenticInit` | Initialize .coder/agents/ and .coder/rules/ |
|
||||
| `:Coder transform` | `:CoderTransform` | Transform all tags |
|
||||
| `:Coder transform-cursor` | `:CoderTransformCursor` | Transform at cursor |
|
||||
| - | `:CoderTransformVisual` | Transform selected |
|
||||
|
||||
### Transform Commands (Inline Tag Processing)
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder transform` | `:CoderTransform` | Transform all /@ @/ tags in file |
|
||||
| `:Coder transform-cursor` | `:CoderTransformCursor` | Transform tag at cursor |
|
||||
| - | `:CoderTransformVisual` | Transform selected tags (visual mode) |
|
||||
|
||||
### Project & Index Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| - | `:CoderIndex` | Open coder companion for current file |
|
||||
| `:Coder index-project` | `:CoderIndexProject` | Index entire project |
|
||||
| `:Coder index-status` | `:CoderIndexStatus` | Show project index status |
|
||||
|
||||
### Tree & Structure Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder tree` | `:CoderTree` | Refresh .coder/tree.log |
|
||||
| `:Coder tree-view` | `:CoderTreeView` | View .coder/tree.log |
|
||||
|
||||
### Queue & Scheduler Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder queue-status` | `:CoderQueueStatus` | Show scheduler/queue status |
|
||||
| `:Coder queue-process` | `:CoderQueueProcess` | Manually trigger queue processing |
|
||||
|
||||
### Processing Mode Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder auto-toggle` | `:CoderAutoToggle` | Toggle automatic/manual processing |
|
||||
| `:Coder auto-set <mode>` | `:CoderAutoSet <mode>` | Set mode (auto/manual) |
|
||||
|
||||
### Memory & Learning Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder memories` | `:CoderMemories` | Show learned memories |
|
||||
| `:Coder forget [pattern]` | `:CoderForget [pattern]` | Clear memories |
|
||||
|
||||
### Brain Commands (Knowledge Graph)
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| - | `:CoderBrain [action]` | Brain management (stats/commit/flush/prune) |
|
||||
| - | `:CoderFeedback <type>` | Give feedback (good/bad/stats) |
|
||||
|
||||
### LLM Statistics & Feedback
|
||||
### Conflict Resolution
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `:Coder llm-stats` | Show LLM provider accuracy stats |
|
||||
| `:Coder llm-feedback-good` | Report positive feedback |
|
||||
| `:Coder llm-feedback-bad` | Report negative feedback |
|
||||
| `:Coder llm-reset-stats` | Reset LLM accuracy stats |
|
||||
| `:CoderConflictToggle` | Toggle conflict mode |
|
||||
| `:CoderConflictMenu` | Show resolution menu |
|
||||
| `:CoderConflictNext` | Go to next conflict |
|
||||
| `:CoderConflictPrev` | Go to previous conflict |
|
||||
| `:CoderConflictStatus` | Show conflict status |
|
||||
| `:CoderConflictResolveAll [keep]` | Resolve all |
|
||||
| `:CoderConflictAcceptCurrent` | Accept original |
|
||||
| `:CoderConflictAcceptIncoming` | Accept AI |
|
||||
| `:CoderConflictAcceptBoth` | Accept both |
|
||||
| `:CoderConflictAcceptNone` | Delete both |
|
||||
| `:CoderConflictAutoMenu` | Toggle auto-show menu |
|
||||
|
||||
### Cost Tracking
|
||||
### Linter Validation
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `:CoderLintCheck` | Check buffer |
|
||||
| `:CoderLintFix` | AI fix errors |
|
||||
| `:CoderLintQuickfix` | Show in quickfix |
|
||||
| `:CoderLintToggleAuto` | Toggle auto lint |
|
||||
|
||||
### Queue & Scheduler
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder cost` | `:CoderCost` | Show LLM cost estimation window |
|
||||
| `:Coder cost-clear` | - | Clear session cost tracking |
|
||||
| `:Coder queue-status` | `:CoderQueueStatus` | Show status |
|
||||
| `:Coder queue-process` | `:CoderQueueProcess` | Trigger processing |
|
||||
|
||||
### Credentials Management
|
||||
### Processing Mode
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder add-api-key` | `:CoderAddApiKey` | Add/update LLM provider credentials |
|
||||
| `:Coder remove-api-key` | `:CoderRemoveApiKey` | Remove provider credentials |
|
||||
| `:Coder credentials` | `:CoderCredentials` | Show credentials status |
|
||||
| `:Coder switch-provider` | `:CoderSwitchProvider` | Switch active provider |
|
||||
| `:Coder auto-toggle` | `:CoderAutoToggle` | Toggle auto/manual |
|
||||
| `:Coder auto-set <mode>` | `:CoderAutoSet` | Set mode |
|
||||
|
||||
### Brain & Memory
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `:CoderMemories` | Show memories |
|
||||
| `:CoderForget [pattern]` | Clear memories |
|
||||
| `:CoderBrain [action]` | Brain management |
|
||||
| `:CoderFeedback <type>` | Give feedback |
|
||||
|
||||
### Cost & Credentials
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `:CoderCost` | Show cost window |
|
||||
| `:CoderAddApiKey` | Add/update API key |
|
||||
| `:CoderRemoveApiKey` | Remove credentials |
|
||||
| `:CoderCredentials` | Show credentials |
|
||||
| `:CoderSwitchProvider` | Switch provider |
|
||||
|
||||
### UI Commands
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `:Coder type-toggle` | `:CoderType` | Show Ask/Agent mode switcher |
|
||||
| `:Coder logs-toggle` | `:CoderLogs` | Toggle logs panel |
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `:CoderLogs` | Toggle logs panel |
|
||||
| `:CoderType` | Show mode switcher |
|
||||
|
||||
## Keymaps Reference
|
||||
|
||||
### Default Keymaps
|
||||
| Key | Mode | Description |
|
||||
|-----|------|-------------|
|
||||
| `<leader>ctt` | Normal | Transform tag at cursor |
|
||||
| `<leader>ctt` | Visual | Transform selected tags |
|
||||
| `<leader>ctT` | Normal | Transform all tags |
|
||||
| `<leader>ca` | Normal | Toggle Agent panel |
|
||||
| `<leader>ci` | Normal | Open coder companion |
|
||||
|
||||
### Ask Panel Keymaps
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `@` | Attach file |
|
||||
| `Ctrl+Enter` | Submit |
|
||||
| `Ctrl+n` | New chat |
|
||||
| `Ctrl+f` | Add current file |
|
||||
| `q` | Close |
|
||||
| `Y` | Copy response |
|
||||
|
||||
### Agent Panel Keymaps
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `<CR>` | Submit |
|
||||
| `Ctrl+c` | Stop agent |
|
||||
| `q` | Close |
|
||||
|
||||
### Logs Panel Keymaps
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `q`/`<Esc>` | Close |
|
||||
|
||||
### Cost Window Keymaps
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `q`/`<Esc>` | Close |
|
||||
| `r` | Refresh |
|
||||
| `c` | Clear session |
|
||||
| `C` | Clear all |
|
||||
|
||||
## Configuration Schema
|
||||
|
||||
```lua
|
||||
{
|
||||
llm = {
|
||||
provider = "claude", -- "claude" | "openai" | "gemini" | "copilot" | "ollama"
|
||||
claude = {
|
||||
api_key = nil, -- string, uses ANTHROPIC_API_KEY env if nil
|
||||
model = "claude-sonnet-4-20250514",
|
||||
},
|
||||
openai = {
|
||||
api_key = nil, -- string, uses OPENAI_API_KEY env if nil
|
||||
model = "gpt-4o",
|
||||
endpoint = nil, -- custom endpoint for Azure, OpenRouter, etc.
|
||||
},
|
||||
gemini = {
|
||||
api_key = nil, -- string, uses GEMINI_API_KEY env if nil
|
||||
model = "gemini-2.0-flash",
|
||||
},
|
||||
copilot = {
|
||||
model = "gpt-4o", -- uses OAuth from copilot.lua/copilot.vim
|
||||
},
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "deepseek-coder:6.7b",
|
||||
},
|
||||
provider = "claude",
|
||||
claude = { api_key = nil, model = "claude-sonnet-4-20250514" },
|
||||
openai = { api_key = nil, model = "gpt-4o", endpoint = nil },
|
||||
gemini = { api_key = nil, model = "gemini-2.0-flash" },
|
||||
copilot = { model = "gpt-4o" },
|
||||
ollama = { host = "http://localhost:11434", model = "deepseek-coder:6.7b" },
|
||||
},
|
||||
window = {
|
||||
width = 25, -- percentage (25 = 25% of screen)
|
||||
position = "left", -- "left" | "right"
|
||||
width = 25,
|
||||
position = "left",
|
||||
border = "rounded",
|
||||
},
|
||||
patterns = {
|
||||
@@ -381,13 +409,14 @@ All commands can be invoked via `:Coder {subcommand}` or dedicated aliases.
|
||||
},
|
||||
auto_gitignore = true,
|
||||
auto_open_ask = true,
|
||||
auto_index = false, -- auto-create coder companion files
|
||||
auto_index = false,
|
||||
scheduler = {
|
||||
enabled = true, -- enable event-driven scheduler
|
||||
ollama_scout = true, -- use Ollama as fast scout
|
||||
enabled = true,
|
||||
ollama_scout = true,
|
||||
escalation_threshold = 0.7,
|
||||
max_concurrent = 2,
|
||||
completion_delay_ms = 100,
|
||||
apply_delay_ms = 5000,
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -396,29 +425,26 @@ All commands can be invoked via `:Coder {subcommand}` or dedicated aliases.
|
||||
|
||||
### Claude API
|
||||
- Endpoint: `https://api.anthropic.com/v1/messages`
|
||||
- Uses `x-api-key` header for authentication
|
||||
- Supports tool use for agent mode
|
||||
- Auth: `x-api-key` header
|
||||
- Supports tool use
|
||||
|
||||
### OpenAI API
|
||||
- Endpoint: `https://api.openai.com/v1/chat/completions` (configurable)
|
||||
- Uses `Authorization: Bearer` header
|
||||
- Supports tool use for agent mode
|
||||
- Compatible with Azure, OpenRouter, and other OpenAI-compatible APIs
|
||||
- Auth: `Authorization: Bearer`
|
||||
- Compatible with Azure, OpenRouter
|
||||
|
||||
### Gemini API
|
||||
- Endpoint: `https://generativelanguage.googleapis.com/v1beta/models`
|
||||
- Uses API key in URL parameter
|
||||
- Supports function calling for agent mode
|
||||
- Auth: API key in URL
|
||||
- Supports function calling
|
||||
|
||||
### Copilot API
|
||||
- Uses GitHub OAuth token from copilot.lua/copilot.vim
|
||||
- Endpoint from token response (typically `api.githubcopilot.com`)
|
||||
- OpenAI-compatible format
|
||||
|
||||
### Ollama API
|
||||
- Endpoint: `{host}/api/generate` or `{host}/api/chat`
|
||||
- No authentication required for local instances
|
||||
- Tool use via prompt-based approach
|
||||
- No auth required locally
|
||||
|
||||
## Agent Tool Definitions
|
||||
|
||||
@@ -431,13 +457,6 @@ tools = {
|
||||
}
|
||||
```
|
||||
|
||||
## Code Injection Strategies
|
||||
|
||||
1. **Refactor**: Replace entire file content
|
||||
2. **Add**: Insert at cursor position in target file
|
||||
3. **Document**: Insert above current function/class
|
||||
4. **Generic**: Prompt user for action
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
| Target File | Coder File |
|
||||
@@ -450,8 +469,8 @@ Pattern: `name.coder.extension`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Required**: Neovim >= 0.8.0, curl
|
||||
- **Optional**: telescope.nvim (enhanced file picker), copilot.lua or copilot.vim (for Copilot provider)
|
||||
- **Required**: Neovim >= 0.8.0, curl, plenary.nvim, nvim-treesitter
|
||||
- **Optional**: telescope.nvim, copilot.lua/copilot.vim, nui.nvim
|
||||
|
||||
## Contact
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Autocommand group name
|
||||
local AUGROUP = "Codetyper"
|
||||
@@ -43,7 +43,7 @@ local function schedule_tree_update()
|
||||
end
|
||||
|
||||
tree_update_timer = vim.defer_fn(function()
|
||||
local tree = require("codetyper.tree")
|
||||
local tree = require("codetyper.support.tree")
|
||||
tree.update_tree_log()
|
||||
tree_update_timer = nil
|
||||
end, TREE_UPDATE_DEBOUNCE_MS)
|
||||
@@ -179,7 +179,7 @@ function M.setup()
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function(ev)
|
||||
local window = require("codetyper.window")
|
||||
local window = require("codetyper.adapters.nvim.windows")
|
||||
if window.is_open() then
|
||||
window.close_split()
|
||||
end
|
||||
@@ -397,11 +397,11 @@ function M.check_for_closed_prompt()
|
||||
if scheduler_enabled then
|
||||
-- Event-driven: emit to queue
|
||||
vim.schedule(function()
|
||||
local queue = require("codetyper.agent.queue")
|
||||
local patch_mod = require("codetyper.agent.patch")
|
||||
local intent_mod = require("codetyper.agent.intent")
|
||||
local scope_mod = require("codetyper.agent.scope")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
local patch_mod = require("codetyper.core.diff.patch")
|
||||
local intent_mod = require("codetyper.core.intent")
|
||||
local scope_mod = require("codetyper.core.scope")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
|
||||
-- Open logs panel to show progress
|
||||
logs_panel.ensure_open()
|
||||
@@ -535,6 +535,163 @@ function M.check_for_closed_prompt()
|
||||
is_processing = false
|
||||
end
|
||||
|
||||
--- Process a single prompt through the scheduler
|
||||
--- This is the core processing logic used by both automatic and manual modes
|
||||
---@param bufnr number Buffer number
|
||||
---@param prompt table Prompt object with start_line, end_line, content
|
||||
---@param current_file string Current file path
|
||||
---@param skip_processed_check? boolean Skip the processed check (for manual mode)
|
||||
function M.process_single_prompt(bufnr, prompt, current_file, skip_processed_check)
|
||||
local parser = require("codetyper.parser")
|
||||
local scheduler = require("codetyper.core.scheduler.scheduler")
|
||||
|
||||
if not prompt.content or prompt.content == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Ensure scheduler is running
|
||||
if not scheduler.status().running then
|
||||
scheduler.start()
|
||||
end
|
||||
|
||||
-- Generate unique key for this prompt
|
||||
local prompt_key = get_prompt_key(bufnr, prompt)
|
||||
|
||||
-- Skip if already processed (unless overridden for manual mode)
|
||||
if not skip_processed_check and processed_prompts[prompt_key] then
|
||||
return
|
||||
end
|
||||
|
||||
-- Mark as processed
|
||||
processed_prompts[prompt_key] = true
|
||||
|
||||
-- Process this prompt
|
||||
vim.schedule(function()
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
local patch_mod = require("codetyper.core.diff.patch")
|
||||
local intent_mod = require("codetyper.core.intent")
|
||||
local scope_mod = require("codetyper.core.scope")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
|
||||
-- Open logs panel to show progress
|
||||
logs_panel.ensure_open()
|
||||
|
||||
-- Take buffer snapshot
|
||||
local snapshot = patch_mod.snapshot_buffer(bufnr, {
|
||||
start_line = prompt.start_line,
|
||||
end_line = prompt.end_line,
|
||||
})
|
||||
|
||||
-- Get target path - for coder files, get the target; for regular files, use self
|
||||
local target_path
|
||||
local is_from_coder_file = utils.is_coder_file(current_file)
|
||||
if is_from_coder_file then
|
||||
target_path = utils.get_target_path(current_file)
|
||||
else
|
||||
target_path = current_file
|
||||
end
|
||||
|
||||
-- Read attached files before cleaning
|
||||
local attached_files = read_attached_files(prompt.content, current_file)
|
||||
|
||||
-- Clean prompt content (strip file references)
|
||||
local cleaned = parser.clean_prompt(parser.strip_file_references(prompt.content))
|
||||
|
||||
-- Resolve scope in target file FIRST (need it to adjust intent)
|
||||
-- Only resolve scope if NOT from coder file (line numbers don't apply)
|
||||
local target_bufnr = vim.fn.bufnr(target_path)
|
||||
local scope = nil
|
||||
local scope_text = nil
|
||||
local scope_range = nil
|
||||
|
||||
if not is_from_coder_file then
|
||||
-- Prompt is in the actual source file, use line position for scope
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = bufnr
|
||||
end
|
||||
scope = scope_mod.resolve_scope(target_bufnr, prompt.start_line, 1)
|
||||
if scope and scope.type ~= "file" then
|
||||
scope_text = scope.text
|
||||
scope_range = {
|
||||
start_line = scope.range.start_row,
|
||||
end_line = scope.range.end_row,
|
||||
}
|
||||
end
|
||||
else
|
||||
-- Prompt is in coder file - load target if needed
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = vim.fn.bufadd(target_path)
|
||||
if target_bufnr ~= 0 then
|
||||
vim.fn.bufload(target_bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Detect intent from prompt
|
||||
local intent = intent_mod.detect(cleaned)
|
||||
|
||||
-- IMPORTANT: If prompt is inside a function/method and intent is "add",
|
||||
-- override to "complete" since we're completing the function body
|
||||
-- But NOT for coder files - they should use "add/append" by default
|
||||
if not is_from_coder_file and scope and (scope.type == "function" or scope.type == "method") then
|
||||
if intent.type == "add" or intent.action == "insert" or intent.action == "append" then
|
||||
-- Override to complete the function instead of adding new code
|
||||
intent = {
|
||||
type = "complete",
|
||||
scope_hint = "function",
|
||||
confidence = intent.confidence,
|
||||
action = "replace",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- For coder files, default to "add" with "append" action
|
||||
if is_from_coder_file and (intent.action == "replace" or intent.type == "complete") then
|
||||
intent = {
|
||||
type = intent.type == "complete" and "add" or intent.type,
|
||||
confidence = intent.confidence,
|
||||
action = "append",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
|
||||
-- Determine priority based on intent
|
||||
local priority = 2
|
||||
if intent.type == "fix" or intent.type == "complete" then
|
||||
priority = 1
|
||||
elseif intent.type == "test" or intent.type == "document" then
|
||||
priority = 3
|
||||
end
|
||||
|
||||
-- Enqueue the event
|
||||
queue.enqueue({
|
||||
id = queue.generate_id(),
|
||||
bufnr = bufnr,
|
||||
range = { start_line = prompt.start_line, end_line = prompt.end_line },
|
||||
timestamp = os.clock(),
|
||||
changedtick = snapshot.changedtick,
|
||||
content_hash = snapshot.content_hash,
|
||||
prompt_content = cleaned,
|
||||
target_path = target_path,
|
||||
priority = priority,
|
||||
status = "pending",
|
||||
attempt_count = 0,
|
||||
intent = intent,
|
||||
scope = scope,
|
||||
scope_text = scope_text,
|
||||
scope_range = scope_range,
|
||||
attached_files = attached_files,
|
||||
})
|
||||
|
||||
local scope_info = scope
|
||||
and scope.type ~= "file"
|
||||
and string.format(" [%s: %s]", scope.type, scope.name or "anonymous")
|
||||
or ""
|
||||
utils.notify(string.format("Prompt queued: %s%s", intent.type, scope_info), vim.log.levels.INFO)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Check and process all closed prompts in the buffer (works on ANY file)
|
||||
function M.check_all_prompts()
|
||||
local parser = require("codetyper.parser")
|
||||
@@ -563,153 +720,14 @@ function M.check_all_prompts()
|
||||
end
|
||||
|
||||
for _, prompt in ipairs(prompts) do
|
||||
if prompt.content and prompt.content ~= "" then
|
||||
-- Generate unique key for this prompt
|
||||
local prompt_key = get_prompt_key(bufnr, prompt)
|
||||
|
||||
-- Skip if already processed
|
||||
if processed_prompts[prompt_key] then
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Mark as processed
|
||||
processed_prompts[prompt_key] = true
|
||||
|
||||
-- Process this prompt
|
||||
vim.schedule(function()
|
||||
local queue = require("codetyper.agent.queue")
|
||||
local patch_mod = require("codetyper.agent.patch")
|
||||
local intent_mod = require("codetyper.agent.intent")
|
||||
local scope_mod = require("codetyper.agent.scope")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
|
||||
-- Open logs panel to show progress
|
||||
logs_panel.ensure_open()
|
||||
|
||||
-- Take buffer snapshot
|
||||
local snapshot = patch_mod.snapshot_buffer(bufnr, {
|
||||
start_line = prompt.start_line,
|
||||
end_line = prompt.end_line,
|
||||
})
|
||||
|
||||
-- Get target path - for coder files, get the target; for regular files, use self
|
||||
local target_path
|
||||
local is_from_coder_file = utils.is_coder_file(current_file)
|
||||
if is_from_coder_file then
|
||||
target_path = utils.get_target_path(current_file)
|
||||
else
|
||||
target_path = current_file
|
||||
end
|
||||
|
||||
-- Read attached files before cleaning
|
||||
local attached_files = read_attached_files(prompt.content, current_file)
|
||||
|
||||
-- Clean prompt content (strip file references)
|
||||
local cleaned = parser.clean_prompt(parser.strip_file_references(prompt.content))
|
||||
|
||||
-- Resolve scope in target file FIRST (need it to adjust intent)
|
||||
-- Only resolve scope if NOT from coder file (line numbers don't apply)
|
||||
local target_bufnr = vim.fn.bufnr(target_path)
|
||||
local scope = nil
|
||||
local scope_text = nil
|
||||
local scope_range = nil
|
||||
|
||||
if not is_from_coder_file then
|
||||
-- Prompt is in the actual source file, use line position for scope
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = bufnr
|
||||
end
|
||||
scope = scope_mod.resolve_scope(target_bufnr, prompt.start_line, 1)
|
||||
if scope and scope.type ~= "file" then
|
||||
scope_text = scope.text
|
||||
scope_range = {
|
||||
start_line = scope.range.start_row,
|
||||
end_line = scope.range.end_row,
|
||||
}
|
||||
end
|
||||
else
|
||||
-- Prompt is in coder file - load target if needed
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = vim.fn.bufadd(target_path)
|
||||
if target_bufnr ~= 0 then
|
||||
vim.fn.bufload(target_bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Detect intent from prompt
|
||||
local intent = intent_mod.detect(cleaned)
|
||||
|
||||
-- IMPORTANT: If prompt is inside a function/method and intent is "add",
|
||||
-- override to "complete" since we're completing the function body
|
||||
-- But NOT for coder files - they should use "add/append" by default
|
||||
if not is_from_coder_file and scope and (scope.type == "function" or scope.type == "method") then
|
||||
if intent.type == "add" or intent.action == "insert" or intent.action == "append" then
|
||||
-- Override to complete the function instead of adding new code
|
||||
intent = {
|
||||
type = "complete",
|
||||
scope_hint = "function",
|
||||
confidence = intent.confidence,
|
||||
action = "replace",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- For coder files, default to "add" with "append" action
|
||||
if is_from_coder_file and (intent.action == "replace" or intent.type == "complete") then
|
||||
intent = {
|
||||
type = intent.type == "complete" and "add" or intent.type,
|
||||
confidence = intent.confidence,
|
||||
action = "append",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
|
||||
-- Determine priority based on intent
|
||||
local priority = 2
|
||||
if intent.type == "fix" or intent.type == "complete" then
|
||||
priority = 1
|
||||
elseif intent.type == "test" or intent.type == "document" then
|
||||
priority = 3
|
||||
end
|
||||
|
||||
-- Enqueue the event
|
||||
queue.enqueue({
|
||||
id = queue.generate_id(),
|
||||
bufnr = bufnr,
|
||||
range = { start_line = prompt.start_line, end_line = prompt.end_line },
|
||||
timestamp = os.clock(),
|
||||
changedtick = snapshot.changedtick,
|
||||
content_hash = snapshot.content_hash,
|
||||
prompt_content = cleaned,
|
||||
target_path = target_path,
|
||||
priority = priority,
|
||||
status = "pending",
|
||||
attempt_count = 0,
|
||||
intent = intent,
|
||||
scope = scope,
|
||||
scope_text = scope_text,
|
||||
scope_range = scope_range,
|
||||
attached_files = attached_files,
|
||||
})
|
||||
|
||||
local scope_info = scope
|
||||
and scope.type ~= "file"
|
||||
and string.format(" [%s: %s]", scope.type, scope.name or "anonymous")
|
||||
or ""
|
||||
utils.notify(string.format("Prompt queued: %s%s", intent.type, scope_info), vim.log.levels.INFO)
|
||||
end)
|
||||
|
||||
::continue::
|
||||
end
|
||||
M.process_single_prompt(bufnr, prompt, current_file)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check for closed prompt with preference check
|
||||
--- If user hasn't chosen auto/manual mode, ask them first
|
||||
function M.check_for_closed_prompt_with_preference()
|
||||
local preferences = require("codetyper.preferences")
|
||||
local preferences = require("codetyper.config.preferences")
|
||||
local parser = require("codetyper.parser")
|
||||
|
||||
-- First check if there are any prompts to process
|
||||
@@ -749,7 +767,7 @@ end
|
||||
|
||||
--- Check all prompts with preference check
|
||||
function M.check_all_prompts_with_preference()
|
||||
local preferences = require("codetyper.preferences")
|
||||
local preferences = require("codetyper.config.preferences")
|
||||
local parser = require("codetyper.parser")
|
||||
|
||||
-- First check if there are any prompts to process
|
||||
@@ -803,14 +821,17 @@ end
|
||||
|
||||
--- Reset processed prompts for a buffer (useful for re-processing)
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
function M.reset_processed(bufnr)
|
||||
---@param silent? boolean Suppress notification (default: false)
|
||||
function M.reset_processed(bufnr, silent)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
for key, _ in pairs(processed_prompts) do
|
||||
if key:match("^" .. bufnr .. ":") then
|
||||
processed_prompts[key] = nil
|
||||
end
|
||||
end
|
||||
utils.notify("Prompt history cleared - prompts can be re-processed")
|
||||
if not silent then
|
||||
utils.notify("Prompt history cleared - prompts can be re-processed")
|
||||
end
|
||||
end
|
||||
|
||||
--- Track if we already opened the split for this buffer
|
||||
@@ -819,7 +840,7 @@ local auto_opened_buffers = {}
|
||||
|
||||
--- Auto-open target file when a coder file is opened directly
|
||||
function M.auto_open_target_file()
|
||||
local window = require("codetyper.window")
|
||||
local window = require("codetyper.adapters.nvim.windows")
|
||||
|
||||
-- Skip if split is already open
|
||||
if window.is_open() then
|
||||
@@ -1487,7 +1508,7 @@ function M.open_coder_companion(open_split)
|
||||
|
||||
if open_split then
|
||||
-- Use the window module to open split view
|
||||
local window = require("codetyper.window")
|
||||
local window = require("codetyper.adapters.nvim.windows")
|
||||
window.open_split(coder_path, filepath)
|
||||
else
|
||||
-- Just open the coder file
|
||||
@@ -172,6 +172,36 @@ local function get_buffer_completions(prefix, bufnr)
|
||||
return items
|
||||
end
|
||||
|
||||
--- Try to get Copilot suggestion if plugin is installed
|
||||
---@param prefix string
|
||||
---@return string|nil suggestion
|
||||
local function get_copilot_suggestion(prefix)
|
||||
-- Try copilot.lua suggestion API first
|
||||
local ok, copilot_suggestion = pcall(require, "copilot.suggestion")
|
||||
if ok and copilot_suggestion and type(copilot_suggestion.get_suggestion) == "function" then
|
||||
local ok2, suggestion = pcall(copilot_suggestion.get_suggestion)
|
||||
if ok2 and suggestion and suggestion ~= "" then
|
||||
-- Only return if suggestion seems to start with prefix (best-effort)
|
||||
if prefix == "" or suggestion:lower():match(prefix:lower(), 1) then
|
||||
return suggestion
|
||||
else
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: try older copilot module if present
|
||||
local ok3, copilot = pcall(require, "copilot")
|
||||
if ok3 and copilot and type(copilot.get_suggestion) == "function" then
|
||||
local ok4, suggestion = pcall(copilot.get_suggestion)
|
||||
if ok4 and suggestion and suggestion ~= "" then
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Create new cmp source instance
|
||||
function source.new()
|
||||
return setmetatable({}, { __index = source })
|
||||
@@ -251,6 +281,32 @@ function source:complete(params, callback)
|
||||
end
|
||||
end
|
||||
|
||||
-- If Copilot is installed, prefer its suggestion as a top-priority completion
|
||||
local ok_cp, _ = pcall(require, "copilot")
|
||||
if ok_cp then
|
||||
local suggestion = nil
|
||||
local ok_sug, res = pcall(get_copilot_suggestion, prefix)
|
||||
if ok_sug then
|
||||
suggestion = res
|
||||
end
|
||||
if suggestion and suggestion ~= "" then
|
||||
-- Truncate suggestion to first line for label display
|
||||
local first_line = suggestion:match("([^
|
||||
]+)") or suggestion
|
||||
-- Avoid duplicates
|
||||
if not seen[first_line] then
|
||||
seen[first_line] = true
|
||||
table.insert(items, 1, {
|
||||
label = first_line,
|
||||
kind = 1,
|
||||
detail = "[copilot]",
|
||||
documentation = suggestion,
|
||||
sortText = "0" .. first_line,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
callback({
|
||||
items = items,
|
||||
isIncomplete = #items >= 50,
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local window = require("codetyper.window")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local window = require("codetyper.adapters.nvim.windows")
|
||||
|
||||
--- Open coder view for current file or select one
|
||||
---@param opts? table Command options
|
||||
@@ -108,7 +108,7 @@ end
|
||||
--- Process prompt at cursor and generate code
|
||||
local function cmd_process()
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
local llm = require("codetyper.core.llm")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
@@ -153,7 +153,7 @@ end
|
||||
local function cmd_status()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local tree = require("codetyper.tree")
|
||||
local tree = require("codetyper.support.tree")
|
||||
|
||||
local stats = tree.get_stats()
|
||||
|
||||
@@ -195,7 +195,7 @@ end
|
||||
|
||||
--- Refresh tree.log manually
|
||||
local function cmd_tree()
|
||||
local tree = require("codetyper.tree")
|
||||
local tree = require("codetyper.support.tree")
|
||||
if tree.update_tree_log() then
|
||||
utils.notify("Tree log updated: " .. tree.get_tree_log_path())
|
||||
else
|
||||
@@ -205,7 +205,7 @@ end
|
||||
|
||||
--- Open tree.log file
|
||||
local function cmd_tree_view()
|
||||
local tree = require("codetyper.tree")
|
||||
local tree = require("codetyper.support.tree")
|
||||
local tree_log_path = tree.get_tree_log_path()
|
||||
|
||||
if not tree_log_path then
|
||||
@@ -224,61 +224,63 @@ end
|
||||
|
||||
--- Reset processed prompts to allow re-processing
|
||||
local function cmd_reset()
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
autocmds.reset_processed()
|
||||
end
|
||||
|
||||
--- Force update gitignore
|
||||
local function cmd_gitignore()
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
local gitignore = require("codetyper.support.gitignore")
|
||||
gitignore.force_update()
|
||||
end
|
||||
|
||||
--- Open ask panel
|
||||
local function cmd_ask()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.open()
|
||||
--- Open ask panel (with optional visual selection)
|
||||
---@param selection table|nil Visual selection info
|
||||
local function cmd_ask(selection)
|
||||
local ask = require("codetyper.features.ask.engine")
|
||||
ask.open(selection)
|
||||
end
|
||||
|
||||
--- Close ask panel
|
||||
local function cmd_ask_close()
|
||||
local ask = require("codetyper.ask")
|
||||
local ask = require("codetyper.features.ask.engine")
|
||||
ask.close()
|
||||
end
|
||||
|
||||
--- Toggle ask panel
|
||||
local function cmd_ask_toggle()
|
||||
local ask = require("codetyper.ask")
|
||||
local ask = require("codetyper.features.ask.engine")
|
||||
ask.toggle()
|
||||
end
|
||||
|
||||
--- Clear ask history
|
||||
local function cmd_ask_clear()
|
||||
local ask = require("codetyper.ask")
|
||||
local ask = require("codetyper.features.ask.engine")
|
||||
ask.clear_history()
|
||||
end
|
||||
|
||||
--- Open agent panel
|
||||
local function cmd_agent()
|
||||
local agent_ui = require("codetyper.agent.ui")
|
||||
agent_ui.open()
|
||||
--- Open agent panel (with optional visual selection)
|
||||
---@param selection table|nil Visual selection info
|
||||
local function cmd_agent(selection)
|
||||
local agent_ui = require("codetyper.adapters.nvim.ui.chat")
|
||||
agent_ui.open(selection)
|
||||
end
|
||||
|
||||
--- Close agent panel
|
||||
local function cmd_agent_close()
|
||||
local agent_ui = require("codetyper.agent.ui")
|
||||
local agent_ui = require("codetyper.adapters.nvim.ui.chat")
|
||||
agent_ui.close()
|
||||
end
|
||||
|
||||
--- Toggle agent panel
|
||||
local function cmd_agent_toggle()
|
||||
local agent_ui = require("codetyper.agent.ui")
|
||||
local agent_ui = require("codetyper.adapters.nvim.ui.chat")
|
||||
agent_ui.toggle()
|
||||
end
|
||||
|
||||
--- Stop running agent
|
||||
local function cmd_agent_stop()
|
||||
local agent = require("codetyper.agent")
|
||||
local agent = require("codetyper.features.agents")
|
||||
if agent.is_running() then
|
||||
agent.stop()
|
||||
utils.notify("Agent stopped")
|
||||
@@ -291,9 +293,9 @@ end
|
||||
---@param task string The task to accomplish
|
||||
---@param agent_name? string Optional agent name
|
||||
local function cmd_agentic_run(task, agent_name)
|
||||
local agentic = require("codetyper.agent.agentic")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local agentic = require("codetyper.features.agents.engine")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
-- Open logs panel
|
||||
logs_panel.open()
|
||||
@@ -353,7 +355,7 @@ end
|
||||
|
||||
--- List available agents
|
||||
local function cmd_agentic_list()
|
||||
local agentic = require("codetyper.agent.agentic")
|
||||
local agentic = require("codetyper.features.agents.engine")
|
||||
local agents = agentic.list_agents()
|
||||
|
||||
local lines = {
|
||||
@@ -377,7 +379,7 @@ end
|
||||
|
||||
--- Initialize .coder/agents/ and .coder/rules/ directories
|
||||
local function cmd_agentic_init()
|
||||
local agentic = require("codetyper.agent.agentic")
|
||||
local agentic = require("codetyper.features.agents.engine")
|
||||
agentic.init()
|
||||
|
||||
local agents_dir = vim.fn.getcwd() .. "/.coder/agents"
|
||||
@@ -407,14 +409,14 @@ end
|
||||
|
||||
--- Toggle logs panel
|
||||
local function cmd_logs_toggle()
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
logs_panel.toggle()
|
||||
end
|
||||
|
||||
--- Show scheduler status and queue info
|
||||
local function cmd_queue_status()
|
||||
local scheduler = require("codetyper.agent.scheduler")
|
||||
local queue = require("codetyper.agent.queue")
|
||||
local scheduler = require("codetyper.core.scheduler.scheduler")
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
local parser = require("codetyper.parser")
|
||||
|
||||
local status = scheduler.status()
|
||||
@@ -453,8 +455,8 @@ end
|
||||
|
||||
--- Manually trigger queue processing for current buffer
|
||||
local function cmd_queue_process()
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
|
||||
-- Open logs panel to show progress
|
||||
logs_panel.open()
|
||||
@@ -482,11 +484,12 @@ end
|
||||
|
||||
--- Transform inline /@ @/ tags in current file
|
||||
--- Works on ANY file, not just .coder.* files
|
||||
--- Uses the same processing logic as automatic mode for consistent results
|
||||
local function cmd_transform()
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
@@ -506,115 +509,27 @@ local function cmd_transform()
|
||||
|
||||
-- Open the logs panel to show generation progress
|
||||
logs_panel.open()
|
||||
logs.info("Transform started: " .. #prompts .. " prompt(s)")
|
||||
logs.info("Transform started: " .. #prompts .. " prompt(s) in " .. vim.fn.fnamemodify(filepath, ":t"))
|
||||
|
||||
utils.notify("Found " .. #prompts .. " prompt(s) to transform...", vim.log.levels.INFO)
|
||||
|
||||
-- Build context for this file
|
||||
local ext = vim.fn.fnamemodify(filepath, ":e")
|
||||
local context = llm.build_context(filepath, "code_generation")
|
||||
-- Reset processed prompts tracking so we can re-process them (silent mode)
|
||||
autocmds.reset_processed(bufnr, true)
|
||||
|
||||
-- Process prompts in reverse order (bottom to top) to maintain line numbers
|
||||
local sorted_prompts = {}
|
||||
for i = #prompts, 1, -1 do
|
||||
table.insert(sorted_prompts, prompts[i])
|
||||
end
|
||||
|
||||
-- Track how many are being processed
|
||||
local pending = #sorted_prompts
|
||||
local completed = 0
|
||||
local errors = 0
|
||||
|
||||
-- Process each prompt
|
||||
for _, prompt in ipairs(sorted_prompts) do
|
||||
local clean_prompt = parser.clean_prompt(prompt.content)
|
||||
local prompt_type = parser.detect_prompt_type(prompt.content)
|
||||
|
||||
-- Build enhanced user prompt
|
||||
local enhanced_prompt = "TASK: " .. clean_prompt .. "\n\n"
|
||||
enhanced_prompt = enhanced_prompt .. "REQUIREMENTS:\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Generate ONLY " .. (context.language or "code") .. " code\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- NO markdown code blocks (no ```)\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- NO explanations or comments about what you did\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Match the coding style of the existing file exactly\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Output must be ready to insert directly into the file\n"
|
||||
|
||||
logs.info("Processing: " .. clean_prompt:sub(1, 40) .. "...")
|
||||
utils.notify("Processing: " .. clean_prompt:sub(1, 40) .. "...", vim.log.levels.INFO)
|
||||
|
||||
-- Generate code for this prompt
|
||||
llm.generate(enhanced_prompt, context, function(response, err)
|
||||
if err then
|
||||
logs.error("Failed: " .. err)
|
||||
utils.notify("Failed: " .. err, vim.log.levels.ERROR)
|
||||
errors = errors + 1
|
||||
elseif response then
|
||||
-- Replace the prompt tag with generated code
|
||||
vim.schedule(function()
|
||||
-- Get current buffer lines
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
|
||||
-- Calculate the exact range to replace
|
||||
local start_line = prompt.start_line
|
||||
local end_line = prompt.end_line
|
||||
|
||||
-- Find the full lines containing the tags
|
||||
local start_line_content = lines[start_line] or ""
|
||||
local end_line_content = lines[end_line] or ""
|
||||
|
||||
-- Check if there's content before the opening tag on the same line
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local before_tag = ""
|
||||
local after_tag = ""
|
||||
|
||||
local open_pos = start_line_content:find(utils.escape_pattern(config.patterns.open_tag))
|
||||
if open_pos and open_pos > 1 then
|
||||
before_tag = start_line_content:sub(1, open_pos - 1)
|
||||
end
|
||||
|
||||
local close_pos = end_line_content:find(utils.escape_pattern(config.patterns.close_tag))
|
||||
if close_pos then
|
||||
local after_close = close_pos + #config.patterns.close_tag
|
||||
if after_close <= #end_line_content then
|
||||
after_tag = end_line_content:sub(after_close)
|
||||
end
|
||||
end
|
||||
|
||||
-- Build the replacement lines
|
||||
local replacement_lines = vim.split(response, "\n", { plain = true })
|
||||
|
||||
-- Add before/after content if any
|
||||
if before_tag ~= "" and #replacement_lines > 0 then
|
||||
replacement_lines[1] = before_tag .. replacement_lines[1]
|
||||
end
|
||||
if after_tag ~= "" and #replacement_lines > 0 then
|
||||
replacement_lines[#replacement_lines] = replacement_lines[#replacement_lines] .. after_tag
|
||||
end
|
||||
|
||||
-- Replace the lines in buffer
|
||||
vim.api.nvim_buf_set_lines(bufnr, start_line - 1, end_line, false, replacement_lines)
|
||||
|
||||
completed = completed + 1
|
||||
if completed + errors >= pending then
|
||||
local msg = "Transform complete: " .. completed .. " succeeded, " .. errors .. " failed"
|
||||
logs.info(msg)
|
||||
utils.notify(msg, errors > 0 and vim.log.levels.WARN or vim.log.levels.INFO)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
-- Use the same processing logic as automatic mode
|
||||
-- This ensures intent detection, scope resolution, and all other logic is identical
|
||||
autocmds.check_all_prompts()
|
||||
end
|
||||
|
||||
--- Transform prompts within a line range (for visual selection)
|
||||
--- Uses the same processing logic as automatic mode for consistent results
|
||||
---@param start_line number Start line (1-indexed)
|
||||
---@param end_line number End line (1-indexed)
|
||||
local function cmd_transform_range(start_line, end_line)
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
@@ -646,85 +561,11 @@ local function cmd_transform_range(start_line, end_line)
|
||||
|
||||
utils.notify("Found " .. #prompts .. " prompt(s) in selection to transform...", vim.log.levels.INFO)
|
||||
|
||||
-- Build context for this file
|
||||
local context = llm.build_context(filepath, "code_generation")
|
||||
|
||||
-- Process prompts in reverse order (bottom to top) to maintain line numbers
|
||||
local sorted_prompts = {}
|
||||
for i = #prompts, 1, -1 do
|
||||
table.insert(sorted_prompts, prompts[i])
|
||||
end
|
||||
|
||||
local pending = #sorted_prompts
|
||||
local completed = 0
|
||||
local errors = 0
|
||||
|
||||
for _, prompt in ipairs(sorted_prompts) do
|
||||
-- Process each prompt using the same logic as automatic mode (skip processed check for manual mode)
|
||||
for _, prompt in ipairs(prompts) do
|
||||
local clean_prompt = parser.clean_prompt(prompt.content)
|
||||
|
||||
local enhanced_prompt = "TASK: " .. clean_prompt .. "\n\n"
|
||||
enhanced_prompt = enhanced_prompt .. "REQUIREMENTS:\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Generate ONLY " .. (context.language or "code") .. " code\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- NO markdown code blocks (no ```)\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- NO explanations or comments about what you did\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Match the coding style of the existing file exactly\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Output must be ready to insert directly into the file\n"
|
||||
|
||||
logs.info("Processing: " .. clean_prompt:sub(1, 40) .. "...")
|
||||
utils.notify("Processing: " .. clean_prompt:sub(1, 40) .. "...", vim.log.levels.INFO)
|
||||
|
||||
llm.generate(enhanced_prompt, context, function(response, err)
|
||||
if err then
|
||||
logs.error("Failed: " .. err)
|
||||
utils.notify("Failed: " .. err, vim.log.levels.ERROR)
|
||||
errors = errors + 1
|
||||
elseif response then
|
||||
vim.schedule(function()
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local p_start_line = prompt.start_line
|
||||
local p_end_line = prompt.end_line
|
||||
|
||||
local start_line_content = lines[p_start_line] or ""
|
||||
local end_line_content = lines[p_end_line] or ""
|
||||
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local before_tag = ""
|
||||
local after_tag = ""
|
||||
|
||||
local open_pos = start_line_content:find(utils.escape_pattern(config.patterns.open_tag))
|
||||
if open_pos and open_pos > 1 then
|
||||
before_tag = start_line_content:sub(1, open_pos - 1)
|
||||
end
|
||||
|
||||
local close_pos = end_line_content:find(utils.escape_pattern(config.patterns.close_tag))
|
||||
if close_pos then
|
||||
local after_close = close_pos + #config.patterns.close_tag
|
||||
if after_close <= #end_line_content then
|
||||
after_tag = end_line_content:sub(after_close)
|
||||
end
|
||||
end
|
||||
|
||||
local replacement_lines = vim.split(response, "\n", { plain = true })
|
||||
|
||||
if before_tag ~= "" and #replacement_lines > 0 then
|
||||
replacement_lines[1] = before_tag .. replacement_lines[1]
|
||||
end
|
||||
if after_tag ~= "" and #replacement_lines > 0 then
|
||||
replacement_lines[#replacement_lines] = replacement_lines[#replacement_lines] .. after_tag
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, p_start_line - 1, p_end_line, false, replacement_lines)
|
||||
|
||||
completed = completed + 1
|
||||
if completed + errors >= pending then
|
||||
local msg = "Transform complete: " .. completed .. " succeeded, " .. errors .. " failed"
|
||||
logs.info(msg)
|
||||
utils.notify(msg, errors > 0 and vim.log.levels.WARN or vim.log.levels.INFO)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
autocmds.process_single_prompt(bufnr, prompt, filepath, true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -738,7 +579,7 @@ end
|
||||
|
||||
--- Index the entire project
|
||||
local function cmd_index_project()
|
||||
local indexer = require("codetyper.indexer")
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
|
||||
utils.notify("Indexing project...", vim.log.levels.INFO)
|
||||
|
||||
@@ -760,8 +601,8 @@ end
|
||||
|
||||
--- Show index status
|
||||
local function cmd_index_status()
|
||||
local indexer = require("codetyper.indexer")
|
||||
local memory = require("codetyper.indexer.memory")
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
local status = indexer.get_status()
|
||||
local mem_stats = memory.get_stats()
|
||||
@@ -798,7 +639,7 @@ end
|
||||
|
||||
--- Show learned memories
|
||||
local function cmd_memories()
|
||||
local memory = require("codetyper.indexer.memory")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
local all = memory.get_all()
|
||||
local lines = {
|
||||
@@ -843,7 +684,7 @@ end
|
||||
--- Clear memories
|
||||
---@param pattern string|nil Optional pattern to match
|
||||
local function cmd_forget(pattern)
|
||||
local memory = require("codetyper.indexer.memory")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
if not pattern or pattern == "" then
|
||||
-- Confirm before clearing all
|
||||
@@ -862,11 +703,12 @@ local function cmd_forget(pattern)
|
||||
end
|
||||
|
||||
--- Transform a single prompt at cursor position
|
||||
--- Uses the same processing logic as automatic mode for consistent results
|
||||
local function cmd_transform_at_cursor()
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
@@ -888,77 +730,18 @@ local function cmd_transform_at_cursor()
|
||||
logs_panel.open()
|
||||
|
||||
local clean_prompt = parser.clean_prompt(prompt.content)
|
||||
local context = llm.build_context(filepath, "code_generation")
|
||||
|
||||
logs.info("Transform cursor: " .. clean_prompt:sub(1, 40) .. "...")
|
||||
|
||||
-- Build enhanced user prompt
|
||||
local enhanced_prompt = "TASK: " .. clean_prompt .. "\n\n"
|
||||
enhanced_prompt = enhanced_prompt .. "REQUIREMENTS:\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Generate ONLY " .. (context.language or "code") .. " code\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- NO markdown code blocks (no ```)\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- NO explanations or comments about what you did\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Match the coding style of the existing file exactly\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Output must be ready to insert directly into the file\n"
|
||||
|
||||
utils.notify("Transforming: " .. clean_prompt:sub(1, 40) .. "...", vim.log.levels.INFO)
|
||||
|
||||
llm.generate(enhanced_prompt, context, function(response, err)
|
||||
if err then
|
||||
logs.error("Transform failed: " .. err)
|
||||
utils.notify("Transform failed: " .. err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if response then
|
||||
vim.schedule(function()
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local start_line = prompt.start_line
|
||||
local end_line = prompt.end_line
|
||||
|
||||
local start_line_content = lines[start_line] or ""
|
||||
local end_line_content = lines[end_line] or ""
|
||||
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local before_tag = ""
|
||||
local after_tag = ""
|
||||
|
||||
local open_pos = start_line_content:find(utils.escape_pattern(config.patterns.open_tag))
|
||||
if open_pos and open_pos > 1 then
|
||||
before_tag = start_line_content:sub(1, open_pos - 1)
|
||||
end
|
||||
|
||||
local close_pos = end_line_content:find(utils.escape_pattern(config.patterns.close_tag))
|
||||
if close_pos then
|
||||
local after_close = close_pos + #config.patterns.close_tag
|
||||
if after_close <= #end_line_content then
|
||||
after_tag = end_line_content:sub(after_close)
|
||||
end
|
||||
end
|
||||
|
||||
local replacement_lines = vim.split(response, "\n", { plain = true })
|
||||
|
||||
if before_tag ~= "" and #replacement_lines > 0 then
|
||||
replacement_lines[1] = before_tag .. replacement_lines[1]
|
||||
end
|
||||
if after_tag ~= "" and #replacement_lines > 0 then
|
||||
replacement_lines[#replacement_lines] = replacement_lines[#replacement_lines] .. after_tag
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, start_line - 1, end_line, false, replacement_lines)
|
||||
logs.info("Transform complete!")
|
||||
utils.notify("Transform complete!", vim.log.levels.INFO)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
-- Use the same processing logic as automatic mode (skip processed check for manual mode)
|
||||
autocmds.process_single_prompt(bufnr, prompt, filepath, true)
|
||||
end
|
||||
|
||||
--- Main command dispatcher
|
||||
---@param args table Command arguments
|
||||
--- Show LLM accuracy statistics
|
||||
local function cmd_llm_stats()
|
||||
local llm = require("codetyper.llm")
|
||||
local llm = require("codetyper.core.llm")
|
||||
local stats = llm.get_accuracy_stats()
|
||||
|
||||
local lines = {
|
||||
@@ -986,13 +769,13 @@ end
|
||||
--- Report feedback on last LLM response
|
||||
---@param was_good boolean Whether the response was good
|
||||
local function cmd_llm_feedback(was_good)
|
||||
local llm = require("codetyper.llm")
|
||||
local llm = require("codetyper.core.llm")
|
||||
-- Get the last used provider from logs or default
|
||||
local provider = "ollama" -- Default assumption
|
||||
|
||||
-- Try to get actual last provider from logs
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local entries = logs.get(10)
|
||||
for i = #entries, 1, -1 do
|
||||
local entry = entries[i]
|
||||
@@ -1010,7 +793,7 @@ end
|
||||
|
||||
--- Reset LLM accuracy statistics
|
||||
local function cmd_llm_reset_stats()
|
||||
local selector = require("codetyper.llm.selector")
|
||||
local selector = require("codetyper.core.llm.selector")
|
||||
selector.reset_accuracy_stats()
|
||||
utils.notify("LLM accuracy statistics reset", vim.log.levels.INFO)
|
||||
end
|
||||
@@ -1061,11 +844,11 @@ local function coder_cmd(args)
|
||||
cmd_forget(args.fargs[2])
|
||||
end,
|
||||
["auto-toggle"] = function()
|
||||
local preferences = require("codetyper.preferences")
|
||||
local preferences = require("codetyper.config.preferences")
|
||||
preferences.toggle_auto_process()
|
||||
end,
|
||||
["auto-set"] = function(args)
|
||||
local preferences = require("codetyper.preferences")
|
||||
local preferences = require("codetyper.config.preferences")
|
||||
local arg = (args[1] or ""):lower()
|
||||
if arg == "auto" or arg == "automatic" or arg == "on" then
|
||||
preferences.set_auto_process(true)
|
||||
@@ -1094,11 +877,11 @@ local function coder_cmd(args)
|
||||
["llm-reset-stats"] = cmd_llm_reset_stats,
|
||||
-- Cost tracking commands
|
||||
["cost"] = function()
|
||||
local cost = require("codetyper.cost")
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.toggle()
|
||||
end,
|
||||
["cost-clear"] = function()
|
||||
local cost = require("codetyper.cost")
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.clear()
|
||||
end,
|
||||
-- Credentials management commands
|
||||
@@ -1118,6 +901,27 @@ local function coder_cmd(args)
|
||||
local credentials = require("codetyper.credentials")
|
||||
credentials.interactive_switch_provider()
|
||||
end,
|
||||
["model"] = function(args)
|
||||
local credentials = require("codetyper.credentials")
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local provider = config.llm.provider
|
||||
|
||||
-- Only available for Copilot provider
|
||||
if provider ~= "copilot" then
|
||||
utils.notify("CoderModel is only available when using Copilot provider. Current: " .. provider:upper(), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local model_arg = args.fargs[2]
|
||||
if model_arg and model_arg ~= "" then
|
||||
local cost = credentials.get_copilot_model_cost(model_arg) or "custom"
|
||||
credentials.set_credentials("copilot", { model = model_arg, configured = true })
|
||||
utils.notify("Copilot model set to: " .. model_arg .. " — " .. cost, vim.log.levels.INFO)
|
||||
else
|
||||
credentials.interactive_copilot_config(true)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local cmd_fn = commands[subcommand]
|
||||
@@ -1146,7 +950,7 @@ function M.setup()
|
||||
"auto-toggle", "auto-set",
|
||||
"llm-stats", "llm-feedback-good", "llm-feedback-bad", "llm-reset-stats",
|
||||
"cost", "cost-clear",
|
||||
"add-api-key", "remove-api-key", "credentials", "switch-provider",
|
||||
"add-api-key", "remove-api-key", "credentials", "switch-provider", "model",
|
||||
}
|
||||
end,
|
||||
desc = "Codetyper.nvim commands",
|
||||
@@ -1178,9 +982,14 @@ function M.setup()
|
||||
end, { desc = "View tree.log" })
|
||||
|
||||
-- Ask panel commands
|
||||
vim.api.nvim_create_user_command("CoderAsk", function()
|
||||
cmd_ask()
|
||||
end, { desc = "Open Ask panel" })
|
||||
vim.api.nvim_create_user_command("CoderAsk", function(opts)
|
||||
local selection = nil
|
||||
-- Check if called from visual mode (range is set)
|
||||
if opts.range > 0 then
|
||||
selection = utils.get_visual_selection()
|
||||
end
|
||||
cmd_ask(selection)
|
||||
end, { range = true, desc = "Open Ask panel (with optional visual selection)" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAskToggle", function()
|
||||
cmd_ask_toggle()
|
||||
@@ -1206,9 +1015,14 @@ function M.setup()
|
||||
end, { range = true, desc = "Transform /@ @/ tags in visual selection" })
|
||||
|
||||
-- Agent commands
|
||||
vim.api.nvim_create_user_command("CoderAgent", function()
|
||||
cmd_agent()
|
||||
end, { desc = "Open Agent panel" })
|
||||
vim.api.nvim_create_user_command("CoderAgent", function(opts)
|
||||
local selection = nil
|
||||
-- Check if called from visual mode (range is set)
|
||||
if opts.range > 0 then
|
||||
selection = utils.get_visual_selection()
|
||||
end
|
||||
cmd_agent(selection)
|
||||
end, { range = true, desc = "Open Agent panel (with optional visual selection)" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAgentToggle", function()
|
||||
cmd_agent_toggle()
|
||||
@@ -1255,7 +1069,7 @@ function M.setup()
|
||||
|
||||
-- Index command - open coder companion for current file
|
||||
vim.api.nvim_create_user_command("CoderIndex", function()
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
autocmds.open_coder_companion()
|
||||
end, { desc = "Open coder companion for current file" })
|
||||
|
||||
@@ -1290,12 +1104,12 @@ function M.setup()
|
||||
|
||||
-- Preferences commands
|
||||
vim.api.nvim_create_user_command("CoderAutoToggle", function()
|
||||
local preferences = require("codetyper.preferences")
|
||||
local preferences = require("codetyper.config.preferences")
|
||||
preferences.toggle_auto_process()
|
||||
end, { desc = "Toggle automatic/manual prompt processing" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAutoSet", function(opts)
|
||||
local preferences = require("codetyper.preferences")
|
||||
local preferences = require("codetyper.config.preferences")
|
||||
local arg = opts.args:lower()
|
||||
if arg == "auto" or arg == "automatic" or arg == "on" then
|
||||
preferences.set_auto_process(true)
|
||||
@@ -1323,7 +1137,7 @@ function M.setup()
|
||||
|
||||
-- Brain feedback command - teach the brain from your experience
|
||||
vim.api.nvim_create_user_command("CoderFeedback", function(opts)
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
if not brain.is_initialized() then
|
||||
vim.notify("Brain not initialized", vim.log.levels.WARN)
|
||||
return
|
||||
@@ -1383,7 +1197,7 @@ function M.setup()
|
||||
|
||||
-- Brain stats command
|
||||
vim.api.nvim_create_user_command("CoderBrain", function(opts)
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
if not brain.is_initialized() then
|
||||
vim.notify("Brain not initialized", vim.log.levels.WARN)
|
||||
return
|
||||
@@ -1437,7 +1251,7 @@ function M.setup()
|
||||
|
||||
-- Cost estimation command
|
||||
vim.api.nvim_create_user_command("CoderCost", function()
|
||||
local cost = require("codetyper.cost")
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.toggle()
|
||||
end, { desc = "Show LLM cost estimation window" })
|
||||
|
||||
@@ -1462,6 +1276,182 @@ function M.setup()
|
||||
credentials.interactive_switch_provider()
|
||||
end, { desc = "Switch active LLM provider" })
|
||||
|
||||
-- Quick model switcher command (Copilot only)
|
||||
vim.api.nvim_create_user_command("CoderModel", function(opts)
|
||||
local credentials = require("codetyper.credentials")
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local provider = config.llm.provider
|
||||
|
||||
-- Only available for Copilot provider
|
||||
if provider ~= "copilot" then
|
||||
utils.notify("CoderModel is only available when using Copilot provider. Current: " .. provider:upper(), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- If an argument is provided, set the model directly
|
||||
if opts.args and opts.args ~= "" then
|
||||
local cost = credentials.get_copilot_model_cost(opts.args) or "custom"
|
||||
credentials.set_credentials("copilot", { model = opts.args, configured = true })
|
||||
utils.notify("Copilot model set to: " .. opts.args .. " — " .. cost, vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Show interactive selector with costs (silent mode - no OAuth message)
|
||||
credentials.interactive_copilot_config(true)
|
||||
end, {
|
||||
nargs = "?",
|
||||
desc = "Quick switch Copilot model (only available with Copilot provider)",
|
||||
complete = function()
|
||||
local codetyper = require("codetyper")
|
||||
local credentials = require("codetyper.credentials")
|
||||
local config = codetyper.get_config()
|
||||
if config.llm.provider == "copilot" then
|
||||
return credentials.get_copilot_model_names()
|
||||
end
|
||||
return {}
|
||||
end,
|
||||
})
|
||||
|
||||
-- Conflict mode commands
|
||||
vim.api.nvim_create_user_command("CoderConflictToggle", function()
|
||||
local patch = require("codetyper.core.diff.patch")
|
||||
local current = patch.is_conflict_mode()
|
||||
patch.configure({ use_conflict_mode = not current })
|
||||
utils.notify("Conflict mode " .. (not current and "enabled" or "disabled"), vim.log.levels.INFO)
|
||||
end, { desc = "Toggle conflict mode for code changes" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictResolveAll", function(opts)
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local keep = opts.args ~= "" and opts.args or "theirs"
|
||||
if not vim.tbl_contains({ "ours", "theirs", "both", "none" }, keep) then
|
||||
utils.notify("Invalid option. Use: ours, theirs, both, or none", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
conflict.resolve_all(bufnr, keep)
|
||||
utils.notify("Resolved all conflicts with: " .. keep, vim.log.levels.INFO)
|
||||
end, {
|
||||
nargs = "?",
|
||||
complete = function() return { "ours", "theirs", "both", "none" } end,
|
||||
desc = "Resolve all conflicts (ours/theirs/both/none)"
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictNext", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
conflict.goto_next(vim.api.nvim_get_current_buf())
|
||||
end, { desc = "Go to next conflict" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictPrev", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
conflict.goto_prev(vim.api.nvim_get_current_buf())
|
||||
end, { desc = "Go to previous conflict" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictStatus", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local patch = require("codetyper.core.diff.patch")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local count = conflict.count_conflicts(bufnr)
|
||||
local mode = patch.is_conflict_mode() and "enabled" or "disabled"
|
||||
utils.notify(string.format("Conflicts in buffer: %d | Conflict mode: %s", count, mode), vim.log.levels.INFO)
|
||||
end, { desc = "Show conflict status" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictMenu", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
-- Ensure conflicts are processed first (sets up highlights and keymaps)
|
||||
conflict.process(bufnr)
|
||||
conflict.show_floating_menu(bufnr)
|
||||
end, { desc = "Show conflict resolution menu" })
|
||||
|
||||
-- Manual commands to accept conflicts
|
||||
vim.api.nvim_create_user_command("CoderConflictAcceptCurrent", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
conflict.process(bufnr) -- Ensure keymaps are set up
|
||||
conflict.accept_ours(bufnr)
|
||||
end, { desc = "Accept current (original) code" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictAcceptIncoming", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
conflict.process(bufnr) -- Ensure keymaps are set up
|
||||
conflict.accept_theirs(bufnr)
|
||||
end, { desc = "Accept incoming (AI) code" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictAcceptBoth", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
conflict.process(bufnr)
|
||||
conflict.accept_both(bufnr)
|
||||
end, { desc = "Accept both versions" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictAcceptNone", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
conflict.process(bufnr)
|
||||
conflict.accept_none(bufnr)
|
||||
end, { desc = "Delete conflict (accept none)" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderConflictAutoMenu", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local conf = conflict.get_config()
|
||||
local new_state = not conf.auto_show_menu
|
||||
conflict.configure({ auto_show_menu = new_state, auto_show_next_menu = new_state })
|
||||
utils.notify("Auto-show conflict menu " .. (new_state and "enabled" or "disabled"), vim.log.levels.INFO)
|
||||
end, { desc = "Toggle auto-show conflict menu after code injection" })
|
||||
|
||||
-- Initialize conflict module
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
conflict.setup()
|
||||
|
||||
-- Linter validation commands
|
||||
vim.api.nvim_create_user_command("CoderLintCheck", function()
|
||||
local linter = require("codetyper.features.agents.linter")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
linter.validate_after_injection(bufnr, nil, nil, function(result)
|
||||
if result then
|
||||
if not result.has_errors and not result.has_warnings then
|
||||
utils.notify("No lint errors found", vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end, { desc = "Check current buffer for lint errors" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderLintFix", function()
|
||||
local linter = require("codetyper.features.agents.linter")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
local result = linter.check_region(bufnr, 1, line_count)
|
||||
if result.has_errors or result.has_warnings then
|
||||
linter.request_ai_fix(bufnr, result)
|
||||
else
|
||||
utils.notify("No lint errors to fix", vim.log.levels.INFO)
|
||||
end
|
||||
end, { desc = "Request AI to fix lint errors in current buffer" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderLintQuickfix", function()
|
||||
local linter = require("codetyper.features.agents.linter")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
local result = linter.check_region(bufnr, 1, line_count)
|
||||
if #result.diagnostics > 0 then
|
||||
linter.show_in_quickfix(bufnr, result)
|
||||
else
|
||||
utils.notify("No lint errors to show", vim.log.levels.INFO)
|
||||
end
|
||||
end, { desc = "Show lint errors in quickfix list" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderLintToggleAuto", function()
|
||||
local conflict = require("codetyper.core.diff.conflict")
|
||||
local linter = require("codetyper.features.agents.linter")
|
||||
local linter_config = linter.get_config()
|
||||
local new_state = not linter_config.auto_save
|
||||
linter.configure({ auto_save = new_state })
|
||||
conflict.configure({ lint_after_accept = new_state, auto_fix_lint_errors = new_state })
|
||||
utils.notify("Auto lint check " .. (new_state and "enabled" or "disabled"), vim.log.levels.INFO)
|
||||
end, { desc = "Toggle automatic lint checking after code acceptance" })
|
||||
|
||||
-- Setup default keymaps
|
||||
M.setup_keymaps()
|
||||
end
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local agent = require("codetyper.agent")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local utils = require("codetyper.utils")
|
||||
local agent = require("codetyper.features.agents")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
---@class AgentUIState
|
||||
---@field chat_buf number|nil Chat buffer
|
||||
@@ -29,6 +29,7 @@ local state = {
|
||||
is_open = false,
|
||||
log_listener_id = nil,
|
||||
referenced_files = {},
|
||||
selection_context = nil, -- Visual selection passed when opening
|
||||
}
|
||||
|
||||
--- Namespace for highlights
|
||||
@@ -121,7 +122,9 @@ local function add_log_entry(entry)
|
||||
local lines = vim.api.nvim_buf_get_lines(state.logs_buf, 0, -1, false)
|
||||
local line_num = #lines
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.logs_buf, -1, -1, false, { formatted })
|
||||
-- Split formatted log into individual lines to avoid passing newline-containing items
|
||||
local formatted_lines = vim.split(formatted, "\n")
|
||||
vim.api.nvim_buf_set_lines(state.logs_buf, -1, -1, false, formatted_lines)
|
||||
|
||||
-- Apply highlighting based on level
|
||||
local hl_map = {
|
||||
@@ -234,8 +237,16 @@ local function create_callbacks()
|
||||
|
||||
on_complete = function()
|
||||
vim.schedule(function()
|
||||
add_message("system", "Done.", "DiagnosticHint")
|
||||
logs.info("Agent loop completed")
|
||||
local changes_count = agent.get_changes_count()
|
||||
if changes_count > 0 then
|
||||
add_message("system",
|
||||
string.format("Done. %d file(s) changed. Press <leader>d to review changes.", changes_count),
|
||||
"DiagnosticHint")
|
||||
logs.info(string.format("Agent completed with %d change(s)", changes_count))
|
||||
else
|
||||
add_message("system", "Done.", "DiagnosticHint")
|
||||
logs.info("Agent loop completed")
|
||||
end
|
||||
M.focus_input()
|
||||
end)
|
||||
end,
|
||||
@@ -303,12 +314,15 @@ local function submit_input()
|
||||
"╔═══════════════════════════════════════════════════════════════╗",
|
||||
"║ [AGENT MODE] Can read/write files ║",
|
||||
"╠═══════════════════════════════════════════════════════════════╣",
|
||||
"║ @ attach file | C-f current file | :CoderType switch mode ║",
|
||||
"║ @ attach | C-f current file | <leader>d review changes ║",
|
||||
"╚═══════════════════════════════════════════════════════════════╝",
|
||||
"",
|
||||
})
|
||||
vim.bo[state.chat_buf].modifiable = false
|
||||
end
|
||||
-- Also clear collected diffs
|
||||
local diff_review = require("codetyper.adapters.nvim.ui.diff_review")
|
||||
diff_review.clear()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -317,6 +331,30 @@ local function submit_input()
|
||||
return
|
||||
end
|
||||
|
||||
if input == "/continue" then
|
||||
if agent.is_running() then
|
||||
add_message("system", "Agent is already running. Use /stop first.")
|
||||
return
|
||||
end
|
||||
|
||||
if not agent.has_saved_session() then
|
||||
add_message("system", "No saved session to continue.")
|
||||
return
|
||||
end
|
||||
|
||||
local info = agent.get_saved_session_info()
|
||||
if info then
|
||||
add_message("system", string.format("Resuming session from %s...", info.saved_at))
|
||||
logs.info(string.format("Resuming: %d messages, iteration %d", info.messages, info.iteration))
|
||||
end
|
||||
|
||||
local success = agent.continue_session(create_callbacks())
|
||||
if not success then
|
||||
add_message("system", "Failed to resume session.")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Build file context
|
||||
local file_context = build_file_context()
|
||||
local file_count = vim.tbl_count(state.referenced_files)
|
||||
@@ -349,7 +387,7 @@ local function submit_input()
|
||||
current_file = vim.fn.expand("%:p")
|
||||
end
|
||||
|
||||
local llm = require("codetyper.llm")
|
||||
local llm = require("codetyper.core.llm")
|
||||
local context = {}
|
||||
|
||||
if current_file ~= "" and vim.fn.filereadable(current_file) == 1 then
|
||||
@@ -359,8 +397,15 @@ local function submit_input()
|
||||
|
||||
-- Append file context to input
|
||||
local full_input = input
|
||||
|
||||
-- Add selection context if present
|
||||
local selection_ctx = M.get_selection_context()
|
||||
if selection_ctx then
|
||||
full_input = full_input .. "\n\n" .. selection_ctx
|
||||
end
|
||||
|
||||
if file_context ~= "" then
|
||||
full_input = input .. "\n\nATTACHED FILES:" .. file_context
|
||||
full_input = full_input .. "\n\nATTACHED FILES:" .. file_context
|
||||
end
|
||||
|
||||
logs.thinking("Starting...")
|
||||
@@ -494,12 +539,20 @@ local function update_logs_title()
|
||||
end
|
||||
|
||||
--- Open the agent UI
|
||||
function M.open()
|
||||
---@param selection table|nil Visual selection context {text, start_line, end_line, filepath, filename, language}
|
||||
function M.open(selection)
|
||||
if state.is_open then
|
||||
-- If already open and new selection provided, add it as context
|
||||
if selection and selection.text and selection.text ~= "" then
|
||||
M.add_selection_context(selection)
|
||||
end
|
||||
M.focus_input()
|
||||
return
|
||||
end
|
||||
|
||||
-- Store selection context
|
||||
state.selection_context = selection
|
||||
|
||||
-- Clear previous state
|
||||
logs.clear()
|
||||
state.referenced_files = {}
|
||||
@@ -574,7 +627,7 @@ function M.open()
|
||||
"╔═══════════════════════════════════════════════════════════════╗",
|
||||
"║ [AGENT MODE] Can read/write files ║",
|
||||
"╠═══════════════════════════════════════════════════════════════╣",
|
||||
"║ @ attach file | C-f current file | :CoderType switch mode ║",
|
||||
"║ @ attach | C-f current file | <leader>d review changes ║",
|
||||
"╚═══════════════════════════════════════════════════════════════╝",
|
||||
"",
|
||||
})
|
||||
@@ -607,6 +660,7 @@ function M.open()
|
||||
vim.keymap.set("n", "<Tab>", M.focus_chat, input_opts)
|
||||
vim.keymap.set("n", "q", M.close, input_opts)
|
||||
vim.keymap.set("n", "<Esc>", M.close, input_opts)
|
||||
vim.keymap.set("n", "<leader>d", M.show_diff_review, input_opts)
|
||||
|
||||
-- Set up keymaps for chat buffer
|
||||
local chat_opts = { buffer = state.chat_buf, noremap = true, silent = true }
|
||||
@@ -617,6 +671,7 @@ function M.open()
|
||||
vim.keymap.set("n", "<C-f>", M.include_current_file, chat_opts)
|
||||
vim.keymap.set("n", "<Tab>", M.focus_logs, chat_opts)
|
||||
vim.keymap.set("n", "q", M.close, chat_opts)
|
||||
vim.keymap.set("n", "<leader>d", M.show_diff_review, chat_opts)
|
||||
|
||||
-- Set up keymaps for logs buffer
|
||||
local logs_opts = { buffer = state.logs_buf, noremap = true, silent = true }
|
||||
@@ -650,6 +705,26 @@ function M.open()
|
||||
M.focus_input()
|
||||
logs.info("Agent ready")
|
||||
|
||||
-- Check for saved session and notify user
|
||||
if agent.has_saved_session() then
|
||||
vim.schedule(function()
|
||||
local info = agent.get_saved_session_info()
|
||||
if info then
|
||||
add_message("system",
|
||||
string.format("Saved session available (%s). Type /continue to resume.", info.saved_at),
|
||||
"DiagnosticHint")
|
||||
logs.info("Saved session found: " .. (info.prompt or ""):sub(1, 30) .. "...")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- If we have a selection, show it as context
|
||||
if selection and selection.text and selection.text ~= "" then
|
||||
vim.schedule(function()
|
||||
M.add_selection_context(selection)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Log provider info
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok then
|
||||
@@ -732,4 +807,101 @@ function M.is_open()
|
||||
return state.is_open
|
||||
end
|
||||
|
||||
--- Show the diff review for all changes made in this session
|
||||
function M.show_diff_review()
|
||||
local changes_count = agent.get_changes_count()
|
||||
if changes_count == 0 then
|
||||
utils.notify("No changes to review", vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
agent.show_diff_review()
|
||||
end
|
||||
|
||||
--- Add visual selection as context in the chat
|
||||
---@param selection table Selection info {text, start_line, end_line, filepath, filename, language}
|
||||
function M.add_selection_context(selection)
|
||||
if not state.chat_buf or not vim.api.nvim_buf_is_valid(state.chat_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
state.selection_context = selection
|
||||
|
||||
vim.bo[state.chat_buf].modifiable = true
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.chat_buf, 0, -1, false)
|
||||
|
||||
-- Format the selection display
|
||||
local location = ""
|
||||
if selection.filename then
|
||||
location = selection.filename
|
||||
if selection.start_line then
|
||||
location = location .. ":" .. selection.start_line
|
||||
if selection.end_line and selection.end_line ~= selection.start_line then
|
||||
location = location .. "-" .. selection.end_line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local new_lines = {
|
||||
"",
|
||||
"┌─ Selected Code ─────────────────────",
|
||||
"│ " .. location,
|
||||
"│",
|
||||
}
|
||||
|
||||
-- Add the selected code
|
||||
for _, line in ipairs(vim.split(selection.text, "\n")) do
|
||||
table.insert(new_lines, "│ " .. line)
|
||||
end
|
||||
|
||||
table.insert(new_lines, "│")
|
||||
table.insert(new_lines, "└──────────────────────────────────────")
|
||||
table.insert(new_lines, "")
|
||||
table.insert(new_lines, "Describe what you'd like to do with this code.")
|
||||
|
||||
for _, line in ipairs(new_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.chat_buf, 0, -1, false, lines)
|
||||
vim.bo[state.chat_buf].modifiable = false
|
||||
|
||||
-- Scroll to bottom
|
||||
if state.chat_win and vim.api.nvim_win_is_valid(state.chat_win) then
|
||||
local line_count = vim.api.nvim_buf_line_count(state.chat_buf)
|
||||
vim.api.nvim_win_set_cursor(state.chat_win, { line_count, 0 })
|
||||
end
|
||||
|
||||
-- Also add the file to referenced_files for context
|
||||
if selection.filepath and selection.filepath ~= "" then
|
||||
state.referenced_files[selection.filename or "selection"] = selection.filepath
|
||||
end
|
||||
|
||||
logs.info("Selection added: " .. location)
|
||||
end
|
||||
|
||||
--- Get selection context for agent prompt
|
||||
---@return string|nil Selection context string
|
||||
function M.get_selection_context()
|
||||
if not state.selection_context or not state.selection_context.text then
|
||||
return nil
|
||||
end
|
||||
|
||||
local sel = state.selection_context
|
||||
local location = sel.filename or "unknown"
|
||||
if sel.start_line then
|
||||
location = location .. ":" .. sel.start_line
|
||||
if sel.end_line and sel.end_line ~= sel.start_line then
|
||||
location = location .. "-" .. sel.end_line
|
||||
end
|
||||
end
|
||||
|
||||
return string.format(
|
||||
"SELECTED CODE (%s):\n```%s\n%s\n```",
|
||||
location,
|
||||
sel.language or "",
|
||||
sel.text
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
381
lua/codetyper/adapters/nvim/ui/context_modal.lua
Normal file
381
lua/codetyper/adapters/nvim/ui/context_modal.lua
Normal file
@@ -0,0 +1,381 @@
|
||||
---@mod codetyper.agent.context_modal Modal for additional context input
|
||||
---@brief [[
|
||||
--- Opens a floating window for user to provide additional context
|
||||
--- when the LLM requests more information.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ContextModalState
|
||||
---@field buf number|nil Buffer number
|
||||
---@field win number|nil Window number
|
||||
---@field original_event table|nil Original prompt event
|
||||
---@field callback function|nil Callback with additional context
|
||||
---@field llm_response string|nil LLM's response asking for context
|
||||
|
||||
local state = {
|
||||
buf = nil,
|
||||
win = nil,
|
||||
original_event = nil,
|
||||
callback = nil,
|
||||
llm_response = nil,
|
||||
attached_files = nil,
|
||||
}
|
||||
|
||||
--- Close the context modal
|
||||
function M.close()
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
vim.api.nvim_win_close(state.win, true)
|
||||
end
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.api.nvim_buf_delete(state.buf, { force = true })
|
||||
end
|
||||
state.win = nil
|
||||
state.buf = nil
|
||||
state.original_event = nil
|
||||
state.callback = nil
|
||||
state.llm_response = nil
|
||||
end
|
||||
|
||||
--- Submit the additional context
|
||||
local function submit()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
|
||||
local additional_context = table.concat(lines, "\n")
|
||||
|
||||
-- Trim whitespace
|
||||
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
|
||||
|
||||
if additional_context == "" then
|
||||
M.close()
|
||||
return
|
||||
end
|
||||
|
||||
local original_event = state.original_event
|
||||
local callback = state.callback
|
||||
|
||||
M.close()
|
||||
|
||||
if callback and original_event then
|
||||
-- Pass attached_files as third optional parameter
|
||||
callback(original_event, additional_context, state.attached_files)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Parse requested file paths from LLM response and resolve to full paths
|
||||
local function parse_requested_files(response)
|
||||
if not response or response == "" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local cwd = vim.fn.getcwd()
|
||||
local candidates = {}
|
||||
local seen = {}
|
||||
|
||||
for path in response:gmatch("`([%w%._%-%/]+%.[%w_]+)`") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
for path in response:gmatch("([%w%._%-%/]+%.[%w_]+)") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Resolve to full paths using cwd and glob
|
||||
local resolved = {}
|
||||
for _, p in ipairs(candidates) do
|
||||
local full = nil
|
||||
if p:sub(1,1) == "/" and vim.fn.filereadable(p) == 1 then
|
||||
full = p
|
||||
else
|
||||
local try1 = cwd .. "/" .. p
|
||||
if vim.fn.filereadable(try1) == 1 then
|
||||
full = try1
|
||||
else
|
||||
local tail = p:match("[^/]+$") or p
|
||||
local matches = vim.fn.globpath(cwd, "**/" .. tail, false, true)
|
||||
if matches and #matches > 0 then
|
||||
full = matches[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
if full and vim.fn.filereadable(full) == 1 then
|
||||
table.insert(resolved, full)
|
||||
end
|
||||
end
|
||||
return resolved
|
||||
end
|
||||
|
||||
|
||||
--- Attach parsed files into the modal buffer and remember them for submission
|
||||
local function attach_requested_files()
|
||||
if not state.llm_response or state.llm_response == "" then
|
||||
return
|
||||
end
|
||||
local files = parse_requested_files(state.llm_response)
|
||||
if #files == 0 then
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header)
|
||||
return
|
||||
end
|
||||
|
||||
state.attached_files = state.attached_files or {}
|
||||
|
||||
for _, full in ipairs(files) do
|
||||
local ok, lines = pcall(vim.fn.readfile, full)
|
||||
if ok and lines and #lines > 0 then
|
||||
table.insert(state.attached_files, { path = vim.fn.fnamemodify(full, ":~:." ) , full_path = full, content = table.concat(lines, "\n") })
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Attached: " .. full .. " --" })
|
||||
for i, l in ipairs(lines) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1 + i, insert_at + 1 + i, false, { l })
|
||||
end
|
||||
else
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Failed to read: " .. full .. " --" })
|
||||
end
|
||||
end
|
||||
-- Move cursor to end and enter insert mode
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
--- Open the context modal
|
||||
---@param original_event table Original prompt event
|
||||
---@param llm_response string LLM's response asking for context
|
||||
---@param callback function(event: table, additional_context: string, attached_files?: table)
|
||||
---@param suggested_commands table[]|nil Optional list of {label,cmd} suggested shell commands
|
||||
function M.open(original_event, llm_response, callback, suggested_commands)
|
||||
-- Close any existing modal
|
||||
M.close()
|
||||
|
||||
state.original_event = original_event
|
||||
state.llm_response = llm_response
|
||||
state.callback = callback
|
||||
|
||||
-- Calculate window size
|
||||
local width = math.min(80, vim.o.columns - 10)
|
||||
local height = 10
|
||||
|
||||
-- Create buffer
|
||||
state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.buf].buftype = "nofile"
|
||||
vim.bo[state.buf].bufhidden = "wipe"
|
||||
vim.bo[state.buf].filetype = "markdown"
|
||||
|
||||
-- Create window
|
||||
local row = math.floor((vim.o.lines - height) / 2)
|
||||
local col = math.floor((vim.o.columns - width) / 2)
|
||||
|
||||
state.win = vim.api.nvim_open_win(state.buf, true, {
|
||||
relative = "editor",
|
||||
row = row,
|
||||
col = col,
|
||||
width = width,
|
||||
height = height,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
title = " Additional Context Needed ",
|
||||
title_pos = "center",
|
||||
})
|
||||
|
||||
-- Set window options
|
||||
vim.wo[state.win].wrap = true
|
||||
vim.wo[state.win].cursorline = true
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
|
||||
-- Add header showing what the LLM said
|
||||
local header_lines = {
|
||||
ui_prompts.llm_response_header,
|
||||
}
|
||||
|
||||
-- Truncate LLM response for display
|
||||
local response_preview = llm_response or ""
|
||||
if #response_preview > 200 then
|
||||
response_preview = response_preview:sub(1, 200) .. "..."
|
||||
end
|
||||
for line in response_preview:gmatch("[^\n]+") do
|
||||
table.insert(header_lines, "-- " .. line)
|
||||
end
|
||||
|
||||
-- If suggested commands were provided, show them in the header
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.suggested_commands_header)
|
||||
for i, s in ipairs(suggested_commands) do
|
||||
local label = s.label or s.cmd
|
||||
table.insert(header_lines, string.format("[%d] %s: %s", i, label, s.cmd))
|
||||
end
|
||||
table.insert(header_lines, ui_prompts.commands_hint)
|
||||
end
|
||||
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.input_header)
|
||||
table.insert(header_lines, "")
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
|
||||
|
||||
-- Move cursor to the end
|
||||
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
|
||||
|
||||
-- Set up keymaps
|
||||
local opts = { buffer = state.buf, noremap = true, silent = true }
|
||||
|
||||
-- Submit with Ctrl+Enter or <leader>s
|
||||
vim.keymap.set("n", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("i", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("n", "<leader>s", submit, opts)
|
||||
vim.keymap.set("n", "<CR><CR>", submit, opts)
|
||||
|
||||
-- Attach parsed files (from LLM response)
|
||||
vim.keymap.set("n", "a", function()
|
||||
attach_requested_files()
|
||||
end, opts)
|
||||
|
||||
-- Confirm and submit with 'c' (convenient when doing question round)
|
||||
vim.keymap.set("n", "c", submit, opts)
|
||||
|
||||
-- Quick run of project inspection from modal with <leader>r / <C-r> in insert mode
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, opts)
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, { buffer = state.buf, noremap = true, silent = true })
|
||||
|
||||
-- If suggested commands provided, create per-command keymaps <leader>1..n to run them
|
||||
state.suggested_commands = suggested_commands
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
for i, s in ipairs(suggested_commands) do
|
||||
local key = "<leader>" .. tostring(i)
|
||||
vim.keymap.set("n", key, function()
|
||||
-- run this single command and append output
|
||||
if not s or not s.cmd then
|
||||
return
|
||||
end
|
||||
local ok, out = pcall(vim.fn.systemlist, s.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
|
||||
if ok and out and #out > 0 then
|
||||
for j, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1, insert_at + 1, false, { "(no output or command failed)" })
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end, opts)
|
||||
end
|
||||
-- Also map <leader>0 to run all suggested commands
|
||||
vim.keymap.set("n", "<leader>0", function()
|
||||
for _, s in ipairs(suggested_commands) do
|
||||
pcall(function()
|
||||
local ok, out = pcall(vim.fn.systemlist, s.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
|
||||
if ok and out and #out > 0 then
|
||||
for j, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1, insert_at + 1, false, { "(no output or command failed)" })
|
||||
end
|
||||
end)
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end, opts)
|
||||
end
|
||||
|
||||
-- Close with Esc or q
|
||||
vim.keymap.set("n", "<Esc>", M.close, opts)
|
||||
vim.keymap.set("n", "q", M.close, opts)
|
||||
|
||||
-- Start in insert mode
|
||||
vim.cmd("startinsert")
|
||||
|
||||
-- Log
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Context modal opened - waiting for user input",
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
--- Run a small set of safe project inspection commands and insert outputs into the modal buffer
|
||||
local function run_project_inspect()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local cmds = {
|
||||
{ label = "List files (ls -la)", cmd = "ls -la" },
|
||||
{ label = "Git status (git status --porcelain)", cmd = "git status --porcelain" },
|
||||
{ label = "Git top (git rev-parse --show-toplevel)", cmd = "git rev-parse --show-toplevel" },
|
||||
{ label = "Show repo files (git ls-files)", cmd = "git ls-files" },
|
||||
}
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
local insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header)
|
||||
|
||||
for _, c in ipairs(cmds) do
|
||||
local ok, out = pcall(vim.fn.systemlist, c.cmd)
|
||||
if ok and out and #out > 0 then
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2, insert_pos + 2, false, { "-- " .. c.label .. " --" })
|
||||
for i, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2 + i, insert_pos + 2 + i, false, { line })
|
||||
end
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2, insert_pos + 2, false, { "-- " .. c.label .. " --", "(no output or command failed)" })
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
end
|
||||
end
|
||||
|
||||
-- Move cursor to end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
-- Provide a keybinding in the modal to run project inspection commands
|
||||
pcall(function()
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, { buffer = state.buf, noremap = true, silent = true })
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, { buffer = state.buf, noremap = true, silent = true })
|
||||
end
|
||||
end)
|
||||
|
||||
--- Check if modal is open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
|
||||
end
|
||||
|
||||
--- Setup autocmds for the context modal
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
|
||||
|
||||
-- Close context modal when exiting Neovim
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
M.close()
|
||||
end,
|
||||
desc = "Close context modal before exiting Neovim",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
386
lua/codetyper/adapters/nvim/ui/diff_review.lua
Normal file
386
lua/codetyper/adapters/nvim/ui/diff_review.lua
Normal file
@@ -0,0 +1,386 @@
|
||||
---@mod codetyper.agent.diff_review Diff review UI for agent changes
|
||||
---
|
||||
--- Provides a lazygit-style window interface for reviewing all changes
|
||||
--- made during an agent session.
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.support.utils")
|
||||
local prompts = require("codetyper.prompts.agents.diff")
|
||||
|
||||
|
||||
---@class DiffEntry
|
||||
---@field path string File path
|
||||
---@field operation string "create"|"edit"|"delete"
|
||||
---@field original string|nil Original content (nil for new files)
|
||||
---@field modified string New/modified content
|
||||
---@field approved boolean Whether change was approved
|
||||
---@field applied boolean Whether change was applied
|
||||
|
||||
---@class DiffReviewState
|
||||
---@field entries DiffEntry[] List of changes
|
||||
---@field current_index number Currently selected entry
|
||||
---@field list_buf number|nil File list buffer
|
||||
---@field list_win number|nil File list window
|
||||
---@field diff_buf number|nil Diff view buffer
|
||||
---@field diff_win number|nil Diff view window
|
||||
---@field is_open boolean Whether review UI is open
|
||||
|
||||
local state = {
|
||||
entries = {},
|
||||
current_index = 1,
|
||||
list_buf = nil,
|
||||
list_win = nil,
|
||||
diff_buf = nil,
|
||||
diff_win = nil,
|
||||
is_open = false,
|
||||
}
|
||||
|
||||
--- Clear all collected diffs
|
||||
function M.clear()
|
||||
state.entries = {}
|
||||
state.current_index = 1
|
||||
end
|
||||
|
||||
--- Add a diff entry
|
||||
---@param entry DiffEntry
|
||||
function M.add(entry)
|
||||
table.insert(state.entries, entry)
|
||||
end
|
||||
|
||||
--- Get all entries
|
||||
---@return DiffEntry[]
|
||||
function M.get_entries()
|
||||
return state.entries
|
||||
end
|
||||
|
||||
--- Get entry count
|
||||
---@return number
|
||||
function M.count()
|
||||
return #state.entries
|
||||
end
|
||||
|
||||
--- Generate unified diff between two strings
|
||||
---@param original string|nil
|
||||
---@param modified string
|
||||
---@param filepath string
|
||||
---@return string[]
|
||||
local function generate_diff_lines(original, modified, filepath)
|
||||
local lines = {}
|
||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||
|
||||
if not original then
|
||||
-- New file
|
||||
table.insert(lines, "--- /dev/null")
|
||||
table.insert(lines, "+++ b/" .. filename)
|
||||
table.insert(lines, "@@ -0,0 +1," .. #vim.split(modified, "\n") .. " @@")
|
||||
for _, line in ipairs(vim.split(modified, "\n")) do
|
||||
table.insert(lines, "+" .. line)
|
||||
end
|
||||
else
|
||||
-- Modified file - use vim's diff
|
||||
table.insert(lines, "--- a/" .. filename)
|
||||
table.insert(lines, "+++ b/" .. filename)
|
||||
|
||||
local orig_lines = vim.split(original, "\n")
|
||||
local mod_lines = vim.split(modified, "\n")
|
||||
|
||||
-- Simple diff: show removed and added lines
|
||||
local max_lines = math.max(#orig_lines, #mod_lines)
|
||||
local context_start = 1
|
||||
local in_change = false
|
||||
|
||||
for i = 1, max_lines do
|
||||
local orig = orig_lines[i] or ""
|
||||
local mod = mod_lines[i] or ""
|
||||
|
||||
if orig ~= mod then
|
||||
if not in_change then
|
||||
table.insert(lines, string.format("@@ -%d,%d +%d,%d @@",
|
||||
math.max(1, i - 2), math.min(5, #orig_lines - i + 3),
|
||||
math.max(1, i - 2), math.min(5, #mod_lines - i + 3)))
|
||||
in_change = true
|
||||
end
|
||||
if orig ~= "" then
|
||||
table.insert(lines, "-" .. orig)
|
||||
end
|
||||
if mod ~= "" then
|
||||
table.insert(lines, "+" .. mod)
|
||||
end
|
||||
else
|
||||
if in_change then
|
||||
table.insert(lines, " " .. orig)
|
||||
in_change = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Update the diff view for current entry
|
||||
local function update_diff_view()
|
||||
if not state.diff_buf or not vim.api.nvim_buf_is_valid(state.diff_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local entry = state.entries[state.current_index]
|
||||
local ui_prompts = prompts.review
|
||||
if not entry then
|
||||
vim.bo[state.diff_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, { ui_prompts.messages.no_changes_short })
|
||||
vim.bo[state.diff_buf].modifiable = false
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
|
||||
-- Header
|
||||
local status_icon = entry.applied and " " or (entry.approved and " " or " ")
|
||||
local op_icon = entry.operation == "create" and "+" or (entry.operation == "delete" and "-" or "~")
|
||||
local current_status = entry.applied and ui_prompts.status.applied
|
||||
or (entry.approved and ui_prompts.status.approved or ui_prompts.status.pending)
|
||||
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.top,
|
||||
status_icon, op_icon, vim.fn.fnamemodify(entry.path, ":t")))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.path, entry.path))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.op, entry.operation))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.status, current_status))
|
||||
table.insert(lines, ui_prompts.diff_header.bottom)
|
||||
table.insert(lines, "")
|
||||
|
||||
-- Diff content
|
||||
local diff_lines = generate_diff_lines(entry.original, entry.modified, entry.path)
|
||||
for _, line in ipairs(diff_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
vim.bo[state.diff_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, lines)
|
||||
vim.bo[state.diff_buf].modifiable = false
|
||||
vim.bo[state.diff_buf].filetype = "diff"
|
||||
end
|
||||
|
||||
--- Update the file list
|
||||
local function update_file_list()
|
||||
if not state.list_buf or not vim.api.nvim_buf_is_valid(state.list_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local ui_prompts = prompts.review
|
||||
local lines = {}
|
||||
table.insert(lines, string.format(ui_prompts.list_menu.top, #state.entries))
|
||||
for _, item in ipairs(ui_prompts.list_menu.items) do
|
||||
table.insert(lines, item)
|
||||
end
|
||||
table.insert(lines, ui_prompts.list_menu.bottom)
|
||||
table.insert(lines, "")
|
||||
|
||||
for i, entry in ipairs(state.entries) do
|
||||
local prefix = (i == state.current_index) and "▶ " or " "
|
||||
local status = entry.applied and "" or (entry.approved and "" or "○")
|
||||
local op = entry.operation == "create" and "[+]" or (entry.operation == "delete" and "[-]" or "[~]")
|
||||
local filename = vim.fn.fnamemodify(entry.path, ":t")
|
||||
|
||||
table.insert(lines, string.format("%s%s %s %s", prefix, status, op, filename))
|
||||
end
|
||||
|
||||
if #state.entries == 0 then
|
||||
table.insert(lines, ui_prompts.messages.no_changes)
|
||||
end
|
||||
|
||||
vim.bo[state.list_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.list_buf, 0, -1, false, lines)
|
||||
vim.bo[state.list_buf].modifiable = false
|
||||
|
||||
-- Highlight current line
|
||||
if state.list_win and vim.api.nvim_win_is_valid(state.list_win) then
|
||||
local target_line = 9 + state.current_index - 1
|
||||
if target_line <= vim.api.nvim_buf_line_count(state.list_buf) then
|
||||
vim.api.nvim_win_set_cursor(state.list_win, { target_line, 0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Navigate to next entry
|
||||
function M.next()
|
||||
if state.current_index < #state.entries then
|
||||
state.current_index = state.current_index + 1
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Navigate to previous entry
|
||||
function M.prev()
|
||||
if state.current_index > 1 then
|
||||
state.current_index = state.current_index - 1
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Approve current entry
|
||||
function M.approve_current()
|
||||
local entry = state.entries[state.current_index]
|
||||
if entry and not entry.applied then
|
||||
entry.approved = true
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Reject current entry
|
||||
function M.reject_current()
|
||||
local entry = state.entries[state.current_index]
|
||||
if entry and not entry.applied then
|
||||
entry.approved = false
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Approve all entries
|
||||
function M.approve_all()
|
||||
for _, entry in ipairs(state.entries) do
|
||||
if not entry.applied then
|
||||
entry.approved = true
|
||||
end
|
||||
end
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
|
||||
--- Apply approved changes
|
||||
function M.apply_approved()
|
||||
local applied_count = 0
|
||||
|
||||
for _, entry in ipairs(state.entries) do
|
||||
if entry.approved and not entry.applied then
|
||||
if entry.operation == "create" or entry.operation == "edit" then
|
||||
local ok = utils.write_file(entry.path, entry.modified)
|
||||
if ok then
|
||||
entry.applied = true
|
||||
applied_count = applied_count + 1
|
||||
end
|
||||
elseif entry.operation == "delete" then
|
||||
local ok = os.remove(entry.path)
|
||||
if ok then
|
||||
entry.applied = true
|
||||
applied_count = applied_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
|
||||
if applied_count > 0 then
|
||||
utils.notify(string.format(prompts.review.messages.applied_count, applied_count))
|
||||
end
|
||||
|
||||
return applied_count
|
||||
end
|
||||
|
||||
--- Open the diff review UI
|
||||
function M.open()
|
||||
if state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
if #state.entries == 0 then
|
||||
utils.notify(prompts.review.messages.no_changes_short, vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create list buffer
|
||||
state.list_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.list_buf].buftype = "nofile"
|
||||
vim.bo[state.list_buf].bufhidden = "wipe"
|
||||
vim.bo[state.list_buf].swapfile = false
|
||||
|
||||
-- Create diff buffer
|
||||
state.diff_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.diff_buf].buftype = "nofile"
|
||||
vim.bo[state.diff_buf].bufhidden = "wipe"
|
||||
vim.bo[state.diff_buf].swapfile = false
|
||||
|
||||
-- Create layout: list on left (30 cols), diff on right
|
||||
vim.cmd("tabnew")
|
||||
state.diff_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.diff_win, state.diff_buf)
|
||||
|
||||
vim.cmd("topleft vsplit")
|
||||
state.list_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.list_win, state.list_buf)
|
||||
vim.api.nvim_win_set_width(state.list_win, 35)
|
||||
|
||||
-- Window options
|
||||
for _, win in ipairs({ state.list_win, state.diff_win }) do
|
||||
vim.wo[win].number = false
|
||||
vim.wo[win].relativenumber = false
|
||||
vim.wo[win].signcolumn = "no"
|
||||
vim.wo[win].wrap = false
|
||||
vim.wo[win].cursorline = true
|
||||
end
|
||||
|
||||
-- Set up keymaps for list buffer
|
||||
local list_opts = { buffer = state.list_buf, noremap = true, silent = true }
|
||||
vim.keymap.set("n", "j", M.next, list_opts)
|
||||
vim.keymap.set("n", "k", M.prev, list_opts)
|
||||
vim.keymap.set("n", "<Down>", M.next, list_opts)
|
||||
vim.keymap.set("n", "<Up>", M.prev, list_opts)
|
||||
vim.keymap.set("n", "<CR>", function() vim.api.nvim_set_current_win(state.diff_win) end, list_opts)
|
||||
vim.keymap.set("n", "a", M.approve_current, list_opts)
|
||||
vim.keymap.set("n", "r", M.reject_current, list_opts)
|
||||
vim.keymap.set("n", "A", M.approve_all, list_opts)
|
||||
vim.keymap.set("n", "q", M.close, list_opts)
|
||||
vim.keymap.set("n", "<Esc>", M.close, list_opts)
|
||||
|
||||
-- Set up keymaps for diff buffer
|
||||
local diff_opts = { buffer = state.diff_buf, noremap = true, silent = true }
|
||||
vim.keymap.set("n", "j", M.next, diff_opts)
|
||||
vim.keymap.set("n", "k", M.prev, diff_opts)
|
||||
vim.keymap.set("n", "<Tab>", function() vim.api.nvim_set_current_win(state.list_win) end, diff_opts)
|
||||
vim.keymap.set("n", "a", M.approve_current, diff_opts)
|
||||
vim.keymap.set("n", "r", M.reject_current, diff_opts)
|
||||
vim.keymap.set("n", "A", M.approve_all, diff_opts)
|
||||
vim.keymap.set("n", "q", M.close, diff_opts)
|
||||
vim.keymap.set("n", "<Esc>", M.close, diff_opts)
|
||||
|
||||
state.is_open = true
|
||||
state.current_index = 1
|
||||
|
||||
-- Initial render
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
|
||||
-- Focus list window
|
||||
vim.api.nvim_set_current_win(state.list_win)
|
||||
end
|
||||
|
||||
--- Close the diff review UI
|
||||
function M.close()
|
||||
if not state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
-- Close the tab (which closes both windows)
|
||||
pcall(vim.cmd, "tabclose")
|
||||
|
||||
state.list_buf = nil
|
||||
state.list_win = nil
|
||||
state.diff_buf = nil
|
||||
state.diff_win = nil
|
||||
state.is_open = false
|
||||
end
|
||||
|
||||
--- Check if review UI is open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return state.is_open
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local params = require("codetyper.params.agents.logs")
|
||||
|
||||
|
||||
---@class LogEntry
|
||||
---@field timestamp string ISO timestamp
|
||||
---@field level string "info" | "debug" | "request" | "response" | "tool" | "error"
|
||||
@@ -119,14 +122,7 @@ end
|
||||
---@param status string "start" | "success" | "error" | "approval"
|
||||
---@param details? string Additional details
|
||||
function M.tool(tool_name, status, details)
|
||||
local icons = {
|
||||
start = "->",
|
||||
success = "OK",
|
||||
error = "ERR",
|
||||
approval = "??",
|
||||
approved = "YES",
|
||||
rejected = "NO",
|
||||
}
|
||||
local icons = params.icons
|
||||
|
||||
local msg = string.format("[%s] %s", icons[status] or status, tool_name)
|
||||
if details then
|
||||
@@ -165,10 +161,83 @@ function M.add(entry)
|
||||
M.log(entry.type or "info", entry.message or "", entry.data)
|
||||
end
|
||||
|
||||
--- Log thinking/reasoning step
|
||||
--- Log thinking/reasoning step (Claude Code style)
|
||||
---@param step string Description of what's happening
|
||||
function M.thinking(step)
|
||||
M.log("debug", "> " .. step)
|
||||
M.log("thinking", step)
|
||||
end
|
||||
|
||||
--- Log a reasoning/explanation message (shown prominently)
|
||||
---@param message string The reasoning message
|
||||
function M.reason(message)
|
||||
M.log("reason", message)
|
||||
end
|
||||
|
||||
--- Log file read operation
|
||||
---@param filepath string Path of file being read
|
||||
---@param lines? number Number of lines read
|
||||
function M.read(filepath, lines)
|
||||
local msg = string.format("Read(%s)", vim.fn.fnamemodify(filepath, ":~:."))
|
||||
if lines then
|
||||
msg = msg .. string.format("\n ⎿ Read %d lines", lines)
|
||||
end
|
||||
M.log("action", msg)
|
||||
end
|
||||
|
||||
--- Log explore/search operation
|
||||
---@param description string What we're exploring
|
||||
function M.explore(description)
|
||||
M.log("action", string.format("Explore(%s)", description))
|
||||
end
|
||||
|
||||
--- Log explore done
|
||||
---@param tool_uses number Number of tool uses
|
||||
---@param tokens number Tokens used
|
||||
---@param duration number Duration in seconds
|
||||
function M.explore_done(tool_uses, tokens, duration)
|
||||
M.log("result", string.format(" ⎿ Done (%d tool uses · %.1fk tokens · %.1fs)", tool_uses, tokens / 1000, duration))
|
||||
end
|
||||
|
||||
--- Log update/edit operation
|
||||
---@param filepath string Path of file being edited
|
||||
---@param added? number Lines added
|
||||
---@param removed? number Lines removed
|
||||
function M.update(filepath, added, removed)
|
||||
local msg = string.format("Update(%s)", vim.fn.fnamemodify(filepath, ":~:."))
|
||||
if added or removed then
|
||||
local parts = {}
|
||||
if added and added > 0 then
|
||||
table.insert(parts, string.format("Added %d lines", added))
|
||||
end
|
||||
if removed and removed > 0 then
|
||||
table.insert(parts, string.format("Removed %d lines", removed))
|
||||
end
|
||||
if #parts > 0 then
|
||||
msg = msg .. "\n ⎿ " .. table.concat(parts, ", ")
|
||||
end
|
||||
end
|
||||
M.log("action", msg)
|
||||
end
|
||||
|
||||
--- Log a task/step that's in progress
|
||||
---@param task string Task name
|
||||
---@param status string Status message (optional)
|
||||
function M.task(task, status)
|
||||
local msg = task
|
||||
if status then
|
||||
msg = msg .. " " .. status
|
||||
end
|
||||
M.log("task", msg)
|
||||
end
|
||||
|
||||
--- Log task completion
|
||||
---@param next_task? string Next task (optional)
|
||||
function M.task_done(next_task)
|
||||
local msg = " ⎿ Done"
|
||||
if next_task then
|
||||
msg = msg .. "\n✶ " .. next_task
|
||||
end
|
||||
M.log("result", msg)
|
||||
end
|
||||
|
||||
--- Register a listener for new log entries
|
||||
@@ -223,18 +292,22 @@ end
|
||||
---@param entry LogEntry
|
||||
---@return string
|
||||
function M.format_entry(entry)
|
||||
local level_prefix = ({
|
||||
info = "i",
|
||||
debug = ".",
|
||||
request = ">",
|
||||
response = "<",
|
||||
tool = "T",
|
||||
error = "!",
|
||||
warning = "?",
|
||||
success = "i",
|
||||
queue = "Q",
|
||||
patch = "P",
|
||||
})[entry.level] or "?"
|
||||
-- Claude Code style formatting for thinking/action entries
|
||||
local thinking_types = params.thinking_types
|
||||
local is_thinking = vim.tbl_contains(thinking_types, entry.level)
|
||||
|
||||
if is_thinking then
|
||||
local prefix = params.thinking_prefixes[entry.level] or "⏺"
|
||||
|
||||
if prefix ~= "" then
|
||||
return prefix .. " " .. entry.message
|
||||
else
|
||||
return entry.message
|
||||
end
|
||||
end
|
||||
|
||||
-- Traditional log format for other types
|
||||
local level_prefix = params.level_icons[entry.level] or "?"
|
||||
|
||||
local base = string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message)
|
||||
|
||||
@@ -248,6 +321,54 @@ function M.format_entry(entry)
|
||||
return base
|
||||
end
|
||||
|
||||
--- Format entry for display in chat (compact Claude Code style)
|
||||
---@param entry LogEntry
|
||||
---@return string|nil Formatted string or nil to skip
|
||||
function M.format_for_chat(entry)
|
||||
-- Skip certain log types in chat view
|
||||
local skip_types = { "debug", "queue", "patch" }
|
||||
if vim.tbl_contains(skip_types, entry.level) then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Claude Code style formatting
|
||||
local thinking_types = params.thinking_types
|
||||
if vim.tbl_contains(thinking_types, entry.level) then
|
||||
local prefix = params.thinking_prefixes[entry.level] or "⏺"
|
||||
|
||||
if prefix ~= "" then
|
||||
return prefix .. " " .. entry.message
|
||||
else
|
||||
return entry.message
|
||||
end
|
||||
end
|
||||
|
||||
-- Tool logs
|
||||
if entry.level == "tool" then
|
||||
return "⏺ " .. entry.message:gsub("^%[.-%] ", "")
|
||||
end
|
||||
|
||||
-- Info/success
|
||||
if entry.level == "info" or entry.level == "success" then
|
||||
return "⏺ " .. entry.message
|
||||
end
|
||||
|
||||
-- Errors
|
||||
if entry.level == "error" then
|
||||
return "⚠ " .. entry.message
|
||||
end
|
||||
|
||||
-- Request/response (compact)
|
||||
if entry.level == "request" then
|
||||
return "⏺ " .. entry.message
|
||||
end
|
||||
if entry.level == "response" then
|
||||
return " ⎿ " .. entry.message
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Estimate token count for a string (rough approximation)
|
||||
---@param text string
|
||||
---@return number
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local queue = require("codetyper.agent.queue")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
|
||||
---@class LogsPanelState
|
||||
---@field buf number|nil Logs buffer
|
||||
@@ -20,8 +20,8 @@ function M.show()
|
||||
end
|
||||
|
||||
-- Close current panel first
|
||||
local ask = require("codetyper.ask")
|
||||
local agent_ui = require("codetyper.agent.ui")
|
||||
local ask = require("codetyper.features.ask.engine")
|
||||
local agent_ui = require("codetyper.adapters.nvim.ui.chat")
|
||||
|
||||
if ask.is_open() then
|
||||
ask.close()
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
---@type number|nil Current coder window ID
|
||||
M._coder_win = nil
|
||||
@@ -43,7 +43,7 @@ function M.open_split(target_path, coder_path)
|
||||
utils.write_file(coder_path, "")
|
||||
|
||||
-- Ensure gitignore is updated when creating a new coder file
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
local gitignore = require("codetyper.support.gitignore")
|
||||
gitignore.ensure_ignored()
|
||||
end
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
---@mod codetyper.agent.context_modal Modal for additional context input
|
||||
---@brief [[
|
||||
--- Opens a floating window for user to provide additional context
|
||||
--- when the LLM requests more information.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ContextModalState
|
||||
---@field buf number|nil Buffer number
|
||||
---@field win number|nil Window number
|
||||
---@field original_event table|nil Original prompt event
|
||||
---@field callback function|nil Callback with additional context
|
||||
---@field llm_response string|nil LLM's response asking for context
|
||||
|
||||
local state = {
|
||||
buf = nil,
|
||||
win = nil,
|
||||
original_event = nil,
|
||||
callback = nil,
|
||||
llm_response = nil,
|
||||
}
|
||||
|
||||
--- Close the context modal
|
||||
function M.close()
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
vim.api.nvim_win_close(state.win, true)
|
||||
end
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.api.nvim_buf_delete(state.buf, { force = true })
|
||||
end
|
||||
state.win = nil
|
||||
state.buf = nil
|
||||
state.original_event = nil
|
||||
state.callback = nil
|
||||
state.llm_response = nil
|
||||
end
|
||||
|
||||
--- Submit the additional context
|
||||
local function submit()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
|
||||
local additional_context = table.concat(lines, "\n")
|
||||
|
||||
-- Trim whitespace
|
||||
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
|
||||
|
||||
if additional_context == "" then
|
||||
M.close()
|
||||
return
|
||||
end
|
||||
|
||||
local original_event = state.original_event
|
||||
local callback = state.callback
|
||||
|
||||
M.close()
|
||||
|
||||
if callback and original_event then
|
||||
callback(original_event, additional_context)
|
||||
end
|
||||
end
|
||||
|
||||
--- Open the context modal
|
||||
---@param original_event table Original prompt event
|
||||
---@param llm_response string LLM's response asking for context
|
||||
---@param callback function(event: table, additional_context: string)
|
||||
function M.open(original_event, llm_response, callback)
|
||||
-- Close any existing modal
|
||||
M.close()
|
||||
|
||||
state.original_event = original_event
|
||||
state.llm_response = llm_response
|
||||
state.callback = callback
|
||||
|
||||
-- Calculate window size
|
||||
local width = math.min(80, vim.o.columns - 10)
|
||||
local height = 10
|
||||
|
||||
-- Create buffer
|
||||
state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.buf].buftype = "nofile"
|
||||
vim.bo[state.buf].bufhidden = "wipe"
|
||||
vim.bo[state.buf].filetype = "markdown"
|
||||
|
||||
-- Create window
|
||||
local row = math.floor((vim.o.lines - height) / 2)
|
||||
local col = math.floor((vim.o.columns - width) / 2)
|
||||
|
||||
state.win = vim.api.nvim_open_win(state.buf, true, {
|
||||
relative = "editor",
|
||||
row = row,
|
||||
col = col,
|
||||
width = width,
|
||||
height = height,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
title = " Additional Context Needed ",
|
||||
title_pos = "center",
|
||||
})
|
||||
|
||||
-- Set window options
|
||||
vim.wo[state.win].wrap = true
|
||||
vim.wo[state.win].cursorline = true
|
||||
|
||||
-- Add header showing what the LLM said
|
||||
local header_lines = {
|
||||
"-- LLM Response: --",
|
||||
}
|
||||
|
||||
-- Truncate LLM response for display
|
||||
local response_preview = llm_response or ""
|
||||
if #response_preview > 200 then
|
||||
response_preview = response_preview:sub(1, 200) .. "..."
|
||||
end
|
||||
for line in response_preview:gmatch("[^\n]+") do
|
||||
table.insert(header_lines, "-- " .. line)
|
||||
end
|
||||
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, "-- Enter additional context below (Ctrl-Enter to submit, Esc to cancel) --")
|
||||
table.insert(header_lines, "")
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
|
||||
|
||||
-- Move cursor to the end
|
||||
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
|
||||
|
||||
-- Set up keymaps
|
||||
local opts = { buffer = state.buf, noremap = true, silent = true }
|
||||
|
||||
-- Submit with Ctrl+Enter or <leader>s
|
||||
vim.keymap.set("n", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("i", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("n", "<leader>s", submit, opts)
|
||||
vim.keymap.set("n", "<CR><CR>", submit, opts)
|
||||
|
||||
-- Close with Esc or q
|
||||
vim.keymap.set("n", "<Esc>", M.close, opts)
|
||||
vim.keymap.set("n", "q", M.close, opts)
|
||||
|
||||
-- Start in insert mode
|
||||
vim.cmd("startinsert")
|
||||
|
||||
-- Log
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Context modal opened - waiting for user input",
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
--- Check if modal is open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
|
||||
end
|
||||
|
||||
--- Setup autocmds for the context modal
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
|
||||
|
||||
-- Close context modal when exiting Neovim
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
M.close()
|
||||
end,
|
||||
desc = "Close context modal before exiting Neovim",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,312 +0,0 @@
|
||||
---@mod codetyper.agent.intent Intent detection from prompts
|
||||
---@brief [[
|
||||
--- Parses prompt content to determine user intent and target scope.
|
||||
--- Intents determine how the generated code should be applied.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Intent
|
||||
---@field type string "complete"|"refactor"|"add"|"fix"|"document"|"test"|"explain"|"optimize"
|
||||
---@field scope_hint string|nil "function"|"class"|"block"|"file"|"selection"|nil
|
||||
---@field confidence number 0.0-1.0 how confident we are about the intent
|
||||
---@field action string "replace"|"insert"|"append"|"none"
|
||||
---@field keywords string[] Keywords that triggered this intent
|
||||
|
||||
--- Intent patterns with associated metadata
|
||||
local intent_patterns = {
|
||||
-- Complete: fill in missing implementation
|
||||
complete = {
|
||||
patterns = {
|
||||
"complete",
|
||||
"finish",
|
||||
"implement",
|
||||
"fill in",
|
||||
"fill out",
|
||||
"stub",
|
||||
"todo",
|
||||
"fixme",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 1,
|
||||
},
|
||||
|
||||
-- Refactor: rewrite existing code
|
||||
refactor = {
|
||||
patterns = {
|
||||
"refactor",
|
||||
"rewrite",
|
||||
"restructure",
|
||||
"reorganize",
|
||||
"clean up",
|
||||
"cleanup",
|
||||
"simplify",
|
||||
"improve",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 2,
|
||||
},
|
||||
|
||||
-- Fix: repair bugs or issues
|
||||
fix = {
|
||||
patterns = {
|
||||
"fix",
|
||||
"repair",
|
||||
"correct",
|
||||
"debug",
|
||||
"solve",
|
||||
"resolve",
|
||||
"patch",
|
||||
"bug",
|
||||
"error",
|
||||
"issue",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 1,
|
||||
},
|
||||
|
||||
-- Add: insert new code
|
||||
add = {
|
||||
patterns = {
|
||||
"add",
|
||||
"create",
|
||||
"insert",
|
||||
"include",
|
||||
"append",
|
||||
"new",
|
||||
"generate",
|
||||
"write",
|
||||
},
|
||||
scope_hint = nil, -- Could be anywhere
|
||||
action = "insert",
|
||||
priority = 3,
|
||||
},
|
||||
|
||||
-- Document: add documentation
|
||||
document = {
|
||||
patterns = {
|
||||
"document",
|
||||
"comment",
|
||||
"jsdoc",
|
||||
"docstring",
|
||||
"describe",
|
||||
"annotate",
|
||||
"type hint",
|
||||
"typehint",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace", -- Replace with documented version
|
||||
priority = 2,
|
||||
},
|
||||
|
||||
-- Test: generate tests
|
||||
test = {
|
||||
patterns = {
|
||||
"test",
|
||||
"spec",
|
||||
"unit test",
|
||||
"integration test",
|
||||
"coverage",
|
||||
},
|
||||
scope_hint = "file",
|
||||
action = "append",
|
||||
priority = 3,
|
||||
},
|
||||
|
||||
-- Optimize: improve performance
|
||||
optimize = {
|
||||
patterns = {
|
||||
"optimize",
|
||||
"performance",
|
||||
"faster",
|
||||
"efficient",
|
||||
"speed up",
|
||||
"reduce",
|
||||
"minimize",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 2,
|
||||
},
|
||||
|
||||
-- Explain: provide explanation (no code change)
|
||||
explain = {
|
||||
patterns = {
|
||||
"explain",
|
||||
"what does",
|
||||
"how does",
|
||||
"why",
|
||||
"describe",
|
||||
"walk through",
|
||||
"understand",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "none",
|
||||
priority = 4,
|
||||
},
|
||||
}
|
||||
|
||||
--- Scope hint patterns
|
||||
local scope_patterns = {
|
||||
["this function"] = "function",
|
||||
["this method"] = "function",
|
||||
["the function"] = "function",
|
||||
["the method"] = "function",
|
||||
["this class"] = "class",
|
||||
["the class"] = "class",
|
||||
["this file"] = "file",
|
||||
["the file"] = "file",
|
||||
["this block"] = "block",
|
||||
["the block"] = "block",
|
||||
["this"] = nil, -- Use Tree-sitter to determine
|
||||
["here"] = nil,
|
||||
}
|
||||
|
||||
--- Detect intent from prompt content
|
||||
---@param prompt string The prompt content
|
||||
---@return Intent
|
||||
function M.detect(prompt)
|
||||
local lower = prompt:lower()
|
||||
local best_match = nil
|
||||
local best_priority = 999
|
||||
local matched_keywords = {}
|
||||
|
||||
-- Check each intent type
|
||||
for intent_type, config in pairs(intent_patterns) do
|
||||
for _, pattern in ipairs(config.patterns) do
|
||||
if lower:find(pattern, 1, true) then
|
||||
if config.priority < best_priority then
|
||||
best_match = intent_type
|
||||
best_priority = config.priority
|
||||
matched_keywords = { pattern }
|
||||
elseif config.priority == best_priority and best_match == intent_type then
|
||||
table.insert(matched_keywords, pattern)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Default to "add" if no clear intent
|
||||
if not best_match then
|
||||
best_match = "add"
|
||||
matched_keywords = {}
|
||||
end
|
||||
|
||||
local config = intent_patterns[best_match]
|
||||
|
||||
-- Detect scope hint from prompt
|
||||
local scope_hint = config.scope_hint
|
||||
for pattern, hint in pairs(scope_patterns) do
|
||||
if lower:find(pattern, 1, true) then
|
||||
scope_hint = hint or scope_hint
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate confidence based on keyword matches
|
||||
local confidence = 0.5 + (#matched_keywords * 0.15)
|
||||
confidence = math.min(confidence, 1.0)
|
||||
|
||||
return {
|
||||
type = best_match,
|
||||
scope_hint = scope_hint,
|
||||
confidence = confidence,
|
||||
action = config.action,
|
||||
keywords = matched_keywords,
|
||||
}
|
||||
end
|
||||
|
||||
--- Check if intent requires code modification
|
||||
---@param intent Intent
|
||||
---@return boolean
|
||||
function M.modifies_code(intent)
|
||||
return intent.action ~= "none"
|
||||
end
|
||||
|
||||
--- Check if intent should replace existing code
|
||||
---@param intent Intent
|
||||
---@return boolean
|
||||
function M.is_replacement(intent)
|
||||
return intent.action == "replace"
|
||||
end
|
||||
|
||||
--- Check if intent adds new code
|
||||
---@param intent Intent
|
||||
---@return boolean
|
||||
function M.is_insertion(intent)
|
||||
return intent.action == "insert" or intent.action == "append"
|
||||
end
|
||||
|
||||
--- Get system prompt modifier based on intent
|
||||
---@param intent Intent
|
||||
---@return string
|
||||
function M.get_prompt_modifier(intent)
|
||||
local modifiers = {
|
||||
complete = [[
|
||||
You are completing an incomplete function.
|
||||
Return the complete function with all missing parts filled in.
|
||||
Keep the existing signature unless changes are required.
|
||||
Output only the code, no explanations.]],
|
||||
|
||||
refactor = [[
|
||||
You are refactoring existing code.
|
||||
Improve the code structure while maintaining the same behavior.
|
||||
Keep the function signature unchanged.
|
||||
Output only the refactored code, no explanations.]],
|
||||
|
||||
fix = [[
|
||||
You are fixing a bug in the code.
|
||||
Identify and correct the issue while minimizing changes.
|
||||
Preserve the original intent of the code.
|
||||
Output only the fixed code, no explanations.]],
|
||||
|
||||
add = [[
|
||||
You are adding new code.
|
||||
Follow the existing code style and conventions.
|
||||
Output only the new code to be inserted, no explanations.]],
|
||||
|
||||
document = [[
|
||||
You are adding documentation to the code.
|
||||
Add appropriate comments/docstrings for the function.
|
||||
Include parameter types, return types, and description.
|
||||
Output the complete function with documentation.]],
|
||||
|
||||
test = [[
|
||||
You are generating tests for the code.
|
||||
Create comprehensive unit tests covering edge cases.
|
||||
Follow the testing conventions of the project.
|
||||
Output only the test code, no explanations.]],
|
||||
|
||||
optimize = [[
|
||||
You are optimizing code for performance.
|
||||
Improve efficiency while maintaining correctness.
|
||||
Document any significant algorithmic changes.
|
||||
Output only the optimized code, no explanations.]],
|
||||
|
||||
explain = [[
|
||||
You are explaining code to a developer.
|
||||
Provide a clear, concise explanation of what the code does.
|
||||
Include information about the algorithm and any edge cases.
|
||||
Do not output code, only explanation.]],
|
||||
}
|
||||
|
||||
return modifiers[intent.type] or modifiers.add
|
||||
end
|
||||
|
||||
--- Format intent for logging
|
||||
---@param intent Intent
|
||||
---@return string
|
||||
function M.format(intent)
|
||||
return string.format(
|
||||
"%s (scope: %s, action: %s, confidence: %.2f)",
|
||||
intent.type,
|
||||
intent.scope_hint or "auto",
|
||||
intent.action,
|
||||
intent.confidence
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,222 +0,0 @@
|
||||
---@mod codetyper.agent.tools Tool definitions for the agent system
|
||||
---
|
||||
--- Defines available tools that the LLM can use to interact with files and system.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Tool definitions in a provider-agnostic format
|
||||
M.definitions = {
|
||||
read_file = {
|
||||
name = "read_file",
|
||||
description = "Read the contents of a file at the specified path",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
path = {
|
||||
type = "string",
|
||||
description = "Absolute or relative path to the file to read",
|
||||
},
|
||||
},
|
||||
required = { "path" },
|
||||
},
|
||||
},
|
||||
|
||||
edit_file = {
|
||||
name = "edit_file",
|
||||
description = "Edit a file by replacing specific content. Provide the exact content to find and the replacement.",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
path = {
|
||||
type = "string",
|
||||
description = "Path to the file to edit",
|
||||
},
|
||||
find = {
|
||||
type = "string",
|
||||
description = "Exact content to find (must match exactly, including whitespace)",
|
||||
},
|
||||
replace = {
|
||||
type = "string",
|
||||
description = "Content to replace with",
|
||||
},
|
||||
},
|
||||
required = { "path", "find", "replace" },
|
||||
},
|
||||
},
|
||||
|
||||
write_file = {
|
||||
name = "write_file",
|
||||
description = "Write content to a file, creating it if it doesn't exist or overwriting if it does",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
path = {
|
||||
type = "string",
|
||||
description = "Path to the file to write",
|
||||
},
|
||||
content = {
|
||||
type = "string",
|
||||
description = "Complete file content to write",
|
||||
},
|
||||
},
|
||||
required = { "path", "content" },
|
||||
},
|
||||
},
|
||||
|
||||
bash = {
|
||||
name = "bash",
|
||||
description = "Execute a bash command and return the output. Use for git, npm, build tools, etc.",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
command = {
|
||||
type = "string",
|
||||
description = "The bash command to execute",
|
||||
},
|
||||
timeout = {
|
||||
type = "number",
|
||||
description = "Timeout in milliseconds (default: 30000)",
|
||||
},
|
||||
},
|
||||
required = { "command" },
|
||||
},
|
||||
},
|
||||
|
||||
delete_file = {
|
||||
name = "delete_file",
|
||||
description = "Delete a file from the filesystem. Use with caution - requires explicit user approval.",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
path = {
|
||||
type = "string",
|
||||
description = "Path to the file to delete",
|
||||
},
|
||||
reason = {
|
||||
type = "string",
|
||||
description = "Reason for deleting this file (shown to user for approval)",
|
||||
},
|
||||
},
|
||||
required = { "path", "reason" },
|
||||
},
|
||||
},
|
||||
|
||||
list_directory = {
|
||||
name = "list_directory",
|
||||
description = "List files and directories in a path. Use to explore project structure.",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
path = {
|
||||
type = "string",
|
||||
description = "Path to the directory to list (defaults to current directory)",
|
||||
},
|
||||
recursive = {
|
||||
type = "boolean",
|
||||
description = "Whether to list recursively (default: false, max depth: 3)",
|
||||
},
|
||||
},
|
||||
required = {},
|
||||
},
|
||||
},
|
||||
|
||||
search_files = {
|
||||
name = "search_files",
|
||||
description = "Search for files by name pattern or content. Use to find relevant files in the project.",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {
|
||||
pattern = {
|
||||
type = "string",
|
||||
description = "Glob pattern for file names (e.g., '*.lua', 'test_*.py')",
|
||||
},
|
||||
content = {
|
||||
type = "string",
|
||||
description = "Search for files containing this text",
|
||||
},
|
||||
path = {
|
||||
type = "string",
|
||||
description = "Directory to search in (defaults to project root)",
|
||||
},
|
||||
},
|
||||
required = {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
--- Convert tool definitions to Claude API format
|
||||
---@return table[] Tools in Claude's expected format
|
||||
function M.to_claude_format()
|
||||
local tools = {}
|
||||
for _, tool in pairs(M.definitions) do
|
||||
table.insert(tools, {
|
||||
name = tool.name,
|
||||
description = tool.description,
|
||||
input_schema = tool.parameters,
|
||||
})
|
||||
end
|
||||
return tools
|
||||
end
|
||||
|
||||
--- Convert tool definitions to OpenAI API format
|
||||
---@return table[] Tools in OpenAI's expected format
|
||||
function M.to_openai_format()
|
||||
local tools = {}
|
||||
for _, tool in pairs(M.definitions) do
|
||||
table.insert(tools, {
|
||||
type = "function",
|
||||
["function"] = {
|
||||
name = tool.name,
|
||||
description = tool.description,
|
||||
parameters = tool.parameters,
|
||||
},
|
||||
})
|
||||
end
|
||||
return tools
|
||||
end
|
||||
|
||||
--- Convert tool definitions to prompt format for Ollama
|
||||
---@return string Formatted tool descriptions for system prompt
|
||||
function M.to_prompt_format()
|
||||
local lines = {
|
||||
"You have access to the following tools. To use a tool, respond with a JSON block.",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, tool in pairs(M.definitions) do
|
||||
table.insert(lines, "## " .. tool.name)
|
||||
table.insert(lines, tool.description)
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Parameters:")
|
||||
for prop_name, prop in pairs(tool.parameters.properties) do
|
||||
local required = vim.tbl_contains(tool.parameters.required or {}, prop_name)
|
||||
local req_str = required and " (required)" or " (optional)"
|
||||
table.insert(lines, " - " .. prop_name .. ": " .. prop.description .. req_str)
|
||||
end
|
||||
table.insert(lines, "")
|
||||
end
|
||||
|
||||
table.insert(lines, "---")
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "To call a tool, output a JSON block like this:")
|
||||
table.insert(lines, "```json")
|
||||
table.insert(lines, '{"tool": "tool_name", "parameters": {"param1": "value1"}}')
|
||||
table.insert(lines, "```")
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "After receiving tool results, continue your response or call another tool.")
|
||||
table.insert(lines, "When you're done, just respond normally without any tool calls.")
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Get a list of tool names
|
||||
---@return string[]
|
||||
function M.get_tool_names()
|
||||
local names = {}
|
||||
for name, _ in pairs(M.definitions) do
|
||||
table.insert(names, name)
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
return M
|
||||
18
lua/codetyper/commands/agents/banned.lua
Normal file
18
lua/codetyper/commands/agents/banned.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
--- Banned commands for safety
|
||||
M.BANNED_COMMANDS = {
|
||||
"rm -rf /",
|
||||
"rm -rf /*",
|
||||
"dd if=/dev/zero",
|
||||
"mkfs",
|
||||
":(){ :|:& };:",
|
||||
"> /dev/sda",
|
||||
}
|
||||
|
||||
--- Banned patterns
|
||||
M.BANNED_PATTERNS = {
|
||||
"curl.*|.*sh",
|
||||
"wget.*|.*sh",
|
||||
"rm%s+%-rf%s+/",
|
||||
}
|
||||
|
||||
return M
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Get the credentials file path
|
||||
---@return string Path to credentials file
|
||||
@@ -221,10 +221,61 @@ M.default_models = {
|
||||
claude = "claude-sonnet-4-20250514",
|
||||
openai = "gpt-4o",
|
||||
gemini = "gemini-2.0-flash",
|
||||
copilot = "gpt-4o",
|
||||
copilot = "claude-sonnet-4",
|
||||
ollama = "deepseek-coder:6.7b",
|
||||
}
|
||||
|
||||
--- Available models for Copilot (GitHub Copilot Chat API)
|
||||
--- Models with cost multipliers: 0x = free, 0.33x = discount, 1x = standard, 3x = premium
|
||||
M.copilot_models = {
|
||||
-- Free tier (0x)
|
||||
{ name = "gpt-4.1", cost = "0x" },
|
||||
{ name = "gpt-4o", cost = "0x" },
|
||||
{ name = "gpt-5-mini", cost = "0x" },
|
||||
{ name = "grok-code-fast-1", cost = "0x" },
|
||||
{ name = "raptor-mini", cost = "0x" },
|
||||
-- Discount tier (0.33x)
|
||||
{ name = "claude-haiku-4.5", cost = "0.33x" },
|
||||
{ name = "gemini-3-flash", cost = "0.33x" },
|
||||
{ name = "gpt-5.1-codex-mini", cost = "0.33x" },
|
||||
-- Standard tier (1x)
|
||||
{ name = "claude-sonnet-4", cost = "1x" },
|
||||
{ name = "claude-sonnet-4.5", cost = "1x" },
|
||||
{ name = "gemini-2.5-pro", cost = "1x" },
|
||||
{ name = "gemini-3-pro", cost = "1x" },
|
||||
{ name = "gpt-5", cost = "1x" },
|
||||
{ name = "gpt-5-codex", cost = "1x" },
|
||||
{ name = "gpt-5.1", cost = "1x" },
|
||||
{ name = "gpt-5.1-codex", cost = "1x" },
|
||||
{ name = "gpt-5.1-codex-max", cost = "1x" },
|
||||
{ name = "gpt-5.2", cost = "1x" },
|
||||
{ name = "gpt-5.2-codex", cost = "1x" },
|
||||
-- Premium tier (3x)
|
||||
{ name = "claude-opus-4.5", cost = "3x" },
|
||||
}
|
||||
|
||||
--- Get list of copilot model names (for completion)
|
||||
---@return string[]
|
||||
function M.get_copilot_model_names()
|
||||
local names = {}
|
||||
for _, model in ipairs(M.copilot_models) do
|
||||
table.insert(names, model.name)
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
--- Get cost for a copilot model
|
||||
---@param model_name string
|
||||
---@return string|nil
|
||||
function M.get_copilot_model_cost(model_name)
|
||||
for _, model in ipairs(M.copilot_models) do
|
||||
if model.name == model_name then
|
||||
return model.cost
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Interactive command to add/update API key
|
||||
function M.interactive_add()
|
||||
local providers = { "claude", "openai", "gemini", "copilot", "ollama" }
|
||||
@@ -277,28 +328,56 @@ function M.interactive_api_key(provider)
|
||||
end
|
||||
|
||||
--- Interactive Copilot configuration (no API key, uses OAuth)
|
||||
function M.interactive_copilot_config()
|
||||
utils.notify("Copilot uses OAuth from copilot.lua/copilot.vim - no API key needed", vim.log.levels.INFO)
|
||||
---@param silent? boolean If true, don't show the OAuth info message
|
||||
function M.interactive_copilot_config(silent)
|
||||
if not silent then
|
||||
utils.notify("Copilot uses OAuth from copilot.lua/copilot.vim - no API key needed", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Just ask for model
|
||||
local default_model = M.default_models.copilot
|
||||
vim.ui.input({
|
||||
prompt = string.format("Copilot model (default: %s): ", default_model),
|
||||
default = default_model,
|
||||
}, function(model)
|
||||
if model == nil then
|
||||
-- Get current model if configured
|
||||
local current_model = M.get_model("copilot") or M.default_models.copilot
|
||||
local current_cost = M.get_copilot_model_cost(current_model) or "?"
|
||||
|
||||
-- Build model options with "Custom..." option
|
||||
local model_options = vim.deepcopy(M.copilot_models)
|
||||
table.insert(model_options, { name = "Custom...", cost = "" })
|
||||
|
||||
vim.ui.select(model_options, {
|
||||
prompt = "Select Copilot model (current: " .. current_model .. " — " .. current_cost .. "):",
|
||||
format_item = function(item)
|
||||
local display = item.name
|
||||
if item.cost and item.cost ~= "" then
|
||||
display = display .. " — " .. item.cost
|
||||
end
|
||||
if item.name == current_model then
|
||||
display = display .. " [current]"
|
||||
end
|
||||
return display
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice == nil then
|
||||
return -- Cancelled
|
||||
end
|
||||
|
||||
if model == "" then
|
||||
model = default_model
|
||||
if choice.name == "Custom..." then
|
||||
-- Allow custom model input
|
||||
vim.ui.input({
|
||||
prompt = "Enter custom model name: ",
|
||||
default = current_model,
|
||||
}, function(custom_model)
|
||||
if custom_model and custom_model ~= "" then
|
||||
M.save_and_notify("copilot", {
|
||||
model = custom_model,
|
||||
configured = true,
|
||||
})
|
||||
end
|
||||
end)
|
||||
else
|
||||
M.save_and_notify("copilot", {
|
||||
model = choice.name,
|
||||
configured = true,
|
||||
})
|
||||
end
|
||||
|
||||
M.save_and_notify("copilot", {
|
||||
model = model,
|
||||
-- Mark as configured even without API key
|
||||
configured = true,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ local defaults = {
|
||||
model = "gemini-2.0-flash",
|
||||
},
|
||||
copilot = {
|
||||
model = "gpt-4o", -- Uses GitHub Copilot authentication
|
||||
model = "claude-sonnet-4", -- Uses GitHub Copilot authentication
|
||||
},
|
||||
},
|
||||
window = {
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
---@class CoderPreferences
|
||||
---@field auto_process boolean Whether to auto-process /@ @/ tags (default: nil = ask)
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Cost history file name
|
||||
local COST_HISTORY_FILE = "cost_history.json"
|
||||
@@ -100,7 +100,7 @@ M.pricing = {
|
||||
["gpt-image-1"] = { input = 5.00, cached_input = 1.25, output = nil },
|
||||
["gpt-image-1-mini"] = { input = 2.00, cached_input = 0.20, output = nil },
|
||||
|
||||
-- Claude models (Anthropic)
|
||||
-- Claude models
|
||||
["claude-3-opus"] = { input = 15.00, cached_input = 7.50, output = 75.00 },
|
||||
["claude-3-sonnet"] = { input = 3.00, cached_input = 1.50, output = 15.00 },
|
||||
["claude-3-haiku"] = { input = 0.25, cached_input = 0.125, output = 1.25 },
|
||||
1052
lua/codetyper/core/diff/conflict.lua
Normal file
1052
lua/codetyper/core/diff/conflict.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -157,17 +157,19 @@ function M.show_diff(diff_data, callback)
|
||||
end
|
||||
|
||||
-- Show help message
|
||||
vim.api.nvim_echo({
|
||||
{ "Diff: ", "Normal" },
|
||||
{ diff_data.path, "Directory" },
|
||||
{ " | ", "Normal" },
|
||||
{ "y/<CR>", "Keyword" },
|
||||
{ " approve ", "Normal" },
|
||||
{ "n/q/<Esc>", "Keyword" },
|
||||
{ " reject ", "Normal" },
|
||||
{ "<Tab>", "Keyword" },
|
||||
{ " switch panes", "Normal" },
|
||||
}, false, {})
|
||||
local help_msg = require("codetyper.prompts.agents.diff").diff_help
|
||||
|
||||
-- Iterate to replace {path} variable
|
||||
local final_help = {}
|
||||
for _, item in ipairs(help_msg) do
|
||||
if item[1] == "{path}" then
|
||||
table.insert(final_help, { diff_data.path, item[2] })
|
||||
else
|
||||
table.insert(final_help, item)
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_echo(final_help, false, {})
|
||||
end
|
||||
|
||||
---@alias BashApprovalResult {approved: boolean, permission_level: string|nil}
|
||||
@@ -176,7 +178,7 @@ end
|
||||
---@param command string The bash command to approve
|
||||
---@param callback fun(result: BashApprovalResult) Called with user decision
|
||||
function M.show_bash_approval(command, callback)
|
||||
local permissions = require("codetyper.agent.permissions")
|
||||
local permissions = require("codetyper.features.agents.permissions")
|
||||
|
||||
-- Check if command is auto-approved
|
||||
local perm_result = permissions.check_bash_permission(command)
|
||||
@@ -188,31 +190,31 @@ function M.show_bash_approval(command, callback)
|
||||
end
|
||||
|
||||
-- Create approval dialog with options
|
||||
local approval_prompts = require("codetyper.prompts.agents.diff").bash_approval
|
||||
local lines = {
|
||||
"",
|
||||
" BASH COMMAND APPROVAL",
|
||||
" " .. string.rep("─", 56),
|
||||
approval_prompts.title,
|
||||
approval_prompts.divider,
|
||||
"",
|
||||
" Command:",
|
||||
approval_prompts.command_label,
|
||||
" $ " .. command,
|
||||
"",
|
||||
}
|
||||
|
||||
-- Add warning for dangerous commands
|
||||
if not perm_result.allowed and perm_result.reason ~= "Requires approval" then
|
||||
table.insert(lines, " ⚠️ WARNING: " .. perm_result.reason)
|
||||
table.insert(lines, approval_prompts.warning_prefix .. perm_result.reason)
|
||||
table.insert(lines, "")
|
||||
end
|
||||
|
||||
table.insert(lines, " " .. string.rep("─", 56))
|
||||
table.insert(lines, approval_prompts.divider)
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, " [y] Allow once - Execute this command")
|
||||
table.insert(lines, " [s] Allow this session - Auto-allow until restart")
|
||||
table.insert(lines, " [a] Add to allow list - Always allow this command")
|
||||
table.insert(lines, " [n] Reject - Cancel execution")
|
||||
for _, opt in ipairs(approval_prompts.options) do
|
||||
table.insert(lines, opt)
|
||||
end
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, " " .. string.rep("─", 56))
|
||||
table.insert(lines, " Press key to choose | [q] or [Esc] to cancel")
|
||||
table.insert(lines, approval_prompts.divider)
|
||||
table.insert(lines, approval_prompts.cancel_hint)
|
||||
table.insert(lines, "")
|
||||
|
||||
local width = math.max(65, #command + 15)
|
||||
@@ -2,16 +2,32 @@
|
||||
---@brief [[
|
||||
--- Manages code patches with buffer snapshots for staleness detection.
|
||||
--- Patches are queued for safe injection when completion popup is not visible.
|
||||
--- Uses smart injection for intelligent import merging.
|
||||
--- Uses SEARCH/REPLACE blocks for reliable code editing.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
local params = require("codetyper.params.agents.patch")
|
||||
|
||||
|
||||
--- Lazy load inject module to avoid circular requires
|
||||
local function get_inject_module()
|
||||
return require("codetyper.agent.inject")
|
||||
return require("codetyper.inject")
|
||||
end
|
||||
|
||||
--- Lazy load search_replace module
|
||||
local function get_search_replace_module()
|
||||
return require("codetyper.core.diff.search_replace")
|
||||
end
|
||||
|
||||
--- Lazy load conflict module
|
||||
local function get_conflict_module()
|
||||
return require("codetyper.core.diff.conflict")
|
||||
end
|
||||
|
||||
--- Configuration for patch behavior
|
||||
local config = params.config
|
||||
|
||||
---@class BufferSnapshot
|
||||
---@field bufnr number Buffer number
|
||||
---@field changedtick number vim.b.changedtick at snapshot time
|
||||
@@ -27,11 +43,13 @@ end
|
||||
---@field original_snapshot BufferSnapshot Snapshot at event creation
|
||||
---@field generated_code string Code to inject
|
||||
---@field injection_range {start_line: number, end_line: number}|nil
|
||||
---@field injection_strategy string "append"|"replace"|"insert"
|
||||
---@field injection_strategy string "append"|"replace"|"insert"|"search_replace"
|
||||
---@field confidence number Confidence score (0.0-1.0)
|
||||
---@field status string "pending"|"applied"|"stale"|"rejected"
|
||||
---@field created_at number Timestamp
|
||||
---@field applied_at number|nil When applied
|
||||
---@field use_search_replace boolean Whether to use SEARCH/REPLACE block parsing
|
||||
---@field search_replace_blocks table[]|nil Parsed SEARCH/REPLACE blocks
|
||||
|
||||
--- Patch storage
|
||||
---@type PatchCandidate[]
|
||||
@@ -152,7 +170,7 @@ function M.queue_patch(patch)
|
||||
|
||||
-- Log patch creation
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "patch",
|
||||
message = string.format(
|
||||
@@ -194,6 +212,10 @@ function M.create_from_event(event, generated_code, confidence, strategy)
|
||||
end
|
||||
end
|
||||
|
||||
-- Detect if this is an inline prompt (source == target, not a .coder. file)
|
||||
local is_inline = (source_bufnr == target_bufnr) or
|
||||
(event.target_path and not event.target_path:match("%.coder%."))
|
||||
|
||||
-- Take snapshot of the scope range in target buffer (for staleness detection)
|
||||
local snapshot_range = event.scope_range or event.range
|
||||
local snapshot = M.snapshot_buffer(
|
||||
@@ -201,22 +223,67 @@ function M.create_from_event(event, generated_code, confidence, strategy)
|
||||
snapshot_range
|
||||
)
|
||||
|
||||
-- Check if the response contains SEARCH/REPLACE blocks
|
||||
local search_replace = get_search_replace_module()
|
||||
local sr_blocks = search_replace.parse_blocks(generated_code)
|
||||
local use_search_replace = #sr_blocks > 0
|
||||
|
||||
-- Determine injection strategy and range based on intent
|
||||
local injection_strategy = strategy
|
||||
local injection_range = nil
|
||||
|
||||
if not injection_strategy and event.intent then
|
||||
local intent_mod = require("codetyper.agent.intent")
|
||||
-- If we have SEARCH/REPLACE blocks, use that strategy
|
||||
if use_search_replace then
|
||||
injection_strategy = "search_replace"
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Using SEARCH/REPLACE mode with %d block(s)", #sr_blocks),
|
||||
})
|
||||
end)
|
||||
elseif not injection_strategy and event.intent then
|
||||
local intent_mod = require("codetyper.core.intent")
|
||||
if intent_mod.is_replacement(event.intent) then
|
||||
injection_strategy = "replace"
|
||||
-- Use scope range for replacement
|
||||
if event.scope_range then
|
||||
|
||||
-- INLINE PROMPTS: Always use tag range
|
||||
-- The LLM is told specifically to replace the tagged region
|
||||
if is_inline and event.range then
|
||||
injection_range = {
|
||||
start_line = event.range.start_line,
|
||||
end_line = event.range.end_line,
|
||||
}
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Inline prompt: will replace tag region (lines %d-%d)",
|
||||
event.range.start_line, event.range.end_line),
|
||||
})
|
||||
end)
|
||||
-- CODER FILES: Use scope range for replacement
|
||||
elseif event.scope_range then
|
||||
injection_range = event.scope_range
|
||||
else
|
||||
-- Fallback: no scope found (treesitter didn't find function)
|
||||
-- Use tag range - the generated code will replace the tag region
|
||||
injection_range = {
|
||||
start_line = event.range.start_line,
|
||||
end_line = event.range.end_line,
|
||||
}
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "warning",
|
||||
message = "No scope found, using tag range as fallback",
|
||||
})
|
||||
end)
|
||||
end
|
||||
elseif event.intent.action == "insert" then
|
||||
injection_strategy = "insert"
|
||||
-- Insert at prompt location
|
||||
injection_range = { start_line = event.range.start_line, end_line = event.range.start_line }
|
||||
-- Insert at prompt location (use full tag range)
|
||||
injection_range = { start_line = event.range.start_line, end_line = event.range.end_line }
|
||||
elseif event.intent.action == "append" then
|
||||
injection_strategy = "append"
|
||||
-- Will append to end of file
|
||||
@@ -244,6 +311,11 @@ function M.create_from_event(event, generated_code, confidence, strategy)
|
||||
scope = event.scope,
|
||||
-- Store the prompt tag range so we can delete it after applying
|
||||
prompt_tag_range = event.range,
|
||||
-- Mark if this is an inline prompt (tags in source file, not coder file)
|
||||
is_inline_prompt = is_inline,
|
||||
-- SEARCH/REPLACE support
|
||||
use_search_replace = use_search_replace,
|
||||
search_replace_blocks = use_search_replace and sr_blocks or nil,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -438,7 +510,7 @@ function M.apply(patch)
|
||||
M.mark_stale(patch.id, stale_reason)
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "warning",
|
||||
message = string.format("Patch %s is stale: %s", patch.id, stale_reason or "unknown"),
|
||||
@@ -464,18 +536,20 @@ function M.apply(patch)
|
||||
-- Prepare code lines
|
||||
local code_lines = vim.split(patch.generated_code, "\n", { plain = true })
|
||||
|
||||
-- FIRST: Remove the prompt tags from the SOURCE buffer (coder file), not target
|
||||
-- The tags are in the coder file where the user wrote the prompt
|
||||
-- Code goes to target file, tags get removed from source file
|
||||
-- Use the stored inline prompt flag (computed during patch creation)
|
||||
-- For inline prompts, we replace the tag region directly instead of separate remove + inject
|
||||
local source_bufnr = patch.source_bufnr
|
||||
local is_inline_prompt = patch.is_inline_prompt or (source_bufnr == target_bufnr)
|
||||
local tags_removed = 0
|
||||
|
||||
if source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
|
||||
-- For CODER FILES (source != target): Remove tags from source, inject into target
|
||||
-- For INLINE PROMPTS (source == target): Include tag range in injection, no separate removal
|
||||
if not is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
|
||||
tags_removed = remove_prompt_tags(source_bufnr)
|
||||
|
||||
pcall(function()
|
||||
if tags_removed > 0 then
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local source_name = vim.api.nvim_buf_get_name(source_bufnr)
|
||||
logs.add({
|
||||
type = "info",
|
||||
@@ -490,6 +564,76 @@ function M.apply(patch)
|
||||
-- Get filetype for smart injection
|
||||
local filetype = vim.fn.fnamemodify(patch.target_path or "", ":e")
|
||||
|
||||
-- SEARCH/REPLACE MODE: Use fuzzy matching to find and replace text
|
||||
if patch.use_search_replace and patch.search_replace_blocks and #patch.search_replace_blocks > 0 then
|
||||
local search_replace = get_search_replace_module()
|
||||
|
||||
-- Remove the /@ @/ tags first (they shouldn't be in the file anymore)
|
||||
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
|
||||
tags_removed = remove_prompt_tags(source_bufnr)
|
||||
if tags_removed > 0 then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Removed %d prompt tag(s)", tags_removed),
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply SEARCH/REPLACE blocks
|
||||
local success, err = search_replace.apply_to_buffer(target_bufnr, patch.search_replace_blocks)
|
||||
|
||||
if success then
|
||||
M.mark_applied(patch.id)
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "success",
|
||||
message = string.format("Patch %s applied via SEARCH/REPLACE (%d block(s))",
|
||||
patch.id, #patch.search_replace_blocks),
|
||||
data = {
|
||||
target_path = patch.target_path,
|
||||
blocks_applied = #patch.search_replace_blocks,
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
-- Learn from successful code generation
|
||||
pcall(function()
|
||||
local brain = require("codetyper.core.memory")
|
||||
if brain.is_initialized() then
|
||||
local intent_type = patch.intent and patch.intent.type or "unknown"
|
||||
brain.learn({
|
||||
type = "code_completion",
|
||||
file = patch.target_path,
|
||||
timestamp = os.time(),
|
||||
data = {
|
||||
intent = intent_type,
|
||||
method = "search_replace",
|
||||
language = filetype,
|
||||
confidence = patch.confidence or 0.5,
|
||||
},
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
return true, nil
|
||||
else
|
||||
-- SEARCH/REPLACE failed, log the error
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "warning",
|
||||
message = string.format("SEARCH/REPLACE failed: %s. Falling back to line-based injection.", err or "unknown"),
|
||||
})
|
||||
end)
|
||||
-- Fall through to line-based injection as fallback
|
||||
end
|
||||
end
|
||||
|
||||
-- Use smart injection module for intelligent import handling
|
||||
local inject = get_inject_module()
|
||||
local inject_result = nil
|
||||
@@ -508,10 +652,10 @@ function M.apply(patch)
|
||||
local start_line = patch.injection_range.start_line
|
||||
local end_line = patch.injection_range.end_line
|
||||
|
||||
-- Adjust for tag removal - find the new range by searching for the scope
|
||||
-- After removing tags, line numbers may have shifted
|
||||
if patch.scope and patch.scope.type then
|
||||
-- Try to find the scope using treesitter if available
|
||||
-- For inline prompts, use scope range directly (tags are inside scope)
|
||||
-- No adjustment needed since we didn't remove tags yet
|
||||
if not is_inline_prompt and patch.scope and patch.scope.type then
|
||||
-- For coder files, tags were already removed, so we may need to find the scope again
|
||||
local found_range = nil
|
||||
pcall(function()
|
||||
local parsers = require("nvim-treesitter.parsers")
|
||||
@@ -562,7 +706,31 @@ function M.apply(patch)
|
||||
|
||||
inject_opts.range = { start_line = start_line, end_line = end_line }
|
||||
elseif patch.injection_strategy == "insert" and patch.injection_range then
|
||||
inject_opts.range = { start_line = patch.injection_range.start_line }
|
||||
-- For inline prompts with "insert" strategy, replace the TAG RANGE
|
||||
-- (the tag itself gets replaced with the new code)
|
||||
if is_inline_prompt and patch.prompt_tag_range then
|
||||
inject_opts.range = {
|
||||
start_line = patch.prompt_tag_range.start_line,
|
||||
end_line = patch.prompt_tag_range.end_line
|
||||
}
|
||||
-- Switch to replace strategy for the tag range
|
||||
inject_opts.strategy = "replace"
|
||||
else
|
||||
inject_opts.range = { start_line = patch.injection_range.start_line }
|
||||
end
|
||||
end
|
||||
|
||||
-- Log inline prompt handling
|
||||
if is_inline_prompt then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Inline prompt: replacing lines %d-%d",
|
||||
inject_opts.range and inject_opts.range.start_line or 0,
|
||||
inject_opts.range and inject_opts.range.end_line or 0),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
-- Use smart injection - handles imports automatically
|
||||
@@ -570,7 +738,7 @@ function M.apply(patch)
|
||||
|
||||
-- Log injection details
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
if inject_result.imports_added > 0 then
|
||||
logs.add({
|
||||
type = "info",
|
||||
@@ -598,7 +766,7 @@ function M.apply(patch)
|
||||
M.mark_applied(patch.id)
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "success",
|
||||
message = string.format("Patch %s applied successfully", patch.id),
|
||||
@@ -612,7 +780,7 @@ function M.apply(patch)
|
||||
-- Learn from successful code generation - this builds neural pathways
|
||||
-- The more code is successfully applied, the better the brain becomes
|
||||
pcall(function()
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
if brain.is_initialized() then
|
||||
-- Learn the successful pattern
|
||||
local intent_type = patch.intent and patch.intent.type or "unknown"
|
||||
@@ -729,4 +897,202 @@ function M.clear()
|
||||
patches = {}
|
||||
end
|
||||
|
||||
--- Configure patch behavior
|
||||
---@param opts table Configuration options
|
||||
--- - use_conflict_mode: boolean Use conflict markers instead of direct apply
|
||||
--- - auto_jump_to_conflict: boolean Auto-jump to first conflict after applying
|
||||
function M.configure(opts)
|
||||
if opts.use_conflict_mode ~= nil then
|
||||
config.use_conflict_mode = opts.use_conflict_mode
|
||||
end
|
||||
if opts.auto_jump_to_conflict ~= nil then
|
||||
config.auto_jump_to_conflict = opts.auto_jump_to_conflict
|
||||
end
|
||||
end
|
||||
|
||||
--- Get current configuration
|
||||
---@return table
|
||||
function M.get_config()
|
||||
return vim.deepcopy(config)
|
||||
end
|
||||
|
||||
--- Check if conflict mode is enabled
|
||||
---@return boolean
|
||||
function M.is_conflict_mode()
|
||||
return config.use_conflict_mode
|
||||
end
|
||||
|
||||
--- Apply a patch using conflict markers for interactive review
|
||||
--- Instead of directly replacing code, inserts git-style conflict markers
|
||||
---@param patch PatchCandidate
|
||||
---@return boolean success
|
||||
---@return string|nil error
|
||||
function M.apply_with_conflict(patch)
|
||||
-- Check if safe to modify (not in insert mode)
|
||||
if not is_safe_to_modify() then
|
||||
return false, "user_typing"
|
||||
end
|
||||
|
||||
-- Check staleness first
|
||||
local is_stale, stale_reason = M.is_stale(patch)
|
||||
if is_stale then
|
||||
M.mark_stale(patch.id, stale_reason)
|
||||
return false, "patch_stale: " .. (stale_reason or "unknown")
|
||||
end
|
||||
|
||||
-- Ensure target buffer is valid
|
||||
local target_bufnr = patch.target_bufnr
|
||||
if target_bufnr == -1 or not vim.api.nvim_buf_is_valid(target_bufnr) then
|
||||
target_bufnr = vim.fn.bufadd(patch.target_path)
|
||||
if target_bufnr == 0 then
|
||||
M.mark_rejected(patch.id, "buffer_not_found")
|
||||
return false, "target buffer not found"
|
||||
end
|
||||
vim.fn.bufload(target_bufnr)
|
||||
patch.target_bufnr = target_bufnr
|
||||
end
|
||||
|
||||
local conflict = get_conflict_module()
|
||||
local source_bufnr = patch.source_bufnr
|
||||
local is_inline_prompt = patch.is_inline_prompt or (source_bufnr == target_bufnr)
|
||||
|
||||
-- Remove tags from coder files
|
||||
if not is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
|
||||
remove_prompt_tags(source_bufnr)
|
||||
end
|
||||
|
||||
-- For SEARCH/REPLACE blocks, convert each block to a conflict
|
||||
if patch.use_search_replace and patch.search_replace_blocks and #patch.search_replace_blocks > 0 then
|
||||
local search_replace = get_search_replace_module()
|
||||
local content = table.concat(vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false), "\n")
|
||||
local applied_count = 0
|
||||
|
||||
-- Sort blocks by position (bottom to top) to maintain line numbers
|
||||
local sorted_blocks = {}
|
||||
for _, block in ipairs(patch.search_replace_blocks) do
|
||||
local match = search_replace.find_match(content, block.search)
|
||||
if match then
|
||||
block._match = match
|
||||
table.insert(sorted_blocks, block)
|
||||
end
|
||||
end
|
||||
table.sort(sorted_blocks, function(a, b)
|
||||
return (a._match and a._match.start_line or 0) > (b._match and b._match.start_line or 0)
|
||||
end)
|
||||
|
||||
-- Apply each block as a conflict
|
||||
for _, block in ipairs(sorted_blocks) do
|
||||
local match = block._match
|
||||
if match then
|
||||
local new_lines = vim.split(block.replace, "\n", { plain = true })
|
||||
conflict.insert_conflict(
|
||||
target_bufnr,
|
||||
match.start_line,
|
||||
match.end_line,
|
||||
new_lines,
|
||||
"AI SUGGESTION"
|
||||
)
|
||||
applied_count = applied_count + 1
|
||||
-- Re-read content for next match (line numbers changed)
|
||||
content = table.concat(vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false), "\n")
|
||||
end
|
||||
end
|
||||
|
||||
if applied_count > 0 then
|
||||
-- Remove tags for inline prompts after inserting conflicts
|
||||
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
|
||||
remove_prompt_tags(source_bufnr)
|
||||
end
|
||||
|
||||
-- Process conflicts (highlight, keymaps) and show menu
|
||||
conflict.process_and_show_menu(target_bufnr)
|
||||
|
||||
M.mark_applied(patch.id)
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "success",
|
||||
message = string.format(
|
||||
"Created %d conflict(s) for review - use co/ct/cb/cn to resolve",
|
||||
applied_count
|
||||
),
|
||||
})
|
||||
end)
|
||||
|
||||
return true, nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: Use injection range if available
|
||||
if patch.injection_range then
|
||||
local start_line = patch.injection_range.start_line
|
||||
local end_line = patch.injection_range.end_line
|
||||
local new_lines = vim.split(patch.generated_code, "\n", { plain = true })
|
||||
|
||||
-- Remove tags for inline prompts
|
||||
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
|
||||
remove_prompt_tags(source_bufnr)
|
||||
end
|
||||
|
||||
-- Insert conflict markers
|
||||
conflict.insert_conflict(target_bufnr, start_line, end_line, new_lines, "AI SUGGESTION")
|
||||
|
||||
-- Process conflicts (highlight, keymaps) and show menu
|
||||
conflict.process_and_show_menu(target_bufnr)
|
||||
|
||||
M.mark_applied(patch.id)
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "success",
|
||||
message = "Created conflict for review - use co/ct/cb/cn to resolve",
|
||||
})
|
||||
end)
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
-- No suitable range found, fall back to direct apply
|
||||
return M.apply(patch)
|
||||
end
|
||||
|
||||
--- Smart apply - uses conflict mode if enabled, otherwise direct apply
|
||||
---@param patch PatchCandidate
|
||||
---@return boolean success
|
||||
---@return string|nil error
|
||||
function M.smart_apply(patch)
|
||||
if config.use_conflict_mode then
|
||||
return M.apply_with_conflict(patch)
|
||||
else
|
||||
return M.apply(patch)
|
||||
end
|
||||
end
|
||||
|
||||
--- Flush all pending patches using smart apply
|
||||
---@return number applied_count
|
||||
---@return number stale_count
|
||||
---@return number deferred_count
|
||||
function M.flush_pending_smart()
|
||||
local applied = 0
|
||||
local stale = 0
|
||||
local deferred = 0
|
||||
|
||||
for _, p in ipairs(patches) do
|
||||
if p.status == "pending" then
|
||||
local success, err = M.smart_apply(p)
|
||||
if success then
|
||||
applied = applied + 1
|
||||
elseif err == "user_typing" then
|
||||
deferred = deferred + 1
|
||||
else
|
||||
stale = stale + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return applied, stale, deferred
|
||||
end
|
||||
|
||||
return M
|
||||
572
lua/codetyper/core/diff/search_replace.lua
Normal file
572
lua/codetyper/core/diff/search_replace.lua
Normal file
@@ -0,0 +1,572 @@
|
||||
---@mod codetyper.agent.search_replace Search/Replace editing system
|
||||
---@brief [[
|
||||
--- Implements SEARCH/REPLACE block parsing and fuzzy matching for reliable code edits.
|
||||
--- Parses and applies SEARCH/REPLACE blocks from LLM responses.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
local params = require("codetyper.params.agents.search_replace").patterns
|
||||
|
||||
---@class SearchReplaceBlock
|
||||
---@field search string The text to search for
|
||||
---@field replace string The text to replace with
|
||||
---@field file_path string|nil Optional file path for multi-file edits
|
||||
|
||||
---@class MatchResult
|
||||
---@field start_line number 1-indexed start line
|
||||
---@field end_line number 1-indexed end line
|
||||
---@field start_col number 1-indexed start column (for partial line matches)
|
||||
---@field end_col number 1-indexed end column
|
||||
---@field strategy string Which matching strategy succeeded
|
||||
---@field confidence number Match confidence (0.0-1.0)
|
||||
|
||||
--- Parse SEARCH/REPLACE blocks from LLM response
|
||||
--- Supports multiple formats:
|
||||
--- Format 1 (dash style):
|
||||
--- ------- SEARCH
|
||||
--- old code
|
||||
--- =======
|
||||
--- new code
|
||||
--- +++++++ REPLACE
|
||||
---
|
||||
--- Format 2 (claude style):
|
||||
--- <<<<<<< SEARCH
|
||||
--- old code
|
||||
--- =======
|
||||
--- new code
|
||||
--- >>>>>>> REPLACE
|
||||
---
|
||||
--- Format 3 (simple):
|
||||
--- [SEARCH]
|
||||
--- old code
|
||||
--- [REPLACE]
|
||||
--- new code
|
||||
--- [END]
|
||||
---
|
||||
---@param response string LLM response text
|
||||
---@return SearchReplaceBlock[]
|
||||
function M.parse_blocks(response)
|
||||
local blocks = {}
|
||||
|
||||
-- Try dash-style format: ------- SEARCH ... ======= ... +++++++ REPLACE
|
||||
for search, replace in response:gmatch(params.dash_style) do
|
||||
table.insert(blocks, { search = search, replace = replace })
|
||||
end
|
||||
|
||||
if #blocks > 0 then
|
||||
return blocks
|
||||
end
|
||||
|
||||
-- Try claude-style format: <<<<<<< SEARCH ... ======= ... >>>>>>> REPLACE
|
||||
for search, replace in response:gmatch(params.claude_style) do
|
||||
table.insert(blocks, { search = search, replace = replace })
|
||||
end
|
||||
|
||||
if #blocks > 0 then
|
||||
return blocks
|
||||
end
|
||||
|
||||
-- Try simple format: [SEARCH] ... [REPLACE] ... [END]
|
||||
for search, replace in response:gmatch(params.simple_style) do
|
||||
table.insert(blocks, { search = search, replace = replace })
|
||||
end
|
||||
|
||||
if #blocks > 0 then
|
||||
return blocks
|
||||
end
|
||||
|
||||
-- Try markdown diff format: ```diff ... ```
|
||||
local diff_block = response:match(params.diff_block)
|
||||
if diff_block then
|
||||
local old_lines = {}
|
||||
local new_lines = {}
|
||||
for line in diff_block:gmatch("[^\n]+") do
|
||||
if line:match("^%-[^%-]") then
|
||||
-- Removed line (starts with single -)
|
||||
table.insert(old_lines, line:sub(2))
|
||||
elseif line:match("^%+[^%+]") then
|
||||
-- Added line (starts with single +)
|
||||
table.insert(new_lines, line:sub(2))
|
||||
elseif line:match("^%s") or line:match("^[^%-%+@]") then
|
||||
-- Context line
|
||||
table.insert(old_lines, line:match("^%s?(.*)"))
|
||||
table.insert(new_lines, line:match("^%s?(.*)"))
|
||||
end
|
||||
end
|
||||
if #old_lines > 0 or #new_lines > 0 then
|
||||
table.insert(blocks, {
|
||||
search = table.concat(old_lines, "\n"),
|
||||
replace = table.concat(new_lines, "\n"),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return blocks
|
||||
end
|
||||
|
||||
--- Get indentation of a line
|
||||
---@param line string
|
||||
---@return string
|
||||
local function get_indentation(line)
|
||||
if not line then
|
||||
return ""
|
||||
end
|
||||
return line:match("^(%s*)") or ""
|
||||
end
|
||||
|
||||
--- Normalize whitespace in a string (collapse multiple spaces to one)
|
||||
---@param str string
|
||||
---@return string
|
||||
local function normalize_whitespace(str)
|
||||
-- Wrap in parentheses to only return first value (gsub returns string + count)
|
||||
return (str:gsub("%s+", " "):gsub("^%s*", ""):gsub("%s*$", ""))
|
||||
end
|
||||
|
||||
--- Trim trailing whitespace from each line
|
||||
---@param str string
|
||||
---@return string
|
||||
local function trim_lines(str)
|
||||
local lines = vim.split(str, "\n", { plain = true })
|
||||
for i, line in ipairs(lines) do
|
||||
-- Wrap in parentheses to only get string, not count
|
||||
lines[i] = (line:gsub("%s+$", ""))
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Calculate Levenshtein distance between two strings
|
||||
---@param s1 string
|
||||
---@param s2 string
|
||||
---@return number
|
||||
local function levenshtein(s1, s2)
|
||||
local len1, len2 = #s1, #s2
|
||||
if len1 == 0 then
|
||||
return len2
|
||||
end
|
||||
if len2 == 0 then
|
||||
return len1
|
||||
end
|
||||
|
||||
local matrix = {}
|
||||
for i = 0, len1 do
|
||||
matrix[i] = { [0] = i }
|
||||
end
|
||||
for j = 0, len2 do
|
||||
matrix[0][j] = j
|
||||
end
|
||||
|
||||
for i = 1, len1 do
|
||||
for j = 1, len2 do
|
||||
local cost = (s1:sub(i, i) == s2:sub(j, j)) and 0 or 1
|
||||
matrix[i][j] = math.min(
|
||||
matrix[i - 1][j] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j - 1] + cost
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return matrix[len1][len2]
|
||||
end
|
||||
|
||||
--- Calculate similarity ratio (0.0-1.0) between two strings
|
||||
---@param s1 string
|
||||
---@param s2 string
|
||||
---@return number
|
||||
local function similarity(s1, s2)
|
||||
if s1 == s2 then
|
||||
return 1.0
|
||||
end
|
||||
local max_len = math.max(#s1, #s2)
|
||||
if max_len == 0 then
|
||||
return 1.0
|
||||
end
|
||||
local distance = levenshtein(s1, s2)
|
||||
return 1.0 - (distance / max_len)
|
||||
end
|
||||
|
||||
--- Strategy 1: Exact match
|
||||
---@param content_lines string[]
|
||||
---@param search_lines string[]
|
||||
---@return MatchResult|nil
|
||||
local function exact_match(content_lines, search_lines)
|
||||
if #search_lines == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, #content_lines - #search_lines + 1 do
|
||||
local match = true
|
||||
for j = 1, #search_lines do
|
||||
if content_lines[i + j - 1] ~= search_lines[j] then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if match then
|
||||
return {
|
||||
start_line = i,
|
||||
end_line = i + #search_lines - 1,
|
||||
start_col = 1,
|
||||
end_col = #content_lines[i + #search_lines - 1],
|
||||
strategy = "exact",
|
||||
confidence = 1.0,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Strategy 2: Line-trimmed match (ignore trailing whitespace)
|
||||
---@param content_lines string[]
|
||||
---@param search_lines string[]
|
||||
---@return MatchResult|nil
|
||||
local function line_trimmed_match(content_lines, search_lines)
|
||||
if #search_lines == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local trimmed_search = {}
|
||||
for _, line in ipairs(search_lines) do
|
||||
table.insert(trimmed_search, (line:gsub("%s+$", "")))
|
||||
end
|
||||
|
||||
for i = 1, #content_lines - #search_lines + 1 do
|
||||
local match = true
|
||||
for j = 1, #search_lines do
|
||||
local trimmed_content = content_lines[i + j - 1]:gsub("%s+$", "")
|
||||
if trimmed_content ~= trimmed_search[j] then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if match then
|
||||
return {
|
||||
start_line = i,
|
||||
end_line = i + #search_lines - 1,
|
||||
start_col = 1,
|
||||
end_col = #content_lines[i + #search_lines - 1],
|
||||
strategy = "line_trimmed",
|
||||
confidence = 0.95,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Strategy 3: Indentation-flexible match (normalize indentation)
|
||||
---@param content_lines string[]
|
||||
---@param search_lines string[]
|
||||
---@return MatchResult|nil
|
||||
local function indentation_flexible_match(content_lines, search_lines)
|
||||
if #search_lines == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get base indentation from search (first non-empty line)
|
||||
local search_indent = ""
|
||||
for _, line in ipairs(search_lines) do
|
||||
if line:match("%S") then
|
||||
search_indent = get_indentation(line)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Strip common indentation from search
|
||||
local stripped_search = {}
|
||||
for _, line in ipairs(search_lines) do
|
||||
if line:match("^" .. vim.pesc(search_indent)) then
|
||||
table.insert(stripped_search, line:sub(#search_indent + 1))
|
||||
else
|
||||
table.insert(stripped_search, line)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #content_lines - #search_lines + 1 do
|
||||
-- Get content indentation at this position
|
||||
local content_indent = ""
|
||||
for j = 0, #search_lines - 1 do
|
||||
local line = content_lines[i + j]
|
||||
if line:match("%S") then
|
||||
content_indent = get_indentation(line)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local match = true
|
||||
for j = 1, #search_lines do
|
||||
local content_line = content_lines[i + j - 1]
|
||||
local expected = content_indent .. stripped_search[j]
|
||||
|
||||
-- Compare with normalized indentation
|
||||
if content_line:gsub("%s+$", "") ~= expected:gsub("%s+$", "") then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if match then
|
||||
return {
|
||||
start_line = i,
|
||||
end_line = i + #search_lines - 1,
|
||||
start_col = 1,
|
||||
end_col = #content_lines[i + #search_lines - 1],
|
||||
strategy = "indentation_flexible",
|
||||
confidence = 0.9,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Strategy 4: Block anchor match (match first/last lines, fuzzy middle)
|
||||
---@param content_lines string[]
|
||||
---@param search_lines string[]
|
||||
---@return MatchResult|nil
|
||||
local function block_anchor_match(content_lines, search_lines)
|
||||
if #search_lines < 2 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local first_search = search_lines[1]:gsub("%s+$", "")
|
||||
local last_search = search_lines[#search_lines]:gsub("%s+$", "")
|
||||
|
||||
-- Find potential start positions
|
||||
local candidates = {}
|
||||
for i = 1, #content_lines - #search_lines + 1 do
|
||||
local first_content = content_lines[i]:gsub("%s+$", "")
|
||||
if similarity(first_content, first_search) > 0.8 then
|
||||
-- Check if last line also matches
|
||||
local last_idx = i + #search_lines - 1
|
||||
if last_idx <= #content_lines then
|
||||
local last_content = content_lines[last_idx]:gsub("%s+$", "")
|
||||
if similarity(last_content, last_search) > 0.8 then
|
||||
-- Calculate overall similarity
|
||||
local total_sim = 0
|
||||
for j = 1, #search_lines do
|
||||
local c = content_lines[i + j - 1]:gsub("%s+$", "")
|
||||
local s = search_lines[j]:gsub("%s+$", "")
|
||||
total_sim = total_sim + similarity(c, s)
|
||||
end
|
||||
local avg_sim = total_sim / #search_lines
|
||||
if avg_sim > 0.7 then
|
||||
table.insert(candidates, { start = i, similarity = avg_sim })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Return best match
|
||||
if #candidates > 0 then
|
||||
table.sort(candidates, function(a, b)
|
||||
return a.similarity > b.similarity
|
||||
end)
|
||||
local best = candidates[1]
|
||||
return {
|
||||
start_line = best.start,
|
||||
end_line = best.start + #search_lines - 1,
|
||||
start_col = 1,
|
||||
end_col = #content_lines[best.start + #search_lines - 1],
|
||||
strategy = "block_anchor",
|
||||
confidence = best.similarity * 0.85,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Strategy 5: Whitespace-normalized match
|
||||
---@param content_lines string[]
|
||||
---@param search_lines string[]
|
||||
---@return MatchResult|nil
|
||||
local function whitespace_normalized_match(content_lines, search_lines)
|
||||
if #search_lines == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Normalize search lines
|
||||
local norm_search = {}
|
||||
for _, line in ipairs(search_lines) do
|
||||
table.insert(norm_search, normalize_whitespace(line))
|
||||
end
|
||||
|
||||
for i = 1, #content_lines - #search_lines + 1 do
|
||||
local match = true
|
||||
for j = 1, #search_lines do
|
||||
local norm_content = normalize_whitespace(content_lines[i + j - 1])
|
||||
if norm_content ~= norm_search[j] then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if match then
|
||||
return {
|
||||
start_line = i,
|
||||
end_line = i + #search_lines - 1,
|
||||
start_col = 1,
|
||||
end_col = #content_lines[i + #search_lines - 1],
|
||||
strategy = "whitespace_normalized",
|
||||
confidence = 0.8,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Find the best match for search text in content
|
||||
---@param content string File content
|
||||
---@param search string Text to search for
|
||||
---@return MatchResult|nil
|
||||
function M.find_match(content, search)
|
||||
local content_lines = vim.split(content, "\n", { plain = true })
|
||||
local search_lines = vim.split(search, "\n", { plain = true })
|
||||
|
||||
-- Remove trailing empty lines from search
|
||||
while #search_lines > 0 and search_lines[#search_lines]:match("^%s*$") do
|
||||
table.remove(search_lines)
|
||||
end
|
||||
|
||||
if #search_lines == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Try strategies in order of strictness
|
||||
local strategies = {
|
||||
exact_match,
|
||||
line_trimmed_match,
|
||||
indentation_flexible_match,
|
||||
block_anchor_match,
|
||||
whitespace_normalized_match,
|
||||
}
|
||||
|
||||
for _, strategy in ipairs(strategies) do
|
||||
local result = strategy(content_lines, search_lines)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Apply a single SEARCH/REPLACE block to content
|
||||
---@param content string Original file content
|
||||
---@param block SearchReplaceBlock
|
||||
---@return string|nil new_content
|
||||
---@return MatchResult|nil match_info
|
||||
---@return string|nil error
|
||||
function M.apply_block(content, block)
|
||||
local match = M.find_match(content, block.search)
|
||||
if not match then
|
||||
return nil, nil, "Could not find search text in file"
|
||||
end
|
||||
|
||||
local content_lines = vim.split(content, "\n", { plain = true })
|
||||
local replace_lines = vim.split(block.replace, "\n", { plain = true })
|
||||
|
||||
-- Adjust indentation of replacement to match original
|
||||
local original_indent = get_indentation(content_lines[match.start_line])
|
||||
local replace_indent = ""
|
||||
for _, line in ipairs(replace_lines) do
|
||||
if line:match("%S") then
|
||||
replace_indent = get_indentation(line)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply indentation adjustment
|
||||
local adjusted_replace = {}
|
||||
for _, line in ipairs(replace_lines) do
|
||||
if line:match("^" .. vim.pesc(replace_indent)) then
|
||||
table.insert(adjusted_replace, original_indent .. line:sub(#replace_indent + 1))
|
||||
elseif line:match("^%s*$") then
|
||||
table.insert(adjusted_replace, "")
|
||||
else
|
||||
table.insert(adjusted_replace, original_indent .. line)
|
||||
end
|
||||
end
|
||||
|
||||
-- Build new content
|
||||
local new_lines = {}
|
||||
for i = 1, match.start_line - 1 do
|
||||
table.insert(new_lines, content_lines[i])
|
||||
end
|
||||
for _, line in ipairs(adjusted_replace) do
|
||||
table.insert(new_lines, line)
|
||||
end
|
||||
for i = match.end_line + 1, #content_lines do
|
||||
table.insert(new_lines, content_lines[i])
|
||||
end
|
||||
|
||||
return table.concat(new_lines, "\n"), match, nil
|
||||
end
|
||||
|
||||
--- Apply multiple SEARCH/REPLACE blocks to content
|
||||
---@param content string Original file content
|
||||
---@param blocks SearchReplaceBlock[]
|
||||
---@return string new_content
|
||||
---@return table results Array of {success: boolean, match: MatchResult|nil, error: string|nil}
|
||||
function M.apply_blocks(content, blocks)
|
||||
local current_content = content
|
||||
local results = {}
|
||||
|
||||
for _, block in ipairs(blocks) do
|
||||
local new_content, match, err = M.apply_block(current_content, block)
|
||||
if new_content then
|
||||
current_content = new_content
|
||||
table.insert(results, { success = true, match = match })
|
||||
else
|
||||
table.insert(results, { success = false, error = err })
|
||||
end
|
||||
end
|
||||
|
||||
return current_content, results
|
||||
end
|
||||
|
||||
--- Apply SEARCH/REPLACE blocks to a buffer
|
||||
---@param bufnr number Buffer number
|
||||
---@param blocks SearchReplaceBlock[]
|
||||
---@return boolean success
|
||||
---@return string|nil error
|
||||
function M.apply_to_buffer(bufnr, blocks)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return false, "Invalid buffer"
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local content = table.concat(lines, "\n")
|
||||
|
||||
local new_content, results = M.apply_blocks(content, blocks)
|
||||
|
||||
-- Check for any failures
|
||||
local failures = {}
|
||||
for i, result in ipairs(results) do
|
||||
if not result.success then
|
||||
table.insert(failures, string.format("Block %d: %s", i, result.error or "unknown error"))
|
||||
end
|
||||
end
|
||||
|
||||
if #failures > 0 then
|
||||
return false, table.concat(failures, "; ")
|
||||
end
|
||||
|
||||
-- Apply to buffer
|
||||
local new_lines = vim.split(new_content, "\n", { plain = true })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines)
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
--- Check if response contains SEARCH/REPLACE blocks
|
||||
---@param response string
|
||||
---@return boolean
|
||||
function M.has_blocks(response)
|
||||
return #M.parse_blocks(response) > 0
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -194,7 +194,7 @@ function M.enqueue(event)
|
||||
|
||||
-- Log to agent logs if available
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "queue",
|
||||
message = string.format("Event queued: %s (priority: %d)", event.id, event.priority),
|
||||
117
lua/codetyper/core/intent/init.lua
Normal file
117
lua/codetyper/core/intent/init.lua
Normal file
@@ -0,0 +1,117 @@
|
||||
---@mod codetyper.agent.intent Intent detection from prompts
|
||||
---@brief [[
|
||||
--- Parses prompt content to determine user intent and target scope.
|
||||
--- Intents determine how the generated code should be applied.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Intent
|
||||
---@field type string "complete"|"refactor"|"add"|"fix"|"document"|"test"|"explain"|"optimize"
|
||||
---@field scope_hint string|nil "function"|"class"|"block"|"file"|"selection"|nil
|
||||
---@field confidence number 0.0-1.0 how confident we are about the intent
|
||||
---@field action string "replace"|"insert"|"append"|"none"
|
||||
---@field keywords string[] Keywords that triggered this intent
|
||||
|
||||
local params = require("codetyper.params.agents.intent")
|
||||
local intent_patterns = params.intent_patterns
|
||||
local scope_patterns = params.scope_patterns
|
||||
local prompts = require("codetyper.prompts.agents.intent")
|
||||
|
||||
--- Detect intent from prompt content
|
||||
---@param prompt string The prompt content
|
||||
---@return Intent
|
||||
function M.detect(prompt)
|
||||
local lower = prompt:lower()
|
||||
local best_match = nil
|
||||
local best_priority = 999
|
||||
local matched_keywords = {}
|
||||
|
||||
-- Check each intent type
|
||||
for intent_type, config in pairs(intent_patterns) do
|
||||
for _, pattern in ipairs(config.patterns) do
|
||||
if lower:find(pattern, 1, true) then
|
||||
if config.priority < best_priority then
|
||||
best_match = intent_type
|
||||
best_priority = config.priority
|
||||
matched_keywords = { pattern }
|
||||
elseif config.priority == best_priority and best_match == intent_type then
|
||||
table.insert(matched_keywords, pattern)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Default to "add" if no clear intent
|
||||
if not best_match then
|
||||
best_match = "add"
|
||||
matched_keywords = {}
|
||||
end
|
||||
|
||||
local config = intent_patterns[best_match]
|
||||
|
||||
-- Detect scope hint from prompt
|
||||
local scope_hint = config.scope_hint
|
||||
for pattern, hint in pairs(scope_patterns) do
|
||||
if lower:find(pattern, 1, true) then
|
||||
scope_hint = hint or scope_hint
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate confidence based on keyword matches
|
||||
local confidence = 0.5 + (#matched_keywords * 0.15)
|
||||
confidence = math.min(confidence, 1.0)
|
||||
|
||||
return {
|
||||
type = best_match,
|
||||
scope_hint = scope_hint,
|
||||
confidence = confidence,
|
||||
action = config.action,
|
||||
keywords = matched_keywords,
|
||||
}
|
||||
end
|
||||
|
||||
--- Check if intent requires code modification
|
||||
---@param intent Intent
|
||||
---@return boolean
|
||||
function M.modifies_code(intent)
|
||||
return intent.action ~= "none"
|
||||
end
|
||||
|
||||
--- Check if intent should replace existing code
|
||||
---@param intent Intent
|
||||
---@return boolean
|
||||
function M.is_replacement(intent)
|
||||
return intent.action == "replace"
|
||||
end
|
||||
|
||||
--- Check if intent adds new code
|
||||
---@param intent Intent
|
||||
---@return boolean
|
||||
function M.is_insertion(intent)
|
||||
return intent.action == "insert" or intent.action == "append"
|
||||
end
|
||||
|
||||
--- Get system prompt modifier based on intent
|
||||
---@param intent Intent
|
||||
---@return string
|
||||
function M.get_prompt_modifier(intent)
|
||||
local modifiers = prompts.modifiers
|
||||
return modifiers[intent.type] or modifiers.add
|
||||
end
|
||||
|
||||
--- Format intent for logging
|
||||
---@param intent Intent
|
||||
---@return string
|
||||
function M.format(intent)
|
||||
return string.format(
|
||||
"%s (scope: %s, action: %s, confidence: %.2f)",
|
||||
intent.type,
|
||||
intent.scope_hint or "auto",
|
||||
intent.action,
|
||||
intent.confidence
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -6,41 +6,14 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local params = require("codetyper.params.agents.confidence")
|
||||
|
||||
--- Heuristic weights (must sum to 1.0)
|
||||
M.weights = {
|
||||
length = 0.15, -- Response length relative to prompt
|
||||
uncertainty = 0.30, -- Uncertainty phrases
|
||||
syntax = 0.25, -- Syntax completeness
|
||||
repetition = 0.15, -- Duplicate lines
|
||||
truncation = 0.15, -- Incomplete ending
|
||||
}
|
||||
M.weights = params.weights
|
||||
|
||||
--- Uncertainty phrases that indicate low confidence
|
||||
local uncertainty_phrases = {
|
||||
-- English
|
||||
"i'm not sure",
|
||||
"i am not sure",
|
||||
"maybe",
|
||||
"perhaps",
|
||||
"might work",
|
||||
"could work",
|
||||
"not certain",
|
||||
"uncertain",
|
||||
"i think",
|
||||
"possibly",
|
||||
"TODO",
|
||||
"FIXME",
|
||||
"XXX",
|
||||
"placeholder",
|
||||
"implement this",
|
||||
"fill in",
|
||||
"your code here",
|
||||
"...", -- Ellipsis as placeholder
|
||||
"# TODO",
|
||||
"// TODO",
|
||||
"-- TODO",
|
||||
"/* TODO",
|
||||
}
|
||||
local uncertainty_phrases = params.uncertainty_phrases
|
||||
|
||||
|
||||
--- Score based on response length relative to prompt
|
||||
---@param response string
|
||||
@@ -94,32 +67,6 @@ local function score_uncertainty(response)
|
||||
end
|
||||
end
|
||||
|
||||
--- Check bracket balance for common languages
|
||||
---@param response string
|
||||
---@return boolean balanced
|
||||
local function check_brackets(response)
|
||||
local pairs = {
|
||||
["{"] = "}",
|
||||
["["] = "]",
|
||||
["("] = ")",
|
||||
}
|
||||
|
||||
local stack = {}
|
||||
|
||||
for char in response:gmatch(".") do
|
||||
if pairs[char] then
|
||||
table.insert(stack, pairs[char])
|
||||
elseif char == "}" or char == "]" or char == ")" then
|
||||
if #stack == 0 or stack[#stack] ~= char then
|
||||
return false
|
||||
end
|
||||
table.remove(stack)
|
||||
end
|
||||
end
|
||||
|
||||
return #stack == 0
|
||||
end
|
||||
|
||||
--- Score based on syntax completeness
|
||||
---@param response string
|
||||
---@return number 0.0-1.0
|
||||
@@ -127,7 +74,7 @@ local function score_syntax(response)
|
||||
local score = 1.0
|
||||
|
||||
-- Check bracket balance
|
||||
if not check_brackets(response) then
|
||||
if not require("codetyper.support.utils").check_brackets(response) then
|
||||
score = score - 0.4
|
||||
end
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local llm = require("codetyper.core.llm")
|
||||
|
||||
--- Copilot API endpoints
|
||||
local AUTH_URL = "https://api.github.com/copilot_internal/v2/token"
|
||||
@@ -296,7 +296,7 @@ local function make_request(token, body, callback)
|
||||
|
||||
-- Record usage for cost tracking
|
||||
if usage.prompt_tokens or usage.completion_tokens then
|
||||
local cost = require("codetyper.cost")
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.record_usage(
|
||||
get_model(),
|
||||
usage.prompt_tokens or 0,
|
||||
@@ -348,7 +348,7 @@ end
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil)
|
||||
function M.generate(prompt, context, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
ensure_initialized()
|
||||
|
||||
@@ -414,7 +414,7 @@ end
|
||||
---@param tool_definitions table Tool definitions
|
||||
---@param callback fun(response: table|nil, error: string|nil)
|
||||
function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
ensure_initialized()
|
||||
|
||||
@@ -436,13 +436,12 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
return
|
||||
end
|
||||
|
||||
local tools_module = require("codetyper.agent.tools")
|
||||
local agent_prompts = require("codetyper.prompts.agent")
|
||||
local tools_module = require("codetyper.core.tools")
|
||||
local agent_prompts = require("codetyper.prompts.agents")
|
||||
|
||||
-- Build system prompt with agent instructions
|
||||
-- Build system prompt with agent instructions and project context
|
||||
local system_prompt = llm.build_system_prompt(context)
|
||||
system_prompt = system_prompt .. "\n\n" .. agent_prompts.system
|
||||
system_prompt = system_prompt .. "\n\n" .. agent_prompts.tool_instructions
|
||||
system_prompt = system_prompt .. "\n\n" .. agent_prompts.build_system_prompt()
|
||||
|
||||
-- Format messages for Copilot (OpenAI-compatible format)
|
||||
local copilot_messages = { { role = "system", content = system_prompt } }
|
||||
@@ -471,9 +470,21 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
role = "assistant",
|
||||
content = type(msg.content) == "string" and msg.content or nil,
|
||||
}
|
||||
-- Preserve tool_calls for the API
|
||||
if msg.tool_calls then
|
||||
assistant_msg.tool_calls = msg.tool_calls
|
||||
-- Convert tool_calls to OpenAI format for the API
|
||||
if msg.tool_calls and #msg.tool_calls > 0 then
|
||||
assistant_msg.tool_calls = {}
|
||||
for _, tc in ipairs(msg.tool_calls) do
|
||||
-- Convert from parsed format {id, name, parameters} to OpenAI format
|
||||
local openai_tc = {
|
||||
id = tc.id,
|
||||
type = "function",
|
||||
["function"] = {
|
||||
name = tc.name,
|
||||
arguments = vim.json.encode(tc.parameters or {}),
|
||||
},
|
||||
}
|
||||
table.insert(assistant_msg.tool_calls, openai_tc)
|
||||
end
|
||||
-- Ensure content is not nil when tool_calls present
|
||||
if assistant_msg.content == nil then
|
||||
assistant_msg.content = ""
|
||||
@@ -497,6 +508,7 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
temperature = 0.3,
|
||||
stream = false,
|
||||
tools = tools_module.to_openai_format(),
|
||||
tool_choice = "auto", -- Encourage the model to use tools when appropriate
|
||||
}
|
||||
|
||||
local endpoint = (token.endpoints and token.endpoints.api or "https://api.githubcopilot.com")
|
||||
@@ -622,7 +634,7 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
logs.response(response.usage.prompt_tokens or 0, response.usage.completion_tokens or 0, "stop")
|
||||
|
||||
-- Record usage for cost tracking
|
||||
local cost_tracker = require("codetyper.cost")
|
||||
local cost_tracker = require("codetyper.core.cost")
|
||||
cost_tracker.record_usage(
|
||||
get_model(),
|
||||
response.usage.prompt_tokens or 0,
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local llm = require("codetyper.core.llm")
|
||||
|
||||
--- Gemini API endpoint
|
||||
local API_URL = "https://generativelanguage.googleapis.com/v1beta/models"
|
||||
@@ -167,7 +167,7 @@ end
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local model = get_model()
|
||||
|
||||
-- Log the request
|
||||
@@ -217,7 +217,7 @@ end
|
||||
---@param tool_definitions table Tool definitions
|
||||
---@param callback fun(response: table|nil, error: string|nil) Callback with raw response
|
||||
function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local model = get_model()
|
||||
|
||||
logs.request("gemini", model)
|
||||
@@ -230,8 +230,8 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
return
|
||||
end
|
||||
|
||||
local tools_module = require("codetyper.agent.tools")
|
||||
local agent_prompts = require("codetyper.prompts.agent")
|
||||
local tools_module = require("codetyper.core.tools")
|
||||
local agent_prompts = require("codetyper.prompts.agents")
|
||||
|
||||
-- Build system prompt with agent instructions
|
||||
local system_prompt = llm.build_system_prompt(context)
|
||||
@@ -1,8 +1,8 @@
|
||||
---@mod codetyper.llm LLM interface for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
local lang_map = require("codetyper.utils.langmap")
|
||||
local utils = require("codetyper.utils")
|
||||
local lang_map = require("codetyper.support.langmap")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Get the appropriate LLM client based on configuration
|
||||
---@return table LLM client module
|
||||
@@ -11,13 +11,13 @@ function M.get_client()
|
||||
local config = codetyper.get_config()
|
||||
|
||||
if config.llm.provider == "ollama" then
|
||||
return require("codetyper.llm.ollama")
|
||||
return require("codetyper.core.llm.ollama")
|
||||
elseif config.llm.provider == "openai" then
|
||||
return require("codetyper.llm.openai")
|
||||
return require("codetyper.core.llm.openai")
|
||||
elseif config.llm.provider == "gemini" then
|
||||
return require("codetyper.llm.gemini")
|
||||
return require("codetyper.core.llm.gemini")
|
||||
elseif config.llm.provider == "copilot" then
|
||||
return require("codetyper.llm.copilot")
|
||||
return require("codetyper.core.llm.copilot")
|
||||
else
|
||||
error("Unknown LLM provider: " .. config.llm.provider)
|
||||
end
|
||||
@@ -39,14 +39,14 @@ end
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil, metadata: table|nil) Callback
|
||||
function M.smart_generate(prompt, context, callback)
|
||||
local selector = require("codetyper.llm.selector")
|
||||
local selector = require("codetyper.core.llm.selector")
|
||||
selector.smart_generate(prompt, context, callback)
|
||||
end
|
||||
|
||||
--- Get accuracy statistics for providers
|
||||
---@return table Statistics for each provider
|
||||
function M.get_accuracy_stats()
|
||||
local selector = require("codetyper.llm.selector")
|
||||
local selector = require("codetyper.core.llm.selector")
|
||||
return selector.get_accuracy_stats()
|
||||
end
|
||||
|
||||
@@ -54,7 +54,7 @@ end
|
||||
---@param provider string Which provider generated the response
|
||||
---@param was_correct boolean Whether the response was good
|
||||
function M.report_feedback(provider, was_correct)
|
||||
local selector = require("codetyper.llm.selector")
|
||||
local selector = require("codetyper.core.llm.selector")
|
||||
selector.report_feedback(provider, was_correct)
|
||||
end
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local llm = require("codetyper.core.llm")
|
||||
|
||||
--- Get Ollama host from stored credentials or config
|
||||
---@return string Host URL
|
||||
@@ -137,7 +137,7 @@ end
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local model = get_model()
|
||||
|
||||
-- Log the request
|
||||
@@ -217,9 +217,9 @@ end
|
||||
---@param tool_definitions table Tool definitions
|
||||
---@param callback fun(response: table|nil, error: string|nil) Callback with Claude-like response format
|
||||
function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local agent_prompts = require("codetyper.prompts.agent")
|
||||
local tools_module = require("codetyper.agent.tools")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local agent_prompts = require("codetyper.prompts.agents")
|
||||
local tools_module = require("codetyper.core.tools")
|
||||
|
||||
logs.request("ollama", get_model())
|
||||
logs.thinking("Preparing agent request...")
|
||||
@@ -322,7 +322,7 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
logs.response(response.prompt_eval_count or 0, response.eval_count or 0, "stop")
|
||||
|
||||
-- Record usage for cost tracking (free for local models)
|
||||
local cost = require("codetyper.cost")
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.record_usage(
|
||||
get_model(),
|
||||
response.prompt_eval_count or 0,
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local llm = require("codetyper.core.llm")
|
||||
|
||||
--- OpenAI API endpoint
|
||||
local API_URL = "https://api.openai.com/v1/chat/completions"
|
||||
@@ -158,7 +158,7 @@ end
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local model = get_model()
|
||||
|
||||
-- Log the request
|
||||
@@ -208,7 +208,7 @@ end
|
||||
---@param tool_definitions table Tool definitions
|
||||
---@param callback fun(response: table|nil, error: string|nil) Callback with raw response
|
||||
function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local model = get_model()
|
||||
|
||||
logs.request("openai", model)
|
||||
@@ -221,8 +221,8 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
return
|
||||
end
|
||||
|
||||
local tools_module = require("codetyper.agent.tools")
|
||||
local agent_prompts = require("codetyper.prompts.agent")
|
||||
local tools_module = require("codetyper.core.tools")
|
||||
local agent_prompts = require("codetyper.prompts.agents")
|
||||
|
||||
-- Build system prompt with agent instructions
|
||||
local system_prompt = llm.build_system_prompt(context)
|
||||
@@ -310,7 +310,7 @@ function M.generate_with_tools(messages, context, tool_definitions, callback)
|
||||
logs.response(response.usage.prompt_tokens or 0, response.usage.completion_tokens or 0, "stop")
|
||||
|
||||
-- Record usage for cost tracking
|
||||
local cost = require("codetyper.cost")
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.record_usage(
|
||||
model,
|
||||
response.usage.prompt_tokens or 0,
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local params = require("codetyper.params.agents.parser")
|
||||
|
||||
|
||||
---@class ParsedResponse
|
||||
---@field text string Text content from the response
|
||||
---@field tool_calls ToolCall[] List of tool calls
|
||||
@@ -48,11 +51,11 @@ function M.parse_ollama_response(response_text)
|
||||
local result = {
|
||||
text = response_text,
|
||||
tool_calls = {},
|
||||
stop_reason = "end_turn",
|
||||
stop_reason = params.defaults.stop_reason,
|
||||
}
|
||||
|
||||
-- Pattern to find JSON tool blocks in fenced code blocks
|
||||
local fenced_pattern = "```json%s*(%b{})%s*```"
|
||||
local fenced_pattern = params.patterns.fenced_json
|
||||
|
||||
-- Find all fenced JSON blocks
|
||||
for json_str in response_text:gmatch(fenced_pattern) do
|
||||
@@ -63,14 +66,14 @@ function M.parse_ollama_response(response_text)
|
||||
name = parsed.tool,
|
||||
parameters = parsed.parameters,
|
||||
})
|
||||
result.stop_reason = "tool_use"
|
||||
result.stop_reason = params.defaults.tool_stop_reason
|
||||
end
|
||||
end
|
||||
|
||||
-- Also try to find inline JSON (not in code blocks)
|
||||
-- Pattern for {"tool": "...", "parameters": {...}}
|
||||
if #result.tool_calls == 0 then
|
||||
local inline_pattern = '(%{"tool"%s*:%s*"[^"]+"%s*,%s*"parameters"%s*:%s*%b{}%})'
|
||||
local inline_pattern = params.patterns.inline_json
|
||||
for json_str in response_text:gmatch(inline_pattern) do
|
||||
local ok, parsed = pcall(vim.json.decode, json_str)
|
||||
if ok and parsed.tool and parsed.parameters then
|
||||
@@ -79,15 +82,15 @@ function M.parse_ollama_response(response_text)
|
||||
name = parsed.tool,
|
||||
parameters = parsed.parameters,
|
||||
})
|
||||
result.stop_reason = "tool_use"
|
||||
result.stop_reason = params.defaults.tool_stop_reason
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean tool JSON from displayed text
|
||||
if #result.tool_calls > 0 then
|
||||
result.text = result.text:gsub("```json%s*%b{}%s*```", "[Tool call]")
|
||||
result.text = result.text:gsub('%{"tool"%s*:%s*"[^"]+"%s*,%s*"parameters"%s*:%s*%b{}%}', "[Tool call]")
|
||||
result.text = result.text:gsub(params.patterns.fenced_json, params.defaults.replacement_text)
|
||||
result.text = result.text:gsub(params.patterns.inline_json, params.defaults.replacement_text)
|
||||
end
|
||||
|
||||
return result
|
||||
@@ -296,7 +296,7 @@ end
|
||||
---@param callback fun(result: PonderResult) Callback with pondering result
|
||||
function M.ponder(prompt, context, ollama_response, callback)
|
||||
-- Use Copilot as verifier
|
||||
local copilot = require("codetyper.llm.copilot")
|
||||
local copilot = require("codetyper.core.llm.copilot")
|
||||
|
||||
-- Build verification prompt
|
||||
local verify_prompt = prompt
|
||||
@@ -393,7 +393,7 @@ function M.smart_generate(prompt, context, callback)
|
||||
|
||||
-- Log selection
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format(
|
||||
@@ -408,9 +408,9 @@ function M.smart_generate(prompt, context, callback)
|
||||
-- Get the selected client
|
||||
local client
|
||||
if selection.provider == "ollama" then
|
||||
client = require("codetyper.llm.ollama")
|
||||
client = require("codetyper.core.llm.ollama")
|
||||
else
|
||||
client = require("codetyper.llm.copilot")
|
||||
client = require("codetyper.core.llm.copilot")
|
||||
end
|
||||
|
||||
-- Generate response
|
||||
@@ -419,7 +419,7 @@ function M.smart_generate(prompt, context, callback)
|
||||
-- Fallback on error
|
||||
if selection.provider == "ollama" then
|
||||
-- Try Copilot as fallback
|
||||
local copilot = require("codetyper.llm.copilot")
|
||||
local copilot = require("codetyper.core.llm.copilot")
|
||||
copilot.generate(prompt, context, function(fallback_response, fallback_error)
|
||||
callback(fallback_response, fallback_error, {
|
||||
provider = "copilot",
|
||||
@@ -1,10 +1,10 @@
|
||||
--- Brain Delta Commit Operations
|
||||
--- Git-like commit creation and management
|
||||
|
||||
local storage = require("codetyper.brain.storage")
|
||||
local hash_mod = require("codetyper.brain.hash")
|
||||
local diff_mod = require("codetyper.brain.delta.diff")
|
||||
local types = require("codetyper.brain.types")
|
||||
local storage = require("codetyper.core.memory.storage")
|
||||
local hash_mod = require("codetyper.core.memory.hash")
|
||||
local diff_mod = require("codetyper.core.memory.delta.diff")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
--- Brain Delta Diff Computation
|
||||
--- Field-level diff algorithms for delta versioning
|
||||
|
||||
local hash = require("codetyper.brain.hash")
|
||||
local hash = require("codetyper.core.memory.hash")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
--- Brain Delta Coordinator
|
||||
--- Git-like versioning system for brain state
|
||||
|
||||
local storage = require("codetyper.brain.storage")
|
||||
local commit_mod = require("codetyper.brain.delta.commit")
|
||||
local diff_mod = require("codetyper.brain.delta.diff")
|
||||
local types = require("codetyper.brain.types")
|
||||
local storage = require("codetyper.core.memory.storage")
|
||||
local commit_mod = require("codetyper.core.memory.delta.commit")
|
||||
local diff_mod = require("codetyper.core.memory.delta.diff")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -17,7 +17,7 @@ M.diff = diff_mod
|
||||
---@param trigger? string Trigger source
|
||||
---@return string|nil Delta hash
|
||||
function M.commit(message, trigger)
|
||||
local graph = require("codetyper.brain.graph")
|
||||
local graph = require("codetyper.core.memory.graph")
|
||||
local changes = graph.get_pending_changes()
|
||||
|
||||
if #changes == 0 then
|
||||
@@ -87,7 +87,7 @@ end
|
||||
--- Apply changes to current state
|
||||
---@param changes table[] Changes to apply
|
||||
function M.apply_changes(changes)
|
||||
local node_mod = require("codetyper.brain.graph.node")
|
||||
local node_mod = require("codetyper.core.memory.graph.node")
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
local parts = vim.split(change.path, ".", { plain = true })
|
||||
@@ -117,7 +117,7 @@ function M.apply_changes(changes)
|
||||
end
|
||||
elseif parts[1] == "graph" then
|
||||
-- Handle graph/edge changes
|
||||
local edge_mod = require("codetyper.brain.graph.edge")
|
||||
local edge_mod = require("codetyper.core.memory.graph.edge")
|
||||
if parts[2] == "edges" and #parts >= 3 then
|
||||
local edge_id = parts[3]
|
||||
if change.op == types.DELTA_OPS.ADD then
|
||||
@@ -168,17 +168,17 @@ end
|
||||
--- Check if there are uncommitted changes
|
||||
---@return boolean
|
||||
function M.has_pending()
|
||||
local graph = require("codetyper.brain.graph")
|
||||
local node_pending = require("codetyper.brain.graph.node").pending
|
||||
local edge_pending = require("codetyper.brain.graph.edge").pending
|
||||
local graph = require("codetyper.core.memory.graph")
|
||||
local node_pending = require("codetyper.core.memory.graph.node").pending
|
||||
local edge_pending = require("codetyper.core.memory.graph.edge").pending
|
||||
return #node_pending > 0 or #edge_pending > 0
|
||||
end
|
||||
|
||||
--- Get status (like git status)
|
||||
---@return table Status info
|
||||
function M.status()
|
||||
local node_pending = require("codetyper.brain.graph.node").pending
|
||||
local edge_pending = require("codetyper.brain.graph.edge").pending
|
||||
local node_pending = require("codetyper.core.memory.graph.node").pending
|
||||
local edge_pending = require("codetyper.core.memory.graph.edge").pending
|
||||
|
||||
local adds = 0
|
||||
local mods = 0
|
||||
@@ -268,8 +268,8 @@ function M.reset()
|
||||
})
|
||||
|
||||
-- Clear pending
|
||||
require("codetyper.brain.graph.node").pending = {}
|
||||
require("codetyper.brain.graph.edge").pending = {}
|
||||
require("codetyper.core.memory.graph.node").pending = {}
|
||||
require("codetyper.core.memory.graph.edge").pending = {}
|
||||
|
||||
storage.flush_all()
|
||||
return true
|
||||
@@ -1,9 +1,9 @@
|
||||
--- Brain Graph Edge Operations
|
||||
--- CRUD operations for node connections
|
||||
|
||||
local storage = require("codetyper.brain.storage")
|
||||
local hash = require("codetyper.brain.hash")
|
||||
local types = require("codetyper.brain.types")
|
||||
local storage = require("codetyper.core.memory.storage")
|
||||
local hash = require("codetyper.core.memory.hash")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
--- Brain Graph Coordinator
|
||||
--- High-level graph operations
|
||||
|
||||
local node = require("codetyper.brain.graph.node")
|
||||
local edge = require("codetyper.brain.graph.edge")
|
||||
local query = require("codetyper.brain.graph.query")
|
||||
local storage = require("codetyper.brain.storage")
|
||||
local types = require("codetyper.brain.types")
|
||||
local node = require("codetyper.core.memory.graph.node")
|
||||
local edge = require("codetyper.core.memory.graph.edge")
|
||||
local query = require("codetyper.core.memory.graph.query")
|
||||
local storage = require("codetyper.core.memory.storage")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
--- Brain Graph Node Operations
|
||||
--- CRUD operations for learning nodes
|
||||
|
||||
local storage = require("codetyper.brain.storage")
|
||||
local hash = require("codetyper.brain.hash")
|
||||
local types = require("codetyper.brain.types")
|
||||
local storage = require("codetyper.core.memory.storage")
|
||||
local hash = require("codetyper.core.memory.hash")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
--- Brain Graph Query Engine
|
||||
--- Multi-dimensional traversal and relevance scoring
|
||||
|
||||
local storage = require("codetyper.brain.storage")
|
||||
local types = require("codetyper.brain.types")
|
||||
local storage = require("codetyper.core.memory.storage")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Lazy load dependencies to avoid circular requires
|
||||
local function get_node_module()
|
||||
return require("codetyper.brain.graph.node")
|
||||
return require("codetyper.core.memory.graph.node")
|
||||
end
|
||||
|
||||
local function get_edge_module()
|
||||
return require("codetyper.brain.graph.edge")
|
||||
return require("codetyper.core.memory.graph.edge")
|
||||
end
|
||||
|
||||
--- Compute text similarity (simple keyword matching)
|
||||
@@ -1,8 +1,8 @@
|
||||
--- Brain Learning System
|
||||
--- Graph-based knowledge storage with delta versioning
|
||||
|
||||
local storage = require("codetyper.brain.storage")
|
||||
local types = require("codetyper.brain.types")
|
||||
local storage = require("codetyper.core.memory.storage")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -72,7 +72,7 @@ function M.learn(event)
|
||||
return nil
|
||||
end
|
||||
|
||||
local learners = require("codetyper.brain.learners")
|
||||
local learners = require("codetyper.core.memory.learners")
|
||||
local node_id = learners.process(event)
|
||||
|
||||
if node_id then
|
||||
@@ -96,7 +96,7 @@ function M.query(opts)
|
||||
return { nodes = {}, edges = {}, stats = {}, truncated = false }
|
||||
end
|
||||
|
||||
local query_engine = require("codetyper.brain.graph.query")
|
||||
local query_engine = require("codetyper.core.memory.graph.query")
|
||||
return query_engine.execute(opts)
|
||||
end
|
||||
|
||||
@@ -112,7 +112,7 @@ function M.get_context_for_llm(opts)
|
||||
opts.max_tokens = opts.max_tokens or config.output.max_tokens
|
||||
|
||||
local result = M.query(opts)
|
||||
local formatter = require("codetyper.brain.output.formatter")
|
||||
local formatter = require("codetyper.core.memory.output.formatter")
|
||||
|
||||
if config.output.format == "json" then
|
||||
return formatter.to_json(result, opts)
|
||||
@@ -129,7 +129,7 @@ function M.commit(message)
|
||||
return nil
|
||||
end
|
||||
|
||||
local delta_mgr = require("codetyper.brain.delta")
|
||||
local delta_mgr = require("codetyper.core.memory.delta")
|
||||
return delta_mgr.commit(message)
|
||||
end
|
||||
|
||||
@@ -141,7 +141,7 @@ function M.rollback(delta_hash)
|
||||
return false
|
||||
end
|
||||
|
||||
local delta_mgr = require("codetyper.brain.delta")
|
||||
local delta_mgr = require("codetyper.core.memory.delta")
|
||||
return delta_mgr.rollback(delta_hash)
|
||||
end
|
||||
|
||||
@@ -153,7 +153,7 @@ function M.get_history(limit)
|
||||
return {}
|
||||
end
|
||||
|
||||
local delta_mgr = require("codetyper.brain.delta")
|
||||
local delta_mgr = require("codetyper.core.memory.delta")
|
||||
return delta_mgr.get_history(limit or 50)
|
||||
end
|
||||
|
||||
@@ -170,7 +170,7 @@ function M.prune(opts)
|
||||
unused_days = config.prune.unused_days,
|
||||
}, opts or {})
|
||||
|
||||
local graph = require("codetyper.brain.graph")
|
||||
local graph = require("codetyper.core.memory.graph")
|
||||
return graph.prune(opts)
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
--- Brain Convention Learner
|
||||
--- Learns project conventions and coding standards
|
||||
|
||||
local types = require("codetyper.brain.types")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
--- Brain Correction Learner
|
||||
--- Learns from user corrections and edits
|
||||
|
||||
local types = require("codetyper.brain.types")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
--- Brain Learners Coordinator
|
||||
--- Routes learning events to appropriate learners
|
||||
|
||||
local types = require("codetyper.brain.types")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Lazy load learners
|
||||
local function get_pattern_learner()
|
||||
return require("codetyper.brain.learners.pattern")
|
||||
return require("codetyper.core.memory.learners.pattern")
|
||||
end
|
||||
|
||||
local function get_correction_learner()
|
||||
return require("codetyper.brain.learners.correction")
|
||||
return require("codetyper.core.memory.learners.correction")
|
||||
end
|
||||
|
||||
local function get_convention_learner()
|
||||
return require("codetyper.brain.learners.convention")
|
||||
return require("codetyper.core.memory.learners.convention")
|
||||
end
|
||||
|
||||
--- All available learners
|
||||
@@ -99,7 +99,7 @@ function M.create_learning(learner, data, event)
|
||||
local params = learner.create_node_params(data)
|
||||
|
||||
-- Get graph module
|
||||
local graph = require("codetyper.brain.graph")
|
||||
local graph = require("codetyper.core.memory.graph")
|
||||
|
||||
-- Find related nodes
|
||||
local related_ids = {}
|
||||
@@ -125,7 +125,7 @@ end
|
||||
---@return string|nil Created node ID
|
||||
function M.process_feedback(event)
|
||||
local data = event.data or {}
|
||||
local graph = require("codetyper.brain.graph")
|
||||
local graph = require("codetyper.core.memory.graph")
|
||||
|
||||
local content = {
|
||||
s = "Feedback: " .. (data.feedback or "unknown"),
|
||||
@@ -167,7 +167,7 @@ end
|
||||
---@return string|nil Created node ID
|
||||
function M.process_session(event)
|
||||
local data = event.data or {}
|
||||
local graph = require("codetyper.brain.graph")
|
||||
local graph = require("codetyper.core.memory.graph")
|
||||
|
||||
local content = {
|
||||
s = event.type == "session_start" and "Session started" or "Session ended",
|
||||
@@ -1,7 +1,7 @@
|
||||
--- Brain Pattern Learner
|
||||
--- Detects and learns code patterns
|
||||
|
||||
local types = require("codetyper.brain.types")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
--- Brain Output Formatter
|
||||
--- LLM-optimized output formatting
|
||||
|
||||
local types = require("codetyper.brain.types")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
--- Brain Output Coordinator
|
||||
--- Manages LLM context generation
|
||||
|
||||
local formatter = require("codetyper.brain.output.formatter")
|
||||
local formatter = require("codetyper.core.memory.output.formatter")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -17,7 +17,7 @@ local DEFAULT_MAX_TOKENS = 4000
|
||||
function M.generate(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
if not brain.is_initialized() then
|
||||
return ""
|
||||
end
|
||||
@@ -138,7 +138,7 @@ end
|
||||
--- Check if context is available
|
||||
---@return boolean
|
||||
function M.has_context()
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
if not brain.is_initialized() then
|
||||
return false
|
||||
end
|
||||
@@ -150,7 +150,7 @@ end
|
||||
--- Get context stats
|
||||
---@return table Stats
|
||||
function M.stats()
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
if not brain.is_initialized() then
|
||||
return { available = false }
|
||||
end
|
||||
@@ -1,8 +1,8 @@
|
||||
--- Brain Storage Layer
|
||||
--- Cache + disk persistence with lazy loading
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local types = require("codetyper.brain.types")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local types = require("codetyper.core.memory.types")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
--- Executes tools requested by the LLM and returns results.
|
||||
|
||||
local M = {}
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
---@class ExecutionResult
|
||||
---@field success boolean Whether the execution succeeded
|
||||
@@ -11,6 +12,72 @@ local utils = require("codetyper.utils")
|
||||
---@field requires_approval boolean Whether user approval is needed
|
||||
---@field diff_data? DiffData Data for diff preview (if requires_approval)
|
||||
|
||||
--- Open a file in a buffer (in a non-agent window)
|
||||
---@param path string File path to open
|
||||
---@param jump_to_line? number Optional line number to jump to
|
||||
local function open_file_in_buffer(path, jump_to_line)
|
||||
if not path or path == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if file exists
|
||||
if vim.fn.filereadable(path) ~= 1 then
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
-- Find a suitable window (not the agent UI windows)
|
||||
local target_win = nil
|
||||
local agent_ui_ok, agent_ui = pcall(require, "codetyper.agent.ui")
|
||||
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
local buftype = vim.bo[buf].buftype
|
||||
|
||||
-- Skip special buffers (agent UI, nofile, etc.)
|
||||
if buftype == "" or buftype == "acwrite" then
|
||||
-- Check if this is not an agent UI window
|
||||
local is_agent_win = false
|
||||
if agent_ui_ok and agent_ui.is_open() then
|
||||
-- Skip agent windows by checking if it's one of our special buffers
|
||||
local bufname = vim.api.nvim_buf_get_name(buf)
|
||||
if bufname == "" then
|
||||
-- Could be agent buffer, check by buffer option
|
||||
is_agent_win = vim.bo[buf].buftype == "nofile"
|
||||
end
|
||||
end
|
||||
|
||||
if not is_agent_win then
|
||||
target_win = win
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If no suitable window found, create a new split
|
||||
if not target_win then
|
||||
-- Get the rightmost non-agent window or create one
|
||||
vim.cmd("rightbelow vsplit")
|
||||
target_win = vim.api.nvim_get_current_win()
|
||||
end
|
||||
|
||||
-- Open the file in the target window
|
||||
vim.api.nvim_set_current_win(target_win)
|
||||
vim.cmd("edit " .. vim.fn.fnameescape(path))
|
||||
|
||||
-- Jump to line if specified
|
||||
if jump_to_line and jump_to_line > 0 then
|
||||
local line_count = vim.api.nvim_buf_line_count(0)
|
||||
local target_line = math.min(jump_to_line, line_count)
|
||||
vim.api.nvim_win_set_cursor(target_win, { target_line, 0 })
|
||||
vim.cmd("normal! zz")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Expose open_file_in_buffer for external use
|
||||
M.open_file_in_buffer = open_file_in_buffer
|
||||
|
||||
---@class DiffData
|
||||
---@field path string File path
|
||||
---@field original string Original content
|
||||
@@ -50,15 +117,28 @@ end
|
||||
---@param callback fun(result: ExecutionResult)
|
||||
function M.handle_read_file(params, callback)
|
||||
local path = M.resolve_path(params.path)
|
||||
|
||||
-- Log the read operation in Claude Code style
|
||||
local relative_path = vim.fn.fnamemodify(path, ":~:.")
|
||||
logs.read(relative_path)
|
||||
|
||||
local content = utils.read_file(path)
|
||||
|
||||
if content then
|
||||
-- Log how many lines were read
|
||||
local lines = vim.split(content, "\n", { plain = true })
|
||||
logs.add({ type = "result", message = string.format(" ⎿ Read %d lines", #lines) })
|
||||
|
||||
-- Open the file in a buffer so user can see it
|
||||
open_file_in_buffer(path)
|
||||
|
||||
callback({
|
||||
success = true,
|
||||
result = content,
|
||||
requires_approval = false,
|
||||
})
|
||||
else
|
||||
logs.add({ type = "error", message = " ⎿ File not found" })
|
||||
callback({
|
||||
success = false,
|
||||
result = "Could not read file: " .. path,
|
||||
@@ -72,9 +152,15 @@ end
|
||||
---@param callback fun(result: ExecutionResult)
|
||||
function M.handle_edit_file(params, callback)
|
||||
local path = M.resolve_path(params.path)
|
||||
local relative_path = vim.fn.fnamemodify(path, ":~:.")
|
||||
|
||||
-- Log the edit operation
|
||||
logs.add({ type = "action", message = string.format("Edit(%s)", relative_path) })
|
||||
|
||||
local original = utils.read_file(path)
|
||||
|
||||
if not original then
|
||||
logs.add({ type = "error", message = " ⎿ File not found" })
|
||||
callback({
|
||||
success = false,
|
||||
result = "File not found: " .. path,
|
||||
@@ -88,6 +174,7 @@ function M.handle_edit_file(params, callback)
|
||||
local new_content, count = original:gsub(escaped_find, params.replace, 1)
|
||||
|
||||
if count == 0 then
|
||||
logs.add({ type = "error", message = " ⎿ Content not found" })
|
||||
callback({
|
||||
success = false,
|
||||
result = "Could not find content to replace in: " .. path,
|
||||
@@ -96,6 +183,18 @@ function M.handle_edit_file(params, callback)
|
||||
return
|
||||
end
|
||||
|
||||
-- Calculate lines changed
|
||||
local original_lines = #vim.split(original, "\n", { plain = true })
|
||||
local new_lines = #vim.split(new_content, "\n", { plain = true })
|
||||
local diff = new_lines - original_lines
|
||||
if diff > 0 then
|
||||
logs.add({ type = "result", message = string.format(" ⎿ +%d lines (pending approval)", diff) })
|
||||
elseif diff < 0 then
|
||||
logs.add({ type = "result", message = string.format(" ⎿ %d lines (pending approval)", diff) })
|
||||
else
|
||||
logs.add({ type = "result", message = " ⎿ Modified (pending approval)" })
|
||||
end
|
||||
|
||||
-- Requires user approval - show diff
|
||||
callback({
|
||||
success = true,
|
||||
@@ -115,9 +214,29 @@ end
|
||||
---@param callback fun(result: ExecutionResult)
|
||||
function M.handle_write_file(params, callback)
|
||||
local path = M.resolve_path(params.path)
|
||||
local relative_path = vim.fn.fnamemodify(path, ":~:.")
|
||||
local original = utils.read_file(path) or ""
|
||||
local operation = original == "" and "create" or "overwrite"
|
||||
|
||||
-- Log the write operation
|
||||
if operation == "create" then
|
||||
logs.add({ type = "action", message = string.format("Write(%s)", relative_path) })
|
||||
local new_lines = #vim.split(params.content, "\n", { plain = true })
|
||||
logs.add({ type = "result", message = string.format(" ⎿ New file (%d lines, pending approval)", new_lines) })
|
||||
else
|
||||
logs.add({ type = "action", message = string.format("Update(%s)", relative_path) })
|
||||
local original_lines = #vim.split(original, "\n", { plain = true })
|
||||
local new_lines = #vim.split(params.content, "\n", { plain = true })
|
||||
local diff = new_lines - original_lines
|
||||
if diff > 0 then
|
||||
logs.add({ type = "result", message = string.format(" ⎿ +%d lines (pending approval)", diff) })
|
||||
elseif diff < 0 then
|
||||
logs.add({ type = "result", message = string.format(" ⎿ %d lines (pending approval)", diff) })
|
||||
else
|
||||
logs.add({ type = "result", message = " ⎿ Modified (pending approval)" })
|
||||
end
|
||||
end
|
||||
|
||||
-- Ensure parent directory exists
|
||||
local dir = vim.fn.fnamemodify(path, ":h")
|
||||
if dir ~= "" and dir ~= "." then
|
||||
@@ -143,6 +262,10 @@ end
|
||||
function M.handle_bash(params, callback)
|
||||
local command = params.command
|
||||
|
||||
-- Log the bash operation
|
||||
logs.add({ type = "action", message = string.format("Bash(%s)", command:sub(1, 50) .. (#command > 50 and "..." or "")) })
|
||||
logs.add({ type = "result", message = " ⎿ Pending approval" })
|
||||
|
||||
-- Requires user approval first
|
||||
callback({
|
||||
success = true,
|
||||
@@ -258,7 +381,8 @@ function M.handle_search_files(params, callback)
|
||||
for _, file in ipairs(files) do
|
||||
-- Skip common ignore patterns
|
||||
if not file:match("node_modules") and not file:match("%.git/") then
|
||||
table.insert(results, file:gsub(search_path .. "/", ""))
|
||||
local relative = file:gsub(search_path .. "/", "")
|
||||
table.insert(results, relative)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -272,7 +396,8 @@ function M.handle_search_files(params, callback)
|
||||
if handle then
|
||||
for line in handle:lines() do
|
||||
if not line:match("node_modules") and not line:match("%.git/") then
|
||||
table.insert(grep_results, line:gsub(search_path .. "/", ""))
|
||||
local relative = line:gsub(search_path .. "/", "")
|
||||
table.insert(grep_results, relative)
|
||||
end
|
||||
end
|
||||
handle:close()
|
||||
@@ -348,7 +473,8 @@ function M.apply_change(diff_data, callback)
|
||||
-- Write file
|
||||
local success = utils.write_file(diff_data.path, diff_data.modified)
|
||||
if success then
|
||||
-- Reload buffer if it's open
|
||||
-- Open and/or reload buffer so user can see the changes
|
||||
open_file_in_buffer(diff_data.path)
|
||||
M.reload_buffer_if_open(diff_data.path)
|
||||
callback({
|
||||
success = true,
|
||||
@@ -1,11 +1,13 @@
|
||||
---@mod codetyper.agent.loop Agent loop with tool orchestration
|
||||
---@brief [[
|
||||
--- Main agent loop that handles multi-turn conversations with tool use.
|
||||
--- Inspired by avante.nvim's agent_loop pattern.
|
||||
--- Agent execution loop with tool calling support.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
local prompts = require("codetyper.prompts.agents.loop")
|
||||
|
||||
---@class AgentMessage
|
||||
---@field role "system"|"user"|"assistant"|"tool"
|
||||
---@field content string|table
|
||||
@@ -142,8 +144,8 @@ end
|
||||
--- Execute the agent loop
|
||||
---@param opts AgentLoopOpts
|
||||
function M.run(opts)
|
||||
local tools_mod = require("codetyper.agent.tools")
|
||||
local llm = require("codetyper.llm")
|
||||
local tools_mod = require("codetyper.core.tools")
|
||||
local llm = require("codetyper.core.llm")
|
||||
|
||||
-- Get tools
|
||||
local tools = opts.tools or tools_mod.list()
|
||||
@@ -278,7 +280,7 @@ function M.run(opts)
|
||||
local tool_opts = {
|
||||
on_log = function(msg)
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({ type = "tool", message = msg })
|
||||
end)
|
||||
end,
|
||||
@@ -345,24 +347,7 @@ end
|
||||
function M.create(task, opts)
|
||||
opts = opts or {}
|
||||
|
||||
local system_prompt = opts.system_prompt or [[You are a helpful coding assistant with access to tools.
|
||||
|
||||
Available tools:
|
||||
- view: Read file contents
|
||||
- grep: Search for patterns in files
|
||||
- glob: Find files by pattern
|
||||
- edit: Make targeted edits to files
|
||||
- write: Create or overwrite files
|
||||
- bash: Execute shell commands
|
||||
|
||||
When you need to perform a task:
|
||||
1. Use tools to gather information
|
||||
2. Plan your approach
|
||||
3. Execute changes using appropriate tools
|
||||
4. Verify the results
|
||||
|
||||
Always explain your reasoning before using tools.
|
||||
When you're done, provide a clear summary of what was accomplished.]]
|
||||
local system_prompt = opts.system_prompt or prompts.default_system_prompt
|
||||
|
||||
M.run(vim.tbl_extend("force", opts, {
|
||||
system_prompt = system_prompt,
|
||||
@@ -378,15 +363,13 @@ function M.dispatch(prompt, on_complete, opts)
|
||||
opts = opts or {}
|
||||
|
||||
-- Sub-agents get limited tools by default
|
||||
local tools_mod = require("codetyper.agent.tools")
|
||||
local tools_mod = require("codetyper.core.tools")
|
||||
local safe_tools = tools_mod.list(function(tool)
|
||||
return tool.name == "view" or tool.name == "grep" or tool.name == "glob"
|
||||
end)
|
||||
|
||||
M.run({
|
||||
system_prompt = [[You are a research assistant. Your task is to find information and report back.
|
||||
You have access to: view (read files), grep (search content), glob (find files).
|
||||
Be thorough and report your findings clearly.]],
|
||||
system_prompt = prompts.dispatch_prompt,
|
||||
user_input = prompt,
|
||||
tools = opts.tools or safe_tools,
|
||||
max_iterations = opts.max_iterations or 5,
|
||||
155
lua/codetyper/core/scheduler/resume.lua
Normal file
155
lua/codetyper/core/scheduler/resume.lua
Normal file
@@ -0,0 +1,155 @@
|
||||
---@mod codetyper.agent.resume Resume context for agent sessions
|
||||
---
|
||||
--- Saves and loads agent state to allow continuing long-running tasks.
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Get the resume context directory
|
||||
---@return string|nil
|
||||
local function get_resume_dir()
|
||||
local root = utils.get_project_root() or vim.fn.getcwd()
|
||||
return root .. "/.coder/tmp"
|
||||
end
|
||||
|
||||
--- Get the resume context file path
|
||||
---@return string|nil
|
||||
local function get_resume_path()
|
||||
local dir = get_resume_dir()
|
||||
if not dir then
|
||||
return nil
|
||||
end
|
||||
return dir .. "/agent_resume.json"
|
||||
end
|
||||
|
||||
--- Ensure the resume directory exists
|
||||
---@return boolean
|
||||
local function ensure_resume_dir()
|
||||
local dir = get_resume_dir()
|
||||
if not dir then
|
||||
return false
|
||||
end
|
||||
return utils.ensure_dir(dir)
|
||||
end
|
||||
|
||||
---@class ResumeContext
|
||||
---@field conversation table[] Message history
|
||||
---@field pending_tool_results table[] Pending results
|
||||
---@field iteration number Current iteration count
|
||||
---@field original_prompt string Original user prompt
|
||||
---@field timestamp number When saved
|
||||
---@field project_root string Project root path
|
||||
|
||||
--- Save the current agent state for resuming later
|
||||
---@param conversation table[] Conversation history
|
||||
---@param pending_results table[] Pending tool results
|
||||
---@param iteration number Current iteration
|
||||
---@param original_prompt string Original prompt
|
||||
---@return boolean Success
|
||||
function M.save(conversation, pending_results, iteration, original_prompt)
|
||||
if not ensure_resume_dir() then
|
||||
return false
|
||||
end
|
||||
|
||||
local path = get_resume_path()
|
||||
if not path then
|
||||
return false
|
||||
end
|
||||
|
||||
local context = {
|
||||
conversation = conversation,
|
||||
pending_tool_results = pending_results,
|
||||
iteration = iteration,
|
||||
original_prompt = original_prompt,
|
||||
timestamp = os.time(),
|
||||
project_root = utils.get_project_root() or vim.fn.getcwd(),
|
||||
}
|
||||
|
||||
local ok, json = pcall(vim.json.encode, context)
|
||||
if not ok then
|
||||
utils.notify("Failed to encode resume context", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local success = utils.write_file(path, json)
|
||||
if success then
|
||||
utils.notify("Agent state saved. Use /continue to resume.", vim.log.levels.INFO)
|
||||
end
|
||||
return success
|
||||
end
|
||||
|
||||
--- Load saved agent state
|
||||
---@return ResumeContext|nil
|
||||
function M.load()
|
||||
local path = get_resume_path()
|
||||
if not path then
|
||||
return nil
|
||||
end
|
||||
|
||||
local content = utils.read_file(path)
|
||||
if not content or content == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ok, context = pcall(vim.json.decode, content)
|
||||
if not ok or not context then
|
||||
return nil
|
||||
end
|
||||
|
||||
return context
|
||||
end
|
||||
|
||||
--- Check if there's a saved resume context
|
||||
---@return boolean
|
||||
function M.has_saved_state()
|
||||
local path = get_resume_path()
|
||||
if not path then
|
||||
return false
|
||||
end
|
||||
return vim.fn.filereadable(path) == 1
|
||||
end
|
||||
|
||||
--- Get info about saved state (for display)
|
||||
---@return table|nil
|
||||
function M.get_info()
|
||||
local context = M.load()
|
||||
if not context then
|
||||
return nil
|
||||
end
|
||||
|
||||
local age_seconds = os.time() - (context.timestamp or 0)
|
||||
local age_str
|
||||
if age_seconds < 60 then
|
||||
age_str = age_seconds .. " seconds ago"
|
||||
elseif age_seconds < 3600 then
|
||||
age_str = math.floor(age_seconds / 60) .. " minutes ago"
|
||||
else
|
||||
age_str = math.floor(age_seconds / 3600) .. " hours ago"
|
||||
end
|
||||
|
||||
return {
|
||||
prompt = context.original_prompt,
|
||||
iteration = context.iteration,
|
||||
messages = #context.conversation,
|
||||
saved_at = age_str,
|
||||
project = context.project_root,
|
||||
}
|
||||
end
|
||||
|
||||
--- Clear saved resume context
|
||||
---@return boolean
|
||||
function M.clear()
|
||||
local path = get_resume_path()
|
||||
if not path then
|
||||
return false
|
||||
end
|
||||
|
||||
if vim.fn.filereadable(path) == 1 then
|
||||
os.remove(path)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local queue = require("codetyper.agent.queue")
|
||||
local patch = require("codetyper.agent.patch")
|
||||
local worker = require("codetyper.agent.worker")
|
||||
local confidence_mod = require("codetyper.agent.confidence")
|
||||
local context_modal = require("codetyper.agent.context_modal")
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
local patch = require("codetyper.core.diff.patch")
|
||||
local worker = require("codetyper.core.scheduler.worker")
|
||||
local confidence_mod = require("codetyper.core.llm.confidence")
|
||||
local context_modal = require("codetyper.adapters.nvim.ui.context_modal")
|
||||
local params = require("codetyper.params.agents.scheduler")
|
||||
|
||||
-- Setup context modal cleanup on exit
|
||||
context_modal.setup()
|
||||
@@ -21,15 +22,7 @@ local state = {
|
||||
timer = nil,
|
||||
poll_interval = 100, -- ms
|
||||
paused = false,
|
||||
config = {
|
||||
enabled = true,
|
||||
ollama_scout = true,
|
||||
escalation_threshold = 0.7,
|
||||
max_concurrent = 2,
|
||||
completion_delay_ms = 100,
|
||||
apply_delay_ms = 5000, -- Wait before applying code
|
||||
remote_provider = "copilot", -- Default fallback provider
|
||||
},
|
||||
config = params.config,
|
||||
}
|
||||
|
||||
--- Autocommand group for injection timing
|
||||
@@ -124,7 +117,7 @@ end
|
||||
--- Retry event with additional context
|
||||
---@param original_event table Original prompt event
|
||||
---@param additional_context string Additional context from user
|
||||
local function retry_with_context(original_event, additional_context)
|
||||
local function retry_with_context(original_event, additional_context, attached_files)
|
||||
-- Create new prompt content combining original + additional
|
||||
local combined_prompt = string.format(
|
||||
"%s\n\nAdditional context:\n%s",
|
||||
@@ -138,10 +131,14 @@ local function retry_with_context(original_event, additional_context)
|
||||
new_event.prompt_content = combined_prompt
|
||||
new_event.attempt_count = 0
|
||||
new_event.status = nil
|
||||
-- Preserve any attached files provided by the context modal
|
||||
if attached_files and #attached_files > 0 then
|
||||
new_event.attached_files = attached_files
|
||||
end
|
||||
|
||||
-- Log the retry
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Retrying with additional context (original: %s)", original_event.id),
|
||||
@@ -152,6 +149,79 @@ local function retry_with_context(original_event, additional_context)
|
||||
queue.enqueue(new_event)
|
||||
end
|
||||
|
||||
--- Try to parse requested file paths from an LLM response asking for more context
|
||||
---@param response string
|
||||
---@return string[] list of resolved full paths
|
||||
local function parse_requested_files(response)
|
||||
if not response or response == "" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local cwd = vim.fn.getcwd()
|
||||
local results = {}
|
||||
local seen = {}
|
||||
|
||||
-- Heuristics: capture backticked paths, lines starting with - or *, or raw paths with slashes and extension
|
||||
for path in response:gmatch("`([%w%._%-%/]+%.[%w_]+)`") do
|
||||
if not seen[path] then
|
||||
table.insert(results, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
|
||||
for path in response:gmatch("([%w%._%-%/]+%.[%w_]+)") do
|
||||
if not seen[path] then
|
||||
-- Filter out common English words that match the pattern
|
||||
if not path:match("^[Ii]$") and not path:match("^[Tt]his$") then
|
||||
table.insert(results, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Also capture list items like '- src/foo.lua'
|
||||
for line in response:gmatch("[^\\n]+") do
|
||||
local m = line:match("^%s*[-*]%s*([%w%._%-%/]+%.[%w_]+)%s*$")
|
||||
if m and not seen[m] then
|
||||
table.insert(results, m)
|
||||
seen[m] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Resolve each candidate to a full path by checking cwd and globbing
|
||||
local resolved = {}
|
||||
for _, p in ipairs(results) do
|
||||
local candidate = p
|
||||
local full = nil
|
||||
|
||||
-- If absolute or already rooted
|
||||
if candidate:sub(1,1) == "/" and vim.fn.filereadable(candidate) == 1 then
|
||||
full = candidate
|
||||
else
|
||||
-- Try relative to cwd
|
||||
local try1 = cwd .. "/" .. candidate
|
||||
if vim.fn.filereadable(try1) == 1 then
|
||||
full = try1
|
||||
else
|
||||
-- Try globbing for filename anywhere in project
|
||||
local basename = candidate
|
||||
-- If candidate contains slashes, try the tail
|
||||
local tail = candidate:match("[^/]+$") or candidate
|
||||
local matches = vim.fn.globpath(cwd, "**/" .. tail, false, true)
|
||||
if matches and #matches > 0 then
|
||||
full = matches[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if full and vim.fn.filereadable(full) == 1 then
|
||||
table.insert(resolved, full)
|
||||
end
|
||||
end
|
||||
|
||||
return resolved
|
||||
end
|
||||
|
||||
--- Process worker result and decide next action
|
||||
---@param event table PromptEvent
|
||||
---@param result table WorkerResult
|
||||
@@ -159,14 +229,94 @@ local function handle_worker_result(event, result)
|
||||
-- Check if LLM needs more context
|
||||
if result.needs_context then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Event %s: LLM needs more context, opening modal", event.id),
|
||||
})
|
||||
end)
|
||||
|
||||
-- Open the context modal
|
||||
-- Try to auto-attach any files the LLM specifically requested in its response
|
||||
local requested = parse_requested_files(result.response or "")
|
||||
|
||||
-- Detect suggested shell commands the LLM may want executed (e.g., "run ls -la", "please run git status")
|
||||
local function detect_suggested_commands(response)
|
||||
if not response then
|
||||
return {}
|
||||
end
|
||||
local cmds = {}
|
||||
-- capture backticked commands: `ls -la`
|
||||
for c in response:gmatch("`([^`]+)`") do
|
||||
if #c > 1 and not c:match("%-%-help") then
|
||||
table.insert(cmds, { label = c, cmd = c })
|
||||
end
|
||||
end
|
||||
-- capture phrases like: run ls -la or run `ls -la`
|
||||
for m in response:gmatch("[Rr]un%s+([%w%p%s%-_/]+)") do
|
||||
local cand = m:gsub("^%s+",""):gsub("%s+$","")
|
||||
if cand and #cand > 1 then
|
||||
-- ignore long sentences; keep first line or command-like substring
|
||||
local line = cand:match("[^\n]+") or cand
|
||||
line = line:gsub("and then.*","")
|
||||
line = line:gsub("please.*","")
|
||||
if not line:match("%a+%s+files") then
|
||||
table.insert(cmds, { label = line, cmd = line })
|
||||
end
|
||||
end
|
||||
end
|
||||
-- dedupe
|
||||
local seen = {}
|
||||
local out = {}
|
||||
for _, v in ipairs(cmds) do
|
||||
if v.cmd and not seen[v.cmd] then
|
||||
seen[v.cmd] = true
|
||||
table.insert(out, v)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local suggested_cmds = detect_suggested_commands(result.response or "")
|
||||
if suggested_cmds and #suggested_cmds > 0 then
|
||||
-- Open modal and show suggested commands for user approval
|
||||
context_modal.open(result.original_event or event, result.response or "", retry_with_context, suggested_cmds)
|
||||
queue.update_status(event.id, "needs_context", { response = result.response })
|
||||
return
|
||||
end
|
||||
if requested and #requested > 0 then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({ type = "info", message = string.format("Auto-attaching %d requested file(s)", #requested) })
|
||||
end)
|
||||
|
||||
-- Build attached_files entries
|
||||
local attached = event.attached_files or {}
|
||||
for _, full in ipairs(requested) do
|
||||
local ok, content = pcall(function()
|
||||
return table.concat(vim.fn.readfile(full), "\n")
|
||||
end)
|
||||
if ok and content then
|
||||
table.insert(attached, {
|
||||
path = vim.fn.fnamemodify(full, ":~:."),
|
||||
full_path = full,
|
||||
content = content,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Retry automatically with same prompt but attached files
|
||||
local new_event = vim.deepcopy(result.original_event or event)
|
||||
new_event.id = nil
|
||||
new_event.attached_files = attached
|
||||
new_event.attempt_count = 0
|
||||
new_event.status = nil
|
||||
queue.enqueue(new_event)
|
||||
|
||||
queue.update_status(event.id, "needs_context", { response = result.response })
|
||||
return
|
||||
end
|
||||
|
||||
-- If no files parsed, open modal for manual context entry
|
||||
context_modal.open(result.original_event or event, result.response or "", retry_with_context)
|
||||
|
||||
-- Mark original event as needing context (not failed)
|
||||
@@ -178,7 +328,7 @@ local function handle_worker_result(event, result)
|
||||
-- Failed - try escalation if this was ollama
|
||||
if result.worker_type == "ollama" and event.attempt_count < 2 then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format(
|
||||
@@ -208,7 +358,7 @@ local function handle_worker_result(event, result)
|
||||
if needs_escalation and result.worker_type == "ollama" and event.attempt_count < 2 then
|
||||
-- Low confidence from ollama - escalate to remote
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format(
|
||||
@@ -233,7 +383,7 @@ local function handle_worker_result(event, result)
|
||||
-- Schedule patch application after delay (gives user time to review/cancel)
|
||||
local delay = state.config.apply_delay_ms or 5000
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Code ready. Applying in %.1f seconds...", delay / 1000),
|
||||
@@ -266,7 +416,7 @@ local function dispatch_next()
|
||||
local should_skip, skip_reason = queue.check_precedence(event)
|
||||
if should_skip then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "warning",
|
||||
message = string.format("Event %s skipped: %s", event.id, skip_reason or "conflict"),
|
||||
@@ -282,7 +432,7 @@ local function dispatch_next()
|
||||
|
||||
-- Log dispatch with intent/scope info
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
local intent_info = event.intent and event.intent.type or "unknown"
|
||||
local scope_info = event.scope and event.scope.type ~= "file"
|
||||
and string.format("%s:%s", event.scope.type, event.scope.name or "anon")
|
||||
@@ -321,10 +471,10 @@ function M.schedule_patch_flush()
|
||||
local safe, reason = M.is_safe_to_inject()
|
||||
if safe then
|
||||
waiting_to_flush = false
|
||||
local applied, stale = patch.flush_pending()
|
||||
local applied, stale = patch.flush_pending_smart()
|
||||
if applied > 0 or stale > 0 then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Patches flushed: %d applied, %d stale", applied, stale),
|
||||
@@ -337,7 +487,7 @@ function M.schedule_patch_flush()
|
||||
if not waiting_to_flush then
|
||||
waiting_to_flush = true
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Waiting for user to finish typing before applying code...",
|
||||
@@ -382,7 +532,7 @@ local function setup_autocmds()
|
||||
callback = function()
|
||||
vim.defer_fn(function()
|
||||
if not M.is_completion_visible() then
|
||||
patch.flush_pending()
|
||||
patch.flush_pending_smart()
|
||||
end
|
||||
end, state.config.completion_delay_ms)
|
||||
end,
|
||||
@@ -394,7 +544,7 @@ local function setup_autocmds()
|
||||
group = augroup,
|
||||
callback = function()
|
||||
if not M.is_insert_mode() and not M.is_completion_visible() then
|
||||
patch.flush_pending()
|
||||
patch.flush_pending_smart()
|
||||
end
|
||||
end,
|
||||
desc = "Flush pending patches on CursorHold",
|
||||
@@ -478,7 +628,7 @@ function M.start(config)
|
||||
scheduler_loop()
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Scheduler started",
|
||||
@@ -510,7 +660,7 @@ function M.stop()
|
||||
end
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Scheduler stopped",
|
||||
@@ -563,7 +713,7 @@ end
|
||||
|
||||
--- Force flush all pending patches (ignores completion check)
|
||||
function M.force_flush()
|
||||
return patch.flush_pending()
|
||||
return patch.flush_pending_smart()
|
||||
end
|
||||
|
||||
--- Update configuration
|
||||
@@ -574,4 +724,33 @@ function M.configure(config)
|
||||
end
|
||||
end
|
||||
|
||||
--- Queue a prompt for processing
|
||||
--- This is a convenience function that creates a proper PromptEvent and enqueues it
|
||||
---@param opts table Prompt options
|
||||
--- - bufnr: number Source buffer number
|
||||
--- - filepath: string Source file path
|
||||
--- - target_path: string Target file for injection (can be same as filepath)
|
||||
--- - prompt_content: string The cleaned prompt text
|
||||
--- - range: {start_line: number, end_line: number} Line range of prompt tag
|
||||
--- - source: string|nil Source identifier (e.g., "transform_command", "autocmd")
|
||||
--- - priority: number|nil Priority (1=high, 2=normal, 3=low) default 2
|
||||
---@return table The enqueued event
|
||||
function M.queue_prompt(opts)
|
||||
-- Build the PromptEvent structure
|
||||
local event = {
|
||||
bufnr = opts.bufnr,
|
||||
filepath = opts.filepath,
|
||||
target_path = opts.target_path or opts.filepath,
|
||||
prompt_content = opts.prompt_content,
|
||||
range = opts.range,
|
||||
priority = opts.priority or 2,
|
||||
source = opts.source or "manual",
|
||||
-- Capture buffer state for staleness detection
|
||||
changedtick = vim.api.nvim_buf_get_changedtick(opts.bufnr),
|
||||
}
|
||||
|
||||
-- Enqueue through the queue module
|
||||
return queue.enqueue(event)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local confidence = require("codetyper.agent.confidence")
|
||||
local params = require("codetyper.params.agents.worker")
|
||||
local confidence = require("codetyper.core.llm.confidence")
|
||||
|
||||
---@class WorkerResult
|
||||
---@field success boolean Whether the request succeeded
|
||||
@@ -32,20 +33,7 @@ local confidence = require("codetyper.agent.confidence")
|
||||
local worker_counter = 0
|
||||
|
||||
--- Patterns that indicate LLM needs more context (must be near start of response)
|
||||
local context_needed_patterns = {
|
||||
"^%s*i need more context",
|
||||
"^%s*i'm sorry.-i need more",
|
||||
"^%s*i apologize.-i need more",
|
||||
"^%s*could you provide more context",
|
||||
"^%s*could you please provide more",
|
||||
"^%s*can you clarify",
|
||||
"^%s*please provide more context",
|
||||
"^%s*more information needed",
|
||||
"^%s*not enough context",
|
||||
"^%s*i don't have enough",
|
||||
"^%s*unclear what you",
|
||||
"^%s*what do you mean by",
|
||||
}
|
||||
local context_needed_patterns = params.context_needed_patterns
|
||||
|
||||
--- Check if response indicates need for more context
|
||||
--- Only triggers if the response primarily asks for context (no substantial code)
|
||||
@@ -83,6 +71,19 @@ local function needs_more_context(response)
|
||||
return false
|
||||
end
|
||||
|
||||
--- Check if response contains SEARCH/REPLACE blocks
|
||||
---@param response string
|
||||
---@return boolean
|
||||
local function has_search_replace_blocks(response)
|
||||
if not response then
|
||||
return false
|
||||
end
|
||||
-- Check for any of the supported SEARCH/REPLACE formats
|
||||
return response:match("<<<<<<<%s*SEARCH") ~= nil
|
||||
or response:match("%-%-%-%-%-%-%-?%s*SEARCH") ~= nil
|
||||
or response:match("%[SEARCH%]") ~= nil
|
||||
end
|
||||
|
||||
--- Clean LLM response to extract only code
|
||||
---@param response string Raw LLM response
|
||||
---@param filetype string|nil File type for language detection
|
||||
@@ -107,6 +108,13 @@ local function clean_response(response, filetype)
|
||||
-- Use [%s%S] to match any character including newlines (Lua's . doesn't match newlines)
|
||||
cleaned = cleaned:gsub("/@[%s%S]-@/", "")
|
||||
|
||||
-- IMPORTANT: If response contains SEARCH/REPLACE blocks, preserve them!
|
||||
-- Don't extract from markdown or remove "explanations" that are actually part of the format
|
||||
if has_search_replace_blocks(cleaned) then
|
||||
-- Just trim whitespace and return - the blocks will be parsed by search_replace module
|
||||
return cleaned:match("^%s*(.-)%s*$") or cleaned
|
||||
end
|
||||
|
||||
-- Try to extract code from markdown code blocks
|
||||
-- Match ```language\n...\n``` or just ```\n...\n```
|
||||
local code_block = cleaned:match("```[%w]*\n(.-)\n```")
|
||||
@@ -176,12 +184,7 @@ end
|
||||
local active_workers = {}
|
||||
|
||||
--- Default timeouts by provider type
|
||||
local default_timeouts = {
|
||||
ollama = 30000, -- 30s for local
|
||||
openai = 60000, -- 60s for remote
|
||||
gemini = 60000,
|
||||
copilot = 60000,
|
||||
}
|
||||
local default_timeouts = params.default_timeouts
|
||||
|
||||
--- Generate worker ID
|
||||
---@return string
|
||||
@@ -352,20 +355,61 @@ local function format_indexed_context(indexed_context)
|
||||
return "\n\n--- Project Context ---\n" .. table.concat(parts, "\n")
|
||||
end
|
||||
|
||||
--- Check if this is an inline prompt (tags in target file, not a coder file)
|
||||
---@param event table
|
||||
---@return boolean
|
||||
local function is_inline_prompt(event)
|
||||
-- Inline prompts have a range with start_line/end_line from tag detection
|
||||
-- and the source file is the same as target (not a .coder. file)
|
||||
if not event.range or not event.range.start_line then
|
||||
return false
|
||||
end
|
||||
-- Check if source path (if any) equals target, or if target has no .coder. in it
|
||||
local target = event.target_path or ""
|
||||
if target:match("%.coder%.") then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Build file content with marked region for inline prompts
|
||||
---@param lines string[] File lines
|
||||
---@param start_line number 1-indexed
|
||||
---@param end_line number 1-indexed
|
||||
---@param prompt_content string The prompt inside the tags
|
||||
---@return string
|
||||
local function build_marked_file_content(lines, start_line, end_line, prompt_content)
|
||||
local result = {}
|
||||
for i, line in ipairs(lines) do
|
||||
if i == start_line then
|
||||
-- Mark the start of the region to be replaced
|
||||
table.insert(result, ">>> REPLACE THIS REGION (lines " .. start_line .. "-" .. end_line .. ") <<<")
|
||||
table.insert(result, "--- User request: " .. prompt_content:gsub("\n", " "):sub(1, 100) .. " ---")
|
||||
end
|
||||
table.insert(result, line)
|
||||
if i == end_line then
|
||||
table.insert(result, ">>> END OF REGION TO REPLACE <<<")
|
||||
end
|
||||
end
|
||||
return table.concat(result, "\n")
|
||||
end
|
||||
|
||||
--- Build prompt for code generation
|
||||
---@param event table PromptEvent
|
||||
---@return string prompt
|
||||
---@return table context
|
||||
local function build_prompt(event)
|
||||
local intent_mod = require("codetyper.agent.intent")
|
||||
local intent_mod = require("codetyper.core.intent")
|
||||
|
||||
-- Get target file content for context
|
||||
local target_content = ""
|
||||
local target_lines = {}
|
||||
if event.target_path then
|
||||
local ok, lines = pcall(function()
|
||||
return vim.fn.readfile(event.target_path)
|
||||
end)
|
||||
if ok and lines then
|
||||
target_lines = lines
|
||||
target_content = table.concat(lines, "\n")
|
||||
end
|
||||
end
|
||||
@@ -376,7 +420,7 @@ local function build_prompt(event)
|
||||
local indexed_context = nil
|
||||
local indexed_content = ""
|
||||
pcall(function()
|
||||
local indexer = require("codetyper.indexer")
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
indexed_context = indexer.get_context_for({
|
||||
file = event.target_path,
|
||||
intent = event.intent,
|
||||
@@ -395,7 +439,7 @@ local function build_prompt(event)
|
||||
-- Get brain memories - contextual recall based on current task
|
||||
local brain_context = ""
|
||||
pcall(function()
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
if brain.is_initialized() then
|
||||
-- Query brain for relevant memories based on:
|
||||
-- 1. Current file (file-specific patterns)
|
||||
@@ -458,6 +502,93 @@ local function build_prompt(event)
|
||||
system_prompt = intent_mod.get_prompt_modifier(event.intent)
|
||||
end
|
||||
|
||||
-- SPECIAL HANDLING: Inline prompts with /@ ... @/ tags
|
||||
-- Uses SEARCH/REPLACE block format for reliable code editing
|
||||
if is_inline_prompt(event) and event.range and event.range.start_line then
|
||||
local start_line = event.range.start_line
|
||||
local end_line = event.range.end_line or start_line
|
||||
|
||||
-- Build full file content WITHOUT the /@ @/ tags for cleaner context
|
||||
local file_content_clean = {}
|
||||
for i, line in ipairs(target_lines) do
|
||||
-- Skip lines that are part of the tag
|
||||
if i < start_line or i > end_line then
|
||||
table.insert(file_content_clean, line)
|
||||
end
|
||||
end
|
||||
|
||||
user_prompt = string.format(
|
||||
[[You are editing a %s file: %s
|
||||
|
||||
TASK: %s
|
||||
|
||||
FULL FILE CONTENT:
|
||||
```%s
|
||||
%s
|
||||
```
|
||||
|
||||
IMPORTANT: The instruction above may ask you to make changes ANYWHERE in the file (e.g., "at the top", "after function X", etc.). Read the instruction carefully to determine WHERE to apply the change.
|
||||
|
||||
INSTRUCTIONS:
|
||||
You MUST respond using SEARCH/REPLACE blocks. This format lets you precisely specify what to find and what to replace it with.
|
||||
|
||||
FORMAT:
|
||||
<<<<<<< SEARCH
|
||||
[exact lines to find in the file - copy them exactly including whitespace]
|
||||
=======
|
||||
[new lines to replace them with]
|
||||
>>>>>>> REPLACE
|
||||
|
||||
RULES:
|
||||
1. The SEARCH section must contain EXACT lines from the file (copy-paste them)
|
||||
2. Include 2-3 context lines to uniquely identify the location
|
||||
3. The REPLACE section contains the modified code
|
||||
4. You can use multiple SEARCH/REPLACE blocks for multiple changes
|
||||
5. Preserve the original indentation style
|
||||
6. If adding new code at the start/end of file, include the first/last few lines in SEARCH
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
Example 1 - Adding code at the TOP of file:
|
||||
Task: "Add a comment at the top"
|
||||
<<<<<<< SEARCH
|
||||
// existing first line
|
||||
// existing second line
|
||||
=======
|
||||
// NEW COMMENT ADDED HERE
|
||||
// existing first line
|
||||
// existing second line
|
||||
>>>>>>> REPLACE
|
||||
|
||||
Example 2 - Modifying a function:
|
||||
Task: "Add validation to setValue"
|
||||
<<<<<<< SEARCH
|
||||
export function setValue(key, value) {
|
||||
cache.set(key, value);
|
||||
}
|
||||
=======
|
||||
export function setValue(key, value) {
|
||||
if (!key) throw new Error("key required");
|
||||
cache.set(key, value);
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
|
||||
Now apply the requested changes using SEARCH/REPLACE blocks:]],
|
||||
filetype,
|
||||
vim.fn.fnamemodify(event.target_path or "", ":t"),
|
||||
event.prompt_content,
|
||||
filetype,
|
||||
table.concat(file_content_clean, "\n"):sub(1, 8000) -- Limit size
|
||||
)
|
||||
|
||||
context.system_prompt = system_prompt
|
||||
context.formatted_prompt = user_prompt
|
||||
context.is_inline_prompt = true
|
||||
context.use_search_replace = true
|
||||
|
||||
return user_prompt, context
|
||||
end
|
||||
|
||||
-- If we have a scope (function/method), include it in the prompt
|
||||
if event.scope_text and event.scope and event.scope.type ~= "file" then
|
||||
local scope_type = event.scope.type
|
||||
@@ -490,6 +621,11 @@ Return ONLY the complete %s with implementation. No explanations, no duplicates.
|
||||
event.prompt_content,
|
||||
scope_type
|
||||
)
|
||||
-- Remind the LLM not to repeat the original file content; ask for only the new/updated code or a unified diff
|
||||
user_prompt = user_prompt .. [[
|
||||
|
||||
IMPORTANT: Do NOT repeat the existing code provided above. Return ONLY the new or modified code (the updated function body). If you modify the file, prefer outputting a unified diff patch using standard diff headers (--- a/<file> / +++ b/<file> and @@ hunks). No explanations, no markdown, no code fences.
|
||||
]]
|
||||
-- For other replacement intents, provide the full scope to transform
|
||||
elseif event.intent and intent_mod.is_replacement(event.intent) then
|
||||
user_prompt = string.format(
|
||||
@@ -530,6 +666,18 @@ Output only the code to insert, no explanations.]],
|
||||
extra_context,
|
||||
event.prompt_content
|
||||
)
|
||||
|
||||
-- Remind the LLM not to repeat the full file content; ask for only the new/modified code or unified diff
|
||||
user_prompt = user_prompt .. [[
|
||||
|
||||
IMPORTANT: Do NOT repeat the full file content shown above. Return ONLY the new or modified code required to satisfy the request. If you modify the file, prefer outputting a unified diff patch using standard diff headers (--- a/<file> / +++ b/<file> and @@ hunks). No explanations, no markdown, no code fences.
|
||||
]]
|
||||
|
||||
-- Remind the LLM not to repeat the original file content; ask for only the inserted code or a unified diff
|
||||
user_prompt = user_prompt .. [[
|
||||
|
||||
IMPORTANT: Do NOT repeat the surrounding code provided above. Return ONLY the code to insert (the new snippet). If you modify multiple parts of the file, prefer outputting a unified diff patch using standard diff headers (--- a/<file> / +++ b/<file> and @@ hunks). No explanations, no markdown, no code fences.
|
||||
]]
|
||||
end
|
||||
else
|
||||
-- No scope resolved, use full file context
|
||||
@@ -578,7 +726,7 @@ function M.create(event, worker_type, callback)
|
||||
|
||||
-- Log worker creation
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "worker",
|
||||
message = string.format("Worker %s started (%s)", worker.id, worker_type),
|
||||
@@ -608,7 +756,7 @@ function M.start(worker)
|
||||
active_workers[worker.id] = nil
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "warning",
|
||||
message = string.format("Worker %s timed out after %dms", worker.id, worker.timeout_ms),
|
||||
@@ -662,7 +810,7 @@ function M.start(worker)
|
||||
-- Log if pondering occurred
|
||||
if usage_or_metadata.pondered then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format(
|
||||
@@ -680,7 +828,7 @@ function M.start(worker)
|
||||
|
||||
-- Use smart selection or direct client
|
||||
if use_smart_selection then
|
||||
local llm = require("codetyper.llm")
|
||||
local llm = require("codetyper.core.llm")
|
||||
llm.smart_generate(prompt, context, handle_response)
|
||||
else
|
||||
-- Get client and execute directly
|
||||
@@ -706,7 +854,7 @@ function M.complete(worker, response, error, usage)
|
||||
active_workers[worker.id] = nil
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "error",
|
||||
message = string.format("Worker %s failed: %s", worker.id, error),
|
||||
@@ -732,7 +880,7 @@ function M.complete(worker, response, error, usage)
|
||||
active_workers[worker.id] = nil
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Worker %s: LLM needs more context", worker.id),
|
||||
@@ -756,7 +904,7 @@ function M.complete(worker, response, error, usage)
|
||||
|
||||
-- Log the full raw LLM response (for debugging)
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "response",
|
||||
message = "--- LLM Response ---",
|
||||
@@ -777,7 +925,7 @@ function M.complete(worker, response, error, usage)
|
||||
active_workers[worker.id] = nil
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "success",
|
||||
message = string.format(
|
||||
@@ -824,7 +972,7 @@ function M.cancel(worker_id)
|
||||
active_workers[worker_id] = nil
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = string.format("Worker %s cancelled", worker_id),
|
||||
@@ -14,62 +14,10 @@ local M = {}
|
||||
---@field name string|nil Name of the function/class if available
|
||||
|
||||
--- Node types that represent function-like scopes per language
|
||||
local function_nodes = {
|
||||
-- Lua
|
||||
["function_declaration"] = "function",
|
||||
["function_definition"] = "function",
|
||||
["local_function"] = "function",
|
||||
["function"] = "function",
|
||||
|
||||
-- JavaScript/TypeScript
|
||||
["function_declaration"] = "function",
|
||||
["function_expression"] = "function",
|
||||
["arrow_function"] = "function",
|
||||
["method_definition"] = "method",
|
||||
["function"] = "function",
|
||||
|
||||
-- Python
|
||||
["function_definition"] = "function",
|
||||
["async_function_definition"] = "function",
|
||||
|
||||
-- Go
|
||||
["function_declaration"] = "function",
|
||||
["method_declaration"] = "method",
|
||||
|
||||
-- Rust
|
||||
["function_item"] = "function",
|
||||
["impl_item"] = "method",
|
||||
|
||||
-- Ruby
|
||||
["method"] = "method",
|
||||
["singleton_method"] = "method",
|
||||
|
||||
-- Java/C#
|
||||
["method_declaration"] = "method",
|
||||
["constructor_declaration"] = "method",
|
||||
|
||||
-- C/C++
|
||||
["function_definition"] = "function",
|
||||
}
|
||||
|
||||
--- Node types that represent class-like scopes
|
||||
local class_nodes = {
|
||||
["class_declaration"] = "class",
|
||||
["class_definition"] = "class",
|
||||
["class"] = "class",
|
||||
["struct_item"] = "class",
|
||||
["impl_item"] = "class",
|
||||
["interface_declaration"] = "class",
|
||||
["module"] = "class",
|
||||
}
|
||||
|
||||
--- Node types that represent block scopes
|
||||
local block_nodes = {
|
||||
["block"] = "block",
|
||||
["statement_block"] = "block",
|
||||
["compound_statement"] = "block",
|
||||
["do_block"] = "block",
|
||||
}
|
||||
local params = require("codetyper.params.agents.scope")
|
||||
local function_nodes = params.function_nodes
|
||||
local class_nodes = params.class_nodes
|
||||
local block_nodes = params.block_nodes
|
||||
|
||||
--- Check if Tree-sitter is available for buffer
|
||||
---@param bufnr number
|
||||
@@ -282,13 +230,21 @@ function M.resolve_scope_heuristic(bufnr, row, col)
|
||||
ending = nil, -- Python uses indentation
|
||||
},
|
||||
javascript = {
|
||||
start = "^%s*function%s+",
|
||||
start_alt = "^%s*const%s+%w+%s*=%s*",
|
||||
start = "^%s*export%s+function%s+",
|
||||
start_alt = "^%s*function%s+",
|
||||
start_alt2 = "^%s*export%s+const%s+%w+%s*=",
|
||||
start_alt3 = "^%s*const%s+%w+%s*=%s*",
|
||||
start_alt4 = "^%s*export%s+async%s+function%s+",
|
||||
start_alt5 = "^%s*async%s+function%s+",
|
||||
ending = "^%s*}%s*$",
|
||||
},
|
||||
typescript = {
|
||||
start = "^%s*function%s+",
|
||||
start_alt = "^%s*const%s+%w+%s*=%s*",
|
||||
start = "^%s*export%s+function%s+",
|
||||
start_alt = "^%s*function%s+",
|
||||
start_alt2 = "^%s*export%s+const%s+%w+%s*=",
|
||||
start_alt3 = "^%s*const%s+%w+%s*=%s*",
|
||||
start_alt4 = "^%s*export%s+async%s+function%s+",
|
||||
start_alt5 = "^%s*async%s+function%s+",
|
||||
ending = "^%s*}%s*$",
|
||||
},
|
||||
}
|
||||
@@ -302,8 +258,13 @@ function M.resolve_scope_heuristic(bufnr, row, col)
|
||||
local start_line = nil
|
||||
for i = row, 1, -1 do
|
||||
local line = lines[i]
|
||||
if line:match(lang_patterns.start) or
|
||||
(lang_patterns.start_alt and line:match(lang_patterns.start_alt)) then
|
||||
-- Check all start patterns
|
||||
if line:match(lang_patterns.start)
|
||||
or (lang_patterns.start_alt and line:match(lang_patterns.start_alt))
|
||||
or (lang_patterns.start_alt2 and line:match(lang_patterns.start_alt2))
|
||||
or (lang_patterns.start_alt3 and line:match(lang_patterns.start_alt3))
|
||||
or (lang_patterns.start_alt4 and line:match(lang_patterns.start_alt4))
|
||||
or (lang_patterns.start_alt5 and line:match(lang_patterns.start_alt5)) then
|
||||
start_line = i
|
||||
break
|
||||
end
|
||||
@@ -3,81 +3,22 @@
|
||||
--- Tool for executing shell commands with safety checks.
|
||||
---@brief ]]
|
||||
|
||||
local Base = require("codetyper.agent.tools.base")
|
||||
local Base = require("codetyper.core.tools.base")
|
||||
local description = require("codetyper.prompts.agents.bash").description
|
||||
local params = require("codetyper.params.agents.bash").params
|
||||
local returns = require("codetyper.params.agents.bash").returns
|
||||
local BANNED_COMMANDS = require("codetyper.commands.agents.banned").BANNED_COMMANDS
|
||||
local BANNED_PATTERNS = require("codetyper.commands.agents.banned").BANNED_PATTERNS
|
||||
|
||||
---@class CoderTool
|
||||
local M = setmetatable({}, Base)
|
||||
|
||||
M.name = "bash"
|
||||
|
||||
M.description = [[Executes a bash command in a shell.
|
||||
|
||||
IMPORTANT RULES:
|
||||
- Do NOT use bash to read files (use 'view' tool instead)
|
||||
- Do NOT use bash to modify files (use 'write' or 'edit' tools instead)
|
||||
- Do NOT use interactive commands (vim, nano, less, etc.)
|
||||
- Commands timeout after 2 minutes by default
|
||||
|
||||
Allowed uses:
|
||||
- Running builds (make, npm run build, cargo build)
|
||||
- Running tests (npm test, pytest, cargo test)
|
||||
- Git operations (git status, git diff, git commit)
|
||||
- Package management (npm install, pip install)
|
||||
- System info commands (ls, pwd, which)]]
|
||||
|
||||
M.params = {
|
||||
{
|
||||
name = "command",
|
||||
description = "The shell command to execute",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "cwd",
|
||||
description = "Working directory for the command (optional)",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
{
|
||||
name = "timeout",
|
||||
description = "Timeout in milliseconds (default: 120000)",
|
||||
type = "integer",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
M.returns = {
|
||||
{
|
||||
name = "stdout",
|
||||
description = "Command output",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "error",
|
||||
description = "Error message if command failed",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
M.description = description
|
||||
M.params = params
|
||||
M.returns = returns
|
||||
M.requires_confirmation = true
|
||||
|
||||
--- Banned commands for safety
|
||||
local BANNED_COMMANDS = {
|
||||
"rm -rf /",
|
||||
"rm -rf /*",
|
||||
"dd if=/dev/zero",
|
||||
"mkfs",
|
||||
":(){ :|:& };:",
|
||||
"> /dev/sda",
|
||||
}
|
||||
|
||||
--- Banned patterns
|
||||
local BANNED_PATTERNS = {
|
||||
"curl.*|.*sh",
|
||||
"wget.*|.*sh",
|
||||
"rm%s+%-rf%s+/",
|
||||
}
|
||||
|
||||
--- Check if command is safe
|
||||
---@param command string
|
||||
---@return boolean safe
|
||||
@@ -2,61 +2,21 @@
|
||||
---@brief [[
|
||||
--- Tool for making targeted edits to files using search/replace.
|
||||
--- Implements multiple fallback strategies for robust matching.
|
||||
--- Inspired by opencode's 9-strategy approach.
|
||||
--- Multi-strategy approach for reliable editing.
|
||||
---@brief ]]
|
||||
|
||||
local Base = require("codetyper.agent.tools.base")
|
||||
local Base = require("codetyper.core.tools.base")
|
||||
local description = require("codetyper.prompts.agents.edit").description
|
||||
local params = require("codetyper.params.agents.edit").params
|
||||
local returns = require("codetyper.params.agents.edit").returns
|
||||
|
||||
---@class CoderTool
|
||||
local M = setmetatable({}, Base)
|
||||
|
||||
M.name = "edit"
|
||||
|
||||
M.description = [[Makes a targeted edit to a file by replacing text.
|
||||
|
||||
The old_string should match the content you want to replace. The tool uses multiple
|
||||
matching strategies with fallbacks:
|
||||
1. Exact match
|
||||
2. Whitespace-normalized match
|
||||
3. Indentation-flexible match
|
||||
4. Line-trimmed match
|
||||
5. Fuzzy anchor-based match
|
||||
|
||||
For creating new files, use old_string="" and provide the full content in new_string.
|
||||
For large changes, consider using 'write' tool instead.]]
|
||||
|
||||
M.params = {
|
||||
{
|
||||
name = "path",
|
||||
description = "Path to the file to edit",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "old_string",
|
||||
description = "Text to find and replace (empty string to create new file or append)",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "new_string",
|
||||
description = "Text to replace with",
|
||||
type = "string",
|
||||
},
|
||||
}
|
||||
|
||||
M.returns = {
|
||||
{
|
||||
name = "success",
|
||||
description = "Whether the edit was applied",
|
||||
type = "boolean",
|
||||
},
|
||||
{
|
||||
name = "error",
|
||||
description = "Error message if edit failed",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
M.description = description
|
||||
M.params = params
|
||||
M.returns = returns
|
||||
M.requires_confirmation = false
|
||||
|
||||
--- Normalize line endings to LF
|
||||
@@ -211,11 +171,7 @@ local function levenshtein(s1, s2)
|
||||
for i = 1, len1 do
|
||||
for j = 1, len2 do
|
||||
local cost = s1:sub(i, i) == s2:sub(j, j) and 0 or 1
|
||||
matrix[i][j] = math.min(
|
||||
matrix[i - 1][j] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j - 1] + cost
|
||||
)
|
||||
matrix[i][j] = math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -245,10 +201,13 @@ local function fuzzy_anchor_match(content, old_str, threshold)
|
||||
local candidates = {}
|
||||
for i, line in ipairs(content_lines) do
|
||||
local trimmed = line:match("^%s*(.-)%s*$")
|
||||
if trimmed == first_line or (
|
||||
#first_line > 0 and
|
||||
1 - (levenshtein(trimmed, first_line) / math.max(#trimmed, #first_line)) >= threshold
|
||||
) then
|
||||
if
|
||||
trimmed == first_line
|
||||
or (
|
||||
#first_line > 0
|
||||
and 1 - (levenshtein(trimmed, first_line) / math.max(#trimmed, #first_line)) >= threshold
|
||||
)
|
||||
then
|
||||
table.insert(candidates, i)
|
||||
end
|
||||
end
|
||||
@@ -258,10 +217,13 @@ local function fuzzy_anchor_match(content, old_str, threshold)
|
||||
local expected_end = start_idx + #old_lines - 1
|
||||
if expected_end <= #content_lines then
|
||||
local end_line = content_lines[expected_end]:match("^%s*(.-)%s*$")
|
||||
if end_line == last_line or (
|
||||
#last_line > 0 and
|
||||
1 - (levenshtein(end_line, last_line) / math.max(#end_line, #last_line)) >= threshold
|
||||
) then
|
||||
if
|
||||
end_line == last_line
|
||||
or (
|
||||
#last_line > 0
|
||||
and 1 - (levenshtein(end_line, last_line) / math.max(#end_line, #last_line)) >= threshold
|
||||
)
|
||||
then
|
||||
-- Calculate positions
|
||||
local before = table.concat(vim.list_slice(content_lines, 1, start_idx - 1), "\n")
|
||||
local block = table.concat(vim.list_slice(content_lines, start_idx, expected_end), "\n")
|
||||
@@ -3,7 +3,7 @@
|
||||
--- Tool for finding files by glob pattern.
|
||||
---@brief ]]
|
||||
|
||||
local Base = require("codetyper.agent.tools.base")
|
||||
local Base = require("codetyper.core.tools.base")
|
||||
|
||||
---@class CoderTool
|
||||
local M = setmetatable({}, Base)
|
||||
@@ -3,61 +3,18 @@
|
||||
--- Tool for searching file contents using ripgrep.
|
||||
---@brief ]]
|
||||
|
||||
local Base = require("codetyper.agent.tools.base")
|
||||
local Base = require("codetyper.core.tools.base")
|
||||
local description = require("codetyper.params.agents.grep").description
|
||||
local params = require("codetyper.prompts.agents.grep").params
|
||||
local returns = require("codetyper.prompts.agents.grep").returns
|
||||
|
||||
---@class CoderTool
|
||||
local M = setmetatable({}, Base)
|
||||
|
||||
M.name = "grep"
|
||||
|
||||
M.description = [[Searches for a pattern in files using ripgrep.
|
||||
|
||||
Returns file paths and matching lines. Use this to find code by content.
|
||||
|
||||
Example patterns:
|
||||
- "function foo" - Find function definitions
|
||||
- "import.*react" - Find React imports
|
||||
- "TODO|FIXME" - Find todo comments]]
|
||||
|
||||
M.params = {
|
||||
{
|
||||
name = "pattern",
|
||||
description = "Regular expression pattern to search for",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "path",
|
||||
description = "Directory or file to search in (default: project root)",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
{
|
||||
name = "include",
|
||||
description = "File glob pattern to include (e.g., '*.lua')",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
{
|
||||
name = "max_results",
|
||||
description = "Maximum number of results (default: 50)",
|
||||
type = "integer",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
M.returns = {
|
||||
{
|
||||
name = "matches",
|
||||
description = "JSON array of matches with file, line_number, and content",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "error",
|
||||
description = "Error message if search failed",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
M.description = description
|
||||
M.params = params
|
||||
M.returns = returns
|
||||
|
||||
M.requires_confirmation = false
|
||||
|
||||
90
lua/codetyper/core/tools/init.lua
Normal file
90
lua/codetyper/core/tools/init.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
---@mod codetyper.agent.tools Tool definitions for the agent system
|
||||
---
|
||||
--- Defines available tools that the LLM can use to interact with files and system.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Tool definitions in a provider-agnostic format
|
||||
M.definitions = require("codetyper.params.agents.tools").definitions
|
||||
|
||||
--- Convert tool definitions to Claude API format
|
||||
---@return table[] Tools in Claude's expected format
|
||||
function M.to_claude_format()
|
||||
local tools = {}
|
||||
for _, tool in pairs(M.definitions) do
|
||||
table.insert(tools, {
|
||||
name = tool.name,
|
||||
description = tool.description,
|
||||
input_schema = tool.parameters,
|
||||
})
|
||||
end
|
||||
return tools
|
||||
end
|
||||
|
||||
--- Convert tool definitions to OpenAI API format
|
||||
---@return table[] Tools in OpenAI's expected format
|
||||
function M.to_openai_format()
|
||||
local tools = {}
|
||||
for _, tool in pairs(M.definitions) do
|
||||
table.insert(tools, {
|
||||
type = "function",
|
||||
["function"] = {
|
||||
name = tool.name,
|
||||
description = tool.description,
|
||||
parameters = tool.parameters,
|
||||
},
|
||||
})
|
||||
end
|
||||
return tools
|
||||
end
|
||||
|
||||
--- Convert tool definitions to prompt format for Ollama
|
||||
---@return string Formatted tool descriptions for system prompt
|
||||
function M.to_prompt_format()
|
||||
local prompts = require("codetyper.prompts.agents.tools").instructions
|
||||
local lines = {
|
||||
prompts.intro,
|
||||
"",
|
||||
}
|
||||
|
||||
for _, tool in pairs(M.definitions) do
|
||||
table.insert(lines, "## " .. tool.name)
|
||||
table.insert(lines, tool.description)
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Parameters:")
|
||||
for prop_name, prop in pairs(tool.parameters.properties) do
|
||||
local required = vim.tbl_contains(tool.parameters.required or {}, prop_name)
|
||||
local req_str = required and " (required)" or " (optional)"
|
||||
table.insert(lines, " - " .. prop_name .. ": " .. prop.description .. req_str)
|
||||
end
|
||||
table.insert(lines, "")
|
||||
end
|
||||
|
||||
table.insert(lines, "---")
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, prompts.header)
|
||||
table.insert(lines, prompts.example)
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, prompts.footer)
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Get a list of tool names
|
||||
---@return string[]
|
||||
function M.get_tool_names()
|
||||
local names = {}
|
||||
for name, _ in pairs(M.definitions) do
|
||||
table.insert(names, name)
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
--- Optional setup function for future extensibility
|
||||
---@param opts table|nil Configuration options
|
||||
function M.setup(opts)
|
||||
-- Currently a no-op. Plugins or tests may call setup(); keep for compatibility.
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---@mod codetyper.agent.tools Tool registry and orchestration
|
||||
---@brief [[
|
||||
--- Registry for LLM tools with execution and schema generation.
|
||||
--- Inspired by avante.nvim's tool system.
|
||||
--- Tool system for agent mode.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
@@ -162,27 +162,27 @@ end
|
||||
--- Load built-in tools
|
||||
function M.load_builtins()
|
||||
-- View file tool
|
||||
local view = require("codetyper.agent.tools.view")
|
||||
local view = require("codetyper.core.tools.view")
|
||||
M.register(view)
|
||||
|
||||
-- Bash tool
|
||||
local bash = require("codetyper.agent.tools.bash")
|
||||
local bash = require("codetyper.core.tools.bash")
|
||||
M.register(bash)
|
||||
|
||||
-- Grep tool
|
||||
local grep = require("codetyper.agent.tools.grep")
|
||||
local grep = require("codetyper.core.tools.grep")
|
||||
M.register(grep)
|
||||
|
||||
-- Glob tool
|
||||
local glob = require("codetyper.agent.tools.glob")
|
||||
local glob = require("codetyper.core.tools.glob")
|
||||
M.register(glob)
|
||||
|
||||
-- Write file tool
|
||||
local write = require("codetyper.agent.tools.write")
|
||||
local write = require("codetyper.core.tools.write")
|
||||
M.register(write)
|
||||
|
||||
-- Edit tool
|
||||
local edit = require("codetyper.agent.tools.edit")
|
||||
local edit = require("codetyper.core.tools.edit")
|
||||
M.register(edit)
|
||||
end
|
||||
|
||||
@@ -3,54 +3,19 @@
|
||||
--- Tool for reading file contents with line range support.
|
||||
---@brief ]]
|
||||
|
||||
local Base = require("codetyper.agent.tools.base")
|
||||
local Base = require("codetyper.core.tools.base")
|
||||
|
||||
---@class CoderTool
|
||||
local M = setmetatable({}, Base)
|
||||
|
||||
M.name = "view"
|
||||
|
||||
M.description = [[Reads the content of a file.
|
||||
local params = require("codetyper.params.agents.view")
|
||||
local description = require("codetyper.prompts.agents.view").description
|
||||
|
||||
Usage notes:
|
||||
- Provide the file path relative to the project root
|
||||
- Use start_line and end_line to read specific sections
|
||||
- If content is truncated, use line ranges to read in chunks
|
||||
- Returns JSON with content, total_line_count, and is_truncated]]
|
||||
|
||||
M.params = {
|
||||
{
|
||||
name = "path",
|
||||
description = "Path to the file (relative to project root or absolute)",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "start_line",
|
||||
description = "Line number to start reading (1-indexed)",
|
||||
type = "integer",
|
||||
optional = true,
|
||||
},
|
||||
{
|
||||
name = "end_line",
|
||||
description = "Line number to end reading (1-indexed, inclusive)",
|
||||
type = "integer",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
M.returns = {
|
||||
{
|
||||
name = "content",
|
||||
description = "File contents as JSON with content, total_line_count, is_truncated",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "error",
|
||||
description = "Error message if file could not be read",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
M.description = description
|
||||
M.params = params.params
|
||||
M.returns = params.returns
|
||||
|
||||
M.requires_confirmation = false
|
||||
|
||||
@@ -3,46 +3,17 @@
|
||||
--- Tool for creating or overwriting files.
|
||||
---@brief ]]
|
||||
|
||||
local Base = require("codetyper.agent.tools.base")
|
||||
local Base = require("codetyper.core.tools.base")
|
||||
local description = require("codetyper.prompts.agents.write").description
|
||||
local params = require("codetyper.params.agents.write")
|
||||
|
||||
---@class CoderTool
|
||||
local M = setmetatable({}, Base)
|
||||
|
||||
M.name = "write"
|
||||
|
||||
M.description = [[Creates or overwrites a file with new content.
|
||||
|
||||
IMPORTANT:
|
||||
- This will completely replace the file contents
|
||||
- Use 'edit' tool for partial modifications
|
||||
- Parent directories will be created if needed]]
|
||||
|
||||
M.params = {
|
||||
{
|
||||
name = "path",
|
||||
description = "Path to the file to write",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "content",
|
||||
description = "Content to write to the file",
|
||||
type = "string",
|
||||
},
|
||||
}
|
||||
|
||||
M.returns = {
|
||||
{
|
||||
name = "success",
|
||||
description = "Whether the file was written successfully",
|
||||
type = "boolean",
|
||||
},
|
||||
{
|
||||
name = "error",
|
||||
description = "Error message if write failed",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
M.description = description
|
||||
M.params = params.params
|
||||
M.returns = params.returns
|
||||
|
||||
M.requires_confirmation = true
|
||||
|
||||
268
lua/codetyper/features/agents/context_builder.lua
Normal file
268
lua/codetyper/features/agents/context_builder.lua
Normal file
@@ -0,0 +1,268 @@
|
||||
---@mod codetyper.agent.context_builder Context builder for agent prompts
|
||||
---
|
||||
--- Builds rich context including project structure, memories, and conventions
|
||||
--- to help the LLM understand the codebase.
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.support.utils")
|
||||
local params = require("codetyper.params.agents.context")
|
||||
|
||||
--- Get project structure as a tree string
|
||||
---@param max_depth? number Maximum depth to traverse (default: 3)
|
||||
---@param max_files? number Maximum files to show (default: 50)
|
||||
---@return string Project tree
|
||||
function M.get_project_structure(max_depth, max_files)
|
||||
max_depth = max_depth or 3
|
||||
max_files = max_files or 50
|
||||
|
||||
local root = utils.get_project_root() or vim.fn.getcwd()
|
||||
local lines = { "PROJECT STRUCTURE:", root, "" }
|
||||
local file_count = 0
|
||||
|
||||
-- Common ignore patterns
|
||||
local ignore_patterns = params.ignore_patterns
|
||||
|
||||
local function should_ignore(name)
|
||||
for _, pattern in ipairs(ignore_patterns) do
|
||||
if name:match(pattern) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function traverse(path, depth, prefix)
|
||||
if depth > max_depth or file_count >= max_files then
|
||||
return
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local handle = vim.loop.fs_scandir(path)
|
||||
if not handle then
|
||||
return
|
||||
end
|
||||
|
||||
while true do
|
||||
local name, type = vim.loop.fs_scandir_next(handle)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
if not should_ignore(name) then
|
||||
table.insert(entries, { name = name, type = type })
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort: directories first, then alphabetically
|
||||
table.sort(entries, function(a, b)
|
||||
if a.type == "directory" and b.type ~= "directory" then
|
||||
return true
|
||||
elseif a.type ~= "directory" and b.type == "directory" then
|
||||
return false
|
||||
else
|
||||
return a.name < b.name
|
||||
end
|
||||
end)
|
||||
|
||||
for i, entry in ipairs(entries) do
|
||||
if file_count >= max_files then
|
||||
table.insert(lines, prefix .. "... (truncated)")
|
||||
return
|
||||
end
|
||||
|
||||
local is_last = (i == #entries)
|
||||
local branch = is_last and "└── " or "├── "
|
||||
local new_prefix = prefix .. (is_last and " " or "│ ")
|
||||
|
||||
local icon = entry.type == "directory" and "/" or ""
|
||||
table.insert(lines, prefix .. branch .. entry.name .. icon)
|
||||
file_count = file_count + 1
|
||||
|
||||
if entry.type == "directory" then
|
||||
traverse(path .. "/" .. entry.name, depth + 1, new_prefix)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
traverse(root, 1, "")
|
||||
|
||||
if file_count >= max_files then
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "(Structure truncated at " .. max_files .. " entries)")
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Get key files that are important for understanding the project
|
||||
---@return table<string, string> Map of filename to description
|
||||
function M.get_key_files()
|
||||
local root = utils.get_project_root() or vim.fn.getcwd()
|
||||
local key_files = {}
|
||||
|
||||
local important_files = {
|
||||
["package.json"] = "Node.js project config",
|
||||
["Cargo.toml"] = "Rust project config",
|
||||
["go.mod"] = "Go module config",
|
||||
["pyproject.toml"] = "Python project config",
|
||||
["setup.py"] = "Python setup config",
|
||||
["Makefile"] = "Build configuration",
|
||||
["CMakeLists.txt"] = "CMake config",
|
||||
[".gitignore"] = "Git ignore patterns",
|
||||
["README.md"] = "Project documentation",
|
||||
["init.lua"] = "Neovim plugin entry",
|
||||
["plugin.lua"] = "Neovim plugin config",
|
||||
}
|
||||
|
||||
for filename, desc in paparams.important_filesnd
|
||||
|
||||
return key_files
|
||||
end
|
||||
|
||||
--- Detect project type and language
|
||||
---@return table { type: string, language: string, framework?: string }
|
||||
function M.detect_project_type()
|
||||
local root = utils.get_project_root() or vim.fn.getcwd()
|
||||
|
||||
local indicators = {
|
||||
["package.json"] = { type = "node", language = "javascript/typescript" },
|
||||
["Cargo.toml"] = { type = "rust", language = "rust" },
|
||||
["go.mod"] = { type = "go", language = "go" },
|
||||
["pyproject.toml"] = { type = "python", language = "python" },
|
||||
["setup.py"] = { type = "python", language = "python" },
|
||||
["Gemfile"] = { type = "ruby", language = "ruby" },
|
||||
["pom.xml"] = { type = "maven", language = "java" },
|
||||
["build.gradle"] = { type = "gradle", language = "java/kotlin" },
|
||||
}
|
||||
|
||||
-- Check for Neovim plugin specifically
|
||||
if vim.fn.isdirectoparams.indicators return info
|
||||
end
|
||||
end
|
||||
|
||||
return { type = "unknown", language = "unknown" }
|
||||
end
|
||||
|
||||
--- Get memories/patterns from the brain system
|
||||
---@return string Formatted memories context
|
||||
function M.get_memories_context()
|
||||
local ok_memory, memory = pcall(require, "codetyper.indexer.memory")
|
||||
if not ok_memory then
|
||||
return ""
|
||||
end
|
||||
|
||||
local all = memory.get_all()
|
||||
if not all then
|
||||
return ""
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
|
||||
-- Add patterns
|
||||
if all.patterns and next(all.patterns) then
|
||||
table.insert(lines, "LEARNED PATTERNS:")
|
||||
local count = 0
|
||||
for _, mem in pairs(all.patterns) do
|
||||
if count >= 5 then
|
||||
break
|
||||
end
|
||||
if mem.content then
|
||||
table.insert(lines, " - " .. mem.content:sub(1, 100))
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
table.insert(lines, "")
|
||||
end
|
||||
|
||||
-- Add conventions
|
||||
if all.conventions and next(all.conventions) then
|
||||
table.insert(lines, "CODING CONVENTIONS:")
|
||||
local count = 0
|
||||
for _, mem in pairs(all.conventions) do
|
||||
if count >= 5 then
|
||||
break
|
||||
end
|
||||
if mem.content then
|
||||
table.insert(lines, " - " .. mem.content:sub(1, 100))
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
table.insert(lines, "")
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Build the full context for agent prompts
|
||||
---@return string Full context string
|
||||
function M.build_full_context()
|
||||
local sections = {}
|
||||
|
||||
-- Project info
|
||||
local project_type = M.detect_project_type()
|
||||
table.insert(sections, string.format(
|
||||
"PROJECT INFO:\n Type: %s\n Language: %s%s\n",
|
||||
project_type.type,
|
||||
project_type.language,
|
||||
project_type.framework and ("\n Framework: " .. project_type.framework) or ""
|
||||
))
|
||||
|
||||
-- Project structure
|
||||
local structure = M.get_project_structure(3, 40)
|
||||
table.insert(sections, structure)
|
||||
|
||||
-- Key files
|
||||
local key_files = M.get_key_files()
|
||||
if next(key_files) then
|
||||
local key_lines = { "", "KEY FILES:" }
|
||||
for name, info in pairs(key_files) do
|
||||
table.insert(key_lines, string.format(" %s - %s", name, info.description))
|
||||
end
|
||||
table.insert(sections, table.concat(key_lines, "\n"))
|
||||
end
|
||||
|
||||
-- Memories
|
||||
local memories = M.get_memories_context()
|
||||
if memories ~= "" then
|
||||
table.insert(sections, "\n" .. memories)
|
||||
end
|
||||
|
||||
return table.concat(sections, "\n")
|
||||
end
|
||||
|
||||
--- Get a compact context summary for token efficiency
|
||||
---@return string Compact context
|
||||
function M.build_compact_context()
|
||||
local root = utils.get_project_root() or vim.fn.getcwd()
|
||||
local project_type = M.detect_project_type()
|
||||
|
||||
local lines = {
|
||||
"CONTEXT:",
|
||||
" Root: " .. root,
|
||||
" Type: " .. project_type.type .. " (" .. project_type.language .. ")",
|
||||
}
|
||||
|
||||
-- Add main directories
|
||||
local main_dirs = {}
|
||||
local handle = vim.loop.fs_scandir(root)
|
||||
if handle then
|
||||
while true do
|
||||
local name, type = vim.loop.fs_scandir_next(handle)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
if type == "directory" and not name:match("^%.") and not name:match("node_modules") then
|
||||
table.insert(main_dirs, name .. "/")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #main_dirs > 0 then
|
||||
table.sort(main_dirs)
|
||||
table.insert(lines, " Main dirs: " .. table.concat(main_dirs, ", "))
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,7 +1,7 @@
|
||||
---@mod codetyper.agent.agentic Agentic loop with proper tool calling
|
||||
---@brief [[
|
||||
--- Full agentic system that handles multi-file changes via tool calling.
|
||||
--- Inspired by avante.nvim and opencode patterns.
|
||||
--- Multi-file agent system with tool orchestration.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
@@ -31,10 +31,7 @@ local M = {}
|
||||
---@field on_complete? fun(result: string|nil, error: string|nil) Called when done
|
||||
---@field on_status? fun(status: string) Status updates
|
||||
|
||||
--- Generate unique tool call ID
|
||||
local function generate_tool_call_id()
|
||||
return "call_" .. string.format("%x", os.time()) .. "_" .. string.format("%x", math.random(0, 0xFFFF))
|
||||
end
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Load agent definition
|
||||
---@param name string Agent name
|
||||
@@ -72,59 +69,7 @@ local function load_agent(name)
|
||||
end
|
||||
|
||||
-- Built-in agents
|
||||
local builtin_agents = {
|
||||
coder = {
|
||||
name = "coder",
|
||||
description = "Full-featured coding agent with file modification capabilities",
|
||||
system_prompt = [[You are an expert software engineer. You have access to tools to read, write, and modify files.
|
||||
|
||||
## Your Capabilities
|
||||
- Read files to understand the codebase
|
||||
- Search for patterns with grep and glob
|
||||
- Create new files with write tool
|
||||
- Edit existing files with precise replacements
|
||||
- Execute shell commands for builds and tests
|
||||
|
||||
## Guidelines
|
||||
1. Always read relevant files before making changes
|
||||
2. Make minimal, focused changes
|
||||
3. Follow existing code style and patterns
|
||||
4. Create tests when adding new functionality
|
||||
5. Verify changes work by running tests or builds
|
||||
|
||||
## Important Rules
|
||||
- NEVER guess file contents - always read first
|
||||
- Make precise edits using exact string matching
|
||||
- Explain your reasoning before making changes
|
||||
- If unsure, ask for clarification]],
|
||||
tools = { "view", "edit", "write", "grep", "glob", "bash" },
|
||||
},
|
||||
planner = {
|
||||
name = "planner",
|
||||
description = "Planning agent - read-only, helps design implementations",
|
||||
system_prompt = [[You are a software architect. Analyze codebases and create implementation plans.
|
||||
|
||||
You can read files and search the codebase, but cannot modify files.
|
||||
Your role is to:
|
||||
1. Understand the existing architecture
|
||||
2. Identify relevant files and patterns
|
||||
3. Create step-by-step implementation plans
|
||||
4. Suggest which files to modify and how
|
||||
|
||||
Be thorough in your analysis before making recommendations.]],
|
||||
tools = { "view", "grep", "glob" },
|
||||
},
|
||||
explorer = {
|
||||
name = "explorer",
|
||||
description = "Exploration agent - quickly find information in codebase",
|
||||
system_prompt = [[You are a codebase exploration assistant. Find information quickly and report back.
|
||||
|
||||
Your goal is to efficiently search and summarize findings.
|
||||
Use glob to find files, grep to search content, and view to read specific files.
|
||||
Be concise and focused in your responses.]],
|
||||
tools = { "view", "grep", "glob" },
|
||||
},
|
||||
}
|
||||
local builtin_agents = require("codetyper.prompts.agents.personas").builtin
|
||||
|
||||
return builtin_agents[name]
|
||||
end
|
||||
@@ -232,7 +177,7 @@ end
|
||||
---@param provider string "openai"|"claude"
|
||||
---@return table[] Formatted tools
|
||||
local function build_tools(tool_names, provider)
|
||||
local tools_mod = require("codetyper.agent.tools")
|
||||
local tools_mod = require("codetyper.core.tools")
|
||||
local tools = {}
|
||||
|
||||
for _, name in ipairs(tool_names) do
|
||||
@@ -289,7 +234,7 @@ end
|
||||
---@return string result
|
||||
---@return string|nil error
|
||||
local function execute_tool(tool_call, opts)
|
||||
local tools_mod = require("codetyper.agent.tools")
|
||||
local tools_mod = require("codetyper.core.tools")
|
||||
local name = tool_call["function"].name
|
||||
local args = tool_call["function"].arguments
|
||||
|
||||
@@ -365,7 +310,7 @@ local function parse_tool_calls(response, provider)
|
||||
end
|
||||
|
||||
table.insert(tool_calls, {
|
||||
id = block.id or generate_tool_call_id(),
|
||||
id = block.id or utils.generate_id("call"),
|
||||
type = "function",
|
||||
["function"] = {
|
||||
name = block.name,
|
||||
@@ -418,7 +363,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac
|
||||
|
||||
-- Use native tool calling APIs
|
||||
if provider == "copilot" then
|
||||
local client = require("codetyper.llm.copilot")
|
||||
local client = require("codetyper.core.llm.copilot")
|
||||
|
||||
-- Copilot's generate_with_tools expects messages in a specific format
|
||||
-- Convert to the format it expects
|
||||
@@ -449,7 +394,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac
|
||||
elseif block.type == "tool_use" then
|
||||
table.insert(result.content, {
|
||||
type = "tool_use",
|
||||
id = block.id or generate_tool_call_id(),
|
||||
id = block.id or utils.generate_id("call"),
|
||||
name = block.name,
|
||||
input = block.input,
|
||||
})
|
||||
@@ -461,7 +406,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac
|
||||
callback(result, nil)
|
||||
end)
|
||||
elseif provider == "openai" then
|
||||
local client = require("codetyper.llm.openai")
|
||||
local client = require("codetyper.core.llm.openai")
|
||||
|
||||
-- OpenAI's generate_with_tools
|
||||
local converted_messages = {}
|
||||
@@ -490,7 +435,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac
|
||||
elseif block.type == "tool_use" then
|
||||
table.insert(result.content, {
|
||||
type = "tool_use",
|
||||
id = block.id or generate_tool_call_id(),
|
||||
id = block.id or utils.generate_id("call"),
|
||||
name = block.name,
|
||||
input = block.input,
|
||||
})
|
||||
@@ -502,7 +447,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac
|
||||
callback(result, nil)
|
||||
end)
|
||||
elseif provider == "ollama" then
|
||||
local client = require("codetyper.llm.ollama")
|
||||
local client = require("codetyper.core.llm.ollama")
|
||||
|
||||
-- Ollama's generate_with_tools (text-based tool calling)
|
||||
local converted_messages = {}
|
||||
@@ -523,24 +468,23 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac
|
||||
end)
|
||||
else
|
||||
-- Fallback for other providers (ollama, etc.) - use text-based parsing
|
||||
local client = require("codetyper.llm." .. provider)
|
||||
local client = require("codetyper.core.llm." .. provider)
|
||||
|
||||
-- Build prompt from messages
|
||||
local prompts = require("codetyper.prompts.agents")
|
||||
local prompt_parts = {}
|
||||
for _, msg in ipairs(messages) do
|
||||
if msg.role == "user" then
|
||||
local content = type(msg.content) == "string" and msg.content or vim.json.encode(msg.content)
|
||||
table.insert(prompt_parts, "User: " .. content)
|
||||
table.insert(prompt_parts, prompts.text_user_prefix .. content)
|
||||
elseif msg.role == "assistant" then
|
||||
local content = type(msg.content) == "string" and msg.content or vim.json.encode(msg.content)
|
||||
table.insert(prompt_parts, "Assistant: " .. content)
|
||||
table.insert(prompt_parts, prompts.text_assistant_prefix .. content)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add tool descriptions to prompt for text-based providers
|
||||
local tool_desc = "\n\n## Available Tools\n"
|
||||
tool_desc = tool_desc .. "Call tools by outputting JSON in this format:\n"
|
||||
tool_desc = tool_desc .. '```json\n{"tool": "tool_name", "arguments": {...}}\n```\n\n'
|
||||
local tool_desc = require("codetyper.prompts.agents").tool_instructions_text
|
||||
for _, tool in ipairs(tools) do
|
||||
local name = tool.name or (tool["function"] and tool["function"].name)
|
||||
local desc = tool.description or (tool["function"] and tool["function"].description)
|
||||
@@ -573,7 +517,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac
|
||||
if ok and parsed.tool then
|
||||
table.insert(result.content, {
|
||||
type = "tool_use",
|
||||
id = generate_tool_call_id(),
|
||||
id = utils.generate_id("call"),
|
||||
name = parsed.tool,
|
||||
input = parsed.arguments or {},
|
||||
})
|
||||
@@ -617,11 +561,7 @@ function M.run(opts)
|
||||
|
||||
-- Add initial file context if provided
|
||||
if opts.files and #opts.files > 0 then
|
||||
local file_context = "# Initial Files\n"
|
||||
for _, file_path in ipairs(opts.files) do
|
||||
local content = table.concat(vim.fn.readfile(file_path) or {}, "\n")
|
||||
file_context = file_context .. string.format("\n## %s\n```\n%s\n```\n", file_path, content)
|
||||
end
|
||||
local file_context = require("codetyper.prompts.agents").format_file_context(opts.files)
|
||||
table.insert(history, { role = "user", content = file_context })
|
||||
table.insert(history, { role = "assistant", content = "I've reviewed the provided files. What would you like me to do?" })
|
||||
end
|
||||
@@ -638,7 +578,7 @@ function M.run(opts)
|
||||
local tool_names = agent.tools or { "view", "edit", "write", "grep", "glob", "bash" }
|
||||
|
||||
-- Ensure tools are loaded
|
||||
local tools_mod = require("codetyper.agent.tools")
|
||||
local tools_mod = require("codetyper.core.tools")
|
||||
tools_mod.setup()
|
||||
|
||||
-- Build tools for API
|
||||
@@ -736,27 +676,7 @@ function M.init_agents_dir()
|
||||
vim.fn.mkdir(agents_dir, "p")
|
||||
|
||||
-- Create example agent
|
||||
local example_agent = [[---
|
||||
description: Example custom agent
|
||||
tools: view,grep,glob,edit,write
|
||||
model:
|
||||
---
|
||||
|
||||
# Custom Agent
|
||||
|
||||
You are a custom coding agent. Describe your specialized behavior here.
|
||||
|
||||
## Your Role
|
||||
- Define what this agent specializes in
|
||||
- List specific capabilities
|
||||
|
||||
## Guidelines
|
||||
- Add agent-specific rules
|
||||
- Define coding standards to follow
|
||||
|
||||
## Examples
|
||||
Provide examples of how to handle common tasks.
|
||||
]]
|
||||
local example_agent = require("codetyper.prompts.agents.templates").agent
|
||||
|
||||
local example_path = agents_dir .. "/example.md"
|
||||
if vim.fn.filereadable(example_path) ~= 1 then
|
||||
@@ -772,30 +692,7 @@ function M.init_rules_dir()
|
||||
vim.fn.mkdir(rules_dir, "p")
|
||||
|
||||
-- Create example rule
|
||||
local example_rule = [[# Code Style
|
||||
|
||||
Follow these coding standards:
|
||||
|
||||
## General
|
||||
- Use consistent indentation (tabs or spaces based on project)
|
||||
- Keep lines under 100 characters
|
||||
- Add comments for complex logic
|
||||
|
||||
## Naming Conventions
|
||||
- Use descriptive variable names
|
||||
- Functions should be verbs (e.g., getUserData, calculateTotal)
|
||||
- Constants in UPPER_SNAKE_CASE
|
||||
|
||||
## Testing
|
||||
- Write tests for new functionality
|
||||
- Aim for >80% code coverage
|
||||
- Test edge cases
|
||||
|
||||
## Documentation
|
||||
- Document public APIs
|
||||
- Include usage examples
|
||||
- Keep docs up to date with code
|
||||
]]
|
||||
local example_rule = require("codetyper.prompts.agents.templates").rule
|
||||
|
||||
local example_path = rules_dir .. "/code-style.md"
|
||||
if vim.fn.filereadable(example_path) ~= 1 then
|
||||
@@ -817,7 +714,10 @@ function M.list_agents()
|
||||
local agents = {}
|
||||
|
||||
-- Built-in agents
|
||||
local builtins = { "coder", "planner", "explorer" }
|
||||
local personas = require("codetyper.prompts.agents.personas").builtin
|
||||
local builtins = vim.tbl_keys(personas)
|
||||
table.sort(builtins)
|
||||
|
||||
for _, name in ipairs(builtins) do
|
||||
local agent = load_agent(name)
|
||||
if agent then
|
||||
@@ -4,12 +4,14 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local tools = require("codetyper.agent.tools")
|
||||
local executor = require("codetyper.agent.executor")
|
||||
local parser = require("codetyper.agent.parser")
|
||||
local diff = require("codetyper.agent.diff")
|
||||
local utils = require("codetyper.utils")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local tools = require("codetyper.core.tools")
|
||||
local executor = require("codetyper.core.scheduler.executor")
|
||||
local parser = require("codetyper.core.llm.parser")
|
||||
local diff = require("codetyper.core.diff.diff")
|
||||
local diff_review = require("codetyper.adapters.nvim.ui.diff_review")
|
||||
local resume = require("codetyper.core.scheduler.resume")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
|
||||
---@class AgentState
|
||||
---@field conversation table[] Message history for multi-turn
|
||||
@@ -21,8 +23,11 @@ local state = {
|
||||
conversation = {},
|
||||
pending_tool_results = {},
|
||||
is_running = false,
|
||||
max_iterations = 10,
|
||||
max_iterations = 25, -- Increased for complex tasks (env setup, tests, fixes)
|
||||
current_iteration = 0,
|
||||
original_prompt = "", -- Store for resume functionality
|
||||
current_context = nil, -- Store context for resume
|
||||
current_callbacks = nil, -- Store callbacks for continue
|
||||
}
|
||||
|
||||
---@class AgentCallbacks
|
||||
@@ -38,6 +43,8 @@ function M.reset()
|
||||
state.pending_tool_results = {}
|
||||
state.is_running = false
|
||||
state.current_iteration = 0
|
||||
-- Clear collected diffs
|
||||
diff_review.clear()
|
||||
end
|
||||
|
||||
--- Check if agent is currently running
|
||||
@@ -67,6 +74,9 @@ function M.run(prompt, context, callbacks)
|
||||
|
||||
state.is_running = true
|
||||
state.current_iteration = 0
|
||||
state.original_prompt = prompt
|
||||
state.current_context = context
|
||||
state.current_callbacks = callbacks
|
||||
|
||||
-- Add user message to conversation
|
||||
table.insert(state.conversation, {
|
||||
@@ -91,13 +101,13 @@ function M.agent_loop(context, callbacks)
|
||||
logs.info(string.format("Agent loop iteration %d/%d", state.current_iteration, state.max_iterations))
|
||||
|
||||
if state.current_iteration > state.max_iterations then
|
||||
logs.error("Max iterations reached")
|
||||
callbacks.on_error("Max iterations reached (" .. state.max_iterations .. ")")
|
||||
state.is_running = false
|
||||
logs.info("Max iterations reached, asking user to continue or stop")
|
||||
-- Ask user if they want to continue
|
||||
M.prompt_continue(context, callbacks)
|
||||
return
|
||||
end
|
||||
|
||||
local llm = require("codetyper.llm")
|
||||
local llm = require("codetyper.core.llm")
|
||||
local client = llm.get_client()
|
||||
|
||||
-- Check if client supports tools
|
||||
@@ -222,8 +232,20 @@ function M.process_tool_calls(tool_calls, index, context, callbacks)
|
||||
end
|
||||
logs.tool(tool_call.name, "approved", log_msg)
|
||||
|
||||
-- Apply the change
|
||||
-- Apply the change and collect for review
|
||||
executor.apply_change(result.diff_data, function(apply_result)
|
||||
-- Collect the diff for end-of-session review
|
||||
if result.diff_data.operation ~= "bash" then
|
||||
diff_review.add({
|
||||
path = result.diff_data.path,
|
||||
operation = result.diff_data.operation,
|
||||
original = result.diff_data.original,
|
||||
modified = result.diff_data.modified,
|
||||
approved = true,
|
||||
applied = true,
|
||||
})
|
||||
end
|
||||
|
||||
-- Store result for sending back to LLM
|
||||
table.insert(state.pending_tool_results, {
|
||||
tool_use_id = tool_call.id,
|
||||
@@ -280,21 +302,16 @@ function M.continue_with_results(context, callbacks)
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
-- Copilot uses Claude-like format for tool results
|
||||
-- Copilot uses OpenAI format for tool results (role: "tool")
|
||||
if config.llm.provider == "copilot" then
|
||||
-- Claude-style tool_result blocks
|
||||
local content = {}
|
||||
-- OpenAI-style tool messages - each result is a separate message
|
||||
for _, result in ipairs(state.pending_tool_results) do
|
||||
table.insert(content, {
|
||||
type = "tool_result",
|
||||
tool_use_id = result.tool_use_id,
|
||||
table.insert(state.conversation, {
|
||||
role = "tool",
|
||||
tool_call_id = result.tool_use_id,
|
||||
content = result.result,
|
||||
})
|
||||
end
|
||||
table.insert(state.conversation, {
|
||||
role = "user",
|
||||
content = content,
|
||||
})
|
||||
else
|
||||
-- Ollama format: plain text describing results
|
||||
local result_text = "Tool results:\n"
|
||||
@@ -325,4 +342,114 @@ function M.set_max_iterations(max)
|
||||
state.max_iterations = max
|
||||
end
|
||||
|
||||
--- Get the count of collected changes
|
||||
---@return number
|
||||
function M.get_changes_count()
|
||||
return diff_review.count()
|
||||
end
|
||||
|
||||
--- Show the diff review UI for all collected changes
|
||||
function M.show_diff_review()
|
||||
diff_review.open()
|
||||
end
|
||||
|
||||
--- Check if diff review is open
|
||||
---@return boolean
|
||||
function M.is_review_open()
|
||||
return diff_review.is_open()
|
||||
end
|
||||
|
||||
--- Prompt user to continue or stop at max iterations
|
||||
---@param context table File context
|
||||
---@param callbacks AgentCallbacks
|
||||
function M.prompt_continue(context, callbacks)
|
||||
vim.schedule(function()
|
||||
vim.ui.select({ "Continue (25 more iterations)", "Stop and save for later" }, {
|
||||
prompt = string.format("Agent reached %d iterations. Continue?", state.max_iterations),
|
||||
}, function(choice)
|
||||
if choice and choice:match("^Continue") then
|
||||
-- Reset iteration counter and continue
|
||||
state.current_iteration = 0
|
||||
logs.info("User chose to continue, resetting iteration counter")
|
||||
M.agent_loop(context, callbacks)
|
||||
else
|
||||
-- Save state for later resume
|
||||
logs.info("User chose to stop, saving state for resume")
|
||||
resume.save(
|
||||
state.conversation,
|
||||
state.pending_tool_results,
|
||||
state.current_iteration,
|
||||
state.original_prompt
|
||||
)
|
||||
state.is_running = false
|
||||
callbacks.on_text("Agent paused. Use /continue to resume later.")
|
||||
callbacks.on_complete()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Continue a previously stopped agent session
|
||||
---@param callbacks AgentCallbacks
|
||||
---@return boolean Success
|
||||
function M.continue_session(callbacks)
|
||||
if state.is_running then
|
||||
utils.notify("Agent is already running", vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
local saved = resume.load()
|
||||
if not saved then
|
||||
utils.notify("No saved agent session to continue", vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
logs.info("Resuming agent session")
|
||||
logs.info(string.format("Loaded %d messages, iteration %d", #saved.conversation, saved.iteration))
|
||||
|
||||
-- Restore state
|
||||
state.conversation = saved.conversation
|
||||
state.pending_tool_results = saved.pending_tool_results or {}
|
||||
state.current_iteration = 0 -- Reset for fresh iterations
|
||||
state.original_prompt = saved.original_prompt
|
||||
state.is_running = true
|
||||
state.current_callbacks = callbacks
|
||||
|
||||
-- Build context from current state
|
||||
local llm = require("codetyper.core.llm")
|
||||
local context = {}
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
if current_file ~= "" and vim.fn.filereadable(current_file) == 1 then
|
||||
context = llm.build_context(current_file, "agent")
|
||||
end
|
||||
state.current_context = context
|
||||
|
||||
-- Clear saved state
|
||||
resume.clear()
|
||||
|
||||
-- Add continuation message
|
||||
table.insert(state.conversation, {
|
||||
role = "user",
|
||||
content = "Continue where you left off. Complete the remaining tasks.",
|
||||
})
|
||||
|
||||
-- Continue the loop
|
||||
callbacks.on_text("Resuming agent session...")
|
||||
M.agent_loop(context, callbacks)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Check if there's a saved session to continue
|
||||
---@return boolean
|
||||
function M.has_saved_session()
|
||||
return resume.has_saved_state()
|
||||
end
|
||||
|
||||
--- Get info about saved session
|
||||
---@return table|nil
|
||||
function M.get_saved_session_info()
|
||||
return resume.get_info()
|
||||
end
|
||||
|
||||
return M
|
||||
425
lua/codetyper/features/agents/linter.lua
Normal file
425
lua/codetyper/features/agents/linter.lua
Normal file
@@ -0,0 +1,425 @@
|
||||
---@mod codetyper.agent.linter Linter validation for generated code
|
||||
---@brief [[
|
||||
--- Validates generated code by checking LSP diagnostics after injection.
|
||||
--- Automatically saves the file and waits for LSP to update before checking.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
local config_params = require("codetyper.params.agents.linter")
|
||||
local prompts = require("codetyper.prompts.agents.linter")
|
||||
|
||||
--- Configuration
|
||||
local config = config_params.config
|
||||
|
||||
--- Diagnostic results for tracking
|
||||
---@type table<number, table>
|
||||
local validation_results = {}
|
||||
|
||||
--- Configure linter behavior
|
||||
---@param opts table Configuration options
|
||||
function M.configure(opts)
|
||||
for k, v in pairs(opts) do
|
||||
if config[k] ~= nil then
|
||||
config[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Get current configuration
|
||||
---@return table
|
||||
function M.get_config()
|
||||
return vim.deepcopy(config)
|
||||
end
|
||||
|
||||
--- Save buffer if modified
|
||||
---@param bufnr number Buffer number
|
||||
---@return boolean success
|
||||
local function save_buffer(bufnr)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Skip if buffer is not modified
|
||||
if not vim.bo[bufnr].modified then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Skip if buffer has no name (unsaved file)
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
if bufname == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Save the buffer
|
||||
local ok, err = pcall(function()
|
||||
vim.api.nvim_buf_call(bufnr, function()
|
||||
vim.cmd("silent! write")
|
||||
end)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "warning",
|
||||
message = "Failed to save buffer: " .. tostring(err),
|
||||
})
|
||||
end)
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Get LSP diagnostics for a buffer
|
||||
---@param bufnr number Buffer number
|
||||
---@param start_line? number Start line (1-indexed)
|
||||
---@param end_line? number End line (1-indexed)
|
||||
---@return table[] diagnostics List of diagnostics
|
||||
function M.get_diagnostics(bufnr, start_line, end_line)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return {}
|
||||
end
|
||||
|
||||
local all_diagnostics = vim.diagnostic.get(bufnr)
|
||||
local filtered = {}
|
||||
|
||||
for _, diag in ipairs(all_diagnostics) do
|
||||
-- Filter by severity
|
||||
if diag.severity <= config.min_severity then
|
||||
-- Filter by line range if specified
|
||||
if start_line and end_line then
|
||||
local diag_line = diag.lnum + 1 -- Convert to 1-indexed
|
||||
if diag_line >= start_line and diag_line <= end_line then
|
||||
table.insert(filtered, diag)
|
||||
end
|
||||
else
|
||||
table.insert(filtered, diag)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return filtered
|
||||
end
|
||||
|
||||
--- Format a diagnostic for display
|
||||
---@param diag table Diagnostic object
|
||||
---@return string
|
||||
local function format_diagnostic(diag)
|
||||
local severity_names = {
|
||||
[vim.diagnostic.severity.ERROR] = "ERROR",
|
||||
[vim.diagnostic.severity.WARN] = "WARN",
|
||||
[vim.diagnostic.severity.INFO] = "INFO",
|
||||
[vim.diagnostic.severity.HINT] = "HINT",
|
||||
}
|
||||
local severity = severity_names[diag.severity] or "UNKNOWN"
|
||||
local line = diag.lnum + 1
|
||||
local source = diag.source or "lsp"
|
||||
return string.format("[%s] Line %d (%s): %s", severity, line, source, diag.message)
|
||||
end
|
||||
|
||||
--- Check if there are errors in generated code region
|
||||
---@param bufnr number Buffer number
|
||||
---@param start_line number Start line (1-indexed)
|
||||
---@param end_line number End line (1-indexed)
|
||||
---@return table result {has_errors, has_warnings, diagnostics, summary}
|
||||
function M.check_region(bufnr, start_line, end_line)
|
||||
local diagnostics = M.get_diagnostics(bufnr, start_line, end_line)
|
||||
|
||||
local errors = 0
|
||||
local warnings = 0
|
||||
|
||||
for _, diag in ipairs(diagnostics) do
|
||||
if diag.severity == vim.diagnostic.severity.ERROR then
|
||||
errors = errors + 1
|
||||
elseif diag.severity == vim.diagnostic.severity.WARN then
|
||||
warnings = warnings + 1
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
has_errors = errors > 0,
|
||||
has_warnings = warnings > 0,
|
||||
error_count = errors,
|
||||
warning_count = warnings,
|
||||
diagnostics = diagnostics,
|
||||
summary = string.format("%d error(s), %d warning(s)", errors, warnings),
|
||||
}
|
||||
end
|
||||
|
||||
--- Validate code after injection and report issues
|
||||
---@param bufnr number Buffer number
|
||||
---@param start_line? number Start line of injected code (1-indexed)
|
||||
---@param end_line? number End line of injected code (1-indexed)
|
||||
---@param callback? function Callback with (result) when validation completes
|
||||
function M.validate_after_injection(bufnr, start_line, end_line, callback)
|
||||
-- Save the file first
|
||||
if config.auto_save then
|
||||
save_buffer(bufnr)
|
||||
end
|
||||
|
||||
-- Wait for LSP to process changes
|
||||
vim.defer_fn(function()
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
if callback then callback(nil) end
|
||||
return
|
||||
end
|
||||
|
||||
local result
|
||||
if start_line and end_line then
|
||||
result = M.check_region(bufnr, start_line, end_line)
|
||||
else
|
||||
-- Check entire buffer
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
result = M.check_region(bufnr, 1, line_count)
|
||||
end
|
||||
|
||||
-- Store result for this buffer
|
||||
validation_results[bufnr] = {
|
||||
timestamp = os.time(),
|
||||
result = result,
|
||||
start_line = start_line,
|
||||
end_line = end_line,
|
||||
}
|
||||
|
||||
-- Log results
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
if result.has_errors then
|
||||
logs.add({
|
||||
type = "error",
|
||||
message = string.format("Linter found issues: %s", result.summary),
|
||||
})
|
||||
-- Log individual errors
|
||||
for _, diag in ipairs(result.diagnostics) do
|
||||
if diag.severity == vim.diagnostic.severity.ERROR then
|
||||
logs.add({
|
||||
type = "error",
|
||||
message = format_diagnostic(diag),
|
||||
})
|
||||
end
|
||||
end
|
||||
elseif result.has_warnings then
|
||||
logs.add({
|
||||
type = "warning",
|
||||
message = string.format("Linter warnings: %s", result.summary),
|
||||
})
|
||||
else
|
||||
logs.add({
|
||||
type = "success",
|
||||
message = "Linter check passed - no errors or warnings",
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- Notify user
|
||||
if result.has_errors then
|
||||
vim.notify(
|
||||
string.format("Generated code has lint errors: %s", result.summary),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
|
||||
-- Offer to fix if configured
|
||||
if config.auto_offer_fix and #result.diagnostics > 0 then
|
||||
M.offer_fix(bufnr, result)
|
||||
end
|
||||
elseif result.has_warnings then
|
||||
vim.notify(
|
||||
string.format("Generated code has warnings: %s", result.summary),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
|
||||
if callback then
|
||||
callback(result)
|
||||
end
|
||||
end, config.diagnostic_delay_ms)
|
||||
end
|
||||
|
||||
--- Offer to fix lint errors using AI
|
||||
---@param bufnr number Buffer number
|
||||
---@param result table Validation result
|
||||
function M.offer_fix(bufnr, result)
|
||||
if not result.has_errors and not result.has_warnings then
|
||||
return
|
||||
end
|
||||
|
||||
-- Build error summary for prompt
|
||||
local error_messages = {}
|
||||
for _, diag in ipairs(result.diagnostics) do
|
||||
table.insert(error_messages, format_diagnostic(diag))
|
||||
end
|
||||
|
||||
vim.ui.select(
|
||||
{ "Yes - Auto-fix with AI", "No - I'll fix manually", "Show errors in quickfix" },
|
||||
{
|
||||
prompt = string.format("Found %d issue(s). Would you like AI to fix them?", #result.diagnostics),
|
||||
},
|
||||
function(choice)
|
||||
if not choice then return end
|
||||
|
||||
if choice:match("^Yes") then
|
||||
M.request_ai_fix(bufnr, result)
|
||||
elseif choice:match("quickfix") then
|
||||
M.show_in_quickfix(bufnr, result)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
--- Show lint errors in quickfix list
|
||||
---@param bufnr number Buffer number
|
||||
---@param result table Validation result
|
||||
function M.show_in_quickfix(bufnr, result)
|
||||
local qf_items = {}
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
|
||||
for _, diag in ipairs(result.diagnostics) do
|
||||
table.insert(qf_items, {
|
||||
bufnr = bufnr,
|
||||
filename = bufname,
|
||||
lnum = diag.lnum + 1,
|
||||
col = diag.col + 1,
|
||||
text = diag.message,
|
||||
type = diag.severity == vim.diagnostic.severity.ERROR and "E" or "W",
|
||||
})
|
||||
end
|
||||
|
||||
vim.fn.setqflist(qf_items, "r")
|
||||
vim.cmd("copen")
|
||||
end
|
||||
|
||||
--- Request AI to fix lint errors
|
||||
---@param bufnr number Buffer number
|
||||
---@param result table Validation result
|
||||
function M.request_ai_fix(bufnr, result)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
local filepath = vim.api.nvim_buf_get_name(bufnr)
|
||||
|
||||
-- Build fix prompt
|
||||
local error_list = {}
|
||||
for _, diag in ipairs(result.diagnostics) do
|
||||
table.insert(error_list, format_diagnostic(diag))
|
||||
end
|
||||
|
||||
-- Get the affected code region
|
||||
local start_line = result.diagnostics[1] and (result.diagnostics[1].lnum + 1) or 1
|
||||
local end_line = start_line
|
||||
for _, diag in ipairs(result.diagnostics) do
|
||||
local line = diag.lnum + 1
|
||||
if line < start_line then start_line = line end
|
||||
if line > end_line then end_line = line end
|
||||
end
|
||||
|
||||
-- Expand range by a few lines for context
|
||||
start_line = math.max(1, start_line - 5)
|
||||
end_line = math.min(vim.api.nvim_buf_line_count(bufnr), end_line + 5)
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false)
|
||||
local code_context = table.concat(lines, "\n")
|
||||
|
||||
-- Create fix prompt using inline tag
|
||||
local fix_prompt = string.format(
|
||||
prompts.fix_request,
|
||||
table.concat(error_list, "\n"),
|
||||
start_line,
|
||||
end_line,
|
||||
code_context
|
||||
)
|
||||
|
||||
-- Queue the fix through the scheduler
|
||||
local scheduler = require("codetyper.core.scheduler.scheduler")
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
local patch_mod = require("codetyper.core.diff.patch")
|
||||
|
||||
-- Ensure scheduler is running
|
||||
if not scheduler.status().running then
|
||||
scheduler.start()
|
||||
end
|
||||
|
||||
-- Take snapshot
|
||||
local snapshot = patch_mod.snapshot_buffer(bufnr, {
|
||||
start_line = start_line,
|
||||
end_line = end_line,
|
||||
})
|
||||
|
||||
-- Enqueue fix request
|
||||
queue.enqueue({
|
||||
id = queue.generate_id(),
|
||||
bufnr = bufnr,
|
||||
range = { start_line = start_line, end_line = end_line },
|
||||
timestamp = os.clock(),
|
||||
changedtick = snapshot.changedtick,
|
||||
content_hash = snapshot.content_hash,
|
||||
prompt_content = fix_prompt,
|
||||
target_path = filepath,
|
||||
priority = 1, -- High priority for fixes
|
||||
status = "pending",
|
||||
attempt_count = 0,
|
||||
intent = {
|
||||
type = "fix",
|
||||
action = "replace",
|
||||
confidence = 0.9,
|
||||
},
|
||||
scope_range = { start_line = start_line, end_line = end_line },
|
||||
source = "linter_fix",
|
||||
})
|
||||
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Queued AI fix request for lint errors",
|
||||
})
|
||||
end)
|
||||
|
||||
vim.notify("Queued AI fix request for lint errors", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
--- Get last validation result for a buffer
|
||||
---@param bufnr number Buffer number
|
||||
---@return table|nil result
|
||||
function M.get_last_result(bufnr)
|
||||
return validation_results[bufnr]
|
||||
end
|
||||
|
||||
--- Clear validation results for a buffer
|
||||
---@param bufnr number Buffer number
|
||||
function M.clear_result(bufnr)
|
||||
validation_results[bufnr] = nil
|
||||
end
|
||||
|
||||
--- Check if buffer has any lint errors currently
|
||||
---@param bufnr number Buffer number
|
||||
---@return boolean has_errors
|
||||
function M.has_errors(bufnr)
|
||||
local diagnostics = vim.diagnostic.get(bufnr, {
|
||||
severity = vim.diagnostic.severity.ERROR,
|
||||
})
|
||||
return #diagnostics > 0
|
||||
end
|
||||
|
||||
--- Check if buffer has any lint warnings currently
|
||||
---@param bufnr number Buffer number
|
||||
---@return boolean has_warnings
|
||||
function M.has_warnings(bufnr)
|
||||
local diagnostics = vim.diagnostic.get(bufnr, {
|
||||
severity = { min = vim.diagnostic.severity.WARN },
|
||||
})
|
||||
return #diagnostics > 0
|
||||
end
|
||||
|
||||
--- Validate all buffers with recent changes
|
||||
function M.validate_all_changed()
|
||||
for bufnr, data in pairs(validation_results) do
|
||||
if vim.api.nvim_buf_is_valid(bufnr) then
|
||||
M.validate_after_injection(bufnr, data.start_line, data.end_line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -10,6 +10,8 @@ local M = {}
|
||||
---@field allow_list table<string, boolean> Patterns always allowed
|
||||
---@field deny_list table<string, boolean> Patterns always denied
|
||||
|
||||
local params = require("codetyper.params.agents.permissions")
|
||||
|
||||
local state = {
|
||||
session_allowed = {},
|
||||
allow_list = {},
|
||||
@@ -17,59 +19,10 @@ local state = {
|
||||
}
|
||||
|
||||
--- Dangerous command patterns that should never be auto-allowed
|
||||
local DANGEROUS_PATTERNS = {
|
||||
"^rm%s+%-rf",
|
||||
"^rm%s+%-r%s+/",
|
||||
"^rm%s+/",
|
||||
"^sudo%s+rm",
|
||||
"^chmod%s+777",
|
||||
"^chmod%s+%-R",
|
||||
"^chown%s+%-R",
|
||||
"^dd%s+",
|
||||
"^mkfs",
|
||||
"^fdisk",
|
||||
"^format",
|
||||
":.*>%s*/dev/",
|
||||
"^curl.*|.*sh",
|
||||
"^wget.*|.*sh",
|
||||
"^eval%s+",
|
||||
"`;.*`",
|
||||
"%$%(.*%)",
|
||||
"fork%s*bomb",
|
||||
}
|
||||
local DANGEROUS_PATTERNS = params.dangerous_patterns
|
||||
|
||||
--- Safe command patterns that can be auto-allowed
|
||||
local SAFE_PATTERNS = {
|
||||
"^ls%s",
|
||||
"^ls$",
|
||||
"^cat%s",
|
||||
"^head%s",
|
||||
"^tail%s",
|
||||
"^grep%s",
|
||||
"^find%s",
|
||||
"^pwd$",
|
||||
"^echo%s",
|
||||
"^wc%s",
|
||||
"^which%s",
|
||||
"^type%s",
|
||||
"^file%s",
|
||||
"^stat%s",
|
||||
"^git%s+status",
|
||||
"^git%s+log",
|
||||
"^git%s+diff",
|
||||
"^git%s+branch",
|
||||
"^git%s+show",
|
||||
"^npm%s+list",
|
||||
"^npm%s+ls",
|
||||
"^npm%s+outdated",
|
||||
"^yarn%s+list",
|
||||
"^cargo%s+check",
|
||||
"^cargo%s+test",
|
||||
"^go%s+test",
|
||||
"^go%s+build",
|
||||
"^make%s+test",
|
||||
"^make%s+check",
|
||||
}
|
||||
local SAFE_PATTERNS = params.safe_patterns
|
||||
|
||||
---@alias PermissionLevel "allow"|"allow_session"|"allow_list"|"reject"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---@mod codetyper.ask Ask window for Codetyper.nvim (similar to avante.nvim)
|
||||
---@mod codetyper.ask Ask window for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
---@class AskState
|
||||
---@field input_buf number|nil Input buffer
|
||||
@@ -26,6 +26,7 @@ local state = {
|
||||
agent_mode = false, -- Whether agent mode is enabled (can make file changes)
|
||||
log_listener_id = nil, -- Listener ID for LLM logs
|
||||
show_logs = true, -- Whether to show LLM logs in chat
|
||||
selection_context = nil, -- Visual selection passed when opening
|
||||
}
|
||||
|
||||
--- Get the ask window configuration
|
||||
@@ -344,7 +345,7 @@ local function setup_log_listener()
|
||||
-- Remove existing listener if any
|
||||
if state.log_listener_id then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.remove_listener(state.log_listener_id)
|
||||
end)
|
||||
state.log_listener_id = nil
|
||||
@@ -361,7 +362,7 @@ end
|
||||
local function remove_log_listener()
|
||||
if state.log_listener_id then
|
||||
pcall(function()
|
||||
local logs = require("codetyper.agent.logs")
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.remove_listener(state.log_listener_id)
|
||||
end)
|
||||
state.log_listener_id = nil
|
||||
@@ -369,13 +370,21 @@ local function remove_log_listener()
|
||||
end
|
||||
|
||||
--- Open the ask panel
|
||||
function M.open()
|
||||
---@param selection table|nil Visual selection context {text, start_line, end_line, filepath, filename, language}
|
||||
function M.open(selection)
|
||||
-- Use the is_open() function which validates window state
|
||||
if M.is_open() then
|
||||
-- If already open and new selection provided, add it as context
|
||||
if selection and selection.text and selection.text ~= "" then
|
||||
M.add_selection_context(selection)
|
||||
end
|
||||
M.focus_input()
|
||||
return
|
||||
end
|
||||
|
||||
-- Store selection context for use in questions
|
||||
state.selection_context = selection
|
||||
|
||||
local dims = calculate_dimensions()
|
||||
|
||||
-- Store the target width
|
||||
@@ -479,6 +488,70 @@ function M.open()
|
||||
-- Focus the input window and start insert mode
|
||||
vim.api.nvim_set_current_win(state.input_win)
|
||||
vim.cmd("startinsert")
|
||||
|
||||
-- If we have a selection, show it as context
|
||||
if selection and selection.text and selection.text ~= "" then
|
||||
vim.schedule(function()
|
||||
M.add_selection_context(selection)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- Add visual selection as context in the chat
|
||||
---@param selection table Selection info {text, start_line, end_line, filepath, filename, language}
|
||||
function M.add_selection_context(selection)
|
||||
if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
state.selection_context = selection
|
||||
|
||||
vim.bo[state.output_buf].modifiable = true
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false)
|
||||
|
||||
-- Format the selection display
|
||||
local location = ""
|
||||
if selection.filename then
|
||||
location = selection.filename
|
||||
if selection.start_line then
|
||||
location = location .. ":" .. selection.start_line
|
||||
if selection.end_line and selection.end_line ~= selection.start_line then
|
||||
location = location .. "-" .. selection.end_line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local new_lines = {
|
||||
"",
|
||||
"┌─ 📋 Selected Code ─────────────────",
|
||||
"│ " .. location,
|
||||
"│",
|
||||
}
|
||||
|
||||
-- Add the selected code with syntax hints
|
||||
local lang = selection.language or "text"
|
||||
for _, line in ipairs(vim.split(selection.text, "\n")) do
|
||||
table.insert(new_lines, "│ " .. line)
|
||||
end
|
||||
|
||||
table.insert(new_lines, "│")
|
||||
table.insert(new_lines, "└─────────────────────────────────────")
|
||||
table.insert(new_lines, "")
|
||||
table.insert(new_lines, "Ask about this code or describe what you'd like to do with it.")
|
||||
|
||||
for _, line in ipairs(new_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines)
|
||||
vim.bo[state.output_buf].modifiable = false
|
||||
|
||||
-- Scroll to bottom
|
||||
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
||||
local line_count = vim.api.nvim_buf_line_count(state.output_buf)
|
||||
vim.api.nvim_win_set_cursor(state.output_win, { line_count, 0 })
|
||||
end
|
||||
end
|
||||
|
||||
--- Show file picker for @ mentions
|
||||
@@ -903,18 +976,49 @@ local function continue_submit(question, intent, context, file_context, file_cou
|
||||
|
||||
local client = llm.get_client()
|
||||
|
||||
-- Build full prompt WITH file contents
|
||||
local full_prompt = question
|
||||
if file_context ~= "" then
|
||||
full_prompt = "USER QUESTION: "
|
||||
.. question
|
||||
.. "\n\n"
|
||||
.. "ATTACHED FILE CONTENTS (please analyze these):"
|
||||
.. file_context
|
||||
-- Build recent conversation context (limit to last N entries)
|
||||
local history_context = ""
|
||||
do
|
||||
local max_entries = 8
|
||||
local total = #state.history
|
||||
local start_i = 1
|
||||
if total > max_entries then
|
||||
start_i = total - max_entries + 1
|
||||
end
|
||||
if total > 0 then
|
||||
history_context = "\n\n=== PREVIOUS CONVERSATION ===\n"
|
||||
for i = start_i, total do
|
||||
local m = state.history[i]
|
||||
local role = (m.role == "assistant") and "ASSISTANT" or "USER"
|
||||
history_context = history_context .. role .. ": " .. (m.content or "") .. "\n"
|
||||
end
|
||||
history_context = history_context .. "=== END PREVIOUS CONVERSATION ===\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
-- Also add current file if no files were explicitly attached
|
||||
if file_count == 0 and context.current_content and context.current_content ~= "" then
|
||||
-- Build full prompt starting with recent conversation + user question
|
||||
local full_prompt = history_context .. "USER QUESTION: " .. question
|
||||
|
||||
-- Add visual selection context if present
|
||||
if state.selection_context and state.selection_context.text and state.selection_context.text ~= "" then
|
||||
local sel = state.selection_context
|
||||
local location = sel.filename or "unknown"
|
||||
if sel.start_line then
|
||||
location = location .. ":" .. sel.start_line
|
||||
if sel.end_line and sel.end_line ~= sel.start_line then
|
||||
location = location .. "-" .. sel.end_line
|
||||
end
|
||||
end
|
||||
full_prompt = full_prompt .. "\n\nSELECTED CODE (" .. location .. "):\n```" .. (sel.language or "") .. "\n"
|
||||
full_prompt = full_prompt .. sel.text .. "\n```"
|
||||
end
|
||||
|
||||
if file_context ~= "" then
|
||||
full_prompt = full_prompt .. "\n\nATTACHED FILE CONTENTS (please analyze these):" .. file_context
|
||||
end
|
||||
|
||||
-- Also add current file if no files were explicitly attached and no selection
|
||||
if file_count == 0 and not state.selection_context and context.current_content and context.current_content ~= "" then
|
||||
full_prompt = "USER QUESTION: "
|
||||
.. question
|
||||
.. "\n\n"
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
---@class ExplorationState
|
||||
---@field is_exploring boolean
|
||||
@@ -17,74 +17,9 @@ local M = {}
|
||||
---@field body string[] Non-import code lines
|
||||
---@field import_lines table<number, boolean> Map of line numbers that are imports
|
||||
|
||||
--- Language-specific import patterns
|
||||
local import_patterns = {
|
||||
-- JavaScript/TypeScript
|
||||
javascript = {
|
||||
{ pattern = "^%s*import%s+.+%s+from%s+['\"]", multi_line = true },
|
||||
{ pattern = "^%s*import%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*import%s*{", multi_line = true },
|
||||
{ pattern = "^%s*import%s*%*", multi_line = true },
|
||||
{ pattern = "^%s*export%s+{.+}%s+from%s+['\"]", multi_line = true },
|
||||
{ pattern = "^%s*const%s+%w+%s*=%s*require%(['\"]", multi_line = false },
|
||||
{ pattern = "^%s*let%s+%w+%s*=%s*require%(['\"]", multi_line = false },
|
||||
{ pattern = "^%s*var%s+%w+%s*=%s*require%(['\"]", multi_line = false },
|
||||
},
|
||||
-- Python
|
||||
python = {
|
||||
{ pattern = "^%s*import%s+%w", multi_line = false },
|
||||
{ pattern = "^%s*from%s+[%w%.]+%s+import%s+", multi_line = true },
|
||||
},
|
||||
-- Lua
|
||||
lua = {
|
||||
{ pattern = "^%s*local%s+%w+%s*=%s*require%s*%(?['\"]", multi_line = false },
|
||||
{ pattern = "^%s*require%s*%(?['\"]", multi_line = false },
|
||||
},
|
||||
-- Go
|
||||
go = {
|
||||
{ pattern = "^%s*import%s+%(?", multi_line = true },
|
||||
},
|
||||
-- Rust
|
||||
rust = {
|
||||
{ pattern = "^%s*use%s+", multi_line = true },
|
||||
{ pattern = "^%s*extern%s+crate%s+", multi_line = false },
|
||||
},
|
||||
-- C/C++
|
||||
c = {
|
||||
{ pattern = "^%s*#include%s*[<\"]", multi_line = false },
|
||||
},
|
||||
-- Java/Kotlin
|
||||
java = {
|
||||
{ pattern = "^%s*import%s+", multi_line = false },
|
||||
},
|
||||
-- Ruby
|
||||
ruby = {
|
||||
{ pattern = "^%s*require%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*require_relative%s+['\"]", multi_line = false },
|
||||
},
|
||||
-- PHP
|
||||
php = {
|
||||
{ pattern = "^%s*use%s+", multi_line = false },
|
||||
{ pattern = "^%s*require%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*require_once%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*include%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*include_once%s+['\"]", multi_line = false },
|
||||
},
|
||||
}
|
||||
|
||||
-- Alias common extensions to language configs
|
||||
import_patterns.ts = import_patterns.javascript
|
||||
import_patterns.tsx = import_patterns.javascript
|
||||
import_patterns.jsx = import_patterns.javascript
|
||||
import_patterns.mjs = import_patterns.javascript
|
||||
import_patterns.cjs = import_patterns.javascript
|
||||
import_patterns.py = import_patterns.python
|
||||
import_patterns.cpp = import_patterns.c
|
||||
import_patterns.hpp = import_patterns.c
|
||||
import_patterns.h = import_patterns.c
|
||||
import_patterns.kt = import_patterns.java
|
||||
import_patterns.rs = import_patterns.rust
|
||||
import_patterns.rb = import_patterns.ruby
|
||||
local utils = require("codetyper.support.utils")
|
||||
local languages = require("codetyper.params.agents.languages")
|
||||
local import_patterns = languages.import_patterns
|
||||
|
||||
--- Check if a line is an import statement for the given language
|
||||
---@param line string
|
||||
@@ -100,83 +35,13 @@ local function is_import_line(line, patterns)
|
||||
return false, false
|
||||
end
|
||||
|
||||
--- Check if a line is empty or a comment
|
||||
---@param line string
|
||||
---@param filetype string
|
||||
---@return boolean
|
||||
local function is_empty_or_comment(line, filetype)
|
||||
local trimmed = line:match("^%s*(.-)%s*$")
|
||||
if trimmed == "" then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Language-specific comment patterns
|
||||
local comment_patterns = {
|
||||
lua = { "^%-%-" },
|
||||
python = { "^#" },
|
||||
javascript = { "^//", "^/%*", "^%*" },
|
||||
typescript = { "^//", "^/%*", "^%*" },
|
||||
go = { "^//", "^/%*", "^%*" },
|
||||
rust = { "^//", "^/%*", "^%*" },
|
||||
c = { "^//", "^/%*", "^%*", "^#" },
|
||||
java = { "^//", "^/%*", "^%*" },
|
||||
ruby = { "^#" },
|
||||
php = { "^//", "^/%*", "^%*", "^#" },
|
||||
}
|
||||
|
||||
local patterns = comment_patterns[filetype] or comment_patterns.javascript
|
||||
for _, pattern in ipairs(patterns) do
|
||||
if trimmed:match(pattern) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Check if a line ends a multi-line import
|
||||
---@param line string
|
||||
---@param filetype string
|
||||
---@return boolean
|
||||
local function ends_multiline_import(line, filetype)
|
||||
-- Check for closing patterns
|
||||
if filetype == "javascript" or filetype == "typescript" or filetype == "ts" or filetype == "tsx" then
|
||||
-- ES6 imports end with 'from "..." ;' or just ';' or a line with just '}'
|
||||
if line:match("from%s+['\"][^'\"]+['\"]%s*;?%s*$") then
|
||||
return true
|
||||
end
|
||||
if line:match("}%s*from%s+['\"]") then
|
||||
return true
|
||||
end
|
||||
if line:match("^%s*}%s*;?%s*$") then
|
||||
return true
|
||||
end
|
||||
if line:match(";%s*$") then
|
||||
return true
|
||||
end
|
||||
elseif filetype == "python" or filetype == "py" then
|
||||
-- Python single-line import: doesn't end with \, (, or ,
|
||||
-- Examples: "from typing import List, Dict" or "import os"
|
||||
if not line:match("\\%s*$") and not line:match("%(%s*$") and not line:match(",%s*$") then
|
||||
return true
|
||||
end
|
||||
-- Python multiline imports end with closing paren
|
||||
if line:match("%)%s*$") then
|
||||
return true
|
||||
end
|
||||
elseif filetype == "go" then
|
||||
-- Go multi-line imports end with ')'
|
||||
if line:match("%)%s*$") then
|
||||
return true
|
||||
end
|
||||
elseif filetype == "rust" or filetype == "rs" then
|
||||
-- Rust use statements end with ';'
|
||||
if line:match(";%s*$") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
return utils.ends_multiline_import(line, filetype)
|
||||
end
|
||||
|
||||
--- Parse code into imports and body
|
||||
@@ -285,7 +150,7 @@ function M.find_import_section(bufnr, filetype)
|
||||
if is_multi and not ends_multiline_import(line, filetype) then
|
||||
in_multiline = true
|
||||
end
|
||||
elseif is_empty_or_comment(line, filetype) then
|
||||
elseif utils.is_empty_or_comment(line, filetype) then
|
||||
-- Allow gaps in import section
|
||||
if first_import then
|
||||
consecutive_non_import = consecutive_non_import + 1
|
||||
@@ -388,34 +253,11 @@ function M.sort_imports(imports, filetype)
|
||||
local local_imports = {}
|
||||
|
||||
for _, imp in ipairs(imports) do
|
||||
-- Detect import type based on patterns
|
||||
local is_local = false
|
||||
local is_builtin = false
|
||||
local category = utils.classify_import(imp, filetype)
|
||||
|
||||
if filetype == "javascript" or filetype == "typescript" or filetype == "ts" or filetype == "tsx" then
|
||||
-- Local: starts with . or ..
|
||||
is_local = imp:match("from%s+['\"]%.") or imp:match("require%(['\"]%.")
|
||||
-- Node builtin modules
|
||||
is_builtin = imp:match("from%s+['\"]node:") or imp:match("from%s+['\"]fs['\"]")
|
||||
or imp:match("from%s+['\"]path['\"]") or imp:match("from%s+['\"]http['\"]")
|
||||
elseif filetype == "python" or filetype == "py" then
|
||||
-- Local: relative imports
|
||||
is_local = imp:match("^from%s+%.") or imp:match("^import%s+%.")
|
||||
-- Python stdlib (simplified check)
|
||||
is_builtin = imp:match("^import%s+os") or imp:match("^import%s+sys")
|
||||
or imp:match("^from%s+os%s+") or imp:match("^from%s+sys%s+")
|
||||
or imp:match("^import%s+re") or imp:match("^import%s+json")
|
||||
elseif filetype == "lua" then
|
||||
-- Local: relative requires
|
||||
is_local = imp:match("require%(['\"]%.") or imp:match("require%s+['\"]%.")
|
||||
elseif filetype == "go" then
|
||||
-- Local: project imports (contain /)
|
||||
is_local = imp:match("['\"][^'\"]+/[^'\"]+['\"]") and not imp:match("github%.com")
|
||||
end
|
||||
|
||||
if is_builtin then
|
||||
if category == "builtin" then
|
||||
table.insert(builtin, imp)
|
||||
elseif is_local then
|
||||
elseif category == "local" then
|
||||
table.insert(local_imports, imp)
|
||||
else
|
||||
table.insert(third_party, imp)
|
||||
@@ -533,7 +375,7 @@ function M.inject(bufnr, code, opts)
|
||||
local trimmed = line:match("^%s*(.-)%s*$")
|
||||
-- Skip shebang, docstrings, and initial comments
|
||||
if trimmed ~= "" and not trimmed:match("^#!")
|
||||
and not trimmed:match("^['\"]") and not is_empty_or_comment(line, filetype) then
|
||||
and not trimmed:match("^['\"]") and not utils.is_empty_or_comment(line, filetype) then
|
||||
insert_at = i - 1
|
||||
break
|
||||
end
|
||||
@@ -5,7 +5,7 @@
|
||||
local M = {}
|
||||
|
||||
local parser = require("codetyper.parser")
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Get list of files for completion
|
||||
---@param prefix string Prefix to filter files
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local scanner = require("codetyper.indexer.scanner")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local scanner = require("codetyper.features.indexer.scanner")
|
||||
|
||||
--- Language-specific query patterns for Tree-sitter
|
||||
local TS_QUERIES = {
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Index schema version for migrations
|
||||
local INDEX_VERSION = 1
|
||||
@@ -194,8 +194,8 @@ end
|
||||
---@param callback? fun(index: ProjectIndex)
|
||||
---@return ProjectIndex|nil
|
||||
function M.index_project(callback)
|
||||
local scanner = require("codetyper.indexer.scanner")
|
||||
local analyzer = require("codetyper.indexer.analyzer")
|
||||
local scanner = require("codetyper.features.indexer.scanner")
|
||||
local analyzer = require("codetyper.features.indexer.analyzer")
|
||||
|
||||
local index = create_empty_index()
|
||||
local root = utils.get_project_root()
|
||||
@@ -256,7 +256,7 @@ function M.index_project(callback)
|
||||
M.save_index(index)
|
||||
|
||||
-- Store memories
|
||||
local memory = require("codetyper.indexer.memory")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
memory.store_index_summary(index)
|
||||
|
||||
-- Sync project summary to brain
|
||||
@@ -331,8 +331,8 @@ end
|
||||
---@param filepath string
|
||||
---@return FileIndex|nil
|
||||
function M.index_file(filepath)
|
||||
local analyzer = require("codetyper.indexer.analyzer")
|
||||
local memory = require("codetyper.indexer.memory")
|
||||
local analyzer = require("codetyper.features.indexer.analyzer")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
local root = utils.get_project_root()
|
||||
|
||||
if not root then
|
||||
@@ -475,7 +475,7 @@ function M.schedule_index_file(filepath)
|
||||
end
|
||||
|
||||
-- Check if file should be indexed
|
||||
local scanner = require("codetyper.indexer.scanner")
|
||||
local scanner = require("codetyper.features.indexer.scanner")
|
||||
if not scanner.should_index(filepath, config) then
|
||||
return
|
||||
end
|
||||
@@ -496,7 +496,7 @@ end
|
||||
---@param opts {file: string, intent: table|nil, prompt: string, scope: string|nil}
|
||||
---@return table Context information
|
||||
function M.get_context_for(opts)
|
||||
local memory = require("codetyper.indexer.memory")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
local index = M.load_index()
|
||||
|
||||
local context = {
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Memory directories
|
||||
local MEMORIES_DIR = "memories"
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Project type markers
|
||||
local PROJECT_MARKERS = {
|
||||
@@ -22,16 +22,16 @@ function M.setup(opts)
|
||||
return
|
||||
end
|
||||
|
||||
local config = require("codetyper.config")
|
||||
local config = require("codetyper.config.defaults")
|
||||
M.config = config.setup(opts)
|
||||
|
||||
-- Initialize modules
|
||||
local commands = require("codetyper.commands")
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
local tree = require("codetyper.tree")
|
||||
local completion = require("codetyper.completion")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local commands = require("codetyper.adapters.nvim.commands")
|
||||
local gitignore = require("codetyper.support.gitignore")
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
local tree = require("codetyper.support.tree")
|
||||
local completion = require("codetyper.features.completion.inline")
|
||||
local logs_panel = require("codetyper.adapters.nvim.ui.logs_panel")
|
||||
|
||||
-- Register commands
|
||||
commands.setup()
|
||||
@@ -53,25 +53,25 @@ function M.setup(opts)
|
||||
|
||||
-- Initialize project indexer if enabled
|
||||
if M.config.indexer and M.config.indexer.enabled then
|
||||
local indexer = require("codetyper.indexer")
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
indexer.setup(M.config.indexer)
|
||||
end
|
||||
|
||||
-- Initialize brain learning system if enabled
|
||||
if M.config.brain and M.config.brain.enabled then
|
||||
local brain = require("codetyper.brain")
|
||||
local brain = require("codetyper.core.memory")
|
||||
brain.setup(M.config.brain)
|
||||
end
|
||||
|
||||
-- Setup inline ghost text suggestions (Copilot-style)
|
||||
if M.config.suggestion and M.config.suggestion.enabled then
|
||||
local suggestion = require("codetyper.suggestion")
|
||||
local suggestion = require("codetyper.features.completion.suggestion")
|
||||
suggestion.setup(M.config.suggestion)
|
||||
end
|
||||
|
||||
-- Start the event-driven scheduler if enabled
|
||||
if M.config.scheduler and M.config.scheduler.enabled then
|
||||
local scheduler = require("codetyper.agent.scheduler")
|
||||
local scheduler = require("codetyper.core.scheduler.scheduler")
|
||||
scheduler.start(M.config.scheduler)
|
||||
end
|
||||
|
||||
@@ -80,7 +80,7 @@ function M.setup(opts)
|
||||
-- Auto-open Ask panel after a short delay (to let UI settle)
|
||||
if M.config.auto_open_ask then
|
||||
vim.defer_fn(function()
|
||||
local ask = require("codetyper.ask")
|
||||
local ask = require("codetyper.features.ask.engine")
|
||||
if not ask.is_open() then
|
||||
ask.open()
|
||||
end
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Inject generated code into target file
|
||||
---@param target_path string Path to target file
|
||||
---@param code string Generated code
|
||||
---@param prompt_type string Type of prompt (refactor, add, document, etc.)
|
||||
function M.inject_code(target_path, code, prompt_type)
|
||||
local window = require("codetyper.window")
|
||||
local window = require("codetyper.adapters.nvim.windows")
|
||||
|
||||
-- Normalize the target path
|
||||
target_path = vim.fn.fnamemodify(target_path, ":p")
|
||||
@@ -109,7 +109,7 @@ function M.inject_add(bufnr, code)
|
||||
local lines = vim.split(code, "\n", { plain = true })
|
||||
|
||||
-- Get cursor position in target window
|
||||
local window = require("codetyper.window")
|
||||
local window = require("codetyper.adapters.nvim.windows")
|
||||
local target_win = window.get_target_win()
|
||||
|
||||
local insert_line
|
||||
|
||||
35
lua/codetyper/params/agents/bash.lua
Normal file
35
lua/codetyper/params/agents/bash.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
M.params = {
|
||||
{
|
||||
name = "command",
|
||||
description = "The shell command to execute",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "cwd",
|
||||
description = "Working directory for the command (optional)",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
{
|
||||
name = "timeout",
|
||||
description = "Timeout in milliseconds (default: 120000)",
|
||||
type = "integer",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
M.returns = {
|
||||
{
|
||||
name = "stdout",
|
||||
description = "Command output",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "error",
|
||||
description = "Error message if command failed",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
40
lua/codetyper/params/agents/confidence.lua
Normal file
40
lua/codetyper/params/agents/confidence.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
---@mod codetyper.params.agents.confidence Parameters for confidence scoring
|
||||
local M = {}
|
||||
|
||||
--- Heuristic weights (must sum to 1.0)
|
||||
M.weights = {
|
||||
length = 0.15, -- Response length relative to prompt
|
||||
uncertainty = 0.30, -- Uncertainty phrases
|
||||
syntax = 0.25, -- Syntax completeness
|
||||
repetition = 0.15, -- Duplicate lines
|
||||
truncation = 0.15, -- Incomplete ending
|
||||
}
|
||||
|
||||
--- Uncertainty phrases that indicate low confidence
|
||||
M.uncertainty_phrases = {
|
||||
-- English
|
||||
"i'm not sure",
|
||||
"i am not sure",
|
||||
"maybe",
|
||||
"perhaps",
|
||||
"might work",
|
||||
"could work",
|
||||
"not certain",
|
||||
"uncertain",
|
||||
"i think",
|
||||
"possibly",
|
||||
"TODO",
|
||||
"FIXME",
|
||||
"XXX",
|
||||
"placeholder",
|
||||
"implement this",
|
||||
"fill in",
|
||||
"your code here",
|
||||
"...", -- Ellipsis as placeholder
|
||||
"# TODO",
|
||||
"// TODO",
|
||||
"-- TODO",
|
||||
"/* TODO",
|
||||
}
|
||||
|
||||
return M
|
||||
33
lua/codetyper/params/agents/conflict.lua
Normal file
33
lua/codetyper/params/agents/conflict.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
---@mod codetyper.params.agents.conflict Parameters for conflict resolution
|
||||
local M = {}
|
||||
|
||||
--- Configuration defaults
|
||||
M.config = {
|
||||
-- Run linter check after accepting AI suggestions
|
||||
lint_after_accept = true,
|
||||
-- Auto-fix lint errors without prompting
|
||||
auto_fix_lint_errors = true,
|
||||
-- Auto-show menu after injecting conflict
|
||||
auto_show_menu = true,
|
||||
-- Auto-show menu for next conflict after resolving one
|
||||
auto_show_next_menu = true,
|
||||
}
|
||||
|
||||
--- Highlight groups
|
||||
M.hl_groups = {
|
||||
current = "CoderConflictCurrent",
|
||||
current_label = "CoderConflictCurrentLabel",
|
||||
incoming = "CoderConflictIncoming",
|
||||
incoming_label = "CoderConflictIncomingLabel",
|
||||
separator = "CoderConflictSeparator",
|
||||
hint = "CoderConflictHint",
|
||||
}
|
||||
|
||||
--- Conflict markers
|
||||
M.markers = {
|
||||
current_start = "<<<<<<< CURRENT",
|
||||
separator = "=======",
|
||||
incoming_end = ">>>>>>> INCOMING",
|
||||
}
|
||||
|
||||
return M
|
||||
48
lua/codetyper/params/agents/context.lua
Normal file
48
lua/codetyper/params/agents/context.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
---@mod codetyper.params.agents.context Parameters for context building
|
||||
local M = {}
|
||||
|
||||
--- Common ignore patterns
|
||||
M.ignore_patterns = {
|
||||
"^%.", -- Hidden files/dirs
|
||||
"node_modules",
|
||||
"%.git$",
|
||||
"__pycache__",
|
||||
"%.pyc$",
|
||||
"target", -- Rust
|
||||
"build",
|
||||
"dist",
|
||||
"%.o$",
|
||||
"%.a$",
|
||||
"%.so$",
|
||||
"%.min%.",
|
||||
"%.map$",
|
||||
}
|
||||
|
||||
--- Key files that are important for understanding the project
|
||||
M.important_files = {
|
||||
["package.json"] = "Node.js project config",
|
||||
["Cargo.toml"] = "Rust project config",
|
||||
["go.mod"] = "Go module config",
|
||||
["pyproject.toml"] = "Python project config",
|
||||
["setup.py"] = "Python setup config",
|
||||
["Makefile"] = "Build configuration",
|
||||
["CMakeLists.txt"] = "CMake config",
|
||||
[".gitignore"] = "Git ignore patterns",
|
||||
["README.md"] = "Project documentation",
|
||||
["init.lua"] = "Neovim plugin entry",
|
||||
["plugin.lua"] = "Neovim plugin config",
|
||||
}
|
||||
|
||||
--- Project type detection indicators
|
||||
M.indicators = {
|
||||
["package.json"] = { type = "node", language = "javascript/typescript" },
|
||||
["Cargo.toml"] = { type = "rust", language = "rust" },
|
||||
["go.mod"] = { type = "go", language = "go" },
|
||||
["pyproject.toml"] = { type = "python", language = "python" },
|
||||
["setup.py"] = { type = "python", language = "python" },
|
||||
["Gemfile"] = { type = "ruby", language = "ruby" },
|
||||
["pom.xml"] = { type = "maven", language = "java" },
|
||||
["build.gradle"] = { type = "gradle", language = "java/kotlin" },
|
||||
}
|
||||
|
||||
return M
|
||||
33
lua/codetyper/params/agents/edit.lua
Normal file
33
lua/codetyper/params/agents/edit.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
M.params = {
|
||||
{
|
||||
name = "path",
|
||||
description = "Path to the file to edit",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "old_string",
|
||||
description = "Text to find and replace (empty string to create new file or append)",
|
||||
type = "string",
|
||||
},
|
||||
{
|
||||
name = "new_string",
|
||||
description = "Text to replace with",
|
||||
type = "string",
|
||||
},
|
||||
}
|
||||
|
||||
M.returns = {
|
||||
{
|
||||
name = "success",
|
||||
description = "Whether the edit was applied",
|
||||
type = "boolean",
|
||||
},
|
||||
{
|
||||
name = "error",
|
||||
description = "Error message if edit failed",
|
||||
type = "string",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
10
lua/codetyper/params/agents/grep.lua
Normal file
10
lua/codetyper/params/agents/grep.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
M.description = [[Searches for a pattern in files using ripgrep.
|
||||
|
||||
Returns file paths and matching lines. Use this to find code by content.
|
||||
|
||||
Example patterns:
|
||||
- "function foo" - Find function definitions
|
||||
- "import.*react" - Find React imports
|
||||
- "TODO|FIXME" - Find todo comments]]
|
||||
|
||||
return M
|
||||
161
lua/codetyper/params/agents/intent.lua
Normal file
161
lua/codetyper/params/agents/intent.lua
Normal file
@@ -0,0 +1,161 @@
|
||||
---@mod codetyper.params.agents.intent Intent patterns and scope configuration
|
||||
local M = {}
|
||||
|
||||
--- Intent patterns with associated metadata
|
||||
M.intent_patterns = {
|
||||
-- Complete: fill in missing implementation
|
||||
complete = {
|
||||
patterns = {
|
||||
"complete",
|
||||
"finish",
|
||||
"implement",
|
||||
"fill in",
|
||||
"fill out",
|
||||
"stub",
|
||||
"todo",
|
||||
"fixme",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 1,
|
||||
},
|
||||
|
||||
-- Refactor: rewrite existing code
|
||||
refactor = {
|
||||
patterns = {
|
||||
"refactor",
|
||||
"rewrite",
|
||||
"restructure",
|
||||
"reorganize",
|
||||
"clean up",
|
||||
"cleanup",
|
||||
"simplify",
|
||||
"improve",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 2,
|
||||
},
|
||||
|
||||
-- Fix: repair bugs or issues
|
||||
fix = {
|
||||
patterns = {
|
||||
"fix",
|
||||
"repair",
|
||||
"correct",
|
||||
"debug",
|
||||
"solve",
|
||||
"resolve",
|
||||
"patch",
|
||||
"bug",
|
||||
"error",
|
||||
"issue",
|
||||
"update",
|
||||
"modify",
|
||||
"change",
|
||||
"adjust",
|
||||
"tweak",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 1,
|
||||
},
|
||||
|
||||
-- Add: insert new code
|
||||
add = {
|
||||
patterns = {
|
||||
"add",
|
||||
"create",
|
||||
"insert",
|
||||
"include",
|
||||
"append",
|
||||
"new",
|
||||
"generate",
|
||||
"write",
|
||||
},
|
||||
scope_hint = nil, -- Could be anywhere
|
||||
action = "insert",
|
||||
priority = 3,
|
||||
},
|
||||
|
||||
-- Document: add documentation
|
||||
document = {
|
||||
patterns = {
|
||||
"document",
|
||||
"comment",
|
||||
"jsdoc",
|
||||
"docstring",
|
||||
"describe",
|
||||
"annotate",
|
||||
"type hint",
|
||||
"typehint",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace", -- Replace with documented version
|
||||
priority = 2,
|
||||
},
|
||||
|
||||
-- Test: generate tests
|
||||
test = {
|
||||
patterns = {
|
||||
"test",
|
||||
"spec",
|
||||
"unit test",
|
||||
"integration test",
|
||||
"coverage",
|
||||
},
|
||||
scope_hint = "file",
|
||||
action = "append",
|
||||
priority = 3,
|
||||
},
|
||||
|
||||
-- Optimize: improve performance
|
||||
optimize = {
|
||||
patterns = {
|
||||
"optimize",
|
||||
"performance",
|
||||
"faster",
|
||||
"efficient",
|
||||
"speed up",
|
||||
"reduce",
|
||||
"minimize",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "replace",
|
||||
priority = 2,
|
||||
},
|
||||
|
||||
-- Explain: provide explanation (no code change)
|
||||
explain = {
|
||||
patterns = {
|
||||
"explain",
|
||||
"what does",
|
||||
"how does",
|
||||
"why",
|
||||
"describe",
|
||||
"walk through",
|
||||
"understand",
|
||||
},
|
||||
scope_hint = "function",
|
||||
action = "none",
|
||||
priority = 4,
|
||||
},
|
||||
}
|
||||
|
||||
--- Scope hint patterns
|
||||
M.scope_patterns = {
|
||||
["this function"] = "function",
|
||||
["this method"] = "function",
|
||||
["the function"] = "function",
|
||||
["the method"] = "function",
|
||||
["this class"] = "class",
|
||||
["the class"] = "class",
|
||||
["this file"] = "file",
|
||||
["the file"] = "file",
|
||||
["this block"] = "block",
|
||||
["the block"] = "block",
|
||||
["this"] = nil, -- Use Tree-sitter to determine
|
||||
["here"] = nil,
|
||||
}
|
||||
|
||||
return M
|
||||
87
lua/codetyper/params/agents/languages.lua
Normal file
87
lua/codetyper/params/agents/languages.lua
Normal file
@@ -0,0 +1,87 @@
|
||||
---@mod codetyper.params.agents.languages Language-specific patterns and configurations
|
||||
local M = {}
|
||||
|
||||
--- Language-specific import patterns
|
||||
M.import_patterns = {
|
||||
-- JavaScript/TypeScript
|
||||
javascript = {
|
||||
{ pattern = "^%s*import%s+.+%s+from%s+['\"]", multi_line = true },
|
||||
{ pattern = "^%s*import%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*import%s*{", multi_line = true },
|
||||
{ pattern = "^%s*import%s*%*", multi_line = true },
|
||||
{ pattern = "^%s*export%s+{.+}%s+from%s+['\"]", multi_line = true },
|
||||
{ pattern = "^%s*const%s+%w+%s*=%s*require%(['\"]", multi_line = false },
|
||||
{ pattern = "^%s*let%s+%w+%s*=%s*require%(['\"]", multi_line = false },
|
||||
{ pattern = "^%s*var%s+%w+%s*=%s*require%(['\"]", multi_line = false },
|
||||
},
|
||||
-- Python
|
||||
python = {
|
||||
{ pattern = "^%s*import%s+%w", multi_line = false },
|
||||
{ pattern = "^%s*from%s+[%w%.]+%s+import%s+", multi_line = true },
|
||||
},
|
||||
-- Lua
|
||||
lua = {
|
||||
{ pattern = "^%s*local%s+%w+%s*=%s*require%s*%(?['\"]", multi_line = false },
|
||||
{ pattern = "^%s*require%s*%(?['\"]", multi_line = false },
|
||||
},
|
||||
-- Go
|
||||
go = {
|
||||
{ pattern = "^%s*import%s+%(?", multi_line = true },
|
||||
},
|
||||
-- Rust
|
||||
rust = {
|
||||
{ pattern = "^%s*use%s+", multi_line = true },
|
||||
{ pattern = "^%s*extern%s+crate%s+", multi_line = false },
|
||||
},
|
||||
-- C/C++
|
||||
c = {
|
||||
{ pattern = "^%s*#include%s*[<\"]", multi_line = false },
|
||||
},
|
||||
-- Java/Kotlin
|
||||
java = {
|
||||
{ pattern = "^%s*import%s+", multi_line = false },
|
||||
},
|
||||
-- Ruby
|
||||
ruby = {
|
||||
{ pattern = "^%s*require%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*require_relative%s+['\"]", multi_line = false },
|
||||
},
|
||||
-- PHP
|
||||
php = {
|
||||
{ pattern = "^%s*use%s+", multi_line = false },
|
||||
{ pattern = "^%s*require%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*require_once%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*include%s+['\"]", multi_line = false },
|
||||
{ pattern = "^%s*include_once%s+['\"]", multi_line = false },
|
||||
},
|
||||
}
|
||||
|
||||
-- Alias common extensions to language configs
|
||||
M.import_patterns.ts = M.import_patterns.javascript
|
||||
M.import_patterns.tsx = M.import_patterns.javascript
|
||||
M.import_patterns.jsx = M.import_patterns.javascript
|
||||
M.import_patterns.mjs = M.import_patterns.javascript
|
||||
M.import_patterns.cjs = M.import_patterns.javascript
|
||||
M.import_patterns.py = M.import_patterns.python
|
||||
M.import_patterns.cpp = M.import_patterns.c
|
||||
M.import_patterns.hpp = M.import_patterns.c
|
||||
M.import_patterns.h = M.import_patterns.c
|
||||
M.import_patterns.kt = M.import_patterns.java
|
||||
M.import_patterns.rs = M.import_patterns.rust
|
||||
M.import_patterns.rb = M.import_patterns.ruby
|
||||
|
||||
--- Language-specific comment patterns
|
||||
M.comment_patterns = {
|
||||
lua = { "^%-%-" },
|
||||
python = { "^#" },
|
||||
javascript = { "^//", "^/%*", "^%*" },
|
||||
typescript = { "^//", "^/%*", "^%*" },
|
||||
go = { "^//", "^/%*", "^%*" },
|
||||
rust = { "^//", "^/%*", "^%*" },
|
||||
c = { "^//", "^/%*", "^%*", "^#" },
|
||||
java = { "^//", "^/%*", "^%*" },
|
||||
ruby = { "^#" },
|
||||
php = { "^//", "^/%*", "^%*", "^#" },
|
||||
}
|
||||
|
||||
return M
|
||||
15
lua/codetyper/params/agents/linter.lua
Normal file
15
lua/codetyper/params/agents/linter.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
---@mod codetyper.params.agents.linter Linter configuration
|
||||
local M = {}
|
||||
|
||||
M.config = {
|
||||
-- Auto-save file after code injection
|
||||
auto_save = true,
|
||||
-- Delay in ms to wait for LSP diagnostics to update
|
||||
diagnostic_delay_ms = 500,
|
||||
-- Severity levels to check (1=Error, 2=Warning, 3=Info, 4=Hint)
|
||||
min_severity = vim.diagnostic.severity.WARN,
|
||||
-- Auto-offer to fix lint errors
|
||||
auto_offer_fix = true,
|
||||
}
|
||||
|
||||
return M
|
||||
36
lua/codetyper/params/agents/logs.lua
Normal file
36
lua/codetyper/params/agents/logs.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
---@mod codetyper.params.agents.logs Log parameters
|
||||
local M = {}
|
||||
|
||||
M.icons = {
|
||||
start = "->",
|
||||
success = "OK",
|
||||
error = "ERR",
|
||||
approval = "??",
|
||||
approved = "YES",
|
||||
rejected = "NO",
|
||||
}
|
||||
|
||||
M.level_icons = {
|
||||
info = "i",
|
||||
debug = ".",
|
||||
request = ">",
|
||||
response = "<",
|
||||
tool = "T",
|
||||
error = "!",
|
||||
warning = "?",
|
||||
success = "i",
|
||||
queue = "Q",
|
||||
patch = "P",
|
||||
}
|
||||
|
||||
M.thinking_types = { "thinking", "reason", "action", "task", "result" }
|
||||
|
||||
M.thinking_prefixes = {
|
||||
thinking = "⏺",
|
||||
reason = "⏺",
|
||||
action = "⏺",
|
||||
task = "✶",
|
||||
result = "",
|
||||
}
|
||||
|
||||
return M
|
||||
15
lua/codetyper/params/agents/parser.lua
Normal file
15
lua/codetyper/params/agents/parser.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
---@mod codetyper.params.agents.parser Parser regex patterns
|
||||
local M = {}
|
||||
|
||||
M.patterns = {
|
||||
fenced_json = "```json%s*(%b{})%s*```",
|
||||
inline_json = '(%{"tool"%s*:%s*"[^"]+"%s*,%s*"parameters"%s*:%s*%b{}%})',
|
||||
}
|
||||
|
||||
M.defaults = {
|
||||
stop_reason = "end_turn",
|
||||
tool_stop_reason = "tool_use",
|
||||
replacement_text = "[Tool call]",
|
||||
}
|
||||
|
||||
return M
|
||||
12
lua/codetyper/params/agents/patch.lua
Normal file
12
lua/codetyper/params/agents/patch.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
---@mod codetyper.params.agents.patch Patch configuration
|
||||
local M = {}
|
||||
|
||||
M.config = {
|
||||
snapshot_range = 5, -- Lines above/below prompt to snapshot
|
||||
clean_interval_ms = 60000, -- Check for stale patches every minute
|
||||
max_age_ms = 3600000, -- 1 hour TTL
|
||||
staleness_check = true,
|
||||
use_search_replace_parser = true, -- Enable new parsing logic
|
||||
}
|
||||
|
||||
return M
|
||||
47
lua/codetyper/params/agents/permissions.lua
Normal file
47
lua/codetyper/params/agents/permissions.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
---@mod codetyper.params.agents.permissions Dangerous and safe command patterns
|
||||
local M = {}
|
||||
|
||||
--- Dangerous command patterns that should never be auto-allowed
|
||||
M.dangerous_patterns = {
|
||||
"^rm%s+%-rf",
|
||||
"^rm%s+%-r%s+/",
|
||||
"^rm%s+/",
|
||||
"^sudo%s+rm",
|
||||
"^chmod%s+777",
|
||||
"^chmod%s+%-R",
|
||||
"^chown%s+%-R",
|
||||
"^dd%s+",
|
||||
"^mkfs",
|
||||
"^fdisk",
|
||||
"^format",
|
||||
":.*>%s*/dev/",
|
||||
"^curl.*|.*sh",
|
||||
"^wget.*|.*sh",
|
||||
"^eval%s+",
|
||||
"`;.*`",
|
||||
"%$%(.*%)",
|
||||
"fork%s*bomb",
|
||||
}
|
||||
|
||||
--- Safe command patterns that can be auto-allowed
|
||||
M.safe_patterns = {
|
||||
"^ls%s",
|
||||
"^ls$",
|
||||
"^cat%s",
|
||||
"^head%s",
|
||||
"^tail%s",
|
||||
"^grep%s",
|
||||
"^find%s",
|
||||
"^pwd$",
|
||||
"^echo%s",
|
||||
"^wc%s",
|
||||
"^git%s+status",
|
||||
"^git%s+diff",
|
||||
"^git%s+log",
|
||||
"^git%s+show",
|
||||
"^git%s+branch",
|
||||
"^git%s+checkout",
|
||||
"^git%s+add", -- Generally safe if reviewing changes
|
||||
}
|
||||
|
||||
return M
|
||||
14
lua/codetyper/params/agents/scheduler.lua
Normal file
14
lua/codetyper/params/agents/scheduler.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
---@mod codetyper.params.agents.scheduler Scheduler configuration
|
||||
local M = {}
|
||||
|
||||
M.config = {
|
||||
enabled = true,
|
||||
ollama_scout = true,
|
||||
escalation_threshold = 0.7,
|
||||
max_concurrent = 2,
|
||||
completion_delay_ms = 100,
|
||||
apply_delay_ms = 5000, -- Wait before applying code
|
||||
remote_provider = "copilot", -- Default fallback provider
|
||||
}
|
||||
|
||||
return M
|
||||
72
lua/codetyper/params/agents/scope.lua
Normal file
72
lua/codetyper/params/agents/scope.lua
Normal file
@@ -0,0 +1,72 @@
|
||||
---@mod codetyper.params.agents.scope Tree-sitter scope mappings
|
||||
local M = {}
|
||||
|
||||
--- Node types that represent function-like scopes per language
|
||||
M.function_nodes = {
|
||||
-- Lua
|
||||
["function_declaration"] = "function",
|
||||
["function_definition"] = "function",
|
||||
["local_function"] = "function",
|
||||
["function"] = "function",
|
||||
|
||||
-- JavaScript/TypeScript
|
||||
["function_declaration"] = "function",
|
||||
["function_expression"] = "function",
|
||||
["arrow_function"] = "function",
|
||||
["method_definition"] = "method",
|
||||
["function"] = "function",
|
||||
|
||||
-- Python
|
||||
["function_definition"] = "function",
|
||||
["lambda"] = "function",
|
||||
|
||||
-- Go
|
||||
["function_declaration"] = "function",
|
||||
["method_declaration"] = "method",
|
||||
["func_literal"] = "function",
|
||||
|
||||
-- Rust
|
||||
["function_item"] = "function",
|
||||
["closure_expression"] = "function",
|
||||
|
||||
-- C/C++
|
||||
["function_definition"] = "function",
|
||||
["lambda_expression"] = "function",
|
||||
|
||||
-- Java
|
||||
["method_declaration"] = "method",
|
||||
["constructor_declaration"] = "method",
|
||||
["lambda_expression"] = "function",
|
||||
|
||||
-- Ruby
|
||||
["method"] = "method",
|
||||
["singleton_method"] = "method",
|
||||
["lambda"] = "function",
|
||||
["block"] = "function",
|
||||
|
||||
-- PHP
|
||||
["function_definition"] = "function",
|
||||
["method_declaration"] = "method",
|
||||
["arrow_function"] = "function",
|
||||
}
|
||||
|
||||
--- Node types that represent class-like scopes
|
||||
M.class_nodes = {
|
||||
["class_declaration"] = "class",
|
||||
["class_definition"] = "class",
|
||||
["struct_declaration"] = "class",
|
||||
["impl_item"] = "class", -- Rust config
|
||||
["interface_declaration"] = "class",
|
||||
["trait_item"] = "class",
|
||||
}
|
||||
|
||||
--- Node types that represent block scopes
|
||||
M.block_nodes = {
|
||||
["block"] = "block",
|
||||
["do_statement"] = "block", -- Lua
|
||||
["if_statement"] = "block",
|
||||
["for_statement"] = "block",
|
||||
["while_statement"] = "block",
|
||||
}
|
||||
|
||||
return M
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user