Q197: connector screens say whether account statuses were checked with Composio, and when

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 09:42:32 -04:00
1 parent 4ca61c5deb
commit 9aa9a2aee7
7 files changed
+70 -9

No files matched your search

@@ -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({
</div>
)}
{signIn && <SignInStatus signIn={signIn} onStop={stopWaiting} onCheckAgain={checkAgain} />}
<AccountSyncNote sync={accountSync} onRetry={syncAccounts} />
{connectError && (
<div
style={{
+3
View File
@@ -698,6 +698,9 @@ export const zh: Record<string, string> = {
"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": "再次检查",
+3 -1
View File
@@ -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 && <SignInStatus signIn={signIn} onStop={stopWaiting} onCheckAgain={checkAgain} />}
<AccountSyncNote sync={accountSync} onRetry={syncAccounts} />
<ConnectedList
accounts={accounts}
@@ -0,0 +1,21 @@
import { Button } from "../../shared/ui";
import { t } from "../../i18n/i18n";
import { formatTime } from "../types";
import type { AccountSync } from "./useConnectorCatalog";
/** Says whether the account statuses shown are what Composio reports now, and when they were last checked. */
export function AccountSyncNote({ sync, onRetry }: { sync: AccountSync; onRetry: () => void }) {
if (sync.phase === "failed") {
return (
<div role="alert" className="row" style={{ gap: 10, fontSize: 13, marginBottom: 14, color: "var(--red)" }}>
<span style={{ flex: 1, minWidth: 0, wordBreak: "break-word" }}>{t("Account status could not be checked with Composio: {reason}. The statuses below may be out of date.", { reason: sync.message })}</span>
<Button size="small" onClick={onRetry}>{t("Retry")}</Button>
</div>
);
}
return (
<div role="status" style={{ fontSize: 12, marginBottom: 14, color: "var(--text-3)" }}>
{sync.phase === "syncing" ? t("Checking account status with Composio…") : t("Account status checked with Composio at {time}.", { time: formatTime(sync.at) })}
</div>
);
}
@@ -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<string | null>(null);
const [signIn, setSignIn] = useState<SignIn | null>(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<ReturnType<typeof setTimeout> | 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,
+2
View File
@@ -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();
+2
View File
@@ -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();