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

7
package-lock.json generated
View File

@@ -1090,7 +1090,10 @@
} }
}, },
"node_modules/@opencode-ai/sdk": { "node_modules/@opencode-ai/sdk": {
"version": "1.0.166" "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.1.1.tgz",
"integrity": "sha512-PfXujMrHGeMnpS8Gd2BXSY+zZajlztcAvcokf06NtAhd0Mbo/hCLXgW0NBCQ+3FX3e/G2PNwz2DqMdtzyIZaCQ==",
"license": "MIT"
}, },
"node_modules/@pinojs/redact": { "node_modules/@pinojs/redact": {
"version": "0.4.0", "version": "0.4.0",
@@ -7476,7 +7479,7 @@
"dependencies": { "dependencies": {
"@git-diff-view/solid": "^0.0.8", "@git-diff-view/solid": "^0.0.8",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@opencode-ai/sdk": "1.0.166", "@opencode-ai/sdk": "1.1.1",
"@solidjs/router": "^0.13.0", "@solidjs/router": "^0.13.0",
"@suid/icons-material": "^0.9.0", "@suid/icons-material": "^0.9.0",
"@suid/material": "^0.19.0", "@suid/material": "^0.19.0",

View File

@@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"@git-diff-view/solid": "^0.0.8", "@git-diff-view/solid": "^0.0.8",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@opencode-ai/sdk": "1.0.166", "@opencode-ai/sdk": "1.1.1",
"@solidjs/router": "^0.13.0", "@solidjs/router": "^0.13.0",
"@suid/icons-material": "^0.9.0", "@suid/icons-material": "^0.9.0",
"@suid/material": "^0.19.0", "@suid/material": "^0.19.0",

View File

@@ -90,9 +90,9 @@ const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) =>
setPendingMcpAction(serverName, action) setPendingMcpAction(serverName, action)
try { try {
if (shouldEnable) { if (shouldEnable) {
await client.mcp.connect({ path: { name: serverName } }) await client.mcp.connect({ name: serverName })
} else { } else {
await client.mcp.disconnect({ path: { name: serverName } }) await client.mcp.disconnect({ name: serverName })
} }
await refreshMetadata() await refreshMetadata()
} catch (error) { } catch (error) {

View File

@@ -10,6 +10,7 @@ import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setAc
import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status" import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
import { showAlertDialog } from "../../stores/alerts" import { showAlertDialog } from "../../stores/alerts"
import { getLogger } from "../../lib/logger" import { getLogger } from "../../lib/logger"
import { requestData } from "../../lib/opencode-api"
const log = getLogger("session") const log = getLogger("session")
@@ -122,10 +123,13 @@ export const SessionView: Component<SessionViewProps> = (props) => {
if (!instance || !instance.client) return if (!instance || !instance.client) return
try { try {
await instance.client.session.revert({ await requestData(
path: { id: props.sessionId }, instance.client.session.revert({
body: { messageID: messageId }, sessionID: props.sessionId,
}) messageID: messageId,
}),
"session.revert",
)
const restoredText = getUserMessageText(messageId) const restoredText = getUserMessageText(messageId)
if (restoredText) { if (restoredText) {

View File

@@ -1,6 +1,6 @@
import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js" import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js"
import type { Agent } from "../types/session" import type { Agent } from "../types/session"
import type { OpencodeClient } from "@opencode-ai/sdk/client" import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import { serverApi } from "../lib/api-client" import { serverApi } from "../lib/api-client"
import { getLogger } from "../lib/logger" import { getLogger } from "../lib/logger"
const log = getLogger("actions") const log = getLogger("actions")

View File

@@ -18,6 +18,7 @@ import type { MessageRecord } from "../../stores/message-v2/types"
import { messageStoreBus } from "../../stores/message-v2/bus" import { messageStoreBus } from "../../stores/message-v2/bus"
import { cleanupBlankSessions } from "../../stores/session-state" import { cleanupBlankSessions } from "../../stores/session-state"
import { getLogger } from "../logger" import { getLogger } from "../logger"
import { requestData } from "../opencode-api"
import { emitSessionSidebarRequest } from "../session-sidebar-events" import { emitSessionSidebarRequest } from "../session-sidebar-events"
const log = getLogger("actions") const log = getLogger("actions")
@@ -241,13 +242,14 @@ export function useCommands(options: UseCommandsOptions) {
try { try {
setSessionCompactionState(instance.id, sessionId, true) setSessionCompactionState(instance.id, sessionId, true)
await instance.client.session.summarize({ await requestData(
path: { id: sessionId }, instance.client.session.summarize({
body: { sessionID: sessionId,
providerID: session.model.providerId, providerID: session.model.providerId,
modelID: session.model.modelId, modelID: session.model.modelId,
}, }),
}) "session.summarize",
)
} catch (error) { } catch (error) {
setSessionCompactionState(instance.id, sessionId, false) setSessionCompactionState(instance.id, sessionId, false)
log.error("Failed to compact session", error) log.error("Failed to compact session", error)
@@ -332,10 +334,13 @@ export function useCommands(options: UseCommandsOptions) {
} }
try { try {
await instance.client.session.revert({ await requestData(
path: { id: sessionId }, instance.client.session.revert({
body: { messageID }, sessionID: sessionId,
}) messageID,
}),
"session.revert",
)
if (!restoredText) { if (!restoredText) {
const fallbackRecord = store.getMessage(messageID) const fallbackRecord = store.getMessage(messageID)

View File

@@ -0,0 +1,37 @@
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
export class OpencodeApiError extends Error {
constructor(message: string, options?: { cause?: unknown }) {
super(message)
this.name = "OpencodeApiError"
if (options && "cause" in options) {
;(this as any).cause = options.cause
}
}
}
type RequestResultLike<T> =
| {
data: T
error?: undefined
}
| {
data?: undefined
error: unknown
}
export async function requestData<T>(
promise: Promise<RequestResultLike<T> | undefined>,
label: string,
): Promise<T> {
const result = await promise
if (!result) {
throw new OpencodeApiError(`${label} returned no result`)
}
if ((result as any).error) {
throw new OpencodeApiError(`${label} failed`, { cause: (result as any).error })
}
return (result as any).data as T
}
export type { OpencodeClient }

View File

@@ -1,18 +1,20 @@
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/client" import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client"
import { CODENOMAD_API_BASE } from "./api-client" import { CODENOMAD_API_BASE } from "./api-client"
class SDKManager { class SDKManager {
private clients = new Map<string, OpencodeClient>() private clients = new Map<string, OpencodeClient>()
createClient(instanceId: string, proxyPath: string): OpencodeClient { createClient(instanceId: string, proxyPath: string): OpencodeClient {
if (this.clients.has(instanceId)) { const existing = this.clients.get(instanceId)
return this.clients.get(instanceId)! if (existing) {
return existing
} }
const baseUrl = buildInstanceBaseUrl(proxyPath) const baseUrl = buildInstanceBaseUrl(proxyPath)
const client = createOpencodeClient({ baseUrl }) const client = createOpencodeClient({ baseUrl })
this.clients.set(instanceId, client) this.clients.set(instanceId, client)
return client return client
} }
@@ -29,6 +31,8 @@ class SDKManager {
} }
} }
export type { OpencodeClient }
function buildInstanceBaseUrl(proxyPath: string): string { function buildInstanceBaseUrl(proxyPath: string): string {
const normalized = normalizeProxyPath(proxyPath) const normalized = normalizeProxyPath(proxyPath)
const base = stripTrailingSlashes(CODENOMAD_API_BASE) const base = stripTrailingSlashes(CODENOMAD_API_BASE)

View File

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

View File

@@ -1,8 +1,9 @@
import { createSignal } from "solid-js" import { createSignal } from "solid-js"
import type { Instance, LogEntry } from "../types/instance" 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 type { PermissionReply, PermissionRequestLike } from "../types/permission"
import { getPermissionCreatedAt, getPermissionSessionId } from "../types/permission" import { getPermissionCreatedAt, getPermissionSessionId } from "../types/permission"
import { requestData } from "../lib/opencode-api"
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"
@@ -20,6 +21,7 @@ import { preferences } from "./preferences"
import { setSessionPendingPermission } from "./session-state" 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 { upsertPermissionV2, removePermissionV2 } from "./message-v2/bridge"
import { clearCacheForInstance } from "../lib/global-cache" import { clearCacheForInstance } from "../lib/global-cache"
import { getLogger } from "../lib/logger" import { getLogger } from "../lib/logger"
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata" import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
@@ -123,6 +125,37 @@ function releaseInstanceResources(instanceId: string) {
sseManager.seedStatus(instanceId, "disconnected") 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) { async function hydrateInstanceData(instanceId: string) {
try { try {
await fetchSessions(instanceId) await fetchSessions(instanceId)
@@ -132,6 +165,7 @@ async function hydrateInstanceData(instanceId: string) {
const instance = instances().get(instanceId) const instance = instances().get(instanceId)
if (!instance?.client) return if (!instance?.client) return
await fetchCommands(instanceId, instance.client) await fetchCommands(instanceId, instance.client)
await syncPendingPermissions(instanceId)
} catch (error) { } catch (error) {
log.error("Failed to fetch initial data", error) log.error("Failed to fetch initial data", error)
} }
@@ -349,8 +383,7 @@ async function fetchLspStatus(instanceId: string): Promise<LspStatus[] | undefin
return undefined return undefined
} }
log.info("lsp.status", { instanceId }) log.info("lsp.status", { instanceId })
const response = await lsp.status() return await requestData<LspStatus[]>(lsp.status(), "lsp.status")
return response.data ?? []
} }
function getActiveInstance(): Instance | null { function getActiveInstance(): Instance | null {
@@ -540,39 +573,14 @@ async function sendPermissionResponse(
throw new Error("Instance not ready") throw new Error("Instance not ready")
} }
const client: any = instance.client
try { try {
// New API (preferred): POST /permission/:requestID/reply await requestData(
if (typeof client.postPermissionRequestIdReply === "function") { instance.client.permission.reply({
await client.postPermissionRequestIdReply({ requestID: requestId,
path: { requestID: requestId }, reply,
body: { reply }, }),
}) "permission.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 // Remove from queue after successful response
removePermissionFromQueue(instanceId, requestId) removePermissionFromQueue(instanceId, requestId)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import type { OpencodeClient } from "@opencode-ai/sdk/client" import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { LspStatus, Project as SDKProject } from "@opencode-ai/sdk" import type { LspStatus, Project as SDKProject } from "@opencode-ai/sdk/v2"
export interface LogEntry { export interface LogEntry {
timestamp: number timestamp: number