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 ( <div className="space-y-1.5"> <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground"> {props.label} </p> <pre className="overflow-x-auto rounded-md border border-border/60 bg-muted/40 px-3 py-2 font-mono text-xs leading-relaxed whitespace-pre-wrap text-foreground"> {props.line} </pre> </div> );} 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 ( <Sheet open={connection !== null} onOpenChange={props.onOpenChange}> <SheetContent side="right"> <SheetHeader> <SheetTitle>Edit connection</SheetTitle> <SheetDescription> Both fields are agent-visible: agents read them from <code>connections.list</code> when they pick an account, so this is the place to say which account this is and what it is for. </SheetDescription> </SheetHeader> <div className="flex-1 space-y-5 overflow-y-auto px-4"> <div className="space-y-1.5"> <Label htmlFor="connection-description">Description</Label> <Textarea id="connection-description" value={description} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.target.value) } placeholder="e.g. Production CRM — reads only; use the sandbox connection for writes." disabled={saving} /> </div> <div className="space-y-1.5"> <Label htmlFor="connection-label">Account label</Label> <Input id="connection-label" value={identityLabel} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setIdentityLabel(e.target.value) } placeholder="Which account this credential belongs to" disabled={saving} /> <p className="text-xs text-muted-foreground"> 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. </p> </div> {connection ? <AgentPreview label="What agents see" line={previewLine} /> : null} </div> <SheetFooter> <Button onClick={() => void handleSave()} disabled={saving || !connection}> {saving ? "Saving..." : "Save"} </Button> <Button variant="outline" onClick={() => props.onOpenChange(false)} disabled={saving}> Cancel </Button> </SheetFooter> </SheetContent> </Sheet> );} 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<EditSheetSectionProps>; 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<EditSheetApplyResult>) | null>(null); const handlePendingChange = useCallback((apply: (() => Promise<EditSheetApplyResult>) | 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 ( <Sheet open={props.open} onOpenChange={props.onOpenChange}> <SheetContent side="right"> <SheetHeader> <SheetTitle>Edit integration</SheetTitle> <SheetDescription> 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. </SheetDescription> </SheetHeader> <div className="flex-1 space-y-5 overflow-y-auto px-4"> <div className="space-y-1.5"> <Label htmlFor="integration-name">Name</Label> <Input id="integration-name" value={nameDraft} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNameDraft(e.target.value)} placeholder="e.g. GitHub" disabled={saving} /> </div> <div className="space-y-1.5"> <Label htmlFor="integration-description">Description</Label> <Textarea id="integration-description" value={descriptionDraft} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescriptionDraft(e.target.value) } onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void handleSave(); }} placeholder="What this API is and when to reach for it" disabled={saving} /> </div> <AgentPreview label="What agents see" line={ preview && preview.toLowerCase() !== slug.toLowerCase() ? `{ id: "${slug}", description: "${preview}", ... }` : `{ id: "${slug}", ... }` } /> {props.pluginSection ? ( <Suspense fallback={null}> <props.pluginSection integrationId={slug} onPendingChange={handlePendingChange} /> </Suspense> ) : null} </div> <SheetFooter> <Button onClick={() => void handleSave()} disabled={saving || nameDraft.trim().length === 0} > {saving ? "Saving..." : "Save"} </Button> <Button variant="outline" onClick={() => props.onOpenChange(false)} disabled={saving}> Cancel </Button> </SheetFooter> </SheetContent> </Sheet> );}
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 (
<div className="space-y-1.5">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
{props.label}
</p>
<pre className="overflow-x-auto rounded-md border border-border/60 bg-muted/40 px-3 py-2 font-mono text-xs leading-relaxed whitespace-pre-wrap text-foreground">
{props.line}
</pre>
</div>
);
}
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 (
<Sheet open={connection !== null} onOpenChange={props.onOpenChange}>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Edit connection</SheetTitle>
<SheetDescription>
Both fields are agent-visible: agents read them from <code>connections.list</code> when
they pick an account, so this is the place to say which account this is and what it is
for.
</SheetDescription>
</SheetHeader>
<div className="flex-1 space-y-5 overflow-y-auto px-4">
<div className="space-y-1.5">
<Label htmlFor="connection-description">Description</Label>
<Textarea
id="connection-description"
value={description}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setDescription(e.target.value)
}
placeholder="e.g. Production CRM — reads only; use the sandbox connection for writes."
disabled={saving}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="connection-label">Account label</Label>
<Input
id="connection-label"
value={identityLabel}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setIdentityLabel(e.target.value)
}
placeholder="Which account this credential belongs to"
disabled={saving}
/>
<p className="text-xs text-muted-foreground">
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.
</p>
</div>
{connection ? <AgentPreview label="What agents see" line={previewLine} /> : null}
</div>
<SheetFooter>
<Button onClick={() => void handleSave()} disabled={saving || !connection}>
{saving ? "Saving..." : "Save"}
</Button>
<Button variant="outline" onClick={() => props.onOpenChange(false)} disabled={saving}>
Cancel
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
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<EditSheetSectionProps>;
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<EditSheetApplyResult>) | null>(null);
const handlePendingChange = useCallback((apply: (() => Promise<EditSheetApplyResult>) | 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 (
<Sheet open={props.open} onOpenChange={props.onOpenChange}>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Edit integration</SheetTitle>
<SheetDescription>
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.
</SheetDescription>
</SheetHeader>
<div className="flex-1 space-y-5 overflow-y-auto px-4">
<div className="space-y-1.5">
<Label htmlFor="integration-name">Name</Label>
<Input
id="integration-name"
value={nameDraft}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNameDraft(e.target.value)}
placeholder="e.g. GitHub"
disabled={saving}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="integration-description">Description</Label>
<Textarea
id="integration-description"
value={descriptionDraft}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setDescriptionDraft(e.target.value)
}
onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void handleSave();
}}
placeholder="What this API is and when to reach for it"
disabled={saving}
/>
</div>
<AgentPreview
label="What agents see"
line={
preview && preview.toLowerCase() !== slug.toLowerCase()
? `{ id: "${slug}", description: "${preview}", ... }`
: `{ id: "${slug}", ... }`
}
/>
{props.pluginSection ? (
<Suspense fallback={null}>
<props.pluginSection integrationId={slug} onPendingChange={handlePendingChange} />
</Suspense>
) : null}
</div>
<SheetFooter>
<Button
onClick={() => void handleSave()}
disabled={saving || nameDraft.trim().length === 0}
>
{saving ? "Saving..." : "Save"}
</Button>
<Button variant="outline" onClick={() => props.onOpenChange(false)} disabled={saving}>
Cancel
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}