Connected connectors can be deleted and reconnected; mirror sweeps stale rows and ops serialize per org

This commit is contained in:
ZacharyZhang-NY committed 2026-07-07 12:09:31 -04:00
1 parent eb16baff50
commit 38bd3588e7
11 files changed
+187 -33

No files matched your search

+11 -2
View File
@@ -13,7 +13,7 @@ import { useConnectorCatalog } from "./connectors/useConnectorCatalog";
*/
export function ConnectorsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const { catalog, busySlug, connectError, connect, retry } = useConnectorCatalog(orgId, actions);
const { catalog, busySlug, busyAction, connectError, connect, disconnect, retry } = useConnectorCatalog(orgId, actions);
const accounts = useMemo(
() => snapshot.connectorAccounts.filter((account) => account.orgId === orgId),
@@ -58,7 +58,16 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
</div>
)}
<ConnectedList accounts={accounts} grants={grants} agents={agents} logoBySlug={logoBySlug} />
<ConnectedList
accounts={accounts}
grants={grants}
agents={agents}
logoBySlug={logoBySlug}
busySlug={busySlug}
busyAction={busyAction}
onReconnect={connect}
onDelete={disconnect}
/>
{catalog.phase === "error" && (
<div className="row" style={{ gap: 10, padding: "24px 0" }}>
@@ -1,4 +1,4 @@
import { Pill } from "../../shared/ui";
import { Button, IconButton, Pill } from "../../shared/ui";
import type { AgentRecord, ConnectorAccountRecord, ConnectorGrantRecord } from "../../../../shared/domain";
import { ToolkitTile } from "./ToolkitCatalog";
@@ -15,18 +15,27 @@ const STATUS_TONE: Record<string, "green" | "amber" | "red" | undefined> = {
/**
* The company's connected toolkits, each with the roster of agents the CEO
* has granted access to. Grants are the CEO's call — made with its
* grant_connector tool — so the roster here is read-only.
* grant_connector tool — so the roster here is read-only. Reconnect restarts
* the link flow for the toolkit; delete removes the connection entirely.
*/
export function ConnectedList({
accounts,
grants,
agents,
logoBySlug,
busySlug,
busyAction,
onReconnect,
onDelete,
}: {
accounts: ConnectorAccountRecord[];
grants: ConnectorGrantRecord[];
agents: AgentRecord[];
logoBySlug: Map<string, string | null>;
busySlug: string | null;
busyAction: "connect" | "disconnect" | null;
onReconnect: (toolkitSlug: string) => void;
onDelete: (toolkitSlug: string) => void;
}) {
if (accounts.length === 0) return null;
const agentNames = new Map(agents.map((agent) => [agent.id, agent.name]));
@@ -39,6 +48,7 @@ export function ConnectedList({
.filter((grant) => grant.toolkitSlug === account.toolkitSlug)
.map((grant) => agentNames.get(grant.agentId))
.filter((name): name is string => Boolean(name));
const busy = busySlug !== null;
return (
<div key={account.id} style={{ border: "1px solid var(--border)", borderRadius: 12, padding: 14, background: "var(--panel)" }}>
<div className="row" style={{ gap: 10 }}>
@@ -50,6 +60,17 @@ export function ConnectedList({
/>
<span style={{ fontWeight: 600, fontSize: 13.5 }}>{account.toolkitName}</span>
<Pill tone={STATUS_TONE[account.status]} dot>{account.status.charAt(0) + account.status.slice(1).toLowerCase()}</Pill>
<span className="row" style={{ gap: 6, marginLeft: "auto" }}>
<Button size="small" disabled={busy} onClick={() => onReconnect(account.toolkitSlug)}>
{busySlug === account.toolkitSlug && busyAction === "connect" ? "Opening…" : "Reconnect"}
</Button>
<IconButton
icon="x"
label={`Delete ${account.toolkitName} connection`}
disabled={busy}
onClick={() => onDelete(account.toolkitSlug)}
/>
</span>
</div>
{holders.length > 0 && (
<div className="row" style={{ gap: 6, marginTop: 12, flexWrap: "wrap" }}>
@@ -53,7 +53,7 @@ export function ToolkitCatalog({
entries: ConnectorCatalogEntry[];
connectedSlugs: Set<string>;
busySlug: string | null;
onConnect: (entry: ConnectorCatalogEntry) => void;
onConnect: (toolkitSlug: string) => void;
}) {
const [query, setQuery] = useState("");
const filtered = useMemo(() => {
@@ -100,7 +100,7 @@ export function ToolkitCatalog({
{connected ? (
<Pill tone="green" dot>Connected</Pill>
) : (
<Button size="small" disabled={busySlug !== null} onClick={() => onConnect(entry)}>
<Button size="small" disabled={busySlug !== null} onClick={() => onConnect(entry.slug)}>
{busySlug === entry.slug ? "Opening…" : "Connect"}
</Button>
)}
@@ -20,12 +20,14 @@ const CONNECT_POLL_ATTEMPTS = 15;
export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
catalog: CatalogState;
busySlug: string | null;
busyAction: "connect" | "disconnect" | null;
connectError: string | null;
connect: (entry: ConnectorCatalogEntry) => void;
connect: (toolkitSlug: string) => void;
disconnect: (toolkitSlug: string) => void;
retry: () => void;
} {
const [catalogSlot, setCatalogSlot] = useState<CatalogSlot>(null);
const [busySlug, setBusySlug] = useState<string | null>(null);
const [busy, setBusy] = useState<{ slug: string; action: "connect" | "disconnect" } | null>(null);
const [connectError, setConnectError] = useState<string | null>(null);
const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const catalog: CatalogState = catalogSlot && catalogSlot.orgId === orgId ? catalogSlot.state : { phase: "loading" };
@@ -89,24 +91,40 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
poll(0);
}, [refreshAccounts, orgId]);
const connect = useCallback((entry: ConnectorCatalogEntry) => {
setBusySlug(entry.slug);
const connect = useCallback((toolkitSlug: string) => {
setBusy({ slug: toolkitSlug, action: "connect" });
setConnectError(null);
void window.agentCompany
.connectComposioToolkit({ orgId, toolkitSlug: entry.slug })
.connectComposioToolkit({ orgId, toolkitSlug })
.then(async (result) => {
if (!result.ok) {
setConnectError(result.error.message);
return;
}
pollUntilActive(entry.slug);
pollUntilActive(toolkitSlug);
if (result.data.redirectUrl) {
const opened = await window.agentCompany.openExternalUrl(result.data.redirectUrl);
if (!opened.ok) setConnectError(opened.error.message);
}
})
.finally(() => setBusySlug(null));
.finally(() => setBusy(null));
}, [orgId, pollUntilActive]);
return { catalog, busySlug, connectError, connect, retry: load };
const disconnect = useCallback((toolkitSlug: string) => {
setBusy({ slug: toolkitSlug, action: "disconnect" });
setConnectError(null);
void actions.run(() => window.agentCompany.disconnectComposioToolkit({ orgId, toolkitSlug }))
.catch((error: unknown) => setConnectError(error instanceof Error ? error.message : String(error)))
.finally(() => setBusy(null));
}, [actions, orgId]);
return {
catalog,
busySlug: busy?.slug ?? null,
busyAction: busy?.action ?? null,
connectError,
connect,
disconnect,
retry: load,
};
}