mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Online upgrades can strand users on stale desktop UI state or malformed local persistence. This adds a deny-by-default Doctor path that resets only regenerable desktop UI state, reports protected local files with redacted metadata, and keeps protected repair as a dry-run no-op until a reviewed backup-first flow exists. Constraint: Chat transcripts, model/provider config, Skills, MCP, IM bindings, adapter sessions, OAuth tokens, plugins, and team/session records are user-owned protected state. Rejected: Automatically rewrite malformed protected JSON | unsafe without schema-specific migrations and backups. Rejected: Continue relying only on startup migrations | users need an explicit recovery action after a white screen. Confidence: high Scope-risk: moderate Directive: Keep Doctor repair deny-by-default; do not mutate protected state without an explicit reviewed backup-first manual repair flow. Tested: bun test src/server/__tests__/doctor-service.test.ts Tested: cd desktop && bun run test src/components/ErrorBoundary.test.tsx src/lib/doctorRepair.test.ts src/__tests__/diagnosticsSettings.test.tsx src/components/layout/StartupErrorView.test.tsx Tested: cd desktop && bun run lint Tested: bun run check:server Tested: bun run verify (failed only existing agent-utils coverage baseline; changed-lines coverage 97.62%) Not-tested: Packaged desktop manual Doctor click path.
55 lines
1.5 KiB
TypeScript
55 lines
1.5 KiB
TypeScript
import React from 'react'
|
|
import { t } from '../i18n'
|
|
import { reportReactError } from '../lib/diagnosticsCapture'
|
|
import { Button } from './shared/Button'
|
|
import { DoctorPanel } from './doctor/DoctorPanel'
|
|
|
|
type Props = {
|
|
children: React.ReactNode
|
|
}
|
|
|
|
type State = {
|
|
hasError: boolean
|
|
}
|
|
|
|
export class ErrorBoundary extends React.Component<Props, State> {
|
|
state: State = { hasError: false }
|
|
|
|
static getDerivedStateFromError(): State {
|
|
return { hasError: true }
|
|
}
|
|
|
|
componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {
|
|
void reportReactError(error, errorInfo)
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return <ErrorBoundaryFallback />
|
|
}
|
|
|
|
return this.props.children
|
|
}
|
|
}
|
|
|
|
function ErrorBoundaryFallback() {
|
|
return (
|
|
<div className="h-screen w-screen bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex items-center justify-center p-6">
|
|
<div className="max-w-md w-full text-center">
|
|
<div className="text-base font-semibold">{t('errorBoundary.title')}</div>
|
|
<div className="mt-2 text-sm text-[var(--color-text-tertiary)]">
|
|
{t('errorBoundary.description')}
|
|
</div>
|
|
<div className="mt-4 flex justify-center">
|
|
<Button type="button" variant="secondary" size="sm" onClick={() => window.location.reload()}>
|
|
{t('common.retry')}
|
|
</Button>
|
|
</div>
|
|
<div className="mt-4 text-left">
|
|
<DoctorPanel compact />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|