refactor(search): simplify provider flow

This commit is contained in:
Shoubhit Dash 2026-07-08 14:45:30 +05:30
commit 61676a1d3c
6 changed files with 33 additions and 43 deletions

View file

@ -192,10 +192,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
connection.type === "credential" ? { ...connection, id: Credential.ID.make(connection.id) } : connection,
),
},
register: (definition) =>
integration.transform((draft) => {
registerIntegration(draft, definition)
}),
register: (definition) => integration.transform((draft) => registerIntegration(draft, definition)),
transform: (callback) =>
integration.transform((draft) => {
callback({

View file

@ -120,14 +120,14 @@ export function fromPromise(plugin: Plugin) {
}
function adaptIntegration(definition: IntegrationDefinition) {
const { methods, search, ...info } = definition
const { methods, search, ...definitionInfo } = definition
return {
...info,
...definitionInfo,
methods: methods?.map((method) => {
if (method.type !== "oauth") return method
const { authorize, refresh, ...info } = method
const { authorize, refresh, ...methodInfo } = method
return {
...info,
...methodInfo,
authorize: (inputs: Parameters<typeof authorize>[0]) =>
Effect.tryPromise({ try: () => authorize(inputs), catch: (cause) => cause }).pipe(
Effect.map((authorization) => {

View file

@ -74,7 +74,8 @@ const layer = Layer.effect(
const integrations = yield* Integration.Service
const onboarding = Semaphore.makeUnsafe(1)
const decodeOutput = Schema.decodeUnknownEffect(ProviderOutput)
let pending: Integration.ID | undefined
const globalConfigPath = path.resolve(global.config)
let pendingProviderID: Integration.ID | undefined
const requireProvider = (
providers: Map<Integration.ID, Integration.SearchImplementation>,
@ -84,45 +85,31 @@ const layer = Layer.effect(
return provider ? Effect.succeed(provider) : Effect.fail(new ProviderNotFoundError({ providerID }))
}
const configuredProvider = Effect.fn("Search.configuredProvider")(function* () {
const providerID = Config.latest(yield* config.entries(), "search")?.provider
if (providerID) return providerID
if (process.env.OPENCODE_WEBSEARCH_PROVIDER) {
return Integration.ID.make(process.env.OPENCODE_WEBSEARCH_PROVIDER)
}
if (truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL")) {
return Integration.ID.make("parallel")
}
if (truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA")) {
return Integration.ID.make("exa")
}
})
const globalProvider = Effect.fn("Search.globalProvider")(function* () {
const globalProviderID = Effect.fn("Search.globalProviderID")(function* () {
const entries = (yield* config.entries()).filter(
(entry) => entry.type === "document" && entry.path && path.dirname(entry.path) === path.resolve(global.config),
(entry) => entry.type === "document" && entry.path && path.dirname(entry.path) === globalConfigPath,
)
return Config.latest(entries, "search")?.provider
})
const selected = Effect.fn("Search.selected")(function* () {
return pending ?? (yield* globalProvider())
return pendingProviderID ?? (yield* globalProviderID())
})
const save = Effect.fn("Search.save")(function* (providerID: Integration.ID) {
pending = providerID
const saveProvider = Effect.fn("Search.saveProvider")(function* (providerID: Integration.ID) {
pendingProviderID = providerID
yield* configGlobal.update(["search"], new ConfigSearch.Info({ provider: providerID })).pipe(
Effect.tapError(() => Effect.sync(() => (pending = undefined))),
Effect.tapError(() => Effect.sync(() => (pendingProviderID = undefined))),
Effect.orDie,
)
})
yield* events.subscribe(ConfigSchema.Event.Updated).pipe(
Stream.runForEach(() =>
globalProvider().pipe(
globalProviderID().pipe(
Effect.tap((providerID) =>
Effect.sync(() => {
if (providerID === pending) pending = undefined
if (providerID === pendingProviderID) pendingProviderID = undefined
}),
),
Effect.ignore,
@ -201,8 +188,17 @@ const layer = Layer.effect(
(yield* integrations.search.list()).map((provider) => [provider.integrationID, provider]),
)
if (input.providerID) return yield* requireProvider(providers, input.providerID)
const override = yield* configuredProvider()
if (override) return yield* requireProvider(providers, override)
const configuredProviderID = Config.latest(yield* config.entries(), "search")?.provider
if (configuredProviderID) return yield* requireProvider(providers, configuredProviderID)
if (process.env.OPENCODE_WEBSEARCH_PROVIDER) {
return yield* requireProvider(providers, Integration.ID.make(process.env.OPENCODE_WEBSEARCH_PROVIDER))
}
if (truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL")) {
return yield* requireProvider(providers, Integration.ID.make("parallel"))
}
if (truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA")) {
return yield* requireProvider(providers, Integration.ID.make("exa"))
}
const providerID = yield* selected()
const provider = providerID ? providers.get(providerID) : undefined
if (provider) return provider
@ -215,7 +211,7 @@ const layer = Layer.effect(
if (selectedProvider) return selectedProvider
const provider = yield* ask(providers, sessionID)
yield* connect(provider, sessionID)
yield* save(provider.integrationID)
yield* saveProvider(provider.integrationID)
return provider
}),
)
@ -226,7 +222,7 @@ const layer = Layer.effect(
select: Effect.fn("Search.select")(function* (providerID) {
const provider = yield* integrations.search.get(providerID)
if (!provider) return yield* new ProviderNotFoundError({ providerID })
yield* save(providerID)
yield* saveProvider(providerID)
}),
query: Effect.fn("Search.query")(function* (input) {
const provider = yield* resolve(input)

View file

@ -207,10 +207,7 @@ export function integrationHost(integration: Integration.Interface): PluginConte
connection.type === "credential" ? { ...connection, id: Credential.ID.make(connection.id) } : connection,
),
},
register: (definition) =>
integration.transform((draft) => {
registerIntegration(draft, definition)
}),
register: (definition) => integration.transform((draft) => registerIntegration(draft, definition)),
transform: (callback) =>
integration.transform((draft) =>
callback({

View file

@ -8,7 +8,7 @@ import { response } from "../location"
export const SearchHandler = HttpApiBuilder.group(Api, "server.search", (handlers) =>
Effect.gen(function* () {
const ready = Effect.fn("server.search.ready")(function* () {
const awaitPlugins = Effect.fn("server.search.awaitPlugins")(function* () {
const plugins = yield* PluginSupervisor.Service
yield* plugins.ready.pipe(
Effect.timeoutOrElse({
@ -34,7 +34,7 @@ export const SearchHandler = HttpApiBuilder.group(Api, "server.search", (handler
.handle(
"search.provider.select",
Effect.fn("server.search.provider.select")(function* (request) {
yield* ready()
yield* awaitPlugins()
const search = yield* Search.Service
yield* search.select(request.payload.providerID).pipe(
Effect.mapError(
@ -52,7 +52,7 @@ export const SearchHandler = HttpApiBuilder.group(Api, "server.search", (handler
.handle(
"search.query",
Effect.fn("server.search.query")(function* (request) {
yield* ready()
yield* awaitPlugins()
const search = yield* Search.Service
return yield* response(
search.query(request.payload).pipe(

View file

@ -121,7 +121,7 @@ function manageIntegration(
const sdk = useSDK()
const toast = useToast()
const credentials = credentialConnections(integration)
const selected = () => data.location.search.provider() === integration.id
const selected = createMemo(() => data.location.search.provider() === integration.id)
const selectSearch = () => {
void sdk.api.search
.selectProvider({