import { useCallback, useEffect, useRef, useState } from "react"; import { useAtomSet, useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Exit from "effect/Exit"; import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { messageFromExit } from "@executor-js/react/api/error-reporting"; import { FormErrorAlert } from "@executor-js/react/lib/integration-add"; import { IntegrationSlug } from "@executor-js/sdk/shared"; import type { EditSheetApplyResult, EditSheetSectionProps } from "@executor-js/sdk/client"; import { Checkbox } from "@executor-js/react/components/checkbox"; import { Label } from "@executor-js/react/components/label"; import { Textarea } from "@executor-js/react/components/textarea"; import { openApiConfigAtom, updateOpenApiSpec } from "./atoms"; // --------------------------------------------------------------------------- // Update spec — the openapi plugin's section of the integration Edit sheet. // The user STAGES a spec update here (check re-fetch, or paste new content); // nothing runs until the sheet's Save. The staged apply re-resolves the spec // and rebuilds the tool catalog in place — connections, credentials, policies, // and the curated description all survive. // --------------------------------------------------------------------------- type UpdateOutcome = { readonly toolCount: number; readonly addedTools: readonly string[]; readonly removedTools: readonly string[]; }; const outcomeSummary = (outcome: UpdateOutcome): string => { const added = outcome.addedTools.length; const removed = outcome.removedTools.length; if (added === 0 && removed === 0) { return `Spec updated — ${outcome.toolCount} tools, no changes to the tool list.`; } const parts = [ ...(added > 0 ? [`+${added} tool${added !== 1 ? "s" : ""}`] : []), ...(removed > 0 ? [`−${removed} tool${removed !== 1 ? "s" : ""}`] : []), ]; return `Spec updated — ${parts.join(", ")} (${outcome.toolCount} total).`; }; export default function UpdateSpecSection(props: EditSheetSectionProps) { const slug = IntegrationSlug.make(props.integrationId); const configResult = useAtomValue(openApiConfigAtom(slug)); const doUpdate = useAtomSet(updateOpenApiSpec, { mode: "promiseExit" }); const [refetchStaged, setRefetchStaged] = useState(false); const [pasted, setPasted] = useState(""); const [error, setError] = useState(null); const config = AsyncResult.isSuccess(configResult) && configResult.value ? configResult.value : null; const specUrl = config?.specUrl; // The staged apply, rebuilt whenever the staged inputs change. Reported to // the sheet through a ref-stable callback so Save can run it. const applyStaged = useCallback(async (): Promise => { const spec = pasted.trim().length > 0 ? { kind: "blob" as const, value: pasted } : undefined; if (!spec && !refetchStaged) return { ok: true, summary: null }; setError(null); const exit = await doUpdate({ params: { slug }, payload: spec ? { spec } : {}, reactivityKeys: integrationWriteKeys, }); if (Exit.isFailure(exit)) { setError(messageFromExit(exit, "Failed to update spec")); return { ok: false }; } setRefetchStaged(false); setPasted(""); return { ok: true, summary: outcomeSummary(exit.value) }; }, [doUpdate, pasted, refetchStaged, slug]); const onPendingChangeRef = useRef(props.onPendingChange); onPendingChangeRef.current = props.onPendingChange; const hasStagedChange = refetchStaged || pasted.trim().length > 0; useEffect(() => { onPendingChangeRef.current?.(hasStagedChange ? applyStaged : null); // Clear the staged apply if this section unmounts mid-edit. return () => onPendingChangeRef.current?.(null); }, [hasStagedChange, applyStaged]); if (!config) return null; return (

Update spec

Rebuild this integration's tools from the latest spec when you save. Connections, credentials, and policies are kept.

{specUrl ? (
) : (