From 9eb8fa1c17c048ba20956bf7e1f81935f10b8e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 6 Aug 2026 23:33:10 +0000 Subject: [PATCH] say something when the chat fails silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read-aloud swallowed every error: the spinner stopped, the speaker icon came back, and a TTS service that was simply down looked identical to a button that did nothing. Both the synthesis failure and the playback failure now say so. errorText moves to helpers — useClient rejects with a plain { status, message } object rather than an Error, so the reflex instanceof check reports every API failure in the app as "unknown error". Two callers now, and it is the wrong thing to reimplement per file. Co-Authored-By: Claude Opus 5 --- src/workspaces/helpers/error-text.ts | 18 ++++++++++++++++++ .../src/apps/Chat/components/MessageBubble.tsx | 9 ++++++++- .../src/apps/ChatHistory/SessionList.tsx | 10 +--------- 3 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 src/workspaces/helpers/error-text.ts diff --git a/src/workspaces/helpers/error-text.ts b/src/workspaces/helpers/error-text.ts new file mode 100644 index 00000000..fc2bf7e4 --- /dev/null +++ b/src/workspaces/helpers/error-text.ts @@ -0,0 +1,18 @@ +/** + * A readable string for anything that was thrown. + * + * `useClient` rejects with a plain `{ status, message }` object, not an `Error`, so the reflex + * `err instanceof Error ? err.message : …` misses the most common failure in this app entirely and + * reports every API error as "unknown error". Check the shape first, the class second. + * + * Truncated because these end up in toasts, and a server that answers a failure with a wall of HTML + * should not push the rest of the screen off it. + */ +export function errorText(err: unknown, fallback = 'unknown error'): string { + if (typeof err === 'object' && err !== null && 'message' in err) { + const message = (err as { message: unknown }).message; + if (typeof message === 'string' && message.trim()) return message.slice(0, 200); + } + if (typeof err === 'string' && err.trim()) return err.slice(0, 200); + return fallback; +} diff --git a/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx b/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx index 8e4a22f3..ae680556 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx @@ -4,8 +4,10 @@ import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import { Volume2, Loader2, Square, Clock, Check, X, CircleSlash } from 'lucide-react'; +import { toast } from '@/components/ui/sonner'; import type { Tone } from '@/components/Data'; import { toneText } from '@/components/Data'; +import { errorText } from 'helpers/error-text'; import type { ChatMessage } from '../types'; import { ToolActivity } from './ToolActivity'; import { QuestionActivity } from './QuestionActivity'; @@ -91,11 +93,16 @@ const ReadAloudButton = ({ id, text }: { id: string; text: string }) => { audio.onerror = () => { audioRef.current = null; setState('idle'); + toast.error('The narration was generated but would not play.'); }; await audio.play(); setState('playing'); - } catch { + } catch (err) { + // Synthesis is slow enough that the spinner is the only feedback there is; when it just stops and + // the icon comes back, the button looks broken rather than failed. Most often this is the TTS + // service being down, which is worth saying out loud. setState('idle'); + toast.error(`Could not read this message aloud: ${errorText(err)}`); } }; diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx index fcc1668e..b00f8ef8 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx @@ -4,6 +4,7 @@ import { Plus, MessageSquare, RefreshCw, Trash2, Pencil, Check, X } from 'lucide import { toast } from '@/components/ui/sonner'; import { DataList, DataRow, EmptyBlock, ErrorBlock, LoadingBlock, RelativeTime } from '@/components/Data'; import { usePanelChannel } from 'hooks/usePanelChannel'; +import { errorText } from 'helpers/error-text'; import { useClaudeSessions } from 'state/useClaudeSessions'; import type { SelectedSession } from './ChatDetailPanel'; import { PwdSelector } from './PwdSelector'; @@ -257,12 +258,3 @@ export const SessionList = () => { ); }; - -/** `useClient` throws `{ status, message }`, not an Error, so `err.message` alone misses the common case. */ -function errorText(err: unknown): string { - if (typeof err === 'object' && err !== null && 'message' in err) { - const message = (err as { message: unknown }).message; - if (typeof message === 'string' && message.trim()) return message.slice(0, 200); - } - return err instanceof Error ? err.message : 'unknown error'; -}