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:
1 parent
9aa9a2aee7
commit
27d4ec3be3
10 files changed
+52
-65
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}。下方状态可能已过时。",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user