@miphamai/cli 0.33.1 → 0.34.0

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.
@@ -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 before = context.getEstimatedTokens()
273
- await context.compact('user requested compaction')
274
- const after = context.getEstimatedTokens()
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: `${t('commands.compact.confirmed')}\n${t('commands.compact.tokens', { before: before.toLocaleString(), after: after.toLocaleString(), saved: ((1 - after / before) * 100).toFixed(0) })}`,
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
 
@@ -907,6 +929,234 @@ const sisCleanupCmd: CommandHandler = async (ctx) => {
907
929
  return { content: lines.join('\n') }
908
930
  }
909
931
 
932
+ // ═══════════════════════════════════════════════════════════════
933
+ // Auto-Dream: Background Memory Consolidation
934
+ // ═══════════════════════════════════════════════════════════════
935
+
936
+ const dreamCmd: CommandHandler = async (ctx, args) => {
937
+ const t = resolveT(ctx)
938
+ const aggressive = args.includes('--aggressive') || args.includes('-a')
939
+ const status = args.includes('--status') || args.includes('-s')
940
+
941
+ const engine = ctx.engine.getDreamEngine?.()
942
+ if (!engine) {
943
+ return { content: 'DreamEngine 未初始化。请确认 Mipham Code 版本 >= v0.34.0。' }
944
+ }
945
+
946
+ if (status) {
947
+ const history = engine.getDreamHistory()
948
+ if (history.length === 0) {
949
+ return { content: '🌙 尚未运行过 Auto-Dream。使用 `/dream` 手动触发首次梦境整合。' }
950
+ }
951
+ const lines: string[] = ['## 🌙 Auto-Dream 历史', '']
952
+ for (const entry of history.slice(0, 5)) {
953
+ const ts = new Date(entry.timestamp).toLocaleString('zh-CN')
954
+ const total = entry.actions.length
955
+ const applied = entry.actions.filter((a) => a.autoApplied).length
956
+ const flagged = entry.actions.filter((a) => !a.autoApplied).length
957
+ lines.push(`- **${ts}**: ${total} 行动 (${applied} 自动应用, ${flagged} 标记审查)`)
958
+ }
959
+ return { content: lines.join('\n') }
960
+ }
961
+
962
+ const report = engine.dream({ aggressive })
963
+ const lines: string[] = [
964
+ '## 🌙 Auto-Dream 完成',
965
+ '',
966
+ `记忆整合: ${report.beforeCount} → ${report.afterCount}`,
967
+ ...(report.phases.deduplicated > 0 ? [`- 去重: ${report.phases.deduplicated} 条`] : []),
968
+ ...(report.phases.contradictionsFound > 0
969
+ ? [`- 矛盾冲突: ${report.phases.contradictionsFound} 处 (需人工裁决)`]
970
+ : []),
971
+ ...(report.phases.merged > 0 ? [`- 合并: ${report.phases.merged} 组`] : []),
972
+ ...(report.phases.solidified > 0
973
+ ? [`- 模糊标记: ${report.phases.solidified} 条 (建议固化)`]
974
+ : []),
975
+ ...(report.phases.pruned > 0 ? [`- 清理过期: ${report.phases.pruned} 条`] : []),
976
+ '',
977
+ ]
978
+
979
+ const flagged = report.actions.filter((a) => !a.autoApplied)
980
+ if (flagged.length > 0 && !aggressive) {
981
+ lines.push(`⚠️ ${flagged.length} 项待审查。使用 \`/dream --aggressive\` 自动应用所有操作。`)
982
+ lines.push('')
983
+ for (const action of flagged) {
984
+ lines.push(`- [ ] **${action.type}**: ${action.description}`)
985
+ }
986
+ }
987
+
988
+ if (report.actions.length === 0) {
989
+ lines.push('✅ 记忆干净,无需整理。')
990
+ }
991
+
992
+ return { content: lines.join('\n') }
993
+ }
994
+
995
+ // ═══════════════════════════════════════════════════════════════
996
+ // Bug Report
997
+ // ═══════════════════════════════════════════════════════════════
998
+
999
+ const bugReportCmd: CommandHandler = async (ctx) => {
1000
+ const t = resolveT(ctx)
1001
+ const engine = ctx.engine
1002
+ const lines: string[] = [
1003
+ '## 🐛 Bug Report',
1004
+ '',
1005
+ '> Copy the report below and paste it into your GitHub Issue.',
1006
+ '',
1007
+ '---',
1008
+ '',
1009
+ ]
1010
+
1011
+ // ── Version & Runtime ──
1012
+ lines.push('### Environment', '')
1013
+ lines.push(`- **Mipham Code**: v${ctx.version}`)
1014
+ lines.push(`- **Session**: ${ctx.sessionId}`)
1015
+ lines.push(`- **Node**: ${process.version}`)
1016
+ lines.push(`- **Platform**: ${process.platform} ${process.arch}`)
1017
+ lines.push(
1018
+ `- **OS**: ${process.platform === 'darwin' ? 'macOS' : process.platform === 'linux' ? 'Linux' : 'Windows'}`,
1019
+ )
1020
+ lines.push(`- **Provider**: ${ctx.providerId}`)
1021
+ lines.push(`- **Model**: ${ctx.modelId}`)
1022
+ lines.push('')
1023
+
1024
+ // ── SIS errors ──
1025
+ try {
1026
+ const db = engine.getErrorSignatureDB?.()
1027
+ if (db) {
1028
+ const active = db.getActive()
1029
+ if (active.length > 0) {
1030
+ lines.push('### Active Error Signatures', '')
1031
+ for (const sig of active.slice(0, 10)) {
1032
+ lines.push(
1033
+ `- \`${sig.id}\`: ${sig.pattern} (${sig.category}, ${sig.occurrences}x, ${(sig.successRate * 100).toFixed(0)}% success)`,
1034
+ )
1035
+ }
1036
+ lines.push('')
1037
+ }
1038
+ }
1039
+ } catch {
1040
+ // SIS unavailable
1041
+ }
1042
+
1043
+ // ── Hook health ──
1044
+ try {
1045
+ const hookEngine = engine.getHookEngine?.()
1046
+ if (hookEngine) {
1047
+ const health = hookEngine.getHookHealth()
1048
+ const disabled = health.filter((h) => h.health.disabled)
1049
+ if (disabled.length > 0) {
1050
+ lines.push('### Disabled Hooks', '')
1051
+ for (const entry of disabled) {
1052
+ lines.push(
1053
+ `- \`${entry.key}\`: ${entry.health.failures} failures, disabled at ${new Date(entry.health.disabledAt).toISOString()}`,
1054
+ )
1055
+ }
1056
+ lines.push('')
1057
+ }
1058
+ }
1059
+ } catch {
1060
+ // Hook health unavailable
1061
+ }
1062
+
1063
+ // ── Recent dream log ──
1064
+ try {
1065
+ const dreamEngine = engine.getDreamEngine?.()
1066
+ if (dreamEngine) {
1067
+ const history = dreamEngine.getDreamHistory()
1068
+ if (history.length > 0) {
1069
+ const last = history[0]!
1070
+ lines.push('### Last Auto-Dream', '')
1071
+ lines.push(`- **Ran at**: ${last.timestamp}`)
1072
+ lines.push(
1073
+ `- **Actions**: ${last.actions.length} (${last.actions.filter((a: { autoApplied: boolean }) => a.autoApplied).length} applied)`,
1074
+ )
1075
+ lines.push('')
1076
+ }
1077
+ }
1078
+ } catch {
1079
+ // Dream engine unavailable
1080
+ }
1081
+
1082
+ // ── Steps to reproduce (template) ──
1083
+ lines.push('### Steps to Reproduce', '')
1084
+ lines.push('1. ')
1085
+ lines.push('2. ')
1086
+ lines.push('3. ')
1087
+ lines.push('')
1088
+ lines.push('### Expected Behavior', '')
1089
+ lines.push('')
1090
+ lines.push('### Actual Behavior', '')
1091
+ lines.push('')
1092
+ lines.push('---')
1093
+ lines.push(`🤖 Generated with Mipham Code v${ctx.version}`)
1094
+
1095
+ return { content: lines.join('\n'), copyContent: lines.join('\n') }
1096
+ }
1097
+
1098
+ // ═══════════════════════════════════════════════════════════════
1099
+ // Feedback
1100
+ // ═══════════════════════════════════════════════════════════════
1101
+ // Changelog
1102
+ // ═══════════════════════════════════════════════════════════════
1103
+
1104
+ const changelogCmd: CommandHandler = async (ctx) => {
1105
+ const t = resolveT(ctx)
1106
+ const lines: string[] = ['## 📋 Changelog', '']
1107
+
1108
+ try {
1109
+ const { execSync } = await import('node:child_process')
1110
+ const tags = execSync('git tag --sort=-creatordate', { encoding: 'utf-8', timeout: 5000 })
1111
+ .trim()
1112
+ .split('\n')
1113
+ .filter(Boolean)
1114
+ .slice(0, 15)
1115
+
1116
+ if (tags.length === 0) {
1117
+ lines.push('No version tags found.')
1118
+ return { content: lines.join('\n') }
1119
+ }
1120
+
1121
+ lines.push(`Current: **v${ctx.version}**`, '')
1122
+
1123
+ for (let i = 0; i < tags.length; i++) {
1124
+ const tag = tags[i]!
1125
+ const nextTag = tags[i + 1]
1126
+ try {
1127
+ const range = nextTag ? `${nextTag}..${tag}` : tag
1128
+ const log = execSync(
1129
+ `git log --oneline --no-merges ${range} --format="%s" 2>/dev/null | head -8`,
1130
+ { encoding: 'utf-8', timeout: 5000 },
1131
+ )
1132
+ .trim()
1133
+ .split('\n')
1134
+ .filter(Boolean)
1135
+
1136
+ const isCurrent = tag === `v${ctx.version}`
1137
+ lines.push(`### ${tag}${isCurrent ? ' ← current' : ''}`, '')
1138
+ if (log.length > 0) {
1139
+ for (const entry of log) {
1140
+ lines.push(`- ${entry}`)
1141
+ }
1142
+ } else {
1143
+ lines.push(' _(release tag only)_')
1144
+ }
1145
+ lines.push('')
1146
+ } catch {
1147
+ lines.push(`### ${tag}`, '', ' _(details unavailable)_', '')
1148
+ }
1149
+ }
1150
+ } catch {
1151
+ lines.push('Unable to read git history. Make sure you are in the Mipham Code repository.')
1152
+ }
1153
+
1154
+ lines.push('---')
1155
+ lines.push(`Full history: \`git log --oneline\``)
1156
+
1157
+ return { content: lines.join('\n') }
1158
+ }
1159
+
910
1160
  // ═══════════════════════════════════════════════════════════════
911
1161
  // Workflow
912
1162
  // ═══════════════════════════════════════════════════════════════
@@ -2125,25 +2375,69 @@ const statsCmd: CommandHandler = (ctx) => {
2125
2375
  const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
2126
2376
  const systemMsgs = msgs.filter((m) => m.role === 'system').length
2127
2377
 
2128
- return {
2129
- content: [
2130
- t('commands.stats.title'),
2131
- '',
2132
- t('commands.stats.messages', {
2133
- total: String(msgs.length),
2134
- user: String(userMsgs),
2135
- assistant: String(assistantMsgs),
2136
- system: String(systemMsgs),
2137
- }),
2138
- t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
2139
- t('commands.stats.tools', { count: String(tools.size) }),
2140
- t('commands.stats.provider', { provider: ctx.providerId }),
2141
- t('commands.stats.model', { model: ctx.modelId }),
2142
- t('commands.stats.permission', { permission: ctx.config.permission }),
2143
- '',
2144
- t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
2145
- ].join('\n'),
2378
+ const base = [
2379
+ t('commands.stats.title'),
2380
+ '',
2381
+ t('commands.stats.messages', {
2382
+ total: String(msgs.length),
2383
+ user: String(userMsgs),
2384
+ assistant: String(assistantMsgs),
2385
+ system: String(systemMsgs),
2386
+ }),
2387
+ t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
2388
+ t('commands.stats.tools', { count: String(tools.size) }),
2389
+ t('commands.stats.provider', { provider: ctx.providerId }),
2390
+ t('commands.stats.model', { model: ctx.modelId }),
2391
+ t('commands.stats.permission', { permission: ctx.config.permission }),
2392
+ '',
2393
+ t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
2394
+ ]
2395
+
2396
+ // ── CRSI & SIS extensions ──
2397
+ const engine = ctx.engine
2398
+ const extras: string[] = []
2399
+
2400
+ try {
2401
+ const db = engine.getErrorSignatureDB?.()
2402
+ if (db) {
2403
+ const s = db.getStats()
2404
+ if (s.total > 0) {
2405
+ extras.push(
2406
+ '',
2407
+ '── SIS Self-Immune ──',
2408
+ `Signatures: ${s.total} total, ${s.active} active | Success rate: ${(s.avgSuccessRate * 100).toFixed(0)}%`,
2409
+ )
2410
+ }
2411
+ }
2412
+ } catch {
2413
+ /* SIS not initialized */
2146
2414
  }
2415
+
2416
+ try {
2417
+ const autoMemory = engine.getAutoMemory?.()
2418
+ if (autoMemory) {
2419
+ const count = autoMemory.sessionReflectionCount ?? 0
2420
+ if (count > 0) extras.push(`CRSI Reflections: ${count} turn(s) analyzed`)
2421
+ }
2422
+ } catch {
2423
+ /* CRSI not initialized */
2424
+ }
2425
+
2426
+ try {
2427
+ const meta = engine.getMetaRuleEngine?.()
2428
+ if (meta) {
2429
+ const analysis = meta.analyze()
2430
+ if (analysis.systemHealth) {
2431
+ extras.push(
2432
+ `System Health: ${analysis.systemHealth.score}/100 — ${analysis.systemHealth.assessment}`,
2433
+ )
2434
+ }
2435
+ }
2436
+ } catch {
2437
+ /* Meta not initialized */
2438
+ }
2439
+
2440
+ return { content: [...base, ...extras].join('\n') }
2147
2441
  }
2148
2442
 
2149
2443
  const summaryCmd: CommandHandler = (ctx) => {
@@ -2256,6 +2550,46 @@ const hooksCmd: CommandHandler = async (ctx) => {
2256
2550
  }
2257
2551
  }
2258
2552
 
2553
+ const hooksHealthCmd: CommandHandler = (ctx) => {
2554
+ const hookEngine = ctx.engine.getHookEngine?.()
2555
+ if (!hookEngine) {
2556
+ return { content: 'Hook engine not available.' }
2557
+ }
2558
+
2559
+ const health = hookEngine.getHookHealth()
2560
+ if (health.length === 0) {
2561
+ return { content: '✅ All hooks are healthy — no failures recorded.' }
2562
+ }
2563
+
2564
+ const lines: string[] = ['## 🪝 Hook Health Status', '']
2565
+ for (const { key, health: h } of health) {
2566
+ const status = h.disabled ? '🔴 DISABLED' : h.failures > 0 ? '🟡 DEGRADED' : '🟢 HEALTHY'
2567
+ lines.push(`### ${status} \`${key}\``)
2568
+ lines.push(`| 连续失败 | ${h.failures} (threshold: 5) |`)
2569
+ lines.push(`| 总失败次数 | ${h.totalFailures} |`)
2570
+ if (h.disabled) {
2571
+ const remaining = Math.max(0, 5 - (Date.now() - h.disabledAt) / 60_000)
2572
+ lines.push(`| 自动恢复 | ${remaining.toFixed(0)} min remaining |`)
2573
+ lines.push('')
2574
+ lines.push(`Use \`/hooks enable "${key}"\` to re-enable manually.`)
2575
+ }
2576
+ lines.push('')
2577
+ }
2578
+ return { content: lines.join('\n') }
2579
+ }
2580
+
2581
+ const hooksEnableCmd: CommandHandler = (ctx, args) => {
2582
+ const hookEngine = ctx.engine.getHookEngine?.()
2583
+ if (!hookEngine) return { content: 'Hook engine not available.' }
2584
+ const keyArg = args[0]?.trim().replace(/"/g, '')
2585
+ if (!keyArg) {
2586
+ return { content: 'Usage: /hooks enable "<hook-key>"\n\nUse /hooks health to see hook keys.' }
2587
+ }
2588
+ return hookEngine.reEnableHook(keyArg)
2589
+ ? { content: `✅ Hook "${keyArg}" re-enabled.` }
2590
+ : { content: `Hook "${keyArg}" not found or not disabled. Use /hooks health to check status.` }
2591
+ }
2592
+
2259
2593
  const batchCmd: CommandHandler = async (ctx) => {
2260
2594
  const t = resolveT(ctx)
2261
2595
  return { content: t('commands.batch.title') }
@@ -2426,6 +2760,7 @@ const resumeLastCmd: CommandHandler = async (ctx) => {
2426
2760
  : t('commands.resume.restored_full_footer', { loaded: String(messages.length) }),
2427
2761
  ].join('\n'),
2428
2762
  forwardedMessages: messages,
2763
+ resumeWarning: true,
2429
2764
  }
2430
2765
  }
2431
2766
 
@@ -3094,31 +3429,57 @@ To switch providers without clearing keys, use /switch <provider> <model>.`,
3094
3429
  // Phase 4 — Feedback
3095
3430
  // ═══════════════════════════════════════════════════════════════
3096
3431
 
3097
- const feedbackCmd: CommandHandler = (ctx, args) => {
3432
+ const feedbackCmd: CommandHandler = async (ctx, args) => {
3098
3433
  const message = args.join(' ').trim()
3099
3434
 
3100
3435
  const lines: string[] = ['── Feedback ──', '']
3101
3436
 
3437
+ // If a message was provided, save it to ~/.mipham/feedback/
3102
3438
  if (message) {
3103
- lines.push('Your feedback:')
3104
- lines.push('')
3105
- lines.push(' """')
3106
- for (const line of message.split('\n')) {
3107
- lines.push(' ' + line)
3439
+ try {
3440
+ const { writeFileSync, mkdirSync, existsSync } = await import('node:fs')
3441
+ const { join } = await import('node:path')
3442
+ const { homedir } = await import('node:os')
3443
+
3444
+ const dir = join(homedir(), '.mipham', 'feedback')
3445
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
3446
+
3447
+ const ts = new Date().toISOString().replace(/[:.]/g, '-')
3448
+ const file = join(dir, `feedback-${ts}.md`)
3449
+ const content = [
3450
+ '---',
3451
+ `timestamp: ${new Date().toISOString()}`,
3452
+ `version: ${ctx.version}`,
3453
+ `session: ${ctx.sessionId}`,
3454
+ `provider: ${ctx.providerId}`,
3455
+ `model: ${ctx.modelId}`,
3456
+ '---',
3457
+ '',
3458
+ message,
3459
+ ].join('\n')
3460
+ writeFileSync(file, content, 'utf-8')
3461
+
3462
+ lines.push('Your feedback:')
3463
+ lines.push('')
3464
+ lines.push(' """')
3465
+ for (const line of message.split('\n')) {
3466
+ lines.push(' ' + line)
3467
+ }
3468
+ lines.push(' """')
3469
+ lines.push('')
3470
+ lines.push(`✅ Saved to \`${file}\``)
3471
+ lines.push('')
3472
+ } catch (err) {
3473
+ lines.push(`⚠️ Save failed: ${String(err)}`)
3474
+ lines.push('')
3108
3475
  }
3109
- lines.push(' """')
3110
- lines.push('')
3111
- lines.push('── Preview Complete ──')
3112
- lines.push('')
3113
- lines.push('Copy the above and submit via one of the channels below.')
3114
- lines.push('')
3115
3476
  }
3116
3477
 
3117
3478
  lines.push('── Feedback Channels ──')
3118
3479
  lines.push('')
3119
3480
  lines.push(' 🐛 Bug Reports')
3120
3481
  lines.push(' GitHub Issues: https://github.com/One-Mipham/mipham-code/issues')
3121
- lines.push(' Template: Bug Report (include version + reproduction steps)')
3482
+ lines.push(' Or run `/bug-report` to auto-generate a diagnostic report')
3122
3483
  lines.push('')
3123
3484
  lines.push(' 💡 Feature Requests')
3124
3485
  lines.push(' GitHub Issues: https://github.com/One-Mipham/mipham-code/issues')
@@ -3499,6 +3860,9 @@ const commandsListCmd: CommandHandler = () => {
3499
3860
  '/recap': 'Session & Identity',
3500
3861
  '/export': 'Session & Identity',
3501
3862
  '/doctor': 'Session & Identity',
3863
+ '/dream': 'Session & Identity',
3864
+ '/bug-report': 'Session & Identity',
3865
+ '/changelog': 'Session & Identity',
3502
3866
  '/resume': 'Session & Identity',
3503
3867
  '/resume last': 'Session & Identity',
3504
3868
  '/resume delete': 'Session & Identity',
@@ -3694,6 +4058,9 @@ registry.set('/sis errors', sisErrorsCmd)
3694
4058
  registry.set('/sis stats', sisStatsCmd)
3695
4059
  registry.set('/sis clear', sisClearCmd)
3696
4060
  registry.set('/sis cleanup', sisCleanupCmd)
4061
+ registry.set('/dream', dreamCmd)
4062
+ registry.set('/bug-report', bugReportCmd)
4063
+ registry.set('/changelog', changelogCmd)
3697
4064
 
3698
4065
  // Workflow
3699
4066
  registry.set('/plan', planCmd)
@@ -3776,6 +4143,8 @@ registry.set('/stats', statsCmd)
3776
4143
  registry.set('/summary', summaryCmd)
3777
4144
  registry.set('/cd', cdCmd)
3778
4145
  registry.set('/hooks', hooksCmd)
4146
+ registry.set('/hooks health', hooksHealthCmd)
4147
+ registry.set('/hooks enable', hooksEnableCmd)
3779
4148
  registry.set('/batch', batchCmd)
3780
4149
 
3781
4150
  // ═══════════════════════════════════════════════════════════════
@@ -3812,6 +4181,9 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
3812
4181
  '/recap': 'Summarize session so far',
3813
4182
  '/export': 'Export conversation to file',
3814
4183
  '/doctor': 'System diagnostics',
4184
+ '/dream': 'Background memory consolidation',
4185
+ '/bug-report': 'Generate diagnostic report for GitHub Issues',
4186
+ '/changelog': 'Recent version history',
3815
4187
  '/resume': 'List saved sessions',
3816
4188
  '/resume last': 'Restore the most recent saved session',
3817
4189
  '/resume delete': 'Delete a saved session',
@@ -3909,6 +4281,8 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
3909
4281
  '/summary': 'Generate session summary',
3910
4282
  '/cd': 'Change session working directory',
3911
4283
  '/hooks': 'Manage lifecycle hook scripts',
4284
+ '/hooks health': 'Check hook health — see failures, disabled hooks, recovery status',
4285
+ '/hooks enable': 'Manually re-enable a hook that was auto-disabled after repeated failures',
3912
4286
  '/batch': 'Apply changes across multiple files',
3913
4287
  }
3914
4288
 
@@ -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
+ }