agents
This commit is contained in:
parent
50c50b94cc
commit
6f18060a51
|
@ -15,3 +15,4 @@ lazy-lock.json
|
|||
|
||||
# Claude Code hooks
|
||||
.claude/
|
||||
.agent-work/
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -44,7 +44,7 @@ end
|
|||
|
||||
-- Create a new worktree
|
||||
function M.create_worktree(path, branch)
|
||||
branch = branch or 'main'
|
||||
branch = branch or M.default_branch()
|
||||
|
||||
-- Check if worktree already exists
|
||||
local worktrees = M.list_worktrees()
|
||||
|
@ -54,22 +54,44 @@ function M.create_worktree(path, branch)
|
|||
end
|
||||
end
|
||||
|
||||
-- Create worktree
|
||||
local cmd = string.format('git worktree add "%s" "%s" 2>&1', path, branch)
|
||||
-- Generate unique branch name for the worktree
|
||||
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)
|
||||
|
||||
if err then
|
||||
return false, result
|
||||
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
|
||||
|
||||
-- Remove a worktree
|
||||
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 _, err = utils.exec(cmd)
|
||||
return err == nil
|
||||
local result, err = utils.exec(cmd)
|
||||
|
||||
-- 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
|
||||
|
||||
-- Add entry to .gitignore
|
||||
|
@ -103,6 +125,32 @@ function M.current_branch()
|
|||
return nil
|
||||
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
|
||||
function M.status(path)
|
||||
local cmd = 'git status --porcelain'
|
||||
|
|
|
@ -20,16 +20,22 @@ end
|
|||
|
||||
-- Load registry from disk
|
||||
function M.load()
|
||||
vim.notify('registry.load() called', vim.log.levels.DEBUG)
|
||||
local content = utils.read_file(M.registry_path)
|
||||
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)
|
||||
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.validate_agents()
|
||||
else
|
||||
vim.notify('registry.load: Failed to decode JSON, clearing agents', vim.log.levels.WARN)
|
||||
M.agents = {}
|
||||
end
|
||||
else
|
||||
vim.notify('registry.load: No content read from file, clearing agents', vim.log.levels.WARN)
|
||||
M.agents = {}
|
||||
end
|
||||
end
|
||||
|
@ -47,14 +53,27 @@ function M.validate_agents()
|
|||
local valid_agents = {}
|
||||
local now = os.time()
|
||||
|
||||
|
||||
for id, agent in pairs(M.agents) do
|
||||
-- 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
|
||||
local window_exists = M.check_window_exists(agent.window_id)
|
||||
|
||||
if window_exists then
|
||||
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
|
||||
else
|
||||
-- Window closed, mark as completed
|
||||
|
@ -79,7 +98,7 @@ function M.check_window_exists(window_id)
|
|||
end
|
||||
|
||||
-- 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 agent = {
|
||||
id = id,
|
||||
|
@ -90,6 +109,9 @@ function M.register(task, work_dir, window_id, window_name)
|
|||
start_time = os.time(),
|
||||
status = 'active',
|
||||
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
|
||||
|
@ -105,12 +127,19 @@ end
|
|||
|
||||
-- Get all agents for current project
|
||||
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_agents = {}
|
||||
|
||||
for id, agent in pairs(M.agents) do
|
||||
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
|
||||
|
||||
|
@ -135,6 +164,16 @@ function M.update_status(id, status)
|
|||
if status == 'completed' or status == 'failed' then
|
||||
M.agents[id].end_time = os.time()
|
||||
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()
|
||||
end
|
||||
end
|
||||
|
@ -187,12 +226,22 @@ function M.format_agent(agent)
|
|||
age_str = string.format('%dd', math.floor(age / 86400))
|
||||
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(
|
||||
'[%s] %s (%s) - %s',
|
||||
'[%s] %s (%s) - %s%s',
|
||||
agent.status:upper(),
|
||||
agent.task,
|
||||
task_preview,
|
||||
age_str,
|
||||
agent.window_name or 'unknown'
|
||||
agent.window_name or 'unknown',
|
||||
progress_str
|
||||
)
|
||||
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
|
||||
function M.file_exists(path)
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
return stat and stat.type == 'file'
|
||||
return stat ~= nil
|
||||
end
|
||||
|
||||
-- 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
|
||||
- [x] `:ClaudeKill [agent]` - Terminate agent
|
||||
- [x] `:ClaudeClean` - Clean up old agents
|
||||
- [ ] `:ClaudeSwitch [agent]` - Switch to agent tmux
|
||||
- [x] `:ClaudeAgents` - List all agents
|
||||
- [x] `:ClaudeAgents` - Interactive agent manager (switch, diff, kill)
|
||||
- [x] `:ClaudeDiffAgent` - Review agent changes with diffview
|
||||
- [x] `:ClaudeResetBaseline` - Reset inline diff baseline
|
||||
- [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
|
||||
- Create demo videos showcasing inline diff system
|
||||
- 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