import { For, Show, Suspense, createMemo, createSignal, createEffect, lazy, onCleanup, type Component } from "solid-js" import type { PermissionRequestLike } from "../types/permission" import { getPermissionCallId, getPermissionDisplayTitle, getPermissionKind, getPermissionMessageId, getPermissionSessionId } from "../types/permission" import { getQuestionCallId, getQuestionMessageId, getQuestionSessionId, type QuestionRequest } from "../types/question" import { useI18n } from "../lib/i18n" import { activeInterruption, getPermissionQueue, getQuestionQueue, getQuestionEnqueuedAtForInstance, sendPermissionResponse, } from "../stores/instances" import { ensureSessionParentExpanded, loadMessages, sessions as sessionStateSessions, setActiveSessionFromList } from "../stores/sessions" import { messageStoreBus } from "../stores/message-v2/bus" const LazyToolCall = lazy(() => import("./tool-call")) interface PermissionApprovalModalProps { instanceId: string isOpen: boolean onClose: () => void } type ResolvedToolCall = { messageId: string sessionId: string toolPart: Extract messageVersion: number partVersion: number } function resolveToolCallFromPermission( instanceId: string, permission: PermissionRequestLike, ): ResolvedToolCall | null { const sessionId = getPermissionSessionId(permission) const messageId = getPermissionMessageId(permission) if (!sessionId || !messageId) return null const store = messageStoreBus.getInstance(instanceId) if (!store) return null const record = store.getMessage(messageId) if (!record) return null const metadata = ((permission as any).metadata || {}) as Record const directPartId = (permission as any).partID ?? (permission as any).partId ?? (metadata as any).partID ?? (metadata as any).partId ?? undefined const callId = getPermissionCallId(permission) const findToolPart = (partId: string) => { const partRecord = record.parts?.[partId] const part = partRecord?.data if (!part || part.type !== "tool") return null return { toolPart: part as ResolvedToolCall["toolPart"], partVersion: partRecord.revision ?? 0, } } if (typeof directPartId === "string" && directPartId.length > 0) { const resolved = findToolPart(directPartId) if (resolved) { return { messageId, sessionId, toolPart: resolved.toolPart, messageVersion: record.revision, partVersion: resolved.partVersion, } } } if (callId) { for (const partId of record.partIds) { const partRecord = record.parts?.[partId] const part = partRecord?.data as any if (!part || part.type !== "tool") continue const partCallId = part.callID ?? part.callId ?? part.toolCallID ?? part.toolCallId ?? undefined if (partCallId === callId && typeof part.id === "string" && part.id.length > 0) { return { messageId, sessionId, toolPart: part as ResolvedToolCall["toolPart"], messageVersion: record.revision, partVersion: partRecord.revision ?? 0, } } } } return null } function resolveToolCallFromQuestion(instanceId: string, request: QuestionRequest): ResolvedToolCall | null { const sessionId = getQuestionSessionId(request) const messageId = getQuestionMessageId(request) if (!sessionId || !messageId) return null const store = messageStoreBus.getInstance(instanceId) if (!store) return null const record = store.getMessage(messageId) if (!record) return null const callId = getQuestionCallId(request) if (!callId) return null for (const partId of record.partIds) { const partRecord = record.parts?.[partId] const part = partRecord?.data as any if (!part || part.type !== "tool") continue const partCallId = part.callID ?? part.callId ?? part.toolCallID ?? part.toolCallId ?? undefined if (partCallId !== callId) continue if (typeof part.id !== "string" || part.id.length === 0) continue return { messageId, sessionId, toolPart: part as ResolvedToolCall["toolPart"], messageVersion: record.revision, partVersion: partRecord?.revision ?? 0, } } return null } const PermissionApprovalModal: Component = (props) => { const { t } = useI18n() const [loadingSession, setLoadingSession] = createSignal(null) const [permissionSubmitting, setPermissionSubmitting] = createSignal>(new Set()) const [permissionError, setPermissionError] = createSignal>(new Map()) const setPermissionBusy = (permissionId: string, busy: boolean) => { setPermissionSubmitting((prev) => { const next = new Set(prev) if (busy) next.add(permissionId) else next.delete(permissionId) return next }) } const setPermissionItemError = (permissionId: string, message: string | null) => { setPermissionError((prev) => { const next = new Map(prev) if (!message) next.delete(permissionId) else next.set(permissionId, message) return next }) } async function handlePermissionDecision(permission: PermissionRequestLike, response: "once" | "always" | "reject") { const permissionId = permission?.id if (!permissionId) return if (permissionSubmitting().has(permissionId)) return setPermissionBusy(permissionId, true) setPermissionItemError(permissionId, null) try { const sessionId = getPermissionSessionId(permission) || "" await sendPermissionResponse(props.instanceId, sessionId, permissionId, response) } catch (error) { setPermissionItemError( permissionId, error instanceof Error ? error.message : t("permissionApproval.errors.unableToUpdatePermission"), ) } finally { setPermissionBusy(permissionId, false) } } const permissionQueue = createMemo(() => getPermissionQueue(props.instanceId)) const questionQueue = createMemo(() => getQuestionQueue(props.instanceId)) const active = createMemo(() => activeInterruption().get(props.instanceId) ?? null) type InterruptionItem = | { kind: "permission"; id: string; sessionId: string; createdAt: number; payload: PermissionRequestLike } | { kind: "question"; id: string; sessionId: string; createdAt: number; payload: QuestionRequest } const orderedQueue = createMemo(() => { const permissions = permissionQueue().map((permission) => ({ kind: "permission" as const, id: permission.id, sessionId: getPermissionSessionId(permission) || "", createdAt: (permission as any)?.time?.created ?? Date.now(), payload: permission, })) const questions = questionQueue().map((question) => ({ kind: "question" as const, id: question.id, sessionId: getQuestionSessionId(question) || "", createdAt: getQuestionEnqueuedAtForInstance(props.instanceId, question.id), payload: question, })) return [...permissions, ...questions].sort((a, b) => a.createdAt - b.createdAt) }) const hasRequests = createMemo(() => orderedQueue().length > 0) const closeOnEscape = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault() props.onClose() } } createEffect(() => { if (!props.isOpen) return document.addEventListener("keydown", closeOnEscape) onCleanup(() => document.removeEventListener("keydown", closeOnEscape)) }) createEffect(() => { if (!props.isOpen) return if (orderedQueue().length === 0) { props.onClose() } }) function handleBackdropClick(event: MouseEvent) { if (event.target === event.currentTarget) { props.onClose() } } async function handleLoadSession(sessionId: string) { if (!sessionId) return setLoadingSession(sessionId) try { await loadMessages(props.instanceId, sessionId) } finally { setLoadingSession((current) => (current === sessionId ? null : current)) } } function handleGoToSession(sessionId: string) { if (!sessionId) return const session = sessionStateSessions().get(props.instanceId)?.get(sessionId) const parentId = session?.parentId ?? session?.id if (parentId) { ensureSessionParentExpanded(props.instanceId, parentId) } setActiveSessionFromList(props.instanceId, sessionId) props.onClose() } return (
) } export default PermissionApprovalModal