Skip to content

Commit 3fd1bd6

Browse files
authored
feat(server): handle working dir change (#268)
This should fix #246
1 parent 1b59388 commit 3fd1bd6

8 files changed

Lines changed: 158 additions & 19 deletions

File tree

lua/opencode/api_client.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local server_job = require('opencode.server_job')
2+
local state = require('opencode.state')
23

34
--- @class OpencodeApiClient
45
--- @field base_url string The base URL of the opencode server
@@ -62,6 +63,10 @@ function OpencodeApiClient:_call(endpoint, method, body, query)
6263
local url = self.base_url .. endpoint
6364

6465
if query then
66+
if not query.directory then
67+
query.directory = state.current_cwd or vim.fn.getcwd()
68+
end
69+
6570
local params = {}
6671

6772
for k, v in pairs(query) do

lua/opencode/core.lua

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local config = require('opencode.config')
1010
local image_handler = require('opencode.image_handler')
1111
local Promise = require('opencode.promise')
1212
local permission_window = require('opencode.ui.permission_window')
13+
local log = require('opencode.log')
1314

1415
local M = {}
1516
M._abort_count = 0
@@ -57,6 +58,27 @@ M.open_if_closed = Promise.async(function(opts)
5758
end
5859
end)
5960

61+
M.is_prompting_allowed = function()
62+
local mentioned_files = context.get_context().mentioned_files or {}
63+
local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files)
64+
if not allowed then
65+
vim.notify(err_msg or 'Prompt denied by prompt_guard', vim.log.levels.ERROR)
66+
end
67+
return allowed
68+
end
69+
70+
M.check_cwd = function()
71+
if state.current_cwd ~= vim.fn.getcwd() then
72+
log.debug(
73+
'CWD changed since last check, resetting session and context',
74+
{ current_cwd = state.current_cwd, new_cwd = vim.fn.getcwd() }
75+
)
76+
state.current_cwd = vim.fn.getcwd()
77+
state.active_session = nil
78+
context.unload_attachments()
79+
end
80+
end
81+
6082
---@param opts? OpenOpts
6183
M.open = Promise.async(function(opts)
6284
opts = opts or { focus = 'input', new_session = false }
@@ -69,13 +91,7 @@ M.open = Promise.async(function(opts)
6991

7092
local are_windows_closed = state.windows == nil
7193
if are_windows_closed then
72-
-- Check if whether prompting will be allowed
73-
local mentioned_files = context.get_context().mentioned_files or {}
74-
local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files)
75-
if not allowed then
76-
vim.notify(err_msg or 'Prompts will be denied by prompt_guard', vim.log.levels.WARN)
77-
end
78-
94+
M.is_prompting_allowed()
7995
state.windows = ui.create_windows()
8096
end
8197

@@ -85,22 +101,16 @@ M.open = Promise.async(function(opts)
85101
ui.focus_output({ restore_position = are_windows_closed })
86102
end
87103

88-
local server
89-
local server_ok, server_err = pcall(function()
90-
server = server_job.ensure_server():await()
91-
end)
104+
local server = server_job.ensure_server():await()
92105

93-
if not server_ok or not server then
106+
if not server then
94107
state.is_opening = false
95-
vim.notify('Failed to start opencode server: ' .. tostring(server_err or 'Unknown error'), vim.log.levels.ERROR)
96-
return Promise.new():reject(server_err or 'Server failed to start')
108+
return Promise.new():reject('Server failed to start')
97109
end
98110

99-
state.opencode_server = server
111+
M.check_cwd()
100112

101113
local ok, err = pcall(function()
102-
state.opencode_server = server
103-
104114
if opts.new_session then
105115
state.active_session = nil
106116
state.last_sent_context = nil
@@ -109,6 +119,7 @@ M.open = Promise.async(function(opts)
109119
M.ensure_current_mode():await()
110120

111121
state.active_session = M.create_new_session():await()
122+
log.debug('Created new session on open', { session = state.active_session.id })
112123
else
113124
M.ensure_current_mode():await()
114125
if not state.active_session then
@@ -543,6 +554,24 @@ function M.paste_image_from_clipboard()
543554
return image_handler.paste_image_from_clipboard()
544555
end
545556

557+
--- Handle working directory changes loading the appropriate session.
558+
--- @return Promise<void>
559+
M.handle_directory_change = Promise.async(function()
560+
local log = require('opencode.log')
561+
562+
local cwd = vim.fn.getcwd()
563+
log.debug('Working directory change %s', vim.inspect({ cwd = cwd }))
564+
vim.notify('Loading last session for new working dir [' .. cwd .. ']', vim.log.levels.INFO)
565+
566+
state.active_session = nil
567+
state.last_sent_context = nil
568+
context.unload_attachments()
569+
570+
state.active_session = session.get_last_workspace_session():await() or M.create_new_session():await()
571+
572+
log.debug('Loaded session for new working dir ' .. vim.inspect({ session = state.active_session }))
573+
end)
574+
546575
function M.setup()
547576
state.subscribe('opencode_server', on_opencode_server)
548577
state.subscribe('user_message_count', M._on_user_message_count_change)

lua/opencode/opencode_server.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ end
3434
--- Create a new ServerJob instance
3535
--- @return OpencodeServer
3636
function OpencodeServer.new()
37-
local log = require('opencode.log')
3837
ensure_vim_leave_autocmd()
3938

4039
return setmetatable({

lua/opencode/server_job.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local state = require('opencode.state')
22
local curl = require('opencode.curl')
33
local Promise = require('opencode.promise')
44
local opencode_server = require('opencode.opencode_server')
5+
local log = require('opencode.log')
56

67
local M = {}
78
M.requests = {}
@@ -146,6 +147,8 @@ function M.ensure_server()
146147
promise:resolve(state.opencode_server)
147148
end,
148149
on_error = function(err)
150+
log.error('Error starting opencode server: ' .. vim.inspect(err))
151+
vim.notify('Failed to start opencode server', vim.log.levels.ERROR)
149152
promise:reject(err)
150153
end,
151154
on_exit = function(exit_opts)

lua/opencode/state.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
---@field pre_zoom_width integer|nil
4444
---@field required_version string
4545
---@field opencode_cli_version string|nil
46+
---@field current_cwd string|nil
4647
---@field append fun( key:string, value:any)
4748
---@field remove fun( key:string, idx:number)
4849
---@field subscribe fun( key:string|nil, cb:fun(key:string, new_val:any, old_val:any))
@@ -97,6 +98,7 @@ local _state = {
9798
-- versions
9899
required_version = '0.6.3',
99100
opencode_cli_version = nil,
101+
current_cwd = vim.fn.getcwd(),
100102
}
101103

102104
-- Listener registry: { [key] = {cb1, cb2, ...}, ['*'] = {cb1, ...} }

lua/opencode/ui/autocmds.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ function M.setup_autocmds(windows)
4848
end,
4949
})
5050

51+
vim.api.nvim_create_autocmd('DirChanged', {
52+
group = group,
53+
callback = function(event)
54+
local state = require('opencode.state')
55+
state.current_cwd = event.file
56+
local core = require('opencode.core')
57+
core.handle_directory_change()
58+
end,
59+
})
60+
5161
if require('opencode.config').ui.position == 'current' then
5262
vim.api.nvim_create_autocmd('BufEnter', {
5363
group = group,

tests/unit/api_client_spec.lua

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ describe('api_client', function()
6262
local server_job = require('opencode.server_job')
6363
local original_call_api = server_job.call_api
6464
local captured_calls = {}
65+
local original_cwd = vim.fn.getcwd
66+
local state = require('opencode.state')
67+
state.current_cwd = '/current/directory'
68+
69+
vim.fn.getcwd = function()
70+
return '/current/directory'
71+
end
6572

6673
server_job.call_api = function(url, method, body)
6774
table.insert(captured_calls, { url = url, method = method, body = body })
@@ -74,7 +81,7 @@ describe('api_client', function()
7481

7582
-- Test without query params
7683
client:list_projects()
77-
assert.are.equal('http://localhost:8080/project', captured_calls[1].url)
84+
assert.are.equal('http://localhost:8080/project?directory=/current/directory', captured_calls[1].url)
7885
assert.are.equal('GET', captured_calls[1].method)
7986

8087
-- Test with query params
@@ -95,5 +102,6 @@ describe('api_client', function()
95102

96103
-- Restore original function
97104
server_job.call_api = original_call_api
105+
vim.fn.getcwd = original_cwd
98106
end)
99107
end)

tests/unit/core_spec.lua

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,37 @@ describe('opencode.core', function()
152152
}, state.windows)
153153
end)
154154

155+
it('ensure the current cwd is correct when opening', function()
156+
local cwd = vim.fn.getcwd()
157+
state.current_cwd = nil
158+
core.open({ new_session = false, focus = 'input' }):wait()
159+
assert.equal(cwd, state.current_cwd)
160+
end)
161+
162+
it('reload the active_session if cwd has changed since last session', function()
163+
local original_getcwd = vim.fn.getcwd
164+
165+
state.windows = nil
166+
state.active_session = { id = 'old-session' }
167+
state.current_cwd = '/some/old/path'
168+
vim.fn.getcwd = function()
169+
return '/some/new/path'
170+
end
171+
session.get_last_workspace_session:revert()
172+
stub(session, 'get_last_workspace_session').invokes(function()
173+
local p = Promise.new()
174+
p:resolve({ id = 'new_cwd-test-session' })
175+
return p
176+
end)
177+
178+
core.open({ new_session = false, focus = 'input' }):wait()
179+
180+
assert.truthy(state.active_session)
181+
assert.equal('new_cwd-test-session', state.active_session.id)
182+
-- Restore original cwd function
183+
vim.fn.getcwd = original_getcwd
184+
end)
185+
155186
it('handles new session properly', function()
156187
state.windows = nil
157188
state.active_session = { id = 'old-session' }
@@ -469,6 +500,58 @@ describe('opencode.core', function()
469500
end)
470501
end)
471502

503+
describe('handle_directory_change', function()
504+
local server_job
505+
local context
506+
507+
before_each(function()
508+
server_job = require('opencode.server_job')
509+
context = require('opencode.context')
510+
511+
stub(context, 'unload_attachments')
512+
end)
513+
514+
after_each(function()
515+
context.unload_attachments:revert()
516+
end)
517+
518+
it('clears active session and context', function()
519+
state.active_session = { id = 'old-session' }
520+
state.last_sent_context = { some = 'context' }
521+
522+
core.handle_directory_change():wait()
523+
524+
-- Should be set to the new session from get_last_workspace_session stub
525+
assert.truthy(state.active_session)
526+
assert.equal('test-session', state.active_session.id)
527+
assert.is_nil(state.last_sent_context)
528+
assert.stub(context.unload_attachments).was_called()
529+
end)
530+
531+
it('loads last workspace session for new directory', function()
532+
core.handle_directory_change():wait()
533+
534+
assert.truthy(state.active_session)
535+
assert.equal('test-session', state.active_session.id)
536+
assert.stub(session.get_last_workspace_session).was_called()
537+
end)
538+
539+
it('creates new session when no last session exists', function()
540+
-- Override stub to return nil (no last session)
541+
session.get_last_workspace_session:revert()
542+
stub(session, 'get_last_workspace_session').invokes(function()
543+
local p = Promise.new()
544+
p:resolve(nil)
545+
return p
546+
end)
547+
548+
core.handle_directory_change():wait()
549+
550+
assert.truthy(state.active_session)
551+
assert.truthy(state.active_session.id)
552+
end)
553+
end)
554+
472555
describe('switch_to_mode', function()
473556
it('sets current model from config file when mode has a model configured', function()
474557
local Promise = require('opencode.promise')

0 commit comments

Comments
 (0)