-
-
{error}
+
+
+
+
+
+ {error.code}
+
+ {error.actionHint}
+
+
{error.message}
+
) : null}
diff --git a/src/renderer/views/sections/OperationsSections.tsx b/src/renderer/views/sections/OperationsSections.tsx
index f914ae5a..a780d6ca 100644
--- a/src/renderer/views/sections/OperationsSections.tsx
+++ b/src/renderer/views/sections/OperationsSections.tsx
@@ -21,7 +21,7 @@ import { SprintManager } from "../../components/company-operations/SprintManager
import { ActionButton, EmptyState, Modal } from "../../components/ui";
import { Plus } from "../../components/ui/icons";
import { useT } from "../../i18n";
-import { unwrap } from "../../lib/desktop";
+import { describeDesktopError, unwrap } from "../../lib/desktop";
import { useAppStore } from "../../state/store";
import { useSectionContext } from "../../hooks/useSectionContext";
import { useAppActions } from "../../hooks/useAppActions";
@@ -135,7 +135,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) {
unwrap(await window.agentCompany.triggerBrowserLogin({ companyId: snapshot.currentCompanyId, socialAccountId: accountId }));
setError(null);
} catch (error) {
- setError(error instanceof Error ? error.message : String(error));
+ setError(describeDesktopError(error));
}
}}
onDelete={async (id) => {
@@ -145,7 +145,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) {
await refresh();
setError(null);
} catch (error) {
- setError(error instanceof Error ? error.message : String(error));
+ setError(describeDesktopError(error));
}
}}
onLogin={async (accountId) => {
@@ -155,7 +155,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) {
await refresh();
setError(null);
} catch (error) {
- setError(error instanceof Error ? error.message : String(error));
+ setError(describeDesktopError(error));
}
}}
onOpenPath={async (path) => {
@@ -163,7 +163,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) {
unwrap(await window.agentCompany.openPath(path));
setError(null);
} catch (error) {
- setError(error instanceof Error ? error.message : String(error));
+ setError(describeDesktopError(error));
}
}}
onCancelAction={async (actionId) => {
@@ -173,7 +173,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) {
await refresh();
setError(null);
} catch (error) {
- setError(error instanceof Error ? error.message : String(error));
+ setError(describeDesktopError(error));
}
}}
onNavigate={(sectionName, entityId) => {
diff --git a/tests/unit/desktop-error-notice.test.ts b/tests/unit/desktop-error-notice.test.ts
new file mode 100644
index 00000000..722b5873
--- /dev/null
+++ b/tests/unit/desktop-error-notice.test.ts
@@ -0,0 +1,78 @@
+import type { DesktopResult } from "@shared/contracts";
+import { describe, expect, it } from "vitest";
+import {
+ createAppErrorNotice,
+ describeDesktopError,
+ DesktopError,
+ formatDesktopResultError,
+ normalizeAppErrorNotice,
+ unwrap,
+} from "../../src/renderer/lib/desktop";
+
+describe("renderer desktop error notices", () => {
+ it("keeps DesktopResult error codes when unwrap throws", () => {
+ const result: DesktopResult
= {
+ ok: false,
+ error: {
+ code: "CONNECTOR_MODEL_NOT_ALLOWED",
+ message: "The assigned agent model is disabled.",
+ },
+ };
+
+ expect(() => unwrap(result)).toThrow(DesktopError);
+
+ try {
+ unwrap(result);
+ throw new Error("unwrap should have thrown");
+ } catch (error) {
+ expect(error).toBeInstanceOf(DesktopError);
+ expect((error as DesktopError).code).toBe("CONNECTOR_MODEL_NOT_ALLOWED");
+ expect((error as DesktopError).message).toBe("The assigned agent model is disabled.");
+ }
+ });
+
+ it("turns IPC error envelopes into actionable notices", () => {
+ const notice = formatDesktopResultError({
+ code: "CONNECTOR_MODEL_NOT_ALLOWED",
+ message: "The assigned agent model is disabled or invalid for its connector.",
+ });
+
+ expect(notice).toEqual({
+ code: "CONNECTOR_MODEL_NOT_ALLOWED",
+ message: "The assigned agent model is disabled or invalid for its connector.",
+ actionHint: "Open Connectors, enable an allowed model, then retry the run.",
+ });
+ });
+
+ it("describes thrown DesktopError instances with their original code", () => {
+ const notice = describeDesktopError(new DesktopError("TASK_UNASSIGNED", "Assign an agent before starting a run."));
+
+ expect(notice).toEqual({
+ code: "TASK_UNASSIGNED",
+ message: "Assign an agent before starting a run.",
+ actionHint: "Assign an agent to the task, then start the run.",
+ });
+ });
+
+ it("normalizes local string errors into action-required notices", () => {
+ expect(normalizeAppErrorNotice("Select a company before starting a run.")).toEqual({
+ code: "ACTION_REQUIRED",
+ message: "Select a company before starting a run.",
+ actionHint: "Complete the required selection, then retry the action.",
+ });
+ });
+
+ it("preserves explicit local action hints", () => {
+ expect(
+ createAppErrorNotice(
+ "Select a company before starting a run.",
+ "COMPANY_REQUIRED",
+ "Select a company in the sidebar, then start the run.",
+ ),
+ ).toEqual({
+ code: "COMPANY_REQUIRED",
+ message: "Select a company before starting a run.",
+ actionHint: "Select a company in the sidebar, then start the run.",
+ });
+ });
+});