Skip to content
22 changes: 20 additions & 2 deletions packages/ui/src/stores/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from "./worktrees"
import { fetchCommands, clearCommands } from "./commands"
import { serverSettings } from "./preferences"
import { setSessionPendingPermission, setSessionPendingQuestion } from "./session-state"
import { sessions, setSessionPendingPermission, setSessionPendingQuestion } from "./session-state"
import { setHasInstances } from "./ui"
import { messageStoreBus } from "./message-v2/bus"
import { upsertPermissionV2, removePermissionV2, upsertQuestionV2, removeQuestionV2 } from "./message-v2/bridge"
Expand All @@ -38,7 +38,14 @@ import {
markPermissionReplied,
pruneRepliedPermissions,
} from "./permission-replies"
import { clearAutoAcceptPermission, drainAutoAcceptPermissions, isPermissionAutoAcceptEnabled, togglePermissionAutoAccept } from "./permission-auto-accept"
import {
clearAutoAcceptPermission,
drainAutoAcceptPermissions,
isPermissionAutoAcceptEnabled,
resolvePermissionAutoAcceptFamilyRoot,
setPermissionAutoAcceptFamilyRootResolver,
togglePermissionAutoAccept,
} from "./permission-auto-accept"
import { clearCacheForInstance } from "../lib/global-cache"
import { getLogger } from "../lib/logger"
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
Expand All @@ -47,6 +54,12 @@ import { activeSidecarToken } from "./sidecars"

const log = getLogger("api")

setPermissionAutoAcceptFamilyRootResolver((instanceId, sessionId) => {
const instanceSessions = sessions().get(instanceId)
if (!instanceSessions) return sessionId
return resolvePermissionAutoAcceptFamilyRoot(sessionId, (id) => instanceSessions.get(id))
})

const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())

const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
Expand Down Expand Up @@ -904,6 +917,10 @@ function togglePermissionAutoAcceptForSession(instanceId: string, sessionId: str
const willEnable = !isPermissionAutoAcceptEnabled(instanceId, sessionId)
togglePermissionAutoAccept(instanceId, sessionId)
if (!willEnable) return
drainAutoAcceptPermissionsForInstance(instanceId)
}

function drainAutoAcceptPermissionsForInstance(instanceId: string): void {
drainAutoAcceptPermissions(instanceId, getPermissionQueue(instanceId), sendPermissionResponse, hasPendingPermission)
}

Expand Down Expand Up @@ -1214,6 +1231,7 @@ export {
markPermissionReplied,
hasRepliedPermission,
togglePermissionAutoAcceptForSession,
drainAutoAcceptPermissionsForInstance,
clearPermissionQueue,
sendPermissionResponse,
setActivePermissionIdForInstance,
Expand Down
36 changes: 36 additions & 0 deletions packages/ui/src/stores/permission-auto-accept.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"

import { resolvePermissionAutoAcceptFamilyRoot } from "./permission-auto-accept.ts"

describe("resolvePermissionAutoAcceptFamilyRoot", () => {
it("keeps a loaded child as root when its parent is missing", () => {
const root = resolvePermissionAutoAcceptFamilyRoot("child", (sessionId) => {
if (sessionId === "child") return { id: "child", parentId: "parent" }
return undefined
})

assert.equal(root, "child")
})

it("resolves to the master session when the full parent chain is loaded", () => {
const root = resolvePermissionAutoAcceptFamilyRoot("grandchild", (sessionId) => {
if (sessionId === "grandchild") return { id: "grandchild", parentId: "child" }
if (sessionId === "child") return { id: "child", parentId: "master" }
if (sessionId === "master") return { id: "master", parentId: null }
return undefined
})

assert.equal(root, "master")
})

it("keeps a fork session as its own root", () => {
const root = resolvePermissionAutoAcceptFamilyRoot("fork", (sessionId) => {
if (sessionId === "fork") return { id: "fork", parentId: "master", revert: { messageID: "msg", partID: "part" } }
if (sessionId === "master") return { id: "master", parentId: null }
return undefined
})

assert.equal(root, "fork")
})
})
33 changes: 32 additions & 1 deletion packages/ui/src/stores/permission-auto-accept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,40 @@ const log = getLogger("api")

type AutoAcceptResponder = (instanceId: string, sessionId: string, requestId: string, reply: PermissionReply) => Promise<void>
type PendingPermissionChecker = (instanceId: string, requestId: string) => boolean
type PermissionAutoAcceptSession = {
id: string
parentId?: string | null
revert?: unknown
}
type SessionLookup = (sessionId: string) => PermissionAutoAcceptSession | undefined
type FamilyRootResolver = (instanceId: string, sessionId: string) => string

let resolveFamilyRoot: FamilyRootResolver = (_instanceId, sessionId) => sessionId

export function resolvePermissionAutoAcceptFamilyRoot(sessionId: string, getSession: SessionLookup): string {
let currentId = sessionId
let lastKnownId = sessionId
const seen = new Set<string>()

while (currentId && !seen.has(currentId)) {
seen.add(currentId)
const session = getSession(currentId)
if (!session) return lastKnownId
lastKnownId = session.id
if (session.revert) return session.id
if (!session.parentId) return session.id
currentId = session.parentId
}

return currentId || sessionId
}

export function setPermissionAutoAcceptFamilyRootResolver(resolver: FamilyRootResolver) {
resolveFamilyRoot = resolver
}

function makeKey(instanceId: string, sessionId: string) {
return `${instanceId}:${sessionId}`
return `${instanceId}:${resolveFamilyRoot(instanceId, sessionId)}`
}

function readInitialState() {
Expand Down
22 changes: 22 additions & 0 deletions packages/ui/src/stores/session-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
hasRepliedPermission,
addQuestionToQueue,
removeQuestionFromQueue,
drainAutoAcceptPermissionsForInstance,
} from "./instances"
import { showAlertDialog } from "./alerts"
import {
Expand Down Expand Up @@ -221,6 +222,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory

let updatedInstanceSessions: Map<string, Session> | undefined
let shouldExpandParent: string | null = null
let shouldDrainAutoAcceptPermissions = false

setSessions((prev) => {
const next = new Map(prev)
Expand All @@ -243,6 +245,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
instanceSessions.set(sessionId, merged)
next.set(instanceId, instanceSessions)
updatedInstanceSessions = instanceSessions
shouldDrainAutoAcceptPermissions = Boolean(merged.parentId)

if (merged.parentId && merged.status === "working" && (existing?.status ?? "idle") !== "working") {
shouldExpandParent = merged.parentId
Expand All @@ -252,6 +255,10 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory

syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)

if (shouldDrainAutoAcceptPermissions) {
drainAutoAcceptPermissionsForInstance(instanceId)
}

if (shouldExpandParent) {
ensureSessionParentExpanded(instanceId, shouldExpandParent)
}
Expand Down Expand Up @@ -477,6 +484,14 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
created: Date.now(),
updated: Date.now(),
},
revert: info.revert
? {
messageID: info.revert.messageID,
partID: info.revert.partID,
snapshot: info.revert.snapshot,
diff: info.revert.diff,
}
: undefined,
} as Session

let updatedInstanceSessions: Map<string, Session> | undefined
Expand All @@ -492,6 +507,9 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo

syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
setSessionRevertV2(instanceId, info.id, info.revert ?? null)
if (newSession.parentId) {
drainAutoAcceptPermissionsForInstance(instanceId)
}

log.info(`[SSE] New session created: ${info.id}`, newSession)
} else {
Expand All @@ -502,6 +520,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
const updatedSession = {
...existingSession,
title: info.title || existingSession.title,
parentId: info.parentID ?? existingSession.parentId,
status: existingSession.status ?? "idle",
retry: existingSession.retry ?? null,
time: mergedTime,
Expand All @@ -528,6 +547,9 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo

syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
setSessionRevertV2(instanceId, info.id, info.revert ?? null)
if (updatedSession.parentId) {
drainAutoAcceptPermissionsForInstance(instanceId)
}
}
}

Expand Down
Loading