From edf5c95fc8b6ddf087b52e71ffe8caf4e9cf74de Mon Sep 17 00:00:00 2001 From: Cameron Ring Date: Wed, 5 Nov 2025 15:37:23 -0800 Subject: [PATCH 1/4] chore(renderer): always set current_permission to nil --- lua/opencode/ui/renderer.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 33bb64f1..2b1762cf 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -48,14 +48,14 @@ function M.reset() if state.current_permission and state.api_client then require('opencode.api').respond_to_permission('reject') - state.current_permission = nil end + state.current_permission = nil trigger_on_data_rendered() end ---Set up all subscriptions, for both local and server events function M.setup_subscriptions(_) - M._subscriptions.active_session = function(_, new, old) + M._subscriptions.active_session = function(_, new, _) M.reset() if new then M.render_full_session() From 8c0bdc9dc9917e5137dc3ef0c66db839caa5724b Mon Sep 17 00:00:00 2001 From: Cameron Ring Date: Wed, 5 Nov 2025 16:38:45 -0800 Subject: [PATCH 2/4] fix(server_job): promise chain The and_then/catch handlers didn't really make sense and we don't need them anyway. --- lua/opencode/server_job.lua | 58 +++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/lua/opencode/server_job.lua b/lua/opencode/server_job.lua index 3132a54d..35a0a086 100644 --- a/lua/opencode/server_job.lua +++ b/lua/opencode/server_job.lua @@ -27,22 +27,42 @@ end function M.call_api(url, method, body) local call_promise = Promise.new() state.job_count = state.job_count + 1 + + local request_entry = { nil, call_promise } + table.insert(M.requests, request_entry) + + -- Remove completed promises from list, update job_count + local function remove_from_requests() + for i, entry in ipairs(M.requests) do + if entry == request_entry then + table.remove(M.requests, i) + break + end + end + state.job_count = #M.requests + end + local opts = { url = url, method = method or 'GET', headers = { ['Content-Type'] = 'application/json' }, proxy = '', callback = function(response) + remove_from_requests() handle_api_response(response, function(err, result) if err then - local ok, pcall_err = pcall(call_promise.reject, call_promise, err) + local ok, pcall_err = pcall(function() + call_promise:reject(err) + end) if not ok then vim.schedule(function() vim.notify('Error while handling API error response: ' .. vim.inspect(pcall_err)) end) end else - local ok, pcall_err = pcall(call_promise.resolve, call_promise, result) + local ok, pcall_err = pcall(function() + call_promise:resolve(result) + end) if not ok then vim.schedule(function() vim.notify('Error while handling API response: ' .. vim.inspect(pcall_err)) @@ -52,7 +72,10 @@ function M.call_api(url, method, body) end) end, on_error = function(err) - local ok, pcall_err = pcall(call_promise.reject, call_promise, err) + remove_from_requests() + local ok, pcall_err = pcall(function() + call_promise:reject(err) + end) if not ok then vim.schedule(function() vim.notify('Error while handling API on_error: ' .. vim.inspect(pcall_err)) @@ -65,29 +88,8 @@ function M.call_api(url, method, body) opts.body = body and vim.json.encode(body) or '{}' end - local request_entry = { opts, call_promise } - table.insert(M.requests, request_entry) - - -- Remove completed promises from list, update job_count - local function remove_from_requests() - for i, entry in ipairs(M.requests) do - if entry == request_entry then - table.remove(M.requests, i) - break - end - end - state.job_count = #M.requests - end - - call_promise:and_then(function(result) - remove_from_requests() - return result - end) - - call_promise:catch(function(err) - remove_from_requests() - error(err) - end) + -- add opts to request_entry for request tracking + request_entry[1] = opts curl.request(opts) return call_promise @@ -98,7 +100,7 @@ end --- @param method string|nil HTTP method (default: 'GET') --- @param body table|nil|boolean Request body (will be JSON encoded) --- @param on_chunk fun(chunk: string) Callback invoked for each chunk of data received ---- @return Job job The underlying job instance +--- @return table The underlying job instance function M.stream_api(url, method, body, on_chunk) local opts = { url = url, @@ -125,7 +127,7 @@ function M.stream_api(url, method, body, on_chunk) opts.body = body and vim.json.encode(body) or '{}' end - return curl.request(opts) + return curl.request(opts) --[[@as table]] end function M.ensure_server() From 4a44347c8d8cabd2b22ef5702c62647925e23d1b Mon Sep 17 00:00:00 2001 From: Cameron Ring Date: Wed, 5 Nov 2025 17:17:17 -0800 Subject: [PATCH 3/4] chore: more emmylua_ls / diag clean up --- lua/opencode/api_client.lua | 2 +- lua/opencode/promise.lua | 7 +------ lua/opencode/provider.lua | 4 ++++ lua/opencode/ui/input_window.lua | 18 ++++++++++++++---- lua/opencode/ui/output.lua | 2 +- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lua/opencode/api_client.lua b/lua/opencode/api_client.lua index a09218a0..7901aeed 100644 --- a/lua/opencode/api_client.lua +++ b/lua/opencode/api_client.lua @@ -384,7 +384,7 @@ end --- Subscribe to events (streaming) --- @param directory string|nil Directory path --- @param on_event fun(event: table) Event callback ---- @return Job The streaming job handle +--- @return table The streaming job handle function OpencodeApiClient:subscribe_to_events(directory, on_event) self:_ensure_base_url() local url = self.base_url .. '/event' diff --git a/lua/opencode/promise.lua b/lua/opencode/promise.lua index ac6d84b3..aa6a44d8 100644 --- a/lua/opencode/promise.lua +++ b/lua/opencode/promise.lua @@ -30,7 +30,6 @@ function Promise.new() end ---@generic T ----@param self Promise ---@param value T ---@return Promise function Promise:resolve(value) @@ -50,8 +49,7 @@ function Promise:resolve(value) end ---@generic T ----@param self Promise ----@param error any +---@param err any ---@return self function Promise:reject(err) if self._resolved then @@ -70,7 +68,6 @@ function Promise:reject(err) end ---@generic T, U ----@param self Promise ---@param callback fun(value: T): U | Promise ---@return Promise function Promise:and_then(callback) @@ -116,7 +113,6 @@ function Promise:and_then(callback) end ---@generic T ----@param self Promise ---@param error_callback fun(err: any): any | Promise ---@return Promise function Promise:catch(error_callback) @@ -161,7 +157,6 @@ function Promise:catch(error_callback) end ---@generic T ----@param self Promise ---@param timeout integer|nil Timeout in milliseconds (default: 5000) ---@param interval integer|nil Interval in milliseconds to check (default: 20) ---@return T diff --git a/lua/opencode/provider.lua b/lua/opencode/provider.lua index 8cc95088..75e63e88 100644 --- a/lua/opencode/provider.lua +++ b/lua/opencode/provider.lua @@ -4,6 +4,10 @@ function M._get_models() local config_file = require('opencode.config_file') local response = config_file.get_opencode_providers() + if not response then + return {} + end + local models = {} for _, provider in ipairs(response.providers) do for _, model in pairs(provider.models) do diff --git a/lua/opencode/ui/input_window.lua b/lua/opencode/ui/input_window.lua index f605f4e9..4f06d009 100644 --- a/lua/opencode/ui/input_window.lua +++ b/lua/opencode/ui/input_window.lua @@ -16,7 +16,7 @@ function M._build_input_win_config() col = 2, style = 'minimal', zindex = 41, - } + } --[[@as vim.api.keyset.win_config]] end function M.create_window(windows) @@ -41,6 +41,7 @@ function M.close() if not M.mounted() then return end + ---@cast state.windows { input_win: integer, input_buf: integer } pcall(vim.api.nvim_win_close, state.windows.input_win, true) pcall(vim.api.nvim_buf_delete, state.windows.input_buf, { force = true }) @@ -51,6 +52,8 @@ function M.handle_submit() if not windows or not M.mounted(windows) then return end + ---@cast windows { input_buf: integer } + local input_content = table.concat(vim.api.nvim_buf_get_lines(windows.input_buf, 0, -1, false), '\n') vim.api.nvim_buf_set_lines(windows.input_buf, 0, -1, false, {}) vim.api.nvim_exec_autocmds('TextChanged', { @@ -177,6 +180,11 @@ function M.recover_input(windows) end function M.focus_input() + if not M.mounted() then + return + end + ---@cast state.windows { input_win: integer, input_buf: integer } + vim.api.nvim_set_current_win(state.windows.input_win) local lines = vim.api.nvim_buf_get_lines(state.windows.input_buf, 0, -1, false) @@ -192,6 +200,7 @@ function M.set_content(text, windows) if not M.mounted(windows) then return end + ---@cast windows { input_win: integer, input_buf: integer } local lines = type(text) == 'table' and text or vim.split(tostring(text), '\n') @@ -212,6 +221,7 @@ function M.remove_mention(mention_name, windows) if not M.mounted(windows) then return end + ---@cast windows { input_buf: integer } local lines = vim.api.nvim_buf_get_lines(windows.input_buf, 0, -1, false) for i, line in ipairs(lines) do @@ -228,12 +238,12 @@ function M.remove_mention(mention_name, windows) end function M.is_empty() - local windows = state.windows - if not windows or not M.mounted() then + if not M.mounted() then return true end + ---@cast state.windows { input_buf: integer } - local lines = vim.api.nvim_buf_get_lines(windows.input_buf, 0, -1, false) + local lines = vim.api.nvim_buf_get_lines(state.windows.input_buf, 0, -1, false) return #lines == 0 or (#lines == 1 and lines[1] == '') end diff --git a/lua/opencode/ui/output.lua b/lua/opencode/ui/output.lua index f7b47bda..bc84cab3 100644 --- a/lua/opencode/ui/output.lua +++ b/lua/opencode/ui/output.lua @@ -3,7 +3,7 @@ Output.__index = Output ---@class Output ---@field lines string[] ----@field extmarks table +---@field extmarks table ---@field actions OutputAction[] ---@field add_line fun(self: Output, line: string, fit?: boolean): number ---@field get_line fun(self: Output, idx: number): string? From 7e5b03caa924aa1871d15f6a656c0f7105e24f54 Mon Sep 17 00:00:00 2001 From: Cameron Ring Date: Wed, 5 Nov 2025 17:54:59 -0800 Subject: [PATCH 4/4] fix(core): create session if no current and no last If we're not starting a new session, we don't have a current session and we don't have a last session, we have to create a new session. Without a session, submitting prompts fails silently. --- lua/opencode/core.lua | 3 +++ tests/unit/core_spec.lua | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index 5d4ea805..d7b70ed1 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -76,6 +76,9 @@ function M.open(opts) else if not state.active_session then state.active_session = session.get_last_workspace_session() + if not state.active_session then + state.active_session = M.create_new_session() + end else if not state.display_route and are_windows_closed then -- We're not displaying /help or something like that but we have an active session diff --git a/tests/unit/core_spec.lua b/tests/unit/core_spec.lua index df2ccec7..3eef5c17 100644 --- a/tests/unit/core_spec.lua +++ b/tests/unit/core_spec.lua @@ -170,6 +170,18 @@ describe('opencode.core', function() assert.is_false(input_focused) assert.is_true(output_focused) end) + + it('creates a new session when no active session and no last session exists', function() + state.windows = nil + state.active_session = nil + session.get_last_workspace_session:revert() + stub(session, 'get_last_workspace_session').returns(nil) + + core.open({ new_session = false, focus = 'input' }) + + assert.truthy(state.active_session) + assert.truthy(state.active_session.id) + end) end) describe('select_session', function()