1+ local config = require (" codecompanion.config" )
2+ local diff_utils = require (" codecompanion.diff.utils" )
13local log = require (" codecompanion.utils.log" )
4+ local ui_utils = require (" codecompanion.utils.ui" )
25local utils = require (" codecompanion.utils" )
36
47local labels = require (" codecompanion.interactions.chat.tools.labels" )
58
9+ local fmt = string.format
10+
611--- Ref: https://agentclientprotocol.com/protocol/schema#permissionoptionkind
712local ACP_OPTIONS = {
813 allow_once = { label = labels .accept , keymap = " accept" },
@@ -14,10 +19,10 @@ local ACP_OPTIONS = {
1419local M = {}
1520
1621--- Find the first reject option from the request options
17- --- @param options table
22+ --- @param opts table
1823--- @return string | nil optionId
19- local function find_reject_option (options )
20- for _ , opt in ipairs (options or {}) do
24+ local function find_reject_option (opts )
25+ for _ , opt in ipairs (opts or {}) do
2126 if opt .kind :find (" ^reject" , 1 , true ) then
2227 return opt .optionId
2328 end
@@ -26,11 +31,11 @@ local function find_reject_option(options)
2631end
2732
2833--- Build a map of kind -> optionId for easy lookup
29- --- @param options table
34+ --- @param opts table
3035--- @return table<string , string> kind -> optionId
31- local function build_kind_map (options )
36+ local function build_kind_map (opts )
3237 local map = {}
33- for _ , opt in ipairs (options or {}) do
38+ for _ , opt in ipairs (opts or {}) do
3439 if type (opt .kind ) == " string" and type (opt .optionId ) == " string" then
3540 map [opt .kind ] = opt .optionId
3641 end
4045
4146--- Get the shared keymap key for an ACP option kind
4247--- @param kind string
43- --- @param keys table Resolved keymaps from labels.keymaps ()
48+ --- @param keys table
4449--- @return string | nil
4550local function key_for_kind (kind , keys )
4651 local opt = ACP_OPTIONS [kind ]
@@ -51,8 +56,8 @@ local function key_for_kind(kind, keys)
5156end
5257
5358--- Build the banner displayed in the diff window winbar
54- --- @param kind_map table<string , string> kind -> optionId
55- --- @param keys table Resolved keymaps from labels.keymaps ()
59+ --- @param kind_map table<string , string>
60+ --- @param keys table
5661--- @return string
5762local function build_banner (kind_map , keys )
5863 local parts = {}
@@ -63,11 +68,11 @@ local function build_banner(kind_map, keys)
6368 local lhs = key_for_kind (kind , keys )
6469 if lhs then
6570 local label = (ACP_OPTIONS [kind ] and ACP_OPTIONS [kind ].label ) or kind :gsub (" _" , " " )
66- table.insert (parts , string.format (" %s %s" , lhs , label ))
71+ table.insert (parts , fmt (" %s %s" , lhs , label ))
6772 end
6873 end
6974
70- table.insert (parts , string.format (" %s/%s Next/Prev" , keys .next_hunk , keys .previous_hunk ))
75+ table.insert (parts , fmt (" %s/%s Next/Prev" , keys .next_hunk , keys .previous_hunk ))
7176 table.insert (parts , " q Close" )
7277
7378 return table.concat (parts , " | " )
@@ -150,28 +155,26 @@ local function setup_diff_keymaps(opts)
150155 })
151156end
152157
153- --- Display the diff preview and resolve permission by user decision
154- --- @param opts { chat : CodeCompanion.Chat , request : table , on_done : fun ( choice_label : string ) }
155- --- @return nil
156- local function show_diff (opts )
157- local d = get_diff (opts .request .tool_call )
158+ --- Open the floating diff view for an ACP permission request
159+ --- @param permission table
160+ local function open_diff_view (permission )
161+ local d = get_diff (permission .request .tool_call )
158162
159- local diff_id = math.random (1000000 )
160- local kind_map = build_kind_map (opts .request .options )
163+ local kind_map = build_kind_map (permission .request .options )
161164 local keys = labels .keymaps ()
162165
163166 local diff_ui = require (" codecompanion.helpers" ).show_diff ({
164167 from_lines = vim .split (d .old or " " , " \n " , { plain = true }),
165168 to_lines = vim .split (d .new or " " , " \n " , { plain = true }),
166169 banner = build_banner (kind_map , keys ),
167- chat_bufnr = opts .chat .bufnr ,
168- diff_id = diff_id ,
170+ chat_bufnr = permission .chat .bufnr ,
171+ diff_id = math.random ( 1000000 ) ,
169172 ft = vim .filetype .match ({ filename = d .path }) or " text" ,
170173 keymaps = {
171174 on_reject = function ()
172- opts .on_done (labels .reject )
173- local rejected = find_reject_option (opts .request .options )
174- opts .request .respond (rejected , false )
175+ permission .on_done (labels .reject )
176+ local rejected = find_reject_option (permission .request .options )
177+ permission .request .respond (rejected , false )
175178 end ,
176179 },
177180 skip_default_keymaps = true ,
@@ -182,53 +185,40 @@ local function show_diff(opts)
182185 diff_ui = diff_ui ,
183186 kind_map = kind_map ,
184187 keys = keys ,
185- request = opts .request ,
186- on_done = opts .on_done ,
188+ request = permission .request ,
189+ on_done = permission .on_done ,
187190 })
188191end
189192
190- --- Show the permission request to the user and handle their response
191- --- @param chat CodeCompanion.Chat
192- --- @param request table
193- --- @return nil
194- function M .confirm (chat , request )
195- local approval_prompt = require (" codecompanion.interactions.chat.helpers.approval_prompt" )
196-
197- local tool_call = request .tool_call
198- local prompt = string.format (
199- " %s: %s" ,
200- utils .capitalize (tool_call and tool_call .kind or " Permission" ),
201- tool_call and tool_call .title or " Agent requested permission"
202- )
203-
204- local has_diff = request .tool_call and requires_diff (request .tool_call )
193+ --- Build the approval choices for an ACP permission request
194+ --- @param permission table
195+ --- @param has_diff boolean
196+ --- @return CodeCompanion.Chat.ApprovalChoice[]
197+ local function build_choices (permission , has_diff )
205198 local keys = labels .keymaps ()
206-
207199 local choices = {}
208200
209- local on_done
210-
211201 if has_diff then
212202 table.insert (choices , {
213203 keymap = keys .view ,
214204 label = labels .view ,
215205 preview = true ,
216206 callback = function ()
217207 log :debug (" [acp::request_permission] Opening diff for review" )
218- show_diff ({ chat = chat , request = request , on_done = on_done } )
208+ open_diff_view ( permission )
219209 end ,
220210 })
221211 end
222212
223- for _ , opt in ipairs (request .options or {}) do
213+ for _ , opt in ipairs (permission . request .options or {}) do
224214 local key = key_for_kind (opt .kind , keys )
225215 if key then
226216 table.insert (choices , {
227217 keymap = key ,
228218 label = (ACP_OPTIONS [opt .kind ] and ACP_OPTIONS [opt .kind ].label ) or opt .name ,
229219 callback = function ()
230220 log :debug (" [acp::request_permission] User selected option %s" , opt .optionId )
231- request .respond (opt .optionId , false )
221+ permission . request .respond (opt .optionId , false )
232222 end ,
233223 })
234224 end
@@ -239,17 +229,77 @@ function M.confirm(chat, request)
239229 label = labels .cancel ,
240230 callback = function ()
241231 log :debug (" [acp::request_permission] User cancelled" )
242- request .respond (nil , true )
232+ permission . request .respond (nil , true )
243233 end ,
244234 })
245235
246- on_done = approval_prompt .request (chat , {
247- id = request .id ,
248- name = tool_call and tool_call .kind or nil ,
249- title = has_diff and " View Proposed Edits" or nil ,
250- prompt = prompt ,
236+ return choices
237+ end
238+
239+ --- Allow the user to approve from within the chat buffer
240+ --- @param permission table
241+ --- @param choices CodeCompanion.Chat.ApprovalChoice[]
242+ --- @param prompt_opts { title ?: string , prompt : string }
243+ local function approve_in_chat (permission , choices , prompt_opts )
244+ local approval_prompt = require (" codecompanion.interactions.chat.helpers.approval_prompt" )
245+ permission .on_done = approval_prompt .request (permission .chat , {
251246 choices = choices ,
247+ id = permission .request .id ,
248+ name = permission .request .tool_call and permission .request .tool_call .kind or nil ,
249+ prompt = prompt_opts .prompt ,
250+ title = prompt_opts .title ,
252251 })
253252end
254253
254+ --- Show the permission request to the user and handle their response
255+ --- @param chat CodeCompanion.Chat
256+ --- @param request table
257+ --- @return nil
258+ function M .confirm (chat , request )
259+ local tool_call = request .tool_call
260+ local has_diff = tool_call and requires_diff (tool_call )
261+
262+ local base_prompt = fmt (
263+ " %s: %s" ,
264+ utils .capitalize (tool_call and tool_call .kind or " Permission" ),
265+ tool_call and tool_call .title or " Agent requested permission"
266+ )
267+
268+ local permission = { chat = chat , request = request }
269+ local choices = build_choices (permission , has_diff )
270+
271+ if not has_diff then
272+ return approve_in_chat (permission , choices , { prompt = base_prompt })
273+ end
274+
275+ local d = get_diff (tool_call )
276+ local from_lines = vim .split (d .old or " " , " \n " , { plain = true })
277+ local to_lines = vim .split (d .new or " " , " \n " , { plain = true })
278+ local changed_lines = diff_utils .changed_lines (from_lines , to_lines )
279+ local threshold = config .display .diff .threshold_for_chat
280+ local threshold_met = threshold and threshold > 0 and changed_lines > 0 and changed_lines <= threshold
281+
282+ if threshold_met then
283+ -- Show small diffs in the chat buffer
284+ local diff_text = diff_utils .unified (from_lines , to_lines )
285+ local prompt = fmt (
286+ [[ %s
287+
288+ `````diff
289+ %s
290+ `````]] ,
291+ base_prompt ,
292+ diff_text
293+ )
294+ return approve_in_chat (permission , choices , { title = " Proposed Edits" , prompt = prompt })
295+ elseif ui_utils .buf_is_active (chat .bufnr ) then
296+ -- If the chat is active, show the diff in the floating window
297+ approve_in_chat (permission , choices , { title = " View Proposed Edits" , prompt = base_prompt })
298+ return open_diff_view (permission )
299+ else
300+ -- Otherwise, don't force the diff on the user, just show the approval
301+ return approve_in_chat (permission , choices , { title = " View Proposed Edits" , prompt = base_prompt })
302+ end
303+ end
304+
255305return M
0 commit comments