Skip to content

Commit 92ad9bf

Browse files
authored
fix: restore pending permissions via REST API after session switch (#365)
* fix: restore pending permissions via REST API after session switch renderer.reset() clears the permission queue on every session change. The previous approach (preserve_permissions flag) tried to keep the queue alive across resets, but permissions that arrived for a different worktree while the user was away were never in the queue to begin with. Mirror the question_window.restore_pending_question pattern instead: after the session loads, query GET /permission for pending permissions, filter for ones belonging to the active session, and show them. This removes all preserve_permissions plumbing and the auto-deny behaviour from reset() (the cancel function handles explicit abort). * test: add list_permissions mock and permission restoration test Add list_permissions to the shared mock API client so tests exercising render_full_session() don't crash on the new restore_pending_permissions code path. Add a dedicated test verifying that pending permissions are fetched and re-queued via on_permission_updated after a session switch. * fix: skip resolved permissions during session restore
1 parent d515b84 commit 92ad9bf

6 files changed

Lines changed: 435 additions & 9 deletions

File tree

lua/opencode/api_client.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,13 @@ function OpencodeApiClient:unrevert_messages(id, directory)
304304
return self:_call('/session/' .. id .. '/unrevert', 'POST', nil, { directory = directory })
305305
end
306306

307+
--- List pending permissions
308+
--- @param directory string|nil Directory path
309+
--- @return Promise<OpencodePermission[]>
310+
function OpencodeApiClient:list_permissions(directory)
311+
return self:_call('/permission', 'GET', nil, { directory = directory })
312+
end
313+
307314
--- Respond to a permission request
308315
--- @param id string Session ID (required)
309316
--- @param permissionID string Permission ID (required)

lua/opencode/ui/permission_window.lua

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,73 @@ M._permission_queue = {}
88
M._dialog = nil
99
M._processing = false
1010

11+
---Get the tool identifiers from a permission (nested or root-level).
12+
---@param permission OpencodePermission|nil
13+
---@return string|nil call_id
14+
---@return string|nil message_id
15+
local function get_tool_ids(permission)
16+
if not permission then
17+
return nil, nil
18+
end
19+
local tool = permission.tool
20+
local call_id = (tool and tool.callID) or permission.callID
21+
local message_id = (tool and tool.messageID) or permission.messageID
22+
return call_id, message_id
23+
end
24+
25+
---Find the message part that corresponds to a permission request.
26+
---@param permission OpencodePermission|nil
27+
---@return OpencodeMessagePart|nil
28+
local function get_permission_part(permission)
29+
local call_id, message_id = get_tool_ids(permission)
30+
if not message_id or message_id == '' then
31+
return nil
32+
end
33+
34+
if state.messages then
35+
for _, message in ipairs(state.messages) do
36+
if message.info and message.info.id == message_id then
37+
for _, part in ipairs(message.parts or {}) do
38+
if call_id and call_id ~= '' then
39+
if part.callID == call_id then
40+
return part
41+
end
42+
else
43+
return part
44+
end
45+
end
46+
end
47+
end
48+
end
49+
50+
if permission and permission.sessionID and permission.sessionID ~= '' then
51+
local render_state = require('opencode.ui.renderer.ctx').render_state
52+
for _, part in ipairs(render_state:get_child_session_parts(permission.sessionID) or {}) do
53+
if call_id and call_id ~= '' then
54+
if part.callID == call_id then
55+
return part
56+
end
57+
else
58+
return part
59+
end
60+
end
61+
end
62+
end
63+
64+
---Check whether a permission has already been resolved (completed, error, etc.)
65+
---by inspecting the corresponding message part's status.
66+
---@param permission OpencodePermission|nil
67+
---@return boolean
68+
local function is_resolved_permission(permission)
69+
local part = get_permission_part(permission)
70+
if not part or not part.state then
71+
return false
72+
end
73+
74+
local part_status = part.state.status
75+
return part_status ~= nil and part_status ~= '' and part_status ~= 'pending' and part_status ~= 'running'
76+
end
77+
1178
---Add permission to queue
1279
---@param permission OpencodePermission
1380
function M.add_permission(permission)
@@ -269,6 +336,68 @@ function M._clear_dialog()
269336
end
270337
end
271338

339+
---Query the server for pending permissions and restore any that belong
340+
---to the active session. Mirrors question_window.restore_pending_question.
341+
---@param session_id string|nil
342+
function M.restore_pending_permissions(session_id)
343+
local Promise = require('opencode.promise')
344+
if not state.api_client or not session_id or session_id == '' then
345+
return Promise.new():resolve(nil)
346+
end
347+
348+
return state.api_client:list_permissions()
349+
:and_then(function(permissions)
350+
if not permissions or type(permissions) ~= 'table' then
351+
return
352+
end
353+
354+
local events = require('opencode.ui.renderer.events')
355+
local render_state = require('opencode.ui.renderer.ctx').render_state
356+
357+
for _, permission in ipairs(permissions) do
358+
if permission and permission.id then
359+
-- Check if this permission belongs to the active session or
360+
-- one of its child sessions (task tool).
361+
local belongs = permission.sessionID == session_id
362+
if not belongs and permission.sessionID and permission.sessionID ~= '' then
363+
belongs = render_state:get_task_part_by_child_session(permission.sessionID) ~= nil
364+
end
365+
if not belongs then
366+
local tool = permission.tool
367+
local tool_message_id = tool and tool.messageID
368+
if tool_message_id and state.messages then
369+
for _, message in ipairs(state.messages) do
370+
if message.info and message.info.id == tool_message_id then
371+
belongs = true
372+
break
373+
end
374+
end
375+
end
376+
end
377+
378+
if belongs and not is_resolved_permission(permission) then
379+
-- Check if already queued (avoid duplicate)
380+
local already_queued = false
381+
for _, existing in ipairs(M._permission_queue) do
382+
if existing.id == permission.id then
383+
already_queued = true
384+
break
385+
end
386+
end
387+
if not already_queued then
388+
events.on_permission_updated(permission)
389+
end
390+
end
391+
end
392+
end
393+
end)
394+
:catch(function(err)
395+
vim.schedule(function()
396+
vim.notify('Failed to restore pending permissions: ' .. vim.inspect(err), vim.log.levels.WARN)
397+
end)
398+
end)
399+
end
400+
272401
---Check if we have permissions
273402
---@return boolean
274403
function M.has_permissions()

lua/opencode/ui/renderer.lua

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,16 +246,8 @@ M.on_session_updated = events.on_session_updated
246246
function M.reset()
247247
ctx:reset()
248248
output_window.clear()
249-
250-
local permissions = state.pending_permissions or {}
251-
if #permissions > 0 and state.api_client then
252-
for _, permission in ipairs(permissions) do
253-
require('opencode.api').permission_deny(permission)
254-
end
255-
end
256249
permission_window.clear_all()
257250
state.renderer.reset()
258-
259251
flush.trigger_on_data_rendered()
260252
end
261253

@@ -405,10 +397,13 @@ function M.render_full_session()
405397
return Promise.new():resolve(nil)
406398
end
407399
return fetch_session():and_then(function(session_data)
408-
M._render_full_session_data(session_data, { restore_model_from_messages = true })
400+
M._render_full_session_data(session_data, {
401+
restore_model_from_messages = true,
402+
})
409403
local active_session = state.active_session
410404
if active_session and active_session.id then
411405
require('opencode.ui.question_window').restore_pending_question(active_session.id)
406+
permission_window.restore_pending_permissions(active_session.id)
412407
end
413408
return session_data
414409
end)

0 commit comments

Comments
 (0)