diff --git a/packages/ui/src/stores/permissionStore.ts b/packages/ui/src/stores/permissionStore.ts index f22cba68..756bc598 100644 --- a/packages/ui/src/stores/permissionStore.ts +++ b/packages/ui/src/stores/permissionStore.ts @@ -169,20 +169,24 @@ export const usePermissionStore = create()( return { autoAccept }; }); - // Mirror state to the server so it can suppress permission - // notifications at the source (otherwise the 500ms debounce - // races with the client's auto-response and can leak). - void fetch('/api/notifications/auto-accept', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionId, enabled }), - }).catch(() => { /* best-effort */ }); + const sessionScope = resolveSessionScope(sessionId, sessions); + + // Mirror inherited state to the server so it can suppress + // permission notifications before the client auto-response + // round-trip. Send known descendants too; server-side + // ancestry lookup can lag OpenCode session indexing. + for (const scopedSessionId of sessionScope) { + void fetch('/api/notifications/auto-accept', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId: scopedSessionId, enabled }), + }).catch(() => { /* best-effort */ }); + } if (!enabled) { return; } - const sessionScope = resolveSessionScope(sessionId, sessions); const sessionDirectory = useSessionUIStore.getState().getDirectoryForSession(sessionId); const directories = new Set(); const currentDirectory = normalizeDirectoryCandidate(opencodeClient.getDirectory()); diff --git a/packages/web/server/lib/notifications/runtime.js b/packages/web/server/lib/notifications/runtime.js index 70bfbca1..276c57f2 100644 --- a/packages/web/server/lib/notifications/runtime.js +++ b/packages/web/server/lib/notifications/runtime.js @@ -60,9 +60,26 @@ export const createNotificationTriggerRuntime = (deps) => { }; const setCachedSessionParentId = (sessionId, parentID) => { + if (!parentID) return; sessionParentIdCache.set(sessionId, { parentID: parentID ?? null, at: Date.now() }); }; + const getParentIdFromPayload = (payload) => { + if (!payload || typeof payload !== 'object') return null; + if (payload.type !== 'session.created' && payload.type !== 'session.updated') return null; + const parentID = payload.properties?.info?.parentID ?? null; + return typeof parentID === 'string' && parentID.length > 0 ? parentID : null; + }; + + const maybeCacheSessionParentFromPayload = (payload) => { + const sessionId = extractSessionIdFromPayload(payload); + if (typeof sessionId !== 'string' || sessionId.length === 0) return; + const parentID = getParentIdFromPayload(payload); + if (parentID) { + setCachedSessionParentId(sessionId, parentID); + } + }; + const fetchSessionParentId = async (sessionId) => { if (!sessionId) return undefined; @@ -82,12 +99,19 @@ export const createNotificationTriggerRuntime = (deps) => { return undefined; } const data = await response.json().catch(() => null); - if (!Array.isArray(data)) { + const sessions = Array.isArray(data) + ? data + : Array.isArray(data?.items) + ? data.items + : Array.isArray(data?.data) + ? data.data + : null; + if (!sessions) { return undefined; } - const match = data.find((session) => session && typeof session === 'object' && session.id === sessionId); - const parentID = match?.parentID ? match.parentID : null; + const match = sessions.find((session) => session && typeof session === 'object' && session.id === sessionId); + const parentID = match?.parentID ?? null; setCachedSessionParentId(sessionId, parentID); return parentID; } catch { @@ -164,6 +188,8 @@ export const createNotificationTriggerRuntime = (deps) => { return; } + maybeCacheSessionParentFromPayload(payload); + const sessionId = extractSessionIdFromPayload(payload); if (payload.type === 'message.updated') { const info = payload.properties?.info; @@ -171,8 +197,7 @@ export const createNotificationTriggerRuntime = (deps) => { const settings = await readSettingsFromDisk(); if (settings.notifyOnSubtasks === false) { - const sessionInfo = payload.properties?.session; - const parentIDFromPayload = sessionInfo?.parentID ?? payload.properties?.parentID; + const parentIDFromPayload = getParentIdFromPayload(payload); const parentID = parentIDFromPayload ? parentIDFromPayload : await fetchSessionParentId(sessionId); @@ -437,6 +462,11 @@ export const createNotificationTriggerRuntime = (deps) => { const timer = setTimeout(async () => { pushPermissionDebounceTimers.delete(sessionId); + if (await isSessionAutoAccepting(sessionId)) { + if (requestKey) notifiedPermissionRequests.add(requestKey); + return; + } + const settings = await readSettingsFromDisk(); if (settings.notifyOnQuestion === false) {