Skip to content

Commit 6847568

Browse files
feat(StartAgent): split task + prompt
Also use local overrides when set. Useful for repos where there's a gitignored local agents dir.
1 parent 2457296 commit 6847568

2 files changed

Lines changed: 223 additions & 38 deletions

File tree

packages/nvim/lua/hjdivad/agent.lua

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,89 @@ local agents = {
66
cursor = "cursor-agent --force --approve-mcps",
77
}
88

9+
local function session_name(cwd)
10+
local path_parts = {}
11+
for part in string.gmatch(cwd, "[^/]+") do
12+
table.insert(path_parts, part)
13+
end
14+
15+
if #path_parts >= 2 then
16+
return path_parts[#path_parts - 1] .. "/" .. path_parts[#path_parts]
17+
elseif #path_parts == 1 then
18+
return path_parts[1]
19+
else
20+
return "unknown"
21+
end
22+
end
23+
24+
local function env_path(env_name, fallback)
25+
local path = vim.env[env_name]
26+
if path == nil or path == "" then
27+
return fallback
28+
end
29+
30+
return path
31+
end
32+
33+
local function agent_paths(cwd)
34+
local name = session_name(cwd)
35+
local base = vim.fn.stdpath("cache") .. "/prompt/" .. name
36+
37+
return {
38+
prompt = env_path("AGENTS_PROMPT", base .. ".prompt.md"),
39+
vadnu = env_path("AGENTS_VADNU", base .. ".vadnu.md"),
40+
}
41+
end
42+
43+
local function ensure_parent_dir(path)
44+
vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p")
45+
end
46+
47+
local function ensure_vadnu_file(path)
48+
ensure_parent_dir(path)
49+
50+
if vim.fn.filereadable(path) == 1 then
51+
return
52+
end
53+
54+
vim.fn.writefile({ "## Stack", "## Tasks" }, path)
55+
end
56+
957
local function do_start_agent(agent_cmd)
10-
-- Get current working directory and extract session name
11-
local cwd = vim.fn.getcwd()
12-
local path_parts = {}
13-
for part in string.gmatch(cwd, "[^/]+") do
14-
table.insert(path_parts, part)
15-
end
16-
17-
-- Get last two directory components for session name
18-
local session_name
19-
if #path_parts >= 2 then
20-
session_name = path_parts[#path_parts - 1] .. "/" .. path_parts[#path_parts]
21-
elseif #path_parts == 1 then
22-
session_name = path_parts[1]
23-
else
24-
session_name = "unknown"
25-
end
26-
27-
-- Create prompt file path
28-
local prompt_path = vim.fn.stdpath('cache') .. "/prompt/" .. session_name .. ".prompt.md"
29-
30-
-- Ensure directory exists
31-
local prompt_dir = vim.fn.fnamemodify(prompt_path, ":h")
32-
vim.fn.mkdir(prompt_dir, "p")
33-
34-
-- Open new tab
35-
vim.cmd("tabnew")
36-
37-
-- Split vertically (creates left and right windows)
38-
vim.cmd("vsplit")
39-
40-
-- Move to right window and start terminal with claude
41-
vim.cmd("wincmd l") -- Move to right window
42-
vim.cmd("term " .. agent_cmd)
43-
44-
-- Move to left window and open prompt file
45-
vim.cmd("wincmd h") -- Move to left window
46-
vim.cmd("edit " .. prompt_path)
58+
local paths = agent_paths(vim.fn.getcwd())
59+
60+
ensure_vadnu_file(paths.vadnu)
61+
ensure_parent_dir(paths.prompt)
62+
63+
vim.cmd("tabnew")
64+
vim.cmd("rightbelow vsplit")
65+
vim.cmd("rightbelow vsplit")
66+
vim.cmd("term " .. agent_cmd)
67+
vim.cmd("wincmd h")
68+
vim.cmd("edit " .. vim.fn.fnameescape(paths.vadnu))
69+
vim.cmd("rightbelow split")
70+
vim.cmd("edit " .. vim.fn.fnameescape(paths.prompt))
71+
vim.cmd("wincmd h")
72+
vim.cmd("terminal")
73+
vim.cmd("rightbelow split")
74+
vim.cmd("terminal")
75+
vim.cmd("wincmd l")
76+
vim.defer_fn(function()
77+
vim.cmd("wincmd =")
78+
end, 100)
4779
end
4880

4981
function M.StartAgent(agent)
50-
local agent_name = agent or 'claude'
82+
local agent_name = agent or "claude"
5183
local agent_cmd = agents[agent_name]
5284
--FIXME: when we execute this eagerly, we run before the autocmds are set up and we don't get the prompt extensions.
5385
-- But when we execute in a VeryLazy callback, the filetype is never set, for some unknown reason.
5486
-- For now just execute eagerly, but we probably want to actually do this in a callback
5587
do_start_agent(agent_cmd)
5688
end
5789

90+
M._agent_paths = agent_paths
91+
M._ensure_vadnu_file = ensure_vadnu_file
92+
M._session_name = session_name
93+
5894
return M
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
describe("agent", function()
2+
local assert = require("luassert")
3+
local stub = require("luassert.stub")
4+
local agent = require("hjdivad.agent")
5+
6+
local original_agents_prompt
7+
local original_agents_vadnu
8+
local stdpath_stub
9+
local getcwd_stub
10+
local original_cmd
11+
local original_defer_fn
12+
13+
local function restore_env()
14+
vim.env.AGENTS_PROMPT = original_agents_prompt
15+
vim.env.AGENTS_VADNU = original_agents_vadnu
16+
end
17+
18+
before_each(function()
19+
original_agents_prompt = vim.env.AGENTS_PROMPT
20+
original_agents_vadnu = vim.env.AGENTS_VADNU
21+
vim.env.AGENTS_PROMPT = nil
22+
vim.env.AGENTS_VADNU = nil
23+
original_cmd = vim.cmd
24+
original_defer_fn = vim.defer_fn
25+
end)
26+
27+
after_each(function()
28+
restore_env()
29+
30+
if stdpath_stub then
31+
stdpath_stub:revert()
32+
stdpath_stub = nil
33+
end
34+
35+
if getcwd_stub then
36+
getcwd_stub:revert()
37+
getcwd_stub = nil
38+
end
39+
40+
vim.cmd = original_cmd
41+
vim.defer_fn = original_defer_fn
42+
end)
43+
44+
describe("_agent_paths", function()
45+
it("uses cache prompt paths as fallbacks", function()
46+
stdpath_stub = stub(vim.fn, "stdpath", function(name)
47+
assert.equals("cache", name)
48+
return "/cache"
49+
end)
50+
51+
assert.same({
52+
prompt = "/cache/prompt/github/repo.prompt.md",
53+
vadnu = "/cache/prompt/github/repo.vadnu.md",
54+
}, agent._agent_paths("/Users/me/github/repo"))
55+
end)
56+
57+
it("uses env var paths without expanding them", function()
58+
stdpath_stub = stub(vim.fn, "stdpath", function()
59+
return "/cache"
60+
end)
61+
vim.env.AGENTS_PROMPT = "relative/prompt.md"
62+
vim.env.AGENTS_VADNU = "~/relative/vadnu.md"
63+
64+
assert.same({
65+
prompt = "relative/prompt.md",
66+
vadnu = "~/relative/vadnu.md",
67+
}, agent._agent_paths("/Users/me/github/repo"))
68+
end)
69+
70+
it("falls back independently for unset env vars", function()
71+
stdpath_stub = stub(vim.fn, "stdpath", function()
72+
return "/cache"
73+
end)
74+
vim.env.AGENTS_PROMPT = "prompt.md"
75+
76+
assert.same({
77+
prompt = "prompt.md",
78+
vadnu = "/cache/prompt/github/repo.vadnu.md",
79+
}, agent._agent_paths("/Users/me/github/repo"))
80+
end)
81+
end)
82+
83+
describe("_ensure_vadnu_file", function()
84+
it("creates vadnu.md with initial sections when missing", function()
85+
local path = vim.fn.tempname() .. "/vadnu.md"
86+
87+
agent._ensure_vadnu_file(path)
88+
89+
assert.same({ "## Stack", "## Tasks" }, vim.fn.readfile(path))
90+
end)
91+
92+
it("leaves existing vadnu.md content alone", function()
93+
local path = vim.fn.tempname() .. "/vadnu.md"
94+
vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p")
95+
vim.fn.writefile({ "custom" }, path)
96+
97+
agent._ensure_vadnu_file(path)
98+
99+
assert.same({ "custom" }, vim.fn.readfile(path))
100+
end)
101+
end)
102+
103+
describe("StartAgent", function()
104+
it("opens the three-column agent layout with terminals on the left and vadnu above prompt in the middle", function()
105+
local root = vim.fn.tempname()
106+
vim.env.AGENTS_VADNU = root .. "/vadnu.md"
107+
vim.env.AGENTS_PROMPT = root .. "/prompt.md"
108+
getcwd_stub = stub(vim.fn, "getcwd", function()
109+
return "/Users/me/github/repo"
110+
end)
111+
112+
local commands = {}
113+
local deferred
114+
local defer_delay
115+
vim.cmd = function(cmd)
116+
table.insert(commands, cmd)
117+
end
118+
vim.defer_fn = function(callback, delay)
119+
deferred = callback
120+
defer_delay = delay
121+
end
122+
123+
agent.StartAgent("cursor")
124+
125+
assert.same({
126+
"tabnew",
127+
"rightbelow vsplit",
128+
"rightbelow vsplit",
129+
"term cursor-agent --force --approve-mcps",
130+
"wincmd h",
131+
"edit " .. vim.fn.fnameescape(vim.env.AGENTS_VADNU),
132+
"rightbelow split",
133+
"edit " .. vim.fn.fnameescape(vim.env.AGENTS_PROMPT),
134+
"wincmd h",
135+
"terminal",
136+
"rightbelow split",
137+
"terminal",
138+
"wincmd l",
139+
}, commands)
140+
assert.same({ "## Stack", "## Tasks" }, vim.fn.readfile(vim.env.AGENTS_VADNU))
141+
assert.equals(0, vim.fn.filereadable(vim.env.AGENTS_PROMPT))
142+
143+
assert.is_function(deferred)
144+
assert.equals(100, defer_delay)
145+
deferred()
146+
assert.equals("wincmd =", commands[#commands])
147+
end)
148+
end)
149+
end)

0 commit comments

Comments
 (0)