agents
This commit is contained in:
parent
50c50b94cc
commit
6f18060a51
|
@ -15,3 +15,4 @@ lazy-lock.json
|
||||||
|
|
||||||
# Claude Code hooks
|
# Claude Code hooks
|
||||||
.claude/
|
.claude/
|
||||||
|
.agent-work/
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -44,7 +44,7 @@ end
|
||||||
|
|
||||||
-- Create a new worktree
|
-- Create a new worktree
|
||||||
function M.create_worktree(path, branch)
|
function M.create_worktree(path, branch)
|
||||||
branch = branch or 'main'
|
branch = branch or M.default_branch()
|
||||||
|
|
||||||
-- Check if worktree already exists
|
-- Check if worktree already exists
|
||||||
local worktrees = M.list_worktrees()
|
local worktrees = M.list_worktrees()
|
||||||
|
@ -54,22 +54,44 @@ function M.create_worktree(path, branch)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create worktree
|
-- Generate unique branch name for the worktree
|
||||||
local cmd = string.format('git worktree add "%s" "%s" 2>&1', path, branch)
|
local worktree_branch = 'agent-' .. utils.timestamp()
|
||||||
|
|
||||||
|
-- Create worktree with new branch based on specified branch
|
||||||
|
local cmd = string.format('git worktree add -b "%s" "%s" "%s" 2>&1', worktree_branch, path, branch)
|
||||||
local result, err = utils.exec(cmd)
|
local result, err = utils.exec(cmd)
|
||||||
|
|
||||||
if err then
|
if err then
|
||||||
return false, result
|
return false, result
|
||||||
end
|
end
|
||||||
|
|
||||||
return true, { path = path, branch = branch }
|
-- For background agents, ensure no hooks by creating empty .claude directory
|
||||||
|
-- This prevents inline diffs from triggering
|
||||||
|
local claude_dir = path .. '/.claude'
|
||||||
|
if vim.fn.isdirectory(claude_dir) == 0 then
|
||||||
|
vim.fn.mkdir(claude_dir, 'p')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create empty settings.json to disable hooks
|
||||||
|
local empty_settings = '{"hooks": {}}'
|
||||||
|
utils.write_file(claude_dir .. '/settings.json', empty_settings)
|
||||||
|
|
||||||
|
return true, { path = path, branch = worktree_branch, base_branch = branch }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Remove a worktree
|
-- Remove a worktree
|
||||||
function M.remove_worktree(path)
|
function M.remove_worktree(path)
|
||||||
|
-- First try to remove as git worktree
|
||||||
local cmd = string.format('git worktree remove "%s" --force 2>&1', path)
|
local cmd = string.format('git worktree remove "%s" --force 2>&1', path)
|
||||||
local _, err = utils.exec(cmd)
|
local result, err = utils.exec(cmd)
|
||||||
return err == nil
|
|
||||||
|
-- If it's not a worktree or already removed, just delete the directory
|
||||||
|
if err or result:match('not a working tree') then
|
||||||
|
local rm_cmd = string.format('rm -rf "%s"', path)
|
||||||
|
utils.exec(rm_cmd)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Add entry to .gitignore
|
-- Add entry to .gitignore
|
||||||
|
@ -103,6 +125,32 @@ function M.current_branch()
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Get default branch (usually main or master)
|
||||||
|
function M.default_branch()
|
||||||
|
-- Try to get the default branch from remote
|
||||||
|
local result = utils.exec('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null')
|
||||||
|
if result and result ~= '' then
|
||||||
|
local branch = result:match('refs/remotes/origin/(.+)')
|
||||||
|
if branch then
|
||||||
|
return branch:gsub('\n', '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fallback: check if main or master exists
|
||||||
|
local main_exists = utils.exec('git show-ref --verify --quiet refs/heads/main')
|
||||||
|
if main_exists and main_exists == '' then
|
||||||
|
return 'main'
|
||||||
|
end
|
||||||
|
|
||||||
|
local master_exists = utils.exec('git show-ref --verify --quiet refs/heads/master')
|
||||||
|
if master_exists and master_exists == '' then
|
||||||
|
return 'master'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Final fallback
|
||||||
|
return 'main'
|
||||||
|
end
|
||||||
|
|
||||||
-- Get git status
|
-- Get git status
|
||||||
function M.status(path)
|
function M.status(path)
|
||||||
local cmd = 'git status --porcelain'
|
local cmd = 'git status --porcelain'
|
||||||
|
|
|
@ -20,16 +20,22 @@ end
|
||||||
|
|
||||||
-- Load registry from disk
|
-- Load registry from disk
|
||||||
function M.load()
|
function M.load()
|
||||||
|
vim.notify('registry.load() called', vim.log.levels.DEBUG)
|
||||||
local content = utils.read_file(M.registry_path)
|
local content = utils.read_file(M.registry_path)
|
||||||
if content then
|
if content then
|
||||||
|
vim.notify(string.format('registry.load: Read %d bytes from %s', #content, M.registry_path), vim.log.levels.DEBUG)
|
||||||
local ok, data = pcall(vim.json.decode, content)
|
local ok, data = pcall(vim.json.decode, content)
|
||||||
if ok and type(data) == 'table' then
|
if ok and type(data) == 'table' then
|
||||||
|
local agent_count = vim.tbl_count(data)
|
||||||
|
vim.notify(string.format('registry.load: Decoded %d agents from JSON', agent_count), vim.log.levels.DEBUG)
|
||||||
M.agents = data
|
M.agents = data
|
||||||
M.validate_agents()
|
M.validate_agents()
|
||||||
else
|
else
|
||||||
|
vim.notify('registry.load: Failed to decode JSON, clearing agents', vim.log.levels.WARN)
|
||||||
M.agents = {}
|
M.agents = {}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
vim.notify('registry.load: No content read from file, clearing agents', vim.log.levels.WARN)
|
||||||
M.agents = {}
|
M.agents = {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -47,14 +53,27 @@ function M.validate_agents()
|
||||||
local valid_agents = {}
|
local valid_agents = {}
|
||||||
local now = os.time()
|
local now = os.time()
|
||||||
|
|
||||||
|
|
||||||
for id, agent in pairs(M.agents) do
|
for id, agent in pairs(M.agents) do
|
||||||
-- Check if agent directory still exists
|
-- Check if agent directory still exists
|
||||||
if utils.file_exists(agent.work_dir .. '/mission.log') then
|
local mission_log_path = agent.work_dir .. '/mission.log'
|
||||||
|
local mission_exists = utils.file_exists(mission_log_path)
|
||||||
|
|
||||||
|
|
||||||
|
if mission_exists then
|
||||||
-- Check if tmux window still exists
|
-- Check if tmux window still exists
|
||||||
local window_exists = M.check_window_exists(agent.window_id)
|
local window_exists = M.check_window_exists(agent.window_id)
|
||||||
|
|
||||||
if window_exists then
|
if window_exists then
|
||||||
agent.status = 'active'
|
agent.status = 'active'
|
||||||
|
|
||||||
|
-- Update progress from file for active agents
|
||||||
|
local progress_file = agent.work_dir .. '/progress.txt'
|
||||||
|
local progress_content = utils.read_file(progress_file)
|
||||||
|
if progress_content and progress_content ~= '' then
|
||||||
|
agent.progress = progress_content:gsub('\n$', '') -- Remove trailing newline
|
||||||
|
end
|
||||||
|
|
||||||
valid_agents[id] = agent
|
valid_agents[id] = agent
|
||||||
else
|
else
|
||||||
-- Window closed, mark as completed
|
-- Window closed, mark as completed
|
||||||
|
@ -79,7 +98,7 @@ function M.check_window_exists(window_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Register a new agent
|
-- Register a new agent
|
||||||
function M.register(task, work_dir, window_id, window_name)
|
function M.register(task, work_dir, window_id, window_name, fork_info)
|
||||||
local id = utils.timestamp() .. '-' .. math.random(1000, 9999)
|
local id = utils.timestamp() .. '-' .. math.random(1000, 9999)
|
||||||
local agent = {
|
local agent = {
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -90,6 +109,9 @@ function M.register(task, work_dir, window_id, window_name)
|
||||||
start_time = os.time(),
|
start_time = os.time(),
|
||||||
status = 'active',
|
status = 'active',
|
||||||
project_root = utils.get_project_root(),
|
project_root = utils.get_project_root(),
|
||||||
|
progress = 'Starting...', -- Add progress field
|
||||||
|
last_update = os.time(),
|
||||||
|
fork_info = fork_info, -- Store branch/stash info
|
||||||
}
|
}
|
||||||
|
|
||||||
M.agents[id] = agent
|
M.agents[id] = agent
|
||||||
|
@ -105,12 +127,19 @@ end
|
||||||
|
|
||||||
-- Get all agents for current project
|
-- Get all agents for current project
|
||||||
function M.get_project_agents()
|
function M.get_project_agents()
|
||||||
|
-- Ensure registry is loaded
|
||||||
|
if not M.agents or vim.tbl_isempty(M.agents) then
|
||||||
|
M.load()
|
||||||
|
end
|
||||||
|
|
||||||
local project_root = utils.get_project_root()
|
local project_root = utils.get_project_root()
|
||||||
local project_agents = {}
|
local project_agents = {}
|
||||||
|
|
||||||
for id, agent in pairs(M.agents) do
|
for id, agent in pairs(M.agents) do
|
||||||
if agent.project_root == project_root then
|
if agent.project_root == project_root then
|
||||||
project_agents[id] = agent
|
-- Include the registry ID with the agent
|
||||||
|
agent._registry_id = id
|
||||||
|
table.insert(project_agents, agent)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -135,6 +164,16 @@ function M.update_status(id, status)
|
||||||
if status == 'completed' or status == 'failed' then
|
if status == 'completed' or status == 'failed' then
|
||||||
M.agents[id].end_time = os.time()
|
M.agents[id].end_time = os.time()
|
||||||
end
|
end
|
||||||
|
M.agents[id].last_update = os.time()
|
||||||
|
M.save()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update agent progress
|
||||||
|
function M.update_progress(id, progress)
|
||||||
|
if M.agents[id] then
|
||||||
|
M.agents[id].progress = progress
|
||||||
|
M.agents[id].last_update = os.time()
|
||||||
M.save()
|
M.save()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -187,12 +226,22 @@ function M.format_agent(agent)
|
||||||
age_str = string.format('%dd', math.floor(age / 86400))
|
age_str = string.format('%dd', math.floor(age / 86400))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local progress_str = ''
|
||||||
|
if agent.progress and agent.status == 'active' then
|
||||||
|
progress_str = string.format(' | %s', agent.progress)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clean up task to single line
|
||||||
|
local task_line = agent.task:match('[^\n]*') or agent.task
|
||||||
|
local task_preview = task_line:sub(1, 50) .. (task_line:len() > 50 and '...' or '')
|
||||||
|
|
||||||
return string.format(
|
return string.format(
|
||||||
'[%s] %s (%s) - %s',
|
'[%s] %s (%s) - %s%s',
|
||||||
agent.status:upper(),
|
agent.status:upper(),
|
||||||
agent.task,
|
task_preview,
|
||||||
age_str,
|
age_str,
|
||||||
agent.window_name or 'unknown'
|
agent.window_name or 'unknown',
|
||||||
|
progress_str
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- Statusline components for nvim-claude
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- Get active agent count and summary
|
||||||
|
function M.get_agent_status()
|
||||||
|
local registry = require('nvim-claude.registry')
|
||||||
|
|
||||||
|
-- Validate agents to update their status
|
||||||
|
registry.validate_agents()
|
||||||
|
|
||||||
|
local agents = registry.get_project_agents()
|
||||||
|
local active_count = 0
|
||||||
|
local latest_progress = nil
|
||||||
|
local latest_task = nil
|
||||||
|
|
||||||
|
for _, agent in ipairs(agents) do
|
||||||
|
if agent.status == 'active' then
|
||||||
|
active_count = active_count + 1
|
||||||
|
-- Get the most recently updated active agent
|
||||||
|
if not latest_progress or (agent.last_update and agent.last_update > (latest_progress.last_update or 0)) then
|
||||||
|
latest_progress = agent.progress
|
||||||
|
latest_task = agent.task
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if active_count == 0 then
|
||||||
|
return ''
|
||||||
|
elseif active_count == 1 and latest_progress then
|
||||||
|
-- Show single agent progress
|
||||||
|
local task_short = latest_task
|
||||||
|
if #latest_task > 20 then
|
||||||
|
task_short = latest_task:sub(1, 17) .. '...'
|
||||||
|
end
|
||||||
|
return string.format('🤖 %s: %s', task_short, latest_progress)
|
||||||
|
else
|
||||||
|
-- Show count of multiple agents
|
||||||
|
return string.format('🤖 %d agents', active_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Lualine component
|
||||||
|
function M.lualine_component()
|
||||||
|
return {
|
||||||
|
M.get_agent_status,
|
||||||
|
cond = function()
|
||||||
|
-- Only show if there are active agents
|
||||||
|
local status = M.get_agent_status()
|
||||||
|
return status ~= ''
|
||||||
|
end,
|
||||||
|
on_click = function()
|
||||||
|
-- Open agent list on click
|
||||||
|
vim.cmd('ClaudeAgents')
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Simple string function for custom statuslines
|
||||||
|
function M.statusline()
|
||||||
|
local status = M.get_agent_status()
|
||||||
|
if status ~= '' then
|
||||||
|
return ' ' .. status .. ' '
|
||||||
|
end
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
|
@ -57,7 +57,7 @@ end
|
||||||
-- Check if file exists
|
-- Check if file exists
|
||||||
function M.file_exists(path)
|
function M.file_exists(path)
|
||||||
local stat = vim.loop.fs_stat(path)
|
local stat = vim.loop.fs_stat(path)
|
||||||
return stat and stat.type == 'file'
|
return stat ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Generate timestamp string
|
-- Generate timestamp string
|
||||||
|
|
38
tasks.md
38
tasks.md
|
@ -200,8 +200,8 @@ Building a Claude Code integration for Neovim that works seamlessly with a tmux-
|
||||||
#### 8.2 Quick Commands
|
#### 8.2 Quick Commands
|
||||||
- [x] `:ClaudeKill [agent]` - Terminate agent
|
- [x] `:ClaudeKill [agent]` - Terminate agent
|
||||||
- [x] `:ClaudeClean` - Clean up old agents
|
- [x] `:ClaudeClean` - Clean up old agents
|
||||||
- [ ] `:ClaudeSwitch [agent]` - Switch to agent tmux
|
- [x] `:ClaudeAgents` - Interactive agent manager (switch, diff, kill)
|
||||||
- [x] `:ClaudeAgents` - List all agents
|
- [x] `:ClaudeDiffAgent` - Review agent changes with diffview
|
||||||
- [x] `:ClaudeResetBaseline` - Reset inline diff baseline
|
- [x] `:ClaudeResetBaseline` - Reset inline diff baseline
|
||||||
- [x] Test: Each command functions correctly
|
- [x] Test: Each command functions correctly
|
||||||
|
|
||||||
|
@ -316,4 +316,36 @@ Building a Claude Code integration for Neovim that works seamlessly with a tmux-
|
||||||
- Improve documentation with examples
|
- Improve documentation with examples
|
||||||
- Create demo videos showcasing inline diff system
|
- Create demo videos showcasing inline diff system
|
||||||
- Add support for partial hunk acceptance
|
- Add support for partial hunk acceptance
|
||||||
- TEST EDIT: Testing single file edit after accept all
|
|
||||||
|
## Background Agent Features (v1.0 Complete)
|
||||||
|
|
||||||
|
### Key Improvements
|
||||||
|
1. **Hook Isolation**: Background agents always run without hooks (no inline diffs)
|
||||||
|
2. **ClaudeSwitch**: Switch to agent's worktree to chat/give follow-ups (hooks remain disabled)
|
||||||
|
3. **ClaudeDiffAgent**: Review agent changes using diffview.nvim
|
||||||
|
4. **Progress Tracking**: Agents can update progress.txt for real-time status updates
|
||||||
|
5. **Statusline Integration**: Shows active agent count and latest progress
|
||||||
|
6. **Enhanced Agent Creation UI**: Interactive popup for mission description with fork options
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
- `ClaudeBg` - Opens interactive UI for creating agents with:
|
||||||
|
- Multi-line mission description editor
|
||||||
|
- Fork options: current branch, main, stash, or any branch
|
||||||
|
- Shows what the agent will be based on
|
||||||
|
- `ClaudeBg <task>` - Quick creation (backwards compatible)
|
||||||
|
- `ClaudeSwitch [agent]` - Switch to agent's worktree to chat (no inline diffs)
|
||||||
|
- `ClaudeDiffAgent [agent]` - Review agent changes with diffview
|
||||||
|
- `ClaudeAgents` - List all agents with progress
|
||||||
|
- Agents update progress: `echo 'status' > progress.txt`
|
||||||
|
|
||||||
|
### Agent Creation Options
|
||||||
|
- **Fork from current branch**: Default, uses your current branch state
|
||||||
|
- **Fork from default branch**: Start fresh from your default branch (auto-detects main/master)
|
||||||
|
- **Stash current changes**: Creates stash of current work, then applies to agent
|
||||||
|
- **Fork from other branch**: Choose any branch to base agent on
|
||||||
|
|
||||||
|
### Smart Branch Detection
|
||||||
|
The plugin automatically detects your repository's default branch (main, master, etc.) instead of assuming "main", making it compatible with older repositories that use "master".
|
||||||
|
|
||||||
|
### Design Philosophy
|
||||||
|
Background agents are kept simple - they're always background agents with hooks disabled. This avoids complexity around state transitions and keeps the workflow predictable. Use regular `:ClaudeChat` in your main workspace for inline diff functionality.
|
||||||
|
|
Loading…
Reference in New Issue