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, connection.type === "credential" ? { ...connection, id: Credential.ID.make(connection.id) } : connection,
), ),
}, },
register: (definition) => register: (definition) => integration.transform((draft) => registerIntegration(draft, definition)),
integration.transform((draft) => {
registerIntegration(draft, definition)
}),
transform: (callback) => transform: (callback) =>
integration.transform((draft) => { integration.transform((draft) => {
callback({ callback({

View file

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

View file

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

View file

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

View file

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