@miphamai/cli 0.33.0 → 0.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/cross-session/file-inbox.ts +30 -12
- package/src/core/compaction-progress.ts +205 -0
- package/src/core/context-compact.ts +70 -6
- package/src/core/context-drain.ts +65 -6
- package/src/core/context.ts +11 -4
- package/src/core/engine.ts +8 -0
- package/src/core/hooks.ts +114 -9
- package/src/shared/types.ts +2 -0
- package/src/skills/fork-executor.ts +6 -2
- package/src/skills/loader.ts +37 -4
- package/src/skills/sanitizer.ts +214 -0
- package/src/tools/agent/skill.ts +6 -1
- package/src/tools/file/edit.ts +4 -0
- package/src/tools/file/read.ts +3 -0
- package/src/tools/file/write.ts +20 -3
- package/src/ui/app.tsx +142 -129
- package/src/ui/commands.ts +71 -4
- package/src/ui/error-boundary.tsx +85 -0
package/src/ui/app.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
|
2
2
|
import { Box, Text, useInput } from 'ink'
|
|
3
3
|
import TextInput from 'ink-text-input'
|
|
4
|
+
import { ErrorBoundary } from './error-boundary'
|
|
4
5
|
import type { QueryEngine } from '../core/engine'
|
|
5
6
|
import type { RemoteEngine } from '../daemon/remote-engine'
|
|
6
7
|
import type { MiphamConfig } from '../shared/index.ts'
|
|
@@ -410,7 +411,17 @@ export function App({
|
|
|
410
411
|
role: msg.role,
|
|
411
412
|
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
412
413
|
}))
|
|
413
|
-
|
|
414
|
+
if (result.resumeWarning) {
|
|
415
|
+
// Fix 7: Session isolation — add a visual separator between current session
|
|
416
|
+
// and restored history to prevent accidental leakage/confusion
|
|
417
|
+
const separator: ChatMessage = {
|
|
418
|
+
role: 'assistant' as const,
|
|
419
|
+
content: '── Restored session history below ──',
|
|
420
|
+
}
|
|
421
|
+
setMessages((prev) => [...prev, separator, ...restored])
|
|
422
|
+
} else {
|
|
423
|
+
setMessages((prev) => [...prev, ...restored])
|
|
424
|
+
}
|
|
414
425
|
}
|
|
415
426
|
if (result.copyContent) {
|
|
416
427
|
// Copy to clipboard via pbcopy (macOS) or clip (Windows)
|
|
@@ -743,140 +754,142 @@ export function App({
|
|
|
743
754
|
}
|
|
744
755
|
|
|
745
756
|
return (
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
<
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
757
|
+
<ErrorBoundary>
|
|
758
|
+
<Box flexDirection="column" padding={1} height="100%">
|
|
759
|
+
{/* Workflow progress — auto-detects active workflows, renders nothing when idle */}
|
|
760
|
+
<WorkflowProgress />
|
|
761
|
+
|
|
762
|
+
{/* Agent View Dashboard — Ctrl+G overlay (replaces chat + input) */}
|
|
763
|
+
{agentViewOpen && agentViewManager ? (
|
|
764
|
+
<AgentViewDashboard
|
|
765
|
+
manager={agentViewManager}
|
|
766
|
+
onAttach={() => {}}
|
|
767
|
+
onExit={() => setAgentViewOpen(false)}
|
|
768
|
+
/>
|
|
769
|
+
) : (
|
|
770
|
+
<>
|
|
771
|
+
{/* Chat panel */}
|
|
772
|
+
<ChatPanel messages={messages} focusMode={focusMode} />
|
|
773
|
+
|
|
774
|
+
{/* Input with separator lines */}
|
|
775
|
+
{pickerOpen ? (
|
|
776
|
+
<ModelPicker
|
|
777
|
+
config={config}
|
|
778
|
+
currentProvider={providerId}
|
|
779
|
+
currentModel={modelId}
|
|
780
|
+
onSelect={(newProvider, newModel) => {
|
|
781
|
+
engine.switchProvider(newProvider, newModel)
|
|
782
|
+
setProviderId(newProvider)
|
|
783
|
+
setModelId(newModel)
|
|
784
|
+
setPickerOpen(false)
|
|
785
|
+
setMessages((prev) => [
|
|
786
|
+
...prev,
|
|
787
|
+
{ role: 'system', content: `✓ Switched to ${newProvider}/${newModel}` },
|
|
788
|
+
])
|
|
789
|
+
}}
|
|
790
|
+
onNeedsApiKey={(providerId, modelId, providerName) => {
|
|
791
|
+
setApiKeyPrompt({ providerId, modelId, providerName })
|
|
792
|
+
setApiKeyInput('')
|
|
793
|
+
setPickerOpen(false)
|
|
794
|
+
setMessages((prev) => [
|
|
795
|
+
...prev,
|
|
796
|
+
{
|
|
797
|
+
role: 'system',
|
|
798
|
+
content: t('ui.picker.needs_api_key', { provider: providerName }),
|
|
799
|
+
},
|
|
800
|
+
])
|
|
801
|
+
}}
|
|
802
|
+
onClose={() => setPickerOpen(false)}
|
|
803
|
+
/>
|
|
804
|
+
) : (
|
|
805
|
+
/* Input bar (hidden when picker is open) */
|
|
806
|
+
<Box flexDirection="column">
|
|
807
|
+
<Text dimColor>──────────────────────────────</Text>
|
|
808
|
+
<InputBar
|
|
809
|
+
onSubmit={handleSubmit}
|
|
810
|
+
isLoading={isLoading}
|
|
811
|
+
onTogglePicker={() => setPickerOpen((prev) => !prev)}
|
|
812
|
+
onToggleFocus={() => setFocusMode((prev) => !prev)}
|
|
813
|
+
onToggleExpand={() => {
|
|
814
|
+
setMessages((prev) => {
|
|
815
|
+
const msgs = [...prev]
|
|
816
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
817
|
+
if (msgs[i]?.toolMeta) {
|
|
818
|
+
const meta = msgs[i]!.toolMeta!
|
|
819
|
+
if (meta.collapsed) {
|
|
820
|
+
msgs[i] = {
|
|
821
|
+
...msgs[i]!,
|
|
822
|
+
content: `🔧 ${meta.name}: ${meta.input}\n📋 Result: ${meta.output || '(pending)'}`,
|
|
823
|
+
toolMeta: { ...meta, collapsed: false },
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
const short =
|
|
827
|
+
meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
|
|
828
|
+
msgs[i] = {
|
|
829
|
+
...msgs[i]!,
|
|
830
|
+
content: `⏺ ${meta.name} · ${short} (Ctrl+O to expand)`,
|
|
831
|
+
toolMeta: { ...meta, collapsed: true },
|
|
832
|
+
}
|
|
820
833
|
}
|
|
834
|
+
break
|
|
821
835
|
}
|
|
822
|
-
break
|
|
823
836
|
}
|
|
837
|
+
return msgs
|
|
838
|
+
})
|
|
839
|
+
}}
|
|
840
|
+
onCyclePermission={() => {
|
|
841
|
+
setPermissionMode((prev) => {
|
|
842
|
+
const idx = PERMISSION_MODES.indexOf(prev)
|
|
843
|
+
const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
|
|
844
|
+
engine.getPermission().setMode(next)
|
|
845
|
+
return next
|
|
846
|
+
})
|
|
847
|
+
}}
|
|
848
|
+
onCancel={() => {
|
|
849
|
+
if (abortRef.current) {
|
|
850
|
+
abortRef.current.abort()
|
|
824
851
|
}
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
setPermissionMode((prev) => {
|
|
830
|
-
const idx = PERMISSION_MODES.indexOf(prev)
|
|
831
|
-
const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
|
|
832
|
-
engine.getPermission().setMode(next)
|
|
833
|
-
return next
|
|
834
|
-
})
|
|
835
|
-
}}
|
|
836
|
-
onCancel={() => {
|
|
837
|
-
if (abortRef.current) {
|
|
838
|
-
abortRef.current.abort()
|
|
839
|
-
}
|
|
840
|
-
}}
|
|
841
|
-
onToggleAgentView={() => setAgentViewOpen((prev) => !prev)}
|
|
842
|
-
/>
|
|
843
|
-
<Text dimColor>──────────────────────────────</Text>
|
|
844
|
-
</Box>
|
|
845
|
-
)}
|
|
846
|
-
|
|
847
|
-
{/* Agent status footer — shows active tool indicator + inline agent progress + running background agents */}
|
|
848
|
-
<AgentFooter
|
|
849
|
-
agents={Object.values(runningAgents)}
|
|
850
|
-
tick={agentTick}
|
|
851
|
-
activeTool={activeTool}
|
|
852
|
-
agentProgress={agentProgress}
|
|
853
|
-
/>
|
|
854
|
-
|
|
855
|
-
{/* Status line — Claude Code style */}
|
|
856
|
-
<Box marginTop={1} flexDirection="column">
|
|
857
|
-
{goalText && (
|
|
858
|
-
<Box>
|
|
859
|
-
<Text color="green">🎯 Goal: {goalText}</Text>
|
|
852
|
+
}}
|
|
853
|
+
onToggleAgentView={() => setAgentViewOpen((prev) => !prev)}
|
|
854
|
+
/>
|
|
855
|
+
<Text dimColor>──────────────────────────────</Text>
|
|
860
856
|
</Box>
|
|
861
857
|
)}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
858
|
+
|
|
859
|
+
{/* Agent status footer — shows active tool indicator + inline agent progress + running background agents */}
|
|
860
|
+
<AgentFooter
|
|
861
|
+
agents={Object.values(runningAgents)}
|
|
862
|
+
tick={agentTick}
|
|
863
|
+
activeTool={activeTool}
|
|
864
|
+
agentProgress={agentProgress}
|
|
865
|
+
/>
|
|
866
|
+
|
|
867
|
+
{/* Status line — Claude Code style */}
|
|
868
|
+
<Box marginTop={1} flexDirection="column">
|
|
869
|
+
{goalText && (
|
|
870
|
+
<Box>
|
|
871
|
+
<Text color="green">🎯 Goal: {goalText}</Text>
|
|
872
|
+
</Box>
|
|
873
|
+
)}
|
|
874
|
+
<Box flexDirection="row">
|
|
875
|
+
<Text color={PERMISSION_COLORS[permissionMode]}>
|
|
876
|
+
⏵⏵ {PERMISSION_LABELS[permissionMode]}
|
|
877
|
+
</Text>
|
|
878
|
+
<Text dimColor>
|
|
879
|
+
{' '}
|
|
880
|
+
({t('ui.status.shift_tab_cycle')}: {PERMISSION_LABELS.default} ·{' '}
|
|
881
|
+
{PERMISSION_LABELS.acceptEdits} · {PERMISSION_LABELS.bypassPermissions} ·{' '}
|
|
882
|
+
{PERMISSION_LABELS.plan} · {PERMISSION_LABELS.auto} · {PERMISSION_LABELS.dontAsk})
|
|
883
|
+
{' · '}
|
|
884
|
+
{t('ui.status.esc_to_interrupt')}
|
|
885
|
+
{' · '}
|
|
886
|
+
{t('ui.status.left_for_agents')}
|
|
887
|
+
</Text>
|
|
888
|
+
</Box>
|
|
876
889
|
</Box>
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
</
|
|
890
|
+
</>
|
|
891
|
+
)}
|
|
892
|
+
</Box>
|
|
893
|
+
</ErrorBoundary>
|
|
881
894
|
)
|
|
882
895
|
}
|
package/src/ui/commands.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface CommandResult {
|
|
|
83
83
|
forwardToAI?: string
|
|
84
84
|
/** If set, restored messages to load into the session (used by /resume last). */
|
|
85
85
|
forwardedMessages?: Message[]
|
|
86
|
+
/** If true, resume into current non-empty session — app should warn before loading */
|
|
87
|
+
resumeWarning?: boolean
|
|
86
88
|
/** API Key 补录请求:切换前需用户输入 Key */
|
|
87
89
|
needsApiKey?: {
|
|
88
90
|
providerId: string
|
|
@@ -269,11 +271,31 @@ const exitCmd: CommandHandler = () => ({ content: '', exit: true })
|
|
|
269
271
|
const compactCmd: CommandHandler = async (ctx) => {
|
|
270
272
|
const t = resolveT(ctx)
|
|
271
273
|
const context = ctx.engine.getContext()
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
const msgsBefore = context.getMessageCount()
|
|
275
|
+
const tokensBefore = context.getEstimatedTokens()
|
|
276
|
+
|
|
277
|
+
// Show start progress
|
|
278
|
+
process.stderr.write(
|
|
279
|
+
`🔄 Compacting: ${msgsBefore} messages, ${tokensBefore.toLocaleString()} tokens...\n`,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const startTime = Date.now()
|
|
283
|
+
const result = await context.compact('user requested compaction')
|
|
284
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
285
|
+
const msgsAfter = context.getMessageCount()
|
|
286
|
+
const saved = result.before > 0 ? ((1 - result.after / result.before) * 100).toFixed(0) : '0'
|
|
287
|
+
|
|
275
288
|
return {
|
|
276
|
-
content:
|
|
289
|
+
content: [
|
|
290
|
+
t('commands.compact.confirmed'),
|
|
291
|
+
`Messages: ${msgsBefore} → ${msgsAfter} | Tokens: ${result.before.toLocaleString()} → ${result.after.toLocaleString()} (${saved}% saved)`,
|
|
292
|
+
`Duration: ${elapsed}s`,
|
|
293
|
+
msgsBefore > msgsAfter
|
|
294
|
+
? `💡 Tip: Use /compact early to prevent automatic compaction delays.`
|
|
295
|
+
: '',
|
|
296
|
+
]
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.join('\n'),
|
|
277
299
|
}
|
|
278
300
|
}
|
|
279
301
|
|
|
@@ -2256,6 +2278,46 @@ const hooksCmd: CommandHandler = async (ctx) => {
|
|
|
2256
2278
|
}
|
|
2257
2279
|
}
|
|
2258
2280
|
|
|
2281
|
+
const hooksHealthCmd: CommandHandler = (ctx) => {
|
|
2282
|
+
const hookEngine = ctx.engine.getHookEngine?.()
|
|
2283
|
+
if (!hookEngine) {
|
|
2284
|
+
return { content: 'Hook engine not available.' }
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
const health = hookEngine.getHookHealth()
|
|
2288
|
+
if (health.length === 0) {
|
|
2289
|
+
return { content: '✅ All hooks are healthy — no failures recorded.' }
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const lines: string[] = ['## 🪝 Hook Health Status', '']
|
|
2293
|
+
for (const { key, health: h } of health) {
|
|
2294
|
+
const status = h.disabled ? '🔴 DISABLED' : h.failures > 0 ? '🟡 DEGRADED' : '🟢 HEALTHY'
|
|
2295
|
+
lines.push(`### ${status} \`${key}\``)
|
|
2296
|
+
lines.push(`| 连续失败 | ${h.failures} (threshold: 5) |`)
|
|
2297
|
+
lines.push(`| 总失败次数 | ${h.totalFailures} |`)
|
|
2298
|
+
if (h.disabled) {
|
|
2299
|
+
const remaining = Math.max(0, 5 - (Date.now() - h.disabledAt) / 60_000)
|
|
2300
|
+
lines.push(`| 自动恢复 | ${remaining.toFixed(0)} min remaining |`)
|
|
2301
|
+
lines.push('')
|
|
2302
|
+
lines.push(`Use \`/hooks enable "${key}"\` to re-enable manually.`)
|
|
2303
|
+
}
|
|
2304
|
+
lines.push('')
|
|
2305
|
+
}
|
|
2306
|
+
return { content: lines.join('\n') }
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
const hooksEnableCmd: CommandHandler = (ctx, args) => {
|
|
2310
|
+
const hookEngine = ctx.engine.getHookEngine?.()
|
|
2311
|
+
if (!hookEngine) return { content: 'Hook engine not available.' }
|
|
2312
|
+
const keyArg = args[0]?.trim().replace(/"/g, '')
|
|
2313
|
+
if (!keyArg) {
|
|
2314
|
+
return { content: 'Usage: /hooks enable "<hook-key>"\n\nUse /hooks health to see hook keys.' }
|
|
2315
|
+
}
|
|
2316
|
+
return hookEngine.reEnableHook(keyArg)
|
|
2317
|
+
? { content: `✅ Hook "${keyArg}" re-enabled.` }
|
|
2318
|
+
: { content: `Hook "${keyArg}" not found or not disabled. Use /hooks health to check status.` }
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2259
2321
|
const batchCmd: CommandHandler = async (ctx) => {
|
|
2260
2322
|
const t = resolveT(ctx)
|
|
2261
2323
|
return { content: t('commands.batch.title') }
|
|
@@ -2426,6 +2488,7 @@ const resumeLastCmd: CommandHandler = async (ctx) => {
|
|
|
2426
2488
|
: t('commands.resume.restored_full_footer', { loaded: String(messages.length) }),
|
|
2427
2489
|
].join('\n'),
|
|
2428
2490
|
forwardedMessages: messages,
|
|
2491
|
+
resumeWarning: true,
|
|
2429
2492
|
}
|
|
2430
2493
|
}
|
|
2431
2494
|
|
|
@@ -3776,6 +3839,8 @@ registry.set('/stats', statsCmd)
|
|
|
3776
3839
|
registry.set('/summary', summaryCmd)
|
|
3777
3840
|
registry.set('/cd', cdCmd)
|
|
3778
3841
|
registry.set('/hooks', hooksCmd)
|
|
3842
|
+
registry.set('/hooks health', hooksHealthCmd)
|
|
3843
|
+
registry.set('/hooks enable', hooksEnableCmd)
|
|
3779
3844
|
registry.set('/batch', batchCmd)
|
|
3780
3845
|
|
|
3781
3846
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -3909,6 +3974,8 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
3909
3974
|
'/summary': 'Generate session summary',
|
|
3910
3975
|
'/cd': 'Change session working directory',
|
|
3911
3976
|
'/hooks': 'Manage lifecycle hook scripts',
|
|
3977
|
+
'/hooks health': 'Check hook health — see failures, disabled hooks, recovery status',
|
|
3978
|
+
'/hooks enable': 'Manually re-enable a hook that was auto-disabled after repeated failures',
|
|
3912
3979
|
'/batch': 'Apply changes across multiple files',
|
|
3913
3980
|
}
|
|
3914
3981
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix 8: Error Boundary for UI Rendering Protection
|
|
3
|
+
*
|
|
4
|
+
* Catches rendering errors in the React/Ink component tree,
|
|
5
|
+
* preventing the "layout freeze" bug where the interactive session
|
|
6
|
+
* stops redrawing while the process stays alive.
|
|
7
|
+
*
|
|
8
|
+
* Recovery path: renders a fallback message with a hint that the
|
|
9
|
+
* user can press Enter to attempt recovery (re-render).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { Component } from 'react'
|
|
13
|
+
import { Text, Box } from 'ink'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
/** Called when a render error is caught */
|
|
18
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface State {
|
|
22
|
+
hasError: boolean
|
|
23
|
+
error: Error | null
|
|
24
|
+
errorCount: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
28
|
+
private resetTimer: ReturnType<typeof setTimeout> | null = null
|
|
29
|
+
|
|
30
|
+
constructor(props: Props) {
|
|
31
|
+
super(props)
|
|
32
|
+
this.state = { hasError: false, error: null, errorCount: 0 }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static getDerivedStateFromError(error: Error): Partial<State> {
|
|
36
|
+
return { hasError: true, error }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
40
|
+
// Log to stderr for diagnostics
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
`\n⚠️ UI Error Boundary caught: ${error.message}\n` +
|
|
43
|
+
` Component: ${errorInfo.componentStack?.slice(0, 200) || 'unknown'}\n`,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
this.props.onError?.(error, errorInfo)
|
|
47
|
+
|
|
48
|
+
// Auto-recovery: reset after 5 seconds to attempt re-render
|
|
49
|
+
this.resetTimer = setTimeout(() => {
|
|
50
|
+
this.setState((prev) => ({
|
|
51
|
+
hasError: false,
|
|
52
|
+
error: null,
|
|
53
|
+
errorCount: prev.errorCount,
|
|
54
|
+
}))
|
|
55
|
+
}, 5_000)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override componentWillUnmount(): void {
|
|
59
|
+
if (this.resetTimer) {
|
|
60
|
+
clearTimeout(this.resetTimer)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override render(): React.ReactNode {
|
|
65
|
+
if (this.state.hasError) {
|
|
66
|
+
return (
|
|
67
|
+
<Box flexDirection="column" padding={1}>
|
|
68
|
+
<Text color="red" bold>
|
|
69
|
+
⚠️ UI Render Error
|
|
70
|
+
</Text>
|
|
71
|
+
<Text dimColor>{this.state.error?.message || 'Unknown rendering error'}</Text>
|
|
72
|
+
<Text> </Text>
|
|
73
|
+
<Text dimColor>
|
|
74
|
+
The terminal interface encountered a rendering issue. Auto-recovery in 5 seconds...
|
|
75
|
+
Press Enter to attempt immediate recovery.
|
|
76
|
+
</Text>
|
|
77
|
+
<Text> </Text>
|
|
78
|
+
<Text color="yellow">Your session data and conversation history are preserved.</Text>
|
|
79
|
+
</Box>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return this.props.children
|
|
84
|
+
}
|
|
85
|
+
}
|