codemini-cli 0.3.1 → 0.3.3

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.
@@ -102,7 +102,7 @@ const TUI_COPY = {
102
102
  startupHint: '使用 /help、/commands、/compact、/exit、!<shell>。Tab 可自动补全 slash 命令。',
103
103
  toolSummaryExpanded: '工具摘要:已展开',
104
104
  toolSummaryCollapsed: '工具摘要:已收起',
105
- toolChainCollapsed: (count) => `已折叠更早的 ${count} 个工具调用,按 Ctrl+T 展开全部`,
105
+ toolChainCollapsed: (count) => `已折叠更早的 ${count} 个工具调用`,
106
106
  toggleToolSummary: 'Ctrl+T 切换',
107
107
  scrollHint: '使用终端自己的滚动条或 scrollback',
108
108
  keyboardDebugEnabled: '键盘调试已开启',
@@ -136,10 +136,8 @@ const TUI_COPY = {
136
136
  doingGrep: '正在搜索关键词',
137
137
  doneCommand: '已执行命令',
138
138
  doingCommand: '正在执行命令',
139
- doneCreateTask: '已创建任务',
140
- doingCreateTask: '正在创建任务',
141
- doneUpdateTask: '已更新任务',
142
- doingUpdateTask: '正在更新任务',
139
+ doneUpdateTodos: '已更新待办',
140
+ doingUpdateTodos: '正在更新待办',
143
141
  doneGeneric: '已完成工具',
144
142
  doingGeneric: '正在执行工具',
145
143
  doneInstall: '已安装依赖',
@@ -156,14 +154,12 @@ const TUI_COPY = {
156
154
  doingDatabase: '正在启动数据库服务',
157
155
  doneDocker: '已完成 Docker 命令',
158
156
  doingDocker: '正在执行 Docker 命令',
159
- doneListServices: '已列出服务',
160
- doingListServices: '正在列出服务',
161
- doneServiceStatus: '已查看服务状态',
162
- doingServiceStatus: '正在查看服务状态',
163
- doneServiceLogs: '已查看服务日志',
164
- doingServiceLogs: '正在查看服务日志',
165
- doneStopService: '已停止服务',
166
- doingStopService: '正在停止服务',
157
+ doneListBackgroundTasks: '已列出后台任务',
158
+ doingListBackgroundTasks: '正在列出后台任务',
159
+ doneBackgroundTaskStatus: '已查看后台任务',
160
+ doingBackgroundTaskStatus: '正在查看后台任务',
161
+ doneStopBackgroundTask: '已停止后台任务',
162
+ doingStopBackgroundTask: '正在停止后台任务',
167
163
  doneCodeGeneration: '已生成代码',
168
164
  doingCodeGeneration: '正在生成代码',
169
165
  doneSkill: '已完成技能',
@@ -247,7 +243,7 @@ const TUI_COPY = {
247
243
  startupHint: 'Use /help, /commands, /compact, /exit, !<shell>. Tab for slash autocomplete.',
248
244
  toolSummaryExpanded: 'Tool summary: expanded',
249
245
  toolSummaryCollapsed: 'Tool summary: collapsed',
250
- toolChainCollapsed: (count) => `${count} earlier tool calls hidden, press Ctrl+T to expand`,
246
+ toolChainCollapsed: (count) => `${count} earlier tool calls hidden`,
251
247
  toggleToolSummary: 'Ctrl+T to toggle',
252
248
  scrollHint: 'Scroll with your terminal scrollbar or scrollback',
253
249
  keyboardDebugEnabled: 'Keyboard debug enabled',
@@ -281,10 +277,8 @@ const TUI_COPY = {
281
277
  doingGrep: 'Searching keywords',
282
278
  doneCommand: 'Ran command',
283
279
  doingCommand: 'Running command',
284
- doneCreateTask: 'Created task',
285
- doingCreateTask: 'Creating task',
286
- doneUpdateTask: 'Updated task',
287
- doingUpdateTask: 'Updating task',
280
+ doneUpdateTodos: 'Updated todos',
281
+ doingUpdateTodos: 'Updating todos',
288
282
  doneGeneric: 'Completed tool',
289
283
  doingGeneric: 'Running tool',
290
284
  doneInstall: 'Dependencies installed',
@@ -301,14 +295,12 @@ const TUI_COPY = {
301
295
  doingDatabase: 'Starting database service',
302
296
  doneDocker: 'Docker command completed',
303
297
  doingDocker: 'Running Docker command',
304
- doneListServices: 'Listed services',
305
- doingListServices: 'Listing services',
306
- doneServiceStatus: 'Checked service status',
307
- doingServiceStatus: 'Checking service status',
308
- doneServiceLogs: 'Viewed service logs',
309
- doingServiceLogs: 'Viewing service logs',
310
- doneStopService: 'Stopped service',
311
- doingStopService: 'Stopping service',
298
+ doneListBackgroundTasks: 'Listed background tasks',
299
+ doingListBackgroundTasks: 'Listing background tasks',
300
+ doneBackgroundTaskStatus: 'Checked background task',
301
+ doingBackgroundTaskStatus: 'Checking background task',
302
+ doneStopBackgroundTask: 'Stopped background task',
303
+ doingStopBackgroundTask: 'Stopping background task',
312
304
  doneCodeGeneration: 'Code generated',
313
305
  doingCodeGeneration: 'Generating code',
314
306
  doneSkill: 'Completed skill',
@@ -697,6 +689,61 @@ function parseRichTextSegments(line, baseColor) {
697
689
  });
698
690
  }
699
691
 
692
+ export function sanitizeRenderableText(value) {
693
+ const input = String(value ?? '');
694
+ if (!input) return '';
695
+
696
+ return input
697
+ .replace(/\r\n/g, '\n')
698
+ .replace(/\r/g, '\n')
699
+ .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '')
700
+ .replace(/[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g, '')
701
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '');
702
+ }
703
+
704
+ function textFromSessionContent(content) {
705
+ if (typeof content === 'string') return sanitizeRenderableText(content);
706
+ if (Array.isArray(content)) {
707
+ return sanitizeRenderableText(
708
+ content
709
+ .map((part) => {
710
+ if (typeof part === 'string') return part;
711
+ if (part?.type === 'text') return part.text || '';
712
+ return '';
713
+ })
714
+ .join('')
715
+ );
716
+ }
717
+ return sanitizeRenderableText(String(content || ''));
718
+ }
719
+
720
+ function buildUiMessagesFromSessionHistory(sessionMessages, nextId) {
721
+ const source = Array.isArray(sessionMessages) ? sessionMessages : [];
722
+ const out = [];
723
+
724
+ for (const message of source) {
725
+ if (!message || typeof message !== 'object') continue;
726
+ if (message.role === 'tool') continue;
727
+
728
+ const text = textFromSessionContent(message.content);
729
+ if (!text.trim() && message.role !== 'assistant') continue;
730
+
731
+ if (message.role === 'user') {
732
+ out.push({ id: nextId(), label: 'you', text, color: 'blueBright' });
733
+ continue;
734
+ }
735
+ if (message.role === 'assistant') {
736
+ out.push({ id: nextId(), label: 'coder', text, color: 'greenBright' });
737
+ continue;
738
+ }
739
+ if (message.role === 'system') {
740
+ out.push({ id: nextId(), label: 'system', text, color: 'yellowBright' });
741
+ }
742
+ }
743
+
744
+ return out;
745
+ }
746
+
700
747
  function safeJsonParse(raw) {
701
748
  try {
702
749
  return JSON.parse(String(raw || '{}'));
@@ -828,7 +875,7 @@ function getActivityDisplayParts(activity) {
828
875
  };
829
876
  }
830
877
  const parsed = parseToolDisplayName(activity?.name);
831
- if (parsed.base === 'run' || parsed.base === 'start_service') {
878
+ if (parsed.base === 'run') {
832
879
  const intent = classifyCommandIntent(parsed.target);
833
880
  return {
834
881
  primary: getIntentLabel(intent.kind),
@@ -862,14 +909,11 @@ function getActivityDisplayParts(activity) {
862
909
  grep: 'Search',
863
910
  glob: 'Glob',
864
911
  list: 'List',
865
- start_service: 'Service',
866
- list_services: 'Services',
867
- get_service_status: 'Status',
868
- get_service_logs: 'Logs',
869
- stop_service: 'Stop',
912
+ list_background_tasks: 'Tasks',
913
+ get_background_task: 'Task',
914
+ stop_background_task: 'Stop',
870
915
  list_files: 'Glob',
871
- create_task: 'Task',
872
- update_task: 'Task'
916
+ update_todos: 'Update Todos'
873
917
  };
874
918
  return {
875
919
  primary: labels[parsed.base] || parsed.base || 'Tool',
@@ -1018,6 +1062,21 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
1018
1062
  function PlanStrip({ planState, copy }) {
1019
1063
  if (!planState || !planState.total) return null;
1020
1064
  const progress = `${planState.current}/${planState.total}`;
1065
+ const stripComplete = Boolean(planState.completed) && !planState.failed;
1066
+ const statusLabel = planState.failed ? copy.generic.attention : stripComplete ? copy.generic.taskCompleted : copy.generic.active;
1067
+ const statusColor = planState.failed ? 'redBright' : stripComplete ? 'cyanBright' : 'greenBright';
1068
+ const roleLabel =
1069
+ planState.resultStatus || stripComplete || planState.failed
1070
+ ? copy?.roleLabels?.system === 'SYSTEM'
1071
+ ? 'RESULT'
1072
+ : '结果'
1073
+ : String(planState.role || 'agent').toUpperCase();
1074
+ const titleLabel =
1075
+ planState.resultStatus || stripComplete || planState.failed
1076
+ ? copy?.roleLabels?.system === 'SYSTEM'
1077
+ ? 'Plan execution result'
1078
+ : '计划执行结果'
1079
+ : planState.title || 'running plan step';
1021
1080
  return h(
1022
1081
  Box,
1023
1082
  {
@@ -1036,11 +1095,18 @@ function PlanStrip({ planState, copy }) {
1036
1095
  null,
1037
1096
  h(Text, { color: 'black', backgroundColor: planState.failed ? 'red' : 'cyanBright' }, ` ${copy.generic.plan} ${progress} `),
1038
1097
  h(Text, { color: 'gray' }, ' '),
1039
- h(Text, { color: 'magentaBright' }, String(planState.role || 'agent').toUpperCase())
1098
+ h(Text, { color: 'magentaBright' }, roleLabel)
1040
1099
  ),
1041
- h(Text, { color: planState.failed ? 'redBright' : 'greenBright' }, planState.failed ? copy.generic.attention : copy.generic.active)
1100
+ h(Text, { color: statusColor }, statusLabel)
1042
1101
  ),
1043
- h(Text, { color: 'white' }, planState.title || 'running plan step'),
1102
+ h(Text, { color: 'white' }, titleLabel),
1103
+ planState.resultVerified
1104
+ ? h(
1105
+ Box,
1106
+ { marginTop: 1 },
1107
+ h(Text, { color: 'gray' }, trimText(planState.resultVerified, 120))
1108
+ )
1109
+ : null,
1044
1110
  planState.steps.length > 0
1045
1111
  ? h(
1046
1112
  Box,
@@ -1058,15 +1124,24 @@ function PlanStrip({ planState, copy }) {
1058
1124
  )
1059
1125
  )
1060
1126
  )
1127
+ : null,
1128
+ planState.resultNext
1129
+ ? h(
1130
+ Box,
1131
+ { marginTop: 1 },
1132
+ h(Text, { color: 'black', backgroundColor: 'yellowBright' }, ' NEXT '),
1133
+ h(Text, { color: 'gray' }, ` ${trimText(planState.resultNext, 108)}`)
1134
+ )
1061
1135
  : null
1062
1136
  );
1063
1137
  }
1064
1138
 
1065
- function Header({ sessionId, model, shellName, safeMode = true }) {
1139
+ function Header({ sessionId, model, sdkProvider, shellName, safeMode = true }) {
1066
1140
  const shortSession = String(sessionId || '').slice(-12) || '-';
1067
1141
  const modeValue = safeMode ? 'SAFE' : 'OPEN';
1068
1142
  const modeColor = safeMode ? 'greenBright' : 'redBright';
1069
1143
  const modeTextColor = safeMode ? 'black' : 'white';
1144
+ const sdkValue = String(sdkProvider || 'openai-compatible');
1070
1145
  return h(
1071
1146
  Box,
1072
1147
  { width: '100%', justifyContent: 'center', marginTop: 1, marginBottom: 2 },
@@ -1094,6 +1169,7 @@ function Header({ sessionId, model, shellName, safeMode = true }) {
1094
1169
  h(
1095
1170
  Box,
1096
1171
  { flexDirection: 'row', justifyContent: 'center' },
1172
+ h(StatusPill, { label: 'SDK', value: sdkValue, color: 'blueBright', textColor: 'white' }),
1097
1173
  h(StatusPill, { label: 'MODEL', value: model, color: 'cyanBright', textColor: 'black' }),
1098
1174
  h(StatusPill, { label: 'SHELL', value: shellName || 'powershell', color: 'yellowBright', textColor: 'black' }),
1099
1175
  h(StatusPill, { label: 'SESSION', value: shortSession, color: 'magentaBright', textColor: 'black' }),
@@ -1149,6 +1225,7 @@ export function parseAutoPlanSummaryMessage(text) {
1149
1225
  filePath: '',
1150
1226
  planSummary: '',
1151
1227
  finalSummary: '',
1228
+ approval: '',
1152
1229
  stepsTotal: '',
1153
1230
  completed: '',
1154
1231
  warnings: '',
@@ -1159,8 +1236,10 @@ export function parseAutoPlanSummaryMessage(text) {
1159
1236
 
1160
1237
  for (const line of lines.slice(1)) {
1161
1238
  if (line.startsWith('File: ')) parsed.filePath = line.slice('File: '.length).trim();
1239
+ else if (line.startsWith('Plan File: ')) parsed.filePath = line.slice('Plan File: '.length).trim();
1162
1240
  else if (line.startsWith('Plan Summary: ')) parsed.planSummary = line.slice('Plan Summary: '.length).trim();
1163
1241
  else if (line.startsWith('Final Summary: ')) parsed.finalSummary = line.slice('Final Summary: '.length).trim();
1242
+ else if (line.startsWith('Approval: ')) parsed.approval = line.slice('Approval: '.length).trim();
1164
1243
  else if (line.startsWith('Steps: ')) parsed.stepsTotal = line.slice('Steps: '.length).trim();
1165
1244
  else if (line.startsWith('Completed: ')) parsed.completed = line.slice('Completed: '.length).trim();
1166
1245
  else if (line.startsWith('Warnings: ')) parsed.warnings = line.slice('Warnings: '.length).trim();
@@ -1184,6 +1263,31 @@ export function parsePlanProgressLine(text) {
1184
1263
  };
1185
1264
  }
1186
1265
 
1266
+ export function parsePlanExecutionResult(text) {
1267
+ const raw = String(text || '').trim();
1268
+ if (!raw) return null;
1269
+ const statusMatch = raw.match(/(?:^|\n)\s*Status:\s*(done|partial|blocked)\s*$/im);
1270
+ const verifiedMatch = raw.match(/(?:^|\n)\s*Verified:\s*(.+)\s*$/im);
1271
+ const nextMatch = raw.match(/(?:^|\n)\s*Next:\s*(.+)\s*$/im);
1272
+ if (!statusMatch && !verifiedMatch && !nextMatch) return null;
1273
+ return {
1274
+ status: String(statusMatch?.[1] || '').trim().toLowerCase(),
1275
+ verified: String(verifiedMatch?.[1] || '').trim(),
1276
+ next: String(nextMatch?.[1] || '').trim()
1277
+ };
1278
+ }
1279
+
1280
+ export function stripPlanExecutionResult(text) {
1281
+ const raw = String(text || '');
1282
+ if (!parsePlanExecutionResult(raw)) return raw;
1283
+ return raw
1284
+ .replace(/(?:^|\n)\s*Status:\s*(done|partial|blocked)\s*$/im, '')
1285
+ .replace(/(?:^|\n)\s*Verified:\s*.+\s*$/im, '')
1286
+ .replace(/(?:^|\n)\s*Next:\s*.+\s*$/im, '')
1287
+ .replace(/\n{3,}/g, '\n\n')
1288
+ .trim();
1289
+ }
1290
+
1187
1291
  function getTailPreviewWindow(text, maxLines = 3) {
1188
1292
  const source = String(text || '');
1189
1293
  if (!source.trim()) return { lines: [], startLine: 1 };
@@ -1410,7 +1514,7 @@ function finishCodeGeneration(msg, now = Date.now()) {
1410
1514
 
1411
1515
  export function injectPlanStateMessage(messages, planState, activeUserMessageId, activeAssistantId) {
1412
1516
  const source = Array.isArray(messages) ? messages : [];
1413
- if (!planState || !planState.total) return source;
1517
+ if (!planState || !planState.total || planState.pendingApproval) return source;
1414
1518
  const synthetic = {
1415
1519
  id: `plan-state-${planState.current}-${planState.total}-${planState.role || 'agent'}`,
1416
1520
  label: 'system',
@@ -1456,6 +1560,7 @@ function PlanSummaryBubble({ msg, copy }) {
1456
1560
  ? {
1457
1561
  conclusion: 'Conclusion',
1458
1562
  plan: 'Plan',
1563
+ approval: 'Approval',
1459
1564
  warnings: 'Warnings',
1460
1565
  failed: 'Failed',
1461
1566
  file: 'File',
@@ -1467,6 +1572,7 @@ function PlanSummaryBubble({ msg, copy }) {
1467
1572
  : {
1468
1573
  conclusion: '结论',
1469
1574
  plan: '计划',
1575
+ approval: '审批',
1470
1576
  warnings: '警告',
1471
1577
  failed: '失败',
1472
1578
  file: '文件',
@@ -1520,11 +1626,19 @@ function PlanSummaryBubble({ msg, copy }) {
1520
1626
  summary.planSummary
1521
1627
  ? h(
1522
1628
  Box,
1523
- { marginBottom: metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
1629
+ { marginBottom: summary.approval || metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
1524
1630
  h(Text, { color: 'cyanBright' }, labels.plan),
1525
1631
  h(Text, { color: 'gray' }, summary.planSummary)
1526
1632
  )
1527
1633
  : null,
1634
+ summary.approval
1635
+ ? h(
1636
+ Box,
1637
+ { marginBottom: metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
1638
+ h(Text, { color: 'yellowBright' }, labels.approval),
1639
+ h(Text, { color: 'gray' }, summary.approval)
1640
+ )
1641
+ : null,
1528
1642
  metaItems.length > 0
1529
1643
  ? h(
1530
1644
  Box,
@@ -1706,7 +1820,7 @@ export function normalizeActivitySpacingRows(inputRows) {
1706
1820
 
1707
1821
  if (isActivityRow(row) && !isActivityRow(next) && next) {
1708
1822
  const last = normalized.at(-1);
1709
- if (!isBlankTextRow(last) && !(next.kind === 'status')) {
1823
+ if (!isBlankTextRow(last) && !(next.kind === 'status') && !isTodoRow(next)) {
1710
1824
  normalized.push({
1711
1825
  kind: 'text',
1712
1826
  text: ' ',
@@ -1723,6 +1837,10 @@ export function normalizeActivitySpacingRows(inputRows) {
1723
1837
  return normalized;
1724
1838
  }
1725
1839
 
1840
+ function isTodoRow(row) {
1841
+ return row?.kind === 'todo-item';
1842
+ }
1843
+
1726
1844
  function isReadActivityName(name) {
1727
1845
  const parsed = parseToolDisplayName(name);
1728
1846
  return parsed.base === 'read' || parsed.base === 'Read';
@@ -1906,6 +2024,19 @@ function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
1906
2024
  durationText,
1907
2025
  isLatestTool: idx === total - 1
1908
2026
  });
2027
+ const todoItems = parseToolDisplayName(tool.name).base === 'update_todos' ? tool?.arguments?.todos : null;
2028
+ if (Array.isArray(todoItems) && todoItems.length > 0 && tool.status !== 'running') {
2029
+ for (const item of todoItems) {
2030
+ const status = String(item?.status || 'pending').trim();
2031
+ rows.push({
2032
+ kind: 'todo-item',
2033
+ status,
2034
+ text: String(item?.content || '').trim(),
2035
+ activeForm: String(item?.activeForm || '').trim()
2036
+ });
2037
+ }
2038
+ return;
2039
+ }
1909
2040
  if ((showToolDetails || idx === total - 1) && tool.summary && tool.status !== 'running') {
1910
2041
  for (const line of String(tool.summary).split('\n')) {
1911
2042
  pushWrappedRow(rows, { kind: 'activity-summary', text: line || ' ', color: 'gray' }, Math.max(8, contentWidth - 4));
@@ -2000,6 +2131,21 @@ function renderMessageRow(msg, row, idx, loaderTick) {
2000
2131
  h(Text, { color: 'gray' }, `└ ${row.text}`)
2001
2132
  );
2002
2133
  }
2134
+ if (row.kind === 'todo-item') {
2135
+ const marker =
2136
+ row.status === 'completed' ? '[✓]' : row.status === 'in_progress' ? '[*]' : '[ ]';
2137
+ const color =
2138
+ row.status === 'completed' ? 'gray' : row.status === 'in_progress' ? 'white' : 'gray';
2139
+ const dimColor = row.status === 'completed';
2140
+ return h(
2141
+ Box,
2142
+ { key: `row-todo-${msg.id}-${idx}`, marginLeft: 2, marginBottom: 1 },
2143
+ h(Text, { color: 'gray' }, ' '),
2144
+ h(Text, { color }, marker),
2145
+ h(Text, { color: 'gray' }, ' '),
2146
+ h(Text, { color, dimColor }, row.text || row.activeForm || ' ')
2147
+ );
2148
+ }
2003
2149
  if (row.kind === 'table') {
2004
2150
  return h(
2005
2151
  Box,
@@ -2497,7 +2643,7 @@ function makeIdleStatus(copy, snapshot, variant = 'ready') {
2497
2643
  );
2498
2644
  }
2499
2645
 
2500
- export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName = 'powershell', version = '', safeMode = true }) {
2646
+ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compatible', language = 'zh', shellName = 'powershell', version = '', safeMode = true }) {
2501
2647
  const copy = getCopy(language);
2502
2648
  const stdoutCols = Number(process.stdout?.columns || 120);
2503
2649
  const [inputValue, setInputValue] = useState('');
@@ -2513,6 +2659,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2513
2659
  const [cursorVisible, setCursorVisible] = useState(true);
2514
2660
  const [displaySessionId, setDisplaySessionId] = useState(sessionId);
2515
2661
  const [displayModel, setDisplayModel] = useState(model);
2662
+ const [displaySdkProvider, setDisplaySdkProvider] = useState(sdkProvider);
2516
2663
  const [pendingQueue, setPendingQueue] = useState([]);
2517
2664
  const [loaderTick, setLoaderTick] = useState(0);
2518
2665
  const [runtimeStatus, setRuntimeStatus] = useState(
@@ -2526,7 +2673,12 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2526
2673
  role: '',
2527
2674
  title: '',
2528
2675
  failed: false,
2529
- steps: []
2676
+ steps: [],
2677
+ pendingApproval: false,
2678
+ completed: false,
2679
+ resultStatus: '',
2680
+ resultVerified: '',
2681
+ resultNext: ''
2530
2682
  });
2531
2683
  const [debugKeys, setDebugKeys] = useState(false);
2532
2684
  const [lastKeyDebug, setLastKeyDebug] = useState('');
@@ -2605,6 +2757,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2605
2757
  if (!snapshot) return;
2606
2758
  setDisplaySessionId(snapshot.sessionId || sessionId);
2607
2759
  setDisplayModel(snapshot.model || model);
2760
+ setDisplaySdkProvider(snapshot.sdkProvider || sdkProvider);
2608
2761
  setRuntimeState(snapshot);
2609
2762
  setRuntimeStatus(makeIdleStatus(copy, snapshot, variant));
2610
2763
  };
@@ -2641,6 +2794,11 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2641
2794
  role,
2642
2795
  title,
2643
2796
  failed: false,
2797
+ pendingApproval: false,
2798
+ completed: false,
2799
+ resultStatus: '',
2800
+ resultVerified: '',
2801
+ resultNext: '',
2644
2802
  steps: [...withoutCurrent, { index: current, total, role, title, status: 'active' }].sort((a, b) => a.index - b.index)
2645
2803
  };
2646
2804
  });
@@ -2763,6 +2921,10 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2763
2921
 
2764
2922
  const appendResultMessage = (result) => {
2765
2923
  if (result.type === 'noop') return;
2924
+ if (Array.isArray(result.restoredMessages)) {
2925
+ setMessages(buildUiMessagesFromSessionHistory(result.restoredMessages, nextId));
2926
+ syncRuntimeVisualState('after');
2927
+ }
2766
2928
  if (
2767
2929
  result.type === 'system' &&
2768
2930
  typeof result.text === 'string' &&
@@ -2790,7 +2952,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2790
2952
  {
2791
2953
  id: nextId(),
2792
2954
  label: 'system',
2793
- text: copy.generic.keyboardDebugStatus(debugKeys),
2955
+ text: sanitizeRenderableText(copy.generic.keyboardDebugStatus(debugKeys)),
2794
2956
  color: 'yellowBright'
2795
2957
  }
2796
2958
  ]);
@@ -2798,13 +2960,14 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2798
2960
  }
2799
2961
  }
2800
2962
  if (result.type === 'assistant') {
2801
- if (!activeAssistantIdRef.current && result.text) {
2963
+ const { displayText } = applyPlanExecutionResult(result.text);
2964
+ if (!activeAssistantIdRef.current && displayText) {
2802
2965
  setMessages((prev) => [
2803
2966
  ...prev,
2804
2967
  {
2805
2968
  id: nextId(),
2806
2969
  label: 'coder',
2807
- text: result.text,
2970
+ text: sanitizeRenderableText(displayText),
2808
2971
  color: 'greenBright',
2809
2972
  autoSkillNames: activeAssistantAutoSkillNamesRef.current
2810
2973
  }
@@ -2813,12 +2976,27 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2813
2976
  return;
2814
2977
  }
2815
2978
  const parsedPlanSummary = result.type === 'system' ? parseAutoPlanSummaryMessage(result.text || '') : null;
2979
+ if (parsedPlanSummary?.approval === 'pending') {
2980
+ setPlanState({
2981
+ current: 0,
2982
+ total: 0,
2983
+ role: '',
2984
+ title: '',
2985
+ failed: false,
2986
+ steps: [],
2987
+ pendingApproval: true,
2988
+ completed: false,
2989
+ resultStatus: '',
2990
+ resultVerified: '',
2991
+ resultNext: ''
2992
+ });
2993
+ }
2816
2994
  setMessages((prev) => [
2817
2995
  ...prev,
2818
2996
  {
2819
2997
  id: nextId(),
2820
2998
  label: 'system',
2821
- text: result.text || '',
2999
+ text: sanitizeRenderableText(result.text || ''),
2822
3000
  color: 'yellowBright',
2823
3001
  ...(parsedPlanSummary ? { planSummary: parsedPlanSummary } : {})
2824
3002
  }
@@ -2865,6 +3043,31 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2865
3043
  });
2866
3044
  };
2867
3045
 
3046
+ const applyPlanExecutionResult = (rawText) => {
3047
+ const parsedExecution = parsePlanExecutionResult(rawText);
3048
+ if (!parsedExecution || !planTextBufferRef.current) return { parsedExecution: null, displayText: rawText };
3049
+ setPlanState((prev) => {
3050
+ if (!prev.total) return prev;
3051
+ return {
3052
+ ...prev,
3053
+ completed: parsedExecution.status === 'done',
3054
+ failed: parsedExecution.status === 'blocked',
3055
+ resultStatus: parsedExecution.status || prev.resultStatus,
3056
+ resultVerified: parsedExecution.verified || prev.resultVerified,
3057
+ resultNext: parsedExecution.next || prev.resultNext,
3058
+ steps: (prev.steps || []).map((step) =>
3059
+ step.index === prev.current && step.status === 'active'
3060
+ ? { ...step, status: parsedExecution.status === 'blocked' ? 'failed' : 'done' }
3061
+ : step
3062
+ )
3063
+ };
3064
+ });
3065
+ return {
3066
+ parsedExecution,
3067
+ displayText: stripPlanExecutionResult(rawText)
3068
+ };
3069
+ };
3070
+
2868
3071
  const ensureActiveAssistant = () => {
2869
3072
  if (activeAssistantIdRef.current) return activeAssistantIdRef.current;
2870
3073
  const aid = nextId();
@@ -2913,7 +3116,19 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2913
3116
  setBusy(true);
2914
3117
  setInputStage('sending');
2915
3118
  setRuntimeStatus(makeStatus(copy.runtime.sendingToGateway, copy.runtime.preparingRequest, 'yellowBright'));
2916
- setPlanState({ current: 0, total: 0, role: '', title: '', failed: false, steps: [] });
3119
+ setPlanState({
3120
+ current: 0,
3121
+ total: 0,
3122
+ role: '',
3123
+ title: '',
3124
+ failed: false,
3125
+ steps: [],
3126
+ pendingApproval: false,
3127
+ completed: false,
3128
+ resultStatus: '',
3129
+ resultVerified: '',
3130
+ resultNext: ''
3131
+ });
2917
3132
  planTextBufferRef.current = '';
2918
3133
  activeAssistantIdRef.current = null;
2919
3134
  activeAssistantAutoSkillNamesRef.current = [];
@@ -2996,16 +3211,19 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2996
3211
  if (hadActiveAssistant) {
2997
3212
  streamedAssistantHandledRef.current = true;
2998
3213
  }
3214
+ const { displayText } = applyPlanExecutionResult(event.text);
2999
3215
  if (targetId && !hasPlannedTools) {
3000
3216
  setMessages((prev) =>
3001
3217
  prev.map((m) => {
3002
3218
  if (m.id !== targetId) return m;
3003
- const responseText = typeof event.text === 'string' ? event.text.trim() : '';
3004
- const shouldSynthesizeCompletion = !responseText && m.syntheticPrelude;
3219
+ const responseText = typeof displayText === 'string' ? displayText.trim() : '';
3220
+ const cleanedExistingText = stripPlanExecutionResult(String(m.text || '')).trim();
3221
+ const finalText = responseText || cleanedExistingText;
3222
+ const shouldSynthesizeCompletion = !finalText && m.syntheticPrelude;
3005
3223
  return {
3006
3224
  ...m,
3007
- ...(responseText
3008
- ? { text: event.text, syntheticPrelude: false }
3225
+ ...(finalText
3226
+ ? { text: finalText, syntheticPrelude: false }
3009
3227
  : shouldSynthesizeCompletion
3010
3228
  ? { text: buildSyntheticCompletionText(m, copy), syntheticPrelude: false }
3011
3229
  : {}),
@@ -3024,9 +3242,10 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3024
3242
  activeAssistantIdRef.current = null;
3025
3243
  }
3026
3244
  if (!hadActiveAssistant && !hasPlannedTools && event.text) {
3245
+ const cleanedStandaloneText = stripPlanExecutionResult(String(displayText || event.text)).trim();
3027
3246
  setMessages((prev) => [
3028
3247
  ...prev,
3029
- { id: nextId(), label: 'coder', text: event.text, color: 'greenBright' }
3248
+ { id: nextId(), label: 'coder', text: cleanedStandaloneText, color: 'greenBright' }
3030
3249
  ]);
3031
3250
  }
3032
3251
  }
@@ -3265,7 +3484,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3265
3484
  appendResultMessage(result);
3266
3485
  })
3267
3486
  .catch((err) => {
3268
- setRuntimeStatus(makeStatus(copy.runtime.requestFailed, err.message, 'redBright'));
3487
+ const message = sanitizeRenderableText(err?.message || String(err));
3488
+ setRuntimeStatus(makeStatus(copy.runtime.requestFailed, message, 'redBright'));
3269
3489
  setInputStage('idle');
3270
3490
  updateMessageMeta(activeUserMessageIdRef.current, {
3271
3491
  loading: false,
@@ -3281,7 +3501,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3281
3501
  }));
3282
3502
  setMessages((prev) => [
3283
3503
  ...prev,
3284
- { id: nextId(), label: 'error', text: err.message, color: 'redBright' }
3504
+ { id: nextId(), label: 'error', text: message, color: 'redBright' }
3285
3505
  ]);
3286
3506
  })
3287
3507
  .finally(() => {
@@ -3343,6 +3563,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3343
3563
  appendResultMessage(result);
3344
3564
  })
3345
3565
  .catch((err) => {
3566
+ const message = sanitizeRenderableText(err?.message || String(err));
3346
3567
  updateMessageMeta(userMessageId, {
3347
3568
  loading: false,
3348
3569
  phase: undefined,
@@ -3350,7 +3571,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3350
3571
  });
3351
3572
  setMessages((prev) => [
3352
3573
  ...prev,
3353
- { id: nextId(), label: 'error', text: err.message, color: 'redBright' }
3574
+ { id: nextId(), label: 'error', text: message, color: 'redBright' }
3354
3575
  ]);
3355
3576
  });
3356
3577
  };
@@ -3677,7 +3898,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3677
3898
  return h(
3678
3899
  Box,
3679
3900
  { flexDirection: 'column' },
3680
- h(Header, { sessionId: displaySessionId, model: displayModel, shellName, safeMode }),
3901
+ h(Header, { sessionId: displaySessionId, model: displayModel, sdkProvider: displaySdkProvider, shellName, safeMode }),
3681
3902
  h(MessageList, {
3682
3903
  messages: visibleMessages,
3683
3904
  loaderTick,