diff --git a/README.md b/README.md index 02f489dd..0a787df4 100644 --- a/README.md +++ b/README.md @@ -937,6 +937,18 @@ integrating with other plugins. vim.lsp.buf.format({ bufnr = data.bufnr, timeout_ms = 5000 }) end end, + + -- Called when the agent needs permission to execute a tool (e.g. shell command). + -- Fires for each pending permission request. + on_request_permission = function(data) + -- data.request: table - The ACP permission request object + -- data.request.toolCall: table - contains .kind, .title, etc. + -- data.session_id: string - The ACP session ID + -- data.tab_page_id: number - The Neovim tabpage ID + local tool = data.request.toolCall + local label = tool.title or tool.kind or "action" + vim.notify("Agent needs permission for: " .. label) + end, } } } diff --git a/doc/agentic.txt b/doc/agentic.txt index ad6c4f67..83207ed4 100644 --- a/doc/agentic.txt +++ b/doc/agentic.txt @@ -689,6 +689,11 @@ Event hooks ~ -- data.tab_page_id: number -- data.bufnr: number|nil (if file is loaded in a buffer) end, + on_request_permission = function(data) + -- data.request: table (contains .toolCall) + -- data.session_id: string + -- data.tab_page_id: number + end, }, } < diff --git a/lua/agentic/config_default.lua b/lua/agentic/config_default.lua index 223cff70..6aebbfb5 100644 --- a/lua/agentic/config_default.lua +++ b/lua/agentic/config_default.lua @@ -58,6 +58,12 @@ --- @field tab_page_id number The tabpage ID --- @field bufnr? number Buffer number if the file is loaded in a buffer +--- Data passed to the on_request_permission hook +--- @class agentic.UserConfig.RequestPermissionData +--- @field request agentic.acp.RequestPermission The permission request object +--- @field session_id string The ACP session ID +--- @field tab_page_id number The tabpage ID + --- @class agentic.UserConfig.KeymapEntry --- @field [1] string The key binding --- @field mode string|string[] The mode(s) for this binding @@ -200,6 +206,7 @@ --- @field on_response_complete? fun(data: agentic.UserConfig.ResponseCompleteData): nil --- @field on_session_update? fun(data: agentic.UserConfig.SessionUpdateData): nil --- @field on_file_edit? fun(data: agentic.UserConfig.FileEditData): nil +--- @field on_request_permission? fun(data: agentic.UserConfig.RequestPermissionData): nil --- Control various behaviors and features of the plugin --- @class agentic.UserConfig.Settings @@ -555,6 +562,7 @@ local ConfigDefault = { on_response_complete = nil, on_session_update = nil, on_file_edit = nil, + on_request_permission = nil, }, headers = {}, diff --git a/lua/agentic/session_manager.lua b/lua/agentic/session_manager.lua index 64c19ce3..1908ea78 100644 --- a/lua/agentic/session_manager.lua +++ b/lua/agentic/session_manager.lua @@ -27,8 +27,8 @@ local FILE_MUTATING_KINDS = { } --- Safely invoke a user-configured hook ---- @param hook_name "on_create_session_response" | "on_prompt_submit" | "on_response_complete" | "on_session_update" | "on_file_edit" ---- @param data agentic.UserConfig.CreateSessionResponseData | agentic.UserConfig.PromptSubmitData | agentic.UserConfig.ResponseCompleteData | agentic.UserConfig.SessionUpdateData | agentic.UserConfig.FileEditData +--- @param hook_name "on_create_session_response" | "on_prompt_submit" | "on_response_complete" | "on_session_update" | "on_file_edit" | "on_request_permission" +--- @param data agentic.UserConfig.CreateSessionResponseData | agentic.UserConfig.PromptSubmitData | agentic.UserConfig.ResponseCompleteData | agentic.UserConfig.SessionUpdateData | agentic.UserConfig.FileEditData | agentic.UserConfig.RequestPermissionData function P.invoke_hook(hook_name, data) local hook = Config.hooks and Config.hooks[hook_name] @@ -977,6 +977,12 @@ function SessionManager:_build_handlers() end, on_request_permission = function(request, callback) + P.invoke_hook("on_request_permission", { + request = request, + session_id = self.session_id, + tab_page_id = self.tab_page_id, + }) + self.status_animation:stop() local function wrapped_callback(option_id) diff --git a/lua/agentic/session_manager.test.lua b/lua/agentic/session_manager.test.lua index a41bf4f6..5cd062d9 100644 --- a/lua/agentic/session_manager.test.lua +++ b/lua/agentic/session_manager.test.lua @@ -1552,4 +1552,94 @@ describe("agentic.SessionManager", function() end ) end) + + describe("_build_handlers: on_request_permission", function() + local Config = require("agentic.config") + --- @type TestStub + local schedule_stub + --- @type TestSpy + local hook_spy + --- @type agentic.SessionManager + local session + + before_each(function() + schedule_stub = spy.stub(vim, "schedule") + schedule_stub:invokes(function(fn) + fn() + end) + hook_spy = spy.new(function() end) + Config.hooks = Config.hooks or {} + Config.hooks.on_request_permission = nil + + session = { + session_id = "test-session-123", + tab_page_id = 1, + status_animation = { + stop = function() end, + start = function() end, + }, + permission_manager = { + has_pending = function() + return false + end, + add_request = function() end, + }, + _show_diff_in_buffer = function() end, + _clear_diff_in_buffer = function() end, + _build_handlers = SessionManager._build_handlers, + } --[[@as agentic.SessionManager]] + end) + + after_each(function() + schedule_stub:revert() + Config.hooks.on_request_permission = nil + end) + + it("invokes on_request_permission hook with correct payload", function() + Config.hooks.on_request_permission = function(data) + hook_spy(data) + end + + local handlers = session:_build_handlers() + local mock_request = { + sessionId = "test-session-123", + toolCall = { + toolCallId = "tool-1", + kind = "edit", + title = "Edit file", + }, + options = { + { + optionId = "allow_once", + name = "Allow Once", + kind = "allow_once", + }, + }, + } + local mock_callback = function() end + + handlers.on_request_permission(mock_request, mock_callback) + + assert.spy(hook_spy).was.called(1) + local data = hook_spy.calls[1][1] + assert.equal("test-session-123", data.session_id) + assert.equal(1, data.tab_page_id) + assert.equal(mock_request, data.request) + end) + + it("does not fail when hook is not configured", function() + Config.hooks.on_request_permission = nil + + local handlers = session:_build_handlers() + local mock_request = { + sessionId = "test-session-123", + toolCall = { toolCallId = "tool-1", kind = "edit" }, + options = {}, + } + local mock_callback = function() end + + -- Should not throw an error + handlers.on_request_permission(mock_request, mock_callback) + end) + end) end)