codemini-cli 0.1.18 → 0.2.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.
@@ -1,6 +1,7 @@
1
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { shouldCaptureEscapeSequence } from './input-escape.js';
4
+ import { classifyCommandIntent } from '../core/shell.js';
4
5
 
5
6
  const h = React.createElement;
6
7
  const SUGGESTION_PAGE_SIZE = 8;
@@ -63,6 +64,8 @@ const TUI_COPY = {
63
64
  ready: '就绪',
64
65
  noMessagesYet: '还没有消息',
65
66
  code: '代码',
67
+ codeActivity: '代码活动',
68
+ textNotes: '说明文本',
66
69
  live: '运行中',
67
70
  idle: '空闲',
68
71
  plan: '计划',
@@ -81,7 +84,7 @@ const TUI_COPY = {
81
84
  pendingQueue: '等待队列',
82
85
  commandPaletteGroupedSelect: '命令面板 | 分组选择模式',
83
86
  commandPaletteGroupedSuggestions: '命令面板 | 分组候选',
84
- startupHint: '使用 /help、/commands、/exit、!<shell>。Tab 可自动补全 slash 命令。',
87
+ startupHint: '使用 /help、/commands、/compact、/exit、!<shell>。Tab 可自动补全 slash 命令。',
85
88
  toolSummaryExpanded: '工具摘要:已展开',
86
89
  toolSummaryCollapsed: '工具摘要:已收起',
87
90
  toggleToolSummary: 'Ctrl+T 切换',
@@ -103,8 +106,12 @@ const TUI_COPY = {
103
106
  blocked: '工具被拦截',
104
107
  doneRead: '已读取文件',
105
108
  doingRead: '正在读取文件',
109
+ doneEdit: '已编辑文件',
110
+ doingEdit: '正在编辑文件',
106
111
  doneWrite: '已写入文件',
107
112
  doingWrite: '正在写入文件',
113
+ donePatch: '已应用补丁',
114
+ doingPatch: '正在应用补丁',
108
115
  doneList: '已查看目录',
109
116
  doingList: '正在查看目录',
110
117
  doneCommand: '已执行命令',
@@ -115,6 +122,22 @@ const TUI_COPY = {
115
122
  doingUpdateTask: '正在更新任务',
116
123
  doneGeneric: '已完成工具',
117
124
  doingGeneric: '正在执行工具',
125
+ doneInstall: '已安装依赖',
126
+ doingInstall: '正在安装依赖',
127
+ doneBuild: '已完成构建',
128
+ doingBuild: '正在构建',
129
+ doneTest: '已完成测试',
130
+ doingTest: '正在运行测试',
131
+ doneFrontend: '已启动前端服务',
132
+ doingFrontend: '正在启动前端服务',
133
+ doneBackend: '已启动后端服务',
134
+ doingBackend: '正在启动后端服务',
135
+ doneDatabase: '已启动数据库服务',
136
+ doingDatabase: '正在启动数据库服务',
137
+ doneDocker: '已完成 Docker 命令',
138
+ doingDocker: '正在执行 Docker 命令',
139
+ doneCodeGeneration: '已生成代码',
140
+ doingCodeGeneration: '正在生成代码',
118
141
  doneSkill: '已完成技能',
119
142
  doingSkill: '正在执行技能',
120
143
  toolFailed: (name) => `工具执行失败: ${name}`,
@@ -137,6 +160,7 @@ const TUI_COPY = {
137
160
  modelThinking: '模型正在思考',
138
161
  requestDelivered: '请求已送达,等待首个 token',
139
162
  generatingReply: '正在生成回复',
163
+ generatingCode: '正在生成代码中',
140
164
  streamingReply: '回复正在流式输出',
141
165
  replyCompleted: '回复已完成',
142
166
  outputFinished: '本轮输出结束',
@@ -147,6 +171,7 @@ const TUI_COPY = {
147
171
  skillRunning: '技能执行中',
148
172
  skillCompleted: '技能已完成',
149
173
  skillFailed: '技能执行失败',
174
+ autoSkillInjected: (names) => `自动启用技能: ${names.map((name) => `/${name}`).join(', ')}`,
150
175
  compactingContext: '正在压缩上下文',
151
176
  autoCompactTriggered: (mode, threshold) => `自动压缩已触发(${mode},阈值 ${threshold}%)`,
152
177
  requestFailed: '请求失败',
@@ -165,6 +190,8 @@ const TUI_COPY = {
165
190
  ready: 'ready',
166
191
  noMessagesYet: 'No messages yet',
167
192
  code: 'code',
193
+ codeActivity: 'CODE ACTIVITY',
194
+ textNotes: 'NOTES',
168
195
  live: 'LIVE',
169
196
  idle: 'IDLE',
170
197
  plan: 'PLAN',
@@ -183,7 +210,7 @@ const TUI_COPY = {
183
210
  pendingQueue: 'pending queue',
184
211
  commandPaletteGroupedSelect: 'command palette | grouped select mode',
185
212
  commandPaletteGroupedSuggestions: 'command palette | grouped suggestions',
186
- startupHint: 'Use /help, /commands, /exit, !<shell>. Tab for slash autocomplete.',
213
+ startupHint: 'Use /help, /commands, /compact, /exit, !<shell>. Tab for slash autocomplete.',
187
214
  toolSummaryExpanded: 'Tool summary: expanded',
188
215
  toolSummaryCollapsed: 'Tool summary: collapsed',
189
216
  toggleToolSummary: 'Ctrl+T to toggle',
@@ -205,8 +232,12 @@ const TUI_COPY = {
205
232
  blocked: 'Tool blocked',
206
233
  doneRead: 'Read file',
207
234
  doingRead: 'Reading file',
235
+ doneEdit: 'Edited file',
236
+ doingEdit: 'Editing file',
208
237
  doneWrite: 'Wrote file',
209
238
  doingWrite: 'Writing file',
239
+ donePatch: 'Applied patch',
240
+ doingPatch: 'Applying patch',
210
241
  doneList: 'Listed directory',
211
242
  doingList: 'Listing directory',
212
243
  doneCommand: 'Ran command',
@@ -217,6 +248,22 @@ const TUI_COPY = {
217
248
  doingUpdateTask: 'Updating task',
218
249
  doneGeneric: 'Completed tool',
219
250
  doingGeneric: 'Running tool',
251
+ doneInstall: 'Dependencies installed',
252
+ doingInstall: 'Installing dependencies',
253
+ doneBuild: 'Build completed',
254
+ doingBuild: 'Building',
255
+ doneTest: 'Tests completed',
256
+ doingTest: 'Running tests',
257
+ doneFrontend: 'Frontend started',
258
+ doingFrontend: 'Starting frontend service',
259
+ doneBackend: 'Backend started',
260
+ doingBackend: 'Starting backend service',
261
+ doneDatabase: 'Database started',
262
+ doingDatabase: 'Starting database service',
263
+ doneDocker: 'Docker command completed',
264
+ doingDocker: 'Running Docker command',
265
+ doneCodeGeneration: 'Code generated',
266
+ doingCodeGeneration: 'Generating code',
220
267
  doneSkill: 'Completed skill',
221
268
  doingSkill: 'Running skill',
222
269
  toolFailed: (name) => `Tool failed: ${name}`,
@@ -239,6 +286,7 @@ const TUI_COPY = {
239
286
  modelThinking: 'model is thinking',
240
287
  requestDelivered: 'request sent, waiting for first token',
241
288
  generatingReply: 'generating reply',
289
+ generatingCode: 'generating code',
242
290
  streamingReply: 'reply is streaming',
243
291
  replyCompleted: 'reply completed',
244
292
  outputFinished: 'turn output finished',
@@ -249,6 +297,7 @@ const TUI_COPY = {
249
297
  skillRunning: 'skill running',
250
298
  skillCompleted: 'skill completed',
251
299
  skillFailed: 'skill failed',
300
+ autoSkillInjected: (names) => `auto-enabled skills: ${names.map((name) => `/${name}`).join(', ')}`,
252
301
  compactingContext: 'compacting context',
253
302
  autoCompactTriggered: (mode, threshold) => `auto-compact triggered (${mode}, threshold ${threshold}%)`,
254
303
  requestFailed: 'request failed',
@@ -294,6 +343,14 @@ function trimText(value, maxLen = 88) {
294
343
  return `${text.slice(0, maxLen - 3)}...`;
295
344
  }
296
345
 
346
+ function safeJsonParse(raw) {
347
+ try {
348
+ return JSON.parse(String(raw || '{}'));
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+
297
354
  function parseToolDisplayName(name) {
298
355
  const raw = String(name || '').trim();
299
356
  const match = raw.match(/^([^(]+)\((.*)\)$/);
@@ -304,18 +361,85 @@ function parseToolDisplayName(name) {
304
361
  };
305
362
  }
306
363
 
364
+ function isCodeGenerationActivityName(name) {
365
+ return String(name || '').trim() === 'Code generation';
366
+ }
367
+
368
+ function formatDurationMs(ms) {
369
+ const safeMs = Math.max(0, Number(ms) || 0);
370
+ return `${(safeMs / 1000).toFixed(1)}s`;
371
+ }
372
+
373
+ function getIntentLabel(kind) {
374
+ switch (kind) {
375
+ case 'install':
376
+ return 'Install';
377
+ case 'build':
378
+ return 'Build';
379
+ case 'test':
380
+ return 'Test';
381
+ case 'frontend-service':
382
+ return 'Frontend';
383
+ case 'backend-service':
384
+ return 'Backend';
385
+ case 'database-service':
386
+ return 'Database';
387
+ case 'docker-service':
388
+ return 'Docker';
389
+ case 'service':
390
+ return 'Service';
391
+ default:
392
+ return 'Run';
393
+ }
394
+ }
395
+
396
+ export function formatActivityDurationText(row, nowMs = Date.now()) {
397
+ if (!row) return '';
398
+ if (row.status === 'running' && Number.isFinite(Number(row.startedAt))) {
399
+ const startedAt = Number(row.startedAt);
400
+ const endedAt = Number(row.endedAt);
401
+ const elapsed = Number.isFinite(endedAt) && endedAt > startedAt ? endedAt - startedAt : Math.max(0, Number(nowMs) - startedAt);
402
+ return formatDurationMs(elapsed);
403
+ }
404
+ if (typeof row.durationText === 'string' && row.durationText.trim()) {
405
+ return row.durationText.trim();
406
+ }
407
+ if (Number.isFinite(Number(row.durationMs))) {
408
+ return formatDurationMs(Number(row.durationMs));
409
+ }
410
+ return '';
411
+ }
412
+
307
413
  function getActivityDisplayParts(activity) {
414
+ if (isCodeGenerationActivityName(activity?.name)) {
415
+ return {
416
+ primary: 'Code',
417
+ secondary: ' (generation)'
418
+ };
419
+ }
420
+ const parsed = parseToolDisplayName(activity?.name);
421
+ if (parsed.base === 'run' || parsed.base === 'start_service') {
422
+ const intent = classifyCommandIntent(parsed.target);
423
+ return {
424
+ primary: getIntentLabel(intent.kind),
425
+ secondary: parsed.target ? `(${parsed.target})` : ''
426
+ };
427
+ }
308
428
  if ((activity?.type || 'tool') === 'skill') {
309
429
  return {
310
430
  primary: `Skill`,
311
431
  secondary: `(${activity?.name || 'unknown'})`
312
432
  };
313
433
  }
314
- const parsed = parseToolDisplayName(activity?.name);
315
434
  const labels = {
316
- read_file: 'Read',
317
- write_file: 'Write',
318
- run_command: 'Command',
435
+ read: 'Read',
436
+ edit: 'Edit',
437
+ write: 'Write',
438
+ patch: 'Patch',
439
+ run: 'Run',
440
+ grep: 'Grep',
441
+ glob: 'Glob',
442
+ list: 'List',
319
443
  start_service: 'Service',
320
444
  list_services: 'Service',
321
445
  get_service_status: 'Service',
@@ -332,35 +456,117 @@ function getActivityDisplayParts(activity) {
332
456
  }
333
457
 
334
458
  function describeToolActivity(name, copy, { done = false, blocked = false } = {}) {
459
+ const parsed = parseToolDisplayName(name);
460
+ if (parsed.base === 'run' || parsed.base === 'start_service') {
461
+ const intent = classifyCommandIntent(parsed.target);
462
+ const target = parsed.target || intent.kind || 'command';
463
+ if (intent.kind === 'install') {
464
+ return blocked
465
+ ? `${copy.toolActivity.blocked}: ${target}`
466
+ : done
467
+ ? `${copy.toolActivity.doneInstall}: ${target}`
468
+ : `${copy.toolActivity.doingInstall}: ${target}`;
469
+ }
470
+ if (intent.kind === 'build') {
471
+ return blocked
472
+ ? `${copy.toolActivity.blocked}: ${target}`
473
+ : done
474
+ ? `${copy.toolActivity.doneBuild}: ${target}`
475
+ : `${copy.toolActivity.doingBuild}: ${target}`;
476
+ }
477
+ if (intent.kind === 'test') {
478
+ return blocked
479
+ ? `${copy.toolActivity.blocked}: ${target}`
480
+ : done
481
+ ? `${copy.toolActivity.doneTest}: ${target}`
482
+ : `${copy.toolActivity.doingTest}: ${target}`;
483
+ }
484
+ if (intent.kind === 'frontend-service') {
485
+ return blocked
486
+ ? `${copy.toolActivity.blocked}: ${target}`
487
+ : done
488
+ ? `${copy.toolActivity.doneFrontend}: ${target}`
489
+ : `${copy.toolActivity.doingFrontend}: ${target}`;
490
+ }
491
+ if (intent.kind === 'backend-service') {
492
+ return blocked
493
+ ? `${copy.toolActivity.blocked}: ${target}`
494
+ : done
495
+ ? `${copy.toolActivity.doneBackend}: ${target}`
496
+ : `${copy.toolActivity.doingBackend}: ${target}`;
497
+ }
498
+ if (intent.kind === 'database-service') {
499
+ return blocked
500
+ ? `${copy.toolActivity.blocked}: ${target}`
501
+ : done
502
+ ? `${copy.toolActivity.doneDatabase}: ${target}`
503
+ : `${copy.toolActivity.doingDatabase}: ${target}`;
504
+ }
505
+ if (intent.kind === 'docker-service') {
506
+ return blocked
507
+ ? `${copy.toolActivity.blocked}: ${target}`
508
+ : done
509
+ ? `${copy.toolActivity.doneDocker}: ${target}`
510
+ : `${copy.toolActivity.doingDocker}: ${target}`;
511
+ }
512
+ if (intent.kind === 'service') {
513
+ return blocked
514
+ ? `${copy.toolActivity.blocked}: ${target}`
515
+ : done
516
+ ? `${copy.toolActivity.doneGeneric}: ${target}`
517
+ : `${copy.toolActivity.doingGeneric}: ${target}`;
518
+ }
519
+ }
520
+ if (isCodeGenerationActivityName(name)) {
521
+ return blocked
522
+ ? `${copy.toolActivity.blocked}: code generation`
523
+ : done
524
+ ? copy.toolActivity.doneCodeGeneration
525
+ : copy.toolActivity.doingCodeGeneration;
526
+ }
335
527
  const { raw, base, target } = parseToolDisplayName(name);
336
528
  const safeTarget = trimText(target, 72);
337
- if (base === 'read_file') {
529
+ if (base === 'read') {
338
530
  return blocked
339
- ? `${copy.toolActivity.blocked}: read_file(${safeTarget || '.'})`
531
+ ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
340
532
  : done
341
533
  ? `${copy.toolActivity.doneRead}: ${safeTarget || '.'}`
342
534
  : `${copy.toolActivity.doingRead}: ${safeTarget || '.'}`;
343
535
  }
344
- if (base === 'write_file') {
536
+ if (base === 'edit') {
537
+ return blocked
538
+ ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
539
+ : done
540
+ ? `${copy.toolActivity.doneEdit}: ${safeTarget || '.'}`
541
+ : `${copy.toolActivity.doingEdit}: ${safeTarget || '.'}`;
542
+ }
543
+ if (base === 'write') {
345
544
  return blocked
346
- ? `${copy.toolActivity.blocked}: write_file(${safeTarget || '.'})`
545
+ ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
347
546
  : done
348
547
  ? `${copy.toolActivity.doneWrite}: ${safeTarget || '.'}`
349
548
  : `${copy.toolActivity.doingWrite}: ${safeTarget || '.'}`;
350
549
  }
351
- if (base === 'list_files') {
550
+ if (base === 'patch') {
551
+ return blocked
552
+ ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
553
+ : done
554
+ ? `${copy.toolActivity.donePatch}: ${safeTarget || '.'}`
555
+ : `${copy.toolActivity.doingPatch}: ${safeTarget || '.'}`;
556
+ }
557
+ if (base === 'list' || base === 'glob' || base === 'grep') {
352
558
  return blocked
353
- ? `${copy.toolActivity.blocked}: list_files(${safeTarget || '.'})`
559
+ ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
354
560
  : done
355
561
  ? `${copy.toolActivity.doneList}: ${safeTarget || '.'}`
356
562
  : `${copy.toolActivity.doingList}: ${safeTarget || '.'}`;
357
563
  }
358
- if (base === 'run_command') {
564
+ if (base === 'run') {
359
565
  return blocked
360
- ? `${copy.toolActivity.blocked}: ${safeTarget || 'run_command'}`
566
+ ? `${copy.toolActivity.blocked}: ${safeTarget || base}`
361
567
  : done
362
- ? `${copy.toolActivity.doneCommand}: ${safeTarget || 'run_command'}`
363
- : `${copy.toolActivity.doingCommand}: ${safeTarget || 'run_command'}`;
568
+ ? `${copy.toolActivity.doneCommand}: ${safeTarget || base}`
569
+ : `${copy.toolActivity.doingCommand}: ${safeTarget || base}`;
364
570
  }
365
571
  if (base === 'start_service' || base === 'list_services' || base === 'get_service_status' || base === 'get_service_logs' || base === 'stop_service') {
366
572
  return blocked
@@ -384,6 +590,21 @@ function describeSkillActivity(name, copy, { done = false, failed = false } = {}
384
590
  return `${copy.toolActivity.doingSkill}: /${name}`;
385
591
  }
386
592
 
593
+ function describeAutoSkillActivity(names, copy) {
594
+ const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
595
+ if (safeNames.length === 0) return '';
596
+ return copy.runtime.autoSkillInjected(safeNames);
597
+ }
598
+
599
+ function formatAutoSkillBadge(names, copy) {
600
+ const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
601
+ if (safeNames.length === 0) return '';
602
+ const [first, ...rest] = safeNames;
603
+ const suffix = rest.length > 0 ? ` +${rest.length}` : '';
604
+ const prefix = copy?.roleLabels?.system === 'SYSTEM' ? 'AUTO' : '自动';
605
+ return `${prefix} /${first}${suffix}`;
606
+ }
607
+
387
608
  function normalizeRuntimeStatus(status, copy) {
388
609
  if (status && typeof status === 'object') {
389
610
  return {
@@ -451,6 +672,52 @@ function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
451
672
  );
452
673
  }
453
674
 
675
+ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false }) {
676
+ const maxContextTokens = Number(runtimeState?.maxContextTokens || 0);
677
+ const currentContextTokens = Number(runtimeState?.currentContextTokens || 0);
678
+ const pctRaw =
679
+ Number.isFinite(runtimeState?.contextUsagePct) && runtimeState.contextUsagePct >= 0
680
+ ? runtimeState.contextUsagePct
681
+ : maxContextTokens > 0
682
+ ? (currentContextTokens / maxContextTokens) * 100
683
+ : 0;
684
+ const pct = Math.min(100, Math.max(0, pctRaw));
685
+ const filled = Math.min(12, Math.max(0, Math.round((pct / 100) * 12)));
686
+ const activeColor = pct < 40 ? 'greenBright' : pct < 75 ? 'yellowBright' : 'redBright';
687
+ const statusColor = runtimeStatus?.color || activeColor;
688
+ const chunks = Array.from({ length: 12 }, (_, idx) => {
689
+ const zoneColor = idx < 5 ? 'greenBright' : idx < 9 ? 'yellowBright' : 'redBright';
690
+ const color = idx < filled ? zoneColor : 'gray';
691
+ return h(Text, { key: `context-meter-${idx}`, color }, '|');
692
+ });
693
+
694
+ if (compact) {
695
+ return h(
696
+ Box,
697
+ { justifyContent: 'flex-end', alignItems: 'center' },
698
+ h(Text, { color: 'gray' }, '上下文 '),
699
+ h(Text, { color: statusColor }, `${Math.round(pct)}% `),
700
+ h(
701
+ Box,
702
+ { flexDirection: 'row' },
703
+ ...chunks
704
+ )
705
+ );
706
+ }
707
+
708
+ return h(
709
+ Box,
710
+ { justifyContent: 'flex-end', alignItems: 'center' },
711
+ h(Text, { color: 'gray' }, '上下文 '),
712
+ h(Text, { color: statusColor }, `${Math.round(pct)}% `),
713
+ h(
714
+ Box,
715
+ null,
716
+ ...chunks
717
+ )
718
+ );
719
+ }
720
+
454
721
  function PlanStrip({ planState, copy }) {
455
722
  if (!planState || !planState.total) return null;
456
723
  const progress = `${planState.current}/${planState.total}`;
@@ -611,6 +878,188 @@ export function parsePlanProgressLine(text) {
611
878
  };
612
879
  }
613
880
 
881
+ function getTailPreviewLines(text, maxLines = 3) {
882
+ const source = String(text || '');
883
+ if (!source.trim()) return [];
884
+
885
+ const lines = source.split('\n').map((line) => line.replace(/\r$/, ''));
886
+ let insideFence = false;
887
+ let fenceLines = [];
888
+ let latestClosedFenceLines = [];
889
+
890
+ for (const line of lines) {
891
+ const trimmed = line.trim();
892
+ if (trimmed.startsWith('```')) {
893
+ if (insideFence) {
894
+ latestClosedFenceLines = fenceLines.slice();
895
+ insideFence = false;
896
+ fenceLines = [];
897
+ continue;
898
+ }
899
+ insideFence = true;
900
+ fenceLines = [];
901
+ continue;
902
+ }
903
+ if (insideFence) {
904
+ fenceLines.push(line);
905
+ }
906
+ }
907
+
908
+ if (insideFence) {
909
+ const codeLines = fenceLines.filter((line) => line.trim().length > 0);
910
+ if (codeLines.length > 0) {
911
+ return codeLines.slice(-Math.max(1, maxLines));
912
+ }
913
+ }
914
+
915
+ const closedFenceLines = latestClosedFenceLines.filter((line) => line.trim().length > 0);
916
+ if (closedFenceLines.length > 0) {
917
+ return closedFenceLines.slice(-Math.max(1, maxLines));
918
+ }
919
+
920
+ const tailLines = source
921
+ .split('\n')
922
+ .map((line) => line.replace(/\r$/, ''))
923
+ .filter((line) => line.trim().length > 0);
924
+ if (tailLines.length === 0) return [];
925
+ return tailLines.slice(-Math.max(1, maxLines));
926
+ }
927
+
928
+ function collectPreviewStrings(value, out = []) {
929
+ if (out.length >= 3 || value == null) return out;
930
+ if (typeof value === 'string') {
931
+ if (value.trim()) out.push(value);
932
+ return out;
933
+ }
934
+ if (Array.isArray(value)) {
935
+ for (const item of value) {
936
+ collectPreviewStrings(item, out);
937
+ if (out.length >= 3) break;
938
+ }
939
+ return out;
940
+ }
941
+ if (typeof value !== 'object') return out;
942
+
943
+ const priorityKeys = ['content', 'new_content', 'new_text', 'patch', 'text', 'code', 'body', 'script', 'source', 'value'];
944
+ if (value.edit && typeof value.edit === 'object') {
945
+ collectPreviewStrings(value.edit, out);
946
+ }
947
+ for (const key of priorityKeys) {
948
+ if (out.length >= 3) break;
949
+ collectPreviewStrings(value[key], out);
950
+ }
951
+ return out;
952
+ }
953
+
954
+ function extractPreviewTextFromRawArguments(raw) {
955
+ const source = String(raw || '');
956
+ if (!source.trim()) return '';
957
+
958
+ const contentMatch = source.match(/"(content|new_content|new_text|patch|code|body|script|source|value)"\s*:\s*"([\s\S]*)$/);
959
+ if (!contentMatch) return '';
960
+
961
+ return contentMatch[2]
962
+ .replace(/\\n/g, '\n')
963
+ .replace(/\\"/g, '"')
964
+ .replace(/\\\\/g, '\\')
965
+ .replace(/",?\s*$/g, '')
966
+ .trim();
967
+ }
968
+
969
+ function compactPreviewLine(line, maxChars = 56) {
970
+ const text = String(line || '').replace(/\t/g, ' ').trimEnd();
971
+ if (!text) return '';
972
+ if (text.length <= maxChars) return text;
973
+ return `${text.slice(0, Math.max(1, maxChars - 3))}...`;
974
+ }
975
+
976
+ function getLatestToolPreviewLines(msg, maxLines = 3) {
977
+ const toolCalls = [
978
+ ...(Array.isArray(msg?.pendingToolCalls) ? msg.pendingToolCalls : []),
979
+ ...(Array.isArray(msg?.toolCalls) ? msg.toolCalls : [])
980
+ ];
981
+ const codeTools = new Set(['edit', 'write', 'patch', 'generate_diff']);
982
+ for (let index = toolCalls.length - 1; index >= 0; index -= 1) {
983
+ const tool = toolCalls[index];
984
+ const parsed = parseToolDisplayName(tool?.name);
985
+ if (!codeTools.has(parsed.base)) continue;
986
+ const rawArgumentPreview =
987
+ typeof tool?.arguments === 'string' ? extractPreviewTextFromRawArguments(tool.arguments) : '';
988
+ const previewSource = rawArgumentPreview
989
+ ? [rawArgumentPreview]
990
+ : collectPreviewStrings(tool?.arguments || tool?.content || tool?.summary || []);
991
+ if (previewSource.length === 0) continue;
992
+ const combined = previewSource.join('\n');
993
+ const previewLines = getTailPreviewLines(combined, maxLines);
994
+ if (previewLines.length > 0) return previewLines.map((line) => compactPreviewLine(line));
995
+ }
996
+ return [];
997
+ }
998
+
999
+ export function getGeneratingCodePlaceholderRows(msg, copy, contentWidth = 72) {
1000
+ const liveStatus = String(msg?.liveStatus || '').trim();
1001
+ if (!msg?.loading || (msg?.phase !== 'generating' && msg?.phase !== 'tooling')) return [];
1002
+ if (liveStatus !== String(copy?.runtime?.generatingCode || '').trim()) return [];
1003
+
1004
+ const previewLines = getLatestToolPreviewLines(msg, 3);
1005
+ if (previewLines.length === 0) return [];
1006
+
1007
+ return previewLines.map((line, idx) => ({
1008
+ kind: 'code-placeholder',
1009
+ lineNo: idx + 1,
1010
+ text: line,
1011
+ color: 'gray'
1012
+ }));
1013
+ }
1014
+
1015
+ export function getCodeGenerationActivityRows(msg) {
1016
+ const startedAt = Number(msg?.codeGenerationStartedAt);
1017
+ const endedAt = Number(msg?.codeGenerationEndedAt);
1018
+ if (!startedAt || !msg?.loading || endedAt > 0) return [];
1019
+
1020
+ const status = 'running';
1021
+ const durationMs = Math.max(0, Date.now() - startedAt);
1022
+
1023
+ return [
1024
+ {
1025
+ kind: 'activity',
1026
+ activityType: 'tool',
1027
+ name: 'Code generation',
1028
+ status,
1029
+ statusIcon: status === 'done' ? '✓' : '…',
1030
+ statusColor: status === 'done' ? 'greenBright' : 'yellow',
1031
+ durationMs,
1032
+ durationText: formatDurationMs(durationMs),
1033
+ isLatestTool: true,
1034
+ synthetic: true
1035
+ }
1036
+ ];
1037
+ }
1038
+
1039
+ export function ensureCodeGenerationTiming(msg, now = Date.now()) {
1040
+ if (!msg || msg.codeGenerationStartedAt) return msg;
1041
+ return {
1042
+ ...msg,
1043
+ codeGenerationStartedAt: now,
1044
+ codeGenerationEndedAt: undefined
1045
+ };
1046
+ }
1047
+
1048
+ export function shouldAppendAssistantResult(result, activeAssistantId, streamedAssistantHandled = false) {
1049
+ if (result?.type !== 'assistant') return true;
1050
+ if (streamedAssistantHandled) return false;
1051
+ return !activeAssistantId;
1052
+ }
1053
+
1054
+ function finishCodeGeneration(msg, now = Date.now()) {
1055
+ if (!msg?.codeGenerationStartedAt || msg?.codeGenerationEndedAt) return msg;
1056
+ return {
1057
+ ...msg,
1058
+ codeGenerationEndedAt: now,
1059
+ pendingToolCalls: []
1060
+ };
1061
+ }
1062
+
614
1063
  export function injectPlanStateMessage(messages, planState, activeUserMessageId, activeAssistantId) {
615
1064
  const source = Array.isArray(messages) ? messages : [];
616
1065
  if (!planState || !planState.total) return source;
@@ -632,6 +1081,21 @@ export function injectPlanStateMessage(messages, planState, activeUserMessageId,
632
1081
  return [...withNoPlanStrip, synthetic];
633
1082
  }
634
1083
 
1084
+ export function injectRuntimeStateMessage(messages, runtimeState, runtimeStatus, busy, activeUserMessageId, activeAssistantId) {
1085
+ const source = Array.isArray(messages) ? messages : [];
1086
+ if (!runtimeState) return source;
1087
+ const withoutRuntimeStrip = source.filter((message) => !message?.runtimeStrip);
1088
+ const userIdx = withoutRuntimeStrip.findIndex((message) => message.id === activeUserMessageId);
1089
+ if (userIdx !== -1) {
1090
+ return withoutRuntimeStrip;
1091
+ }
1092
+ const assistantIdx = withoutRuntimeStrip.findIndex((message) => message.id === activeAssistantId);
1093
+ if (assistantIdx !== -1) {
1094
+ return withoutRuntimeStrip;
1095
+ }
1096
+ return withoutRuntimeStrip;
1097
+ }
1098
+
635
1099
  function PlanSummaryBubble({ msg, copy }) {
636
1100
  const theme = roleStyle(msg.label);
637
1101
  const summary = msg.planSummary || parseAutoPlanSummaryMessage(msg.text);
@@ -817,6 +1281,56 @@ function isBlankTextRow(row) {
817
1281
  return row?.kind === 'text' && String(row?.text || '').trim() === '';
818
1282
  }
819
1283
 
1284
+ function isCodeActivityName(name) {
1285
+ const parsed = parseToolDisplayName(name);
1286
+ return new Set([
1287
+ 'edit',
1288
+ 'write',
1289
+ 'write_file',
1290
+ 'patch',
1291
+ 'replace_text',
1292
+ 'replace_block',
1293
+ 'insert_before',
1294
+ 'insert_after',
1295
+ 'validate_edit',
1296
+ 'generate_diff'
1297
+ ]).has(parsed.base);
1298
+ }
1299
+
1300
+ export function isCodeLikeRow(row) {
1301
+ if (!row) return false;
1302
+ if (row.kind === 'code' || row.kind === 'activity' || row.kind === 'activity-summary') return true;
1303
+ if (row.kind === 'status') return true;
1304
+ return false;
1305
+ }
1306
+
1307
+ export function splitMessageRows(rows) {
1308
+ const textRows = [];
1309
+ const codeRows = [];
1310
+ for (const row of Array.isArray(rows) ? rows : []) {
1311
+ if (isCodeLikeRow(row)) codeRows.push(row);
1312
+ else textRows.push(row);
1313
+ }
1314
+ return { textRows, codeRows };
1315
+ }
1316
+
1317
+ export function insertRowsAfterLastCodeRow(rows, extraRows) {
1318
+ const source = Array.isArray(rows) ? rows : [];
1319
+ const inserts = Array.isArray(extraRows) ? extraRows.filter(Boolean) : [];
1320
+ if (inserts.length === 0) return source.slice();
1321
+
1322
+ let insertIndex = -1;
1323
+ for (let index = source.length - 1; index >= 0; index -= 1) {
1324
+ if (source[index]?.kind === 'code') {
1325
+ insertIndex = index + 1;
1326
+ break;
1327
+ }
1328
+ }
1329
+
1330
+ if (insertIndex === -1) return [...source, ...inserts];
1331
+ return [...source.slice(0, insertIndex), ...inserts, ...source.slice(insertIndex)];
1332
+ }
1333
+
820
1334
  export function normalizeActivitySpacingRows(inputRows) {
821
1335
  const rows = Array.isArray(inputRows) ? inputRows : [];
822
1336
  const normalized = [];
@@ -863,7 +1377,7 @@ export function normalizeActivitySpacingRows(inputRows) {
863
1377
 
864
1378
  function isReadActivityName(name) {
865
1379
  const parsed = parseToolDisplayName(name);
866
- return parsed.base === 'read_file' || parsed.base === 'Read';
1380
+ return parsed.base === 'read' || parsed.base === 'Read';
867
1381
  }
868
1382
 
869
1383
  function isIgnorableSegmentAfterRead(item, activityType, activityName) {
@@ -917,7 +1431,7 @@ export function mergeActivitySummary(previousSummary, nextSummary, activityName)
917
1431
  return lines.join('\n');
918
1432
  }
919
1433
 
920
- function buildMessageRows(msg, showToolDetails, contentWidth = 72) {
1434
+ function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
921
1435
  const rows = [];
922
1436
  const pushTextRows = (text) => {
923
1437
  const lines = String(text || '').split('\n');
@@ -1000,18 +1514,130 @@ function buildMessageRows(msg, showToolDetails, contentWidth = 72) {
1000
1514
  toolCalls.forEach((tool, idx) => pushActivityRows(tool, idx, toolCalls.length));
1001
1515
  }
1002
1516
 
1517
+ const codeGenerationRows = getCodeGenerationActivityRows(msg);
1518
+ const generatingCodeRows = getGeneratingCodePlaceholderRows(msg, copy, contentWidth);
1519
+ const syntheticRows = [...codeGenerationRows, ...generatingCodeRows];
1003
1520
  if (msg?.loading && (msg?.liveStatus || msg?.phase)) {
1521
+ const statusRows = [];
1004
1522
  pushWrappedRow(
1005
- rows,
1523
+ statusRows,
1006
1524
  {
1007
1525
  kind: 'status',
1008
1526
  text: trimText(msg.liveStatus || msg.phase, 144)
1009
1527
  },
1010
1528
  Math.max(8, contentWidth - 2)
1011
1529
  );
1530
+ syntheticRows.push(...statusRows);
1012
1531
  }
1013
1532
 
1014
- return normalizeActivitySpacingRows(rows);
1533
+ return normalizeActivitySpacingRows(insertRowsAfterLastCodeRow(rows, syntheticRows));
1534
+ }
1535
+
1536
+ function renderMessageRow(msg, row, idx, loaderTick) {
1537
+ if (row.kind === 'activity') {
1538
+ const activity = { type: row.activityType, name: row.name, status: row.status };
1539
+ const display = getActivityDisplayParts(activity);
1540
+ const dotColor =
1541
+ row.status === 'error' || row.status === 'blocked'
1542
+ ? 'redBright'
1543
+ : row.status === 'done'
1544
+ ? 'greenBright'
1545
+ : 'yellowBright';
1546
+ const textColor =
1547
+ activity.type === 'skill'
1548
+ ? row.status === 'error'
1549
+ ? 'redBright'
1550
+ : 'cyanBright'
1551
+ : row.status === 'error' || row.status === 'blocked'
1552
+ ? 'redBright'
1553
+ : 'cyanBright';
1554
+ const durationText = formatActivityDurationText(row);
1555
+ return h(
1556
+ Box,
1557
+ { key: `row-tool-${msg.id}-${idx}` },
1558
+ h(Text, { color: 'gray' }, ' '),
1559
+ h(Text, { color: dotColor }, '●'),
1560
+ h(Text, { color: 'gray' }, ' '),
1561
+ h(Text, { color: textColor }, display.primary),
1562
+ h(Text, { color: 'gray' }, display.secondary),
1563
+ durationText ? h(Text, { color: row.statusColor }, ` ${durationText}`) : null
1564
+ );
1565
+ }
1566
+ if (row.kind === 'activity-summary') {
1567
+ return h(
1568
+ Box,
1569
+ { key: `row-tool-summary-${msg.id}-${idx}`, marginLeft: 1 },
1570
+ h(Text, { color: 'gray' }, `└ ${row.text}`)
1571
+ );
1572
+ }
1573
+ if (row.kind === 'plan-progress') {
1574
+ return h(
1575
+ Box,
1576
+ { key: `row-plan-progress-${msg.id}-${idx}`, marginTop: 1, marginBottom: 1 },
1577
+ h(Text, { color: 'cyanBright' }, '[plan] '),
1578
+ h(Text, { color: 'yellowBright' }, `Step ${row.current}/${row.total}`),
1579
+ h(Text, { color: 'gray' }, ' -> '),
1580
+ h(Text, { color: 'magentaBright' }, String(row.role || 'agent').toUpperCase()),
1581
+ h(Text, { color: 'gray' }, ': '),
1582
+ h(Text, { color: 'white' }, row.title)
1583
+ );
1584
+ }
1585
+ if (row.kind === 'status') {
1586
+ const dots = '.'.repeat((loaderTick % 3) + 1);
1587
+ const phase = msg.phase;
1588
+ const color =
1589
+ phase === 'sending'
1590
+ ? 'yellowBright'
1591
+ : phase === 'queued'
1592
+ ? 'cyanBright'
1593
+ : phase === 'tooling'
1594
+ ? 'magentaBright'
1595
+ : phase === 'generating'
1596
+ ? 'greenBright'
1597
+ : 'cyanBright';
1598
+ return h(
1599
+ Box,
1600
+ { key: `row-status-${msg.id}-${idx}`, marginTop: 1 },
1601
+ h(Text, { color: 'gray' }, ' '),
1602
+ h(Text, { color }, `${row.text}${dots}`)
1603
+ );
1604
+ }
1605
+ if (row.kind === 'quote') {
1606
+ return h(
1607
+ Box,
1608
+ { key: `row-quote-${msg.id}-${idx}`, marginTop: 1, marginLeft: 1, paddingLeft: 1 },
1609
+ h(Text, { color: 'yellow' }, '▍ '),
1610
+ h(Text, { color: row.color }, ...renderInlineCode(row.text, row.color))
1611
+ );
1612
+ }
1613
+ if (row.kind === 'tree') {
1614
+ return h(
1615
+ Box,
1616
+ { key: `row-tree-${msg.id}-${idx}`, marginLeft: 1 },
1617
+ h(Text, { color: row.color }, row.text)
1618
+ );
1619
+ }
1620
+ if (row.kind === 'code') {
1621
+ return h(
1622
+ Box,
1623
+ { key: `row-code-${msg.id}-${idx}`, marginLeft: 1 },
1624
+ h(Text, { color: 'gray' }, row.text)
1625
+ );
1626
+ }
1627
+ if (row.kind === 'code-placeholder') {
1628
+ return h(
1629
+ Box,
1630
+ { key: `row-code-placeholder-${msg.id}-${idx}`, marginLeft: 1 },
1631
+ h(Text, { color: 'gray', dimColor: true }, String(row.lineNo || idx + 1).padStart(2, ' ')),
1632
+ h(Text, { color: 'gray' }, ' │ '),
1633
+ h(Text, { color: 'gray', dimColor: true }, row.text)
1634
+ );
1635
+ }
1636
+ return renderTextLine(msg, row.text, idx, row.color);
1637
+ }
1638
+
1639
+ function renderMessageRowsInOrder(msg, rows, loaderTick, copy) {
1640
+ return rows.map((row, idx) => renderMessageRow(msg, row, idx, loaderTick));
1015
1641
  }
1016
1642
 
1017
1643
 
@@ -1124,104 +1750,12 @@ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, con
1124
1750
  return h(PlanSummaryBubble, { msg, copy });
1125
1751
  }
1126
1752
  const theme = roleStyle(msg.label);
1127
- const allRows = buildMessageRows(msg, showToolDetails, contentWidth);
1753
+ const allRows = buildMessageRows(msg, showToolDetails, contentWidth, copy);
1128
1754
  const start = rowWindow ? Math.max(0, rowWindow.start || 0) : 0;
1129
1755
  const end = rowWindow ? Math.max(start, rowWindow.end || allRows.length) : allRows.length;
1130
1756
  const visibleRows = allRows.slice(start, end);
1131
- const rendered = visibleRows.map((row, idx) => {
1132
- if (row.kind === 'activity') {
1133
- const activity = { type: row.activityType, name: row.name, status: row.status };
1134
- const display = getActivityDisplayParts(activity);
1135
- const dotColor =
1136
- activity.type === 'skill'
1137
- ? row.status === 'error'
1138
- ? 'redBright'
1139
- : 'blueBright'
1140
- : row.status === 'error' || row.status === 'blocked'
1141
- ? 'redBright'
1142
- : 'greenBright';
1143
- const textColor =
1144
- activity.type === 'skill'
1145
- ? row.status === 'error'
1146
- ? 'redBright'
1147
- : 'cyanBright'
1148
- : row.status === 'error' || row.status === 'blocked'
1149
- ? 'redBright'
1150
- : 'greenBright';
1151
- return h(
1152
- Box,
1153
- { key: `row-tool-${msg.id}-${idx}` },
1154
- h(Text, { color: 'gray' }, ' '),
1155
- h(Text, { color: dotColor }, '●'),
1156
- h(Text, { color: 'gray' }, ' '),
1157
- h(Text, { color: textColor }, display.primary),
1158
- h(Text, { color: 'gray' }, display.secondary),
1159
- row.durationText ? h(Text, { color: row.statusColor }, ` ${row.durationText}`) : null
1160
- );
1161
- }
1162
- if (row.kind === 'activity-summary') {
1163
- return h(
1164
- Box,
1165
- { key: `row-tool-summary-${msg.id}-${idx}`, marginLeft: 2 },
1166
- h(Text, { color: 'gray' }, `└ ${row.text}`)
1167
- );
1168
- }
1169
- if (row.kind === 'plan-progress') {
1170
- return h(
1171
- Box,
1172
- { key: `row-plan-progress-${msg.id}-${idx}`, marginTop: 1, marginBottom: 1 },
1173
- h(Text, { color: 'cyanBright' }, '[plan] '),
1174
- h(Text, { color: 'yellowBright' }, `Step ${row.current}/${row.total}`),
1175
- h(Text, { color: 'gray' }, ' -> '),
1176
- h(Text, { color: 'magentaBright' }, String(row.role || 'agent').toUpperCase()),
1177
- h(Text, { color: 'gray' }, ': '),
1178
- h(Text, { color: 'white' }, row.title)
1179
- );
1180
- }
1181
- if (row.kind === 'status') {
1182
- const dots = '.'.repeat((loaderTick % 3) + 1);
1183
- const phase = msg.phase;
1184
- const color =
1185
- phase === 'sending'
1186
- ? 'yellowBright'
1187
- : phase === 'queued'
1188
- ? 'cyanBright'
1189
- : phase === 'tooling'
1190
- ? 'magentaBright'
1191
- : phase === 'generating'
1192
- ? 'greenBright'
1193
- : 'cyanBright';
1194
- return h(
1195
- Box,
1196
- { key: `row-status-${msg.id}-${idx}`, marginTop: 1 },
1197
- h(Text, { color: 'gray' }, ' '),
1198
- h(Text, { color }, `${row.text}${dots}`)
1199
- );
1200
- }
1201
- if (row.kind === 'quote') {
1202
- return h(
1203
- Box,
1204
- { key: `row-quote-${msg.id}-${idx}`, marginTop: 1, marginLeft: 1, paddingLeft: 1 },
1205
- h(Text, { color: 'yellow' }, '▍ '),
1206
- h(Text, { color: row.color }, ...renderInlineCode(row.text, row.color))
1207
- );
1208
- }
1209
- if (row.kind === 'tree') {
1210
- return h(
1211
- Box,
1212
- { key: `row-tree-${msg.id}-${idx}`, marginLeft: 1 },
1213
- h(Text, { color: row.color }, row.text)
1214
- );
1215
- }
1216
- if (row.kind === 'code') {
1217
- return h(
1218
- Box,
1219
- { key: `row-code-${msg.id}-${idx}`, marginLeft: 1 },
1220
- h(Text, { color: 'gray' }, row.text)
1221
- );
1222
- }
1223
- return renderTextLine(msg, row.text, idx, row.color);
1224
- });
1757
+ const rendered = renderMessageRowsInOrder(msg, visibleRows, loaderTick, copy);
1758
+ const autoSkillBadge = formatAutoSkillBadge(msg.autoSkillNames, copy);
1225
1759
 
1226
1760
  return h(
1227
1761
  Box,
@@ -1245,7 +1779,9 @@ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, con
1245
1779
  null,
1246
1780
  h(Text, { color: theme.badgeText, backgroundColor: theme.badgeBg }, ` ${messageLabel(msg.label, copy)} `)
1247
1781
  ),
1248
- h(Text, { color: theme.chrome }, ' ')
1782
+ autoSkillBadge
1783
+ ? h(Text, { color: 'blueBright' }, autoSkillBadge)
1784
+ : h(Text, { color: theme.chrome }, ' ')
1249
1785
  ),
1250
1786
  ...rendered
1251
1787
  )
@@ -1336,7 +1872,7 @@ function PendingPanel({ pendingQueue, copy }) {
1336
1872
  return h(
1337
1873
  Box,
1338
1874
  {
1339
- marginTop: 1,
1875
+ marginTop: 0,
1340
1876
  flexDirection: 'column',
1341
1877
  borderStyle: 'round',
1342
1878
  borderColor: 'cyan',
@@ -1398,7 +1934,7 @@ function InputBar({
1398
1934
  return h(
1399
1935
  Box,
1400
1936
  {
1401
- marginTop: 1,
1937
+ marginTop: 0,
1402
1938
  flexDirection: 'column',
1403
1939
  borderStyle: 'round',
1404
1940
  borderColor: 'cyan',
@@ -1416,11 +1952,10 @@ function InputBar({
1416
1952
  ),
1417
1953
  h(
1418
1954
  Box,
1419
- null,
1420
- h(Text, { color: 'black', backgroundColor: 'greenBright' }, ` ${copy.generic.safeMode} `),
1421
- inputStage !== 'idle' || busy ? h(Text, { color: status.color }, ` ${status.tag}`) : null,
1422
- pendingQueueLength > 0 ? h(Text, { color: 'cyanBright' }, ` ${copy.generic.queued} ${pendingQueueLength}`) : null,
1423
- h(Text, { color: showToolDetails ? 'greenBright' : 'gray' }, ` ${copy.generic.tools} ${showToolDetails ? copy.generic.open : copy.generic.collapsed}`)
1955
+ { flexDirection: 'column', alignItems: 'flex-end' },
1956
+ h(Text, { color: showToolDetails ? 'greenBright' : 'gray' }, ` ${copy.generic.tools} ${showToolDetails ? copy.generic.open : copy.generic.collapsed}`),
1957
+ pendingQueueLength > 0 ? h(Text, { color: 'cyanBright' }, ` ${copy.generic.queued} ${pendingQueueLength}`) : null,
1958
+ inputStage !== 'idle' || busy ? h(Text, { color: status.color }, ` ${status.tag}`) : null
1424
1959
  )
1425
1960
  ),
1426
1961
  h(
@@ -1503,6 +2038,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1503
2038
  const [runtimeStatus, setRuntimeStatus] = useState(
1504
2039
  makeIdleStatus(copy, runtime.getRuntimeState?.(), 'ready')
1505
2040
  );
2041
+ const [runtimeState, setRuntimeState] = useState(runtime.getRuntimeState?.() || null);
1506
2042
  const [inputStage, setInputStage] = useState('idle');
1507
2043
  const [planState, setPlanState] = useState({
1508
2044
  current: 0,
@@ -1516,15 +2052,22 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1516
2052
  const [lastKeyDebug, setLastKeyDebug] = useState('');
1517
2053
  const [showToolDetails, setShowToolDetails] = useState(false);
1518
2054
  const activeAssistantIdRef = useRef(null);
2055
+ const activeAssistantAutoSkillNamesRef = useRef([]);
2056
+ const streamedAssistantHandledRef = useRef(false);
1519
2057
  const activeUserMessageIdRef = useRef(null);
1520
2058
  const cursorIndexRef = useRef(0);
1521
2059
  const inFlightRef = useRef(false);
2060
+ const messagesRef = useRef([]);
1522
2061
  const pendingQueueRef = useRef([]);
1523
2062
  const deltaBufferRef = useRef('');
1524
2063
  const deltaFlushTimerRef = useRef(null);
1525
2064
  const escSeqRef = useRef('');
1526
2065
  const planTextBufferRef = useRef('');
1527
2066
  const { exit } = useApp();
2067
+
2068
+ useEffect(() => {
2069
+ messagesRef.current = messages;
2070
+ }, [messages]);
1528
2071
  const startupHint = copy.generic.startupHint;
1529
2072
  const isBackspaceKey = (value, key) =>
1530
2073
  Boolean(key?.backspace) || value === '\u0008' || value === '\u007f' || (key?.ctrl && value === 'h');
@@ -1565,6 +2108,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1565
2108
  if (!snapshot) return;
1566
2109
  setDisplaySessionId(snapshot.sessionId || sessionId);
1567
2110
  setDisplayModel(snapshot.model || model);
2111
+ setRuntimeState(snapshot);
1568
2112
  setRuntimeStatus(makeIdleStatus(copy, snapshot, variant));
1569
2113
  };
1570
2114
 
@@ -1620,7 +2164,12 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1620
2164
  } else {
1621
2165
  segments.push({ type: 'text', text: delta });
1622
2166
  }
1623
- return { ...m, text: `${m.text}${delta}`, segments };
2167
+ const nextText = `${m.text}${delta}`;
2168
+ return {
2169
+ ...m,
2170
+ text: nextText,
2171
+ segments
2172
+ };
1624
2173
  })
1625
2174
  );
1626
2175
  };
@@ -1644,6 +2193,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1644
2193
  const toolCalls = Array.isArray(m.toolCalls) ? [...m.toolCalls] : [];
1645
2194
  const activityType = toolEvent.type || 'tool';
1646
2195
  const idx = findActivityUpdateIndex(toolCalls, toolEvent);
2196
+ const startedAt = toolEvent.status === 'running' ? Date.now() : undefined;
1647
2197
 
1648
2198
  if (idx === -1) {
1649
2199
  toolCalls.push({
@@ -1651,6 +2201,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1651
2201
  id: toolEvent.id || '',
1652
2202
  name: toolEvent.name,
1653
2203
  status: toolEvent.status,
2204
+ ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
2205
+ ...(startedAt ? { startedAt } : {}),
1654
2206
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1655
2207
  ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
1656
2208
  });
@@ -1660,6 +2212,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1660
2212
  type: activityType,
1661
2213
  id: toolEvent.id || toolCalls[idx].id,
1662
2214
  status: toolEvent.status,
2215
+ ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
2216
+ ...(startedAt ? { startedAt } : {}),
1663
2217
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1664
2218
  ...(toolEvent.summary
1665
2219
  ? { summary: mergeActivitySummary(toolCalls[idx].summary, toolEvent.summary, toolEvent.name) }
@@ -1673,6 +2227,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1673
2227
  id: toolEvent.id || '',
1674
2228
  name: toolEvent.name,
1675
2229
  status: toolEvent.status,
2230
+ ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
2231
+ ...(startedAt ? { startedAt } : {}),
1676
2232
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1677
2233
  ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
1678
2234
  };
@@ -1737,7 +2293,13 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1737
2293
  if (!activeAssistantIdRef.current && result.text) {
1738
2294
  setMessages((prev) => [
1739
2295
  ...prev,
1740
- { id: nextId(), label: 'coder', text: result.text, color: 'greenBright' }
2296
+ {
2297
+ id: nextId(),
2298
+ label: 'coder',
2299
+ text: result.text,
2300
+ color: 'greenBright',
2301
+ autoSkillNames: activeAssistantAutoSkillNamesRef.current
2302
+ }
1741
2303
  ]);
1742
2304
  }
1743
2305
  return;
@@ -1761,8 +2323,38 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1761
2323
  setMessages((prev) => prev.map((m) => (m.id === targetId ? { ...m, ...patch } : m)));
1762
2324
  };
1763
2325
 
2326
+ const updatePendingToolCallOnActiveAssistant = (toolCall) => {
2327
+ const targetId = activeAssistantIdRef.current;
2328
+ if (!targetId || !toolCall) return;
2329
+ setMessages((prev) =>
2330
+ prev.map((m) => {
2331
+ if (m.id !== targetId) return m;
2332
+ const pendingToolCalls = Array.isArray(m.pendingToolCalls) ? [...m.pendingToolCalls] : [];
2333
+ const nextCall = {
2334
+ id: toolCall.id || '',
2335
+ name: toolCall.name || '',
2336
+ arguments: typeof toolCall.arguments === 'string' ? safeJsonParse(toolCall.arguments) ?? toolCall.arguments : toolCall.arguments,
2337
+ status: 'pending',
2338
+ type: 'tool'
2339
+ };
2340
+ const idx = pendingToolCalls.findIndex((entry) => entry.id && entry.id === nextCall.id);
2341
+ if (idx === -1) pendingToolCalls.push(nextCall);
2342
+ else pendingToolCalls[idx] = { ...pendingToolCalls[idx], ...nextCall };
2343
+ return { ...m, pendingToolCalls };
2344
+ })
2345
+ );
2346
+ };
2347
+
1764
2348
  const finalizeActiveAssistant = () => {
1765
- setActiveAssistantMeta({ loading: false, phase: undefined, liveStatus: undefined, planStep: undefined });
2349
+ setActiveAssistantMeta({
2350
+ loading: false,
2351
+ phase: undefined,
2352
+ liveStatus: undefined,
2353
+ planStep: undefined,
2354
+ pendingToolCalls: [],
2355
+ codeGenerationEndedAt: undefined,
2356
+ autoSkillNames: activeAssistantAutoSkillNamesRef.current
2357
+ });
1766
2358
  };
1767
2359
 
1768
2360
  const ensureActiveAssistant = () => {
@@ -1780,7 +2372,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1780
2372
  segments: [],
1781
2373
  loading: true,
1782
2374
  phase: 'thinking',
1783
- liveStatus: copy.runtime.modelThinking
2375
+ liveStatus: copy.runtime.modelThinking,
2376
+ autoSkillNames: activeAssistantAutoSkillNamesRef.current
1784
2377
  }
1785
2378
  ]);
1786
2379
  return aid;
@@ -1795,11 +2388,14 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1795
2388
  setPlanState({ current: 0, total: 0, role: '', title: '', failed: false, steps: [] });
1796
2389
  planTextBufferRef.current = '';
1797
2390
  activeAssistantIdRef.current = null;
2391
+ activeAssistantAutoSkillNamesRef.current = [];
2392
+ streamedAssistantHandledRef.current = false;
1798
2393
  deltaBufferRef.current = '';
1799
2394
 
1800
2395
  runtime
1801
2396
  .submit(line, (event) => {
1802
2397
  if (event?.type === 'assistant:start') {
2398
+ streamedAssistantHandledRef.current = true;
1803
2399
  setRuntimeStatus(makeStatus(copy.runtime.modelThinking, copy.runtime.requestDelivered, 'cyanBright'));
1804
2400
  setInputStage('thinking');
1805
2401
  updateMessageMeta(activeUserMessageIdRef.current, {
@@ -1815,15 +2411,85 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1815
2411
  updatePlanProgressFromText(event.text);
1816
2412
  setRuntimeStatus(makeStatus(copy.runtime.generatingReply, copy.runtime.streamingReply, 'greenBright'));
1817
2413
  setInputStage('streaming');
1818
- setActiveAssistantMeta({ loading: true, phase: 'generating', liveStatus: copy.runtime.generatingReply });
2414
+ setActiveAssistantMeta((() => {
2415
+ const targetId = activeAssistantIdRef.current;
2416
+ let liveStatus = copy.runtime.generatingReply;
2417
+ if (targetId) {
2418
+ const current = messagesRef.current?.find?.((m) => m.id === targetId);
2419
+ const pendingToolCalls = Array.isArray(current?.pendingToolCalls) ? current.pendingToolCalls : [];
2420
+ if (pendingToolCalls.length > 0) {
2421
+ liveStatus = copy.runtime.generatingCode;
2422
+ }
2423
+ }
2424
+ return { loading: true, phase: 'generating', liveStatus };
2425
+ })());
1819
2426
  queueAssistantDelta(event.text);
1820
2427
  }
2428
+ if (event?.type === 'assistant:tool_call_delta') {
2429
+ ensureActiveAssistant();
2430
+ const parsed = parseToolDisplayName(event.toolCall?.name);
2431
+ const isCodeTool = new Set(['write', 'edit', 'patch', 'generate_diff']).has(parsed.base);
2432
+ if (isCodeTool) {
2433
+ setRuntimeStatus(makeStatus(copy.runtime.generatingCode, copy.runtime.streamingReply, 'greenBright'));
2434
+ setInputStage('streaming');
2435
+ const startedAt = Date.now();
2436
+ const targetId = activeAssistantIdRef.current;
2437
+ if (targetId) {
2438
+ setMessages((prev) =>
2439
+ prev.map((m) => {
2440
+ if (m.id !== targetId) return m;
2441
+ return ensureCodeGenerationTiming(
2442
+ {
2443
+ ...m,
2444
+ loading: true,
2445
+ phase: 'generating',
2446
+ liveStatus: copy.runtime.generatingCode
2447
+ },
2448
+ startedAt
2449
+ );
2450
+ })
2451
+ );
2452
+ }
2453
+ }
2454
+ updatePendingToolCallOnActiveAssistant(event.toolCall);
2455
+ }
1821
2456
  if (event?.type === 'assistant:response') {
1822
- setRuntimeStatus(makeStatus(copy.runtime.replyCompleted, copy.runtime.outputFinished, 'greenBright'));
1823
- setInputStage('idle');
2457
+ const hasPlannedTools = Array.isArray(event.toolCalls) && event.toolCalls.length > 0;
2458
+ if (hasPlannedTools) {
2459
+ setRuntimeStatus(makeStatus(copy.runtime.toolRunning, copy.runtime.waitingToolStart || copy.runtime.streamingReply, 'magentaBright'));
2460
+ setInputStage('thinking');
2461
+ } else {
2462
+ setRuntimeStatus(makeStatus(copy.runtime.replyCompleted, copy.runtime.outputFinished, 'greenBright'));
2463
+ setInputStage('idle');
2464
+ }
1824
2465
  flushAssistantDelta();
1825
- finalizeActiveAssistant();
1826
- if (!activeAssistantIdRef.current && event.text) {
2466
+ const targetId = activeAssistantIdRef.current;
2467
+ const hadActiveAssistant = Boolean(targetId);
2468
+ if (hadActiveAssistant) {
2469
+ streamedAssistantHandledRef.current = true;
2470
+ }
2471
+ if (targetId && !hasPlannedTools) {
2472
+ setMessages((prev) =>
2473
+ prev.map((m) => {
2474
+ if (m.id !== targetId) return m;
2475
+ return {
2476
+ ...m,
2477
+ ...(typeof event.text === 'string' && event.text.length > 0 ? { text: event.text } : {}),
2478
+ loading: false,
2479
+ phase: undefined,
2480
+ liveStatus: undefined,
2481
+ planStep: undefined,
2482
+ pendingToolCalls: [],
2483
+ autoSkillNames: activeAssistantAutoSkillNamesRef.current,
2484
+ ...(m.codeGenerationStartedAt && !m.codeGenerationEndedAt ? { codeGenerationEndedAt: Date.now() } : {})
2485
+ };
2486
+ })
2487
+ );
2488
+ }
2489
+ if (!hasPlannedTools) {
2490
+ activeAssistantIdRef.current = null;
2491
+ }
2492
+ if (!hadActiveAssistant && !hasPlannedTools && event.text) {
1827
2493
  setMessages((prev) => [
1828
2494
  ...prev,
1829
2495
  { id: nextId(), label: 'coder', text: event.text, color: 'greenBright' }
@@ -1835,12 +2501,28 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1835
2501
  const detail = describeToolActivity(event.name, copy);
1836
2502
  setRuntimeStatus(makeStatus(copy.runtime.toolRunning, detail, 'magentaBright'));
1837
2503
  setInputStage('tooling');
1838
- setActiveAssistantMeta({ loading: true, phase: 'tooling', liveStatus: detail });
2504
+ const targetId = activeAssistantIdRef.current;
2505
+ if (targetId) {
2506
+ const finishedAt = Date.now();
2507
+ setMessages((prev) =>
2508
+ prev.map((m) => {
2509
+ if (m.id !== targetId) return m;
2510
+ const nextMessage = isCodeActivityName(event.name) ? finishCodeGeneration(m, finishedAt) : m;
2511
+ return {
2512
+ ...nextMessage,
2513
+ loading: true,
2514
+ phase: 'tooling',
2515
+ liveStatus: detail
2516
+ };
2517
+ })
2518
+ );
2519
+ }
1839
2520
  updateActivityStatusOnActiveAssistant({
1840
2521
  type: 'tool',
1841
2522
  id: event.id,
1842
2523
  name: event.name,
1843
- status: 'running'
2524
+ status: 'running',
2525
+ arguments: event.arguments
1844
2526
  });
1845
2527
  }
1846
2528
  if (event?.type === 'tool:end') {
@@ -1854,7 +2536,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1854
2536
  name: event.name,
1855
2537
  status: 'done',
1856
2538
  durationMs: event.durationMs,
1857
- summary: event.summary
2539
+ summary: event.summary,
2540
+ arguments: event.arguments
1858
2541
  });
1859
2542
  }
1860
2543
  if (event?.type === 'tool:blocked') {
@@ -1873,7 +2556,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1873
2556
  type: 'tool',
1874
2557
  id: event.id,
1875
2558
  name: event.name,
1876
- status: 'blocked'
2559
+ status: 'blocked',
2560
+ arguments: event.arguments
1877
2561
  });
1878
2562
  }
1879
2563
  if (event?.type === 'tool:error') {
@@ -1894,7 +2578,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1894
2578
  name: event.name,
1895
2579
  status: 'error',
1896
2580
  durationMs: event.durationMs,
1897
- summary: event.summary
2581
+ summary: event.summary,
2582
+ arguments: event.arguments
1898
2583
  });
1899
2584
  }
1900
2585
  if (event?.type === 'skill:start') {
@@ -1932,6 +2617,21 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1932
2617
  summary: event.summary
1933
2618
  });
1934
2619
  }
2620
+ if (event?.type === 'skill:auto') {
2621
+ const detail = describeAutoSkillActivity(event.names, copy);
2622
+ if (Array.isArray(event.names) && event.names.length > 0) {
2623
+ activeAssistantAutoSkillNamesRef.current = event.names.filter(Boolean);
2624
+ const targetId = activeAssistantIdRef.current;
2625
+ if (targetId) {
2626
+ setMessages((prev) =>
2627
+ prev.map((m) => (m.id === targetId ? { ...m, autoSkillNames: activeAssistantAutoSkillNamesRef.current } : m))
2628
+ );
2629
+ }
2630
+ }
2631
+ if (detail) {
2632
+ setRuntimeStatus(makeStatus(copy.runtime.skillRunning, detail, 'blueBright'));
2633
+ }
2634
+ }
1935
2635
  if (event?.type === 'compact:auto') {
1936
2636
  setRuntimeStatus(makeStatus(copy.runtime.compactingContext, `auto compact ${event.mode}`, 'yellowBright'));
1937
2637
  setMessages((prev) => [
@@ -1971,6 +2671,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1971
2671
  }
1972
2672
  syncRuntimeVisualState(result.type === 'noop' ? 'ready' : 'after');
1973
2673
  if (result.type === 'noop') return;
2674
+ if (!shouldAppendAssistantResult(result, activeAssistantIdRef.current, streamedAssistantHandledRef.current)) return;
1974
2675
  appendResultMessage(result);
1975
2676
  })
1976
2677
  .catch((err) => {
@@ -1997,6 +2698,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1997
2698
  flushAssistantDelta();
1998
2699
  finalizeActiveAssistant();
1999
2700
  activeAssistantIdRef.current = null;
2701
+ streamedAssistantHandledRef.current = false;
2000
2702
  activeUserMessageIdRef.current = null;
2001
2703
  if (deltaFlushTimerRef.current) {
2002
2704
  clearTimeout(deltaFlushTimerRef.current);
@@ -2386,7 +3088,6 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2386
3088
  Box,
2387
3089
  { flexDirection: 'column' },
2388
3090
  h(Header, { sessionId: displaySessionId, model: displayModel, shellName }),
2389
- h(RuntimeStrip, { busy, runtimeStatus, loaderTick, copy }),
2390
3091
  h(MessageList, {
2391
3092
  messages: visibleMessages,
2392
3093
  loaderTick,
@@ -2396,12 +3097,17 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2396
3097
  }),
2397
3098
  h(
2398
3099
  Box,
2399
- { marginTop: 1 },
3100
+ { marginTop: 0, marginBottom: 0, justifyContent: 'space-between', width: '100%' },
2400
3101
  h(
2401
- Text,
2402
- { color: 'gray' },
2403
- `${showToolDetails ? copy.generic.toolSummaryExpanded : copy.generic.toolSummaryCollapsed} (${copy.generic.toggleToolSummary}) · ${copy.generic.scrollHint}`
2404
- )
3102
+ Box,
3103
+ { flexGrow: 1 },
3104
+ h(
3105
+ Text,
3106
+ { color: 'gray' },
3107
+ `${showToolDetails ? copy.generic.toolSummaryExpanded : copy.generic.toolSummaryCollapsed} (${copy.generic.toggleToolSummary}) · ${copy.generic.scrollHint}`
3108
+ )
3109
+ ),
3110
+ h(ContextProgressMeter, { runtimeState, runtimeStatus, compact: true })
2405
3111
  ),
2406
3112
  h(SuggestionPanel, { commandSuggestions, suggestionNav, menuIndex, copy }),
2407
3113
  h(PendingPanel, { pendingQueue, copy }),