From 56f44dbcaa4b9d3e407a9c77db810f145d1458af Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Mon, 6 Jul 2026 19:46:39 +0530 Subject: [PATCH] refactor: simplify search integration flow --- packages/core/src/form.ts | 18 ++- packages/core/src/plugin/search/exa.ts | 32 ++--- packages/core/src/search.ts | 57 +++++---- .../tui/src/component/dialog-integration.tsx | 109 +++++++++--------- 4 files changed, 105 insertions(+), 111 deletions(-) diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index 25d594e95c..f18f61a8ac 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -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 { + 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])) diff --git a/packages/core/src/plugin/search/exa.ts b/packages/core/src/plugin/search/exa.ts index 23e5d3764c..d1fc44bfdd 100644 --- a/packages/core/src/plugin/search/exa.ts +++ b/packages/core/src/plugin/search/exa.ts @@ -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({ id: "opencode.search.exa", effect: Effect.fn("SearchExa.Plugin")(function* (ctx) { @@ -33,20 +26,17 @@ export const Plugin = define({ 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 ?? "" }))) + }, }) }) }), diff --git a/packages/core/src/search.ts b/packages/core/src/search.ts index a11b1a0292..3583356877 100644 --- a/packages/core/src/search.ts +++ b/packages/core/src/search.ts @@ -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, 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 }) }), ) diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index c9cea13b10..fbf9dfef5d 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -58,8 +58,6 @@ export function DialogIntegration( ) { const data = useData() const dialog = useDialog() - const sdk = useSDK() - const toast = useToast() const { theme } = useTheme() const options = createMemo(() => integrationOptions(data.location.integration.list() ?? []) @@ -68,26 +66,28 @@ export function DialogIntegration( const methods = connectMethods(integration) const connected = integration.connections.length > 0 const search = integration.capabilities.find((capability) => capability.type === "search") + const credentials = credentialConnections(integration) + const description = search?.connection === "optional" ? "API key optional" : undefined + const category = search ? "Web search" : undefined return { title: integration.name, value: integration.id, - description: - search?.connection === "optional" ? "API key optional" : methods.length ? undefined : "Environment only", + description: description ?? (methods.length === 0 ? "Environment only" : undefined), footer: [connectionSummary(integration), search?.selected ? "Web search default" : undefined] .filter((value) => value !== undefined && value.length > 0) .join(" · ") || undefined, - category: search ? "Web search" : integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services", + category: category ?? (integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services"), disabled: methods.length === 0 && !search, gutter: connected ? () => : undefined, onSelect: () => { if (props.connectionOnly) { - return credentialConnections(integration).length + return credentials.length ? manageConnections(integration, methods, dialog, props.onConnected) : selectMethod(integration, methods, dialog, props.onConnected) } - if (search) return manageIntegration(integration, methods, search, data, sdk, dialog, toast) - return credentialConnections(integration).length + if (search) return manageIntegration(integration, methods, search, dialog) + return credentials.length ? manageConnections(integration, methods, dialog, props.onConnected) : selectMethod(integration, methods, dialog, props.onConnected) }, @@ -112,55 +112,58 @@ function manageIntegration( integration: IntegrationInfo, methods: ConnectMethod[], search: IntegrationInfo["capabilities"][number], - data: ReturnType, - sdk: ReturnType, dialog: ReturnType, - toast: ReturnType, ) { const connected = integration.connections.length > 0 - const select = () => { - void sdk.api.integration - .selectCapability({ - integrationID: integration.id, - capability: "search", - location: location(data), - }) - .then(async () => { - await data.location.integration.refresh() - toast.show({ variant: "success", message: `${integration.name} is now the web search default` }) - dialog.clear() - }) - .catch(toast.error) - } + dialog.replace(() => { + const data = useData() + const sdk = useSDK() + const toast = useToast() + const credentials = credentialConnections(integration) + const selectSearch = () => { + void sdk.api.integration + .selectCapability({ + integrationID: integration.id, + capability: "search", + location: location(data), + }) + .then(async () => { + await data.location.integration.refresh() + toast.show({ variant: "success", message: `${integration.name} is now the web search default` }) + dialog.clear() + }) + .catch(toast.error) + } - dialog.replace(() => ( - selectMethod(integration, methods, dialog, select) - : select, - }, - ...(methods.length - ? [ - { - title: credentialConnections(integration).length ? "Manage connections" : "Connect", - value: "connect", - onSelect: () => - credentialConnections(integration).length - ? manageConnections(integration, methods, dialog) - : selectMethod(integration, methods, dialog), - }, - ] - : []), - ]} - /> - )) + return ( + selectMethod(integration, methods, dialog, selectSearch) + : selectSearch, + }, + ...(methods.length + ? [ + { + title: credentials.length ? "Manage connections" : "Connect", + value: "connect", + onSelect: () => + credentials.length + ? manageConnections(integration, methods, dialog) + : selectMethod(integration, methods, dialog), + }, + ] + : []), + ]} + /> + ) + }) } function manageConnections(