Skip to content

Commit 35841d0

Browse files
authored
feat: concurent permissions (#173)
* feat: support multiple concurrent permission requests with permission queue and window
1 parent 53a762a commit 35841d0

22 files changed

+719
-6929
lines changed

lua/opencode/api.lua

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -927,32 +927,40 @@ function M.redo()
927927
end
928928

929929
---@param answer? 'once'|'always'|'reject'
930-
function M.respond_to_permission(answer)
930+
---@param permission? OpencodePermission
931+
function M.respond_to_permission(answer, permission)
931932
answer = answer or 'once'
932-
if not state.current_permission then
933+
934+
local permission_window = require('opencode.ui.permission_window')
935+
local current_permission = permission or permission_window.get_current_permission()
936+
937+
if not current_permission then
933938
vim.notify('No permission request to accept', vim.log.levels.WARN)
934939
return
935940
end
936941

937942
state.api_client
938-
:respond_to_permission(state.current_permission.sessionID, state.current_permission.id, { response = answer })
943+
:respond_to_permission(current_permission.sessionID, current_permission.id, { response = answer })
939944
:catch(function(err)
940945
vim.schedule(function()
941946
vim.notify('Failed to reply to permission: ' .. vim.inspect(err), vim.log.levels.ERROR)
942947
end)
943948
end)
944949
end
945950

946-
function M.permission_accept()
947-
M.respond_to_permission('once')
951+
---@param permission? OpencodePermission
952+
function M.permission_accept(permission)
953+
M.respond_to_permission('once', permission)
948954
end
949955

950-
function M.permission_accept_all()
951-
M.respond_to_permission('always')
956+
---@param permission? OpencodePermission
957+
function M.permission_accept_all(permission)
958+
M.respond_to_permission('always', permission)
952959
end
953960

954-
function M.permission_deny()
955-
M.respond_to_permission('reject')
961+
---@param permission? OpencodePermission
962+
function M.permission_deny(permission)
963+
M.respond_to_permission('reject', permission)
956964
end
957965

958966
function M.toggle_tool_output()
@@ -1262,12 +1270,24 @@ M.commands = {
12621270
completions = { 'accept', 'accept_all', 'deny' },
12631271
fn = function(args)
12641272
local subcmd = args[1]
1273+
local index = tonumber(args[2])
1274+
local permission = nil
1275+
if index then
1276+
local permission_window = require('opencode.ui.permission_window')
1277+
local permissions = permission_window.get_all_permissions()
1278+
if not permissions or not permissions[index] then
1279+
vim.notify('Invalid permission index: ' .. tostring(index), vim.log.levels.ERROR)
1280+
return
1281+
end
1282+
permission = permissions[index]
1283+
end
1284+
12651285
if subcmd == 'accept' then
1266-
M.permission_accept()
1286+
M.permission_accept(permission)
12671287
elseif subcmd == 'accept_all' then
1268-
M.permission_accept_all()
1288+
M.permission_accept_all(permission)
12691289
elseif subcmd == 'deny' then
1270-
M.permission_deny()
1290+
M.permission_deny(permission)
12711291
else
12721292
local valid_subcmds = table.concat(M.commands.permission.completions or {}, ', ')
12731293
vim.notify('Invalid permission subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)

lua/opencode/context/base_context.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ M.get_git_diff = Promise.async(function(context_config)
218218
return nil
219219
end
220220

221-
return Promise.system({ 'git', 'diff', '--cached' }):and_then(function(output)
221+
return Promise.system({ 'git', 'diff', '--cached', '--minimal' }):and_then(function(output)
222222
if output == '' then
223223
return nil
224224
end

lua/opencode/core.lua

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local util = require('opencode.util')
99
local config = require('opencode.config')
1010
local image_handler = require('opencode.image_handler')
1111
local Promise = require('opencode.promise')
12+
local permission_window = require('opencode.ui.permission_window')
1213

1314
local M = {}
1415
M._abort_count = 0
@@ -277,9 +278,11 @@ M.cancel = Promise.async(function()
277278
if state.is_running() then
278279
M._abort_count = M._abort_count + 1
279280

280-
-- if there's a current permission, reject it
281-
if state.current_permission then
282-
require('opencode.api').permission_deny()
281+
local permissions = state.pending_permissions or {}
282+
if #permissions and state.api_client then
283+
for _, permission in ipairs(permissions) do
284+
require('opencode.api').permission_deny(permission)
285+
end
283286
end
284287

285288
local ok, result = pcall(function()
@@ -348,7 +351,7 @@ M.opencode_ok = Promise.async(function()
348351
end)
349352

350353
local function on_opencode_server()
351-
state.current_permission = nil
354+
permission_window.clear_all()
352355
end
353356

354357
--- Switches the current mode to the specified agent.
@@ -439,7 +442,7 @@ M._on_user_message_count_change = Promise.async(function(_, new, old)
439442
end)
440443

441444
M._on_current_permission_change = Promise.async(function(_, new, old)
442-
local permission_requested = old == nil and new ~= nil
445+
local permission_requested = #old < #new
443446
if config.hooks and config.hooks.on_permission_requested and permission_requested then
444447
local local_session = (state.active_session and state.active_session.id)
445448
and session.get_by_id(state.active_session.id):await()
@@ -457,7 +460,7 @@ end
457460
function M.setup()
458461
state.subscribe('opencode_server', on_opencode_server)
459462
state.subscribe('user_message_count', M._on_user_message_count_change)
460-
state.subscribe('current_permission', M._on_current_permission_change)
463+
state.subscribe('pending_permissions', M._on_current_permission_change)
461464

462465
vim.schedule(function()
463466
M.opencode_ok()

lua/opencode/event_manager.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ local util = require('opencode.util')
6565

6666
--- @class EventPermissionReplied
6767
--- @field type "permission.replied"
68-
--- @field properties {sessionID: string, permissionID: string, response: string}
68+
--- @field properties {sessionID: string, permissionID?: string, requestID?: string, response: string}
6969

7070
--- @class EventFileEdited
7171
--- @field type "file.edited"

lua/opencode/keymap.lua

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,39 +45,4 @@ function M.setup_window_keymaps(keymap_config, buf_id)
4545
process_keymap_entry(keymap_config or {}, { 'n' }, { silent = true, buffer = buf_id })
4646
end
4747

48-
---Add permission keymaps if permissions are being requested,
49-
---otherwise remove them
50-
---@param buf any
51-
function M.toggle_permission_keymap(buf)
52-
if not vim.api.nvim_buf_is_valid(buf) then
53-
return
54-
end
55-
local state = require('opencode.state')
56-
local config = require('opencode.config')
57-
local api = require('opencode.api')
58-
59-
local permission_config = config.keymap.permission
60-
if not permission_config then
61-
return
62-
end
63-
64-
if state.current_permission then
65-
for action, key in pairs(permission_config) do
66-
local api_func = api['permission_' .. action]
67-
if key and api_func then
68-
vim.keymap.set({ 'n', 'i' }, key, api_func, { buffer = buf, silent = true })
69-
end
70-
end
71-
return
72-
end
73-
74-
-- not requesting permissions, clear keymaps
75-
for _, key in pairs(permission_config) do
76-
if key then
77-
pcall(vim.api.nvim_buf_del_keymap, buf, 'n', key)
78-
pcall(vim.api.nvim_buf_del_keymap, buf, 'i', key)
79-
end
80-
end
81-
end
82-
8348
return M

lua/opencode/state.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
---@field messages OpencodeMessage[]|nil
3030
---@field current_message OpencodeMessage|nil
3131
---@field last_user_message OpencodeMessage|nil
32-
---@field current_permission OpencodePermission|nil
32+
---@field pending_permissions OpencodePermission[]
3333
---@field cost number
3434
---@field tokens_count number
3535
---@field job_count number
@@ -78,7 +78,7 @@ local _state = {
7878
messages = nil,
7979
current_message = nil,
8080
last_user_message = nil,
81-
current_permission = nil,
81+
pending_permissions = {},
8282
cost = 0,
8383
tokens_count = 0,
8484
-- job

lua/opencode/ui/formatter.lua

Lines changed: 7 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ local state = require('opencode.state')
66
local config = require('opencode.config')
77
local snapshot = require('opencode.snapshot')
88
local mention = require('opencode.ui.mention')
9+
local permission_window = require('opencode.ui.permission_window')
910

1011
local M = {}
1112

@@ -54,43 +55,6 @@ function M._format_reasoning(output, part)
5455
end
5556
end
5657

57-
function M._handle_permission_request(output, part)
58-
if part.state and part.state.status == 'error' and part.state.error then
59-
if part.state.error:match('rejected permission') then
60-
state.current_permission = nil
61-
else
62-
vim.notify('Unknown part state error: ' .. part.state.error)
63-
end
64-
return
65-
end
66-
67-
M._format_permission_request(output)
68-
end
69-
70-
function M._format_permission_request(output)
71-
local keys
72-
73-
if require('opencode.ui.ui').is_opencode_focused() then
74-
keys = {
75-
config.keymap.permission.accept,
76-
config.keymap.permission.accept_all,
77-
config.keymap.permission.deny,
78-
}
79-
else
80-
keys = {
81-
config.get_key_for_function('editor', 'permission_accept'),
82-
config.get_key_for_function('editor', 'permission_accept_all'),
83-
config.get_key_for_function('editor', 'permission_deny'),
84-
}
85-
end
86-
87-
output:add_empty_line()
88-
output:add_line('> [!WARNING] Permission required to run this tool.')
89-
output:add_line('>')
90-
output:add_line(('> Accept `%s` Always `%s` Deny `%s`'):format(unpack(keys)))
91-
output:add_empty_line()
92-
end
93-
9458
---Calculate statistics for reverted messages and tool calls
9559
---@param messages {info: MessageInfo, parts: OpencodeMessagePart[]}[] All messages in the session
9660
---@param revert_index number Index of the message where revert occurred
@@ -646,10 +610,6 @@ function M._format_tool(output, part)
646610
local metadata = part.state.metadata or {}
647611
local tool_output = part.state.output or ''
648612

649-
if state.current_permission and state.current_permission.messageID == part.messageID then
650-
metadata = state.current_permission.metadata or metadata
651-
end
652-
653613
if tool == 'bash' then
654614
M._format_bash_tool(output, input --[[@as BashToolInput]], metadata --[[@as BashToolMetadata]])
655615
elseif tool == 'read' or tool == 'edit' or tool == 'write' then
@@ -681,25 +641,6 @@ function M._format_tool(output, part)
681641
M._format_callout(output, 'ERROR', part.state.input.error)
682642
end
683643

684-
if
685-
state.current_permission
686-
and (
687-
(
688-
state.current_permission.tool
689-
and state.current_permission.tool.callID == part.callID
690-
and state.current_permission.tool.messageID == part.messageID
691-
)
692-
---@TODO this is for backward compatibility, remove later
693-
or (
694-
not state.current_permission.tool
695-
and state.current_permission.messageID == part.messageID
696-
and state.current_permission.callID == part.callID
697-
)
698-
)
699-
then
700-
M._handle_permission_request(output, part)
701-
end
702-
703644
local end_line = output:get_line_count()
704645
if end_line - start_line > 1 then
705646
M._add_vertical_border(output, start_line, end_line, 'OpencodeToolBorder', -1)
@@ -893,6 +834,12 @@ function M.format_part(part, message, is_last_part)
893834
M._format_patch(output, part)
894835
content_added = true
895836
end
837+
elseif role == 'system' then
838+
if part.type == 'permissions-display' then
839+
local text = table.concat(permission_window.get_display_lines(), '\n')
840+
output:add_lines(vim.split(vim.trim(text), '\n'))
841+
content_added = true
842+
end
896843
end
897844

898845
if content_added then

lua/opencode/ui/input_window.lua

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,10 +385,6 @@ function M.setup_autocmds(windows, group)
385385
require('opencode.ui.context_bar').render()
386386
end,
387387
})
388-
389-
state.subscribe('current_permission', function()
390-
require('opencode.keymap').toggle_permission_keymap(windows.input_buf)
391-
end)
392388
end
393389

394390
---Toggle the input window visibility (hide/show)

lua/opencode/ui/output_window.lua

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,6 @@ function M.setup_autocmds(windows, group)
214214
end,
215215
})
216216

217-
state.subscribe('current_permission', function()
218-
require('opencode.keymap').toggle_permission_keymap(windows.output_buf)
219-
end)
220-
221217
-- Track scroll position when window is scrolled
222218
vim.api.nvim_create_autocmd('WinScrolled', {
223219
group = group,
@@ -236,4 +232,16 @@ function M.clear()
236232
M.viewport_at_bottom = true
237233
end
238234

235+
---Get the output buffer
236+
---@return integer|nil Buffer ID
237+
function M.get_buf()
238+
return state.windows and state.windows.output_buf
239+
end
240+
241+
---Trigger a re-render by calling the renderer
242+
function M.render()
243+
local renderer = require('opencode.ui.renderer')
244+
renderer._render_all_messages()
245+
end
246+
239247
return M

lua/opencode/ui/permission/permission.lua

Whitespace-only changes.

0 commit comments

Comments
 (0)