diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index bdfed7f3b..1f857591d 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -36,7 +36,7 @@ import { serverApi } from "../../lib/api-client" import { loadBackgroundProcesses } from "../../stores/background-processes" import { BackgroundProcessOutputDialog } from "../background-process-output-dialog" import { useI18n } from "../../lib/i18n" -import { getPermissionQueue, getPermissionQueueLength, getQuestionQueueLength, sendPermissionResponse } from "../../stores/instances" +import { getPermissionQueueLength, getQuestionQueueLength } from "../../stores/instances" import SessionSidebar from "./shell/SessionSidebar" import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests" import RightPanel from "./shell/right-panel/RightPanel" @@ -58,13 +58,7 @@ import { useDrawerHostMeasure } from "./shell/useDrawerHostMeasure" import { useDrawerResize } from "./shell/useDrawerResize" import { useSessionCache } from "./shell/useSessionCache" import { useInstanceSessionContext } from "./shell/useInstanceSessionContext" -import { getPermissionSessionId } from "../../types/permission" -import { - canAutoRespondPermission, - finishAutoRespondPermission, - getPermissionAutoAcceptInFlightVersion, - isPermissionAutoAcceptEnabled, -} from "../../stores/permission-auto-accept" +import { isPermissionAutoAcceptEnabled } from "../../stores/permission-auto-accept" const log = getLogger("session") const OPEN_SESSION_SEARCH_EVENT = "codenomad:open-session-search" @@ -269,8 +263,6 @@ const InstanceShell2: Component = (props) => { return permissions + questions > 0 }) - const permissionQueue = createMemo(() => getPermissionQueue(props.instance.id)) - const activePromptInputApi = createMemo(() => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") return null @@ -284,25 +276,6 @@ const InstanceShell2: Component = (props) => { })) } - createEffect(() => { - getPermissionAutoAcceptInFlightVersion() - - for (const permission of permissionQueue()) { - const sessionId = getPermissionSessionId(permission) - if (!sessionId) continue - if (!permission?.id) continue - if (!canAutoRespondPermission(props.instance.id, sessionId, permission.id)) continue - - void sendPermissionResponse(props.instance.id, sessionId, permission.id, "once") - .catch((error) => { - log.error("Failed to auto-accept permission", error) - }) - .finally(() => { - finishAutoRespondPermission(props.instance.id, sessionId, permission.id) - }) - } - }) - const yoloModeEnabled = createMemo(() => { const session = activeSessionForInstance() if (!session) return false diff --git a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx index f618bf03e..7be3284c4 100644 --- a/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/tabs/StatusTab.tsx @@ -13,7 +13,8 @@ import type { Session } from "../../../../../types/session" import ContextUsagePanel from "../../../../session/context-usage-panel" import { TodoListView } from "../../../../tool-call/renderers/todo" import InstanceServiceStatus from "../../../../instance-service-status" -import { isPermissionAutoAcceptEnabled, togglePermissionAutoAccept } from "../../../../../stores/permission-auto-accept" +import { togglePermissionAutoAcceptForSession } from "../../../../../stores/instances" +import { isPermissionAutoAcceptEnabled } from "../../../../../stores/permission-auto-accept" interface StatusTabProps { t: (key: string, vars?: Record) => string @@ -63,7 +64,7 @@ const StatusTab: Component = (props) => { color="warning" size="small" inputProps={{ "aria-label": props.t("instanceShell.yoloMode.title") }} - onChange={() => togglePermissionAutoAccept(props.instanceId, session.id)} + onChange={() => togglePermissionAutoAcceptForSession(props.instanceId, session.id)} /> diff --git a/packages/ui/src/stores/instances.ts b/packages/ui/src/stores/instances.ts index c6b494827..b4d8878d6 100644 --- a/packages/ui/src/stores/instances.ts +++ b/packages/ui/src/stores/instances.ts @@ -32,6 +32,7 @@ import { setSessionPendingPermission, setSessionPendingQuestion } from "./sessio import { setHasInstances } from "./ui" import { messageStoreBus } from "./message-v2/bus" import { upsertPermissionV2, removePermissionV2, upsertQuestionV2, removeQuestionV2 } from "./message-v2/bridge" +import { clearAutoAcceptPermission, drainAutoAcceptPermissions, isPermissionAutoAcceptEnabled, togglePermissionAutoAccept } from "./permission-auto-accept" import { clearCacheForInstance } from "../lib/global-cache" import { getLogger } from "../lib/logger" import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata" @@ -207,6 +208,7 @@ async function syncPendingPermissions(instanceId: string): Promise { addPermissionToQueue(instanceId, permission) upsertPermissionV2(instanceId, permission) } + drainAutoAcceptPermissions(instanceId, getPermissionQueue(instanceId), sendPermissionResponse, hasPendingPermission) } catch (error) { log.warn("Failed to sync pending permissions", { instanceId, error }) } @@ -615,6 +617,10 @@ function getPermissionQueueLength(instanceId: string): number { return getPermissionQueue(instanceId).length } +function hasPendingPermission(instanceId: string, permissionId: string): boolean { + return getPermissionQueue(instanceId).some((permission) => permission.id === permissionId) +} + function getQuestionQueue(instanceId: string): QuestionRequest[] { const queue = questionQueues().get(instanceId) if (!queue) { @@ -796,6 +802,8 @@ function addPermissionToQueue(instanceId: string, permission: PermissionRequestL } byPermissionId.set(permission.id, slug) } + + drainAutoAcceptPermissions(instanceId, [permission], sendPermissionResponse, hasPendingPermission) } function removePermissionFromQueue(instanceId: string, permissionId: string): void { @@ -822,8 +830,6 @@ function removePermissionFromQueue(instanceId: string, permissionId: string): vo return next }) - const updatedQueue = getPermissionQueue(instanceId) - recomputeActiveInterruption(instanceId) const removed = removedPermission @@ -832,13 +838,27 @@ function removePermissionFromQueue(instanceId: string, permissionId: string): vo permissionWorktreeSlugByInstance.get(instanceId)?.delete(permissionId) const removedSessionId = getPermissionSessionId(removed) if (removedSessionId) { + clearAutoAcceptPermission(instanceId, removedSessionId, permissionId) const remaining = decrementSessionPendingCount(instanceId, removedSessionId) setSessionPendingPermission(instanceId, removedSessionId, remaining > 0) } } } +function togglePermissionAutoAcceptForSession(instanceId: string, sessionId: string): void { + const willEnable = !isPermissionAutoAcceptEnabled(instanceId, sessionId) + togglePermissionAutoAccept(instanceId, sessionId) + if (!willEnable) return + drainAutoAcceptPermissions(instanceId, getPermissionQueue(instanceId), sendPermissionResponse, hasPendingPermission) +} + function clearPermissionQueue(instanceId: string): void { + for (const permission of getPermissionQueue(instanceId)) { + const sessionId = getPermissionSessionId(permission) + if (sessionId) { + clearAutoAcceptPermission(instanceId, sessionId, permission.id) + } + } setPermissionQueues((prev) => { const next = new Map(prev) next.delete(instanceId) @@ -1130,6 +1150,7 @@ export { getPermissionQueueLength, addPermissionToQueue, removePermissionFromQueue, + togglePermissionAutoAcceptForSession, clearPermissionQueue, sendPermissionResponse, setActivePermissionIdForInstance, diff --git a/packages/ui/src/stores/permission-auto-accept.ts b/packages/ui/src/stores/permission-auto-accept.ts index 6607d3a8b..1b1ea402c 100644 --- a/packages/ui/src/stores/permission-auto-accept.ts +++ b/packages/ui/src/stores/permission-auto-accept.ts @@ -1,7 +1,15 @@ import { createSignal } from "solid-js" +import type { PermissionReply, PermissionRequestLike } from "../types/permission" +import { getPermissionSessionId } from "../types/permission" +import { getLogger } from "../lib/logger" const STORAGE_KEY = "codenomad:permission-auto-accept:v1" +const log = getLogger("api") + +type AutoAcceptResponder = (instanceId: string, sessionId: string, requestId: string, reply: PermissionReply) => Promise +type PendingPermissionChecker = (instanceId: string, requestId: string) => boolean + function makeKey(instanceId: string, sessionId: string) { return `${instanceId}:${sessionId}` } @@ -34,7 +42,6 @@ function persist(next: Map) { } const [autoAcceptState, setAutoAcceptState] = createSignal(readInitialState()) -const [inFlightVersion, setInFlightVersion] = createSignal(0) const inFlight = new Set() @@ -54,28 +61,65 @@ export function setPermissionAutoAcceptEnabled(instanceId: string, sessionId: st persist(next) return next }) + if (!enabled) { + clearAutoAcceptSession(instanceId, sessionId) + } } export function togglePermissionAutoAccept(instanceId: string, sessionId: string) { setPermissionAutoAcceptEnabled(instanceId, sessionId, !isPermissionAutoAcceptEnabled(instanceId, sessionId)) } -export function canAutoRespondPermission(instanceId: string, sessionId: string, requestId: string) { - const key = makeKey(instanceId, sessionId) - if (!autoAcceptState().get(key)) return false - const requestKey = `${key}:${requestId}` - if (inFlight.has(requestKey)) return false - inFlight.add(requestKey) - return true +function makeRequestKey(instanceId: string, sessionId: string, requestId: string) { + return `${makeKey(instanceId, sessionId)}:${requestId}` } -export function getPermissionAutoAcceptInFlightVersion() { - return inFlightVersion() +export function clearAutoAcceptPermission(instanceId: string, sessionId: string, requestId: string) { + const requestKey = makeRequestKey(instanceId, sessionId, requestId) + inFlight.delete(requestKey) } -export function finishAutoRespondPermission(instanceId: string, sessionId: string, requestId: string) { - if (!inFlight.delete(`${makeKey(instanceId, sessionId)}:${requestId}`)) { - return +export function clearAutoAcceptSession(instanceId: string, sessionId: string) { + const prefix = `${makeKey(instanceId, sessionId)}:` + for (const requestKey of Array.from(inFlight)) { + if (requestKey.startsWith(prefix)) { + inFlight.delete(requestKey) + } + } +} + +export function drainAutoAcceptPermission( + instanceId: string, + permission: PermissionRequestLike, + responder: AutoAcceptResponder, + isPending: PendingPermissionChecker, +) { + const sessionId = getPermissionSessionId(permission) + if (!sessionId || !permission?.id) return + if (!isPermissionAutoAcceptEnabled(instanceId, sessionId)) return + if (!isPending(instanceId, permission.id)) return + + const requestKey = makeRequestKey(instanceId, sessionId, permission.id) + if (inFlight.has(requestKey)) return + + inFlight.add(requestKey) + + void responder(instanceId, sessionId, permission.id, "once") + .catch((error) => { + log.error("Failed to auto-accept permission", error) + }) + .finally(() => { + inFlight.delete(requestKey) + }) +} + +export function drainAutoAcceptPermissions( + instanceId: string, + permissions: PermissionRequestLike[], + responder: AutoAcceptResponder, + isPending: PendingPermissionChecker, +) { + for (const permission of permissions) { + drainAutoAcceptPermission(instanceId, permission, responder, isPending) } - setInFlightVersion((value) => value + 1) }