refactor: simplify search integration flow

This commit is contained in:
Shoubhit Dash 2026-07-06 19:46:39 +05:30
commit 56f44dbcaa
4 changed files with 105 additions and 111 deletions

View file

@ -136,12 +136,7 @@ export const layer = Layer.effect(
title: input.title,
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
}
const form: Info =
input.mode === "form"
? { ...base, mode: "form", fields: input.fields }
: input.mode === "url"
? { ...base, mode: "url", url: input.url }
: { ...base, mode: "integration", integrationID: input.integrationID }
const form = makeInfo(input, base)
const entry: Entry = {
form,
state: { status: "pending" },
@ -252,6 +247,17 @@ function validateAnswer(form: Info, answer: Answer) {
}
}
function makeInfo(input: CreateInput, base: Omit<Info, "mode" | "fields" | "url" | "integrationID">): Info {
switch (input.mode) {
case "form":
return { ...base, mode: "form", fields: input.fields }
case "url":
return { ...base, mode: "url", url: input.url }
case "integration":
return { ...base, mode: "integration", integrationID: input.integrationID }
}
}
function isActive(field: Form.Field, answer: Answer) {
if (!field.when) return true
return field.when.every((when) => matches(when, answer[when.key]))

View file

@ -15,13 +15,6 @@ const Args = Schema.Struct({
contextMaxCharacters: Schema.optional(Schema.Number),
})
const url = (apiKey: string | undefined) => {
if (!apiKey) return endpoint
const value = new URL(endpoint)
value.searchParams.set("exaApiKey", apiKey)
return value.toString()
}
export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
id: "opencode.search.exa",
effect: Effect.fn("SearchExa.Plugin")(function* (ctx) {
@ -33,20 +26,17 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
draft.capability.search.update({
integrationID: "exa",
capability: { type: "search", connection: "optional" },
execute: (input, context) =>
SearchMcp.call(
http,
url(context.credential?.type === "key" ? context.credential.key : undefined),
"web_search_exa",
Args,
{
query: input.query,
type: input.type ?? "auto",
numResults: input.numResults ?? 8,
livecrawl: input.livecrawl ?? "fallback",
contextMaxCharacters: input.contextMaxCharacters,
},
).pipe(Effect.map((text) => ({ text: text ?? "" }))),
execute: (input, context) => {
const url = new URL(endpoint)
if (context.credential?.type === "key") url.searchParams.set("exaApiKey", context.credential.key)
return SearchMcp.call(http, url.toString(), "web_search_exa", Args, {
query: input.query,
type: input.type ?? "auto",
numResults: input.numResults ?? 8,
livecrawl: input.livecrawl ?? "fallback",
contextMaxCharacters: input.contextMaxCharacters,
}).pipe(Effect.map((text) => ({ text: text ?? "" })))
},
})
})
}),

View file

@ -64,12 +64,6 @@ const layer = Layer.effect(
const onboarding = Semaphore.makeUnsafe(1)
const decodeOutput = Schema.decodeUnknownEffect(ProviderOutput)
const available = Effect.fn("Search.available")(function* () {
return new Map(
(yield* integrations.capability.search.list()).map((provider) => [provider.integrationID, provider]),
)
})
const requireProvider = (
providers: Map<Integration.ID, Integration.SearchImplementation>,
providerID: Integration.ID,
@ -78,7 +72,7 @@ const layer = Layer.effect(
return provider ? Effect.succeed(provider) : Effect.fail(new ProviderNotFoundError({ providerID }))
}
const configured = Effect.fn("Search.configured")(function* () {
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) {
@ -115,17 +109,16 @@ const layer = Layer.effect(
options: Array.from(providers.values())
.flatMap((provider) => {
const info = infos.get(provider.integrationID)
return info ? [{ provider, info }] : []
if (!info) return []
const disconnected =
provider.capability.connection === "optional" ? "Keyless available" : "Connection required"
return [{ info, description: info.connections.length ? "Connected" : disconnected }]
})
.toSorted((a, b) => a.info.name.localeCompare(b.info.name))
.map(({ provider, info }) => ({
.map(({ info, description }) => ({
value: info.id,
label: info.name,
description: info.connections.length
? "Connected"
: provider.capability.connection === "optional"
? "Keyless available"
: "Connection required",
description,
})),
},
],
@ -160,9 +153,11 @@ const layer = Layer.effect(
})
const select = Effect.fn("Search.select")(function* (input: QueryInput) {
const providers = yield* available()
const providers = new Map(
(yield* integrations.capability.search.list()).map((provider) => [provider.integrationID, provider]),
)
if (input.providerID) return yield* requireProvider(providers, input.providerID)
const override = yield* configured()
const override = yield* configuredProvider()
if (override) return yield* requireProvider(providers, override)
const selected = yield* integrations.capability.search.selected()
const provider = selected ? providers.get(selected) : undefined
@ -182,22 +177,22 @@ const layer = Layer.effect(
)
})
const query = Effect.fn("Search.query")(function* (input: QueryInput) {
const provider = yield* select(input)
const connection = yield* connect(provider, input.sessionID)
const credential = connection
? yield* integrations.connection
.resolve(connection)
.pipe(Effect.mapError((cause) => new RequestError({ providerID: provider.integrationID, cause })))
: undefined
const output = yield* provider.execute(input, { credential, sessionID: input.sessionID }).pipe(
Effect.flatMap(decodeOutput),
Effect.mapError((cause) => new RequestError({ providerID: provider.integrationID, cause })),
)
return new Result({ providerID: provider.integrationID, ...output })
return Service.of({
query: Effect.fn("Search.query")(function* (input) {
const provider = yield* select(input)
const connection = yield* connect(provider, input.sessionID)
const credential = connection
? yield* integrations.connection
.resolve(connection)
.pipe(Effect.mapError((cause) => new RequestError({ providerID: provider.integrationID, cause })))
: undefined
const output = yield* provider.execute(input, { credential, sessionID: input.sessionID }).pipe(
Effect.flatMap(decodeOutput),
Effect.mapError((cause) => new RequestError({ providerID: provider.integrationID, cause })),
)
return new Result({ providerID: provider.integrationID, ...output })
}),
})
return Service.of({ query })
}),
)