diff --git a/web/src/components/chat/MessageList.tsx b/web/src/components/chat/MessageList.tsx index cf9e390..9dca5de 100644 --- a/web/src/components/chat/MessageList.tsx +++ b/web/src/components/chat/MessageList.tsx @@ -2,6 +2,7 @@ import { AlertCircleIcon } from "lucide-react"; import { useMemo, type ReactNode } from "react"; import type { AgentMessage, PendingTool, ToolExecution, ToolResultContentPart } from "@/lib/pi-events"; import { contentText } from "@/lib/pi-events"; +import { friendlyErrorMessage } from "@/lib/error-messages"; import { MessageShell } from "@/components/chat-ui/MessageShell"; import { ZeroLoader } from "@/components/chat-ui/ZeroLoader"; import { MessageView } from "./pi-transcript"; @@ -183,7 +184,7 @@ export function MessageList({
- Something went wrong. + {friendlyErrorMessage(error.message)}
)} diff --git a/web/src/components/chat/pi-transcript/MessageView.tsx b/web/src/components/chat/pi-transcript/MessageView.tsx index b514f85..2a04df4 100644 --- a/web/src/components/chat/pi-transcript/MessageView.tsx +++ b/web/src/components/chat/pi-transcript/MessageView.tsx @@ -7,6 +7,7 @@ import type { UserMessage, } from "@/lib/pi-events"; import { contentText } from "@/lib/pi-events"; +import { friendlyErrorMessage } from "@/lib/error-messages"; import { MessageShell } from "@/components/chat-ui/MessageShell"; import { Markdown } from "@/components/chat-ui/Markdown"; import { ToolCallCard } from "./ToolCallCard"; @@ -147,7 +148,9 @@ function AssistantMessageView({ {message.stopReason === "aborted" ? (
Agent was interrupted.
) : ( -
{message.errorMessage}
+
+ {friendlyErrorMessage(message.errorMessage)} +
)} )} diff --git a/web/src/lib/error-messages.ts b/web/src/lib/error-messages.ts new file mode 100644 index 0000000..ec3d357 --- /dev/null +++ b/web/src/lib/error-messages.ts @@ -0,0 +1,50 @@ +/** + * Chat errors arrive from the server as raw technical strings (an exception's + * `err.message`: stack-y SDK errors, HTTP status text, provider payloads). We + * don't want to surface those verbatim — they're noise to the user and can leak + * internals. This maps a raw error to a short, generic, user-facing line. + * + * A few broad, genuinely-actionable categories get a tailored (but still + * non-technical) message; everything else falls back to a single generic line. + */ +const GENERIC = "Something went wrong. Please try again."; + +const CATEGORIES: Array<{ match: RegExp; message: string }> = [ + { + // Rate limits / provider overload (429, "overloaded", "capacity"). + match: /\b(429|rate.?limit|overloaded|too many requests|capacity)\b/i, + message: "The service is busy right now. Please try again in a moment.", + }, + { + // Network / connectivity failures. + match: /\b(network|fetch failed|econnrefused|enotfound|econnreset|socket|offline|dns)\b/i, + message: "Connection problem. Please check your network and try again.", + }, + { + // Request took too long. + match: /\b(timed?.?out|timeout|etimedout|deadline)\b/i, + message: "The request timed out. Please try again.", + }, + { + // Conversation exceeded the model's context window. + match: /\b(context length|context window|too many tokens|maximum.*tokens|token limit)\b/i, + message: "This conversation is too long for the model. Try starting a new chat.", + }, + { + // Auth / permission issues. + match: /\b(401|403|unauthorized|forbidden|invalid api key|authentication)\b/i, + message: "There was an authentication problem. Please reconnect and try again.", + }, +]; + +/** + * Turn a raw error string into a generic, user-facing message. Returns the + * generic fallback when the input is empty or unrecognized. + */ +export function friendlyErrorMessage(raw?: string | null): string { + if (!raw) return GENERIC; + for (const { match, message } of CATEGORIES) { + if (match.test(raw)) return message; + } + return GENERIC; +} diff --git a/web/src/pages/CanvasPage.tsx b/web/src/pages/CanvasPage.tsx index 8dda24b..8cdb614 100644 --- a/web/src/pages/CanvasPage.tsx +++ b/web/src/pages/CanvasPage.tsx @@ -793,6 +793,20 @@ export function CanvasPage() { onPointerMove={onCanvasPointerMove} onWheel={onWheel} > + {/* Background dot grid — pans and zooms with the view. */} + + + + + + + {list.map((s) => ( )} - {list.length === 0 && ( -
-

- Pick a tool and click to start — or ask the agent to draw here. -

-
- )}