feat(ui): surface friendly agent and project failure messages

Add friendlyAgentErrors to map raw agent/image errors to user-facing
copy, and use it for timeline error bubbles and project failure banners.
Redirect home with a toast when a project returns 403/404 instead of
showing a generic error, and render the final agent response inline under
skill progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 12:07:05 +08:00
parent d13a4c9a48
commit 92343ef1f6
5 changed files with 406 additions and 50 deletions
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, type Dispatch, type RefObject, type Set
import type { AgentChatPayload, AgentMessage, Project } from "@/domain/design";
import { designGateway } from "@/infrastructure/designGateway";
import { useUserMessage } from "@/ui/hooks/useUserMessage";
import { friendlyProjectFailureMessage } from "@/ui/lib/friendlyAgentErrors";
import { generatorTaskPollIntervalMs } from "@/ui/pages/CanvasWorkspace/canvas/generatorTaskPolling";
export type GenerationChannel = "agent" | "image-generator" | "canvas-action";
@@ -404,6 +405,5 @@ function isActiveCanvasWorkNode(node: Project["nodes"][number]) {
}
function projectFailureMessage(project: Project) {
const failure = [...project.messages].reverse().find((message) => message.role === "error" || message.status === "failed");
return failure?.content || failure?.title || "";
return friendlyProjectFailureMessage(project.messages);
}
@@ -8,6 +8,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import type { AgentContent, AgentMessage, AgentThreadSummary, CanvasNode, CanvasViewport, ExtractedTextLayer, ExtractNodeTextResponse, MockupModel, MockupModelMap, MockupPlacement, MockupSurface, NodeActionRequest, Project } from "@/domain/design";
import { useI18n, type Locale } from "@/i18n/i18n";
import { designGateway } from "@/infrastructure/designGateway";
import { httpStatusOf } from "@/infrastructure/userMessages";
import { useAuth } from "@/ui/auth/AuthProvider";
import { AccountManagementDialog } from "@/ui/components/AccountManagementDialog";
import { ArtboardPreview } from "@/ui/components/ArtboardPreview";
@@ -29,6 +30,7 @@ import { TextStyleToolbar, type TextStylePatch } from "@/ui/components/TextStyle
import { loadMotevaFontCatalog } from "@/ui/components/TextStyleToolbar/fontCatalog";
import { WorkspaceTitle } from "@/ui/components/WorkspaceTitle";
import { autoImageAdjustments, defaultImageAdjustments, parseImageAdjustments, serializeImageAdjustments } from "@/ui/lib/imageAdjustments";
import { friendlyProjectFailureMessage } from "@/ui/lib/friendlyAgentErrors";
import { canvasImageUrl } from "@/ui/lib/imageDelivery";
import { visiblePromptText } from "@/ui/lib/promptImageReferences";
import { isImageGeneratorThreadMessage, visibleAgentMessages } from "./canvas/agentMessageFilters";
@@ -257,6 +259,11 @@ function shouldBypassCanvasWheel(target: EventTarget | null) {
return target instanceof Element && Boolean(target.closest(canvasWheelGuardSelector));
}
function isProjectAccessDenied(err: unknown) {
const status = httpStatusOf(err);
return status === 403 || status === 404;
}
function expandTargetGeometryForNode(node: CanvasNode, options: ExpandOptions) {
const ratio = expandRatioValue(options.ratio, node.width / Math.max(node.height, 1));
const sourceRatio = node.width / Math.max(node.height, 1);
@@ -795,7 +802,14 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
}
}
})
.catch((err: unknown) => setError(toUserMessageText(err, "generateError")));
.catch((err: unknown) => {
if (isProjectAccessDenied(err)) {
showToast("你没有访问该项目权限", "error");
openHome();
return;
}
setError(toUserMessageText(err, "generateError"));
});
}, [applyProject, projectId, startProjectEvents]);
useEffect(() => {
@@ -5330,8 +5344,7 @@ function projectHasWebSearchEnabled(project: Project) {
}
function projectFailureMessage(project: Project) {
const failure = [...project.messages].reverse().find((message) => message.role === "error" || message.status === "failed");
return failure?.content || failure?.title || "";
return friendlyProjectFailureMessage(project.messages);
}
function runningCanvasActionThreadIds(project: Project) {