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.
-
-
- )}