align permission attachments with SSE stream
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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<Map<string, Instance>>(new Map())
|
||||
|
||||
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
|
||||
const [instanceLogs, setInstanceLogs] = createSignal<Map<string, LogEntry[]>>(new Map())
|
||||
const [logStreamingState, setLogStreamingState] = createSignal<Map<string, boolean>>(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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user