diff --git a/AgentCompany-New/src/renderer/src/app/App.tsx b/AgentCompany-New/src/renderer/src/app/App.tsx index d25f1ef4..bb20ee23 100644 --- a/AgentCompany-New/src/renderer/src/app/App.tsx +++ b/AgentCompany-New/src/renderer/src/app/App.tsx @@ -8,7 +8,7 @@ import { WorkPage } from "../pages/WorkPage"; import { ChatPage } from "../pages/ChatPage"; import { FilesPage } from "../pages/FilesPage"; import { ApprovalsPage } from "../pages/ApprovalsPage"; -import { SocialPage } from "../pages/SocialPage"; +import { ConnectorsPage } from "../pages/ConnectorsPage"; import { DocumentsPage } from "../pages/DocumentsPage"; import { RunsPage } from "../pages/RunsPage"; import { SettingsPage } from "../pages/SettingsPage"; @@ -43,7 +43,7 @@ export function App() { docs: , runs: , approvals: , - social: , + connectors: , office: , settings: , }[route]; diff --git a/AgentCompany-New/src/renderer/src/pages/ConnectorsPage.tsx b/AgentCompany-New/src/renderer/src/pages/ConnectorsPage.tsx new file mode 100644 index 00000000..fff0b424 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/ConnectorsPage.tsx @@ -0,0 +1,150 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { Button } from "../shared/ui"; +import type { ComposioToolkitSummaryDto } from "../../../shared/commands"; +import type { PageProps } from "./types"; +import { activeOrgId, requireActiveEmployee } from "./types"; +import { ConnectedList } from "./connectors/ConnectedList"; +import { ToolkitCatalog } from "./connectors/ToolkitCatalog"; + +type CatalogState = + | { phase: "loading" } + | { phase: "ready"; toolkits: ComposioToolkitSummaryDto[] } + | { phase: "error"; message: string }; + +type CatalogSlot = { orgId: string; state: CatalogState } | null; + +const CONNECT_POLL_INTERVAL_MS = 4000; +const CONNECT_POLL_ATTEMPTS = 15; + +/** + * The company's connector control room: what is connected, who may use it, + * and the Composio catalog to connect more. Connections are org-level; the + * grant chips decide which agents carry each toolkit's tools. + */ +export function ConnectorsPage({ snapshot, actions }: PageProps) { + const orgId = activeOrgId(snapshot); + const employee = requireActiveEmployee(snapshot); + const [catalogSlot, setCatalogSlot] = useState(null); + const [busySlug, setBusySlug] = useState(null); + const pollTimer = useRef | null>(null); + const catalog: CatalogState = catalogSlot && catalogSlot.orgId === orgId ? catalogSlot.state : { phase: "loading" }; + + const accounts = useMemo( + () => snapshot.connectorAccounts.filter((account) => account.orgId === orgId), + [snapshot.connectorAccounts, orgId], + ); + const grants = useMemo( + () => snapshot.connectorGrants.filter((grant) => grant.orgId === orgId), + [snapshot.connectorGrants, orgId], + ); + const agents = useMemo( + () => snapshot.agents.filter((agent) => agent.orgId === orgId && agent.lifecycle === "active"), + [snapshot.agents, orgId], + ); + const connectedSlugs = useMemo( + () => new Set(accounts.filter((account) => account.status === "ACTIVE").map((account) => account.toolkitSlug)), + [accounts], + ); + + useEffect(() => { + let cancelled = false; + void window.agentCompany.listComposioToolkits({ orgId }).then((result) => { + if (cancelled) return; + setCatalogSlot({ + orgId, + state: result.ok + ? { phase: "ready", toolkits: result.data } + : { phase: "error", message: result.error.message }, + }); + }); + return () => { + cancelled = true; + }; + }, [orgId]); + + useEffect(() => () => { + if (pollTimer.current) clearTimeout(pollTimer.current); + }, []); + + const refreshAccounts = () => + actions.run(() => window.agentCompany.refreshComposioAccounts({ orgId })); + + const pollUntilActive = (toolkitSlug: string, 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, + ); + if (account?.status === "ACTIVE") return; + pollUntilActive(toolkitSlug, attempt + 1); + }); + }, CONNECT_POLL_INTERVAL_MS); + }; + + const connect = (toolkit: ComposioToolkitSummaryDto) => { + setBusySlug(toolkit.slug); + void window.agentCompany + .connectComposioToolkit({ orgId, toolkitSlug: toolkit.slug, toolkitName: toolkit.name }) + .then((result) => { + if (!result.ok) throw new Error(result.error.message); + if (result.data.redirectUrl) { + void window.agentCompany.openExternalUrl(result.data.redirectUrl); + } + pollUntilActive(toolkit.slug, 0); + }) + .finally(() => setBusySlug(null)); + }; + + const grant = (agentId: string, toolkitSlug: string) => + void actions.run(() => window.agentCompany.grantConnector({ orgId, agentId, toolkitSlug, employeeId: employee.id })); + + const revoke = (agentId: string, toolkitSlug: string) => + void actions.run(() => window.agentCompany.revokeConnector({ orgId, agentId, toolkitSlug, employeeId: employee.id })); + + return ( +
+
+
+

Connectors

+
+ Company connections your agents can be granted to use. +
+
+
+ +
+
+ + + + {catalog.phase === "loading" && ( +
Loading catalog…
+ )} + {catalog.phase === "error" && ( +
+ {catalog.message} + +
+ )} + {catalog.phase === "ready" && ( + + )} +
+ ); +} diff --git a/AgentCompany-New/src/renderer/src/pages/SocialPage.tsx b/AgentCompany-New/src/renderer/src/pages/SocialPage.tsx deleted file mode 100644 index 9ed4333c..00000000 --- a/AgentCompany-New/src/renderer/src/pages/SocialPage.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import { useMemo, useState, type FormEvent } from "react"; -import { Avatar, Button, Field, Icon, Pill } from "../shared/ui"; -import { TopbarActions } from "../shared/TopbarActions"; -import { Dialog } from "../shared/Dialog"; -import { - SOCIAL_PLATFORM, - type EmployeeRecord, - type ApprovalState, - type ScheduledPostRecord, - type SocialAccountRecord, - type SocialPlatform, -} from "../../../shared/domain"; -import { APPROVAL_STATE } from "../../../shared/domain"; -import { SocialCalendar } from "./social/SocialCalendar"; -import { SocialActivity } from "./social/SocialActivity"; -import { PlatformIcon } from "./social/PlatformIcon"; -import type { PageProps } from "./types"; -import { activeOrgId, requireActiveEmployee } from "./types"; - -function value(form: FormData, key: string): string { - return String(form.get(key) ?? "").trim(); -} - -function optional(form: FormData, key: string): string | null { - return value(form, key) || null; -} - -export function SocialPage({ snapshot, actions }: PageProps) { - const orgId = activeOrgId(snapshot); - const employee = requireActiveEmployee(snapshot); - const accounts = useMemo(() => snapshot.socialAccounts.filter((account) => account.orgId === orgId), [snapshot.socialAccounts, orgId]); - const scheduledPosts = useMemo(() => snapshot.scheduledPosts.filter((post) => post.orgId === orgId), [snapshot.scheduledPosts, orgId]); - const approvalStateById = useMemo(() => { - const states = new Map(); - for (const approval of snapshot.approvals) { - if (approval.orgId === orgId) states.set(approval.id, approval.state); - } - return states; - }, [snapshot.approvals, orgId]); - const employees = snapshot.employees.filter((item) => item.orgId === orgId); - const socialGrants = useMemo(() => snapshot.socialAccountGrants.filter((grant) => grant.orgId === orgId), [snapshot.socialAccountGrants, orgId]); - const socialAudit = useMemo(() => snapshot.socialAuditRecords.filter((record) => record.orgId === orgId), [snapshot.socialAuditRecords, orgId]); - const browserActions = useMemo(() => snapshot.browserActions.filter((action) => action.orgId === orgId), [snapshot.browserActions, orgId]); - - const [connectOpen, setConnectOpen] = useState(false); - const [draftOpen, setDraftOpen] = useState(false); - - const scheduled = scheduledPosts.filter((post) => post.state === "scheduled"); - const drafts = scheduledPosts.filter((post) => post.state === "draft"); - const history = scheduledPosts.filter((post) => post.state === "published" || post.state === "cancelled"); - const operator = accounts.find((account) => account.authorizedEmployeeId)?.authorizedEmployeeId ?? null; - const operatorName = operator ? employees.find((item) => item.id === operator)?.displayName ?? null : null; - - const connect = (event: FormEvent) => { - event.preventDefault(); - const form = new FormData(event.currentTarget); - void actions.run(() => window.agentCompany.createSocialAccount({ - orgId, - platform: value(form, "platform") as SocialPlatform, - handle: value(form, "handle"), - displayName: value(form, "displayName"), - createdByEmployeeId: employee.id, - authorizedEmployeeId: optional(form, "authorizedEmployeeId"), - })).then((result) => { if (result) setConnectOpen(false); }); - }; - - const draft = (event: FormEvent) => { - event.preventDefault(); - const form = new FormData(event.currentTarget); - void actions.run(() => window.agentCompany.createScheduledPost({ - orgId, - socialAccountId: value(form, "socialAccountId"), - bodyMd: value(form, "bodyMd"), - scheduledFor: value(form, "scheduledFor"), - createdByEmployeeId: employee.id, - })).then((result) => { if (result) setDraftOpen(false); }); - }; - - const updatePostState = (post: ScheduledPostRecord, state: "draft" | "scheduled" | "published" | "cancelled") => - void actions.run(() => window.agentCompany.setScheduledPostState({ orgId, scheduledPostId: post.id, state, employeeId: employee.id })); - - return ( -
- - - - -

Social

-

- {accounts.length} {accounts.length === 1 ? "account" : "accounts"} · {scheduled.length} scheduled - {operatorName ? ` · managed by ${operatorName}` : ""} -

- -
- {accounts.length > 0 && ( - <> -
Accounts
-
- {accounts.map((account) => ( - item.id === account.authorizedEmployeeId)?.displayName ?? null : null} - canSignIn={account.authorizedEmployeeId === employee.id} - onSignIn={() => void actions.run(() => window.agentCompany.launchSocialLogin({ orgId, socialAccountId: account.id, employeeId: employee.id }))} - /> - ))} -
- - )} - -
-
- {scheduled.length > 0 &&
Scheduled
} - {scheduled.map((post) => ( - updatePostState(post, "cancelled")} - onPublish={() => updatePostState(post, "published")} - /> - ))} - {drafts.length > 0 &&
Drafts
} - {drafts.map((post) => ( - updatePostState(post, "cancelled")} - onPublish={() => updatePostState(post, "scheduled")} - /> - ))} - {history.length > 0 &&
History
} - {history.map((post) => ( - updatePostState(post, "cancelled")} - onPublish={() => updatePostState(post, "published")} - /> - ))} -
- - -
- - -
- - setConnectOpen(false)} onSubmit={connect}> - - - - - - - - - - - setDraftOpen(false)} onSubmit={draft}> - - - -