From 1f0e5793fa1d8600dacd63d448039eedf330fc51 Mon Sep 17 00:00:00 2001 From: oujinsai Date: Wed, 18 Mar 2026 16:30:30 +0800 Subject: [PATCH 1/2] fix(ui): handle split resize safely Use window-type-aware resize logic in output_window.update_dimensions. - guard missing or invalid output_win before resizing - use nvim_win_set_width for split windows (relative == '') - keep nvim_win_set_config for floating windows - add regression tests for float-focus resize and invalid window This prevents "Cannot split a floating window" on VimResized while preserving existing zoom width behavior. Verified with: - ./run_tests.sh -t tests/unit/zoom_spec.lua --- lua/opencode/ui/output_window.lua | 16 +++++++- tests/unit/zoom_spec.lua | 62 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/lua/opencode/ui/output_window.lua b/lua/opencode/ui/output_window.lua index c4b98535..a9b8d0db 100644 --- a/lua/opencode/ui/output_window.lua +++ b/lua/opencode/ui/output_window.lua @@ -132,6 +132,11 @@ function M.update_dimensions(windows) if config.ui.position == 'current' then return end + + if not windows or not windows.output_win or not vim.api.nvim_win_is_valid(windows.output_win) then + return + end + local total_width = vim.api.nvim_get_option_value('columns', {}) local width_ratio @@ -145,8 +150,17 @@ function M.update_dimensions(windows) end local width = math.floor(total_width * width_ratio) + local ok, win_config = pcall(vim.api.nvim_win_get_config, windows.output_win) + if not ok then + return + end + + if win_config.relative == '' then + pcall(vim.api.nvim_win_set_width, windows.output_win, width) + return + end - vim.api.nvim_win_set_config(windows.output_win, { width = width }) + pcall(vim.api.nvim_win_set_config, windows.output_win, { width = width }) end function M.get_buf_line_count() diff --git a/tests/unit/zoom_spec.lua b/tests/unit/zoom_spec.lua index 2ec50c6e..a6ee11ed 100644 --- a/tests/unit/zoom_spec.lua +++ b/tests/unit/zoom_spec.lua @@ -228,6 +228,68 @@ describe('ui zoom state', function() assert.equals(80, state.pre_zoom_width) end) + + it('does not error when focused window is floating and output window is split', function() + local split_buf = vim.api.nvim_create_buf(false, true) + local focus_buf = vim.api.nvim_create_buf(false, true) + local split_win + local focus_win + + local normal_win + for _, win in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_config(win).relative == '' then + normal_win = win + break + end + end + + assert.is_not_nil(normal_win) + vim.api.nvim_set_current_win(normal_win) + vim.cmd('vsplit') + split_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(split_win, split_buf) + + focus_win = vim.api.nvim_open_win(focus_buf, true, { + relative = 'editor', + width = 20, + height = 5, + row = 1, + col = 1, + style = 'minimal', + }) + + assert.has_no.errors(function() + output_window.update_dimensions({ output_win = split_win, output_buf = split_buf }) + end) + + local expected_width = math.floor(config.ui.window_width * vim.o.columns) + assert.equals(expected_width, vim.api.nvim_win_get_width(split_win)) + + pcall(vim.api.nvim_win_close, focus_win, true) + pcall(vim.api.nvim_win_close, split_win, true) + pcall(vim.api.nvim_buf_delete, focus_buf, { force = true }) + pcall(vim.api.nvim_buf_delete, split_buf, { force = true }) + end) + + it('does not error when output window is invalid', function() + local invalid_buf = vim.api.nvim_create_buf(false, true) + local invalid_win = vim.api.nvim_open_win(invalid_buf, false, { + relative = 'editor', + width = 20, + height = 5, + row = 2, + col = 2, + style = 'minimal', + }) + + vim.api.nvim_win_close(invalid_win, true) + + assert.has_no.errors(function() + output_window.update_dimensions({ output_win = invalid_win, output_buf = invalid_buf }) + end) + + pcall(vim.api.nvim_buf_delete, invalid_buf, { force = true }) + end) end) describe('zoom state persistence', function() From 86b4d1d63337f9b378303503662e879640073c1d Mon Sep 17 00:00:00 2001 From: oujinsai Date: Wed, 18 Mar 2026 16:31:18 +0800 Subject: [PATCH 2/2] fix(reference-picker): use store subscribe Follow the state observable API migration by switching reference picker setup from state.subscribe(...) to state.store.subscribe(...). This matches the refactor that centralized observable helpers under state.store and fixes startup error: attempt to call field 'subscribe' (a nil value). Verified with: ./run_tests.sh -t tests/unit/reference_picker_spec.lua --- lua/opencode/ui/reference_picker.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/opencode/ui/reference_picker.lua b/lua/opencode/ui/reference_picker.lua index 632d664f..9c618fa3 100644 --- a/lua/opencode/ui/reference_picker.lua +++ b/lua/opencode/ui/reference_picker.lua @@ -239,7 +239,7 @@ function M.setup() end) end - state.subscribe('messages', function() + state.store.subscribe('messages', function() M._parse_session_messages() end) end