Skip to content

Commit fc8440d

Browse files
authored
fix(session): block messaging and hide input for child sessions (#381)
* fix: block sending messages to child sessions Child sessions (sessions with a parentID) are read-only views of subagent progress. The OpenCode TUI does not allow messaging them directly, but the plugin had no such guard. Add a parentID check in messaging.send_message that returns false early, matching the existing pattern for missing active sessions. Add a test that verifies child session sends are blocked and api_client.create_message is never called. * fix: hide input window when viewing child sessions Child sessions are read-only, so the input window should be hidden when switching to one, and restored when switching back to a regular session. This prevents the user from attempting to type in a session that cannot accept messages. - Hide input and focus output when switching to a child session - Show input and focus it when switching back to a non-child session - Skip redundant hide/show when window is already in the right state
1 parent 36d313d commit fc8440d

4 files changed

Lines changed: 97 additions & 1 deletion

File tree

lua/opencode/services/messaging.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ M.send_message = Promise.async(function(prompt, opts)
1818
return false
1919
end
2020

21+
if state.active_session.parentID then
22+
return false
23+
end
24+
2125
local mentioned_files = context.get_context().mentioned_files or {}
2226
local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files)
2327

lua/opencode/services/session_runtime.lua

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,17 @@ M.switch_session = Promise.async(function(session_id)
4949

5050
state.session.set_active(selected_session)
5151
if state.ui.is_visible() then
52-
ui.focus_input()
52+
if selected_session and selected_session.parentID then
53+
if not input_window.is_hidden() then
54+
input_window._hide()
55+
end
56+
ui.focus_output()
57+
else
58+
if input_window.is_hidden() then
59+
input_window._show()
60+
end
61+
ui.focus_input()
62+
end
5363
else
5464
M.open()
5565
end

tests/unit/services_messaging_spec.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ describe('opencode.services.messaging', function()
127127
config_file.get_opencode_agents:revert()
128128
end)
129129

130+
it('returns false when active session is a child session', function()
131+
state.ui.set_windows({ mock = 'windows' })
132+
state.session.set_active({ id = 'child1', parentID = 'parent1' })
133+
134+
local create_called = false
135+
local orig = state.api_client.create_message
136+
state.api_client.create_message = function(_, sid, params)
137+
create_called = true
138+
return Promise.new():resolve({ id = 'm1' })
139+
end
140+
141+
local sent = messaging.send_message('hello world'):wait()
142+
assert.is_false(sent)
143+
assert.is_false(create_called)
144+
state.api_client.create_message = orig
145+
end)
146+
130147
it('increments and decrements user_message_count correctly', function()
131148
state.ui.set_windows({ mock = 'windows' })
132149
state.session.set_active({ id = 'sess1' })

tests/unit/services_session_runtime_spec.lua

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,71 @@ describe('opencode.services.session_runtime', function()
333333
end)
334334
end)
335335

336+
describe('switch_session', function()
337+
local input_window = require('opencode.ui.input_window')
338+
339+
it('hides input window when switching to a child session', function()
340+
state.ui.set_windows({ mock = 'windows', input_buf = 1, output_buf = 2, input_win = 3, output_win = 4 })
341+
local orig_is_visible = state.ui.is_visible
342+
state.ui.is_visible = function() return true end
343+
stub(input_window, 'is_hidden').returns(false)
344+
stub(input_window, '_hide')
345+
346+
session.get_by_id:revert()
347+
stub(session, 'get_by_id').invokes(function(id)
348+
return Promise.new():resolve({ id = id, title = id, modified = os.time(), parentID = 'parent1' })
349+
end)
350+
351+
session_runtime.switch_session('child1'):wait()
352+
353+
assert.stub(input_window._hide).was_called()
354+
assert.stub(ui.focus_output).was_called()
355+
356+
input_window.is_hidden:revert()
357+
input_window._hide:revert()
358+
state.ui.is_visible = orig_is_visible
359+
end)
360+
361+
it('shows input window when switching to a non-child session', function()
362+
state.ui.set_windows({ mock = 'windows', input_buf = 1, output_buf = 2, input_win = 3, output_win = 4 })
363+
local orig_is_visible = state.ui.is_visible
364+
state.ui.is_visible = function() return true end
365+
stub(input_window, 'is_hidden').returns(true)
366+
stub(input_window, '_show')
367+
368+
session_runtime.switch_session('root1'):wait()
369+
370+
assert.stub(input_window._show).was_called()
371+
assert.stub(ui.focus_input).was_called()
372+
373+
input_window.is_hidden:revert()
374+
input_window._show:revert()
375+
state.ui.is_visible = orig_is_visible
376+
end)
377+
378+
it('does not hide input when already hidden on child session switch', function()
379+
state.ui.set_windows({ mock = 'windows', input_buf = 1, output_buf = 2, input_win = 3, output_win = 4 })
380+
local orig_is_visible = state.ui.is_visible
381+
state.ui.is_visible = function() return true end
382+
stub(input_window, 'is_hidden').returns(true)
383+
stub(input_window, '_hide')
384+
385+
session.get_by_id:revert()
386+
stub(session, 'get_by_id').invokes(function(id)
387+
return Promise.new():resolve({ id = id, title = id, modified = os.time(), parentID = 'parent1' })
388+
end)
389+
390+
session_runtime.switch_session('child1'):wait()
391+
392+
assert.stub(input_window._hide).was_not_called()
393+
assert.stub(ui.focus_output).was_called()
394+
395+
input_window.is_hidden:revert()
396+
input_window._hide:revert()
397+
state.ui.is_visible = orig_is_visible
398+
end)
399+
end)
400+
336401
describe('send_message', function()
337402
it('delegates message-sending coverage to services_messaging_spec', function()
338403
-- This spec focuses on session_runtime responsibilities.

0 commit comments

Comments
 (0)