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:
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={{
|
||||
|
||||
@@ -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": "再次检查",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user