import { Suspense, useCallback, useEffect, useRef, useState, type ComponentType } from "react";
import { useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import { type Connection, type IntegrationSlug } from "@executor-js/sdk/shared";
import type { EditSheetApplyResult, EditSheetSectionProps } from "@executor-js/sdk/client";
import { toast } from "sonner";
import { updateConnection, updateIntegration } from "../api/atoms";
import { connectionWriteKeys, integrationWriteKeys } from "../api/reactivity-keys";
import { trackEvent } from "../api/analytics";
import { messageFromExit } from "../api/error-reporting";
import { Button } from "./button";
import { Input } from "./input";
import { Label } from "./label";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "./sheet";
import { Textarea } from "./textarea";
// ---------------------------------------------------------------------------
// Metadata edit sheets — the user-curated, AGENT-VISIBLE metadata for an
// integration and for a connection. The point of the sheet (vs. an inline
// input) is the preview: it shows the record an agent reads, so editing a
// field is editing what the model sees.
// ---------------------------------------------------------------------------
/** The `connections.list` item an agent reads for this connection, reduced to
* the fields the sheet edits. The callable name is fixed at connect time and
* is not editable here; only the label and description are. */
const connectionListItemPreview = (input: {
readonly name: string;
readonly identityLabel: string;
readonly description: string;
}): string => {
const fields = [
`name: ${JSON.stringify(input.name)}`,
...(input.identityLabel ? [`identityLabel: ${JSON.stringify(input.identityLabel)}`] : []),
...(input.description ? [`description: ${JSON.stringify(input.description)}`] : []),
];
return `{ ${fields.join(", ")}, ... }`;
};
function AgentPreview(props: { readonly label: string; readonly line: string }) {
return (
{props.label}
{props.line}
);
}
export function ConnectionEditSheet(props: {
readonly connection: Connection | null;
readonly onOpenChange: (open: boolean) => void;
}) {
const { connection } = props;
const doUpdate = useAtomSet(updateConnection, { mode: "promiseExit" });
const [description, setDescription] = useState("");
const [identityLabel, setIdentityLabel] = useState("");
const [saving, setSaving] = useState(false);
useEffect(() => {
setDescription(connection?.description ?? "");
setIdentityLabel(connection?.identityLabel ?? "");
}, [connection]);
const handleSave = async () => {
if (!connection) return;
setSaving(true);
const exit = await doUpdate({
params: {
owner: connection.owner,
integration: connection.integration,
name: connection.name,
},
payload: {
description: description.trim().length > 0 ? description.trim() : null,
identityLabel: identityLabel.trim().length > 0 ? identityLabel.trim() : null,
},
reactivityKeys: connectionWriteKeys,
});
setSaving(false);
if (Exit.isFailure(exit)) {
toast.error(messageFromExit(exit, "Failed to update connection"));
return;
}
props.onOpenChange(false);
};
const previewLine = connection
? connectionListItemPreview({
name: String(connection.name),
identityLabel: identityLabel.trim(),
description: description.trim(),
})
: "";
return (
Edit connection
Both fields are agent-visible: agents read them from connections.list when
they pick an account, so this is the place to say which account this is and what it is
for.
Shown in the accounts list instead of the connection name. Agents see it too, but the
callable name stays as it was at connect time.
{connection ? : null}
);
}
export function IntegrationEditSheet(props: {
readonly slug: IntegrationSlug;
readonly open: boolean;
/** The integration's display name. */
readonly name: string;
/** The integration's agent-visible description. */
readonly description: string;
/** Plugin-owned configuration (e.g. OpenAPI spec update) rendered below the
* shared fields — the same surface the plugin's add flow configures. The
* section STAGES its change and reports an apply thunk via
* `onPendingChange`; the sheet's one Save runs it after the metadata
* update. */
readonly pluginSection?: ComponentType;
readonly onOpenChange: (open: boolean) => void;
}) {
const doUpdate = useAtomSet(updateIntegration, { mode: "promiseExit" });
const [nameDraft, setNameDraft] = useState(props.name);
const [descriptionDraft, setDescriptionDraft] = useState(props.description);
const [saving, setSaving] = useState(false);
// The plugin section's staged change — a thunk that applies it and resolves
// to a summary line. Held in a ref: it changes per keystroke in the section
// and is only read at Save.
const pendingPluginApply = useRef<(() => Promise) | null>(null);
const handlePendingChange = useCallback((apply: (() => Promise) | null) => {
pendingPluginApply.current = apply;
}, []);
useEffect(() => {
if (props.open) {
setNameDraft(props.name);
setDescriptionDraft(props.description);
}
}, [props.open, props.name, props.description]);
const handleSave = async () => {
const nextName = nameDraft.trim();
if (nextName.length === 0) return;
setSaving(true);
const exit = await doUpdate({
params: { slug: props.slug },
payload: { name: nextName, description: descriptionDraft.trim() },
reactivityKeys: integrationWriteKeys,
});
trackEvent("integration_renamed", {
integration_slug: String(props.slug),
success: Exit.isSuccess(exit),
});
if (Exit.isFailure(exit)) {
setSaving(false);
toast.error(messageFromExit(exit, "Failed to update integration"));
return;
}
// Then the plugin section's staged change (e.g. the spec update). On
// failure the section shows its own error inline — keep the sheet open.
const apply = pendingPluginApply.current;
if (apply) {
const result = await apply();
if (!result.ok) {
setSaving(false);
return;
}
if (result.summary) toast.success(result.summary);
}
setSaving(false);
props.onOpenChange(false);
};
const slug = String(props.slug);
const preview = descriptionDraft.trim().split("\n", 1)[0];
return (
Edit integration
The name is what people see; the description is agent-visible context — agents read it
when browsing integrations and as fallback context on connections without one of their
own.