2026-07-17 12:59:41 +03:00
|
|
|
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
|
|
|
|
import { opencodeClient } from "@/lib/opencode/client"
|
|
|
|
|
import { usePermissionStore } from "@/stores/permissionStore"
|
2026-08-01 03:14:59 +03:00
|
|
|
import { getAllSyncSessionMap, getDirectoryState } from "./sync-refs"
|
2026-07-17 12:59:41 +03:00
|
|
|
import * as sessionActions from "./session-actions"
|
|
|
|
|
|
|
|
|
|
const RETRY_DELAYS_MS = [0, 250, 1000]
|
|
|
|
|
|
|
|
|
|
type Dependencies = {
|
|
|
|
|
getPolicy: () => Record<string, boolean>
|
|
|
|
|
getSessions: () => ReadonlyMap<string, Session>
|
|
|
|
|
getSession: (sessionId: string, directory?: string) => Promise<Session>
|
2026-08-01 03:14:59 +03:00
|
|
|
getKnownPendingPermissions?: (directory?: string) => PermissionRequest[]
|
2026-07-17 12:59:41 +03:00
|
|
|
listPendingPermissions: (directory?: string) => Promise<PermissionRequest[]>
|
2026-08-01 03:14:59 +03:00
|
|
|
getPermissionState: (sessionId: string, requestId: string, directory?: string) => Promise<"ok" | "resolved" | "unknown">
|
|
|
|
|
reply: (sessionId: string, requestId: string, directory?: string) => Promise<void>
|
2026-07-17 12:59:41 +03:00
|
|
|
wait: (delayMs: number) => Promise<void>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createVSCodePermissionAutoAcceptRuntime(dependencies: Dependencies) {
|
|
|
|
|
const inFlight = new Map<string, Promise<boolean>>()
|
|
|
|
|
const reconcileInFlight = new Map<string, Promise<void>>()
|
|
|
|
|
const recentOutcomes = new Map<string, boolean>()
|
|
|
|
|
|
|
|
|
|
const isEnabled = async (sessionId: string, directory?: string) => {
|
|
|
|
|
const policy = dependencies.getPolicy()
|
|
|
|
|
const syncedSessions = dependencies.getSessions()
|
|
|
|
|
const fetchedSessions = new Map<string, Session>()
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
let current: string | undefined = sessionId
|
|
|
|
|
let currentDirectory = directory
|
|
|
|
|
|
|
|
|
|
while (current && !seen.has(current)) {
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(policy, current)) return policy[current] === true
|
|
|
|
|
seen.add(current)
|
|
|
|
|
|
|
|
|
|
let session: Session | undefined = syncedSessions.get(current) ?? fetchedSessions.get(current)
|
|
|
|
|
if (!session) {
|
|
|
|
|
try {
|
|
|
|
|
session = await dependencies.getSession(current, currentDirectory)
|
|
|
|
|
fetchedSessions.set(session.id, session)
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
current = session.parentID
|
|
|
|
|
currentDirectory = session.directory || currentDirectory
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 03:14:59 +03:00
|
|
|
const processPermission = (
|
|
|
|
|
permission: PermissionRequest,
|
|
|
|
|
directory?: string,
|
|
|
|
|
options?: { verifyPending?: boolean },
|
|
|
|
|
) => {
|
2026-07-17 12:59:41 +03:00
|
|
|
const recent = recentOutcomes.get(permission.id)
|
|
|
|
|
if (recent !== undefined) return Promise.resolve(recent)
|
|
|
|
|
const existing = inFlight.get(permission.id)
|
|
|
|
|
if (existing) return existing
|
|
|
|
|
|
|
|
|
|
const task = (async () => {
|
|
|
|
|
if (!(await isEnabled(permission.sessionID, directory))) return false
|
|
|
|
|
|
2026-08-01 03:14:59 +03:00
|
|
|
if (options?.verifyPending !== false) {
|
|
|
|
|
const permissionState = await dependencies.getPermissionState(permission.sessionID, permission.id, directory)
|
|
|
|
|
if (permissionState === "resolved") return true
|
|
|
|
|
}
|
2026-07-17 12:59:41 +03:00
|
|
|
|
|
|
|
|
for (const delay of RETRY_DELAYS_MS) {
|
|
|
|
|
if (delay > 0) await dependencies.wait(delay)
|
|
|
|
|
try {
|
2026-08-01 03:14:59 +03:00
|
|
|
await dependencies.reply(permission.sessionID, permission.id, directory)
|
2026-07-17 12:59:41 +03:00
|
|
|
return true
|
|
|
|
|
} catch {
|
|
|
|
|
// A failed reply stays visible after the bounded retries.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
})().then((accepted) => {
|
|
|
|
|
if (accepted) {
|
|
|
|
|
recentOutcomes.set(permission.id, true)
|
|
|
|
|
setTimeout(() => recentOutcomes.delete(permission.id), 5000)
|
|
|
|
|
}
|
|
|
|
|
return accepted
|
|
|
|
|
}).finally(() => inFlight.delete(permission.id))
|
|
|
|
|
|
|
|
|
|
inFlight.set(permission.id, task)
|
|
|
|
|
return task
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reconcilePending = (directory?: string) => {
|
|
|
|
|
const key = directory?.trim() || "all"
|
|
|
|
|
const existing = reconcileInFlight.get(key)
|
|
|
|
|
if (existing) return existing
|
|
|
|
|
|
2026-08-01 03:14:59 +03:00
|
|
|
const task = (async () => {
|
|
|
|
|
const processed = new Set<string>()
|
|
|
|
|
const processAll = async (permissions: PermissionRequest[], verifyPending: boolean) => {
|
|
|
|
|
const pending = permissions.filter((permission) => {
|
|
|
|
|
if (!permission?.id || processed.has(permission.id)) return false
|
|
|
|
|
processed.add(permission.id)
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
await Promise.all(pending.map((permission) => processPermission(permission, directory, { verifyPending })))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A permission.asked event is already authoritative local state. Process
|
|
|
|
|
// those visible cards before the network reconciliation so enabling the
|
|
|
|
|
// toggle works even when permission.list is unavailable or stale.
|
|
|
|
|
await processAll(dependencies.getKnownPendingPermissions?.(directory) ?? [], false)
|
|
|
|
|
await processAll(await dependencies.listPendingPermissions(directory), true)
|
|
|
|
|
})()
|
2026-07-17 12:59:41 +03:00
|
|
|
.finally(() => reconcileInFlight.delete(key))
|
|
|
|
|
|
|
|
|
|
reconcileInFlight.set(key, task)
|
|
|
|
|
return task
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { processPermission, reconcilePending }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const runtime = createVSCodePermissionAutoAcceptRuntime({
|
|
|
|
|
getPolicy: () => usePermissionStore.getState().autoAccept,
|
|
|
|
|
getSessions: getAllSyncSessionMap,
|
|
|
|
|
getSession: (sessionId, directory) => opencodeClient.getSession(sessionId, directory),
|
2026-08-01 03:14:59 +03:00
|
|
|
getKnownPendingPermissions: (directory) => Object.values(getDirectoryState(directory)?.permission ?? {}).flat(),
|
2026-07-17 12:59:41 +03:00
|
|
|
listPendingPermissions: (directory) => opencodeClient.listPendingPermissions({ directories: [directory] }),
|
2026-08-01 03:14:59 +03:00
|
|
|
getPermissionState: async (sessionId, requestId, directory) => (await opencodeClient.fetchPermission(sessionId, requestId, directory)).state,
|
|
|
|
|
reply: (sessionId, requestId, directory) => sessionActions.respondToPermission(sessionId, requestId, "once", directory),
|
2026-07-17 12:59:41 +03:00
|
|
|
wait: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-01 03:14:59 +03:00
|
|
|
export const processVSCodePermissionAutoAccept = (
|
|
|
|
|
permission: PermissionRequest,
|
|
|
|
|
directory?: string,
|
|
|
|
|
) => runtime.processPermission(permission, directory, { verifyPending: false })
|
|
|
|
|
export const processVSCodeReconciledPermissionAutoAccept = runtime.processPermission
|
2026-07-17 12:59:41 +03:00
|
|
|
export const reconcileVSCodePendingPermissions = runtime.reconcilePending
|