fix(claude): release permission prompt abort listeners (#23195)

* fix(claude): release permission prompt abort listeners

* fix(i18n): register existing diff note fallback strings

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
This commit is contained in:
OrcaWin
2026-09-26 13:55:27 -07:00
committed by GitHub
co-authored by m4air
parent f6631ddeee
commit 0daa175be1
2 changed files with 13 additions and 5 deletions
@@ -41,10 +41,12 @@ function callbacksFor() {
describe('Claude permission callbacks', () => {
it('registers a decodable can_use_tool as a durable prompt and settles it from the registry', async () => {
const control = callbacksFor()
const controller = new AbortController()
const removeListener = vi.spyOn(controller.signal, 'removeEventListener')
const answered = control.canUseTool(
'Bash',
{ command: 'git status' },
permissionOptions('perm-1', 'tool-1', new AbortController().signal, [
permissionOptions('perm-1', 'tool-1', controller.signal, [
{ type: 'addRules', rules: [], behavior: 'allow', destination: 'session' }
])
)
@@ -63,6 +65,7 @@ describe('Claude permission callbacks', () => {
// The prompt's settle is the SDK callback's own resolve — answering resolves this promise.
found?.prompt.settle({ behavior: 'allow', toolUseID: 'tool-1' })
await expect(answered).resolves.toEqual({ behavior: 'allow', toolUseID: 'tool-1' })
expect(removeListener).toHaveBeenCalledTimes(1)
})
it('keeps the SDK permission presentation and strips terminal escapes', async () => {
@@ -56,6 +56,11 @@ export function buildClaudePermissionCallbacks(deps: ClaudePermissionCallbackDep
} {
const canUseTool: CanUseTool = (toolName, input, options) =>
new Promise<PermissionResult | null>((resolve) => {
let cancel = (): void => {}
const settle = (response: PermissionResult | null): void => {
options.signal.removeEventListener('abort', cancel)
resolve(response)
}
// Classify first so later permission-mode policy cannot swallow a plan proposal.
const subject = claudePermissionSubject(toolName, input)
const prompt = deps.prompts.register({
@@ -66,14 +71,14 @@ export function buildClaudePermissionCallbacks(deps: ClaudePermissionCallbackDep
toolUseId: options.toolUseID,
input,
suggestions: options.suggestions ?? [],
settle: resolve,
settle,
turnId: deps.currentTurnId?.() ?? null
})
if (!prompt) {
resolve(denySafeResult(options.toolUseID))
settle(denySafeResult(options.toolUseID))
return
}
const cancel = (): void => {
cancel = (): void => {
if (deps.prompts.forgetIfPending(prompt)) {
deps.emit({
type: 'prompt-cancelled',
@@ -82,7 +87,7 @@ export function buildClaudePermissionCallbacks(deps: ClaudePermissionCallbackDep
})
// Null is the SDK's "no response written" sentinel: a cancelled request must not
// be answered, only forgotten.
resolve(null)
settle(null)
}
}
if (options.signal.aborted) {