From 6a16dd8f981913b84c6ec5c1fac8c55bcbc71592 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 2 Dec 2025 23:53:34 +0000 Subject: [PATCH] align permission attachments with SSE stream --- packages/ui/src/components/tool-call.tsx | 10 +- packages/ui/src/stores/instances.ts | 149 +----------------- .../src/stores/message-v2/instance-store.ts | 17 +- .../ui/src/stores/message-v2/normalizers.ts | 26 +++ packages/ui/src/stores/session-api.ts | 3 +- packages/ui/src/stores/session-events.ts | 8 +- 6 files changed, 53 insertions(+), 160 deletions(-) diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index c274df79..0fbaf910 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -208,17 +208,19 @@ export default function ToolCall(props: ToolCallProps) { const { isDark } = useTheme() const toolCallMemo = createMemo(() => props.toolCall) const toolName = createMemo(() => toolCallMemo()?.tool || "") - const toolCallId = () => props.toolCallId || toolCallMemo()?.id || "" + const toolCallIdentifier = createMemo(() => toolCallMemo()?.callID || props.toolCallId || toolCallMemo()?.id || "") const toolState = createMemo(() => toolCallMemo()?.state) - const store = createMemo(() => messageStoreBus.getOrCreate(props.instanceId)) const cacheContext = createMemo(() => ({ - toolCallId: toolCallId(), + toolCallId: toolCallIdentifier(), messageId: props.messageId, partId: toolCallMemo()?.id ?? null, })) + const store = createMemo(() => messageStoreBus.getOrCreate(props.instanceId)) + const createVariantCache = (variant: string) => + useGlobalCache({ instanceId: () => props.instanceId, sessionId: () => props.sessionId, @@ -232,7 +234,7 @@ export default function ToolCall(props: ToolCallProps) { const diffCache = createVariantCache("diff") const permissionDiffCache = createVariantCache("permission-diff") const markdownCache = createVariantCache("markdown") - const permissionState = createMemo(() => store().getPermissionState(props.messageId, toolCallMemo()?.id)) + const permissionState = createMemo(() => store().getPermissionState(props.messageId, toolCallIdentifier())) const pendingPermission = createMemo(() => { const state = permissionState() if (state) { diff --git a/packages/ui/src/stores/instances.ts b/packages/ui/src/stores/instances.ts index 89e571ec..e6ae9def 100644 --- a/packages/ui/src/stores/instances.ts +++ b/packages/ui/src/stores/instances.ts @@ -1,8 +1,6 @@ import { createSignal } from "solid-js" -import { produce } from "solid-js/store" import type { Instance, LogEntry } from "../types/instance" import type { LspStatus, Permission } from "@opencode-ai/sdk" -import type { ClientPart } from "../types/message" import { sdkManager } from "../lib/sdk-manager" import { sseManager } from "../lib/sse-manager" import { serverApi } from "../lib/api-client" @@ -21,10 +19,9 @@ import { setSessionPendingPermission } from "./session-state" import { setHasInstances } from "./ui" import { messageStoreBus } from "./message-v2/bus" import { clearCacheForInstance } from "../lib/global-cache" -import type { MessageRecord } from "./message-v2/types" - const [instances, setInstances] = createSignal>(new Map()) + const [activeInstanceId, setActiveInstanceId] = createSignal(null) const [instanceLogs, setInstanceLogs] = createSignal>(new Map()) const [logStreamingState, setLogStreamingState] = createSignal>(new Map()) @@ -461,17 +458,6 @@ function addPermissionToQueue(instanceId: string, permission: Permission): void const sessionId = getPermissionSessionId(permission) incrementSessionPendingCount(instanceId, sessionId) setSessionPendingPermission(instanceId, sessionId, true) - - const isActive = getActivePermission(instanceId)?.id === permission.id - attachPermissionToToolPart(instanceId, permission, isActive) -} - -function getActivePermission(instanceId: string): Permission | null { - const activeId = activePermissionId().get(instanceId) - if (!activeId) return null - - const queue = getPermissionQueue(instanceId) - return queue.find(p => p.id === activeId) ?? null } function removePermissionFromQueue(instanceId: string, permissionId: string): void { @@ -512,16 +498,10 @@ function removePermissionFromQueue(instanceId: string, permissionId: string): vo const removed = removedPermission if (removed) { - clearPermissionFromToolPart(instanceId, removed) const removedSessionId = getPermissionSessionId(removed) const remaining = decrementSessionPendingCount(instanceId, removedSessionId) setSessionPendingPermission(instanceId, removedSessionId, remaining > 0) } - - const nextActivePermission = getActivePermission(instanceId) - if (nextActivePermission) { - attachPermissionToToolPart(instanceId, nextActivePermission, true) - } } function clearPermissionQueue(instanceId: string): void { @@ -542,131 +522,6 @@ function getPermissionSessionId(permission: Permission): string { return (permission as any).sessionID } -function getPermissionMessageId(permission: Permission): string | undefined { - return (permission as any).messageID ?? (permission as any).messageId ?? undefined -} - -function getPermissionCallIdentifier(permission: Permission): string | undefined { - return ( - (permission as any).callID ?? - (permission as any).callId ?? - (permission as any).toolCallID ?? - (permission as any).toolCallId ?? - undefined - ) -} - -function findToolPartForPermission(record: MessageRecord, permission: Permission): { partId: string; part: ClientPart } | null { - const expectedCallId = getPermissionCallIdentifier(permission) - const permissionId = permission.id - const permissionMessageId = getPermissionMessageId(permission) - - for (const partId of record.partIds) { - const entry = record.parts[partId] - if (!entry) continue - const part = entry.data - if (!part || part.type !== "tool") continue - const toolCallId = (part as any).callID ?? (part as any).callId - const partMessageId = (part as any).messageID ?? (part as any).messageId - - if (expectedCallId) { - if (toolCallId === expectedCallId) { - return { partId, part } - } - if (!toolCallId && (part.id === expectedCallId || (permissionMessageId && partMessageId === permissionMessageId))) { - return { partId, part } - } - continue - } - - if ( - (toolCallId && toolCallId === permissionId) || - part.id === permissionId || - (permissionMessageId && partMessageId === permissionMessageId) - ) { - return { partId, part } - } - } - return null -} - -function mutateToolPartPermission( - instanceId: string, - permission: Permission, - mutator: (part: ClientPart) => boolean, -): void { - const messageId = getPermissionMessageId(permission) - if (!messageId) return - const store = messageStoreBus.getOrCreate(instanceId) - const messageRecord = store.getMessage(messageId) - if (!messageRecord) return - const targetPart = findToolPartForPermission(messageRecord, permission) - if (!targetPart) return - - store.setState( - "messages", - messageId, - produce((draft: MessageRecord) => { - const partRecord = draft.parts[targetPart.partId] - if (!partRecord) return - const changed = mutator(partRecord.data) - if (!changed) return - const nextVersion = typeof partRecord.data.version === "number" ? partRecord.data.version + 1 : 1 - partRecord.data.version = nextVersion - partRecord.revision += 1 - draft.revision += 1 - draft.updatedAt = Date.now() - }), - ) - - // Permission attachment/removal can change the rendered height of the - // message list (e.g., permission blocks or diffs), so bump the - // session revision to ensure auto-scroll reacts. - if (messageRecord.sessionId) { - store.setState("sessionRevisions", messageRecord.sessionId, (value: number = 0) => value + 1) - } -} - -function attachPermissionToToolPart(instanceId: string, permission: Permission, active: boolean): void { - mutateToolPartPermission(instanceId, permission, (part) => { - const existing = part.pendingPermission - if (existing && existing.permission.id === permission.id && existing.active === active) { - return false - } - part.pendingPermission = { permission, active } - return true - }) -} - -function clearPermissionFromToolPart(instanceId: string, permission: Permission): void { - mutateToolPartPermission(instanceId, permission, (part) => { - if (!part.pendingPermission || part.pendingPermission.permission.id !== permission.id) { - return false - } - delete part.pendingPermission - return true - }) -} - -function refreshPermissionsForSession(instanceId: string, sessionId: string): void { - const queue = getPermissionQueue(instanceId) - if (queue.length === 0) { - setSessionPendingPermission(instanceId, sessionId, false) - return - } - - const activeId = activePermissionId().get(instanceId) - - for (const permission of queue) { - if (getPermissionSessionId(permission) !== sessionId) continue - const isActive = permission.id === activeId - attachPermissionToToolPart(instanceId, permission, isActive) - } - - const pendingCount = permissionSessionCounts.get(instanceId)?.get(sessionId) ?? 0 - setSessionPendingPermission(instanceId, sessionId, pendingCount > 0) -} - async function sendPermissionResponse( instanceId: string, sessionId: string, @@ -768,10 +623,8 @@ export { getPermissionQueue, getPermissionQueueLength, addPermissionToQueue, - getActivePermission, removePermissionFromQueue, clearPermissionQueue, - refreshPermissionsForSession, sendPermissionResponse, disconnectedInstance, acknowledgeDisconnectedInstance, diff --git a/packages/ui/src/stores/message-v2/instance-store.ts b/packages/ui/src/stores/message-v2/instance-store.ts index 111d256b..19509be8 100644 --- a/packages/ui/src/stores/message-v2/instance-store.ts +++ b/packages/ui/src/stores/message-v2/instance-store.ts @@ -40,7 +40,22 @@ function ensurePartId(messageId: string, part: ClientPart, index: number): strin if (typeof part.id === "string" && part.id.length > 0) { return part.id } - return `${messageId}-part-${index}` + + const toolCallId = + (part as any).callID ?? + (part as any).callId ?? + (part as any).toolCallID ?? + (part as any).toolCallId ?? + undefined + + if (part.type === "tool" && typeof toolCallId === "string" && toolCallId.length > 0) { + part.id = toolCallId + return toolCallId + } + + const fallbackId = `${messageId}-part-${index}` + part.id = fallbackId + return fallbackId } const PENDING_PART_MAX_AGE_MS = 30_000 diff --git a/packages/ui/src/stores/message-v2/normalizers.ts b/packages/ui/src/stores/message-v2/normalizers.ts index c88296d6..b249fb4c 100644 --- a/packages/ui/src/stores/message-v2/normalizers.ts +++ b/packages/ui/src/stores/message-v2/normalizers.ts @@ -26,11 +26,37 @@ function decodeTextSegment(segment: any): any { return segment } +function deriveToolPartId(part: any): string | undefined { + if (!part || typeof part !== "object") { + return undefined + } + if (part.type !== "tool") { + return undefined + } + const callId = + part.callID ?? + part.callId ?? + part.toolCallID ?? + part.toolCallId ?? + undefined + if (typeof callId === "string" && callId.length > 0) { + return callId + } + return undefined +} + export function normalizeMessagePart(part: any): any { if (!part || typeof part !== "object") { return part } + if ((typeof part.id !== "string" || part.id.length === 0) && part.type === "tool") { + const inferredId = deriveToolPartId(part) + if (inferredId) { + part = { ...part, id: inferredId } + } + } + if (part.type !== "text") { return part } diff --git a/packages/ui/src/stores/session-api.ts b/packages/ui/src/stores/session-api.ts index ab213316..818e39f8 100644 --- a/packages/ui/src/stores/session-api.ts +++ b/packages/ui/src/stores/session-api.ts @@ -1,7 +1,7 @@ import type { Session } from "../types/session" import type { Message } from "../types/message" -import { instances, refreshPermissionsForSession } from "./instances" +import { instances } from "./instances" import { preferences, setAgentModelPreference } from "./preferences" import { setSessionCompactionState } from "./session-compaction" import { @@ -618,7 +618,6 @@ async function loadMessages(instanceId: string, sessionId: string, force = false } updateSessionInfo(instanceId, sessionId) - refreshPermissionsForSession(instanceId, sessionId) } export { diff --git a/packages/ui/src/stores/session-events.ts b/packages/ui/src/stores/session-events.ts index 5c23cb85..5ae404f8 100644 --- a/packages/ui/src/stores/session-events.ts +++ b/packages/ui/src/stores/session-events.ts @@ -16,7 +16,7 @@ import type { import type { MessageStatus } from "./message-v2/types" import { showToastNotification, ToastVariant } from "../lib/notifications" -import { instances, addPermissionToQueue, removePermissionFromQueue, refreshPermissionsForSession } from "./instances" +import { instances, addPermissionToQueue, removePermissionFromQueue } from "./instances" import { showAlertDialog } from "./alerts" import { sessions, @@ -129,7 +129,6 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes updateSessionInfo(instanceId, sessionId) - refreshPermissionsForSession(instanceId, sessionId) } else if (event.type === "message.updated") { const info = event.properties?.info if (!info) return @@ -171,13 +170,12 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes upsertMessageInfoV2(instanceId, info, { status, bumpRevision: true }) updateSessionInfo(instanceId, sessionId) - refreshPermissionsForSession(instanceId, sessionId) } - } - +} function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): void { const info = event.properties?.info + if (!info) return const compactingFlag = info.time?.compacting