Skip to content

Commit 9d10edb

Browse files
committed
fix(renderer): mark dirty on tab switch; picker: multi-session tab open
1 parent 39f8266 commit 9d10edb

4 files changed

Lines changed: 104 additions & 1 deletion

File tree

lua/opencode/ui/renderer.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,10 @@ function M.render_full_session()
574574
or not state.active_session
575575
or state.active_session.id ~= target_session_id
576576
then
577+
local runtime = session_tabs.get(target_tab_id)
578+
if runtime then
579+
runtime.renderer_dirty = true
580+
end
577581
return nil
578582
end
579583
M._render_full_session_data(session_data, {

lua/opencode/ui/session_picker.lua

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,20 @@ function M.pick(sessions, callback, opts)
342342
open_in_tab = {
343343
key = config.keymap.session_picker.open_in_tab,
344344
label = 'tab',
345+
multi_selection = true,
345346
fn = Promise.async(function(selected, opts)
347+
local session_runtime = require('opencode.services.session_runtime')
348+
local sessions = type(selected) == 'table' and selected.id == nil and selected or { selected }
349+
346350
if opts.close then
347351
opts.close()
352+
Promise.delay(0):await()
353+
end
354+
355+
for _, session in ipairs(sessions) do
356+
session_runtime.open_session_in_tab(session):await()
357+
Promise.delay(0):await()
348358
end
349-
return require('opencode.services.session_runtime').open_session_in_tab(selected):await()
350359
end),
351360
},
352361
fork = {

tests/unit/renderer_session_tabs_spec.lua

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local store = require('opencode.state.store')
33
local session_tabs = require('opencode.state.session_tabs')
44
local renderer = require('opencode.ui.renderer')
55
local renderer_ctx = require('opencode.ui.renderer.ctx')
6+
local session = require('opencode.session')
67
local Promise = require('opencode.promise')
78
local stub = require('luassert.stub')
89

@@ -152,4 +153,37 @@ describe('renderer session tab contexts', function()
152153
assert.is_false(second.renderer_dirty)
153154
render_stub:revert()
154155
end)
156+
157+
it('marks an in-flight render dirty when its tab becomes inactive', function()
158+
local first = session_tabs.ensure_current()
159+
first.active_session = { id = 'session-one', title = 'One' }
160+
local second = session_tabs.create({ id = 'session-two', title = 'Two' })
161+
162+
output_buf = vim.api.nvim_create_buf(false, true)
163+
output_win = vim.api.nvim_open_win(output_buf, false, {
164+
relative = 'editor',
165+
width = 60,
166+
height = 10,
167+
row = 1,
168+
col = 1,
169+
})
170+
state.ui.set_windows({ output_buf = output_buf, output_win = output_win })
171+
state.jobs.set_api_client({})
172+
store.set_raw('active_session', first.active_session)
173+
store.set_raw('active_session_tab', first.id)
174+
175+
local messages = Promise.new()
176+
local messages_stub = stub(session, 'get_messages').returns(messages)
177+
renderer.render_full_session()
178+
179+
store.set_raw('active_session', second.active_session)
180+
store.set_raw('active_session_tab', second.id)
181+
messages:resolve({})
182+
vim.wait(50, function()
183+
return first.renderer_dirty
184+
end)
185+
186+
assert.is_true(first.renderer_dirty)
187+
messages_stub:revert()
188+
end)
155189
end)

tests/unit/session_picker_spec.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ describe('opencode.ui.session_picker', function()
250250

251251
local open_stub = stub(session_runtime, 'open_session_in_tab').returns(Promise.new():resolve(selected_session))
252252
local closed = false
253+
assert.is_true(captured_action.multi_selection)
253254
captured_action
254255
.fn(selected_session, {
255256
close = function()
@@ -265,6 +266,61 @@ describe('opencode.ui.session_picker', function()
265266
base_picker.pick = original_pick
266267
end)
267268

269+
it('opens multiple selected sessions in panel tabs', function()
270+
local base_picker = require('opencode.ui.base_picker')
271+
local original_pick = base_picker.pick
272+
local sessions = {
273+
{ id = 'session-1', title = 'First session' },
274+
{ id = 'session-2', title = 'Second session' },
275+
}
276+
local captured_action
277+
278+
base_picker.pick = function(opts)
279+
captured_action = opts.actions.open_in_tab
280+
return true
281+
end
282+
283+
session_picker.pick(sessions, function() end)
284+
285+
local opened = {}
286+
local open_stub = stub(session_runtime, 'open_session_in_tab').invokes(function(session)
287+
opened[#opened + 1] = session
288+
return Promise.new():resolve(session)
289+
end)
290+
local closed = false
291+
local original_delay = Promise.delay
292+
local close_delay = Promise.new()
293+
local between_opens_delay = Promise.new()
294+
local delays = { close_delay, between_opens_delay, Promise.new():resolve(true) }
295+
Promise.delay = function()
296+
return table.remove(delays, 1)
297+
end
298+
299+
local action_promise = captured_action.fn(sessions, {
300+
close = function()
301+
closed = true
302+
end,
303+
})
304+
assert.is_true(closed)
305+
assert.same({}, opened)
306+
307+
close_delay:resolve(true)
308+
vim.wait(50, function()
309+
return #opened == 1
310+
end)
311+
assert.same({ sessions[1] }, opened)
312+
313+
between_opens_delay:resolve(true)
314+
action_promise:wait()
315+
Promise.delay = original_delay
316+
317+
assert.same(sessions, opened)
318+
assert.stub(open_stub).was_called(2)
319+
320+
open_stub:revert()
321+
base_picker.pick = original_pick
322+
end)
323+
268324
-- -----------------------------------------------------------------------
269325
-- Integration tests: delete action triggers switch when parent/grandparent
270326
-- of the active session is deleted

0 commit comments

Comments
 (0)