fix: improve ErrorBoundary i18n and update provider preset descriptions

ErrorBoundary (PR #49 follow-up):
- Replace inline SVG with lucide-react CircleAlert icon for consistency
- Add i18n support: extract fallback UI into functional ErrorFallback
  component so it can use useTranslation hook
- Remove unused withErrorBoundary HOC
- Add error.* translation keys to en.ts and zh.ts

ProviderManager:
- Add Claude Code default config entry in connected providers section
  with cc switch hint explaining configurations may not be readable
- Update GLM preset descriptions: "GLM" → "GLM Code Plan / 编程套餐"
- Update MiniMax preset descriptions: "MiniMax API" → "MiniMax Code Plan / 编程套餐"
- Add provider.ccSwitchHint translation key to en.ts and zh.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-26 10:34:42 +08:00
1 parent cd807ef175
commit 5bace38e47
4 files changed
+112 -106

No files matched your search

+68 -90
View File
@@ -1,6 +1,8 @@
"use client";
import React from "react";
import React, { useState } from "react";
import { CircleAlert } from "lucide-react";
import { useTranslation } from "@/hooks/useTranslation";
interface ErrorBoundaryProps {
children: React.ReactNode;
@@ -10,16 +12,77 @@ interface ErrorBoundaryProps {
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
showDetails: boolean;
}
/* ── Fallback UI (functional, so it can use hooks) ──────────── */
function ErrorFallback({
error,
onReset,
}: {
error: Error | null;
onReset: () => void;
}) {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
return (
<div className="flex h-full w-full items-center justify-center bg-background p-8">
<div className="flex max-w-md flex-col items-center gap-4 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<CircleAlert size={24} />
</div>
<h2 className="text-lg font-semibold text-foreground">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{t("error.description")}
</p>
{error && (
<button
onClick={() => setShowDetails((s) => !s)}
className="text-xs text-muted-foreground underline hover:text-foreground"
>
{showDetails ? t("error.hideDetails") : t("error.showDetails")}
</button>
)}
{showDetails && error && (
<pre className="max-h-40 w-full overflow-auto rounded-md border border-border/50 bg-muted/30 p-3 text-left text-xs text-muted-foreground">
{error.message}
{error.stack && `\n\n${error.stack}`}
</pre>
)}
<div className="flex gap-2">
<button
onClick={onReset}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
{t("error.tryAgain")}
</button>
<button
onClick={() => window.location.reload()}
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
>
{t("error.reloadApp")}
</button>
</div>
</div>
</div>
);
}
/* ── Error Boundary (class component, required by React) ────── */
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null, showDetails: false };
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
@@ -32,11 +95,7 @@ export class ErrorBoundary extends React.Component<
}
handleReset = () => {
this.setState({ hasError: false, error: null, showDetails: false });
};
handleReload = () => {
window.location.reload();
this.setState({ hasError: false, error: null });
};
render() {
@@ -44,91 +103,10 @@ export class ErrorBoundary extends React.Component<
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="flex h-full w-full items-center justify-center bg-background p-8">
<div className="flex max-w-md flex-col items-center gap-4 text-center">
{/* Error icon */}
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<h2 className="text-lg font-semibold text-foreground">
Something went wrong
</h2>
<p className="text-sm text-muted-foreground">
An unexpected error occurred. You can try again or reload the app.
</p>
{/* Expandable error details */}
{this.state.error && (
<button
onClick={() =>
this.setState((s) => ({ showDetails: !s.showDetails }))
}
className="text-xs text-muted-foreground underline hover:text-foreground"
>
{this.state.showDetails ? "Hide details" : "Show details"}
</button>
)}
{this.state.showDetails && this.state.error && (
<pre className="max-h-40 w-full overflow-auto rounded-md border border-border/50 bg-muted/30 p-3 text-left text-xs text-muted-foreground">
{this.state.error.message}
{this.state.error.stack && `\n\n${this.state.error.stack}`}
</pre>
)}
{/* Action buttons */}
<div className="flex gap-2">
<button
onClick={this.handleReset}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Try Again
</button>
<button
onClick={this.handleReload}
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
>
Reload App
</button>
</div>
</div>
</div>
<ErrorFallback error={this.state.error} onReset={this.handleReset} />
);
}
return this.props.children;
}
}
/** Convenience HOC to wrap any component with an ErrorBoundary */
export function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
fallback?: React.ReactNode
) {
const displayName = Component.displayName || Component.name || "Component";
const Wrapped = (props: P) => (
<ErrorBoundary fallback={fallback}>
<Component {...props} />
</ErrorBoundary>
);
Wrapped.displayName = `withErrorBoundary(${displayName})`;
return Wrapped;
}
+26 -16
View File
@@ -146,8 +146,8 @@ const QUICK_PRESETS: QuickPreset[] = [
{
key: "glm-cn",
name: "GLM (CN)",
description: "Zhipu GLM — China region",
descriptionZh: "智谱 GLM — 中国区",
description: "Zhipu GLM Code Plan — China region",
descriptionZh: "智谱 GLM 编程套餐 — 中国区",
icon: <Zhipu size={18} />,
provider_type: "custom",
base_url: "https://open.bigmodel.cn/api/anthropic",
@@ -157,8 +157,8 @@ const QUICK_PRESETS: QuickPreset[] = [
{
key: "glm-global",
name: "GLM (Global)",
description: "Zhipu GLM — Global region",
descriptionZh: "智谱 GLM — 国际区",
description: "Zhipu GLM Code Plan — Global region",
descriptionZh: "智谱 GLM 编程套餐 — 国际区",
icon: <Zhipu size={18} />,
provider_type: "custom",
base_url: "https://api.z.ai/api/anthropic",
@@ -190,8 +190,8 @@ const QUICK_PRESETS: QuickPreset[] = [
{
key: "minimax-cn",
name: "MiniMax (CN)",
description: "MiniMax API — China region",
descriptionZh: "MiniMax API — 中国区",
description: "MiniMax Code Plan — China region",
descriptionZh: "MiniMax 编程套餐 — 中国区",
icon: <Minimax size={18} />,
provider_type: "custom",
base_url: "https://api.minimaxi.com/anthropic",
@@ -201,8 +201,8 @@ const QUICK_PRESETS: QuickPreset[] = [
{
key: "minimax-global",
name: "MiniMax (Global)",
description: "MiniMax API — Global region",
descriptionZh: "MiniMax API — 国际区",
description: "MiniMax Code Plan — Global region",
descriptionZh: "MiniMax 编程套餐 — 国际区",
icon: <Minimax size={18} />,
provider_type: "custom",
base_url: "https://api.minimax.io/anthropic",
@@ -563,20 +563,30 @@ export function ProviderManager() {
<div className="rounded-lg border border-border/50 p-4 space-y-2">
<h3 className="text-sm font-medium mb-1">{t('provider.connectedProviders')}</h3>
{/* Environment variable detection */}
{Object.keys(envDetected).length > 0 && (
<div className="flex items-center gap-3 py-2.5 px-1 border-b border-border/30">
{/* Claude Code default config */}
<div className="border-b border-border/30 pb-2">
<div className="flex items-center gap-3 py-2.5 px-1">
<div className="shrink-0 w-[22px] flex justify-center">
<Anthropic size={18} />
</div>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium">{t('provider.environment')}</span>
<Badge variant="outline" className="ml-2 text-[10px] px-1.5 py-0">
ENV
</Badge>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Claude Code</span>
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
{t('provider.default')}
</Badge>
{Object.keys(envDetected).length > 0 && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0 text-green-600 dark:text-green-400 border-green-500/30">
ENV
</Badge>
)}
</div>
</div>
</div>
)}
<p className="text-[11px] text-muted-foreground ml-[34px] leading-relaxed">
{t('provider.ccSwitchHint')}
</p>
</div>
{/* Connected provider list */}
{sorted.length > 0 ? (
+9
View File
@@ -145,6 +145,7 @@ const en = {
'provider.disconnecting': 'Disconnecting...',
'provider.disconnectProvider': 'Disconnect Provider',
'provider.disconnectConfirm': 'Are you sure you want to disconnect "{name}"? This action cannot be undone.',
'provider.ccSwitchHint': 'Claude Code configurations added via tools like cc switch may not be readable by CodePilot. We recommend re-adding your provider here.',
'provider.addProviderSection': 'Add Provider',
'provider.addProviderDesc': 'Select a provider to connect. Most presets only require an API key.',
@@ -290,6 +291,14 @@ const en = {
'common.enabled': 'Enabled',
'common.disabled': 'Disabled',
// ── Error boundary ────────────────────────────────────────
'error.title': 'Something went wrong',
'error.description': 'An unexpected error occurred. You can try again or reload the app.',
'error.showDetails': 'Show details',
'error.hideDetails': 'Hide details',
'error.tryAgain': 'Try Again',
'error.reloadApp': 'Reload App',
// ── CLI dynamic field labels ──────────────────────────────
'cli.loadingSettings': 'Loading settings...',
'cli.field.skipDangerousModePermissionPrompt': 'Skip Dangerous Mode Permission Prompt',
+9
View File
@@ -142,6 +142,7 @@ const zh: Record<TranslationKey, string> = {
'provider.disconnecting': '断开中...',
'provider.disconnectProvider': '断开提供商',
'provider.disconnectConfirm': '确定要断开"{name}"吗?此操作无法撤销。',
'provider.ccSwitchHint': '通过类似 cc switch 等工具添加的 Claude Code 配置可能无法被 CodePilot 读取,建议在此处重新添加。',
'provider.addProviderSection': '添加提供商',
'provider.addProviderDesc': '选择要连接的提供商。大多数预设只需填写 API 密钥。',
@@ -287,6 +288,14 @@ const zh: Record<TranslationKey, string> = {
'common.enabled': '已启用',
'common.disabled': '已禁用',
// ── Error boundary ────────────────────────────────────────
'error.title': '出错了',
'error.description': '发生了意外错误。您可以重试或重新加载应用。',
'error.showDetails': '显示详情',
'error.hideDetails': '隐藏详情',
'error.tryAgain': '重试',
'error.reloadApp': '重新加载',
// ── CLI dynamic field labels ──────────────────────────────
'cli.loadingSettings': '加载设置中...',
'cli.field.skipDangerousModePermissionPrompt': '跳过危险模式权限提示',