Skip to content

Commit 42490ec

Browse files
committed
feat: implement basic permissions worflow
1 parent 476ce82 commit 42490ec

19 files changed

+927
-66
lines changed

README.md

Lines changed: 73 additions & 46 deletions
Large diffs are not rendered by default.

lua/opencode/api.lua

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,42 @@ function M.redo()
512512
end)
513513
end
514514

515+
---@param answer? 'once'|'always'|'reject'
516+
function M.respond_to_permission(answer)
517+
answer = answer or 'once'
518+
if not state.current_permission then
519+
vim.notify('No permission request to accept', vim.log.levels.WARN)
520+
return
521+
end
522+
523+
ui.render_output(true)
524+
state.api_client
525+
:respond_to_permission(state.active_session.id, state.current_permission.id, { response = answer })
526+
:and_then(function()
527+
vim.schedule(function()
528+
state.current_permission = nil
529+
ui.render_output(true)
530+
end)
531+
end)
532+
:catch(function(err)
533+
vim.schedule(function()
534+
vim.notify('Failed to reply to permission: ' .. vim.inspect(err), vim.log.levels.ERROR)
535+
end)
536+
end)
537+
end
538+
539+
function M.permission_accept()
540+
M.respond_to_permission('once')
541+
end
542+
543+
function M.permission_accept_all()
544+
M.respond_to_permission('always')
545+
end
546+
547+
function M.permission_deny()
548+
M.respond_to_permission('reject')
549+
end
550+
515551
-- Command def/compactinitions that call the API functions
516552
M.commands = {
517553
swap_position = {
@@ -912,6 +948,30 @@ M.commands = {
912948
end,
913949
slash_cmd = '/redo',
914950
},
951+
952+
permission_accept = {
953+
name = 'OpencodePermissionAccept',
954+
desc = 'Accept current permission request',
955+
fn = function()
956+
M.respond_to_permission('once')
957+
end,
958+
},
959+
960+
permission_accept_all = {
961+
name = 'OpencodePermissionAcceptAll',
962+
desc = 'Accept all permission requests',
963+
fn = function()
964+
M.respond_to_permission('always')
965+
end,
966+
},
967+
968+
permission_deny = {
969+
name = 'OpencodePermissionDeny',
970+
desc = 'Deny current permission request',
971+
fn = function()
972+
M.respond_to_permission('reject')
973+
end,
974+
},
915975
}
916976

917977
function M.get_slash_commands()

lua/opencode/api_client.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,14 @@ function OpencodeApiClient:subscribe_to_events(directory, on_event)
361361
url = url .. '?directory=' .. directory
362362
end
363363

364-
return server_job.stream_api(url, 'GET', nil, on_event)
364+
return server_job.stream_api(url, 'GET', nil, function(chunk)
365+
-- strip data: prefix if present
366+
chunk = chunk:gsub('^data:%s*', '')
367+
local ok, event = pcall(vim.json.decode, vim.trim(chunk))
368+
if ok and event then
369+
on_event(event)
370+
end
371+
end)
365372
end
366373

367374
-- Tool endpoints

lua/opencode/config.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ M.defaults = {
4646
diff_restore_snapshot_all = '<leader>orR',
4747
open_configuration_file = '<leader>oC',
4848
swap_position = '<leader>ox', -- Swap Opencode pane left/right
49+
permission_accept = '<leader>opa',
50+
permission_accept_all = '<leader>opA',
51+
permission_deny = '<leader>opd',
4952
},
5053
window = {
5154
submit = '<cr>',
@@ -66,6 +69,9 @@ M.defaults = {
6669
debug_message = '<leader>oD',
6770
debug_output = '<leader>oO',
6871
debug_session = '<leader>ods',
72+
permission_accept = 'a',
73+
permission_accept_all = 'A',
74+
permission_deny = 'd',
6975
},
7076
},
7177
ui = {

lua/opencode/core.lua

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ end
4040
function M.open(opts)
4141
opts = opts or { focus = 'input', new_session = false }
4242

43-
local state = require('opencode.state')
4443
if not state.opencode_server_job or not state.opencode_server_job:is_running() then
4544
state.opencode_server_job = server_job.ensure_server() --[[@as OpencodeServer]]
4645
end
@@ -239,7 +238,7 @@ end
239238

240239
function M.setup()
241240
local OpencodeApiClient = require('opencode.api_client')
242-
state.api_client = OpencodeApiClient.new() --[[@as OpencodeApiClient]]
241+
state.api_client = OpencodeApiClient.new()
243242
end
244243

245244
return M

lua/opencode/curl.lua

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
local M = {}
2+
3+
--- Build curl command arguments from options
4+
--- @param opts table Options for the curl request
5+
--- @return table args Array of curl command arguments
6+
local function build_curl_args(opts)
7+
local args = { 'curl', '-s', '--no-buffer' }
8+
9+
if opts.method and opts.method ~= 'GET' then
10+
table.insert(args, '-X')
11+
table.insert(args, opts.method)
12+
end
13+
14+
if opts.headers then
15+
for key, value in pairs(opts.headers) do
16+
table.insert(args, '-H')
17+
table.insert(args, key .. ': ' .. value)
18+
end
19+
end
20+
21+
if opts.body then
22+
table.insert(args, '-d')
23+
table.insert(args, opts.body)
24+
end
25+
26+
if opts.proxy and opts.proxy ~= '' then
27+
table.insert(args, '--proxy')
28+
table.insert(args, opts.proxy)
29+
end
30+
31+
table.insert(args, opts.url)
32+
33+
return args
34+
end
35+
36+
--- Parse HTTP response headers and body
37+
--- @param output string Raw curl output with headers
38+
--- @return table response Response object with status, headers, and body
39+
local function parse_response(output)
40+
local lines = vim.split(output, '\n')
41+
local status = 200
42+
local headers = {}
43+
local body_start = 1
44+
45+
-- Find status line and headers
46+
for i, line in ipairs(lines) do
47+
if line:match('^HTTP/') then
48+
status = tonumber(line:match('HTTP/[%d%.]+%s+(%d+)')) or 200
49+
elseif line:match('^[%w%-]+:') then
50+
local key, value = line:match('^([%w%-]+):%s*(.*)$')
51+
if key and value then
52+
headers[key:lower()] = value
53+
end
54+
elseif line == '' then
55+
body_start = i + 1
56+
break
57+
end
58+
end
59+
60+
local body_lines = {}
61+
for i = body_start, #lines do
62+
table.insert(body_lines, lines[i])
63+
end
64+
local body = table.concat(body_lines, '\n')
65+
66+
return {
67+
status = status,
68+
headers = headers,
69+
body = body,
70+
}
71+
end
72+
73+
--- Make an HTTP request
74+
--- @param opts table Request options
75+
--- @return table|nil job Job object for streaming requests, nil for regular requests
76+
function M.request(opts)
77+
local args = build_curl_args(opts)
78+
79+
if opts.stream then
80+
local buffer = ''
81+
82+
local job = vim.system(args, {
83+
stdout = function(err, chunk)
84+
if err then
85+
if opts.on_error then
86+
opts.on_error({ message = err })
87+
end
88+
return
89+
end
90+
91+
if chunk then
92+
buffer = buffer .. chunk
93+
94+
-- Extract complete lines
95+
while buffer:find('\n') do
96+
local line, rest = buffer:match('([^\n]*\n)(.*)')
97+
if line then
98+
opts.stream(nil, line)
99+
buffer = rest
100+
else
101+
break
102+
end
103+
end
104+
end
105+
end,
106+
stderr = function(err, data)
107+
if err and opts.on_error then
108+
opts.on_error({ message = err })
109+
end
110+
end,
111+
}, opts.on_exit and function(result)
112+
-- Flush any remaining buffer content
113+
if buffer and buffer ~= '' then
114+
opts.stream(nil, buffer)
115+
end
116+
opts.on_exit(result.code, result.signal)
117+
end or nil)
118+
119+
return {
120+
_job = job,
121+
is_running = function()
122+
return job and job.pid ~= nil
123+
end,
124+
shutdown = function()
125+
if job and job.pid then
126+
pcall(function()
127+
job:kill('sigterm')
128+
end)
129+
end
130+
end,
131+
}
132+
else
133+
table.insert(args, 2, '-i')
134+
135+
vim.system(args, {
136+
text = true,
137+
}, function(result)
138+
if result.code ~= 0 then
139+
if opts.on_error then
140+
opts.on_error({ message = result.stderr or 'curl failed' })
141+
end
142+
return
143+
end
144+
145+
local response = parse_response(result.stdout or '')
146+
147+
if opts.callback then
148+
opts.callback(response)
149+
end
150+
end)
151+
end
152+
end
153+
154+
return M

0 commit comments

Comments
 (0)