Q198: connector errors are shown whole and can be copied

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:49:42 -04:00
1 parent 9aa9a2aee7
commit 27d4ec3be3
10 files changed
+52 -65

No files matched your search

+1
View File
@@ -15,6 +15,7 @@ export const IPC_CONTRACTS: { [K in InvokedMethod]: z.ZodType<InputOf<K>> } = {
loadSnapshot: noInput,
relaunchApp: noInput,
reportUnsaved: z.strictObject({ unsaved: z.boolean() }),
copyText: z.strictObject({ text: z.string().max(100_000) }),
saveViewState: z.strictObject({ key: z.string().min(1).max(200), value: z.json().refine((value) => JSON.stringify(value).length <= 4_000, "View state value is too large.") }),
runningSessionCount: noInput,
pickDirectory: noInput,
+5 -1
View File
@@ -1,6 +1,6 @@
import { bumpDataVersion, currentDataVersion } from "../services/data-version";
import { createIpcGate } from "./ipc-gate";
import { app, dialog, shell, type BrowserWindow } from "electron";
import { app, dialog, shell, type BrowserWindow, clipboard } from "electron";
import type { DatabaseConnection } from "../database/connection";
import { IPC_CHANNELS } from "../../shared/ipc";
import { fail, ok, type DesktopResult } from "../../shared/result";
@@ -159,6 +159,10 @@ export function registerIpcHandlers(deps: IpcDeps): void {
}
});
handle("completeSetup", (input) => mutate(() => completeSetup(deps.connection.db, input.orgId)));
handle("copyText", (input) => {
clipboard.writeText(input.text);
return ok(null);
});
handle("saveViewState", (input) => {
try {
saveViewState(deps.connection.db, input.key, input.value);
+1
View File
@@ -11,6 +11,7 @@ const api: DesktopApi = {
relaunchApp: () => invoke(IPC_CHANNELS.relaunchApp),
reportUnsaved: (input) => invoke(IPC_CHANNELS.reportUnsaved, input),
saveViewState: (input) => invoke(IPC_CHANNELS.saveViewState, input),
copyText: (input) => invoke(IPC_CHANNELS.copyText, input),
saveOnboardingDraft: (input) => invoke(IPC_CHANNELS.saveOnboardingDraft, input),
completeSetup: (input) => invoke(IPC_CHANNELS.completeSetup, input),
runningSessionCount: () => invoke(IPC_CHANNELS.runningSessionCount),
@@ -1,3 +1,4 @@
import { ErrorText } from "../shared/ErrorText";
import { AccountSyncNote } from "../pages/connectors/AccountSyncNote";
import { SignInStatus } from "../pages/connectors/SignInStatus";
import { useMemo } from "react";
@@ -39,24 +40,7 @@ export function ConnectorsStep({
return (
<div className="form-grid">
<h2 className="page-h1">{t("Connectors")}</h2>
{catalog.phase === "error" && (
<div className="row" style={{ gap: 10 }}>
<span
style={{
color: "var(--text-2)",
fontSize: 13,
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{catalog.message}
</span>
<Button size="small" onClick={retry}>{t("Retry")}</Button>
</div>
)}
{catalog.phase === "error" && <ErrorText message={catalog.message} action={{ label: t("Retry"), onClick: retry }} />}
{catalog.phase === "ready" && (
<div style={{ maxHeight: 420, overflowY: "auto", paddingRight: 4 }}>
<ToolkitCatalog
@@ -69,19 +53,7 @@ export function ConnectorsStep({
)}
{signIn && <SignInStatus signIn={signIn} onStop={stopWaiting} onCheckAgain={checkAgain} />}
<AccountSyncNote sync={accountSync} onRetry={syncAccounts} />
{connectError && (
<div
style={{
color: "var(--red)",
fontSize: 13,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{connectError}
</div>
)}
{connectError && <ErrorText message={connectError} />}
<div className="modal-actions" style={{ marginTop: 4 }}>
<Button tone="primary" onClick={onFinish}>{t("Finish")}</Button>
</div>
+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": "停止等待",
"Copy": "复制",
"Copied": "已复制",
"Copy failed": "复制失败",
"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}。下方状态可能已过时。",
+3 -33
View File
@@ -1,7 +1,7 @@
import { ErrorText } from "../shared/ErrorText";
import { AccountSyncNote } from "./connectors/AccountSyncNote";
import { SignInStatus } from "./connectors/SignInStatus";
import { useMemo } from "react";
import { Button } from "../shared/ui";
import { t } from "../i18n/i18n";
import type { PageProps } from "./types";
import { activeOrgId, requireActiveEmployee } from "./types";
@@ -48,20 +48,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
<div style={{ maxWidth: 1060 }}>
<h2 style={{ margin: "0 0 18px", fontSize: 16, fontWeight: 600 }}>{t("Connectors")}</h2>
{connectError && (
<div
style={{
color: "var(--text-2)",
fontSize: 13,
marginBottom: 14,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{connectError}
</div>
)}
{connectError && <ErrorText message={connectError} />}
{signIn && <SignInStatus signIn={signIn} onStop={stopWaiting} onCheckAgain={checkAgain} />}
<AccountSyncNote sync={accountSync} onRetry={syncAccounts} />
@@ -79,24 +66,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
onRevoke={revoke}
/>
{catalog.phase === "error" && (
<div className="row" style={{ gap: 10, padding: "24px 0" }}>
<span
style={{
color: "var(--text-2)",
fontSize: 13,
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{catalog.message}
</span>
<Button size="small" onClick={retry}>{t("Retry")}</Button>
</div>
)}
{catalog.phase === "error" && <ErrorText message={catalog.message} action={{ label: t("Retry"), onClick: retry }} />}
{catalog.phase === "ready" && (
<ToolkitCatalog
entries={catalog.entries}
+26
View File
@@ -0,0 +1,26 @@
import { useState } from "react";
import { Button } from "./ui";
import { t } from "../i18n/i18n";
/** A failure reason shown whole (wrapped, never cut) with a way to copy it, so the full cause can be read or passed on. */
export function ErrorText({ message, action }: { message: string; action?: { label: string; onClick: () => void } }) {
// The copy result belongs to the message it was for; a new message starts fresh.
const [copied, setCopied] = useState<{ message: string; ok: boolean } | null>(null);
const copy = copied && copied.message === message ? (copied.ok ? "copied" : "failed") : "idle";
const copyMessage = () => {
void window.agentCompany.copyText({ text: message }).then((result) => {
if (!result.ok) console.error(`[errors] copy failed: ${result.error.message}`);
setCopied({ message, ok: result.ok });
}, (error: unknown) => {
console.error(`[errors] copy failed: ${error instanceof Error ? error.message : String(error)}`);
setCopied({ message, ok: false });
});
};
return (
<div role="alert" className="row" style={{ gap: 10, alignItems: "flex-start", fontSize: 13, color: "var(--red)", marginBottom: 14 }}>
<span style={{ flex: 1, minWidth: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", userSelect: "text" }}>{message}</span>
<Button size="small" onClick={copyMessage}>{copy === "copied" ? t("Copied") : copy === "failed" ? t("Copy failed") : t("Copy")}</Button>
{action && <Button size="small" onClick={action.onClick}>{action.label}</Button>}
</div>
);
}
+2
View File
@@ -129,6 +129,8 @@ export interface DesktopApi {
reportUnsaved(input: { unsaved: boolean }): Promise<DesktopResult<null>>;
/** Remembers one piece of where the operator was; null forgets it. */
saveViewState(input: { key: string; value: unknown }): Promise<DesktopResult<null>>;
/** Puts text on the system clipboard. */
copyText(input: { text: string }): Promise<DesktopResult<null>>;
saveOnboardingDraft(input: { draft: OnboardingDraft | null }): Promise<DesktopResult<null>>;
completeSetup(input: { orgId: string }): Promise<DesktopResult<AppSnapshot>>;
/** Every agent session running now, in every company: what a restart interrupts. */
+1
View File
@@ -3,6 +3,7 @@ export const IPC_CHANNELS = {
relaunchApp: "agentcompany:relaunch-app",
reportUnsaved: "agentcompany:report-unsaved",
saveViewState: "agentcompany:save-view-state",
copyText: "agentcompany:copy-text",
saveOnboardingDraft: "agentcompany:save-onboarding-draft",
completeSetup: "agentcompany:complete-setup",
runningSessionCount: "agentcompany:running-session-count",
+7
View File
@@ -220,6 +220,13 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
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();
// A connector failure is shown whole and can be copied.
const catalogError = page.getByRole("alert").filter({ has: page.getByRole("button", { name: "Retry" }) }).filter({ hasNotText: "Account status" }).first();
await expect(catalogError).toBeVisible();
const shownReason = (await catalogError.locator("span").first().innerText()).trim();
await catalogError.getByRole("button", { name: "Copy" }).click();
await expect(catalogError.getByRole("button", { name: "Copied" })).toBeVisible();
expect((await app.evaluate(({ clipboard }) => clipboard.readText())).trim()).toBe(shownReason);
await page.getByRole("button", { name: "Settings" }).click();
await expect(page.getByText("AI CEO", { exact: true })).toBeVisible();