codemini-cli 0.4.1 → 0.4.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.
@@ -142,6 +142,7 @@ const TUI_COPY = {
142
142
  '🧩 用 /mode plan 切换到规划模式,让 AI 先出方案再动手。',
143
143
  '🆕 /new 可以新建一个干净的会话,重新开始工作。',
144
144
  '🧠 /memory 查看和管理 AI 的持久记忆,帮助它更好地理解你的偏好。',
145
+ '🌐 web_fetch 默认轻量读取网页;如需更好读取 JS 渲染页面,可运行 npm install -g playwright && playwright install chromium。',
145
146
  '💤 CodeMini 会自动"做梦"休息,整理错误信息并自我优化,越用越聪明~'
146
147
  ],
147
148
  toolSummaryExpanded: '工具摘要:已展开',
@@ -314,6 +315,18 @@ const TUI_COPY = {
314
315
  inputLocked: '计划审批进行中,请在审批框输入 /yes、/edit 或 /reject',
315
316
  answerLabel: '审批输入',
316
317
  answerPlaceholder: '/yes | /edit <反馈> | /reject'
318
+ },
319
+ reflectApproval: {
320
+ title: '审阅 Reflect 技能草稿?',
321
+ scopeLabel: '范围',
322
+ nameLabel: '名称',
323
+ targetLabel: '目标',
324
+ prompt: '输入 /yes 写入,输入 /edit <反馈> 修改,输入 /no 丢弃。',
325
+ invalidAnswer: '请输入 /yes、/edit <反馈> 或 /no。',
326
+ missingFeedback: '请在 /edit 后提供反馈内容。',
327
+ inputLocked: 'Reflect 审阅进行中,请在审阅框输入 /yes、/edit 或 /no',
328
+ answerLabel: '审阅输入',
329
+ answerPlaceholder: '/yes | /edit <反馈> | /no'
317
330
  }
318
331
  },
319
332
  en: {
@@ -354,6 +367,7 @@ const TUI_COPY = {
354
367
  '🧩 Use /mode plan to switch to planning mode — AI proposes a plan before coding.',
355
368
  '🆕 /new starts a fresh session to begin a clean slate.',
356
369
  '🧠 /memory lets you view and manage the AI\'s persistent memory for better personalization.',
370
+ '🌐 web_fetch uses a lightweight reader by default. For better JS-rendered pages: npm install -g playwright && playwright install chromium.',
357
371
  '💤 CodeMini auto-"dreams" to rest, consolidate errors, and self-optimize — it gets smarter over time~'
358
372
  ],
359
373
  toolSummaryExpanded: 'Tool summary: expanded',
@@ -526,6 +540,18 @@ const TUI_COPY = {
526
540
  inputLocked: 'Plan approval is active; type /yes, /edit <feedback>, or /reject',
527
541
  answerLabel: 'Approval input',
528
542
  answerPlaceholder: '/yes | /edit <feedback> | /reject'
543
+ },
544
+ reflectApproval: {
545
+ title: 'Review this reflected skill draft?',
546
+ scopeLabel: 'Scope',
547
+ nameLabel: 'Name',
548
+ targetLabel: 'Target',
549
+ prompt: 'Type /yes to write, /edit <feedback> to revise, or /no to discard.',
550
+ invalidAnswer: 'Please enter /yes, /edit <feedback>, or /no.',
551
+ missingFeedback: 'Please provide feedback after /edit.',
552
+ inputLocked: 'Reflect review is active; type /yes, /edit <feedback>, or /no',
553
+ answerLabel: 'Review input',
554
+ answerPlaceholder: '/yes | /edit <feedback> | /no'
529
555
  }
530
556
  }
531
557
  };
@@ -539,13 +565,24 @@ function getCopy(language) {
539
565
  }
540
566
 
541
567
  function messageLabel(label, copy) {
542
- return copy.roleLabels[label] || String(label || '').toUpperCase();
568
+ return copy?.roleLabels?.[label] || String(label || '').toUpperCase();
543
569
  }
544
570
 
545
571
  function roleStyle(label) {
546
572
  return ROLE_STYLES[label] || ROLE_STYLES.system;
547
573
  }
548
574
 
575
+ const PLAN_AGENT_ROLES = new Set(['planner', 'coder', 'reviewer', 'tester', 'summarizer']);
576
+
577
+ function normalizePlanAgentRole(role) {
578
+ const roleKey = String(role || '').trim().toLowerCase();
579
+ return PLAN_AGENT_ROLES.has(roleKey) ? roleKey : 'coder';
580
+ }
581
+
582
+ export function formatPlanAgentLabel(role, copy) {
583
+ return messageLabel(normalizePlanAgentRole(role), copy);
584
+ }
585
+
549
586
  function StatusPill({ label, value, color = 'cyanBright', textColor = 'black' }) {
550
587
  return h(
551
588
  Box,
@@ -892,7 +929,7 @@ function textFromSessionContent(content) {
892
929
  return sanitizeRenderableText(String(content || ''));
893
930
  }
894
931
 
895
- function buildUiMessagesFromSessionHistory(sessionMessages, nextId) {
932
+ export function buildUiMessagesFromSessionHistory(sessionMessages, nextId) {
896
933
  const source = Array.isArray(sessionMessages) ? sessionMessages : [];
897
934
  const out = [];
898
935
 
@@ -901,7 +938,7 @@ function buildUiMessagesFromSessionHistory(sessionMessages, nextId) {
901
938
  if (message.role === 'tool') continue;
902
939
 
903
940
  const text = textFromSessionContent(message.content);
904
- if (!text.trim() && message.role !== 'assistant') continue;
941
+ if (!text.trim()) continue;
905
942
 
906
943
  if (message.role === 'user') {
907
944
  out.push({ id: nextId(), label: 'you', text, color: 'blueBright' });
@@ -1109,6 +1146,25 @@ export function parsePlanApprovalAnswer(value) {
1109
1146
  return { action: 'invalid', command: '' };
1110
1147
  }
1111
1148
 
1149
+ export function parseReflectApprovalAnswer(value) {
1150
+ const raw = String(value || '').trim();
1151
+ if (!raw) return { action: 'empty', command: '' };
1152
+ const normalized = raw.toLowerCase();
1153
+ if (normalized === '/yes' || normalized === 'yes') {
1154
+ return { action: 'approve', command: '/yes' };
1155
+ }
1156
+ if (normalized === '/no' || normalized === 'no') {
1157
+ return { action: 'reject', command: '/no' };
1158
+ }
1159
+ const editMatch = raw.match(/^\/?edit(?:\s+(.+))?$/i);
1160
+ if (editMatch) {
1161
+ const feedback = String(editMatch[1] || '').trim();
1162
+ if (!feedback) return { action: 'missing_feedback', command: '' };
1163
+ return { action: 'edit', feedback, command: `/edit ${feedback}` };
1164
+ }
1165
+ return { action: 'invalid', command: '' };
1166
+ }
1167
+
1112
1168
  export function parsePendingPlanApprovalMessage(text = '') {
1113
1169
  const raw = String(text || '');
1114
1170
  if (!/^Plan approval is still pending\./i.test(raw.trim())) return null;
@@ -1122,6 +1178,21 @@ export function parsePendingPlanApprovalMessage(text = '') {
1122
1178
  return out;
1123
1179
  }
1124
1180
 
1181
+ export function parsePendingReflectSkillMessage(text = '') {
1182
+ const raw = String(text || '');
1183
+ if (!/\bReflect skill draft pending\./i.test(raw)) return null;
1184
+ const lines = raw.split(/\r?\n/);
1185
+ const out = { scope: '', name: '', confidence: '', targetPath: '' };
1186
+ for (const line of lines) {
1187
+ const trimmed = line.trim();
1188
+ if (trimmed.startsWith('Scope: ')) out.scope = trimmed.slice('Scope: '.length).trim();
1189
+ else if (/^\[\d+\]\s+/.test(trimmed) && !out.name) out.name = trimmed.replace(/^\[\d+\]\s+/, '').trim();
1190
+ else if (trimmed.startsWith('Confidence: ')) out.confidence = trimmed.slice('Confidence: '.length).trim();
1191
+ else if (trimmed.startsWith('Target: ')) out.targetPath = trimmed.slice('Target: '.length).trim();
1192
+ }
1193
+ return out;
1194
+ }
1195
+
1125
1196
  export function formatDeleteApprovalLines(copy, request) {
1126
1197
  const details = normalizeDeleteApprovalRequest(request);
1127
1198
  if (!details) return [];
@@ -1141,6 +1212,17 @@ export function formatPlanApprovalLines(copy, request) {
1141
1212
  return [String(copy?.planApproval?.title || '').trim()].filter(Boolean);
1142
1213
  }
1143
1214
 
1215
+ export function formatReflectApprovalLines(copy, request) {
1216
+ if (!request) return [];
1217
+ const c = copy?.reflectApproval || {};
1218
+ const lines = [String(c.title || '').trim()];
1219
+ if (request.scope) lines.push(`${c.scopeLabel || 'Scope'}: ${request.scope}`);
1220
+ if (request.name) lines.push(`${c.nameLabel || 'Name'}: ${request.name}`);
1221
+ if (request.targetPath) lines.push(`${c.targetLabel || 'Target'}: ${request.targetPath}`);
1222
+ if (c.prompt) lines.push(c.prompt);
1223
+ return lines.filter(Boolean);
1224
+ }
1225
+
1144
1226
  function getActivityDisplayParts(activity) {
1145
1227
  if (isCodeGenerationActivityName(activity?.name)) {
1146
1228
  return {
@@ -1419,10 +1501,9 @@ function PlanStrip({ planState, copy }) {
1419
1501
  Box,
1420
1502
  { flexDirection: 'column' },
1421
1503
  ...planState.steps.map((step, idx) => {
1422
- const roleKey = step.role ? step.role.toLowerCase() : '';
1423
- const normalizedRole = ['planner', 'coder', 'reviewer', 'tester', 'summarizer'].includes(roleKey) ? roleKey : 'coder';
1504
+ const normalizedRole = normalizePlanAgentRole(step.role);
1424
1505
  const stepTheme = roleStyle(normalizedRole);
1425
- const roleTag = step.role ? step.role.toUpperCase() : '';
1506
+ const roleTag = formatPlanAgentLabel(normalizedRole, copy);
1426
1507
  const stepDone = step.status === 'done' || isDone;
1427
1508
  const stepFailed = step.status === 'failed';
1428
1509
  const marker = stepFailed ? '✗' : stepDone ? '✓' : '·';
@@ -1431,8 +1512,8 @@ function PlanStrip({ planState, copy }) {
1431
1512
  Box,
1432
1513
  { key: `plan-step-${idx}` },
1433
1514
  h(Text, { color: markerColor }, `${marker} `),
1434
- roleTag ? h(Text, { color: stepTheme.badgeText, backgroundColor: stepTheme.badgeBg }, ` ${roleTag} `) : null,
1435
- roleTag ? h(Text, { color: 'gray' }, ' ') : null,
1515
+ h(Text, { color: stepTheme.badgeText, backgroundColor: stepTheme.badgeBg }, ` ${roleTag} `),
1516
+ h(Text, { color: 'gray' }, ' '),
1436
1517
  h(Text, { color: stepDone && !stepFailed ? 'gray' : 'white' }, `${step.index}. ${step.title}`)
1437
1518
  );
1438
1519
  }
@@ -1519,6 +1600,30 @@ function renderTextLine(msg, line, idx, color) {
1519
1600
  );
1520
1601
  }
1521
1602
 
1603
+ function historyListLineColor(line, fallbackColor) {
1604
+ const raw = String(line || '');
1605
+ const trimmed = raw.trim();
1606
+ if (!trimmed) return fallbackColor;
1607
+ if (/^Current session\s+/i.test(trimmed) || /^Recent sessions$/i.test(trimmed) || /^\d+\.\s+\S+/.test(trimmed)) {
1608
+ return 'cyanBright';
1609
+ }
1610
+ if (/^Messages\s+\d+$/i.test(trimmed) || /^\d+\s+msgs?\s+\|\s+updated\b/i.test(trimmed) || /^Tip:/i.test(trimmed)) {
1611
+ return 'gray';
1612
+ }
1613
+ if (/^resume:\s+\/history resume\b/i.test(trimmed)) {
1614
+ return 'blueBright';
1615
+ }
1616
+ return 'white';
1617
+ }
1618
+
1619
+ function isHistoryListMessage(msg) {
1620
+ const text = String(msg?.text || '');
1621
+ return msg?.label === 'system' &&
1622
+ /^Current session\s+/m.test(text) &&
1623
+ /^Recent sessions$/m.test(text) &&
1624
+ /resume:\s+\/history resume\b/m.test(text);
1625
+ }
1626
+
1522
1627
  export function parseAutoPlanSummaryMessage(text) {
1523
1628
  const raw = String(text || '').trim();
1524
1629
  if (!/^Auto plan finished\b/i.test(raw)) return null;
@@ -1963,7 +2068,7 @@ function PlanSummaryBubble({ msg, copy }) {
1963
2068
  Box,
1964
2069
  { marginBottom: planSteps.length > 0 || summary.approval || metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
1965
2070
  h(Text, { color: 'cyanBright' }, labels.plan),
1966
- h(Text, { color: 'gray' }, summary.planSummary)
2071
+ h(Text, { color: 'white' }, summary.planSummary)
1967
2072
  )
1968
2073
  : null,
1969
2074
  planSteps.length > 0
@@ -1972,12 +2077,20 @@ function PlanSummaryBubble({ msg, copy }) {
1972
2077
  { marginBottom: summary.approval || metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
1973
2078
  h(Text, { color: 'cyanBright' }, labels.steps),
1974
2079
  ...planSteps.flatMap((step, idx) => {
1975
- const roleTag = String(step?.role || '').trim().toUpperCase() || 'CODER';
2080
+ const roleKey = normalizePlanAgentRole(step?.role);
2081
+ const stepTheme = roleStyle(roleKey);
2082
+ const roleTag = formatPlanAgentLabel(roleKey, copy);
1976
2083
  const titleText = String(step?.title || '-').trim() || '-';
1977
2084
  const taskText = String(step?.task || '').trim();
1978
- const titleRow = h(Text, { key: `plan-step-title-${idx}`, color: 'gray' }, `${idx + 1}. [${roleTag}] ${titleText}`);
2085
+ const titleRow = h(
2086
+ Text,
2087
+ { key: `plan-step-title-${idx}`, color: 'white' },
2088
+ `${idx + 1}. `,
2089
+ h(Text, { color: stepTheme.badgeText, backgroundColor: stepTheme.badgeBg }, ` ${roleTag} `),
2090
+ ` ${titleText}`
2091
+ );
1979
2092
  if (!taskText) return [titleRow];
1980
- const taskRow = h(Text, { key: `plan-step-task-${idx}`, color: 'gray' }, ` - task: ${taskText}`);
2093
+ const taskRow = h(Text, { key: `plan-step-task-${idx}`, color: 'white' }, ` - task: ${taskText}`);
1981
2094
  return [titleRow, taskRow];
1982
2095
  })
1983
2096
  )
@@ -1987,7 +2100,7 @@ function PlanSummaryBubble({ msg, copy }) {
1987
2100
  Box,
1988
2101
  { marginBottom: metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
1989
2102
  h(Text, { color: 'yellowBright' }, labels.approval),
1990
- h(Text, { color: 'gray' }, summary.approval)
2103
+ h(Text, { color: 'white' }, summary.approval)
1991
2104
  )
1992
2105
  : null,
1993
2106
  metaItems.length > 0
@@ -1996,7 +2109,7 @@ function PlanSummaryBubble({ msg, copy }) {
1996
2109
  { marginBottom: summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0 },
1997
2110
  ...metaItems.flatMap((item, idx) => [
1998
2111
  idx > 0 ? h(Text, { key: `sep-${idx}`, color: 'gray' }, ' ') : null,
1999
- h(Text, { key: `meta-${idx}`, color: 'gray' }, item)
2112
+ h(Text, { key: `meta-${idx}`, color: 'white' }, item)
2000
2113
  ])
2001
2114
  )
2002
2115
  : null,
@@ -2005,7 +2118,7 @@ function PlanSummaryBubble({ msg, copy }) {
2005
2118
  Box,
2006
2119
  { marginBottom: summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
2007
2120
  h(Text, { color: 'yellowBright' }, labels.warnings),
2008
- h(Text, { color: 'gray' }, summary.warningSteps)
2121
+ h(Text, { color: 'white' }, summary.warningSteps)
2009
2122
  )
2010
2123
  : null,
2011
2124
  summary.failedSteps
@@ -2013,7 +2126,7 @@ function PlanSummaryBubble({ msg, copy }) {
2013
2126
  Box,
2014
2127
  { marginBottom: shortFile ? 1 : 0, flexDirection: 'column' },
2015
2128
  h(Text, { color: 'redBright' }, labels.failed),
2016
- h(Text, { color: 'gray' }, summary.failedSteps)
2129
+ h(Text, { color: 'white' }, summary.failedSteps)
2017
2130
  )
2018
2131
  : null,
2019
2132
  shortFile
@@ -2309,6 +2422,7 @@ export function collapseActivityChainRows(inputRows, showToolDetails, copy, maxV
2309
2422
 
2310
2423
  export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
2311
2424
  const rows = [];
2425
+ const isHistoryList = isHistoryListMessage(msg);
2312
2426
  const pushTextRows = (text) => {
2313
2427
  const lines = String(text || '').split('\n');
2314
2428
  let codeFence = false;
@@ -2329,8 +2443,8 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
2329
2443
  continue;
2330
2444
  }
2331
2445
  if (isMarkdownTableHeader(line, lines[lineIndex + 1])) {
2332
- const tableLines = [line];
2333
- lineIndex += 1; // skip separator
2446
+ const tableLines = [line, lines[lineIndex + 1]];
2447
+ lineIndex += 1; // separator included above
2334
2448
  while (lineIndex + 1 < lines.length && splitMarkdownTableCells(lines[lineIndex + 1]).length > 1) {
2335
2449
  tableLines.push(lines[lineIndex + 1]);
2336
2450
  lineIndex += 1;
@@ -2339,7 +2453,8 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
2339
2453
  continue;
2340
2454
  }
2341
2455
  let color = msg.color || roleStyle(msg.label).text || 'white';
2342
- if (line.startsWith('#')) color = 'cyanBright';
2456
+ if (isHistoryList) color = historyListLineColor(line, color);
2457
+ else if (line.startsWith('#')) color = 'cyanBright';
2343
2458
  else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) color = 'magentaBright';
2344
2459
  else if (trimmed.startsWith('>')) color = 'yellow';
2345
2460
  else if (/^[|└├│]/.test(trimmed)) color = 'gray';
@@ -2515,36 +2630,36 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
2515
2630
  return h(
2516
2631
  Box,
2517
2632
  { key: `row-table-${msg.id}-${idx}`, marginLeft: 1 },
2518
- h(Text, { color: row.isHeader ? 'cyanBright' : 'gray', bold: Boolean(row.isHeader) }, row.text)
2633
+ h(Text, { color: 'white', bold: Boolean(row.isHeader) }, row.text)
2519
2634
  );
2520
2635
  }
2521
2636
  if (row.kind === 'table-separator') {
2522
2637
  return h(
2523
2638
  Box,
2524
2639
  { key: `row-table-sep-${msg.id}-${idx}`, marginLeft: 1 },
2525
- h(Text, { color: 'gray' }, row.text)
2640
+ h(Text, { color: 'white' }, row.text)
2526
2641
  );
2527
2642
  }
2528
2643
  if (row.kind === 'table-vertical') {
2529
2644
  return h(
2530
2645
  Box,
2531
2646
  { key: `row-table-v-${msg.id}-${idx}`, marginLeft: 1 },
2532
- h(Text, { color: 'cyanBright', bold: true }, `${row.label}:`),
2533
- h(Text, { color: 'gray' }, row.text ? ` ${row.text}` : '')
2647
+ h(Text, { color: 'white', bold: true }, `${row.label}:`),
2648
+ h(Text, { color: 'white' }, row.text ? ` ${row.text}` : '')
2534
2649
  );
2535
2650
  }
2536
2651
  if (row.kind === 'table-vertical-continuation') {
2537
2652
  return h(
2538
2653
  Box,
2539
2654
  { key: `row-table-vc-${msg.id}-${idx}`, marginLeft: 3 },
2540
- h(Text, { color: 'gray' }, row.text)
2655
+ h(Text, { color: 'white' }, row.text)
2541
2656
  );
2542
2657
  }
2543
2658
  if (row.kind === 'table-vertical-separator') {
2544
2659
  return h(
2545
2660
  Box,
2546
2661
  { key: `row-table-vs-${msg.id}-${idx}`, marginLeft: 1 },
2547
- h(Text, { color: 'gray' }, row.text)
2662
+ h(Text, { color: 'white' }, row.text)
2548
2663
  );
2549
2664
  }
2550
2665
  if (row.kind === 'activity-collapsed') {
@@ -3150,6 +3265,38 @@ function PlanApprovalPanel({ request, inputValue, errorText, copy, cursorVisible
3150
3265
  );
3151
3266
  }
3152
3267
 
3268
+ function ReflectApprovalPanel({ request, inputValue, errorText, copy, cursorVisible }) {
3269
+ if (!request) return null;
3270
+ const placeholder = String(copy.reflectApproval.answerPlaceholder || '').trim();
3271
+ const lines = formatReflectApprovalLines(copy, request);
3272
+ return h(
3273
+ Box,
3274
+ {
3275
+ marginTop: 1,
3276
+ flexDirection: 'column',
3277
+ borderStyle: 'round',
3278
+ borderColor: 'yellowBright',
3279
+ paddingX: 1,
3280
+ paddingY: 0
3281
+ },
3282
+ ...lines.map((line, index) =>
3283
+ h(Text, { key: `reflect-approval-line-${index}`, color: 'yellowBright' }, line)
3284
+ ),
3285
+ h(
3286
+ Box,
3287
+ { marginTop: 1 },
3288
+ h(Text, { color: 'yellowBright' }, `${copy.reflectApproval.answerLabel}: `),
3289
+ h(ApprovalCursorLine, {
3290
+ inputValue,
3291
+ placeholder: placeholder || ' ',
3292
+ cursorVisible,
3293
+ accent: 'yellowBright'
3294
+ })
3295
+ ),
3296
+ errorText ? h(Text, { color: 'yellowBright' }, errorText) : null
3297
+ );
3298
+ }
3299
+
3153
3300
  function SignatureBar({ version = '' }) {
3154
3301
  return h(
3155
3302
  Box,
@@ -3240,7 +3387,10 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3240
3387
  const [pendingPlanApproval, setPendingPlanApproval] = useState(null);
3241
3388
  const [planApprovalInput, setPlanApprovalInput] = useState('');
3242
3389
  const [planApprovalError, setPlanApprovalError] = useState('');
3243
- const approvalLockActive = Boolean(pendingDeleteApproval || pendingRunApproval || pendingPlanApproval);
3390
+ const [pendingReflectApproval, setPendingReflectApproval] = useState(null);
3391
+ const [reflectApprovalInput, setReflectApprovalInput] = useState('');
3392
+ const [reflectApprovalError, setReflectApprovalError] = useState('');
3393
+ const approvalLockActive = Boolean(pendingDeleteApproval || pendingRunApproval || pendingPlanApproval || pendingReflectApproval);
3244
3394
  const activeAssistantIdRef = useRef(null);
3245
3395
  const activeAssistantAutoSkillNamesRef = useRef([]);
3246
3396
  const streamedAssistantHandledRef = useRef(false);
@@ -3656,6 +3806,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3656
3806
  setPlanState((prev) => ({ ...prev, pendingApproval: true }));
3657
3807
  setPlanApprovalInput('');
3658
3808
  setPlanApprovalError('');
3809
+ } else {
3810
+ const pendingReflectMeta = parsePendingReflectSkillMessage(result.text || '');
3811
+ if (pendingReflectMeta) {
3812
+ setPendingReflectApproval(pendingReflectMeta);
3813
+ setReflectApprovalInput('');
3814
+ setReflectApprovalError('');
3815
+ }
3659
3816
  }
3660
3817
  }
3661
3818
  setMessages((prev) => [
@@ -3798,6 +3955,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3798
3955
  setPendingPlanApproval(null);
3799
3956
  setPlanApprovalInput('');
3800
3957
  setPlanApprovalError('');
3958
+ setPendingReflectApproval(null);
3959
+ setReflectApprovalInput('');
3960
+ setReflectApprovalError('');
3801
3961
  setPlanState({
3802
3962
  current: 0,
3803
3963
  total: 0,
@@ -4485,6 +4645,46 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4485
4645
  return;
4486
4646
  }
4487
4647
 
4648
+ if (pendingReflectApproval) {
4649
+ if (key.return) {
4650
+ const parsed = parseReflectApprovalAnswer(reflectApprovalInput);
4651
+ if (parsed.action === 'approve' || parsed.action === 'reject' || parsed.action === 'edit') {
4652
+ setPendingReflectApproval(null);
4653
+ setReflectApprovalInput('');
4654
+ setReflectApprovalError('');
4655
+ runSubmission(parsed.command);
4656
+ } else if (parsed.action === 'missing_feedback') {
4657
+ setReflectApprovalError(copy.reflectApproval.missingFeedback);
4658
+ } else {
4659
+ setReflectApprovalError(copy.reflectApproval.invalidAnswer);
4660
+ }
4661
+ return;
4662
+ }
4663
+
4664
+ if (isBackspaceKey(value, key) || isDeleteKey(value, key)) {
4665
+ setReflectApprovalInput((prev) => prev.slice(0, -1));
4666
+ setReflectApprovalError('');
4667
+ return;
4668
+ }
4669
+
4670
+ if (isPrintableInput(value, key)) {
4671
+ setReflectApprovalInput((prev) => `${prev}${value}`);
4672
+ setReflectApprovalError('');
4673
+ return;
4674
+ }
4675
+
4676
+ if (key.ctrl && value === 'c') {
4677
+ if (busy && typeof runtime.abort === 'function') {
4678
+ runtime.abort();
4679
+ return;
4680
+ }
4681
+ exit();
4682
+ return;
4683
+ }
4684
+
4685
+ return;
4686
+ }
4687
+
4488
4688
  if (key.upArrow) {
4489
4689
  if (suggestionNav && commandSuggestions.length > 0) {
4490
4690
  setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'up'));
@@ -4754,6 +4954,17 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4754
4954
  }
4755
4955
  }, [runtimeState?.pendingPlanApproval, busy]);
4756
4956
 
4957
+ useEffect(() => {
4958
+ const pending = Boolean(runtimeState?.pendingReflectSkill);
4959
+ if (!pending) {
4960
+ setPendingReflectApproval(null);
4961
+ return;
4962
+ }
4963
+ if (!busy) {
4964
+ setPendingReflectApproval((prev) => prev || { scope: '', name: '', targetPath: '' });
4965
+ }
4966
+ }, [runtimeState?.pendingReflectSkill, busy]);
4967
+
4757
4968
  useEffect(() => {
4758
4969
  if (commandSuggestions.length === 0) {
4759
4970
  setSuggestionNav(false);
@@ -4800,7 +5011,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4800
5011
  ? { text: copy.runApproval.inputLocked }
4801
5012
  : pendingPlanApproval
4802
5013
  ? { text: copy.planApproval.inputLocked }
4803
- : null;
5014
+ : pendingReflectApproval
5015
+ ? { text: copy.reflectApproval.inputLocked }
5016
+ : null;
4804
5017
 
4805
5018
  return h(
4806
5019
  Box,
@@ -4850,6 +5063,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4850
5063
  copy,
4851
5064
  cursorVisible
4852
5065
  }),
5066
+ h(ReflectApprovalPanel, {
5067
+ request: pendingReflectApproval,
5068
+ inputValue: reflectApprovalInput,
5069
+ errorText: reflectApprovalError,
5070
+ copy,
5071
+ cursorVisible
5072
+ }),
4853
5073
  debugKeys
4854
5074
  ? h(
4855
5075
  Box,