fix(core): load OpenCode provider config asynchronously

This commit is contained in:
Dax Raad
2026-06-26 21:16:15 -04:00
parent 62b2bc39df
commit 658cbe9caf
2 changed files with 33 additions and 6 deletions
@@ -1,4 +1,4 @@
import { Duration, Effect, Schema, Stream } from "effect" import { Duration, Effect, Schema, Semaphore, Stream } from "effect"
import type { Scope } from "effect" import type { Scope } from "effect"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
@@ -79,6 +79,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const http = yield* HttpClient.HttpClient const http = yield* HttpClient.HttpClient
const loading = Semaphore.makeUnsafe(1)
let connected = false let connected = false
let providers: typeof ConfigV1.Info.Type.provider | undefined let providers: typeof ConfigV1.Info.Type.provider | undefined
@@ -105,7 +106,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } }) draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } })
}) })
yield* load() connected = (yield* ctx.integration.connection.active("opencode")) !== undefined
yield* ctx.catalog.transform((catalog) => { yield* ctx.catalog.transform((catalog) => {
for (const [providerID, item] of Object.entries(providers ?? {})) { for (const [providerID, item] of Object.entries(providers ?? {})) {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
@@ -176,11 +177,13 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
} }
}) })
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")), Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")),
Stream.runForEach(() => load().pipe(Effect.andThen(ctx.catalog.reload()))), Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
yield* refresh().pipe(Effect.forkScoped)
}), }),
}) })
@@ -25,6 +25,20 @@ function required<T>(value: T | undefined): T {
return value return value
} }
function eventually<A>(
effect: Effect.Effect<A>,
predicate: (value: A) => boolean,
remaining = 1000,
): Effect.Effect<A, Error> {
return Effect.gen(function* () {
const value = yield* effect
if (predicate(value)) return value
if (remaining === 0) return yield* Effect.fail(new Error("Timed out waiting for value"))
yield* Effect.promise(() => Bun.sleep(1))
return yield* eventually(effect, predicate, remaining - 1)
})
}
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) { function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease( return Effect.acquireUseRelease(
Effect.sync(() => { Effect.sync(() => {
@@ -67,11 +81,14 @@ describe("OpencodePlugin", () => {
Effect.acquireUseRelease( Effect.acquireUseRelease(
Effect.sync(() => { Effect.sync(() => {
const authorization: Array<string | null> = [] const authorization: Array<string | null> = []
const gate = Promise.withResolvers<void>()
return { return {
authorization, authorization,
release: gate.resolve,
server: Bun.serve({ server: Bun.serve({
port: 0, port: 0,
fetch: (request) => { fetch: async (request) => {
await gate.promise
authorization.push(request.headers.get("authorization")) authorization.push(request.headers.get("authorization"))
const origin = new URL(request.url).origin const origin = new URL(request.url).origin
return Response.json({ return Response.json({
@@ -110,7 +127,7 @@ describe("OpencodePlugin", () => {
}), }),
} }
}), }),
({ authorization, server }) => ({ authorization, release, server }) =>
Effect.gen(function* () { Effect.gen(function* () {
const credentials = yield* Credential.Service const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
@@ -128,8 +145,15 @@ describe("OpencodePlugin", () => {
}) })
yield* addPlugin() yield* addPlugin()
expect(authorization).toEqual([])
release()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("remote"))) const provider = required(
yield* eventually(
catalog.provider.get(ProviderV2.ID.make("remote")),
(item) => item?.integrationID === Integration.ID.make("opencode"),
),
)
expect(provider).toMatchObject({ expect(provider).toMatchObject({
name: "Remote", name: "Remote",
integrationID: "opencode", integrationID: "opencode",