Skip to content

Commit a52636b

Browse files
committed
fix(questions/permissions): prevent scroll jump when navigating
1 parent 6ffda7d commit a52636b

4 files changed

Lines changed: 137 additions & 3 deletions

File tree

lua/opencode/ui/renderer/events.lua

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ function M.render_permissions_display()
7676
return
7777
end
7878

79+
local should_scroll = ctx.render_state:get_part('permission-display-part') == nil
80+
7981
local fake_message = {
8082
info = {
8183
id = 'permission-display-message',
@@ -93,6 +95,10 @@ function M.render_permissions_display()
9395
type = 'permissions-display',
9496
}
9597
M.on_part_updated({ part = fake_part })
98+
99+
if should_scroll then
100+
scroll(true)
101+
end
96102
end
97103

98104
---Render the current question as a synthetic part at the end of the buffer
@@ -111,6 +117,8 @@ function M.render_question_display()
111117
return
112118
end
113119

120+
local should_scroll = ctx.render_state:get_part('question-display-part') == nil
121+
114122
local fake_message = {
115123
info = {
116124
id = 'question-display-message',
@@ -128,7 +136,9 @@ function M.render_question_display()
128136
type = 'questions-display',
129137
}
130138
M.on_part_updated({ part = fake_part })
131-
scroll(true)
139+
if should_scroll then
140+
scroll(true)
141+
end
132142
end
133143

134144
---Remove the question display from the buffer
@@ -460,7 +470,6 @@ function M.on_permission_updated(permission)
460470

461471
permission_window.add_permission(permission)
462472
M.render_permissions_display()
463-
scroll(true)
464473
end
465474

466475
---Handle permission.replied — remove the resolved permission and update display

lua/opencode/ui/renderer/flush.lua

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ local append = require('opencode.ui.renderer.append')
1010
local M = {}
1111
local warned_part_render_error = false
1212

13+
local pinned_overlay_part_ids = {
14+
['permission-display-part'] = true,
15+
['question-display-part'] = true,
16+
}
17+
18+
local pinned_overlay_message_ids = {
19+
['permission-display-message'] = true,
20+
['question-display-message'] = true,
21+
}
22+
1323
---@param part_id string
1424
---@param message_id string|nil
1525
---@param err any
@@ -401,7 +411,39 @@ local function apply_pending(pending)
401411
return false
402412
end
403413

404-
local scroll_snapshot = scroll.pre_flush(buf)
414+
local only_pinned_overlay_updates = true
415+
for _, part_id in ipairs(pending.removed_part_order) do
416+
if pending.removed_parts[part_id] and not pinned_overlay_part_ids[part_id] then
417+
only_pinned_overlay_updates = false
418+
break
419+
end
420+
end
421+
if only_pinned_overlay_updates then
422+
for _, message_id in ipairs(pending.removed_message_order) do
423+
if pending.removed_messages[message_id] and not pinned_overlay_message_ids[message_id] then
424+
only_pinned_overlay_updates = false
425+
break
426+
end
427+
end
428+
end
429+
if only_pinned_overlay_updates then
430+
for _, message_id in ipairs(pending.dirty_message_order) do
431+
if pending.dirty_messages[message_id] and not pinned_overlay_message_ids[message_id] then
432+
only_pinned_overlay_updates = false
433+
break
434+
end
435+
end
436+
end
437+
if only_pinned_overlay_updates then
438+
for _, part_id in ipairs(pending.dirty_part_order) do
439+
if pending.dirty_parts[part_id] and not pinned_overlay_part_ids[part_id] then
440+
only_pinned_overlay_updates = false
441+
break
442+
end
443+
end
444+
end
445+
446+
local scroll_snapshot = only_pinned_overlay_updates and nil or scroll.pre_flush(buf)
405447
with_suppressed_output_autocmds(function()
406448
for _, part_id in ipairs(pending.removed_part_order) do
407449
if pending.removed_parts[part_id] then

tests/unit/permission_integration_spec.lua

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,38 @@ describe('permission prompt rendering', function()
505505
assert.are.equal('perm_no_meta', state.pending_permissions[1].id)
506506
assert.are.equal(1, permission_window.get_permission_count())
507507
end)
508+
509+
it('does not auto-scroll on permission navigation redraws', function()
510+
helpers.replay_setup()
511+
state.session.set_active({ id = 'session_123' })
512+
vim.api.nvim_set_current_win(state.windows.output_win)
513+
514+
local output_window_local = require('opencode.ui.output_window')
515+
516+
local lines = {}
517+
for i = 1, 40 do
518+
lines[i] = 'line ' .. i
519+
end
520+
output_window_local.set_lines(lines)
521+
vim.api.nvim_win_set_cursor(state.windows.output_win, { 5, 0 })
522+
output_window_local.sync_cursor_with_viewport(state.windows.output_win)
523+
524+
events.on_permission_updated({
525+
id = 'perm_nav',
526+
permission = 'bash',
527+
title = 'Run command',
528+
metadata = {},
529+
})
530+
531+
flush.flush()
532+
output_window_local.sync_cursor_with_viewport(state.windows.output_win)
533+
534+
local before = vim.api.nvim_win_get_cursor(state.windows.output_win)
535+
permission_window._dialog:navigate(1)
536+
flush.flush()
537+
538+
local after = vim.api.nvim_win_get_cursor(state.windows.output_win)
539+
assert.equals(before[1], after[1])
540+
assert.equals(before[2], after[2])
541+
end)
508542
end)

tests/unit/question_window_spec.lua

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local Output = require('opencode.ui.output')
33
local Promise = require('opencode.promise')
44
local state = require('opencode.state')
55
local stub = require('luassert.stub')
6+
local helpers = require('tests.helpers')
67

78
describe('question_window', function()
89
after_each(function()
@@ -151,4 +152,52 @@ describe('question_window', function()
151152

152153
show_stub:revert()
153154
end)
155+
156+
it('does not force-scroll on question navigation redraws', function()
157+
helpers.replay_setup()
158+
state.session.set_active({ id = 'sess1' })
159+
vim.api.nvim_set_current_win(state.windows.output_win)
160+
161+
local renderer = require('opencode.ui.renderer')
162+
local output_window = require('opencode.ui.output_window')
163+
164+
local lines = {}
165+
for i = 1, 40 do
166+
lines[i] = 'line ' .. i
167+
end
168+
output_window.set_lines(lines)
169+
vim.api.nvim_win_set_cursor(state.windows.output_win, { 5, 0 })
170+
output_window.sync_cursor_with_viewport(state.windows.output_win)
171+
172+
question_window.show_question({
173+
id = 'q-nav',
174+
sessionID = 'sess1',
175+
questions = {
176+
{
177+
question = 'Pick one',
178+
options = {
179+
{ label = 'One' },
180+
{ label = 'Two' },
181+
},
182+
},
183+
},
184+
})
185+
186+
local flush = require('opencode.ui.renderer.flush')
187+
flush.flush()
188+
output_window.sync_cursor_with_viewport(state.windows.output_win)
189+
190+
local before = vim.api.nvim_win_get_cursor(state.windows.output_win)
191+
question_window._dialog:navigate(1)
192+
flush.flush()
193+
194+
local after = vim.api.nvim_win_get_cursor(state.windows.output_win)
195+
assert.equals(before[1], after[1])
196+
assert.equals(before[2], after[2])
197+
198+
question_window.clear_question()
199+
if state.windows then
200+
require('opencode.ui.ui').close_windows(state.windows)
201+
end
202+
end)
154203
end)

0 commit comments

Comments
 (0)