From fd91832bc46a1127adf68a2479fe40d75b0214e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 05:51:33 -0400 Subject: [PATCH] Q180: a failure no screen handles is shown instead of lost Co-Authored-By: Claude Opus 5.5 --- src/renderer/src/i18n/zh.ts | 1 + src/renderer/src/main.tsx | 2 ++ src/renderer/src/shared/FailureToasts.tsx | 39 +++++++++++++++++++++++ tests/e2e/desktop.spec.ts | 12 +++++++ 4 files changed, 54 insertions(+) create mode 100644 src/renderer/src/shared/FailureToasts.tsx diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index ab11df3e..1c7c1977 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -694,6 +694,7 @@ export const zh: Record = { "Resets {date} (UTC)": "{date} 重置(UTC)", "Unsaved": "未保存", "Working language": "工作语言", + "That did not work": "操作未成功", "AgentCompany could not load its data.": "AgentCompany 无法加载数据。", "Retrying every few seconds (attempt {count}).": "每隔几秒重试一次(第 {count} 次)。", "Retry now": "立即重试", diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index c9bbb3c5..85204a64 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { App } from "./app/App"; +import { FailureToasts } from "./shared/FailureToasts"; import "./styles/fonts.css"; import "./styles.css"; import "./styles/components.css"; @@ -9,5 +10,6 @@ import "./overlays.css"; ReactDOM.createRoot(document.getElementById("root")!).render( + , ); diff --git a/src/renderer/src/shared/FailureToasts.tsx b/src/renderer/src/shared/FailureToasts.tsx new file mode 100644 index 00000000..c1d8d97c --- /dev/null +++ b/src/renderer/src/shared/FailureToasts.tsx @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; +import { IconButton } from "./ui"; +import { t } from "../i18n/i18n"; + +const SHOWN = 3; + +/** Any failure no screen handled itself (a rejected action or a thrown handler) is shown here until dismissed, so nothing fails without a trace. */ +export function FailureToasts() { + const [failures, setFailures] = useState>([]); + useEffect(() => { + let next = 0; + const show = (reason: unknown) => { + const message = reason instanceof Error ? reason.message : String(reason); + setFailures((current) => [...current, { id: next++, message }].slice(-SHOWN)); + }; + const onRejection = (event: PromiseRejectionEvent) => show(event.reason); + const onError = (event: ErrorEvent) => show(event.error ?? event.message); + window.addEventListener("unhandledrejection", onRejection); + window.addEventListener("error", onError); + return () => { + window.removeEventListener("unhandledrejection", onRejection); + window.removeEventListener("error", onError); + }; + }, []); + if (failures.length === 0) return null; + return ( +
+ {failures.map((failure) => ( +
+
+ {t("That did not work")} + {failure.message} +
+ setFailures((current) => current.filter((item) => item.id !== failure.id))} /> +
+ ))} +
+ ); +} diff --git a/tests/e2e/desktop.spec.ts b/tests/e2e/desktop.spec.ts index 6f609016..55a3a313 100644 --- a/tests/e2e/desktop.spec.ts +++ b/tests/e2e/desktop.spec.ts @@ -126,6 +126,18 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await expect(page.getByText("Alex Turing").first()).toBeVisible(); await expect(page.locator(".table-row", { hasText: "Alex Turing" }).locator(".avatar", { hasText: "🚀" })).toBeVisible(); + // A failure the dialog does not handle itself still says what went wrong. + await page.getByRole("button", { name: "Hire" }).click(); + await page.locator(".modal").getByLabel("Name", { exact: true }).fill("Alex Turing"); + await page.locator(".modal").getByLabel("Role", { exact: true }).fill("Engineer"); + await page.locator(".modal").getByLabel("Department").fill("Engineering"); + await page.locator(".modal").getByRole("button", { name: "Hire" }).click(); + const toast = page.getByRole("alert").filter({ hasText: "That did not work" }); + await expect(toast).toContainText('An agent named "Alex Turing" is already on staff'); + await toast.getByRole("button", { name: "Dismiss" }).click(); + await expect(toast).toHaveCount(0); + await page.locator(".modal").getByRole("button", { name: "Cancel" }).click(); + // The operator edits their own name and avatar from the sidebar foot. await page.getByRole("button", { name: "Edit profile" }).click(); await page.locator(".modal").getByLabel("Name", { exact: true }).fill("Dana Zhang");