mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-22 10:15:31 -04:00
refactor: simplify websearch flow
This commit is contained in:
@@ -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<void, FSUtil.Error | EffectFlock.LockError>
|
||||
readonly update: (jsonPath: JSONPath, value: unknown) => Effect.Effect<void, FSUtil.Error | EffectFlock.LockError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@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,
|
||||
|
||||
@@ -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<typeof authorize>[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<typeof refresh>[0]) =>
|
||||
Effect.tryPromise({ try: () => refresh(credential), catch: (cause) => cause }),
|
||||
refresh: (credential: Parameters<typeof refresh>[0]) => attempt(() => refresh(credential)),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
@@ -320,6 +315,10 @@ function adaptIntegration(definition: IntegrationDefinition) {
|
||||
}
|
||||
}
|
||||
|
||||
function attempt<A>(evaluate: (signal: AbortSignal) => PromiseLike<A>) {
|
||||
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),
|
||||
|
||||
@@ -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<ProviderOutput, unknown>
|
||||
}
|
||||
@@ -43,9 +43,12 @@ export class ProviderRequiredError extends Schema.TaggedErrorClass<ProviderRequi
|
||||
{},
|
||||
) {}
|
||||
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()("WebSearch.ProviderNotFound", {
|
||||
providerID: ID,
|
||||
}) {}
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
|
||||
"WebSearch.ProviderNotFound",
|
||||
{
|
||||
providerID: ID,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("WebSearch.Cancelled", {}) {}
|
||||
|
||||
@@ -54,20 +57,14 @@ export class RequestError extends Schema.TaggedErrorClass<RequestError>()("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<State.Registration, never, Scope.Scope>
|
||||
readonly register: (provider: ProviderImplementation) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
readonly list: () => Effect.Effect<readonly Provider[]>
|
||||
readonly selected: () => Effect.Effect<ID | undefined>
|
||||
readonly select: (providerID: ID) => Effect.Effect<void, ProviderNotFoundError>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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<string, string>
|
||||
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([]) }))],
|
||||
[
|
||||
|
||||
@@ -97,11 +97,7 @@ export function DialogIntegration(
|
||||
? () => <text fg={themeV2.text.feedback.success.default}>✓</text>
|
||||
: 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)
|
||||
},
|
||||
|
||||
@@ -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<string, ShellInfo>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user