Add CLI server and move UI to HTTP API

This commit is contained in:
Shantur Rathore
2025-11-17 18:18:45 +00:00
parent 89bd32814f
commit 08d81f8bb5
40 changed files with 3153 additions and 462 deletions

View File

@@ -4,6 +4,9 @@ import type { LspStatus, Permission } from "@opencode-ai/sdk"
import type { ClientPart, Message } from "../types/message"
import { sdkManager } from "../lib/sdk-manager"
import { sseManager } from "../lib/sse-manager"
import { cliApi } from "../lib/api-client"
import { cliEvents } from "../lib/cli-events"
import type { WorkspaceDescriptor, WorkspaceEventPayload, WorkspaceLogEntry } from "../../../cli/src/api-types"
import {
fetchSessions,
fetchAgents,
@@ -35,6 +38,133 @@ const [disconnectedInstance, setDisconnectedInstance] = createSignal<Disconnecte
const MAX_LOG_ENTRIES = 1000
function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor): Instance {
const existing = instances().get(descriptor.id)
return {
id: descriptor.id,
folder: descriptor.path,
port: descriptor.port ?? existing?.port ?? 0,
pid: descriptor.pid ?? existing?.pid ?? 0,
status: descriptor.status,
error: descriptor.error,
client: existing?.client ?? null,
metadata: existing?.metadata,
binaryPath: descriptor.binaryLabel,
environmentVariables: existing?.environmentVariables ?? preferences().environmentVariables ?? {},
}
}
function upsertWorkspace(descriptor: WorkspaceDescriptor) {
const mapped = workspaceDescriptorToInstance(descriptor)
if (instances().has(descriptor.id)) {
updateInstance(descriptor.id, mapped)
} else {
addInstance(mapped)
setHasInstances(true)
}
if (descriptor.status === "ready" && descriptor.port) {
attachClient(descriptor.id, descriptor.port)
}
}
function attachClient(instanceId: string, port: number) {
const instance = instances().get(instanceId)
if (!instance) return
if (instance.port === port && instance.client) {
return
}
if (instance.port && instance.client) {
sdkManager.destroyClient(instance.port)
sseManager.disconnect(instanceId)
}
const client = sdkManager.createClient(port)
updateInstance(instanceId, {
client,
port,
status: "ready",
})
sseManager.connect(instanceId, port)
void hydrateInstanceData(instanceId).catch((error) => {
console.error("Failed to hydrate instance data", error)
})
}
function releaseInstanceResources(instanceId: string) {
const instance = instances().get(instanceId)
if (!instance) return
if (instance.port) {
sdkManager.destroyClient(instance.port)
}
sseManager.disconnect(instanceId)
}
async function hydrateInstanceData(instanceId: string) {
try {
await fetchSessions(instanceId)
await fetchAgents(instanceId)
await fetchProviders(instanceId)
const instance = instances().get(instanceId)
if (!instance?.client) return
await fetchCommands(instanceId, instance.client)
} catch (error) {
console.error("Failed to fetch initial data:", error)
}
}
void (async function initializeWorkspaces() {
try {
const workspaces = await cliApi.fetchWorkspaces()
workspaces.forEach((workspace) => upsertWorkspace(workspace))
if (workspaces.length === 0) {
setHasInstances(false)
}
} catch (error) {
console.error("Failed to load workspaces", error)
}
})()
cliEvents.on("*", (event) => handleWorkspaceEvent(event))
function handleWorkspaceEvent(event: WorkspaceEventPayload) {
switch (event.type) {
case "workspace.created":
upsertWorkspace(event.workspace)
break
case "workspace.started":
upsertWorkspace(event.workspace)
break
case "workspace.error":
upsertWorkspace(event.workspace)
break
case "workspace.stopped":
releaseInstanceResources(event.workspaceId)
removeInstance(event.workspaceId)
if (instances().size === 0) {
setHasInstances(false)
}
break
case "workspace.log":
handleWorkspaceLog(event.entry)
break
default:
break
}
}
function handleWorkspaceLog(entry: WorkspaceLogEntry) {
const logEntry: LogEntry = {
timestamp: new Date(entry.timestamp).getTime(),
level: (entry.level as LogEntry["level"]) ?? "info",
message: entry.message,
}
addLog(entry.workspaceId, logEntry)
}
function ensureLogContainer(id: string) {
setInstanceLogs((prev) => {
if (prev.has(id)) {
@@ -157,61 +287,17 @@ function removeInstance(id: string) {
}
async function createInstance(folder: string, binaryPath?: string): Promise<string> {
const id = `instance-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
const instance: Instance = {
id,
folder,
port: 0,
pid: 0,
status: "starting",
client: null,
environmentVariables: preferences().environmentVariables ?? {},
}
addInstance(instance)
// Update last used binary
if (binaryPath) {
updateLastUsedBinary(binaryPath)
}
try {
const {
id: returnedId,
port,
pid,
binaryPath: actualBinaryPath,
} = await window.electronAPI.createInstance(id, folder, binaryPath, preferences().environmentVariables)
const client = sdkManager.createClient(port)
updateInstance(id, {
port,
pid,
client,
status: "ready",
binaryPath: actualBinaryPath,
})
setActiveInstanceId(id)
sseManager.connect(id, port)
try {
await fetchSessions(id)
await fetchAgents(id)
await fetchProviders(id)
await fetchCommands(id, client)
} catch (error) {
console.error("Failed to fetch initial data:", error)
}
return id
const workspace = await cliApi.createWorkspace({ path: folder })
upsertWorkspace(workspace)
setActiveInstanceId(workspace.id)
return workspace.id
} catch (error) {
updateInstance(id, {
status: "error",
error: error instanceof Error ? error.message : String(error),
})
console.error("Failed to create workspace", error)
throw error
}
}
@@ -220,17 +306,18 @@ async function stopInstance(id: string) {
const instance = instances().get(id)
if (!instance) return
sseManager.disconnect(id)
releaseInstanceResources(id)
if (instance.port) {
sdkManager.destroyClient(instance.port)
}
if (instance.pid) {
await window.electronAPI.stopInstance(instance.pid)
try {
await cliApi.deleteWorkspace(id)
} catch (error) {
console.error("Failed to stop workspace", error)
}
removeInstance(id)
if (instances().size === 0) {
setHasInstances(false)
}
}
async function fetchLspStatus(instanceId: string): Promise<LspStatus[] | undefined> {