From 09b6f37903c10d0449c0bfa4f296965f5fa1510e Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Thu, 12 Feb 2026 13:14:04 -0500 Subject: [PATCH 1/4] feat(server): handle working dir change This should fix #246 --- lua/opencode/opencode_server.lua | 1 - lua/opencode/ui/autocmds.lua | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lua/opencode/opencode_server.lua b/lua/opencode/opencode_server.lua index e63a1b6d..5db72fcc 100644 --- a/lua/opencode/opencode_server.lua +++ b/lua/opencode/opencode_server.lua @@ -34,7 +34,6 @@ end --- Create a new ServerJob instance --- @return OpencodeServer function OpencodeServer.new() - local log = require('opencode.log') ensure_vim_leave_autocmd() return setmetatable({ diff --git a/lua/opencode/ui/autocmds.lua b/lua/opencode/ui/autocmds.lua index 7c780eef..37d7f5b8 100644 --- a/lua/opencode/ui/autocmds.lua +++ b/lua/opencode/ui/autocmds.lua @@ -1,3 +1,4 @@ +local Promise = require('opencode.promise') local input_window = require('opencode.ui.input_window') local output_window = require('opencode.ui.output_window') local M = {} @@ -48,6 +49,30 @@ function M.setup_autocmds(windows) end, }) + vim.api.nvim_create_autocmd('DirChanged', { + group = group, + callback = Promise.async(function(event) + local log = require('opencode.log') + local state = require('opencode.state') + local server_job = require('opencode.server_job') + local session = require('opencode.session') + local core = require('opencode.core') + + if state.opencode_server then + vim.notify('Directory changed, restarting Opencode server...', vim.log.levels.INFO) + log.info('Shutting down Opencode server due to directory change...') + state.opencode_server:shutdown():await() + server_job.ensure_server():await() + state.active_session = nil + vim.notify('Loading last session for new working dir', vim.log.levels.INFO) + state.active_session = session.get_last_workspace_session():await() + if not state.active_session then + state.active_session = core.create_new_session():await() + end + end + end), + }) + if require('opencode.config').ui.position == 'current' then vim.api.nvim_create_autocmd('BufEnter', { group = group, From 73b4241dc7dbf998b8a8cd3acdca2f74333e24d2 Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Thu, 12 Feb 2026 15:26:39 -0500 Subject: [PATCH 2/4] refactor: extract directory change handler to core module --- lua/opencode/core.lua | 27 ++++++++ lua/opencode/ui/autocmds.lua | 18 +---- tests/unit/core_spec.lua | 128 +++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 17 deletions(-) diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index aea4b478..1e34148b 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -543,6 +543,33 @@ function M.paste_image_from_clipboard() return image_handler.paste_image_from_clipboard() end +--- Handle working directory changes by restarting the server and loading the appropriate session. +--- This function performs the following steps: +--- 1. Shuts down the existing opencode server +--- 2. Starts a new server instance +--- 3. Clears the active session and context +--- 4. Loads the last workspace session for the new directory, or creates a new one if none exists +--- @return Promise +M.handle_directory_change = Promise.async(function() + local log = require('opencode.log') + + if state.opencode_server then + vim.notify('Directory changed, restarting Opencode server...', vim.log.levels.INFO) + log.info('Shutting down Opencode server due to directory change...') + state.opencode_server:shutdown():await() + server_job.ensure_server():await() + state.active_session = nil + vim.notify('Loading last session for new working dir', vim.log.levels.INFO) + state.last_sent_context = nil + context.unload_attachments() + + state.active_session = session.get_last_workspace_session():await() + if not state.active_session then + state.active_session = M.create_new_session():await() + end + end +end) + function M.setup() state.subscribe('opencode_server', on_opencode_server) state.subscribe('user_message_count', M._on_user_message_count_change) diff --git a/lua/opencode/ui/autocmds.lua b/lua/opencode/ui/autocmds.lua index 37d7f5b8..5cc14480 100644 --- a/lua/opencode/ui/autocmds.lua +++ b/lua/opencode/ui/autocmds.lua @@ -52,24 +52,8 @@ function M.setup_autocmds(windows) vim.api.nvim_create_autocmd('DirChanged', { group = group, callback = Promise.async(function(event) - local log = require('opencode.log') - local state = require('opencode.state') - local server_job = require('opencode.server_job') - local session = require('opencode.session') local core = require('opencode.core') - - if state.opencode_server then - vim.notify('Directory changed, restarting Opencode server...', vim.log.levels.INFO) - log.info('Shutting down Opencode server due to directory change...') - state.opencode_server:shutdown():await() - server_job.ensure_server():await() - state.active_session = nil - vim.notify('Loading last session for new working dir', vim.log.levels.INFO) - state.active_session = session.get_last_workspace_session():await() - if not state.active_session then - state.active_session = core.create_new_session():await() - end - end + core.handle_directory_change():await() end), }) diff --git a/tests/unit/core_spec.lua b/tests/unit/core_spec.lua index 94e5f843..9eeea5a5 100644 --- a/tests/unit/core_spec.lua +++ b/tests/unit/core_spec.lua @@ -469,6 +469,134 @@ describe('opencode.core', function() end) end) + describe('handle_directory_change', function() + local server_job + local context + + before_each(function() + server_job = require('opencode.server_job') + context = require('opencode.context') + stub(server_job, 'ensure_server').invokes(function() + local p = Promise.new() + p:resolve({ + is_running = function() + return true + end, + shutdown = function() + return Promise.new():resolve() + end, + url = 'http://127.0.0.1:4000', + }) + return p + end) + stub(context, 'unload_attachments') + end) + + after_each(function() + if server_job.ensure_server.revert then + server_job.ensure_server:revert() + end + if context.unload_attachments.revert then + context.unload_attachments:revert() + end + end) + + it('does nothing when no server is running', function() + state.opencode_server = nil + state.active_session = { id = 'sess1' } + + core.handle_directory_change():wait() + + assert.is_nil(state.opencode_server) + assert.equal('sess1', state.active_session.id) + assert.stub(server_job.ensure_server).was_not_called() + end) + + it('shuts down existing server and starts new one', function() + local shutdown_called = false + state.opencode_server = { + is_running = function() + return true + end, + shutdown = function() + shutdown_called = true + return Promise.new():resolve() + end, + url = 'http://127.0.0.1:4000', + } + + core.handle_directory_change():wait() + + assert.is_true(shutdown_called) + assert.stub(server_job.ensure_server).was_called() + end) + + it('clears active session and context', function() + state.opencode_server = { + is_running = function() + return true + end, + shutdown = function() + return Promise.new():resolve() + end, + url = 'http://127.0.0.1:4000', + } + state.active_session = { id = 'old-session' } + state.last_sent_context = { some = 'context' } + + core.handle_directory_change():wait() + + -- Should be set to the new session from get_last_workspace_session stub + assert.truthy(state.active_session) + assert.equal('test-session', state.active_session.id) + assert.is_nil(state.last_sent_context) + assert.stub(context.unload_attachments).was_called() + end) + + it('loads last workspace session for new directory', function() + state.opencode_server = { + is_running = function() + return true + end, + shutdown = function() + return Promise.new():resolve() + end, + url = 'http://127.0.0.1:4000', + } + + core.handle_directory_change():wait() + + assert.truthy(state.active_session) + assert.equal('test-session', state.active_session.id) + assert.stub(session.get_last_workspace_session).was_called() + end) + + it('creates new session when no last session exists', function() + state.opencode_server = { + is_running = function() + return true + end, + shutdown = function() + return Promise.new():resolve() + end, + url = 'http://127.0.0.1:4000', + } + + -- Override stub to return nil (no last session) + session.get_last_workspace_session:revert() + stub(session, 'get_last_workspace_session').invokes(function() + local p = Promise.new() + p:resolve(nil) + return p + end) + + core.handle_directory_change():wait() + + assert.truthy(state.active_session) + assert.truthy(state.active_session.id) + end) + end) + describe('switch_to_mode', function() it('sets current model from config file when mode has a model configured', function() local Promise = require('opencode.promise') From 9fdb565ceba2f61616cc8592464ff23533284aa0 Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Fri, 13 Feb 2026 08:12:18 -0500 Subject: [PATCH 3/4] fix(cwd): multiple cwd changes --- lua/opencode/core.lua | 41 ++++++++++++++++++++++++------------ lua/opencode/ui/autocmds.lua | 8 +++---- tests/unit/core_spec.lua | 9 ++++++++ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index 1e34148b..ef53361a 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -545,10 +545,6 @@ end --- Handle working directory changes by restarting the server and loading the appropriate session. --- This function performs the following steps: ---- 1. Shuts down the existing opencode server ---- 2. Starts a new server instance ---- 3. Clears the active session and context ---- 4. Loads the last workspace session for the new directory, or creates a new one if none exists --- @return Promise M.handle_directory_change = Promise.async(function() local log = require('opencode.log') @@ -556,17 +552,34 @@ M.handle_directory_change = Promise.async(function() if state.opencode_server then vim.notify('Directory changed, restarting Opencode server...', vim.log.levels.INFO) log.info('Shutting down Opencode server due to directory change...') + state.opencode_server:shutdown():await() - server_job.ensure_server():await() - state.active_session = nil - vim.notify('Loading last session for new working dir', vim.log.levels.INFO) - state.last_sent_context = nil - context.unload_attachments() - - state.active_session = session.get_last_workspace_session():await() - if not state.active_session then - state.active_session = M.create_new_session():await() - end + + vim.defer_fn( + Promise.async(function() + state.opencode_server = nil + server_job.ensure_server():await() + + vim.notify('Loading last session for new working dir [' .. vim.fn.getcwd() .. ']', vim.log.levels.INFO) + + state.active_session = nil + state.last_sent_context = nil + context.unload_attachments() + + local is_new = false + state.active_session = session.get_last_workspace_session():await() + + if not state.active_session then + is_new = true + state.active_session = M.create_new_session():await() + end + + log.debug( + 'Loaded session for new working dir' .. vim.inspect({ session = state.active_session, is_new = is_new }) + ) + end), + 200 + ) end end) diff --git a/lua/opencode/ui/autocmds.lua b/lua/opencode/ui/autocmds.lua index 5cc14480..9db3996a 100644 --- a/lua/opencode/ui/autocmds.lua +++ b/lua/opencode/ui/autocmds.lua @@ -49,12 +49,12 @@ function M.setup_autocmds(windows) end, }) - vim.api.nvim_create_autocmd('DirChanged', { + vim.api.nvim_create_autocmd('DirChangedPre', { group = group, - callback = Promise.async(function(event) + callback = function(event) local core = require('opencode.core') - core.handle_directory_change():await() - end), + core.handle_directory_change() + end, }) if require('opencode.config').ui.position == 'current' then diff --git a/tests/unit/core_spec.lua b/tests/unit/core_spec.lua index 9eeea5a5..f15f9bd2 100644 --- a/tests/unit/core_spec.lua +++ b/tests/unit/core_spec.lua @@ -472,10 +472,18 @@ describe('opencode.core', function() describe('handle_directory_change', function() local server_job local context + local original_defer_fn before_each(function() server_job = require('opencode.server_job') context = require('opencode.context') + original_defer_fn = vim.defer_fn + + -- Mock vim.defer_fn to execute immediately in tests + vim.defer_fn = function(fn, delay) + fn() + end + stub(server_job, 'ensure_server').invokes(function() local p = Promise.new() p:resolve({ @@ -493,6 +501,7 @@ describe('opencode.core', function() end) after_each(function() + vim.defer_fn = original_defer_fn if server_job.ensure_server.revert then server_job.ensure_server:revert() end From 0f3b34c3ae2d2b4c941fc812d987a81c22a6de46 Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Tue, 17 Feb 2026 08:26:01 -0500 Subject: [PATCH 4/4] fix: auto-set cwd in API queries - Remove server restart logic on directory change - Set default directory to current working directory in API client queries - Change DirChangedPre to DirChanged autocmd for proper timing --- lua/opencode/api_client.lua | 4 ++ lua/opencode/core.lua | 20 +++------ lua/opencode/ui/autocmds.lua | 2 +- tests/unit/api_client_spec.lua | 7 ++- tests/unit/core_spec.lua | 78 ---------------------------------- 5 files changed, 18 insertions(+), 93 deletions(-) diff --git a/lua/opencode/api_client.lua b/lua/opencode/api_client.lua index f0362cc8..9da85172 100644 --- a/lua/opencode/api_client.lua +++ b/lua/opencode/api_client.lua @@ -62,6 +62,10 @@ function OpencodeApiClient:_call(endpoint, method, body, query) local url = self.base_url .. endpoint if query then + if not query.directory then + query.directory = vim.fn.getcwd() + end + local params = {} for k, v in pairs(query) do diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index ef53361a..e63799e1 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -548,18 +548,17 @@ end --- @return Promise M.handle_directory_change = Promise.async(function() local log = require('opencode.log') + if not state.active_session then + is_new = true + state.active_session = M.create_new_session():await() + end if state.opencode_server then - vim.notify('Directory changed, restarting Opencode server...', vim.log.levels.INFO) - log.info('Shutting down Opencode server due to directory change...') - - state.opencode_server:shutdown():await() + vim.notify('Working directory changed.', vim.log.levels.INFO) + log.debug('Working directory change %s', vim.inspect({ cwd = vim.fn.getcwd() })) vim.defer_fn( Promise.async(function() - state.opencode_server = nil - server_job.ensure_server():await() - vim.notify('Loading last session for new working dir [' .. vim.fn.getcwd() .. ']', vim.log.levels.INFO) state.active_session = nil @@ -567,12 +566,7 @@ M.handle_directory_change = Promise.async(function() context.unload_attachments() local is_new = false - state.active_session = session.get_last_workspace_session():await() - - if not state.active_session then - is_new = true - state.active_session = M.create_new_session():await() - end + state.active_session = session.get_last_workspace_session():await() or M.create_new_session():await() log.debug( 'Loaded session for new working dir' .. vim.inspect({ session = state.active_session, is_new = is_new }) diff --git a/lua/opencode/ui/autocmds.lua b/lua/opencode/ui/autocmds.lua index 9db3996a..e90d5408 100644 --- a/lua/opencode/ui/autocmds.lua +++ b/lua/opencode/ui/autocmds.lua @@ -49,7 +49,7 @@ function M.setup_autocmds(windows) end, }) - vim.api.nvim_create_autocmd('DirChangedPre', { + vim.api.nvim_create_autocmd('DirChanged', { group = group, callback = function(event) local core = require('opencode.core') diff --git a/tests/unit/api_client_spec.lua b/tests/unit/api_client_spec.lua index ce7445d1..ff214c98 100644 --- a/tests/unit/api_client_spec.lua +++ b/tests/unit/api_client_spec.lua @@ -62,6 +62,10 @@ describe('api_client', function() local server_job = require('opencode.server_job') local original_call_api = server_job.call_api local captured_calls = {} + local original_cwd = vim.fn.getcwd + vim.fn.getcwd = function() + return '/current/directory' + end server_job.call_api = function(url, method, body) table.insert(captured_calls, { url = url, method = method, body = body }) @@ -74,7 +78,7 @@ describe('api_client', function() -- Test without query params client:list_projects() - assert.are.equal('http://localhost:8080/project', captured_calls[1].url) + assert.are.equal('http://localhost:8080/project?directory=/current/directory', captured_calls[1].url) assert.are.equal('GET', captured_calls[1].method) -- Test with query params @@ -95,5 +99,6 @@ describe('api_client', function() -- Restore original function server_job.call_api = original_call_api + vim.fn.getcwd = original_cwd end) end) diff --git a/tests/unit/core_spec.lua b/tests/unit/core_spec.lua index f15f9bd2..b8a2efb4 100644 --- a/tests/unit/core_spec.lua +++ b/tests/unit/core_spec.lua @@ -484,72 +484,14 @@ describe('opencode.core', function() fn() end - stub(server_job, 'ensure_server').invokes(function() - local p = Promise.new() - p:resolve({ - is_running = function() - return true - end, - shutdown = function() - return Promise.new():resolve() - end, - url = 'http://127.0.0.1:4000', - }) - return p - end) stub(context, 'unload_attachments') end) after_each(function() vim.defer_fn = original_defer_fn - if server_job.ensure_server.revert then - server_job.ensure_server:revert() - end - if context.unload_attachments.revert then - context.unload_attachments:revert() - end - end) - - it('does nothing when no server is running', function() - state.opencode_server = nil - state.active_session = { id = 'sess1' } - - core.handle_directory_change():wait() - - assert.is_nil(state.opencode_server) - assert.equal('sess1', state.active_session.id) - assert.stub(server_job.ensure_server).was_not_called() - end) - - it('shuts down existing server and starts new one', function() - local shutdown_called = false - state.opencode_server = { - is_running = function() - return true - end, - shutdown = function() - shutdown_called = true - return Promise.new():resolve() - end, - url = 'http://127.0.0.1:4000', - } - - core.handle_directory_change():wait() - - assert.is_true(shutdown_called) - assert.stub(server_job.ensure_server).was_called() end) it('clears active session and context', function() - state.opencode_server = { - is_running = function() - return true - end, - shutdown = function() - return Promise.new():resolve() - end, - url = 'http://127.0.0.1:4000', - } state.active_session = { id = 'old-session' } state.last_sent_context = { some = 'context' } @@ -563,16 +505,6 @@ describe('opencode.core', function() end) it('loads last workspace session for new directory', function() - state.opencode_server = { - is_running = function() - return true - end, - shutdown = function() - return Promise.new():resolve() - end, - url = 'http://127.0.0.1:4000', - } - core.handle_directory_change():wait() assert.truthy(state.active_session) @@ -581,16 +513,6 @@ describe('opencode.core', function() end) it('creates new session when no last session exists', function() - state.opencode_server = { - is_running = function() - return true - end, - shutdown = function() - return Promise.new():resolve() - end, - url = 'http://127.0.0.1:4000', - } - -- Override stub to return nil (no last session) session.get_last_workspace_session:revert() stub(session, 'get_last_workspace_session').invokes(function()