feat(core): add integration-backed search

This commit is contained in:
Shoubhit Dash 2026-07-06 19:33:13 +05:30
commit 129012c3ee
50 changed files with 1745 additions and 478 deletions

View file

@ -53,28 +53,46 @@ 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 sdk = useSDK()
const toast = useToast()
const { theme } = useTheme()
const options = createMemo(() =>
integrationOptions(data.location.integration.list() ?? []).map((integration) => {
const methods = connectMethods(integration)
const connected = integration.connections.length > 0
return {
title: integration.name,
value: integration.id,
description: methods.length ? undefined : "Environment only",
footer: connectionSummary(integration) || undefined,
category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services",
disabled: methods.length === 0,
gutter: connected ? () => <text fg={theme.success}></text> : undefined,
onSelect: () =>
credentialConnections(integration).length
? manageConnections(integration, methods, dialog, props.onConnected)
: selectMethod(integration, methods, dialog, props.onConnected),
}
}),
integrationOptions(data.location.integration.list() ?? [])
.filter((integration) => props.integrationID === undefined || integration.id === props.integrationID)
.map((integration) => {
const methods = connectMethods(integration)
const connected = integration.connections.length > 0
const search = integration.capabilities.find((capability) => capability.type === "search")
return {
title: integration.name,
value: integration.id,
description:
search?.connection === "optional" ? "API key optional" : methods.length ? undefined : "Environment only",
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",
disabled: methods.length === 0 && !search,
gutter: connected ? () => <text fg={theme.success}></text> : undefined,
onSelect: () => {
if (props.connectionOnly) {
return credentialConnections(integration).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
? manageConnections(integration, methods, dialog, props.onConnected)
: selectMethod(integration, methods, dialog, props.onConnected)
},
}
}),
)
return (
@ -90,6 +108,61 @@ export function DialogIntegration(props: { onConnected?: OnIntegrationConnected
)
}
function manageIntegration(
integration: IntegrationInfo,
methods: ConnectMethod[],
search: IntegrationInfo["capabilities"][number],
data: ReturnType<typeof useData>,
sdk: ReturnType<typeof useSDK>,
dialog: ReturnType<typeof useDialog>,
toast: ReturnType<typeof useToast>,
) {
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(() => (
<DialogSelect
title={integration.name}
options={[
{
title: search.selected ? "Web search default" : "Use for web search",
value: "search",
disabled: search.selected,
onSelect:
search.connection === "required" && !connected
? () => 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),
},
]
: []),
]}
/>
))
}
function manageConnections(
integration: IntegrationInfo,
methods: ConnectMethod[],

View file

@ -2,6 +2,7 @@ import type {
AgentV2Info,
CommandV2Info,
FormFormInfo,
FormIntegrationInfo,
FormUrlInfo,
IntegrationInfo,
LocationRef,
@ -30,7 +31,7 @@ export type DataSessionStatus = "idle" | "running"
const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_")
export type FormInfo = FormFormInfo | FormUrlInfo
export type FormInfo = FormFormInfo | FormUrlInfo | FormIntegrationInfo
type LocationData = {
agent?: AgentV2Info[]

View file

@ -10,6 +10,8 @@ import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../ui/border"
import { useTuiConfig } from "../../config"
import { useBindings, useOpencodeModeStack } from "../../keymap"
import { DialogIntegration } from "../../component/dialog-integration"
import { useDialog } from "../../ui/dialog"
const FORM_MODE = "form"
@ -131,7 +133,50 @@ function display(field: Field, value: FormValue | undefined) {
}
export function FormPrompt(props: { form: FormInfo }) {
return props.form.mode === "url" ? <UrlPrompt form={props.form} /> : <FieldsPrompt form={props.form} />
if (props.form.mode === "url") return <UrlPrompt form={props.form} />
if (props.form.mode === "integration") return <IntegrationPrompt form={props.form} />
return <FieldsPrompt form={props.form} />
}
function IntegrationPrompt(props: { form: FormInfo & { mode: "integration" } }) {
const sdk = useSDK()
const dialog = useDialog()
const { theme } = useTheme()
let settled = false
onMount(() => {
dialog.replace(
() => (
<DialogIntegration
integrationID={props.form.integrationID}
connectionOnly
onConnected={() => {
settled = true
dialog.clear()
void sdk.api.form.reply({ sessionID: props.form.sessionID, formID: props.form.id, answer: {} })
}}
/>
),
() => {
if (settled) return
void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
},
)
})
return (
<box
backgroundColor={theme.backgroundPanel}
border={["left"]}
borderColor={theme.accent}
customBorderChars={SplitBorder.customBorderChars}
>
<box gap={1} paddingLeft={2} paddingRight={3} paddingTop={1} paddingBottom={1}>
<text fg={theme.text}>{props.form.title ?? "Connect integration"}</text>
<text fg={theme.textMuted}>Complete the connection dialog to continue.</text>
</box>
</box>
)
}
function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) {

View file

@ -9,6 +9,7 @@ import {
const integration = (value: Partial<IntegrationInfo> & Pick<IntegrationInfo, "id" | "name">): IntegrationInfo => ({
methods: [],
capabilities: [],
connections: [],
...value,
})