diff --git a/lua/opencode/api_client.lua b/lua/opencode/api_client.lua index f0362cc8..b2b0862e 100644 --- a/lua/opencode/api_client.lua +++ b/lua/opencode/api_client.lua @@ -1,4 +1,5 @@ local server_job = require('opencode.server_job') +local state = require('opencode.state') --- @class OpencodeApiClient --- @field base_url string The base URL of the opencode server @@ -62,6 +63,10 @@ function OpencodeApiClient:_call(endpoint, method, body, query) local url = self.base_url .. endpoint if query then + if not query.directory then + query.directory = state.current_cwd or 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 aea4b478..30eac5b5 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -10,6 +10,7 @@ local config = require('opencode.config') local image_handler = require('opencode.image_handler') local Promise = require('opencode.promise') local permission_window = require('opencode.ui.permission_window') +local log = require('opencode.log') local M = {} M._abort_count = 0 @@ -57,6 +58,27 @@ M.open_if_closed = Promise.async(function(opts) end end) +M.is_prompting_allowed = function() + local mentioned_files = context.get_context().mentioned_files or {} + local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files) + if not allowed then + vim.notify(err_msg or 'Prompt denied by prompt_guard', vim.log.levels.ERROR) + end + return allowed +end + +M.check_cwd = function() + if state.current_cwd ~= vim.fn.getcwd() then + log.debug( + 'CWD changed since last check, resetting session and context', + { current_cwd = state.current_cwd, new_cwd = vim.fn.getcwd() } + ) + state.current_cwd = vim.fn.getcwd() + state.active_session = nil + context.unload_attachments() + end +end + ---@param opts? OpenOpts M.open = Promise.async(function(opts) opts = opts or { focus = 'input', new_session = false } @@ -69,13 +91,7 @@ M.open = Promise.async(function(opts) local are_windows_closed = state.windows == nil if are_windows_closed then - -- Check if whether prompting will be allowed - local mentioned_files = context.get_context().mentioned_files or {} - local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files) - if not allowed then - vim.notify(err_msg or 'Prompts will be denied by prompt_guard', vim.log.levels.WARN) - end - + M.is_prompting_allowed() state.windows = ui.create_windows() end @@ -85,22 +101,16 @@ M.open = Promise.async(function(opts) ui.focus_output({ restore_position = are_windows_closed }) end - local server - local server_ok, server_err = pcall(function() - server = server_job.ensure_server():await() - end) + local server = server_job.ensure_server():await() - if not server_ok or not server then + if not server then state.is_opening = false - vim.notify('Failed to start opencode server: ' .. tostring(server_err or 'Unknown error'), vim.log.levels.ERROR) - return Promise.new():reject(server_err or 'Server failed to start') + return Promise.new():reject('Server failed to start') end - state.opencode_server = server + M.check_cwd() local ok, err = pcall(function() - state.opencode_server = server - if opts.new_session then state.active_session = nil state.last_sent_context = nil @@ -109,6 +119,7 @@ M.open = Promise.async(function(opts) M.ensure_current_mode():await() state.active_session = M.create_new_session():await() + log.debug('Created new session on open', { session = state.active_session.id }) else M.ensure_current_mode():await() if not state.active_session then @@ -543,6 +554,24 @@ function M.paste_image_from_clipboard() return image_handler.paste_image_from_clipboard() end +--- Handle working directory changes loading the appropriate session. +--- @return Promise +M.handle_directory_change = Promise.async(function() + local log = require('opencode.log') + + local cwd = vim.fn.getcwd() + log.debug('Working directory change %s', vim.inspect({ cwd = cwd })) + vim.notify('Loading last session for new working dir [' .. cwd .. ']', vim.log.levels.INFO) + + state.active_session = nil + state.last_sent_context = nil + context.unload_attachments() + + 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 })) +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/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/server_job.lua b/lua/opencode/server_job.lua index 789ab19b..c571c83e 100644 --- a/lua/opencode/server_job.lua +++ b/lua/opencode/server_job.lua @@ -2,6 +2,7 @@ local state = require('opencode.state') local curl = require('opencode.curl') local Promise = require('opencode.promise') local opencode_server = require('opencode.opencode_server') +local log = require('opencode.log') local M = {} M.requests = {} @@ -146,6 +147,8 @@ function M.ensure_server() promise:resolve(state.opencode_server) end, on_error = function(err) + log.error('Error starting opencode server: ' .. vim.inspect(err)) + vim.notify('Failed to start opencode server', vim.log.levels.ERROR) promise:reject(err) end, on_exit = function(exit_opts) diff --git a/lua/opencode/state.lua b/lua/opencode/state.lua index 0f43666f..c8644db2 100644 --- a/lua/opencode/state.lua +++ b/lua/opencode/state.lua @@ -43,6 +43,7 @@ ---@field pre_zoom_width integer|nil ---@field required_version string ---@field opencode_cli_version string|nil +---@field current_cwd string|nil ---@field append fun( key:string, value:any) ---@field remove fun( key:string, idx:number) ---@field subscribe fun( key:string|nil, cb:fun(key:string, new_val:any, old_val:any)) @@ -97,6 +98,7 @@ local _state = { -- versions required_version = '0.6.3', opencode_cli_version = nil, + current_cwd = vim.fn.getcwd(), } -- Listener registry: { [key] = {cb1, cb2, ...}, ['*'] = {cb1, ...} } diff --git a/lua/opencode/ui/autocmds.lua b/lua/opencode/ui/autocmds.lua index 7c780eef..9b3fef2d 100644 --- a/lua/opencode/ui/autocmds.lua +++ b/lua/opencode/ui/autocmds.lua @@ -48,6 +48,16 @@ function M.setup_autocmds(windows) end, }) + vim.api.nvim_create_autocmd('DirChanged', { + group = group, + callback = function(event) + local state = require('opencode.state') + state.current_cwd = event.file + local core = require('opencode.core') + core.handle_directory_change() + end, + }) + if require('opencode.config').ui.position == 'current' then vim.api.nvim_create_autocmd('BufEnter', { group = group, diff --git a/tests/unit/api_client_spec.lua b/tests/unit/api_client_spec.lua index ce7445d1..130d1bc7 100644 --- a/tests/unit/api_client_spec.lua +++ b/tests/unit/api_client_spec.lua @@ -62,6 +62,13 @@ 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 + local state = require('opencode.state') + state.current_cwd = '/current/directory' + + 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 +81,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 +102,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 94e5f843..623d48fb 100644 --- a/tests/unit/core_spec.lua +++ b/tests/unit/core_spec.lua @@ -152,6 +152,37 @@ describe('opencode.core', function() }, state.windows) end) + it('ensure the current cwd is correct when opening', function() + local cwd = vim.fn.getcwd() + state.current_cwd = nil + core.open({ new_session = false, focus = 'input' }):wait() + assert.equal(cwd, state.current_cwd) + end) + + it('reload the active_session if cwd has changed since last session', function() + local original_getcwd = vim.fn.getcwd + + state.windows = nil + state.active_session = { id = 'old-session' } + state.current_cwd = '/some/old/path' + vim.fn.getcwd = function() + return '/some/new/path' + end + session.get_last_workspace_session:revert() + stub(session, 'get_last_workspace_session').invokes(function() + local p = Promise.new() + p:resolve({ id = 'new_cwd-test-session' }) + return p + end) + + core.open({ new_session = false, focus = 'input' }):wait() + + assert.truthy(state.active_session) + assert.equal('new_cwd-test-session', state.active_session.id) + -- Restore original cwd function + vim.fn.getcwd = original_getcwd + end) + it('handles new session properly', function() state.windows = nil state.active_session = { id = 'old-session' } @@ -469,6 +500,58 @@ 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(context, 'unload_attachments') + end) + + after_each(function() + context.unload_attachments:revert() + end) + + it('clears active session and context', function() + 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() + 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() + -- 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')