From 2ce045db7329bbfd8b7c93c84efd73fefb746027 Mon Sep 17 00:00:00 2001 From: David Herman Date: Sat, 23 May 2026 19:24:04 +0200 Subject: [PATCH] fix(session): prevent input window from reopening in child sessions Pressing or triggering focus_input in a child session could re-show the input window that was intentionally hidden on session switch. The root cause was that toggle_pane, ui.focus_input, and input_window._show had no awareness of child session state. Add child-session guards at three layers: - input_window._show(): safety net at the lowest level, prevents the window from ever being recreated during a child session - ui.focus_input(): early return so service-layer callers (agent_model, session_runtime) cannot accidentally reveal the input - ui.toggle_pane(): early return so stays on the output pane --- lua/opencode/ui/input_window.lua | 5 +++ lua/opencode/ui/ui.lua | 7 +++++ tests/unit/input_window_spec.lua | 26 +++++++++++++++ tests/unit/services_session_runtime_spec.lua | 33 ++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/lua/opencode/ui/input_window.lua b/lua/opencode/ui/input_window.lua index ae1aa1e0..dec47f93 100644 --- a/lua/opencode/ui/input_window.lua +++ b/lua/opencode/ui/input_window.lua @@ -591,6 +591,11 @@ end ---Show the input window by recreating it function M._show() + -- Child sessions must never show the input window + if state.active_session and state.active_session.parentID then + return + end + local windows = state.windows if not windows or not windows.input_buf or not windows.output_win then return diff --git a/lua/opencode/ui/ui.lua b/lua/opencode/ui/ui.lua index b80d0000..7fd0856c 100644 --- a/lua/opencode/ui/ui.lua +++ b/lua/opencode/ui/ui.lua @@ -403,6 +403,10 @@ end ---@param opts? { restore_position?: boolean, start_insert?: boolean } function M.focus_input(opts) + if state.active_session and state.active_session.parentID then + return + end + opts = opts or {} local windows = state.windows if not windows then @@ -557,6 +561,9 @@ function M.toggle_pane() if state.windows and current_win == state.windows.input_win then output_window.focus_output(true) else + if state.active_session and state.active_session.parentID then + return + end input_window.focus_input() end end diff --git a/tests/unit/input_window_spec.lua b/tests/unit/input_window_spec.lua index 67c5f7d2..63070e47 100644 --- a/tests/unit/input_window_spec.lua +++ b/tests/unit/input_window_spec.lua @@ -548,4 +548,30 @@ describe('input_window', function() assert.is_false(input_window._hidden) end) end) + + describe('child session input visibility', function() + after_each(function() + state.session.clear_active() + end) + + it('_show() is a no-op when active session is a child session', function() + state.session.set_active({ id = 'child1', parentID = 'parent1' }) + input_window._hidden = true + + input_window._show() + + assert.is_true(input_window._hidden) + end) + + it('_show() proceeds when active session is a root session', function() + state.session.set_active({ id = 'root1' }) + input_window._hidden = true + + -- _show will early-return due to missing windows, but it should pass the guard + input_window._show() + + -- _hidden remains true because windows are nil, but the parentID guard was passed + assert.is_true(input_window._hidden) + end) + end) end) diff --git a/tests/unit/services_session_runtime_spec.lua b/tests/unit/services_session_runtime_spec.lua index 99cdd1fc..258e1bc1 100644 --- a/tests/unit/services_session_runtime_spec.lua +++ b/tests/unit/services_session_runtime_spec.lua @@ -398,6 +398,39 @@ describe('opencode.services.session_runtime', function() end) end) + describe('child session UI guards', function() + local input_window = require('opencode.ui.input_window') + + after_each(function() + state.session.clear_active() + end) + + it('toggle_pane does not show input when in a child session', function() + state.session.set_active({ id = 'child1', parentID = 'parent1' }) + stub(input_window, 'focus_input') + + -- Simulate being in the output window (not input) + state.ui.set_windows({ input_win = -1, output_win = vim.api.nvim_get_current_win(), input_buf = 1, output_buf = 2 }) + + ui.toggle_pane() + + assert.stub(input_window.focus_input).was_not_called() + input_window.focus_input:revert() + end) + + it('focus_input is a no-op when in a child session', function() + state.session.set_active({ id = 'child1', parentID = 'parent1' }) + stub(input_window, 'is_hidden').returns(true) + stub(input_window, '_show') + + ui.focus_input() + + assert.stub(input_window._show).was_not_called() + input_window.is_hidden:revert() + input_window._show:revert() + end) + end) + describe('send_message', function() it('delegates message-sending coverage to services_messaging_spec', function() -- This spec focuses on session_runtime responsibilities.