diff --git a/src/renderer/src/app/OnboardingConnectorsStep.tsx b/src/renderer/src/app/OnboardingConnectorsStep.tsx index e54eb71b..5a03a124 100644 --- a/src/renderer/src/app/OnboardingConnectorsStep.tsx +++ b/src/renderer/src/app/OnboardingConnectorsStep.tsx @@ -1,3 +1,4 @@ +import { AccountSyncNote } from "../pages/connectors/AccountSyncNote"; import { SignInStatus } from "../pages/connectors/SignInStatus"; import { useMemo } from "react"; import type { AppSnapshot } from "../../../shared/domain"; @@ -24,7 +25,7 @@ export function ConnectorsStep({ actions: SnapshotActions; onFinish: () => void; }) { - const { catalog, busySlug, connectError, signIn, stopWaiting, checkAgain, connect, retry } = useConnectorCatalog(orgId, actions); + const { catalog, busySlug, connectError, signIn, accountSync, syncAccounts, stopWaiting, checkAgain, connect, retry } = useConnectorCatalog(orgId, actions); const connectedSlugs = useMemo( () => new Set( @@ -67,6 +68,7 @@ export function ConnectorsStep({ )} {signIn && } + {connectError && (
= { "Working…": "处理中…", "Waiting for the {name} sign-in in your browser (check {check} of {checks}).": "正在等待你在浏览器中完成 {name} 登录(第 {check}/{checks} 次检查)。", "Stop waiting": "停止等待", + "Checking account status with Composio…": "正在向 Composio 核对账号状态…", + "Account status checked with Composio at {time}.": "已于 {time} 向 Composio 核对账号状态。", + "Account status could not be checked with Composio: {reason}. The statuses below may be out of date.": "无法向 Composio 核对账号状态:{reason}。下方状态可能已过时。", "{name} ({label}) is connected.": "{name}({label})已连接。", "{name} ({label}) is still not connected. If you finished signing in, check again; otherwise connect again.": "{name}({label})仍未连接。如果已经完成登录,请再次检查;否则请重新连接。", "Check again": "再次检查", diff --git a/src/renderer/src/pages/ConnectorsPage.tsx b/src/renderer/src/pages/ConnectorsPage.tsx index 6f6856bf..dd6a874f 100644 --- a/src/renderer/src/pages/ConnectorsPage.tsx +++ b/src/renderer/src/pages/ConnectorsPage.tsx @@ -1,3 +1,4 @@ +import { AccountSyncNote } from "./connectors/AccountSyncNote"; import { SignInStatus } from "./connectors/SignInStatus"; import { useMemo } from "react"; import { Button } from "../shared/ui"; @@ -17,7 +18,7 @@ import { DEFAULT_CONNECTOR_ACCOUNT_LABEL } from "../../../shared/commands"; export function ConnectorsPage({ snapshot, actions }: PageProps) { const orgId = activeOrgId(snapshot); const employee = requireActiveEmployee(snapshot); - const { catalog, busySlug, busyAction, connectError, signIn, stopWaiting, checkAgain, connect, disconnect, grant, revoke, retry } = useConnectorCatalog(orgId, actions); + const { catalog, busySlug, busyAction, connectError, signIn, accountSync, syncAccounts, stopWaiting, checkAgain, connect, disconnect, grant, revoke, retry } = useConnectorCatalog(orgId, actions); const accounts = useMemo( () => snapshot.connectorAccounts.filter((account) => account.orgId === orgId), @@ -63,6 +64,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { )} {signIn && } + void }) { + if (sync.phase === "failed") { + return ( +
+ {t("Account status could not be checked with Composio: {reason}. The statuses below may be out of date.", { reason: sync.message })} + +
+ ); + } + return ( +
+ {sync.phase === "syncing" ? t("Checking account status with Composio…") : t("Account status checked with Composio at {time}.", { time: formatTime(sync.at) })} +
+ ); +} diff --git a/src/renderer/src/pages/connectors/useConnectorCatalog.ts b/src/renderer/src/pages/connectors/useConnectorCatalog.ts index c077f286..f612df03 100644 --- a/src/renderer/src/pages/connectors/useConnectorCatalog.ts +++ b/src/renderer/src/pages/connectors/useConnectorCatalog.ts @@ -12,6 +12,9 @@ export type CatalogState = type CatalogSlot = { orgId: string; state: CatalogState } | null; +/** Whether the account statuses on screen are what Composio reports now; the catalog loading says nothing about this. */ +export type AccountSync = { phase: "syncing" } | { phase: "synced"; at: string } | { phase: "failed"; message: string }; + /** Where a sign-in started from this window stands; it always ends connected, failed, or timed out. */ export type SignIn = { slug: string; label: string; accountId: string } & ( | { phase: "waiting"; check: number; checks: number } @@ -34,6 +37,8 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): { busyAction: "connect" | "disconnect" | null; connectError: string | null; signIn: SignIn | null; + accountSync: AccountSync; + syncAccounts: () => void; stopWaiting: () => void; checkAgain: () => void; connect: (toolkitSlug: string, label: string) => void; @@ -46,6 +51,8 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): { const [busy, setBusy] = useState<{ slug: string; action: "connect" | "disconnect" } | null>(null); const [connectError, setConnectError] = useState(null); const [signIn, setSignIn] = useState(null); + const [sync, setSync] = useState<{ orgId: string; state: AccountSync } | null>(null); + const accountSync: AccountSync = sync && sync.orgId === orgId ? sync.state : { phase: "syncing" }; const pollTimer = useRef | null>(null); // Each sign-in check run has its own number; a stopped or replaced run's late answers are dropped. const pollRun = useRef(0); @@ -84,17 +91,37 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): { if (pollTimer.current) clearTimeout(pollTimer.current); }, []); - // Account statuses sync from Composio on mount; a failure here is already - // expressed by the catalog error state. - useEffect(() => { - void window.agentCompany.refreshComposioAccounts({ orgId }); - }, [orgId]); - + // Every account refresh records whether it worked, so stale statuses are never shown as current. const refreshAccounts = useCallback( - () => actions.run(() => window.agentCompany.refreshComposioAccounts({ orgId })), + () => actions.run(() => window.agentCompany.refreshComposioAccounts({ orgId })).then((data) => { + setSync({ orgId, state: { phase: "synced", at: new Date().toISOString() } }); + return data; + }, (error: unknown) => { + setSync({ orgId, state: { phase: "failed", message: error instanceof Error ? error.message : String(error) } }); + throw error; + }), [actions, orgId], ); + const syncAccounts = useCallback(() => { + setSync({ orgId, state: { phase: "syncing" } }); + // The outcome is shown through accountSync. + void refreshAccounts().catch(() => undefined); + }, [refreshAccounts, orgId]); + + useEffect(() => { + let live = true; + void window.agentCompany.refreshComposioAccounts({ orgId }).then((result) => { + if (!live) return; + setSync({ orgId, state: result.ok ? { phase: "synced", at: new Date().toISOString() } : { phase: "failed", message: result.error.message } }); + }, (error: unknown) => { + if (live) setSync({ orgId, state: { phase: "failed", message: error instanceof Error ? error.message : String(error) } }); + }); + return () => { + live = false; + }; + }, [orgId]); + // Checks the account until it is ACTIVE, failed, or the checks run out; the card stays busy until one of those. const pollUntilActive = useCallback((toolkitSlug: string, label: string, accountId: string) => { if (pollTimer.current) clearTimeout(pollTimer.current); @@ -198,6 +225,8 @@ export function useConnectorCatalog(orgId: string, actions: SnapshotActions): { busyAction: busy?.action ?? null, connectError, signIn, + accountSync, + syncAccounts, stopWaiting, checkAgain, connect, diff --git a/tests/e2e/desktop.spec.ts b/tests/e2e/desktop.spec.ts index c69305bd..4391cea4 100644 --- a/tests/e2e/desktop.spec.ts +++ b/tests/e2e/desktop.spec.ts @@ -218,6 +218,8 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await page.getByRole("button", { name: "Connectors", exact: true }).click(); await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible(); await expect(page.getByRole("button", { name: "Refresh" })).toHaveCount(0); + // Without a Composio key the account check fails, and the page says the statuses may be stale. + await expect(page.getByRole("alert").filter({ hasText: "Account status could not be checked with Composio" })).toBeVisible(); await page.getByRole("button", { name: "Settings" }).click(); await expect(page.getByText("AI CEO", { exact: true })).toBeVisible(); diff --git a/tests/e2e/onboarding-resume.spec.ts b/tests/e2e/onboarding-resume.spec.ts index 0b4724d1..0d323a47 100644 --- a/tests/e2e/onboarding-resume.spec.ts +++ b/tests/e2e/onboarding-resume.spec.ts @@ -57,6 +57,8 @@ test("onboarding resumes where it was left after the app is closed", async () => app = await launch(); page = await app.firstWindow(); await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible(); + // The step says when connector statuses could not be checked, as the Connectors page does. + await expect(page.getByRole("alert").filter({ hasText: "Account status could not be checked with Composio" })).toBeVisible(); await page.getByRole("button", { name: "Finish" }).click(); await expect(page.getByRole("button", { name: "Settings" })).toBeVisible();