From 14eb996b95b7d64d1ca5f9f387d0b88cb2e17518 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Mon, 18 May 2026 11:21:03 +0200 Subject: [PATCH 1/2] feat(hooks): add on_request_permission hook for external notifications Add new hook that fires when the agent requests user permission (e.g., for file edits). This enables external notification systems (like Ghostty OSC 9) to alert users when their attention is needed, even when they're not looking at the editor. Changes: - Add RequestPermissionData type to config_default.lua - Add on_request_permission to Hooks type and default config - Invoke hook in SessionManager:_build_handlers() with request, session_id, tab_page_id - Add unit tests in session_manager.test.lua Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- lua/agentic/config_default.lua | 8 +++ lua/agentic/session_manager.lua | 10 +++- lua/agentic/session_manager.test.lua | 90 ++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) 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) From 1e07af2d2dc94e97c7816163eb5f4a97f975e40e Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Thu, 21 May 2026 01:18:26 +0200 Subject: [PATCH 2/2] docs: add on_request_permission hook documentation --- README.md | 12 ++++++++++++ doc/agentic.txt | 5 +++++ 2 files changed, 17 insertions(+) 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, }, } <