codemini-cli 0.1.19 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.1.19",
3
+ "version": "0.2.0",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -262,7 +262,7 @@ export async function runAgentLoop({
262
262
  }
263
263
 
264
264
  if (!approved) {
265
- if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id });
265
+ if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id, arguments: args });
266
266
  const blockedMessage = {
267
267
  role: 'tool',
268
268
  tool_call_id: call.id,
@@ -274,6 +274,7 @@ export async function runAgentLoop({
274
274
  type: 'tool:result',
275
275
  name: displayName,
276
276
  id: call.id,
277
+ arguments: args,
277
278
  content: blockedMessage.content,
278
279
  blocked: true
279
280
  });
@@ -281,7 +282,7 @@ export async function runAgentLoop({
281
282
  continue;
282
283
  }
283
284
 
284
- if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id });
285
+ if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id, arguments: args });
285
286
  const handler = toolHandlers[toolName];
286
287
  if (!handler) {
287
288
  throw new Error(`Unknown tool: ${call.name}`);
@@ -297,6 +298,7 @@ export async function runAgentLoop({
297
298
  type: 'tool:error',
298
299
  name: displayName,
299
300
  id: call.id,
301
+ arguments: args,
300
302
  durationMs,
301
303
  summary: trimInline(message, 120)
302
304
  });
@@ -312,6 +314,7 @@ export async function runAgentLoop({
312
314
  type: 'tool:result',
313
315
  name: displayName,
314
316
  id: call.id,
317
+ arguments: args,
315
318
  content: toolMessage.content,
316
319
  error: true
317
320
  });
@@ -324,6 +327,7 @@ export async function runAgentLoop({
324
327
  type: 'tool:end',
325
328
  name: displayName,
326
329
  id: call.id,
330
+ arguments: args,
327
331
  durationMs,
328
332
  summary: summarizeToolResult(toolResult)
329
333
  });
@@ -339,6 +343,7 @@ export async function runAgentLoop({
339
343
  type: 'tool:result',
340
344
  name: displayName,
341
345
  id: call.id,
346
+ arguments: args,
342
347
  content: toolMessage.content
343
348
  });
344
349
  }
@@ -1377,6 +1377,9 @@ async function askModel({
1377
1377
  maxRetries: config.gateway.max_retries ?? 2,
1378
1378
  onTextDelta: (delta) => {
1379
1379
  if (onAgentEvent) onAgentEvent({ type: 'assistant:delta', text: delta });
1380
+ },
1381
+ onToolCallDelta: (toolCall) => {
1382
+ if (onAgentEvent) onAgentEvent({ type: 'assistant:tool_call_delta', toolCall });
1380
1383
  }
1381
1384
  });
1382
1385
  }
@@ -2672,6 +2675,13 @@ export async function createChatRuntime({
2672
2675
  }
2673
2676
 
2674
2677
  const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
2678
+ const selectedAutoSkills = selectAutoSkillNames(expandedText).filter((name) => isSkillEnabled(config, name));
2679
+ if (selectedAutoSkills.length > 0 && onAgentEvent) {
2680
+ onAgentEvent({
2681
+ type: 'skill:auto',
2682
+ names: selectedAutoSkills
2683
+ });
2684
+ }
2675
2685
  const routedSystemPrompt = buildAutoSkillSystemPrompt(activeReplySystemPrompt, commands, config, expandedText);
2676
2686
  const result = await askModel({
2677
2687
  text: expandedText,
@@ -205,6 +205,7 @@ export async function createChatCompletionStream({
205
205
  temperature = 0.2,
206
206
  tools,
207
207
  onTextDelta,
208
+ onToolCallDelta,
208
209
  timeoutMs = 90000,
209
210
  maxRetries = 2
210
211
  }) {
@@ -248,6 +249,14 @@ export async function createChatCompletionStream({
248
249
  if (td.function?.name) current.name = `${current.name}${td.function.name}`;
249
250
  if (td.function?.arguments) current.arguments = `${current.arguments}${td.function.arguments}`;
250
251
  toolCallsByIndex.set(idx, current);
252
+ if (onToolCallDelta) {
253
+ onToolCallDelta({
254
+ index: idx,
255
+ id: current.id || `tc-${idx + 1}`,
256
+ name: current.name,
257
+ arguments: current.arguments || '{}'
258
+ });
259
+ }
251
260
  }
252
261
  }
253
262
 
@@ -118,5 +118,5 @@ export function getEffectivePolicy(config) {
118
118
 
119
119
  export function getShellSystemPrompt(value) {
120
120
  const profile = getShellProfile(value);
121
- return `You are CodeMini CLI working in a ${profile.label} shell environment. Prefer OpenCode-style primary tools first: use read to inspect files, grep to search file contents, glob to find files by pattern, list to inspect directories, edit to modify existing files, write to create or fully rewrite files when appropriate, patch to apply unified diffs, and run for one-shot shell commands. Treat edit as the default editing path for existing code. Internal low-level edit strategies such as target resolution, block replacement, exact text replacement, and anchored inserts are handled inside edit rather than exposed as separate tools. Use generate_diff when you need a structured preview of a proposed file change. Use start_service, list_services, get_service_status, get_service_logs, and stop_service for long-running servers or watchers. Use run only for one-shot commands that should exit on their own. For existing code files, prefer grep/read/edit and only use write with full_file_rewrite=true when a whole-file rewrite is truly intended. Avoid unnecessary tool calls.`;
121
+ return `You are CodeMini CLI working in a ${profile.label} shell environment. Prefer OpenCode-style primary tools first: use read to inspect files, grep to search file contents, glob to find files by pattern, list to inspect directories, edit to modify existing files, write to create or fully rewrite files when appropriate, patch to apply unified diffs, and run for one-shot shell commands like install, build, test, or other finite tasks. Classify frontend, backend, database, and Docker work carefully: use run for finite commands, and use start_service, list_services, get_service_status, get_service_logs, and stop_service for long-running servers, watchers, and dev processes. Treat edit as the default editing path for existing code. Internal low-level edit strategies such as target resolution, block replacement, exact text replacement, and anchored inserts are handled inside edit rather than exposed as separate tools. Use generate_diff when you need a structured preview of a proposed file change. For existing code files, prefer grep/read/edit and only use write with full_file_rewrite=true when a whole-file rewrite is truly intended. Avoid unnecessary tool calls.`;
122
122
  }
package/src/core/shell.js CHANGED
@@ -26,9 +26,129 @@ const READY_OUTPUT_PATTERNS = [
26
26
  const AUTO_STOP_GRACE_MS = 150;
27
27
  const LONG_RUNNING_STARTUP_WINDOW_MS = 1500;
28
28
 
29
+ function normalizeCommand(command) {
30
+ return String(command || '').trim();
31
+ }
32
+
33
+ function matchesAny(value, patterns) {
34
+ return patterns.some((pattern) => pattern.test(value));
35
+ }
36
+
37
+ export function classifyCommandIntent(command) {
38
+ const value = normalizeCommand(command);
39
+
40
+ if (!value) {
41
+ return { kind: 'generic', longRunning: false };
42
+ }
43
+
44
+ if (
45
+ /\b(?:npm|pnpm|yarn|bun)\s+install\b/i.test(value) ||
46
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:ci|i|add)\b/i.test(value) ||
47
+ /\buv\s+pip\s+install\b/i.test(value) ||
48
+ /\bpip\s+install\b/i.test(value) ||
49
+ /\bcargo\s+install\b/i.test(value) ||
50
+ /\bbundle\s+install\b/i.test(value) ||
51
+ /\bcomposer\s+install\b/i.test(value)
52
+ ) {
53
+ return { kind: 'install', longRunning: false };
54
+ }
55
+
56
+ if (/\b(?:build|compile|bundle|pack|transpile)\b/i.test(value)) {
57
+ return { kind: 'build', longRunning: false };
58
+ }
59
+
60
+ if (
61
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|lint|check|typecheck)\b/i.test(value) ||
62
+ /\b(?:jest|vitest|mocha|ava|pytest|go\s+test|cargo\s+test|dotnet\s+test)\b/i.test(value)
63
+ ) {
64
+ return { kind: 'test', longRunning: false };
65
+ }
66
+
67
+ const frontendServicePatterns = [
68
+ /\bvite\b/i,
69
+ /\bnext\s+dev\b/i,
70
+ /\bnuxt\s+dev\b/i,
71
+ /\bastro\s+dev\b/i,
72
+ /\bremix\s+dev\b/i,
73
+ /\bsvelte-kit\s+dev\b/i,
74
+ /\bwebpack\s+serve\b/i,
75
+ /\bvue-cli-service\s+serve\b/i,
76
+ /\breact-scripts\s+start\b/i,
77
+ /\bstorybook\b/i,
78
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:client|frontend|front-end|web|ui)\b/i,
79
+ /\b(?:client|frontend|front-end|web|ui)\b.*\b(?:dev|start|serve|preview)\b/i
80
+ ];
81
+ if (matchesAny(value, frontendServicePatterns)) {
82
+ return { kind: 'frontend-service', longRunning: true };
83
+ }
84
+
85
+ const backendServicePatterns = [
86
+ /\bpython\s+-m\s+http\.server\b/i,
87
+ /\buvicorn\b/i,
88
+ /\bgunicorn\b/i,
89
+ /\bflask\s+run\b/i,
90
+ /\bdjango\s+runserver\b/i,
91
+ /\brails\s+(?:s|server)\b/i,
92
+ /\bmvn(?:w)?\s+spring-boot:run\b/i,
93
+ /\bgradle(?:w)?\s+bootRun\b/i,
94
+ /\bgradle(?:w)?\s+run\b/i,
95
+ /\bjava\b.*\bserver\b/i,
96
+ /\bdotnet\s+run\b/i,
97
+ /\bgo\s+run\b.*\b(server|cmd\/server|main\.go)\b/i,
98
+ /\bnest\s+start\b/i,
99
+ /\bnodemon\b/i,
100
+ /\bts-node-dev\b/i,
101
+ /\bair\b/i,
102
+ /\bphp\s+artisan\s+serve\b/i,
103
+ /\bsymfony\s+server:start\b/i,
104
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:server|api|backend)\b/i,
105
+ /\b(?:server|api|backend)\b.*\b(?:dev|start|serve|preview)\b/i
106
+ ];
107
+ if (matchesAny(value, backendServicePatterns)) {
108
+ return { kind: 'backend-service', longRunning: true };
109
+ }
110
+
111
+ const databaseServicePatterns = [
112
+ /\bpostgres(?:ql)?\b/i,
113
+ /\bmysql\b/i,
114
+ /\bmariadb\b/i,
115
+ /\bmongod\b/i,
116
+ /\bredis-server\b/i,
117
+ /\b(?:docker|docker-compose|docker compose)\s+.*\b(?:db|database|postgres|mysql|mongo|redis)\b/i,
118
+ /\b(?:db|database|postgres|mysql|mongo|redis)\b.*\b(?:start|up|serve|run)\b/i
119
+ ];
120
+ if (matchesAny(value, databaseServicePatterns)) {
121
+ return { kind: 'database-service', longRunning: true };
122
+ }
123
+
124
+ const dockerServicePatterns = [
125
+ /\bdocker\s+compose\s+up\b/i,
126
+ /\bdocker-compose\s+up\b/i,
127
+ /\bdocker\s+run\b/i,
128
+ /\bdocker\s+start\b/i
129
+ ];
130
+ if (matchesAny(value, dockerServicePatterns)) {
131
+ return { kind: 'docker-service', longRunning: true };
132
+ }
133
+
134
+ if (
135
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview|watch)\b/i.test(value) ||
136
+ /\b(?:vite|serve)\b/i.test(value) ||
137
+ /\b(?:watch|serve|server|dev|preview)\b/i.test(value)
138
+ ) {
139
+ return { kind: 'service', longRunning: true };
140
+ }
141
+
142
+ if (/\b(?:watch|serve|server|dev|preview)\b/i.test(value)) {
143
+ return { kind: 'service', longRunning: true };
144
+ }
145
+
146
+ return { kind: 'generic', longRunning: false };
147
+ }
148
+
29
149
  export function isLikelyLongRunningCommand(command) {
30
- const value = String(command || '');
31
- return LONG_RUNNING_COMMAND_RE.test(value) || GENERIC_LONG_RUNNING_HINT_RE.test(value);
150
+ const { longRunning } = classifyCommandIntent(command);
151
+ return longRunning || LONG_RUNNING_COMMAND_RE.test(normalizeCommand(command)) || GENERIC_LONG_RUNNING_HINT_RE.test(normalizeCommand(command));
32
152
  }
33
153
 
34
154
  export function hasReadyOutput(text) {
package/src/core/tools.js CHANGED
@@ -4,6 +4,7 @@ import crypto from 'node:crypto';
4
4
  import { spawn } from 'node:child_process';
5
5
  import net from 'node:net';
6
6
  import {
7
+ classifyCommandIntent,
7
8
  hasReadyOutput,
8
9
  isDangerousCommand,
9
10
  isLikelyLongRunningCommand,
@@ -793,7 +794,16 @@ async function runCommand(root, config, args) {
793
794
  throw new Error('run requires command');
794
795
  }
795
796
  if (isLikelyLongRunningCommand(command)) {
796
- throw new Error('Command looks like a long-running service. Use start_service instead of run.');
797
+ const intent = classifyCommandIntent(command);
798
+ const labelMap = {
799
+ 'frontend-service': 'frontend service',
800
+ 'backend-service': 'backend service',
801
+ 'database-service': 'database service',
802
+ 'docker-service': 'Docker service',
803
+ service: 'long-running service'
804
+ };
805
+ const label = labelMap[intent.kind] || 'long-running service';
806
+ throw new Error(`Command looks like a ${label}. Use start_service instead of run.`);
797
807
  }
798
808
  if (
799
809
  !config.policy.allow_dangerous_commands &&
@@ -1727,7 +1737,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
1727
1737
  type: 'function',
1728
1738
  function: {
1729
1739
  name: 'run',
1730
- description: 'Primary run tool. Execute a one-shot shell command in workspace. Do not use for long-running services.',
1740
+ description:
1741
+ 'Primary run tool. Execute a one-shot shell command in workspace such as install, build, test, or other finite tasks. Do not use for long-running services or watchers.',
1731
1742
  parameters: {
1732
1743
  type: 'object',
1733
1744
  properties: {
@@ -1771,7 +1782,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
1771
1782
  type: 'function',
1772
1783
  function: {
1773
1784
  name: 'start_service',
1774
- description: 'Start a long-running local service and return a compact service handle instead of blocking on process exit.',
1785
+ description:
1786
+ 'Start a long-running local service, such as a frontend, backend, database, or dev watcher, and return a compact service handle instead of blocking on process exit.',
1775
1787
  parameters: {
1776
1788
  type: 'object',
1777
1789
  properties: {
@@ -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;
@@ -121,6 +122,22 @@ const TUI_COPY = {
121
122
  doingUpdateTask: '正在更新任务',
122
123
  doneGeneric: '已完成工具',
123
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: '正在生成代码',
124
141
  doneSkill: '已完成技能',
125
142
  doingSkill: '正在执行技能',
126
143
  toolFailed: (name) => `工具执行失败: ${name}`,
@@ -154,6 +171,7 @@ const TUI_COPY = {
154
171
  skillRunning: '技能执行中',
155
172
  skillCompleted: '技能已完成',
156
173
  skillFailed: '技能执行失败',
174
+ autoSkillInjected: (names) => `自动启用技能: ${names.map((name) => `/${name}`).join(', ')}`,
157
175
  compactingContext: '正在压缩上下文',
158
176
  autoCompactTriggered: (mode, threshold) => `自动压缩已触发(${mode},阈值 ${threshold}%)`,
159
177
  requestFailed: '请求失败',
@@ -230,6 +248,22 @@ const TUI_COPY = {
230
248
  doingUpdateTask: 'Updating task',
231
249
  doneGeneric: 'Completed tool',
232
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',
233
267
  doneSkill: 'Completed skill',
234
268
  doingSkill: 'Running skill',
235
269
  toolFailed: (name) => `Tool failed: ${name}`,
@@ -263,6 +297,7 @@ const TUI_COPY = {
263
297
  skillRunning: 'skill running',
264
298
  skillCompleted: 'skill completed',
265
299
  skillFailed: 'skill failed',
300
+ autoSkillInjected: (names) => `auto-enabled skills: ${names.map((name) => `/${name}`).join(', ')}`,
266
301
  compactingContext: 'compacting context',
267
302
  autoCompactTriggered: (mode, threshold) => `auto-compact triggered (${mode}, threshold ${threshold}%)`,
268
303
  requestFailed: 'request failed',
@@ -308,6 +343,14 @@ function trimText(value, maxLen = 88) {
308
343
  return `${text.slice(0, maxLen - 3)}...`;
309
344
  }
310
345
 
346
+ function safeJsonParse(raw) {
347
+ try {
348
+ return JSON.parse(String(raw || '{}'));
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+
311
354
  function parseToolDisplayName(name) {
312
355
  const raw = String(name || '').trim();
313
356
  const match = raw.match(/^([^(]+)\((.*)\)$/);
@@ -318,14 +361,76 @@ function parseToolDisplayName(name) {
318
361
  };
319
362
  }
320
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
+
321
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
+ }
322
428
  if ((activity?.type || 'tool') === 'skill') {
323
429
  return {
324
430
  primary: `Skill`,
325
431
  secondary: `(${activity?.name || 'unknown'})`
326
432
  };
327
433
  }
328
- const parsed = parseToolDisplayName(activity?.name);
329
434
  const labels = {
330
435
  read: 'Read',
331
436
  edit: 'Edit',
@@ -351,6 +456,74 @@ function getActivityDisplayParts(activity) {
351
456
  }
352
457
 
353
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
+ }
354
527
  const { raw, base, target } = parseToolDisplayName(name);
355
528
  const safeTarget = trimText(target, 72);
356
529
  if (base === 'read') {
@@ -417,6 +590,21 @@ function describeSkillActivity(name, copy, { done = false, failed = false } = {}
417
590
  return `${copy.toolActivity.doingSkill}: /${name}`;
418
591
  }
419
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
+
420
608
  function normalizeRuntimeStatus(status, copy) {
421
609
  if (status && typeof status === 'object') {
422
610
  return {
@@ -690,6 +878,188 @@ export function parsePlanProgressLine(text) {
690
878
  };
691
879
  }
692
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
+
693
1063
  export function injectPlanStateMessage(messages, planState, activeUserMessageId, activeAssistantId) {
694
1064
  const source = Array.isArray(messages) ? messages : [];
695
1065
  if (!planState || !planState.total) return source;
@@ -915,6 +1285,7 @@ function isCodeActivityName(name) {
915
1285
  const parsed = parseToolDisplayName(name);
916
1286
  return new Set([
917
1287
  'edit',
1288
+ 'write',
918
1289
  'write_file',
919
1290
  'patch',
920
1291
  'replace_text',
@@ -943,6 +1314,23 @@ export function splitMessageRows(rows) {
943
1314
  return { textRows, codeRows };
944
1315
  }
945
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
+
946
1334
  export function normalizeActivitySpacingRows(inputRows) {
947
1335
  const rows = Array.isArray(inputRows) ? inputRows : [];
948
1336
  const normalized = [];
@@ -1043,7 +1431,7 @@ export function mergeActivitySummary(previousSummary, nextSummary, activityName)
1043
1431
  return lines.join('\n');
1044
1432
  }
1045
1433
 
1046
- function buildMessageRows(msg, showToolDetails, contentWidth = 72) {
1434
+ function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
1047
1435
  const rows = [];
1048
1436
  const pushTextRows = (text) => {
1049
1437
  const lines = String(text || '').split('\n');
@@ -1126,18 +1514,23 @@ function buildMessageRows(msg, showToolDetails, contentWidth = 72) {
1126
1514
  toolCalls.forEach((tool, idx) => pushActivityRows(tool, idx, toolCalls.length));
1127
1515
  }
1128
1516
 
1517
+ const codeGenerationRows = getCodeGenerationActivityRows(msg);
1518
+ const generatingCodeRows = getGeneratingCodePlaceholderRows(msg, copy, contentWidth);
1519
+ const syntheticRows = [...codeGenerationRows, ...generatingCodeRows];
1129
1520
  if (msg?.loading && (msg?.liveStatus || msg?.phase)) {
1521
+ const statusRows = [];
1130
1522
  pushWrappedRow(
1131
- rows,
1523
+ statusRows,
1132
1524
  {
1133
1525
  kind: 'status',
1134
1526
  text: trimText(msg.liveStatus || msg.phase, 144)
1135
1527
  },
1136
1528
  Math.max(8, contentWidth - 2)
1137
1529
  );
1530
+ syntheticRows.push(...statusRows);
1138
1531
  }
1139
1532
 
1140
- return normalizeActivitySpacingRows(rows);
1533
+ return normalizeActivitySpacingRows(insertRowsAfterLastCodeRow(rows, syntheticRows));
1141
1534
  }
1142
1535
 
1143
1536
  function renderMessageRow(msg, row, idx, loaderTick) {
@@ -1145,13 +1538,11 @@ function renderMessageRow(msg, row, idx, loaderTick) {
1145
1538
  const activity = { type: row.activityType, name: row.name, status: row.status };
1146
1539
  const display = getActivityDisplayParts(activity);
1147
1540
  const dotColor =
1148
- activity.type === 'skill'
1149
- ? row.status === 'error'
1150
- ? 'redBright'
1151
- : 'blueBright'
1152
- : row.status === 'error' || row.status === 'blocked'
1153
- ? 'redBright'
1154
- : 'greenBright';
1541
+ row.status === 'error' || row.status === 'blocked'
1542
+ ? 'redBright'
1543
+ : row.status === 'done'
1544
+ ? 'greenBright'
1545
+ : 'yellowBright';
1155
1546
  const textColor =
1156
1547
  activity.type === 'skill'
1157
1548
  ? row.status === 'error'
@@ -1159,7 +1550,8 @@ function renderMessageRow(msg, row, idx, loaderTick) {
1159
1550
  : 'cyanBright'
1160
1551
  : row.status === 'error' || row.status === 'blocked'
1161
1552
  ? 'redBright'
1162
- : 'greenBright';
1553
+ : 'cyanBright';
1554
+ const durationText = formatActivityDurationText(row);
1163
1555
  return h(
1164
1556
  Box,
1165
1557
  { key: `row-tool-${msg.id}-${idx}` },
@@ -1168,7 +1560,7 @@ function renderMessageRow(msg, row, idx, loaderTick) {
1168
1560
  h(Text, { color: 'gray' }, ' '),
1169
1561
  h(Text, { color: textColor }, display.primary),
1170
1562
  h(Text, { color: 'gray' }, display.secondary),
1171
- row.durationText ? h(Text, { color: row.statusColor }, ` ${row.durationText}`) : null
1563
+ durationText ? h(Text, { color: row.statusColor }, ` ${durationText}`) : null
1172
1564
  );
1173
1565
  }
1174
1566
  if (row.kind === 'activity-summary') {
@@ -1232,6 +1624,15 @@ function renderMessageRow(msg, row, idx, loaderTick) {
1232
1624
  h(Text, { color: 'gray' }, row.text)
1233
1625
  );
1234
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
+ }
1235
1636
  return renderTextLine(msg, row.text, idx, row.color);
1236
1637
  }
1237
1638
 
@@ -1349,11 +1750,12 @@ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, con
1349
1750
  return h(PlanSummaryBubble, { msg, copy });
1350
1751
  }
1351
1752
  const theme = roleStyle(msg.label);
1352
- const allRows = buildMessageRows(msg, showToolDetails, contentWidth);
1753
+ const allRows = buildMessageRows(msg, showToolDetails, contentWidth, copy);
1353
1754
  const start = rowWindow ? Math.max(0, rowWindow.start || 0) : 0;
1354
1755
  const end = rowWindow ? Math.max(start, rowWindow.end || allRows.length) : allRows.length;
1355
1756
  const visibleRows = allRows.slice(start, end);
1356
1757
  const rendered = renderMessageRowsInOrder(msg, visibleRows, loaderTick, copy);
1758
+ const autoSkillBadge = formatAutoSkillBadge(msg.autoSkillNames, copy);
1357
1759
 
1358
1760
  return h(
1359
1761
  Box,
@@ -1377,7 +1779,9 @@ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, con
1377
1779
  null,
1378
1780
  h(Text, { color: theme.badgeText, backgroundColor: theme.badgeBg }, ` ${messageLabel(msg.label, copy)} `)
1379
1781
  ),
1380
- h(Text, { color: theme.chrome }, ' ')
1782
+ autoSkillBadge
1783
+ ? h(Text, { color: 'blueBright' }, autoSkillBadge)
1784
+ : h(Text, { color: theme.chrome }, ' ')
1381
1785
  ),
1382
1786
  ...rendered
1383
1787
  )
@@ -1530,7 +1934,7 @@ function InputBar({
1530
1934
  return h(
1531
1935
  Box,
1532
1936
  {
1533
- marginTop: 1,
1937
+ marginTop: 0,
1534
1938
  flexDirection: 'column',
1535
1939
  borderStyle: 'round',
1536
1940
  borderColor: 'cyan',
@@ -1648,6 +2052,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1648
2052
  const [lastKeyDebug, setLastKeyDebug] = useState('');
1649
2053
  const [showToolDetails, setShowToolDetails] = useState(false);
1650
2054
  const activeAssistantIdRef = useRef(null);
2055
+ const activeAssistantAutoSkillNamesRef = useRef([]);
2056
+ const streamedAssistantHandledRef = useRef(false);
1651
2057
  const activeUserMessageIdRef = useRef(null);
1652
2058
  const cursorIndexRef = useRef(0);
1653
2059
  const inFlightRef = useRef(false);
@@ -1758,7 +2164,12 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1758
2164
  } else {
1759
2165
  segments.push({ type: 'text', text: delta });
1760
2166
  }
1761
- return { ...m, text: `${m.text}${delta}`, segments };
2167
+ const nextText = `${m.text}${delta}`;
2168
+ return {
2169
+ ...m,
2170
+ text: nextText,
2171
+ segments
2172
+ };
1762
2173
  })
1763
2174
  );
1764
2175
  };
@@ -1782,6 +2193,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1782
2193
  const toolCalls = Array.isArray(m.toolCalls) ? [...m.toolCalls] : [];
1783
2194
  const activityType = toolEvent.type || 'tool';
1784
2195
  const idx = findActivityUpdateIndex(toolCalls, toolEvent);
2196
+ const startedAt = toolEvent.status === 'running' ? Date.now() : undefined;
1785
2197
 
1786
2198
  if (idx === -1) {
1787
2199
  toolCalls.push({
@@ -1789,6 +2201,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1789
2201
  id: toolEvent.id || '',
1790
2202
  name: toolEvent.name,
1791
2203
  status: toolEvent.status,
2204
+ ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
2205
+ ...(startedAt ? { startedAt } : {}),
1792
2206
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1793
2207
  ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
1794
2208
  });
@@ -1798,6 +2212,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1798
2212
  type: activityType,
1799
2213
  id: toolEvent.id || toolCalls[idx].id,
1800
2214
  status: toolEvent.status,
2215
+ ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
2216
+ ...(startedAt ? { startedAt } : {}),
1801
2217
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1802
2218
  ...(toolEvent.summary
1803
2219
  ? { summary: mergeActivitySummary(toolCalls[idx].summary, toolEvent.summary, toolEvent.name) }
@@ -1811,6 +2227,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1811
2227
  id: toolEvent.id || '',
1812
2228
  name: toolEvent.name,
1813
2229
  status: toolEvent.status,
2230
+ ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
2231
+ ...(startedAt ? { startedAt } : {}),
1814
2232
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1815
2233
  ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
1816
2234
  };
@@ -1875,7 +2293,13 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1875
2293
  if (!activeAssistantIdRef.current && result.text) {
1876
2294
  setMessages((prev) => [
1877
2295
  ...prev,
1878
- { 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
+ }
1879
2303
  ]);
1880
2304
  }
1881
2305
  return;
@@ -1899,8 +2323,38 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1899
2323
  setMessages((prev) => prev.map((m) => (m.id === targetId ? { ...m, ...patch } : m)));
1900
2324
  };
1901
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
+
1902
2348
  const finalizeActiveAssistant = () => {
1903
- 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
+ });
1904
2358
  };
1905
2359
 
1906
2360
  const ensureActiveAssistant = () => {
@@ -1918,7 +2372,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1918
2372
  segments: [],
1919
2373
  loading: true,
1920
2374
  phase: 'thinking',
1921
- liveStatus: copy.runtime.modelThinking
2375
+ liveStatus: copy.runtime.modelThinking,
2376
+ autoSkillNames: activeAssistantAutoSkillNamesRef.current
1922
2377
  }
1923
2378
  ]);
1924
2379
  return aid;
@@ -1933,11 +2388,14 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1933
2388
  setPlanState({ current: 0, total: 0, role: '', title: '', failed: false, steps: [] });
1934
2389
  planTextBufferRef.current = '';
1935
2390
  activeAssistantIdRef.current = null;
2391
+ activeAssistantAutoSkillNamesRef.current = [];
2392
+ streamedAssistantHandledRef.current = false;
1936
2393
  deltaBufferRef.current = '';
1937
2394
 
1938
2395
  runtime
1939
2396
  .submit(line, (event) => {
1940
2397
  if (event?.type === 'assistant:start') {
2398
+ streamedAssistantHandledRef.current = true;
1941
2399
  setRuntimeStatus(makeStatus(copy.runtime.modelThinking, copy.runtime.requestDelivered, 'cyanBright'));
1942
2400
  setInputStage('thinking');
1943
2401
  updateMessageMeta(activeUserMessageIdRef.current, {
@@ -1958,8 +2416,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1958
2416
  let liveStatus = copy.runtime.generatingReply;
1959
2417
  if (targetId) {
1960
2418
  const current = messagesRef.current?.find?.((m) => m.id === targetId);
1961
- const segments = Array.isArray(current?.segments) ? current.segments : [];
1962
- if (segments.some((segment) => (segment.type === 'tool' || segment.type === 'skill') && isCodeActivityName(segment.name))) {
2419
+ const pendingToolCalls = Array.isArray(current?.pendingToolCalls) ? current.pendingToolCalls : [];
2420
+ if (pendingToolCalls.length > 0) {
1963
2421
  liveStatus = copy.runtime.generatingCode;
1964
2422
  }
1965
2423
  }
@@ -1967,12 +2425,71 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1967
2425
  })());
1968
2426
  queueAssistantDelta(event.text);
1969
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
+ }
1970
2456
  if (event?.type === 'assistant:response') {
1971
- setRuntimeStatus(makeStatus(copy.runtime.replyCompleted, copy.runtime.outputFinished, 'greenBright'));
1972
- 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
+ }
1973
2465
  flushAssistantDelta();
1974
- finalizeActiveAssistant();
1975
- 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) {
1976
2493
  setMessages((prev) => [
1977
2494
  ...prev,
1978
2495
  { id: nextId(), label: 'coder', text: event.text, color: 'greenBright' }
@@ -1984,16 +2501,28 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1984
2501
  const detail = describeToolActivity(event.name, copy);
1985
2502
  setRuntimeStatus(makeStatus(copy.runtime.toolRunning, detail, 'magentaBright'));
1986
2503
  setInputStage('tooling');
1987
- setActiveAssistantMeta({
1988
- loading: true,
1989
- phase: 'tooling',
1990
- liveStatus: isCodeActivityName(event.name) ? copy.runtime.generatingCode : detail
1991
- });
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
+ }
1992
2520
  updateActivityStatusOnActiveAssistant({
1993
2521
  type: 'tool',
1994
2522
  id: event.id,
1995
2523
  name: event.name,
1996
- status: 'running'
2524
+ status: 'running',
2525
+ arguments: event.arguments
1997
2526
  });
1998
2527
  }
1999
2528
  if (event?.type === 'tool:end') {
@@ -2007,7 +2536,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2007
2536
  name: event.name,
2008
2537
  status: 'done',
2009
2538
  durationMs: event.durationMs,
2010
- summary: event.summary
2539
+ summary: event.summary,
2540
+ arguments: event.arguments
2011
2541
  });
2012
2542
  }
2013
2543
  if (event?.type === 'tool:blocked') {
@@ -2026,7 +2556,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2026
2556
  type: 'tool',
2027
2557
  id: event.id,
2028
2558
  name: event.name,
2029
- status: 'blocked'
2559
+ status: 'blocked',
2560
+ arguments: event.arguments
2030
2561
  });
2031
2562
  }
2032
2563
  if (event?.type === 'tool:error') {
@@ -2047,7 +2578,8 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2047
2578
  name: event.name,
2048
2579
  status: 'error',
2049
2580
  durationMs: event.durationMs,
2050
- summary: event.summary
2581
+ summary: event.summary,
2582
+ arguments: event.arguments
2051
2583
  });
2052
2584
  }
2053
2585
  if (event?.type === 'skill:start') {
@@ -2085,6 +2617,21 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2085
2617
  summary: event.summary
2086
2618
  });
2087
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
+ }
2088
2635
  if (event?.type === 'compact:auto') {
2089
2636
  setRuntimeStatus(makeStatus(copy.runtime.compactingContext, `auto compact ${event.mode}`, 'yellowBright'));
2090
2637
  setMessages((prev) => [
@@ -2124,6 +2671,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2124
2671
  }
2125
2672
  syncRuntimeVisualState(result.type === 'noop' ? 'ready' : 'after');
2126
2673
  if (result.type === 'noop') return;
2674
+ if (!shouldAppendAssistantResult(result, activeAssistantIdRef.current, streamedAssistantHandledRef.current)) return;
2127
2675
  appendResultMessage(result);
2128
2676
  })
2129
2677
  .catch((err) => {
@@ -2150,6 +2698,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2150
2698
  flushAssistantDelta();
2151
2699
  finalizeActiveAssistant();
2152
2700
  activeAssistantIdRef.current = null;
2701
+ streamedAssistantHandledRef.current = false;
2153
2702
  activeUserMessageIdRef.current = null;
2154
2703
  if (deltaFlushTimerRef.current) {
2155
2704
  clearTimeout(deltaFlushTimerRef.current);