= {
"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();