From bbf7ab91e276740727e30e3f567fdc692d7be979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 22 Apr 2026 19:00:53 -0400 Subject: [PATCH] fix(renderer): preserve desktop result errors --- src/renderer/App.tsx | 7 +++--- src/renderer/lib/desktop.ts | 14 +++++++++++- tests/unit/desktop-error-notice.test.ts | 30 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b44461ad..96d3bebe 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -15,7 +15,7 @@ import { useCompanyController } from "./hooks/useCompanyController"; import { useCommandPaletteHotkeys } from "./hooks/useCommandPaletteHotkeys"; import { AppActionsContext, type AppActions } from "./hooks/useAppActions"; import { SectionContext } from "./hooks/useSectionContext"; -import { createAppErrorNotice, describeDesktopError, formatDesktopResultError, unwrap } from "./lib/desktop"; +import { createAppErrorNotice, describeDesktopError, formatDesktopResultNotice } from "./lib/desktop"; function Shell({ locale, @@ -64,7 +64,7 @@ export function App() { return; } const result = await window.agentCompany.startTaskRun({ companyId: company.currentCompany.id, taskId }); - if (!result.ok) { setError(formatDesktopResultError(result.error)); return; } + if (!result.ok) { setError(formatDesktopResultNotice(result.error)); return; } setSelectedRunId(result.data); await company.refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters and company.refresh are stable @@ -74,7 +74,8 @@ export function App() { try { const run = company.runs.find((entry) => entry.id === runId); if (!run) throw new Error("Selected run is no longer available."); - unwrap(await window.agentCompany.cancelRun({ companyId: run.companyId, runId })); + const result = await window.agentCompany.cancelRun({ companyId: run.companyId, runId }); + if (!result.ok) { setError(formatDesktopResultNotice(result.error)); return; } await company.refresh(); setError(null); } catch (err) { setError(describeDesktopError(err)); } diff --git a/src/renderer/lib/desktop.ts b/src/renderer/lib/desktop.ts index f9855731..507bc9cf 100644 --- a/src/renderer/lib/desktop.ts +++ b/src/renderer/lib/desktop.ts @@ -21,6 +21,7 @@ export type AppErrorInput = const GENERIC_ERROR_CODE = "APP_ERROR"; const ACTION_REQUIRED_CODE = "ACTION_REQUIRED"; const DEFAULT_ACTION_HINT = "Use this code when reporting the issue."; +const SILENT_ERROR_CODES = new Set(["APP_SHUTTING_DOWN"]); const ACTION_HINTS_BY_CODE: Record = { ACTION_REQUIRED: "Complete the required selection, then retry the action.", @@ -29,7 +30,7 @@ const ACTION_HINTS_BY_CODE: Record = { AGENT_NOT_RUNNABLE: "Open Agents, activate the assigned agent, then retry the task.", APP_SHUTTING_DOWN: "Restart AgentCompany after shutdown completes.", BUDGET_POLICY_BLOCKED: "Open Budget, update the blocking policy, then retry the run.", - CHECKOUT_RACE_LOST: "Refresh tasks, review the current assignee, then retry the run.", + CHECKOUT_RACE_LOST: "Refresh tasks, review the current assignee, then try again.", COMPANY_NOT_ACTIVE: "Select the active company, then retry the action.", COMPANY_NOT_FOUND: "Refresh companies, select an existing company, then retry the action.", CONNECTOR_MODEL_NOT_ALLOWED: "Open Connectors, enable an allowed model, then retry the run.", @@ -76,6 +77,13 @@ export function formatDesktopResultError(error: DesktopErrorEnvelope): AppErrorN return createAppErrorNotice(error.message, error.code); } +export function formatDesktopResultNotice(error: DesktopErrorEnvelope): AppErrorNotice | null { + if (isSilentDesktopErrorCode(error.code)) { + return null; + } + return formatDesktopResultError(error); +} + export function describeDesktopError(error: unknown): AppErrorNotice { if (isAppErrorNotice(error)) { return error; @@ -150,6 +158,10 @@ function actionHintForCode(code: string): string { return DEFAULT_ACTION_HINT; } +function isSilentDesktopErrorCode(code: string): boolean { + return SILENT_ERROR_CODES.has(normalizeCode(code).toUpperCase()); +} + function isDesktopErrorEnvelope(error: unknown): error is DesktopErrorEnvelope { return Boolean( error && diff --git a/tests/unit/desktop-error-notice.test.ts b/tests/unit/desktop-error-notice.test.ts index 722b5873..d469311d 100644 --- a/tests/unit/desktop-error-notice.test.ts +++ b/tests/unit/desktop-error-notice.test.ts @@ -1,3 +1,5 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; import type { DesktopResult } from "@shared/contracts"; import { describe, expect, it } from "vitest"; import { @@ -5,6 +7,7 @@ import { describeDesktopError, DesktopError, formatDesktopResultError, + formatDesktopResultNotice, normalizeAppErrorNotice, unwrap, } from "../../src/renderer/lib/desktop"; @@ -44,6 +47,33 @@ describe("renderer desktop error notices", () => { }); }); + it("keeps checkout races actionable for retry", () => { + const notice = formatDesktopResultNotice({ + code: "CHECKOUT_RACE_LOST", + message: "Another run claimed this task first.", + }); + + expect(notice).toEqual({ + code: "CHECKOUT_RACE_LOST", + message: "Another run claimed this task first.", + actionHint: "Refresh tasks, review the current assignee, then try again.", + }); + }); + + it("suppresses shutdown noise from DesktopResult notices", () => { + expect(formatDesktopResultNotice({ + code: "APP_SHUTTING_DOWN", + message: "The app is shutting down.", + })).toBeNull(); + }); + + it("keeps App cancelRun on the DesktopResult envelope path", () => { + const source = readFileSync(join(process.cwd(), "src/renderer/App.tsx"), "utf8"); + + expect(source).not.toContain("unwrap(await window.agentCompany.cancelRun"); + expect(source).toContain("formatDesktopResultNotice(result.error)"); + }); + it("describes thrown DesktopError instances with their original code", () => { const notice = describeDesktopError(new DesktopError("TASK_UNASSIGNED", "Assign an agent before starting a run."));