Q101: a connector can hold several labelled company accounts, each granted and used by name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 16:46:46 -04:00
1 parent 736c45c0ee
commit 4a6641fd49
23 files changed
+306 -151

No files matched your search

@@ -3,6 +3,7 @@ import type { AppSnapshot } from "../../../shared/domain";
import { Button } from "../shared/ui";
import { ToolkitCatalog } from "../pages/connectors/ToolkitCatalog";
import { useConnectorCatalog } from "../pages/connectors/useConnectorCatalog";
import { DEFAULT_CONNECTOR_ACCOUNT_LABEL } from "../../../shared/commands";
import type { SnapshotActions } from "./useSnapshot";
import { t } from "../i18n/i18n";
@@ -60,7 +61,7 @@ export function ConnectorsStep({
entries={catalog.entries}
connectedSlugs={connectedSlugs}
busySlug={busySlug}
onConnect={connect}
onConnect={(slug) => connect(slug, DEFAULT_CONNECTOR_ACCOUNT_LABEL)}
/>
</div>
)}
+3
View File
@@ -383,6 +383,9 @@ export const zh: Record<string, string> = {
"Recently decided": "最近已决",
"Reconnect": "重新连接",
"Reconnect waiting for authorization": "重新连接等待授权",
"Service": "服务",
"Account label, e.g. sales": "账号标签,例如 sales",
"Add account": "添加账号",
"Refresh models": "刷新模型",
"Region": "区域",
"Rejected": "已驳回",
+3 -2
View File
@@ -6,6 +6,7 @@ import { activeOrgId } from "./types";
import { ConnectedList } from "./connectors/ConnectedList";
import { ToolkitCatalog } from "./connectors/ToolkitCatalog";
import { useConnectorCatalog } from "./connectors/useConnectorCatalog";
import { DEFAULT_CONNECTOR_ACCOUNT_LABEL } from "../../../shared/commands";
/**
* The company's connector control room: what is connected, who holds it, and
@@ -66,7 +67,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
logoBySlug={logoBySlug}
busySlug={busySlug}
busyAction={busyAction}
onReconnect={connect}
onConnect={connect}
onDelete={disconnect}
/>
@@ -93,7 +94,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
entries={catalog.entries}
connectedSlugs={connectedSlugs}
busySlug={busySlug}
onConnect={connect}
onConnect={(slug) => connect(slug, DEFAULT_CONNECTOR_ACCOUNT_LABEL)}
/>
)}
</div>
@@ -1,3 +1,4 @@
import { useState, type FormEvent } from "react";
import { Button, IconButton, Pill } from "../../shared/ui";
import type { AgentRecord, ConnectorAccountRecord, ConnectorGrantRecord } from "../../../../shared/domain";
import { ToolkitTile } from "./ToolkitCatalog";
@@ -26,7 +27,7 @@ export function ConnectedList({
logoBySlug,
busySlug,
busyAction,
onReconnect,
onConnect,
onDelete,
}: {
accounts: ConnectorAccountRecord[];
@@ -35,18 +36,26 @@ export function ConnectedList({
logoBySlug: Map<string, string | null>;
busySlug: string | null;
busyAction: "connect" | "disconnect" | null;
onReconnect: (toolkitSlug: string) => void;
onDelete: (toolkitSlug: string) => void;
onConnect: (toolkitSlug: string, label: string) => void;
onDelete: (toolkitSlug: string, label: string) => void;
}) {
const [addSlug, setAddSlug] = useState("");
const [addLabel, setAddLabel] = useState("");
if (accounts.length === 0) return null;
const agentNames = new Map(agents.map((agent) => [agent.id, agent.name]));
const toolkits = [...new Map(accounts.map((account) => [account.toolkitSlug, { slug: account.toolkitSlug, name: account.toolkitName }])).values()];
const addAccount = (event: FormEvent) => {
event.preventDefault();
onConnect(addSlug, addLabel);
setAddLabel("");
};
return (
<section style={{ marginBottom: 24 }}>
<div className="section-label" style={{ marginBottom: 12 }}>{t("Connected")}</div>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{accounts.map((account) => {
const holders = grants
.filter((grant) => grant.toolkitSlug === account.toolkitSlug)
.filter((grant) => grant.toolkitSlug === account.toolkitSlug && grant.accountLabel === account.label)
.map((grant) => agentNames.get(grant.agentId))
.filter((name): name is string => Boolean(name));
const busy = busySlug !== null;
@@ -60,17 +69,18 @@ export function ConnectedList({
size={30}
/>
<span style={{ fontWeight: 600, fontSize: 13.5 }}>{account.toolkitName}</span>
<span style={{ color: "var(--text-2)", fontSize: 12.5 }}>{account.label}</span>
<Pill tone={STATUS_TONE[account.status]} dot>{account.status.charAt(0) + account.status.slice(1).toLowerCase()}</Pill>
{account.pendingAccountId && <Pill tone="amber" dot>{t("Reconnect waiting for authorization")}</Pill>}
<span className="row" style={{ gap: 6, marginLeft: "auto" }}>
<Button size="small" disabled={busy} onClick={() => onReconnect(account.toolkitSlug)}>
<Button size="small" disabled={busy} onClick={() => onConnect(account.toolkitSlug, account.label)}>
{busySlug === account.toolkitSlug && busyAction === "connect" ? t("Opening…") : t("Reconnect")}
</Button>
<IconButton
icon="x"
label={t("Delete {name} connection", { name: account.toolkitName })}
label={t("Delete {name} connection", { name: `${account.toolkitName} ${account.label}` })}
disabled={busy}
onClick={() => onDelete(account.toolkitSlug)}
onClick={() => onDelete(account.toolkitSlug, account.label)}
/>
</span>
</div>
@@ -83,6 +93,14 @@ export function ConnectedList({
);
})}
</div>
<form className="row" style={{ gap: 8, marginTop: 12 }} onSubmit={addAccount}>
<select className="input" style={{ width: 180 }} value={addSlug} onChange={(event) => setAddSlug(event.target.value)} required>
<option value="" disabled>{t("Service")}</option>
{toolkits.map((toolkit) => <option key={toolkit.slug} value={toolkit.slug}>{toolkit.name}</option>)}
</select>
<input className="input" style={{ width: 200 }} placeholder={t("Account label, e.g. sales")} value={addLabel} onChange={(event) => setAddLabel(event.target.value)} required maxLength={40} />
<Button size="small" type="submit" disabled={busySlug !== null}>{t("Add account")}</Button>
</form>
</section>
);
}
@@ -22,8 +22,8 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
busySlug: string | null;
busyAction: "connect" | "disconnect" | null;
connectError: string | null;
connect: (toolkitSlug: string) => void;
disconnect: (toolkitSlug: string) => void;
connect: (toolkitSlug: string, label: string) => void;
disconnect: (toolkitSlug: string, label: string) => void;
retry: () => void;
} {
const [catalogSlot, setCatalogSlot] = useState<CatalogSlot>(null);
@@ -75,13 +75,13 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
[actions, orgId],
);
const pollUntilActive = useCallback((toolkitSlug: string) => {
const pollUntilActive = useCallback((toolkitSlug: string, label: string) => {
const poll = (attempt: number) => {
if (attempt >= CONNECT_POLL_ATTEMPTS) return;
pollTimer.current = setTimeout(() => {
void refreshAccounts().then((data) => {
const account = data?.connectorAccounts.find(
(item) => item.orgId === orgId && item.toolkitSlug === toolkitSlug,
(item) => item.orgId === orgId && item.toolkitSlug === toolkitSlug && item.label === label,
);
if (account?.status === "ACTIVE" && !account.pendingAccountId) return;
poll(attempt + 1);
@@ -91,17 +91,17 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
poll(0);
}, [refreshAccounts, orgId]);
const connect = useCallback((toolkitSlug: string) => {
const connect = useCallback((toolkitSlug: string, label: string) => {
setBusy({ slug: toolkitSlug, action: "connect" });
setConnectError(null);
void window.agentCompany
.connectComposioToolkit({ orgId, toolkitSlug })
.connectComposioToolkit({ orgId, toolkitSlug, label })
.then(async (result) => {
if (!result.ok) {
setConnectError(result.error.message);
return;
}
pollUntilActive(toolkitSlug);
pollUntilActive(toolkitSlug, label.trim().toLowerCase());
if (result.data.redirectUrl) {
const opened = await window.agentCompany.openExternalUrl(result.data.redirectUrl);
if (!opened.ok) setConnectError(opened.error.message);
@@ -110,10 +110,10 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): {
.finally(() => setBusy(null));
}, [orgId, pollUntilActive]);
const disconnect = useCallback((toolkitSlug: string) => {
const disconnect = useCallback((toolkitSlug: string, label: string) => {
setBusy({ slug: toolkitSlug, action: "disconnect" });
setConnectError(null);
void actions.run(() => window.agentCompany.disconnectComposioToolkit({ orgId, toolkitSlug }))
void actions.run(() => window.agentCompany.disconnectComposioToolkit({ orgId, toolkitSlug, label }))
.catch((error: unknown) => setConnectError(error instanceof Error ? error.message : String(error)))
.finally(() => setBusy(null));
}, [actions, orgId]);