Q179: a failed first load says why and keeps retrying visibly

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:46:55 -04:00
1 parent 1cc67c244e
commit 7a4d986344
5 files changed
+87 -6

No files matched your search

+4 -2
View File
@@ -1,3 +1,4 @@
import { StartupScreen } from "./StartupScreen";
import type { Focus } from "../pages/types";
import { Fragment, useEffect, useMemo, useState } from "react";
import { Shell, type RouteId } from "../shared/Shell";
@@ -17,7 +18,7 @@ import { SettingsPage } from "../pages/SettingsPage";
import { OfficePage } from "../pages/OfficePage";
export function App() {
const { snapshot, actions } = useSnapshot();
const { snapshot, syncFailure, actions } = useSnapshot();
const [route, setRoute] = useState<RouteId>("home");
const [focus, setFocus] = useState<Focus | null>(null);
// Opening a page for an object remembers it; plain navigation forgets it.
@@ -61,7 +62,8 @@ export function App() {
}, [theme]);
if (!snapshot) {
return <div className="app-shell" style={{ display: "grid", placeItems: "center" }} />;
// A failed manual retry is already recorded in syncFailure, which the screen shows.
return <StartupScreen failure={syncFailure} onRetry={() => void actions.reload().catch(() => undefined)} />;
}
// A company exists from Launch on, but its setup is unfinished until the connectors step is done; that is stored, so a restart resumes it.
const unfinished = snapshot.organizations.find((organization) => organization.setupCompletedAt === null) ?? null;
+21
View File
@@ -0,0 +1,21 @@
import { Button } from "../shared/ui";
import { t } from "../i18n/i18n";
import type { SyncFailure } from "./useSnapshot";
/** What the window shows before the first data arrives: starting, or why loading failed and that it keeps retrying. */
export function StartupScreen({ failure, onRetry }: { failure: SyncFailure | null; onRetry: () => void }) {
return (
<div className="app-shell" style={{ display: "grid", placeItems: "center", padding: 24 }}>
{failure ? (
<div role="alert" style={{ maxWidth: 480, display: "grid", gap: 12, textAlign: "center" }}>
<strong>{t("AgentCompany could not load its data.")}</strong>
<div className="mono" style={{ fontSize: 12, color: "var(--text-2)", wordBreak: "break-word" }}>{failure.message}</div>
<div style={{ fontSize: 13, color: "var(--text-3)" }}>{t("Retrying every few seconds (attempt {count}).", { count: failure.attempts })}</div>
<div><Button onClick={onRetry}>{t("Retry now")}</Button></div>
</div>
) : (
<div role="status" style={{ color: "var(--text-3)" }}>{t("Starting AgentCompany…")}</div>
)}
</div>
);
}
+19 -4
View File
@@ -7,16 +7,31 @@ export interface SnapshotActions {
reload(): Promise<void>;
}
/** The latest failed load since the last success, and how many loads in a row have failed. */
export interface SyncFailure {
message: string;
attempts: number;
}
export function useSnapshot(): {
snapshot: AppSnapshot | null;
syncFailure: SyncFailure | null;
actions: SnapshotActions;
} {
const [snapshot, setSnapshot] = useState<AppSnapshot | null>(null);
const [syncFailure, setSyncFailure] = useState<SyncFailure | null>(null);
const load = useCallback(async () => {
const result = await window.agentCompany.loadSnapshot();
if (!result.ok) throw new Error(result.error.message);
setSnapshot(result.data);
try {
const result = await window.agentCompany.loadSnapshot();
if (!result.ok) throw new Error(result.error.message);
setSnapshot(result.data);
setSyncFailure(null);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setSyncFailure((current) => ({ message, attempts: (current?.attempts ?? 0) + 1 }));
throw error;
}
}, []);
const run = useCallback(async <T extends AppSnapshot | null>(
@@ -72,5 +87,5 @@ export function useSnapshot(): {
}, [load]);
const actions = useMemo<SnapshotActions>(() => ({ run, reload: load }), [run, load]);
return { snapshot, actions };
return { snapshot, syncFailure, actions };
}
+4
View File
@@ -694,6 +694,10 @@ export const zh: Record<string, string> = {
"Resets {date} (UTC)": "{date} 重置(UTC)",
"Unsaved": "未保存",
"Working language": "工作语言",
"AgentCompany could not load its data.": "AgentCompany 无法加载数据。",
"Retrying every few seconds (attempt {count}).": "每隔几秒重试一次(第 {count} 次)。",
"Retry now": "立即重试",
"Starting AgentCompany…": "正在启动 AgentCompany…",
"Direct: only its members take part, and every message wakes the other members. As the board, you can read it even when you are not a member.": "私聊:只有成员参与,每条消息都会唤醒其他成员。作为董事会,即使你不是成员也能阅读。",
"Company, division, department, project, pod and ad-hoc are labels for finding the conversation; they behave the same. An agent replies when @mentioned or already in the thread; a message that names no one goes to the most senior agent member.": "公司、事业部、部门、项目、小组和临时只是便于查找的标签,行为相同。Agent 在被 @提及或已在该话题中时回复;没有指名任何人的消息交给级别最高的 Agent 成员。",
"Workspace: everyone in the company is a member, including agents hired later, and any agent can read it.": "工作区:公司里的每个人都是成员,包括之后招聘的 Agent,任何 Agent 都能读取。",
+39
View File
@@ -0,0 +1,39 @@
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";
test("a first load that fails says why, keeps retrying, and recovers", async () => {
const profileDir = join(tmpdir(), `agentcompany-new-e2e-startup-${randomUUID()}`);
mkdirSync(profileDir, { recursive: true });
const launch = () => electron.launch({
args: [resolve("out/main/index.js")],
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0" },
});
const setDraft = (json: string | null) => {
const db = new DatabaseSync(join(profileDir, "agentcompany.db"));
db.prepare("update app_state set onboarding_draft_json = ? where id = 1").run(json);
db.close();
};
let app = await launch();
let page = await app.firstWindow();
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
await app.close();
// A stored value the snapshot cannot read makes every load fail.
setDraft("{");
app = await launch();
page = await app.firstWindow();
const failure = page.getByRole("alert");
await expect(failure).toContainText("AgentCompany could not load its data.");
await expect(failure).toContainText("JSON");
await expect(failure).toContainText(/attempt [2-9]/);
setDraft(null);
await page.getByRole("button", { name: "Retry now" }).click();
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
await app.close();
});