feat(core): add pluggable web search (#35558)

Co-authored-by: Dax Raad <d@ironbay.co>
This commit is contained in:
Shoubhit Dash 2026-07-26 09:25:05 +05:30 committed by GitHub
commit efb629a33a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 1700 additions and 587 deletions

View file

@ -59,29 +59,43 @@ export function connectionSummary(integration: IntegrationInfo) {
.join(", ")
}
export function DialogIntegration(props: { onConnected?: OnIntegrationConnected } = {}) {
export function DialogIntegration(
props: { onConnected?: OnIntegrationConnected; integrationID?: string; connectionOnly?: boolean } = {},
) {
const data = useData()
const dialog = useDialog()
const { themeV2 } = useTheme().contextual("elevated")
const options = createMemo(() =>
integrationOptions(data.location.integration.list() ?? []).map((integration) => {
const options = createMemo(() => {
const providers = data.location.websearch.list() ?? []
const providersByID = new Map(providers.map((provider) => [provider.id, provider]))
const integrations = integrationOptions(data.location.integration.list() ?? []).filter(
(integration) => props.integrationID === undefined || integration.id === props.integrationID,
)
return integrations.map((integration) => {
const methods = connectMethods(integration)
const connected = integration.connections.length > 0
const provider = providersByID.get(integration.id)
const credentials = credentialConnections(integration)
let category = "Services"
if (integration.id in INTEGRATION_PRIORITY) category = "Popular"
if (provider) category = "Web search"
return {
title: integration.name,
value: integration.id,
description: methods.length ? undefined : "Environment only",
description: methods.length === 0 ? "Environment only" : undefined,
footer: connectionSummary(integration) || undefined,
category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services",
disabled: methods.length === 0,
gutter: connected ? () => <text fg={themeV2.text.feedback.success.default}></text> : undefined,
onSelect: () =>
credentialConnections(integration).length
? manageConnections(integration, methods, dialog, props.onConnected)
: selectMethod(integration, methods, dialog, props.onConnected),
category,
disabled: methods.length === 0 && credentials.length === 0,
gutter:
integration.connections.length > 0
? () => <text fg={themeV2.text.feedback.success.default}></text>
: undefined,
onSelect: () => {
if (credentials.length) return manageConnections(integration, methods, dialog, props.onConnected)
return selectMethod(integration, methods, dialog, props.onConnected)
},
}
}),
)
})
})
return (
<DialogSelect

View file

@ -27,6 +27,7 @@ import type {
ShellInfo,
SkillInfo,
OpenCodeEvent,
WebSearchProvider,
} from "@opencode-ai/client"
import type { Plugin } from "@opencode-ai/plugin/v2/tui"
import { createStore, produce, reconcile } from "solid-js/store"
@ -56,6 +57,7 @@ type LocationData = {
model?: ModelInfo[]
provider?: ProviderV2Info[]
reference?: ReferenceInfo[]
websearch?: WebSearchProvider[]
// 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>
@ -875,6 +877,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
result.location.provider.sync(event.location),
])
break
case "config.updated":
case "websearch.updated":
void result.location.websearch.refresh(event.location)
break
// Authenticating an MCP integration reconnects its server, which emits mcp.status.changed,
// so the mcp list syncs here rather than off integration.updated.
case "mcp.status.changed":
@ -1251,6 +1257,20 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
sync.invalidate(`location.reference:${locationKey(ref ?? defaultLocation())}`)
},
},
websearch: {
list(location?: LocationRef) {
return store.location[locationKey(location ?? defaultLocation())]?.websearch
},
async refresh(ref?: LocationRef) {
const input = { location: locationQuery(ref ?? defaultLocation()) }
const providers = await client.api.websearch.providers(input)
const key = locationKey(providers.location)
setStore("location", key, {
...store.location[key],
websearch: providers.data,
})
},
},
skill: {
list(location?: LocationRef) {
return store.location[locationKey(location ?? defaultLocation())]?.skill