Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lua/opencode/api_client.lua
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
63 changes: 46 additions & 17 deletions lua/opencode/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<void>
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)
Expand Down
1 change: 0 additions & 1 deletion lua/opencode/opencode_server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
3 changes: 3 additions & 0 deletions lua/opencode/server_job.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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, ...} }
Expand Down
10 changes: 10 additions & 0 deletions lua/opencode/ui/autocmds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion tests/unit/api_client_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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
Expand All @@ -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)
83 changes: 83 additions & 0 deletions tests/unit/core_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -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')
Expand Down