Q181: a render error stays inside its page and says what failed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 05:56:11 -04:00
1 parent fd91832bc4
commit 3039835644
5 files changed
+98 -3

No files matched your search

+3 -2
View File
@@ -1,6 +1,7 @@
import { ErrorBoundary } from "../shared/ErrorBoundary";
import { StartupScreen } from "./StartupScreen";
import type { Focus } from "../pages/types";
import { Fragment, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Shell, type RouteId } from "../shared/Shell";
import { AvatarDirectoryContext, type AvatarLook } from "../shared/ui";
import { Onboarding } from "./Onboarding";
@@ -105,7 +106,7 @@ export function App() {
onDeleteOrg={(orgId) => void actions.run(() => window.agentCompany.deleteOrganization({ orgId }))}
onNewOrg={() => setCreatingOrg(true)}
>
<Fragment key={pageKey}>{page}</Fragment>
<ErrorBoundary key={pageKey} scope="page">{page}</ErrorBoundary>
</Shell>
</AvatarDirectoryContext.Provider>
);
+6
View File
@@ -695,6 +695,12 @@ export const zh: Record<string, string> = {
"Unsaved": "未保存",
"Working language": "工作语言",
"That did not work": "操作未成功",
"This page could not be shown.": "此页面无法显示。",
"AgentCompany hit an error it could not recover from here.": "AgentCompany 遇到了无法在此恢复的错误。",
"The rest of the app still works; go to another page or try this one again.": "应用的其他部分仍可使用;可以前往其他页面,或重试此页面。",
"Reloading the window starts it again; your data is kept.": "重新加载窗口即可重新开始;你的数据会保留。",
"Try again": "重试",
"Reload": "重新加载",
"AgentCompany could not load its data.": "AgentCompany 无法加载数据。",
"Retrying every few seconds (attempt {count}).": "每隔几秒重试一次(第 {count} 次)。",
"Retry now": "立即重试",
+4 -1
View File
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./app/App";
import { FailureToasts } from "./shared/FailureToasts";
import { ErrorBoundary } from "./shared/ErrorBoundary";
import "./styles/fonts.css";
import "./styles.css";
import "./styles/components.css";
@@ -9,7 +10,9 @@ import "./overlays.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
<ErrorBoundary scope="app">
<App />
</ErrorBoundary>
<FailureToasts />
</React.StrictMode>,
);
+36
View File
@@ -0,0 +1,36 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
import { Button } from "./ui";
import { t } from "../i18n/i18n";
/** Contains a render error to the part of the app that threw, says what failed, and offers a way back; the error is logged with its component stack. */
export class ErrorBoundary extends Component<{ scope: "page" | "app"; children: ReactNode }, { error: Error | null }> {
state: { error: Error | null } = { error: null };
static getDerivedStateFromError(error: Error): { error: Error } {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.error(`[render] ${this.props.scope} failed: ${error.message}`, info.componentStack);
}
render(): ReactNode {
const { error } = this.state;
if (!error) return this.props.children;
const page = this.props.scope === "page";
return (
<div role="alert" style={{ padding: 24, display: "grid", gap: 10, maxWidth: 560 }}>
<strong>{page ? t("This page could not be shown.") : t("AgentCompany hit an error it could not recover from here.")}</strong>
<div className="mono" style={{ fontSize: 12, color: "var(--text-2)", wordBreak: "break-word" }}>{error.message}</div>
<div style={{ fontSize: 13, color: "var(--text-3)" }}>
{page ? t("The rest of the app still works; go to another page or try this one again.") : t("Reloading the window starts it again; your data is kept.")}
</div>
<div>
{page
? <Button onClick={() => this.setState({ error: null })}>{t("Try again")}</Button>
: <Button onClick={() => window.location.reload()}>{t("Reload")}</Button>}
</div>
</div>
);
}
}
+49
View File
@@ -0,0 +1,49 @@
import { _electron as electron, expect, test } from "@playwright/test";
import { mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { randomUUID } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { seedBuiltinSkillsFixture } from "./builtin-skills-fixture";
test("a record that breaks one page leaves the rest of the app working", async () => {
const profileDir = join(tmpdir(), `agentcompany-new-e2e-render-${randomUUID()}`);
mkdirSync(profileDir, { recursive: true });
const builtinDir = seedBuiltinSkillsFixture(join(tmpdir(), `agentcompany-new-e2e-render-builtin-${randomUUID()}`));
const app = await electron.launch({
args: [resolve("out/main/index.js")],
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0", AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR: builtinDir },
});
try {
const page = await app.firstWindow();
await page.getByRole("button", { name: "Get started" }).click();
await page.getByLabel("API key").fill("e2e-secret");
await page.getByLabel("Model").fill("gpt-5.4");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByLabel("Company name").fill("Render Co");
await page.getByLabel("Your name").fill("Dana Lee");
await page.getByLabel("Description").fill("d");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByLabel("Goal").fill("Ship.");
await page.getByLabel("Acceptance criteria").fill("It ships.");
await page.getByRole("button", { name: "Launch" }).click();
await page.getByRole("button", { name: "Finish" }).click();
// A pending request with a risk level the Approvals page cannot draw.
const db = new DatabaseSync(join(profileDir, "agentcompany.db"));
const orgId = (db.prepare("select id from organizations").get() as { id: string }).id;
db.prepare("insert into approvals(id, org_id, kind, title, details, risk, requested_by_type, state, created_at) values ('app_bad', ?, 'task_risk', 'Broken request', 'd', 'unknown', 'system', 'pending', ?)")
.run(orgId, new Date().toISOString());
db.close();
await page.getByRole("button", { name: "Approvals" }).click();
const failure = page.getByRole("alert").filter({ hasText: "This page could not be shown." });
await expect(failure).toContainText("Approval risk tone is missing.");
await page.getByRole("button", { name: "Home" }).click();
await expect(failure).toHaveCount(0);
await expect(page.getByRole("button", { name: "Approvals" })).toBeVisible();
} finally {
await app.close();
}
});