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 { isDark } = useTheme()
|
||||||
const toolCallMemo = createMemo(() => props.toolCall)
|
const toolCallMemo = createMemo(() => props.toolCall)
|
||||||
const toolName = createMemo(() => toolCallMemo()?.tool || "")
|
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 toolState = createMemo(() => toolCallMemo()?.state)
|
||||||
const store = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
|
||||||
|
|
||||||
const cacheContext = createMemo(() => ({
|
const cacheContext = createMemo(() => ({
|
||||||
toolCallId: toolCallId(),
|
toolCallId: toolCallIdentifier(),
|
||||||
messageId: props.messageId,
|
messageId: props.messageId,
|
||||||
partId: toolCallMemo()?.id ?? null,
|
partId: toolCallMemo()?.id ?? null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const store = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
||||||
|
|
||||||
const createVariantCache = (variant: string) =>
|
const createVariantCache = (variant: string) =>
|
||||||
|
|
||||||
useGlobalCache({
|
useGlobalCache({
|
||||||
instanceId: () => props.instanceId,
|
instanceId: () => props.instanceId,
|
||||||
sessionId: () => props.sessionId,
|
sessionId: () => props.sessionId,
|
||||||
@@ -232,7 +234,7 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const diffCache = createVariantCache("diff")
|
const diffCache = createVariantCache("diff")
|
||||||
const permissionDiffCache = createVariantCache("permission-diff")
|
const permissionDiffCache = createVariantCache("permission-diff")
|
||||||
const markdownCache = createVariantCache("markdown")
|
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 pendingPermission = createMemo(() => {
|
||||||
const state = permissionState()
|
const state = permissionState()
|
||||||
if (state) {
|
if (state) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { produce } from "solid-js/store"
|
|
||||||
import type { Instance, LogEntry } from "../types/instance"
|
import type { Instance, LogEntry } from "../types/instance"
|
||||||
import type { LspStatus, Permission } from "@opencode-ai/sdk"
|
import type { LspStatus, Permission } from "@opencode-ai/sdk"
|
||||||
import type { ClientPart } from "../types/message"
|
|
||||||
import { sdkManager } from "../lib/sdk-manager"
|
import { sdkManager } from "../lib/sdk-manager"
|
||||||
import { sseManager } from "../lib/sse-manager"
|
import { sseManager } from "../lib/sse-manager"
|
||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
@@ -21,10 +19,9 @@ import { setSessionPendingPermission } from "./session-state"
|
|||||||
import { setHasInstances } from "./ui"
|
import { setHasInstances } from "./ui"
|
||||||
import { messageStoreBus } from "./message-v2/bus"
|
import { messageStoreBus } from "./message-v2/bus"
|
||||||
import { clearCacheForInstance } from "../lib/global-cache"
|
import { clearCacheForInstance } from "../lib/global-cache"
|
||||||
import type { MessageRecord } from "./message-v2/types"
|
|
||||||
|
|
||||||
|
|
||||||
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
|
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
|
||||||
|
|
||||||
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
|
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
|
||||||
const [instanceLogs, setInstanceLogs] = createSignal<Map<string, LogEntry[]>>(new Map())
|
const [instanceLogs, setInstanceLogs] = createSignal<Map<string, LogEntry[]>>(new Map())
|
||||||
const [logStreamingState, setLogStreamingState] = createSignal<Map<string, boolean>>(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)
|
const sessionId = getPermissionSessionId(permission)
|
||||||
incrementSessionPendingCount(instanceId, sessionId)
|
incrementSessionPendingCount(instanceId, sessionId)
|
||||||
setSessionPendingPermission(instanceId, sessionId, true)
|
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 {
|
function removePermissionFromQueue(instanceId: string, permissionId: string): void {
|
||||||
@@ -512,16 +498,10 @@ function removePermissionFromQueue(instanceId: string, permissionId: string): vo
|
|||||||
|
|
||||||
const removed = removedPermission
|
const removed = removedPermission
|
||||||
if (removed) {
|
if (removed) {
|
||||||
clearPermissionFromToolPart(instanceId, removed)
|
|
||||||
const removedSessionId = getPermissionSessionId(removed)
|
const removedSessionId = getPermissionSessionId(removed)
|
||||||
const remaining = decrementSessionPendingCount(instanceId, removedSessionId)
|
const remaining = decrementSessionPendingCount(instanceId, removedSessionId)
|
||||||
setSessionPendingPermission(instanceId, removedSessionId, remaining > 0)
|
setSessionPendingPermission(instanceId, removedSessionId, remaining > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextActivePermission = getActivePermission(instanceId)
|
|
||||||
if (nextActivePermission) {
|
|
||||||
attachPermissionToToolPart(instanceId, nextActivePermission, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPermissionQueue(instanceId: string): void {
|
function clearPermissionQueue(instanceId: string): void {
|
||||||
@@ -542,131 +522,6 @@ function getPermissionSessionId(permission: Permission): string {
|
|||||||
return (permission as any).sessionID
|
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(
|
async function sendPermissionResponse(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -768,10 +623,8 @@ export {
|
|||||||
getPermissionQueue,
|
getPermissionQueue,
|
||||||
getPermissionQueueLength,
|
getPermissionQueueLength,
|
||||||
addPermissionToQueue,
|
addPermissionToQueue,
|
||||||
getActivePermission,
|
|
||||||
removePermissionFromQueue,
|
removePermissionFromQueue,
|
||||||
clearPermissionQueue,
|
clearPermissionQueue,
|
||||||
refreshPermissionsForSession,
|
|
||||||
sendPermissionResponse,
|
sendPermissionResponse,
|
||||||
disconnectedInstance,
|
disconnectedInstance,
|
||||||
acknowledgeDisconnectedInstance,
|
acknowledgeDisconnectedInstance,
|
||||||
|
|||||||
@@ -40,7 +40,22 @@ function ensurePartId(messageId: string, part: ClientPart, index: number): strin
|
|||||||
if (typeof part.id === "string" && part.id.length > 0) {
|
if (typeof part.id === "string" && part.id.length > 0) {
|
||||||
return part.id
|
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
|
const PENDING_PART_MAX_AGE_MS = 30_000
|
||||||
|
|||||||
@@ -26,11 +26,37 @@ function decodeTextSegment(segment: any): any {
|
|||||||
return segment
|
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 {
|
export function normalizeMessagePart(part: any): any {
|
||||||
if (!part || typeof part !== "object") {
|
if (!part || typeof part !== "object") {
|
||||||
return part
|
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") {
|
if (part.type !== "text") {
|
||||||
return part
|
return part
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Session } from "../types/session"
|
import type { Session } from "../types/session"
|
||||||
import type { Message } from "../types/message"
|
import type { Message } from "../types/message"
|
||||||
|
|
||||||
import { instances, refreshPermissionsForSession } from "./instances"
|
import { instances } from "./instances"
|
||||||
import { preferences, setAgentModelPreference } from "./preferences"
|
import { preferences, setAgentModelPreference } from "./preferences"
|
||||||
import { setSessionCompactionState } from "./session-compaction"
|
import { setSessionCompactionState } from "./session-compaction"
|
||||||
import {
|
import {
|
||||||
@@ -618,7 +618,6 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
refreshPermissionsForSession(instanceId, sessionId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
import type { MessageStatus } from "./message-v2/types"
|
import type { MessageStatus } from "./message-v2/types"
|
||||||
|
|
||||||
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
||||||
import { instances, addPermissionToQueue, removePermissionFromQueue, refreshPermissionsForSession } from "./instances"
|
import { instances, addPermissionToQueue, removePermissionFromQueue } from "./instances"
|
||||||
import { showAlertDialog } from "./alerts"
|
import { showAlertDialog } from "./alerts"
|
||||||
import {
|
import {
|
||||||
sessions,
|
sessions,
|
||||||
@@ -129,7 +129,6 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
|
|
||||||
|
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
refreshPermissionsForSession(instanceId, sessionId)
|
|
||||||
} else if (event.type === "message.updated") {
|
} else if (event.type === "message.updated") {
|
||||||
const info = event.properties?.info
|
const info = event.properties?.info
|
||||||
if (!info) return
|
if (!info) return
|
||||||
@@ -171,13 +170,12 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
upsertMessageInfoV2(instanceId, info, { status, bumpRevision: true })
|
upsertMessageInfoV2(instanceId, info, { status, bumpRevision: true })
|
||||||
|
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
refreshPermissionsForSession(instanceId, sessionId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): void {
|
function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): void {
|
||||||
const info = event.properties?.info
|
const info = event.properties?.info
|
||||||
|
|
||||||
if (!info) return
|
if (!info) return
|
||||||
|
|
||||||
const compactingFlag = info.time?.compacting
|
const compactingFlag = info.time?.compacting
|
||||||
|
|||||||
Reference in New Issue
Block a user