diff --git a/package-lock.json b/package-lock.json index 516e3f4f..44e8ed40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7476,7 +7476,7 @@ "dependencies": { "@git-diff-view/solid": "^0.0.8", "@kobalte/core": "0.13.11", - "@opencode-ai/sdk": "^1.0.138", + "@opencode-ai/sdk": "1.0.166", "@solidjs/router": "^0.13.0", "@suid/icons-material": "^0.9.0", "@suid/material": "^0.19.0", diff --git a/packages/ui/package.json b/packages/ui/package.json index e31c3392..98093ea6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -12,7 +12,7 @@ "dependencies": { "@git-diff-view/solid": "^0.0.8", "@kobalte/core": "0.13.11", - "@opencode-ai/sdk": "^1.0.138", + "@opencode-ai/sdk": "1.0.166", "@solidjs/router": "^0.13.0", "@suid/icons-material": "^0.9.0", "@suid/material": "^0.19.0", diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index 86762b43..6f83eb9d 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -7,6 +7,7 @@ import { useGlobalCache } from "../lib/hooks/use-global-cache" import { useConfig } from "../stores/preferences" import type { DiffViewMode } from "../stores/preferences" import { sendPermissionResponse } from "../stores/instances" +import { getPermissionDisplayTitle, getPermissionKind, getPermissionSessionId } from "../types/permission" import type { TextPart, RenderCache } from "../types/message" import { resolveToolRenderer } from "./tool-call/renderers" import type { @@ -837,7 +838,7 @@ export default function ToolCall(props: ToolCallProps) { setPermissionSubmitting(true) setPermissionError(null) try { - const sessionId = permission.sessionID || props.sessionId + const sessionId = getPermissionSessionId(permission) || props.sessionId await sendPermissionResponse(props.instanceId, sessionId, permission.id, response) } catch (error) { log.error("Failed to send permission response", error) @@ -882,11 +883,11 @@ export default function ToolCall(props: ToolCallProps) {
{active ? "Permission Required" : "Permission Queued"} - {permission.type} + {getPermissionKind(permission)}
- {permission.title} + {getPermissionDisplayTitle(permission)}
{(payload) => ( diff --git a/packages/ui/src/lib/sse-manager.ts b/packages/ui/src/lib/sse-manager.ts index fe7a13bc..847ca660 100644 --- a/packages/ui/src/lib/sse-manager.ts +++ b/packages/ui/src/lib/sse-manager.ts @@ -7,8 +7,7 @@ import { } from "../types/message" import type { EventLspUpdated, - EventPermissionReplied, - EventPermissionUpdated, + EventSessionCompacted, EventSessionError, EventSessionIdle, @@ -62,8 +61,8 @@ type SSEEvent = | EventSessionCompacted | EventSessionError | EventSessionIdle - | EventPermissionUpdated - | EventPermissionReplied + | { type: "permission.updated" | "permission.asked"; properties?: any } + | { type: "permission.replied"; properties?: any } | EventLspUpdated | TuiToastEvent | BackgroundProcessUpdatedEvent @@ -139,10 +138,11 @@ class SSEManager { this.onSessionStatus?.(instanceId, event as EventSessionStatus) break case "permission.updated": - this.onPermissionUpdated?.(instanceId, event as EventPermissionUpdated) + case "permission.asked": + this.onPermissionUpdated?.(instanceId, event as any) break case "permission.replied": - this.onPermissionReplied?.(instanceId, event as EventPermissionReplied) + this.onPermissionReplied?.(instanceId, event as any) break case "lsp.updated": this.onLspUpdated?.(instanceId, event as EventLspUpdated) @@ -176,8 +176,8 @@ class SSEManager { onTuiToast?: (instanceId: string, event: TuiToastEvent) => void onSessionIdle?: (instanceId: string, event: EventSessionIdle) => void onSessionStatus?: (instanceId: string, event: EventSessionStatus) => void - onPermissionUpdated?: (instanceId: string, event: EventPermissionUpdated) => void - onPermissionReplied?: (instanceId: string, event: EventPermissionReplied) => void + onPermissionUpdated?: (instanceId: string, event: any) => void + onPermissionReplied?: (instanceId: string, event: any) => void onLspUpdated?: (instanceId: string, event: EventLspUpdated) => void onBackgroundProcessUpdated?: (instanceId: string, event: BackgroundProcessUpdatedEvent) => void onBackgroundProcessRemoved?: (instanceId: string, event: BackgroundProcessRemovedEvent) => void diff --git a/packages/ui/src/stores/instances.ts b/packages/ui/src/stores/instances.ts index dbce02b6..fac1cead 100644 --- a/packages/ui/src/stores/instances.ts +++ b/packages/ui/src/stores/instances.ts @@ -1,6 +1,8 @@ import { createSignal } from "solid-js" import type { Instance, LogEntry } from "../types/instance" -import type { LspStatus, Permission } from "@opencode-ai/sdk" +import type { LspStatus } from "@opencode-ai/sdk" +import type { PermissionReply, PermissionRequestLike } from "../types/permission" +import { getPermissionCreatedAt, getPermissionSessionId } from "../types/permission" import { sdkManager } from "../lib/sdk-manager" import { sseManager } from "../lib/sse-manager" import { serverApi } from "../lib/api-client" @@ -31,7 +33,7 @@ const [instanceLogs, setInstanceLogs] = createSignal>(ne const [logStreamingState, setLogStreamingState] = createSignal>(new Map()) // Permission queue management per instance -const [permissionQueues, setPermissionQueues] = createSignal>(new Map()) +const [permissionQueues, setPermissionQueues] = createSignal>(new Map()) const [activePermissionId, setActivePermissionId] = createSignal>(new Map()) const permissionSessionCounts = new Map>() @@ -382,7 +384,7 @@ function clearLogs(id: string) { } // Permission management functions -function getPermissionQueue(instanceId: string): Permission[] { +function getPermissionQueue(instanceId: string): PermissionRequestLike[] { const queue = permissionQueues().get(instanceId) if (!queue) { return [] @@ -429,7 +431,7 @@ function clearSessionPendingCounts(instanceId: string): void { permissionSessionCounts.delete(instanceId) } -function addPermissionToQueue(instanceId: string, permission: Permission): void { +function addPermissionToQueue(instanceId: string, permission: PermissionRequestLike): void { let inserted = false setPermissionQueues((prev) => { @@ -440,7 +442,7 @@ function addPermissionToQueue(instanceId: string, permission: Permission): void return next } - const updatedQueue = [...queue, permission].sort((a, b) => a.time.created - b.time.created) + const updatedQueue = [...queue, permission].sort((a, b) => getPermissionCreatedAt(a) - getPermissionCreatedAt(b)) next.set(instanceId, updatedQueue) inserted = true return next @@ -459,17 +461,19 @@ function addPermissionToQueue(instanceId: string, permission: Permission): void }) const sessionId = getPermissionSessionId(permission) - incrementSessionPendingCount(instanceId, sessionId) - setSessionPendingPermission(instanceId, sessionId, true) + if (sessionId) { + incrementSessionPendingCount(instanceId, sessionId) + setSessionPendingPermission(instanceId, sessionId, true) + } } function removePermissionFromQueue(instanceId: string, permissionId: string): void { - let removedPermission: Permission | null = null + let removedPermission: PermissionRequestLike | null = null setPermissionQueues((prev) => { const next = new Map(prev) const queue = next.get(instanceId) ?? [] - const filtered: Permission[] = [] + const filtered: PermissionRequestLike[] = [] for (const item of queue) { if (item.id === permissionId) { @@ -493,7 +497,7 @@ function removePermissionFromQueue(instanceId: string, permissionId: string): vo const next = new Map(prev) const activeId = next.get(instanceId) if (activeId === permissionId) { - const nextPermission = updatedQueue.length > 0 ? (updatedQueue[0] as Permission) : null + const nextPermission = updatedQueue.length > 0 ? (updatedQueue[0] as PermissionRequestLike) : null next.set(instanceId, nextPermission?.id ?? null) } return next @@ -502,8 +506,10 @@ function removePermissionFromQueue(instanceId: string, permissionId: string): vo const removed = removedPermission if (removed) { const removedSessionId = getPermissionSessionId(removed) - const remaining = decrementSessionPendingCount(instanceId, removedSessionId) - setSessionPendingPermission(instanceId, removedSessionId, remaining > 0) + if (removedSessionId) { + const remaining = decrementSessionPendingCount(instanceId, removedSessionId) + setSessionPendingPermission(instanceId, removedSessionId, remaining > 0) + } } } @@ -521,29 +527,55 @@ function clearPermissionQueue(instanceId: string): void { clearSessionPendingCounts(instanceId) } -function getPermissionSessionId(permission: Permission): string { - return (permission as any).sessionID -} + async function sendPermissionResponse( instanceId: string, sessionId: string, - permissionId: string, - response: "once" | "always" | "reject" + requestId: string, + reply: PermissionReply ): Promise { const instance = instances().get(instanceId) if (!instance?.client) { throw new Error("Instance not ready") } + const client: any = instance.client + try { - await instance.client.postSessionIdPermissionsPermissionId({ - path: { id: sessionId, permissionID: permissionId }, - body: { response }, - }) + // New API (preferred): POST /permission/:requestID/reply + if (typeof client.postPermissionRequestIdReply === "function") { + await client.postPermissionRequestIdReply({ + path: { requestID: requestId }, + body: { reply }, + }) + } else if (typeof client.postPermissionRequestIDReply === "function") { + await client.postPermissionRequestIDReply({ + path: { requestID: requestId }, + body: { reply }, + }) + } else if (typeof client.postPermissionRequestIdReply2 === "function") { + await client.postPermissionRequestIdReply2({ + path: { requestID: requestId }, + body: { reply }, + }) + } else if (client.permission && typeof client.permission.reply === "function") { + await client.permission.reply({ + path: { requestID: requestId }, + body: { reply }, + }) + } else if (typeof client.postSessionIdPermissionsPermissionId === "function") { + // Legacy API fallback: POST /session/:sessionID/permissions/:permissionID + await client.postSessionIdPermissionsPermissionId({ + path: { id: sessionId, permissionID: requestId }, + body: { response: reply }, + }) + } else { + throw new Error("Unsupported permissions API in client") + } // Remove from queue after successful response - removePermissionFromQueue(instanceId, permissionId) + removePermissionFromQueue(instanceId, requestId) } catch (error) { log.error("Failed to send permission response", error) throw error diff --git a/packages/ui/src/stores/message-v2/bridge.ts b/packages/ui/src/stores/message-v2/bridge.ts index 7c570377..d4cec513 100644 --- a/packages/ui/src/stores/message-v2/bridge.ts +++ b/packages/ui/src/stores/message-v2/bridge.ts @@ -1,4 +1,5 @@ -import type { Permission } from "@opencode-ai/sdk" +import type { PermissionRequestLike } from "../../types/permission" +import { getPermissionCallId, getPermissionMessageId } from "../../types/permission" import type { Message, MessageInfo, ClientPart } from "../../types/message" import type { Session } from "../../types/session" import { messageStoreBus } from "./bus" @@ -107,11 +108,11 @@ export function replaceMessageIdV2(instanceId: string, oldId: string, newId: str store.replaceMessageId({ oldId, newId }) } -function extractPermissionMessageId(permission: Permission): string | undefined { - return (permission as any).messageID || (permission as any).messageId +function extractPermissionMessageId(permission: PermissionRequestLike): string | undefined { + return getPermissionMessageId(permission) } -function extractPermissionPartId(permission: Permission): string | undefined { +function extractPermissionPartId(permission: PermissionRequestLike): string | undefined { const metadata = (permission as any).metadata || {} return ( (permission as any).partID || @@ -122,17 +123,8 @@ function extractPermissionPartId(permission: Permission): string | undefined { ) } -function extractPermissionCallId(permission: Permission): string | undefined { - const metadata = (permission as any).metadata || {} - return ( - (permission as any).callID || - (permission as any).callId || - (permission as any).toolCallID || - (permission as any).toolCallId || - metadata.callID || - metadata.callId || - undefined - ) +function extractPermissionCallId(permission: PermissionRequestLike): string | undefined { + return getPermissionCallId(permission) } function resolvePartIdFromCallId(store: ReturnType, messageId?: string, callId?: string): string | undefined { @@ -155,7 +147,7 @@ function resolvePartIdFromCallId(store: ReturnType + time?: { created?: number } + + // New fields + permission?: string + patterns?: string[] + always?: string[] + tool?: PermissionToolRefLike +} + +export interface PermissionReplyEventPropertiesLike { + sessionID?: string + sessionId?: string + permissionID?: string + permissionId?: string + requestID?: string + requestId?: string + response?: PermissionReply + reply?: PermissionReply +} + +export function getPermissionId(permission: PermissionRequestLike | null | undefined): string { + return permission?.id ?? "" +} + +export function getPermissionSessionId(permission: PermissionRequestLike | null | undefined): string | undefined { + return ( + (permission as any)?.sessionID ?? + (permission as any)?.sessionId ?? + undefined + ) +} + +export function getPermissionMessageId(permission: PermissionRequestLike | null | undefined): string | undefined { + const tool = (permission as any)?.tool as PermissionToolRefLike | undefined + return ( + tool?.messageID ?? + tool?.messageId ?? + (permission as any)?.messageID ?? + (permission as any)?.messageId ?? + undefined + ) +} + +export function getPermissionCallId(permission: PermissionRequestLike | null | undefined): string | undefined { + const tool = (permission as any)?.tool as PermissionToolRefLike | undefined + const metadata = (permission as any)?.metadata || {} + return ( + tool?.callID ?? + tool?.callId ?? + (permission as any)?.callID ?? + (permission as any)?.callId ?? + (permission as any)?.toolCallID ?? + (permission as any)?.toolCallId ?? + metadata.callID ?? + metadata.callId ?? + undefined + ) +} + +export function getPermissionCreatedAt(permission: PermissionRequestLike | null | undefined): number { + const created = (permission as any)?.time?.created + return typeof created === "number" ? created : Date.now() +} + +export function getPermissionKind(permission: PermissionRequestLike | null | undefined): string { + return ( + (permission as any)?.permission ?? + (permission as any)?.type ?? + "permission" + ) +} + +export function getPermissionPatterns(permission: PermissionRequestLike | null | undefined): string[] { + const patterns = (permission as any)?.patterns + if (Array.isArray(patterns)) { + return patterns.filter((value) => typeof value === "string") + } + const pattern = (permission as any)?.pattern + if (typeof pattern === "string" && pattern.length > 0) { + return [pattern] + } + return [] +} + +export function getPermissionDisplayTitle(permission: PermissionRequestLike | null | undefined): string { + const title = (permission as any)?.title + if (typeof title === "string" && title.trim().length > 0) { + return title + } + + const kind = getPermissionKind(permission) + const patterns = getPermissionPatterns(permission) + if (patterns.length > 0) { + return `${kind}: ${patterns.join(", ")}` + } + return kind +} + +export function getRequestIdFromPermissionReply(properties: PermissionReplyEventPropertiesLike | null | undefined): string | undefined { + return ( + (properties as any)?.requestID ?? + (properties as any)?.requestId ?? + (properties as any)?.permissionID ?? + (properties as any)?.permissionId ?? + undefined + ) +}