From dd01e4cd778be83122b836ce746331d0a9a463fe Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Tue, 21 Jul 2026 19:01:15 +0530 Subject: [PATCH] refactor: simplify websearch flow --- packages/core/src/config/global.ts | 6 ++-- packages/core/src/plugin/promise.ts | 19 +++++------ packages/core/src/websearch.ts | 34 +++++++++---------- packages/core/test/plugin/host.ts | 14 ++++---- .../core/test/plugin/websearch-fixture.ts | 17 +++------- .../tui/src/component/dialog-integration.tsx | 6 +--- packages/tui/src/context/data.tsx | 5 +-- 7 files changed, 42 insertions(+), 59 deletions(-) diff --git a/packages/core/src/config/global.ts b/packages/core/src/config/global.ts index efb375b911..6d07ef31c0 100644 --- a/packages/core/src/config/global.ts +++ b/packages/core/src/config/global.ts @@ -10,7 +10,7 @@ import { Global } from "../global" import { EffectFlock } from "../util/effect-flock" export interface Interface { - readonly update: (path: JSONPath, value: unknown) => Effect.Effect + readonly update: (jsonPath: JSONPath, value: unknown) => Effect.Effect } export class Service extends Context.Service()("@opencode/ConfigGlobal") {} @@ -26,11 +26,11 @@ const layer = Layer.effect( update: Effect.fn("ConfigGlobal.update")(function* (jsonPath, value) { yield* flock.withLock( Effect.gen(function* () { - const existing = yield* Effect.filter( + const existingFiles = yield* Effect.filter( ["opencode.jsonc", "opencode.json"].map((name) => path.join(global.config, name)), fs.existsSafe, ) - const filepath = existing[0] ?? path.join(global.config, "opencode.json") + const filepath = existingFiles[0] ?? path.join(global.config, "opencode.json") const text = (yield* fs.readFileStringSafe(filepath)) ?? "{}" const next = applyEdits( text, diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index 9de777232b..e5560555b4 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -204,10 +204,7 @@ export function fromPromise(plugin: Plugin) { id: definition.id, name: definition.name, execute: (input, execution) => - Effect.tryPromise({ - try: (signal) => definition.execute(input, { ...execution, signal }), - catch: (cause) => cause, - }), + attempt((signal) => definition.execute(input, { ...execution, signal })), }), ), }, @@ -294,25 +291,23 @@ function adaptIntegration(definition: IntegrationDefinition) { return { ...methodInfo, authorize: (inputs: Parameters[0]) => - Effect.tryPromise({ try: () => authorize(inputs), catch: (cause) => cause }).pipe( + attempt(() => authorize(inputs)).pipe( Effect.map((authorization) => { if (authorization.mode === "auto") { return { ...authorization, - callback: Effect.tryPromise({ try: () => authorization.callback, catch: (cause) => cause }), + callback: attempt(() => authorization.callback), } } return { ...authorization, - callback: (code: string) => - Effect.tryPromise({ try: () => authorization.callback(code), catch: (cause) => cause }), + callback: (code: string) => attempt(() => authorization.callback(code)), } }), ), ...(refresh ? { - refresh: (credential: Parameters[0]) => - Effect.tryPromise({ try: () => refresh(credential), catch: (cause) => cause }), + refresh: (credential: Parameters[0]) => attempt(() => refresh(credential)), } : {}), } @@ -320,6 +315,10 @@ function adaptIntegration(definition: IntegrationDefinition) { } } +function attempt(evaluate: (signal: AbortSignal) => PromiseLike) { + return Effect.tryPromise({ try: evaluate, catch: (cause) => cause }) +} + function model(input: { readonly id: string; readonly providerID: string; readonly variant?: string }) { return Model.Ref.make({ id: Model.ID.make(input.id), diff --git a/packages/core/src/websearch.ts b/packages/core/src/websearch.ts index 4daa86c655..7451e5b33e 100644 --- a/packages/core/src/websearch.ts +++ b/packages/core/src/websearch.ts @@ -33,7 +33,7 @@ export type Result = WebSearch.Result export interface ProviderImplementation extends Provider { readonly execute: ( - input: WebSearch.ProviderInput, + input: ProviderInput, context: { readonly sessionID?: string }, ) => Effect.Effect } @@ -43,9 +43,12 @@ export class ProviderRequiredError extends Schema.TaggedErrorClass()("WebSearch.ProviderNotFound", { - providerID: ID, -}) {} +export class ProviderNotFoundError extends Schema.TaggedErrorClass()( + "WebSearch.ProviderNotFound", + { + providerID: ID, + }, +) {} export class CancelledError extends Schema.TaggedErrorClass()("WebSearch.Cancelled", {}) {} @@ -54,20 +57,14 @@ export class RequestError extends Schema.TaggedErrorClass()("WebSe cause: Schema.Defect(), }) {} -export type Error = - | ProviderRequiredError - | ProviderNotFoundError - | CancelledError - | RequestError +export type Error = ProviderRequiredError | ProviderNotFoundError | CancelledError | RequestError export interface QueryInput extends Input { readonly sessionID?: string } export interface Interface { - readonly register: ( - provider: ProviderImplementation, - ) => Effect.Effect + readonly register: (provider: ProviderImplementation) => Effect.Effect readonly list: () => Effect.Effect readonly selected: () => Effect.Effect readonly select: (providerID: ID) => Effect.Effect @@ -110,10 +107,12 @@ const layer = Layer.effect( } const globalProviderID = Effect.fn("WebSearch.globalProviderID")(function* () { - const entries = (yield* config.entries()).filter( - (entry) => entry.type === "document" && entry.path && path.dirname(entry.path) === globalConfigPath, - ) - return Config.latest(entries, "websearch")?.provider + return Config.latest( + (yield* config.entries()).filter( + (entry) => entry.type === "document" && entry.path && path.dirname(entry.path) === globalConfigPath, + ), + "websearch", + )?.provider }) const selected = Effect.fn("WebSearch.selected")(function* () { @@ -214,8 +213,7 @@ const layer = Layer.effect( }), selected, select: Effect.fn("WebSearch.select")(function* (providerID) { - const provider = state.get().providers.get(providerID) - if (!provider) return yield* new ProviderNotFoundError({ providerID }) + if (!state.get().providers.has(providerID)) return yield* new ProviderNotFoundError({ providerID }) yield* saveProvider(providerID) }), query: Effect.fn("WebSearch.query")(function* (input) { diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 734fbedd9d..4708ab4b77 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -350,20 +350,20 @@ export function webSearchHost(websearch: WebSearch.Interface): Plugin.Context["w function registerIntegration(draft: Integration.Draft, definition: IntegrationDefinition) { const integrationID = Integration.ID.make(definition.id) draft.update(integrationID, (integration) => (integration.name = definition.name)) - for (const item of definition.methods ?? []) { - if (item.type === "env") { - draft.method.update(methodImplementation({ integrationID: definition.id, method: item })) + for (const method of definition.methods ?? []) { + if (method.type === "env") { + draft.method.update(methodImplementation({ integrationID: definition.id, method })) continue } - if (item.type === "key") { - draft.method.update(methodImplementation({ integrationID: definition.id, method: item })) + if (method.type === "key") { + draft.method.update(methodImplementation({ integrationID: definition.id, method })) continue } - const { authorize, refresh, credentialLabel, ...method } = item + const { authorize, refresh, credentialLabel, ...info } = method draft.method.update( methodImplementation({ integrationID: definition.id, - method, + method: info, authorize, ...(refresh ? { refresh } : {}), ...(credentialLabel ? { label: credentialLabel } : {}), diff --git a/packages/core/test/plugin/websearch-fixture.ts b/packages/core/test/plugin/websearch-fixture.ts index 4ad61ad3e1..0013f9e785 100644 --- a/packages/core/test/plugin/websearch-fixture.ts +++ b/packages/core/test/plugin/websearch-fixture.ts @@ -11,18 +11,18 @@ import { Integration } from "@opencode-ai/core/integration" import { WebSearch } from "@opencode-ai/core/websearch" import { testEffect } from "../lib/effect" -export interface WebSearchRequest { +interface WebSearchRequest { readonly url: string readonly headers: Record readonly body: unknown } export const requests: WebSearchRequest[] = [] -export const response = { body: "" } +let responseBody = "" export function resetWebSearchFixture(body: string) { requests.length = 0 - response.body = body + responseBody = body } const http = Layer.succeed( @@ -35,7 +35,7 @@ const http = Layer.succeed( headers: request.headers, body: JSON.parse(new TextDecoder().decode(request.body.body)), }) - return HttpClientResponse.fromWeb(request, new Response(response.body, { status: 200 })) + return HttpClientResponse.fromWeb(request, new Response(responseBody, { status: 200 })) }), ), ) @@ -43,14 +43,7 @@ const http = Layer.succeed( export const webSearchIntegrationTest = testEffect( Layer.merge( AppNodeBuilder.build( - LayerNode.group([ - Integration.node, - Credential.node, - EventV2.node, - Form.node, - ConfigGlobal.node, - WebSearch.node, - ]), + LayerNode.group([Integration.node, Credential.node, EventV2.node, Form.node, ConfigGlobal.node, WebSearch.node]), [ [Config.node, Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))], [ diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index 381d5fe799..e6e75ec4b9 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -97,11 +97,7 @@ export function DialogIntegration( ? () => : undefined, onSelect: () => { - if (props.connectionOnly) { - if (credentials.length) return manageConnections(integration, methods, dialog, props.onConnected) - return selectMethod(integration, methods, dialog, props.onConnected) - } - if (provider) return manageWebSearch(provider, dialog, integration, methods) + if (!props.connectionOnly && provider) return manageWebSearch(provider, dialog, integration, methods) if (credentials.length) return manageConnections(integration, methods, dialog, props.onConnected) return selectMethod(integration, methods, dialog, props.onConnected) }, diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index c62aeffd8b..d9fda2e300 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -36,7 +36,6 @@ import { useClient } from "./client" import { createEffect, createSignal, onCleanup } from "solid-js" export type DataSessionStatus = "idle" | "running" -type Data = Plugin.Context["data"] const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_") @@ -58,7 +57,7 @@ type LocationData = { provider?: ProviderV2Info[] reference?: ReferenceInfo[] websearch?: WebSearchProvider[] - websearchSelected?: string | null + websearchSelected?: WebSearchProvider["id"] | null // Currently running shell commands for this location, keyed by shell id. Entries are removed // once the command exits or is deleted, so this only ever holds in-flight shells. shell?: Record @@ -876,8 +875,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ]) break case "config.updated": - void result.location.websearch.refresh(event.location) - break case "websearch.updated": void result.location.websearch.refresh(event.location) break