Add CLI instance proxy and route UI traffic through it
This commit is contained in:
@@ -25,6 +25,8 @@ const DEFAULT_EVENTS_PATH = typeof window !== "undefined" ? window.__CODENOMAD_E
|
||||
const API_BASE = import.meta.env.VITE_CODENOMAD_API_BASE ?? DEFAULT_BASE
|
||||
const EVENTS_URL = buildEventsUrl(API_BASE, DEFAULT_EVENTS_PATH)
|
||||
|
||||
export const CODENOMAD_API_BASE = API_BASE
|
||||
|
||||
function buildEventsUrl(base: string | undefined, path: string): string {
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/client"
|
||||
import { CODENOMAD_API_BASE } from "./api-client"
|
||||
|
||||
class SDKManager {
|
||||
private clients = new Map<number, OpencodeClient>()
|
||||
private clients = new Map<string, OpencodeClient>()
|
||||
|
||||
createClient(port: number): OpencodeClient {
|
||||
if (this.clients.has(port)) {
|
||||
return this.clients.get(port)!
|
||||
createClient(instanceId: string, proxyPath: string): OpencodeClient {
|
||||
if (this.clients.has(instanceId)) {
|
||||
return this.clients.get(instanceId)!
|
||||
}
|
||||
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: `http://localhost:${port}`,
|
||||
})
|
||||
const baseUrl = buildInstanceBaseUrl(proxyPath)
|
||||
const client = createOpencodeClient({ baseUrl })
|
||||
|
||||
this.clients.set(port, client)
|
||||
this.clients.set(instanceId, client)
|
||||
return client
|
||||
}
|
||||
|
||||
getClient(port: number): OpencodeClient | null {
|
||||
return this.clients.get(port) || null
|
||||
getClient(instanceId: string): OpencodeClient | null {
|
||||
return this.clients.get(instanceId) ?? null
|
||||
}
|
||||
|
||||
destroyClient(port: number): void {
|
||||
this.clients.delete(port)
|
||||
destroyClient(instanceId: string): void {
|
||||
this.clients.delete(instanceId)
|
||||
}
|
||||
|
||||
destroyAll(): void {
|
||||
@@ -29,4 +29,19 @@ class SDKManager {
|
||||
}
|
||||
}
|
||||
|
||||
function buildInstanceBaseUrl(proxyPath: string): string {
|
||||
const normalized = normalizeProxyPath(proxyPath)
|
||||
const base = stripTrailingSlashes(CODENOMAD_API_BASE)
|
||||
return `${base}${normalized}/`
|
||||
}
|
||||
|
||||
function normalizeProxyPath(proxyPath: string): string {
|
||||
const withLeading = proxyPath.startsWith("/") ? proxyPath : `/${proxyPath}`
|
||||
return withLeading.replace(/\/+/g, "/").replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
function stripTrailingSlashes(input: string): string {
|
||||
return input.replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
export const sdkManager = new SDKManager()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import {
|
||||
MessageUpdateEvent,
|
||||
MessageRemovedEvent,
|
||||
MessagePartUpdatedEvent,
|
||||
MessagePartRemovedEvent
|
||||
import {
|
||||
MessageUpdateEvent,
|
||||
MessageRemovedEvent,
|
||||
MessagePartUpdatedEvent,
|
||||
MessagePartRemovedEvent,
|
||||
} from "../types/message"
|
||||
import type {
|
||||
EventLspUpdated,
|
||||
@@ -14,10 +14,11 @@ import type {
|
||||
EventSessionIdle,
|
||||
EventSessionUpdated,
|
||||
} from "@opencode-ai/sdk"
|
||||
import { CODENOMAD_API_BASE } from "./api-client"
|
||||
|
||||
interface SSEConnection {
|
||||
instanceId: string
|
||||
port: number
|
||||
proxyPath: string
|
||||
eventSource: EventSource
|
||||
status: "connecting" | "connected" | "disconnected" | "error"
|
||||
reconnectAttempts: number
|
||||
@@ -57,19 +58,19 @@ class SSEManager {
|
||||
private connections = new Map<string, SSEConnection>()
|
||||
private static readonly MAX_RECONNECT_ATTEMPTS = 3
|
||||
|
||||
connect(instanceId: string, port: number, reconnectAttempts = 0): void {
|
||||
connect(instanceId: string, proxyPath: string, reconnectAttempts = 0): void {
|
||||
const existing = this.connections.get(instanceId)
|
||||
if (existing) {
|
||||
this.clearReconnectTimer(existing)
|
||||
existing.eventSource.close()
|
||||
}
|
||||
|
||||
const url = `http://localhost:${port}/event`
|
||||
const url = buildInstanceEventsUrl(proxyPath)
|
||||
const eventSource = new EventSource(url)
|
||||
|
||||
const connection: SSEConnection = {
|
||||
instanceId,
|
||||
port,
|
||||
proxyPath,
|
||||
eventSource,
|
||||
status: "connecting",
|
||||
reconnectAttempts,
|
||||
@@ -180,7 +181,7 @@ class SSEManager {
|
||||
|
||||
connection.reconnectTimer = setTimeout(() => {
|
||||
connection.reconnectTimer = undefined
|
||||
this.connect(instanceId, connection.port, nextAttempt)
|
||||
this.connect(instanceId, connection.proxyPath, nextAttempt)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
@@ -234,4 +235,19 @@ class SSEManager {
|
||||
}
|
||||
}
|
||||
|
||||
function buildInstanceEventsUrl(proxyPath: string): string {
|
||||
const normalized = normalizeProxyPath(proxyPath)
|
||||
const base = stripTrailingSlashes(CODENOMAD_API_BASE)
|
||||
return `${base}${normalized}/event`
|
||||
}
|
||||
|
||||
function normalizeProxyPath(proxyPath: string): string {
|
||||
const withLeading = proxyPath.startsWith("/") ? proxyPath : `/${proxyPath}`
|
||||
return withLeading.replace(/\/+/g, "/").replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
function stripTrailingSlashes(input: string): string {
|
||||
return input.replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
export const sseManager = new SSEManager()
|
||||
|
||||
@@ -45,6 +45,7 @@ function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor): Instanc
|
||||
folder: descriptor.path,
|
||||
port: descriptor.port ?? existing?.port ?? 0,
|
||||
pid: descriptor.pid ?? existing?.pid ?? 0,
|
||||
proxyPath: descriptor.proxyPath,
|
||||
status: descriptor.status,
|
||||
error: descriptor.error,
|
||||
client: existing?.client ?? null,
|
||||
@@ -63,32 +64,39 @@ function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
||||
setHasInstances(true)
|
||||
}
|
||||
|
||||
if (descriptor.status === "ready" && descriptor.port) {
|
||||
attachClient(descriptor.id, descriptor.port)
|
||||
if (descriptor.status === "ready") {
|
||||
attachClient(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
function attachClient(instanceId: string, port: number) {
|
||||
const instance = instances().get(instanceId)
|
||||
function attachClient(descriptor: WorkspaceDescriptor) {
|
||||
const instance = instances().get(descriptor.id)
|
||||
if (!instance) return
|
||||
|
||||
if (instance.port === port && instance.client) {
|
||||
const nextPort = descriptor.port ?? instance.port
|
||||
const nextProxyPath = descriptor.proxyPath
|
||||
|
||||
if (instance.client && instance.proxyPath === nextProxyPath) {
|
||||
if (nextPort && instance.port !== nextPort) {
|
||||
updateInstance(descriptor.id, { port: nextPort })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (instance.port && instance.client) {
|
||||
sdkManager.destroyClient(instance.port)
|
||||
sseManager.disconnect(instanceId)
|
||||
if (instance.client) {
|
||||
sdkManager.destroyClient(descriptor.id)
|
||||
sseManager.disconnect(descriptor.id)
|
||||
}
|
||||
|
||||
const client = sdkManager.createClient(port)
|
||||
updateInstance(instanceId, {
|
||||
const client = sdkManager.createClient(descriptor.id, nextProxyPath)
|
||||
updateInstance(descriptor.id, {
|
||||
client,
|
||||
port,
|
||||
port: nextPort ?? 0,
|
||||
proxyPath: nextProxyPath,
|
||||
status: "ready",
|
||||
})
|
||||
sseManager.connect(instanceId, port)
|
||||
void hydrateInstanceData(instanceId).catch((error) => {
|
||||
sseManager.connect(descriptor.id, nextProxyPath)
|
||||
void hydrateInstanceData(descriptor.id).catch((error) => {
|
||||
console.error("Failed to hydrate instance data", error)
|
||||
})
|
||||
}
|
||||
@@ -97,8 +105,8 @@ function releaseInstanceResources(instanceId: string) {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance) return
|
||||
|
||||
if (instance.port) {
|
||||
sdkManager.destroyClient(instance.port)
|
||||
if (instance.client) {
|
||||
sdkManager.destroyClient(instanceId)
|
||||
}
|
||||
sseManager.disconnect(instanceId)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface Instance {
|
||||
folder: string
|
||||
port: number
|
||||
pid: number
|
||||
proxyPath: string
|
||||
status: "starting" | "ready" | "error" | "stopped"
|
||||
error?: string
|
||||
client: OpencodeClient | null
|
||||
|
||||
Reference in New Issue
Block a user