Skip to content

Commit 0e91bde

Browse files
ro0grmistral-vibe
andauthored
feat(hooks): add on_request_permission hook for external notifications (#246)
Add a new `on_request_permission` hook that fires when the agent requests user permission (e.g., for file edits). This complements the `on_response_complete` to enable external notification systems (like Ghostty OSC 9) to alert users when their attention is needed, even when they're not looking at the editor. - [x] update docs --------- Co-authored-by: Mistral Vibe <vibe@mistral.ai>
1 parent e08304f commit 0e91bde

5 files changed

Lines changed: 123 additions & 2 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,18 @@ integrating with other plugins.
937937
vim.lsp.buf.format({ bufnr = data.bufnr, timeout_ms = 5000 })
938938
end
939939
end,
940+
941+
-- Called when the agent needs permission to execute a tool (e.g. shell command).
942+
-- Fires for each pending permission request.
943+
on_request_permission = function(data)
944+
-- data.request: table - The ACP permission request object
945+
-- data.request.toolCall: table - contains .kind, .title, etc.
946+
-- data.session_id: string - The ACP session ID
947+
-- data.tab_page_id: number - The Neovim tabpage ID
948+
local tool = data.request.toolCall
949+
local label = tool.title or tool.kind or "action"
950+
vim.notify("Agent needs permission for: " .. label)
951+
end,
940952
}
941953
}
942954
}

doc/agentic.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,11 @@ Event hooks ~
689689
-- data.tab_page_id: number
690690
-- data.bufnr: number|nil (if file is loaded in a buffer)
691691
end,
692+
on_request_permission = function(data)
693+
-- data.request: table (contains .toolCall)
694+
-- data.session_id: string
695+
-- data.tab_page_id: number
696+
end,
692697
},
693698
}
694699
<

lua/agentic/config_default.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
--- @field tab_page_id number The tabpage ID
5959
--- @field bufnr? number Buffer number if the file is loaded in a buffer
6060

61+
--- Data passed to the on_request_permission hook
62+
--- @class agentic.UserConfig.RequestPermissionData
63+
--- @field request agentic.acp.RequestPermission The permission request object
64+
--- @field session_id string The ACP session ID
65+
--- @field tab_page_id number The tabpage ID
66+
6167
--- @class agentic.UserConfig.KeymapEntry
6268
--- @field [1] string The key binding
6369
--- @field mode string|string[] The mode(s) for this binding
@@ -200,6 +206,7 @@
200206
--- @field on_response_complete? fun(data: agentic.UserConfig.ResponseCompleteData): nil
201207
--- @field on_session_update? fun(data: agentic.UserConfig.SessionUpdateData): nil
202208
--- @field on_file_edit? fun(data: agentic.UserConfig.FileEditData): nil
209+
--- @field on_request_permission? fun(data: agentic.UserConfig.RequestPermissionData): nil
203210

204211
--- Control various behaviors and features of the plugin
205212
--- @class agentic.UserConfig.Settings
@@ -555,6 +562,7 @@ local ConfigDefault = {
555562
on_response_complete = nil,
556563
on_session_update = nil,
557564
on_file_edit = nil,
565+
on_request_permission = nil,
558566
},
559567

560568
headers = {},

lua/agentic/session_manager.lua

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ local FILE_MUTATING_KINDS = {
2727
}
2828

2929
--- Safely invoke a user-configured hook
30-
--- @param hook_name "on_create_session_response" | "on_prompt_submit" | "on_response_complete" | "on_session_update" | "on_file_edit"
31-
--- @param data agentic.UserConfig.CreateSessionResponseData | agentic.UserConfig.PromptSubmitData | agentic.UserConfig.ResponseCompleteData | agentic.UserConfig.SessionUpdateData | agentic.UserConfig.FileEditData
30+
--- @param hook_name "on_create_session_response" | "on_prompt_submit" | "on_response_complete" | "on_session_update" | "on_file_edit" | "on_request_permission"
31+
--- @param data agentic.UserConfig.CreateSessionResponseData | agentic.UserConfig.PromptSubmitData | agentic.UserConfig.ResponseCompleteData | agentic.UserConfig.SessionUpdateData | agentic.UserConfig.FileEditData | agentic.UserConfig.RequestPermissionData
3232
function P.invoke_hook(hook_name, data)
3333
local hook = Config.hooks and Config.hooks[hook_name]
3434

@@ -977,6 +977,12 @@ function SessionManager:_build_handlers()
977977
end,
978978

979979
on_request_permission = function(request, callback)
980+
P.invoke_hook("on_request_permission", {
981+
request = request,
982+
session_id = self.session_id,
983+
tab_page_id = self.tab_page_id,
984+
})
985+
980986
self.status_animation:stop()
981987

982988
local function wrapped_callback(option_id)

lua/agentic/session_manager.test.lua

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,4 +1552,94 @@ describe("agentic.SessionManager", function()
15521552
end
15531553
)
15541554
end)
1555+
1556+
describe("_build_handlers: on_request_permission", function()
1557+
local Config = require("agentic.config")
1558+
--- @type TestStub
1559+
local schedule_stub
1560+
--- @type TestSpy
1561+
local hook_spy
1562+
--- @type agentic.SessionManager
1563+
local session
1564+
1565+
before_each(function()
1566+
schedule_stub = spy.stub(vim, "schedule")
1567+
schedule_stub:invokes(function(fn)
1568+
fn()
1569+
end)
1570+
hook_spy = spy.new(function() end)
1571+
Config.hooks = Config.hooks or {}
1572+
Config.hooks.on_request_permission = nil
1573+
1574+
session = {
1575+
session_id = "test-session-123",
1576+
tab_page_id = 1,
1577+
status_animation = {
1578+
stop = function() end,
1579+
start = function() end,
1580+
},
1581+
permission_manager = {
1582+
has_pending = function()
1583+
return false
1584+
end,
1585+
add_request = function() end,
1586+
},
1587+
_show_diff_in_buffer = function() end,
1588+
_clear_diff_in_buffer = function() end,
1589+
_build_handlers = SessionManager._build_handlers,
1590+
} --[[@as agentic.SessionManager]]
1591+
end)
1592+
1593+
after_each(function()
1594+
schedule_stub:revert()
1595+
Config.hooks.on_request_permission = nil
1596+
end)
1597+
1598+
it("invokes on_request_permission hook with correct payload", function()
1599+
Config.hooks.on_request_permission = function(data)
1600+
hook_spy(data)
1601+
end
1602+
1603+
local handlers = session:_build_handlers()
1604+
local mock_request = {
1605+
sessionId = "test-session-123",
1606+
toolCall = {
1607+
toolCallId = "tool-1",
1608+
kind = "edit",
1609+
title = "Edit file",
1610+
},
1611+
options = {
1612+
{
1613+
optionId = "allow_once",
1614+
name = "Allow Once",
1615+
kind = "allow_once",
1616+
},
1617+
},
1618+
}
1619+
local mock_callback = function() end
1620+
1621+
handlers.on_request_permission(mock_request, mock_callback)
1622+
1623+
assert.spy(hook_spy).was.called(1)
1624+
local data = hook_spy.calls[1][1]
1625+
assert.equal("test-session-123", data.session_id)
1626+
assert.equal(1, data.tab_page_id)
1627+
assert.equal(mock_request, data.request)
1628+
end)
1629+
1630+
it("does not fail when hook is not configured", function()
1631+
Config.hooks.on_request_permission = nil
1632+
1633+
local handlers = session:_build_handlers()
1634+
local mock_request = {
1635+
sessionId = "test-session-123",
1636+
toolCall = { toolCallId = "tool-1", kind = "edit" },
1637+
options = {},
1638+
}
1639+
local mock_callback = function() end
1640+
1641+
-- Should not throw an error
1642+
handlers.on_request_permission(mock_request, mock_callback)
1643+
end)
1644+
end)
15551645
end)

0 commit comments

Comments
 (0)