mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
web-shell ships as an embeddable React component, so an uncaught render error anywhere in the tree (Markdown / KaTeX / Mermaid / a tool panel) unmounts the whole React root and blanks the host page. Add two layers of isolation plus a generic boundary: - ErrorBoundary (components/ErrorBoundary.tsx): generic class boundary with reset-on-key recovery, so a streamed/edited/retried subtree recovers on its own while a stably-broken one stays on the fallback without looping. - Message level: each MessageItem body is wrapped, so one bad message degrades to an inline notice instead of taking down the transcript. The notice is role-aware (right-aligned for user messages, left for assistant) so it still reads as the right turn and doesn't blur turn boundaries. - Top level: the WebShell lib entry and the standalone main.tsx wrap <App> in a self-contained RootErrorFallback (no provider/theme/i18n dependency, inherits host text color) offering a retry, so an App-level crash shows a recoverable surface instead of a blank page. Adds message.renderError strings (en + zh-CN) and unit tests covering the boundary mechanism and the root fallback + retry path.
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import type { CSSProperties } from 'react';
|
|
import type { WebShellLanguage } from '../i18n';
|
|
|
|
interface RootErrorFallbackProps {
|
|
error: Error;
|
|
onRetry: () => void;
|
|
/** Selects the fallback copy. Defaults to English when omitted. */
|
|
language?: WebShellLanguage;
|
|
}
|
|
|
|
interface FallbackCopy {
|
|
title: string;
|
|
body: string;
|
|
retry: string;
|
|
}
|
|
|
|
// This surface renders OUTSIDE the in-app I18nProvider (the boundary wraps the
|
|
// whole App, which owns that provider), so it cannot call useI18n. It carries
|
|
// its own minimal copy instead of pulling the full translation table.
|
|
const COPY: Record<WebShellLanguage, FallbackCopy> = {
|
|
en: {
|
|
title: 'Something went wrong',
|
|
body: 'An unexpected error occurred and this content could not be displayed.',
|
|
retry: 'Try again',
|
|
},
|
|
'zh-CN': {
|
|
title: '出了点问题',
|
|
body: '发生意外错误,无法显示此内容。',
|
|
retry: '重试',
|
|
},
|
|
};
|
|
|
|
// The boundary wraps the whole App, so this surface renders when the themed
|
|
// App container (which defines --text-primary etc.) never mounted. It therefore
|
|
// cannot use app theme tokens. Inheriting the host's text color keeps it
|
|
// readable on both light and dark hosts; opacity carries the hierarchy and a
|
|
// neutral gray border reads acceptably on either background.
|
|
const containerStyle: CSSProperties = {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '0.75rem',
|
|
padding: '2rem',
|
|
minHeight: '8rem',
|
|
height: '100%',
|
|
textAlign: 'center',
|
|
color: 'inherit',
|
|
};
|
|
|
|
const titleStyle: CSSProperties = {
|
|
margin: 0,
|
|
fontSize: '1rem',
|
|
fontWeight: 600,
|
|
};
|
|
|
|
const bodyStyle: CSSProperties = {
|
|
margin: 0,
|
|
fontSize: '0.85rem',
|
|
opacity: 0.7,
|
|
maxWidth: '32rem',
|
|
};
|
|
|
|
const buttonStyle: CSSProperties = {
|
|
appearance: 'none',
|
|
cursor: 'pointer',
|
|
padding: '0.4rem 1rem',
|
|
fontSize: '0.85rem',
|
|
borderRadius: '6px',
|
|
border: '1px solid rgba(128, 128, 128, 0.4)',
|
|
background: 'transparent',
|
|
color: 'inherit',
|
|
};
|
|
|
|
/**
|
|
* Last-resort surface for the top-level boundary. Self-contained (no provider,
|
|
* theme-token, or i18n dependency) so it survives even when the whole App tree
|
|
* fails to mount. Offers a retry instead of forcing a host-page reload, which
|
|
* would be hostile in embedded integrations.
|
|
*/
|
|
export function RootErrorFallback({
|
|
error,
|
|
onRetry,
|
|
language = 'en',
|
|
}: RootErrorFallbackProps) {
|
|
const copy = COPY[language] ?? COPY.en;
|
|
return (
|
|
<div role="alert" style={containerStyle} data-web-shell-error>
|
|
<p style={titleStyle}>{copy.title}</p>
|
|
<p style={bodyStyle}>{copy.body}</p>
|
|
{/* Dev-only: end users (and embedded hosts) shouldn't see raw internal
|
|
error text — it stays in console.error for everyone. In the published
|
|
lib build DEV is statically false, so this never ships to consumers. */}
|
|
{import.meta.env.DEV && error.message && (
|
|
<p style={{ ...bodyStyle, fontFamily: 'monospace', opacity: 0.55 }}>
|
|
{error.message}
|
|
</p>
|
|
)}
|
|
<button type="button" style={buttonStyle} onClick={onRetry}>
|
|
{copy.retry}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|