Migrate UI to v2 SDK client

Use v2 OpencodeClient with normalized request handling and rehydrate pending permissions via GET /permission on instance hydration.
This commit is contained in:
Shantur Rathore
2026-01-04 22:01:49 +00:00
parent fcb5998474
commit 1377bc6b91
15 changed files with 186 additions and 117 deletions

View File

@@ -1,12 +1,12 @@
import { createSignal } from "solid-js"
import type { Command as SDKCommand } from "@opencode-ai/sdk"
import type { OpencodeClient } from "@opencode-ai/sdk/client"
import type { Command as SDKCommand } from "@opencode-ai/sdk/v2"
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import { requestData } from "../lib/opencode-api"
const [commandMap, setCommandMap] = createSignal<Map<string, SDKCommand[]>>(new Map())
export async function fetchCommands(instanceId: string, client: OpencodeClient): Promise<void> {
const response = await client.command.list()
const commands = response.data ?? []
const commands = await requestData<SDKCommand[]>(client.command.list(), "command.list").catch(() => [])
setCommandMap((prev) => {
const next = new Map(prev)
next.set(instanceId, commands)

View File

@@ -1,8 +1,9 @@
import { createSignal } from "solid-js"
import type { Instance, LogEntry } from "../types/instance"
import type { LspStatus } from "@opencode-ai/sdk"
import type { LspStatus } from "@opencode-ai/sdk/v2"
import type { PermissionReply, PermissionRequestLike } from "../types/permission"
import { getPermissionCreatedAt, getPermissionSessionId } from "../types/permission"
import { requestData } from "../lib/opencode-api"
import { sdkManager } from "../lib/sdk-manager"
import { sseManager } from "../lib/sse-manager"
import { serverApi } from "../lib/api-client"
@@ -20,6 +21,7 @@ import { preferences } from "./preferences"
import { setSessionPendingPermission } from "./session-state"
import { setHasInstances } from "./ui"
import { messageStoreBus } from "./message-v2/bus"
import { upsertPermissionV2, removePermissionV2 } from "./message-v2/bridge"
import { clearCacheForInstance } from "../lib/global-cache"
import { getLogger } from "../lib/logger"
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
@@ -123,6 +125,37 @@ function releaseInstanceResources(instanceId: string) {
sseManager.seedStatus(instanceId, "disconnected")
}
async function syncPendingPermissions(instanceId: string): Promise<void> {
const instance = instances().get(instanceId)
if (!instance?.client) return
try {
const remote = await requestData<PermissionRequestLike[]>(
instance.client.permission.list(),
"permission.list",
)
const remoteIds = new Set(remote.map((item) => item.id))
const local = getPermissionQueue(instanceId)
// Remove any stale local permissions missing from server.
for (const entry of local) {
if (!remoteIds.has(entry.id)) {
removePermissionFromQueue(instanceId, entry.id)
removePermissionV2(instanceId, entry.id)
}
}
// Upsert all server-side pending permissions.
for (const permission of remote) {
addPermissionToQueue(instanceId, permission)
upsertPermissionV2(instanceId, permission)
}
} catch (error) {
log.warn("Failed to sync pending permissions", { instanceId, error })
}
}
async function hydrateInstanceData(instanceId: string) {
try {
await fetchSessions(instanceId)
@@ -132,6 +165,7 @@ async function hydrateInstanceData(instanceId: string) {
const instance = instances().get(instanceId)
if (!instance?.client) return
await fetchCommands(instanceId, instance.client)
await syncPendingPermissions(instanceId)
} catch (error) {
log.error("Failed to fetch initial data", error)
}
@@ -349,8 +383,7 @@ async function fetchLspStatus(instanceId: string): Promise<LspStatus[] | undefin
return undefined
}
log.info("lsp.status", { instanceId })
const response = await lsp.status()
return response.data ?? []
return await requestData<LspStatus[]>(lsp.status(), "lsp.status")
}
function getActiveInstance(): Instance | null {
@@ -540,39 +573,14 @@ async function sendPermissionResponse(
throw new Error("Instance not ready")
}
const client: any = instance.client
try {
// 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")
}
await requestData(
instance.client.permission.reply({
requestID: requestId,
reply,
}),
"permission.reply",
)
// Remove from queue after successful response
removePermissionFromQueue(instanceId, requestId)

View File

@@ -7,6 +7,7 @@ import { getDefaultModel, isModelValid } from "./session-models"
import { updateSessionInfo } from "./message-v2/session-info"
import { messageStoreBus } from "./message-v2/bus"
import { getLogger } from "../lib/logger"
import { requestData } from "../lib/opencode-api"
const log = getLogger("actions")
@@ -179,17 +180,13 @@ async function sendMessage(
try {
log.info("session.promptAsync", { instanceId, sessionId, requestBody })
const response = await instance.client.session.promptAsync({
path: { id: sessionId },
body: requestBody,
})
log.info("sendMessage response", response)
if (response.error) {
log.error("sendMessage server error", response.error)
throw new Error(JSON.stringify(response.error) || "Failed to send message")
}
await requestData(
instance.client.session.promptAsync({
sessionID: sessionId,
...(requestBody as any),
}),
"session.promptAsync",
)
} catch (error) {
log.error("Failed to send prompt", error)
throw error
@@ -232,10 +229,13 @@ async function executeCustomCommand(
body.model = `${session.model.providerId}/${session.model.modelId}`
}
await instance.client.session.command({
path: { id: sessionId },
body,
})
await requestData(
instance.client.session.command({
sessionID: sessionId,
...(body as any),
}),
"session.command",
)
}
async function runShellCommand(instanceId: string, sessionId: string, command: string): Promise<void> {
@@ -251,13 +251,14 @@ async function runShellCommand(instanceId: string, sessionId: string, command: s
const agent = session.agent || "build"
await instance.client.session.shell({
path: { id: sessionId },
body: {
await requestData(
instance.client.session.shell({
sessionID: sessionId,
agent,
command,
},
})
}),
"session.shell",
)
}
async function abortSession(instanceId: string, sessionId: string): Promise<void> {
@@ -270,9 +271,12 @@ async function abortSession(instanceId: string, sessionId: string): Promise<void
try {
log.info("session.abort", { instanceId, sessionId })
await instance.client.session.abort({
path: { id: sessionId },
})
await requestData(
instance.client.session.abort({
sessionID: sessionId,
}),
"session.abort",
)
log.info("abortSession complete", { instanceId, sessionId })
} catch (error) {
log.error("Failed to abort session", error)
@@ -350,10 +354,13 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s
throw new Error("Session title is required")
}
await instance.client.session.update({
path: { id: sessionId },
body: { title: trimmedTitle },
})
await requestData(
instance.client.session.update({
sessionID: sessionId,
title: trimmedTitle,
}),
"session.update",
)
withSession(instanceId, sessionId, (current) => {
current.title = trimmedTitle

View File

@@ -33,6 +33,7 @@ import { seedSessionMessagesV2 } from "./message-v2/bridge"
import { messageStoreBus } from "./message-v2/bus"
import { clearCacheForSession } from "../lib/global-cache"
import { getLogger } from "../lib/logger"
import { requestData } from "../lib/opencode-api"
const log = getLogger("api")
@@ -269,25 +270,16 @@ async function forkSession(
throw new Error("Instance not ready")
}
const request: {
path: { id: string }
body?: { messageID: string }
} = {
path: { id: sourceSessionId },
}
if (options?.messageId) {
request.body = { messageID: options.messageId }
const request: { sessionID: string; messageID?: string } = {
sessionID: sourceSessionId,
messageID: options?.messageId,
}
log.info(`[HTTP] POST /session.fork for instance ${instanceId}`, request)
const response = await instance.client.session.fork(request)
if (!response.data) {
throw new Error("Failed to fork session: No data returned")
}
const info = response.data as SessionForkResponse
const info = await requestData<SessionForkResponse>(
instance.client.session.fork(request),
"session.fork",
)
const forkedSession = {
id: info.id,
instanceId,
@@ -365,7 +357,7 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
try {
log.info(`[HTTP] DELETE /session.delete for instance ${instanceId}`, { sessionId })
await instance.client.session.delete({ path: { id: sessionId } })
await requestData(instance.client.session.delete({ sessionID: sessionId }), "session.delete")
setSessions((prev) => {
const next = new Map(prev)
@@ -528,14 +520,17 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
try {
log.info(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId })
const response = await instance.client.session["messages"]({ path: { id: sessionId } })
const apiMessages = await requestData<any[]>(
instance.client.session.messages({ sessionID: sessionId }),
"session.messages",
)
if (!response.data || !Array.isArray(response.data)) {
if (!Array.isArray(apiMessages) || apiMessages.length === 0) {
return
}
const messagesInfo = new Map<string, any>()
const messages: Message[] = response.data.map((apiMessage: any) => {
const messages: Message[] = apiMessages.map((apiMessage: any) => {
const info = apiMessage.info || apiMessage
const role = info.role || "assistant"
const messageId = info.id || String(Date.now())
@@ -561,8 +556,8 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
let providerID = ""
let modelID = ""
for (let i = response.data.length - 1; i >= 0; i--) {
const apiMessage = response.data[i]
for (let i = apiMessages.length - 1; i >= 0; i--) {
const apiMessage = apiMessages[i]
const info = apiMessage.info || apiMessage
if (info.role === "assistant") {

View File

@@ -15,6 +15,7 @@ import type {
import type { MessageStatus } from "./message-v2/types"
import { getLogger } from "../lib/logger"
import { requestData } from "../lib/opencode-api"
import { getPermissionId, getPermissionKind, getRequestIdFromPermissionReply } from "../types/permission"
import type { PermissionReplyEventPropertiesLike, PermissionRequestLike } from "../types/permission"
import { showToastNotification, ToastVariant } from "../lib/notifications"
@@ -79,10 +80,12 @@ async function fetchSessionInfo(instanceId: string, sessionId: string): Promise<
if (!instance?.client) return null
try {
const response = await instance.client.session.get({ path: { id: sessionId } })
if (!response.data) return null
const info = await requestData<any>(
instance.client.session.get({ sessionID: sessionId }),
"session.get",
)
const fetched = createClientSession(response.data, instanceId)
const fetched = createClientSession(info, instanceId)
setSessions((prev) => {
const next = new Map(prev)

View File

@@ -7,6 +7,7 @@ import { messageStoreBus } from "./message-v2/bus"
import { instances } from "./instances"
import { showConfirmDialog } from "./alerts"
import { getLogger } from "../lib/logger"
import { requestData } from "../lib/opencode-api"
const log = getLogger("session")
@@ -283,8 +284,10 @@ async function isBlankSession(session: Session, instanceId: string, fetchIfNeede
}
let messages: any[] = []
try {
const response = await instance.client.session.messages({ path: { id: session.id } })
messages = response.data || []
messages = await requestData<any[]>(
instance.client.session.messages({ sessionID: session.id }),
"session.messages",
)
} catch (error) {
log.error(`Failed to fetch messages for session ${session.id}`, error)
return isFreshSession