Files
neuron/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
T
Dax Raad 1a734adb4d core: consolidate shared infrastructure into core package
Moves effect logging, observability, runtime utilities, flags, installation
version info, and process utilities from opencode to core package. This
enables better code sharing across packages and establishes core as the
single source of truth for foundational utilities.

All internal imports updated to use @opencode-ai/core paths for consistency.
2026-04-25 13:30:37 -04:00

143 lines
3.8 KiB
TypeScript

import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { Flag } from "@opencode-ai/core/flag/flag"
import { batch, onCleanup, onMount } from "solid-js"
export type EventSource = {
subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void>
}
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK",
init: (props: {
url: string
directory?: string
fetch?: typeof fetch
headers?: RequestInit["headers"]
events?: EventSource
}) => {
const abort = new AbortController()
let sse: AbortController | undefined
function createSDK() {
return createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
directory: props.directory,
fetch: props.fetch,
headers: props.headers,
})
}
let sdk = createSDK()
const emitter = createGlobalEmitter<{
event: GlobalEvent
}>()
let queue: GlobalEvent[] = []
let timer: Timer | undefined
let last = 0
const retryDelay = 1000
const maxRetryDelay = 30000
const flush = () => {
if (queue.length === 0) return
const events = queue
queue = []
timer = undefined
last = Date.now()
// Batch all event emissions so all store updates result in a single render
batch(() => {
for (const event of events) {
emitter.emit("event", event)
}
})
}
const handleEvent = (event: GlobalEvent) => {
queue.push(event)
const elapsed = Date.now() - last
if (timer) return
// If we just flushed recently (within 16ms), batch this with future events
// Otherwise, process immediately to avoid latency
if (elapsed < 16) {
timer = setTimeout(flush, 16)
return
}
flush()
}
function startSSE() {
sse?.abort()
const ctrl = new AbortController()
sse = ctrl
;(async () => {
let attempt = 0
while (true) {
if (abort.signal.aborted || ctrl.signal.aborted) break
const events = await sdk.global.event({
signal: ctrl.signal,
sseMaxRetryAttempts: 0,
})
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
// Start syncing workspaces, it's important to do this after
// we've started listening to events
await sdk.sync.start().catch(() => {})
}
for await (const event of events.stream) {
if (ctrl.signal.aborted) break
handleEvent(event)
}
if (timer) clearTimeout(timer)
if (queue.length > 0) flush()
attempt += 1
if (abort.signal.aborted || ctrl.signal.aborted) break
// Exponential backoff
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
await new Promise((resolve) => setTimeout(resolve, backoff))
}
})().catch(() => {})
}
onMount(async () => {
if (props.events) {
const unsub = await props.events.subscribe(handleEvent)
onCleanup(unsub)
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
// Start syncing workspaces, it's important to do this after
// we've started listening to events
await sdk.sync.start().catch(() => {})
}
} else {
startSSE()
}
})
onCleanup(() => {
abort.abort()
sse?.abort()
if (timer) clearTimeout(timer)
})
return {
get client() {
return sdk
},
directory: props.directory,
event: emitter,
fetch: props.fetch ?? fetch,
url: props.url,
}
},
})