mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
fix(ui): drain yolo permissions outside shell render (#424)
## Summary - Split out from #422 as the YOLO-only permission fix. - Moves YOLO auto-accept draining out of `InstanceShell` render effects and into the permission queue flow. - Keeps behavior unchanged: YOLO remains per-session and replies with `once` only. - Keeps in-flight cleanup for auto-accept attempts so duplicate sends are guarded outside UI render timing. ## Why In YOLO mode, the app could still appear to wait on a permission or block the UI, even though auto-accept was enabled and the permission was already queued. The auto-accept drain was tied to an `InstanceShell` render effect, so the reply path depended on a specific UI shell rendering. Draining from permission sync/enqueue and from the YOLO toggle makes auto-accept run from the permission queue itself instead of UI render timing. ## Validation - `git diff --check` - `npm run typecheck --workspace @codenomad/ui`
This commit is contained in:
parent
6d120b0d84
commit
8976d9985b
4 changed files with 86 additions and 47 deletions
|
|
@ -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<InstanceShellProps> = (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<InstanceShellProps> = (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
|
||||
|
|
|
|||
|
|
@ -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, any>) => string
|
||||
|
|
@ -63,7 +64,7 @@ const StatusTab: Component<StatusTabProps> = (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)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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<void>
|
||||
type PendingPermissionChecker = (instanceId: string, requestId: string) => boolean
|
||||
|
||||
function makeKey(instanceId: string, sessionId: string) {
|
||||
return `${instanceId}:${sessionId}`
|
||||
}
|
||||
|
|
@ -34,7 +42,6 @@ function persist(next: Map<string, boolean>) {
|
|||
}
|
||||
|
||||
const [autoAcceptState, setAutoAcceptState] = createSignal(readInitialState())
|
||||
const [inFlightVersion, setInFlightVersion] = createSignal(0)
|
||||
|
||||
const inFlight = new Set<string>()
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue