feat(core): add integration-backed search
This commit is contained in:
parent
41d11a8a89
commit
129012c3ee
50 changed files with 1745 additions and 478 deletions
|
|
@ -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[],
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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" } }) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
|
||||
const integration = (value: Partial<IntegrationInfo> & Pick<IntegrationInfo, "id" | "name">): IntegrationInfo => ({
|
||||
methods: [],
|
||||
capabilities: [],
|
||||
connections: [],
|
||||
...value,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue