miii-cli 1.2.3 → 1.2.4

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.
@@ -7,6 +7,12 @@ import { getTavilyKey, tavilySearch, tavilyExtract } from '../tavily/client.js';
7
7
  const run = promisify(exec);
8
8
  const runFile = promisify(execFile);
9
9
  const EXEC_TIMEOUT_MS = 30_000;
10
+ function requireArg(val, name, tool) {
11
+ if (typeof val !== 'string' || !val.length) {
12
+ throw new Error(`${tool}: "${name}" argument is required and must be a non-empty string`);
13
+ }
14
+ return val;
15
+ }
10
16
  export const tools = [
11
17
  {
12
18
  name: 'read_file',
@@ -14,7 +20,7 @@ export const tools = [
14
20
  params: '{"path": "string"}',
15
21
  execute: async ({ path }) => {
16
22
  try {
17
- return readFile(guardPath(path));
23
+ return readFile(guardPath(requireArg(path, 'path', 'read_file')));
18
24
  }
19
25
  catch (e) {
20
26
  throw new Error(`read_file: ${e}`);
@@ -26,7 +32,7 @@ export const tools = [
26
32
  description: 'List directory contents',
27
33
  params: '{"path": "string", "recursive": "boolean (optional)"}',
28
34
  execute: async ({ path, recursive = false }) => {
29
- const entries = listFiles(guardPath(path), recursive);
35
+ const entries = listFiles(guardPath(requireArg(path, 'path', 'list_files')), recursive);
30
36
  if (!entries.length)
31
37
  return '(empty)';
32
38
  return entries.map(e => `${e.type === 'dir' ? 'd' : 'f'} ${e.rel}`).join('\n');
@@ -37,10 +43,10 @@ export const tools = [
37
43
  description: 'Create a new file with content — fails if file already exists. Prefer edit_file for new files.',
38
44
  params: '{"path": "string", "content": "string"}',
39
45
  execute: async ({ path, content }) => {
40
- const safe = guardPath(path);
46
+ const safe = guardPath(requireArg(path, 'path', 'create_file'));
41
47
  if (existsSync(safe))
42
48
  throw new Error(`file already exists: ${path}`);
43
- writeFile(safe, content);
49
+ writeFile(safe, requireArg(content, 'content', 'create_file'));
44
50
  return `created: ${path}`;
45
51
  },
46
52
  },
@@ -49,13 +55,13 @@ export const tools = [
49
55
  description: 'Write a new file — only for files that do not exist yet. Use update_file to modify existing files.',
50
56
  params: '{"path": "string", "content": "string"}',
51
57
  execute: async ({ path, content }) => {
52
- const safe = guardPath(path);
58
+ const safe = guardPath(requireArg(path, 'path', 'edit_file'));
53
59
  if (existsSync(safe)) {
54
60
  throw new Error(`edit_file cannot overwrite existing file: ${path}\n` +
55
61
  `Use update_file with <old> and <new> blocks to make targeted edits.\n` +
56
62
  `Call read_file first to get the exact current text.`);
57
63
  }
58
- const text = content;
64
+ const text = requireArg(content, 'content', 'edit_file');
59
65
  writeFile(safe, text);
60
66
  const lines = text.split('\n').length;
61
67
  return `created: ${path} (${lines} line${lines === 1 ? '' : 's'})`;
@@ -66,13 +72,15 @@ export const tools = [
66
72
  description: 'Replace an exact unique string in an existing file. Always call read_file first to get the exact text.',
67
73
  params: '{"path": "string", "old": "string", "new": "string"}',
68
74
  execute: async ({ path, old: oldStr, new: newStr }) => {
69
- const safe = guardPath(path);
75
+ const safe = guardPath(requireArg(path, 'path', 'update_file'));
70
76
  const current = readFile(safe);
71
77
  if (current === null)
72
78
  throw new Error(`file not found: ${path}`);
73
79
  if (current === '')
74
80
  throw new Error(`file empty: ${path}`);
75
- const old = oldStr;
81
+ const old = requireArg(oldStr, 'old', 'update_file');
82
+ if (newStr === undefined || newStr === null)
83
+ throw new Error('update_file: "new" argument is required');
76
84
  const count = current.split(old).length - 1;
77
85
  if (count === 0) {
78
86
  throw new Error(`old text not found in ${path} — file may have changed since last read.\n` +
@@ -104,7 +112,7 @@ export const tools = [
104
112
  description: 'Delete a file',
105
113
  params: '{"path": "string"}',
106
114
  execute: async ({ path }) => {
107
- deleteFile(guardPath(path));
115
+ deleteFile(guardPath(requireArg(path, 'path', 'delete_file')));
108
116
  return `deleted: ${path}`;
109
117
  },
110
118
  },
@@ -113,7 +121,7 @@ export const tools = [
113
121
  description: 'Run a shell command in cwd',
114
122
  params: '{"command": "string"}',
115
123
  execute: async ({ command }) => {
116
- const { stdout, stderr } = await run(command, { cwd: process.cwd(), timeout: EXEC_TIMEOUT_MS });
124
+ const { stdout, stderr } = await run(requireArg(command, 'command', 'run_command'), { cwd: process.cwd(), timeout: EXEC_TIMEOUT_MS });
117
125
  const out = [stdout, stderr ? `stderr: ${stderr}` : ''].filter(Boolean).join('\n').trim();
118
126
  return out.length > 8000 ? out.slice(0, 8000) + '\n…[truncated]' : out;
119
127
  },
@@ -123,7 +131,7 @@ export const tools = [
123
131
  description: 'Create a directory (and any missing parents)',
124
132
  params: '{"path": "string"}',
125
133
  execute: async ({ path }) => {
126
- createDir(guardPath(path));
134
+ createDir(guardPath(requireArg(path, 'path', 'create_folder')));
127
135
  return `created: ${path}`;
128
136
  },
129
137
  },
@@ -132,7 +140,7 @@ export const tools = [
132
140
  description: 'Move or rename a file or directory',
133
141
  params: '{"from": "string", "to": "string"}',
134
142
  execute: async ({ from, to }) => {
135
- moveFile(guardPath(from), guardPath(to));
143
+ moveFile(guardPath(requireArg(from, 'from', 'move_file')), guardPath(requireArg(to, 'to', 'move_file')));
136
144
  return `moved: ${from} → ${to}`;
137
145
  },
138
146
  },
@@ -9,7 +9,7 @@ import { ConfigPicker } from './components/ConfigPicker.js';
9
9
  import { Divider } from './components/StatusBar.js';
10
10
  import { DesignTeachModal, DESIGN_TEACH_QUESTIONS, buildDesignPrompt } from './components/DesignTeachModal.js';
11
11
  import { tools } from '../tools/index.js';
12
- import { toolArgSummary, formatElapsed } from './printer.js';
12
+ import { toolLabel, permissionDesc, formatElapsed, EDIT_TOOLS, DELETE_TOOLS } from './printer.js';
13
13
  import { MacroQueue } from '../tasks/queue.js';
14
14
  import { TaskExecutor } from '../tasks/executor.js';
15
15
  import { THINKING_PHRASES, SPARKLE } from './thinking.js';
@@ -165,7 +165,7 @@ export function InputBar({ config: initialConfig, skills, cwd, session, version,
165
165
  setConfig(c => ({ ...c, ...configPatch }));
166
166
  saveConfig(configPatch);
167
167
  }
168
- }, onTavilyKey: (key) => { saveTavilyKey(key); setTavilyKey(key); }, onClose: () => { setConfigOpen(false); } }), _jsx(Divider, { cols: cols })] })) : pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : designTeachState ? (_jsx(DesignTeachModal, { question: DESIGN_TEACH_QUESTIONS[designTeachState.idx], index: designTeachState.idx, total: DESIGN_TEACH_QUESTIONS.length })) : permissionRequest ? (_jsxs(Box, { paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { color: "white", bold: true, children: permissionRequest.toolName }), _jsx(Text, { color: "gray", children: toolArgSummary(permissionRequest.args) })] }), _jsx(DiffPreview, { toolName: permissionRequest.toolName, args: permissionRequest.args })] })) : (status === 'thinking' || status === 'tool') ? (_jsx(Box, { paddingX: 1, gap: 1, children: status === 'thinking'
168
+ }, onTavilyKey: (key) => { saveTavilyKey(key); setTavilyKey(key); }, onClose: () => { setConfigOpen(false); } }), _jsx(Divider, { cols: cols })] })) : pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : designTeachState ? (_jsx(DesignTeachModal, { question: DESIGN_TEACH_QUESTIONS[designTeachState.idx], index: designTeachState.idx, total: DESIGN_TEACH_QUESTIONS.length })) : permissionRequest ? (_jsxs(Box, { paddingX: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: DELETE_TOOLS.has(permissionRequest.toolName) ? 'red' : EDIT_TOOLS.has(permissionRequest.toolName) ? 'green' : 'blue', children: "\u25CF" }), _jsx(Text, { color: "white", bold: true, children: toolLabel(permissionRequest.toolName, permissionRequest.args) })] }), _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: "gray", dimColor: true, children: ['└ ', permissionDesc(permissionRequest.toolName)] }) }), _jsx(DiffPreview, { toolName: permissionRequest.toolName, args: permissionRequest.args })] })) : (status === 'thinking' || status === 'tool') ? (_jsx(Box, { paddingX: 1, gap: 1, children: status === 'thinking'
169
169
  ? _jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: SPARKLE[tick % SPARKLE.length] }), _jsx(Text, { color: Math.floor(tick / 4) % 6 >= 2 && Math.floor(tick / 4) % 6 <= 4 ? 'white' : 'gray', italic: true, children: THINKING_PHRASES[phraseSeq[Math.floor(tick / 62) % phraseSeq.length]] }), _jsx(Text, { color: "gray", dimColor: true, children: formatElapsed(Date.now() - thinkingStartRef.current) }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })
170
170
  : _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }), _jsx(Text, { color: "gray", dimColor: true, children: formatElapsed(Date.now() - thinkingStartRef.current) }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] }) })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, permissionRequest: permissionRequest, onPermissionResponse: resolvePermission, designTeach: designTeachState ? {
171
171
  question: DESIGN_TEACH_QUESTIONS[designTeachState.idx],
@@ -180,19 +180,19 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
180
180
  onSubmit(name);
181
181
  }
182
182
  function selectFile(file) {
183
+ const r = cursor.row;
184
+ const line = lines[r];
185
+ const before = line.slice(0, cursor.col);
186
+ const atIdx = before.lastIndexOf('@');
187
+ if (atIdx === -1)
188
+ return;
189
+ const newLine = line.slice(0, atIdx) + '@' + file.rel + ' ' + line.slice(cursor.col);
183
190
  setLines(prev => {
184
191
  const next = [...prev];
185
- const r = cursor.row;
186
- const line = next[r];
187
- const before = line.slice(0, cursor.col);
188
- const atIdx = before.lastIndexOf('@');
189
- if (atIdx === -1)
190
- return prev;
191
- const newLine = line.slice(0, atIdx) + '@' + file.rel + ' ' + line.slice(cursor.col);
192
192
  next[r] = newLine;
193
- setCursor({ row: r, col: atIdx + 1 + file.rel.length + 1 });
194
193
  return next;
195
194
  });
195
+ setCursor({ row: r, col: atIdx + 1 + file.rel.length + 1 });
196
196
  setOverlay('none');
197
197
  setOverlayIdx(0);
198
198
  }
@@ -428,14 +428,20 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
428
428
  appendChar(input);
429
429
  if (prospective.startsWith('/')) {
430
430
  if (prospective.slice(1).includes(' ')) {
431
- setOverlay('none');
431
+ if (input === '@' || overlay === 'at') {
432
+ setOverlay('at');
433
+ setOverlayIdx(0);
434
+ }
435
+ else {
436
+ setOverlay('none');
437
+ }
432
438
  }
433
439
  else {
434
440
  setOverlay('command');
435
441
  setOverlayIdx(0);
436
442
  }
437
443
  }
438
- else if (input === '@' || (overlay === 'at' && atQuery !== undefined)) {
444
+ else if (input === '@' || (overlay === 'at' && atQuery !== '')) {
439
445
  setOverlay('at');
440
446
  setOverlayIdx(0);
441
447
  }
@@ -11,7 +11,7 @@ const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
11
11
  const PERMISSION_TOOLS = new Set(['edit_file', 'update_file', 'delete_file', 'create_file', 'move_file', 'run_command', 'git_commit']);
12
12
  const CHECKPOINT_TOOLS = new Set(['edit_file', 'update_file', 'create_file', 'delete_file']);
13
13
  // Tool result messages that are ephemeral — never worth storing in memory or compact summaries
14
- const EPHEMERAL_PATTERN = /^Tool (read_file|list_files|run_tests) result:|^\[current state of|^\[Context compacted/;
14
+ const EPHEMERAL_PATTERN = /^Tool (read_file|list_files|run_tests) result:|^\[current state of|^\[Context compacted|^\[file updated:/;
15
15
  export function stripEphemeral(messages) {
16
16
  return messages.filter(m => m.role !== 'user' || !EPHEMERAL_PATTERN.test(m.content));
17
17
  }
@@ -163,13 +163,13 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
163
163
  if (occurrences === 0) {
164
164
  printer.errorMsg(`patch stale: old text not found in ${filePath} — injecting fresh content`);
165
165
  next.push({ role: 'user', content: `Tool read_file result:\n${current}` });
166
- next.push({ role: 'user', content: `update_file failed: old text not found in ${filePath}. The file content above is the current state. Retry update_file with the correct exact text.` });
166
+ next.push({ role: 'user', content: `update_file failed: the <old> text you used does not exist in ${filePath}. The CURRENT file content is shown above. Re-read it carefully, find the exact text you want to replace, and retry update_file using text that exactly matches what is in the file now.` });
167
167
  continue;
168
168
  }
169
169
  if (occurrences > 1) {
170
170
  printer.errorMsg(`patch ambiguous: old text matches ${occurrences} locations in ${filePath} — injecting fresh content`);
171
171
  next.push({ role: 'user', content: `Tool read_file result:\n${current}` });
172
- next.push({ role: 'user', content: `update_file failed: old text matches ${occurrences} locations in ${filePath}. Use more surrounding context to make old text unique, then retry.` });
172
+ next.push({ role: 'user', content: `update_file failed: the <old> text matches ${occurrences} locations in ${filePath}. Add more surrounding lines to the <old> block to make it unique, then retry.` });
173
173
  continue;
174
174
  }
175
175
  }
@@ -209,7 +209,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
209
209
  if (didEditFiles) {
210
210
  const systemMsg = msgs.find(m => m.role === 'system');
211
211
  const goalMsg = msgs.find(m => m.role === 'user' && !m.content.startsWith('[') && !m.content.startsWith('Tool '));
212
- const batchStart = msgs.length + 1; // index in next where this batch's messages start
212
+ const batchStart = msgs.length; // include assistant message so model sees its own tool call on retry
213
213
  const batchMsgs = next.slice(batchStart);
214
214
  const slimCtx = [
215
215
  ...(systemMsg ? [systemMsg] : []),
@@ -244,6 +244,9 @@ export function useSubmit(deps) {
244
244
  printer.systemMsg('usage: /refactor <goal>');
245
245
  return;
246
246
  }
247
+ const atRefactorContext = buildAtContext(text);
248
+ if (atRefactorContext)
249
+ pushHistory({ role: 'user', content: atRefactorContext });
247
250
  await runRefactor(goal);
248
251
  return;
249
252
  }
@@ -254,6 +257,7 @@ export function useSubmit(deps) {
254
257
  return;
255
258
  }
256
259
  // Design task phase: use DESIGN.md context + impeccable principles
260
+ const atDesignContext = buildAtContext(text);
257
261
  let designContext = '';
258
262
  try {
259
263
  const designFile = readFile(guardPath('DESIGN.md', cwd));
@@ -273,13 +277,14 @@ Impeccable design rules — follow strictly:
273
277
  - Anti-patterns to eliminate: nested card shadows, purple-to-blue gradients, disabled gray without reason, centered walls of text, auto-playing anything.
274
278
  - Write distinctive, crafted UI — not generic SaaS templates.
275
279
  - Write all code to files using tools. No code blocks in responses.`;
280
+ const taskDesc = sub.replace(/@[\w./\-]+/g, '').trim();
276
281
  const taskPrompt = `${designContext}${impeccableRules}
277
282
 
278
- Design task: ${sub}
283
+ Design task: ${taskDesc}
279
284
 
280
285
  Analyze what exists, then implement the design. Use the design system above if available. Make it distinctive and well-crafted.`;
281
- printer.userMsg(`/design ${sub}`);
282
- pushHistory({ role: 'user', content: taskPrompt });
286
+ printer.userMsg(text);
287
+ pushHistory({ role: 'user', content: atDesignContext + taskPrompt });
283
288
  await runLoop(buildContext(), 0, sub);
284
289
  return;
285
290
  }
@@ -289,7 +294,10 @@ Analyze what exists, then implement the design. Use the design system above if a
289
294
  printer.systemMsg('usage: /think <query>');
290
295
  return;
291
296
  }
292
- printer.userMsg(`/think ${query}`);
297
+ const atThinkContext = buildAtContext(text);
298
+ if (atThinkContext)
299
+ pushHistory({ role: 'user', content: atThinkContext });
300
+ printer.userMsg(text);
293
301
  setStatus('thinking');
294
302
  setTaskLabel(`gathering: ${query}`);
295
303
  abortRef.current = new AbortController();
@@ -512,6 +520,9 @@ Analyze what exists, then implement the design. Use the design system above if a
512
520
  return;
513
521
  }
514
522
  if (skill.execute) {
523
+ const atContext = buildAtContext(text);
524
+ if (atContext)
525
+ pushHistory({ role: 'user', content: atContext });
515
526
  const ctx = {
516
527
  messages: historyRef.current.map(m => ({ role: m.role, content: m.content })),
517
528
  appendMessage: (_role, content) => printer.systemMsg(content),
@@ -524,8 +535,9 @@ Analyze what exists, then implement the design. Use the design system above if a
524
535
  return;
525
536
  }
526
537
  if (skill.prompt) {
527
- printer.userMsg(skill.prompt);
528
- pushHistory({ role: 'user', content: skill.prompt });
538
+ const atContext = buildAtContext(text);
539
+ printer.userMsg(text);
540
+ pushHistory({ role: 'user', content: atContext + skill.prompt });
529
541
  await runLoop(buildContext());
530
542
  return;
531
543
  }
@@ -25,6 +25,8 @@ const gray = (s) => col(90, s);
25
25
  const yellow = (s) => col(93, s);
26
26
  const purple = (s) => col(95, s);
27
27
  const red = (s) => col(91, s);
28
+ function bgRed(s) { return `\x1b[48;2;65;18;18m\x1b[91m${s}${R}`; }
29
+ function bgGreen(s) { return `\x1b[48;2;14;46;14m\x1b[92m${s}${R}`; }
28
30
  function stripMarkdown(s) {
29
31
  return s
30
32
  .replace(/\*\*\*(.+?)\*\*\*/g, '$1')
@@ -121,6 +123,7 @@ export function welcome(provider, model, cwd, version, updateAvailable, linked)
121
123
  ` ${bold(yellow('Tips for getting started'))}`,
122
124
  ` Type ${cyan('@filename')} to inject file into context`,
123
125
  ` Use ${cyan('/skill')} to run a skill or command`,
126
+ ` Use ${cyan('/design teach')} to generate a design system`,
124
127
  ` Use ${cyan('/config')} to switch provider, model, or API key`,
125
128
  '',
126
129
  ];
@@ -159,30 +162,42 @@ export function assistantMsg(text) {
159
162
  const tail = lines.slice(idx + 1).join('\n');
160
163
  write(`\n${blue('●')} ${head}${tail ? '\n' + tail : ''}\n`);
161
164
  }
162
- const EDIT_TOOLS = new Set(['edit_file', 'update_file', 'create_file', 'write_file']);
163
- const DELETE_TOOLS = new Set(['delete_file', 'remove_file']);
164
- function toolLabel(name, args) {
165
+ export const EDIT_TOOLS = new Set(['edit_file', 'update_file', 'create_file', 'write_file']);
166
+ export const DELETE_TOOLS = new Set(['delete_file', 'remove_file']);
167
+ const PERM_DESC = {
168
+ delete_file: 'delete this file',
169
+ update_file: 'edit this file',
170
+ create_file: 'create this file',
171
+ edit_file: 'create this file',
172
+ move_file: 'move this file',
173
+ run_command: 'run in shell',
174
+ git_commit: 'commit to git',
175
+ };
176
+ export function permissionDesc(toolName) {
177
+ return PERM_DESC[toolName] ?? 'allow this action';
178
+ }
179
+ export function toolLabel(name, args) {
165
180
  const a = args;
166
181
  const short = (s, n = 55) => s.length > n ? s.slice(0, n) + '…' : s;
167
182
  switch (name) {
168
- case 'read_file': return `Reading ${a.path ?? ''}`;
169
- case 'list_files': return `Listing ${a.path || '.'}`;
170
- case 'create_file': return `Creating ${a.path ?? ''}`;
171
- case 'edit_file': return `Writing ${a.path ?? ''}`;
172
- case 'update_file': return `Updating ${a.path ?? ''}`;
173
- case 'delete_file': return `Deleting ${a.path ?? ''}`;
174
- case 'move_file': return `Moving ${a.from} → ${a.to}`;
175
- case 'create_folder': return `Creating folder ${a.path ?? ''}`;
176
- case 'run_command': return `Running ${short(a.command ?? '')}`;
177
- case 'git_status': return 'Checking git status';
178
- case 'git_diff': return 'Reading diff';
179
- case 'git_log': return 'Reading commits';
180
- case 'git_commit': return `Committing: ${short(a.message ?? '')}`;
181
- case 'run_tests': return a.path ? `Running tests › ${a.path}` : 'Running tests';
182
- case 'web_search': return `Searching: ${short(a.query ?? '')}`;
183
- case 'web_extract': return `Extracting page`;
184
- case 'deep_think': return `Researching: ${short(a.query ?? '')}`;
185
- case 'search_codebase': return `Searching codebase: ${short(a.query ?? '')}`;
183
+ case 'read_file': return `Read(${a.path ?? ''})`;
184
+ case 'list_files': return `List(${a.path || '.'})`;
185
+ case 'create_file': return `Create(${a.path ?? ''})`;
186
+ case 'edit_file': return `Create(${a.path ?? ''})`;
187
+ case 'update_file': return `Update(${a.path ?? ''})`;
188
+ case 'delete_file': return `Delete(${a.path ?? ''})`;
189
+ case 'move_file': return `Move(${a.from} → ${a.to})`;
190
+ case 'create_folder': return `Mkdir(${a.path ?? ''})`;
191
+ case 'run_command': return `Run(${short(a.command ?? '')})`;
192
+ case 'git_status': return 'Git(status)';
193
+ case 'git_diff': return 'Git(diff)';
194
+ case 'git_log': return 'Git(log)';
195
+ case 'git_commit': return `Git(commit: ${short(a.message ?? '')})`;
196
+ case 'run_tests': return a.path ? `Test(${a.path})` : 'Test(suite)';
197
+ case 'web_search': return `Search(${short(a.query ?? '')})`;
198
+ case 'web_extract': return `Extract(${Array.isArray(a.urls) ? String(a.urls[0] ?? '') : String(a.urls ?? 'url')})`;
199
+ case 'deep_think': return `Think(${short(a.query ?? '')})`;
200
+ case 'search_codebase': return `Index(${short(a.query ?? '')})`;
186
201
  default: {
187
202
  const s = toolArgSummary(args);
188
203
  return s ? `${name} ${s}` : name;
@@ -192,20 +207,31 @@ function toolLabel(name, args) {
192
207
  export function planSummary(tools) {
193
208
  if (!tools.length)
194
209
  return;
195
- const header = gray(`─ plan (${tools.length} action${tools.length === 1 ? '' : 's'})`);
196
- write(header + '\n');
210
+ const lines = [gray(`─ plan (${tools.length} action${tools.length === 1 ? '' : 's'})`)];
197
211
  for (const t of tools) {
198
212
  const dot = DELETE_TOOLS.has(t.name) ? red('◦') : EDIT_TOOLS.has(t.name) ? green('◦') : blue('◦');
199
- const label = toolLabel(t.name, t.args);
200
- write(` ${dot} ${gray(label)}\n`);
213
+ lines.push(` ${dot} ${gray(toolLabel(t.name, t.args))}`);
201
214
  }
215
+ write(lines.join('\n') + '\n');
202
216
  }
203
217
  const DIFF_CTX = 2;
204
218
  const DIFF_MAX = 40;
205
219
  function printUpdateDiff(filePath, oldText, newText) {
206
220
  const oldLines = oldText.split('\n');
207
221
  const newLines = newText.split('\n');
208
- write(gray(` └ Added ${newLines.length} line${newLines.length !== 1 ? 's' : ''}, removed ${oldLines.length} line${oldLines.length !== 1 ? 's' : ''}\n`));
222
+ const addedCount = newLines.length;
223
+ const removedCount = oldLines.length;
224
+ const parts = [];
225
+ if (addedCount > 0 && removedCount > 0) {
226
+ parts.push(green(`+${addedCount}`), gray(' / '), red(`-${removedCount}`));
227
+ }
228
+ else if (addedCount > 0) {
229
+ parts.push(green(`+${addedCount} line${addedCount !== 1 ? 's' : ''}`));
230
+ }
231
+ else {
232
+ parts.push(red(`-${removedCount} line${removedCount !== 1 ? 's' : ''}`));
233
+ }
234
+ const out = [` ${gray('└')} ${parts.join('')}\n`];
209
235
  let fileLines = [];
210
236
  let lineOffset = 0;
211
237
  try {
@@ -216,47 +242,50 @@ function printUpdateDiff(filePath, oldText, newText) {
216
242
  if (idx >= 0)
217
243
  lineOffset = content.slice(0, idx).split('\n').length - 1;
218
244
  else
219
- fileLines = []; // old text not in file — skip context lines
245
+ fileLines = [];
220
246
  }
221
247
  }
222
248
  catch { }
223
249
  let shown = 0;
224
250
  const ctxStart = Math.max(0, lineOffset - DIFF_CTX);
225
251
  for (let i = ctxStart; i < lineOffset && shown < DIFF_MAX; i++, shown++) {
226
- write(gray(` ${String(i + 1).padStart(4)} ${fileLines[i] ?? ''}\n`));
252
+ out.push(` ${gray(String(i + 1).padStart(4))} ${gray(fileLines[i] ?? '')}\n`);
227
253
  }
228
254
  for (let i = 0; i < oldLines.length && shown < DIFF_MAX; i++, shown++) {
229
- write(` ${gray(String(lineOffset + i + 1).padStart(4))} ${red('- ')}${red(oldLines[i])}\n`);
255
+ out.push(` ${gray(String(lineOffset + i + 1).padStart(4))} ${bgRed(`- ${oldLines[i]}`)}\n`);
230
256
  }
231
257
  for (let i = 0; i < newLines.length && shown < DIFF_MAX; i++, shown++) {
232
- write(` ${gray(String(lineOffset + i + 1).padStart(4))} ${green('+ ')}${green(newLines[i])}\n`);
258
+ out.push(` ${gray(String(lineOffset + i + 1).padStart(4))} ${bgGreen(`+ ${newLines[i]}`)}\n`);
233
259
  }
234
260
  const ctxEnd = Math.min(fileLines.length, lineOffset + oldLines.length + DIFF_CTX);
235
261
  for (let i = lineOffset + oldLines.length; i < ctxEnd && shown < DIFF_MAX; i++, shown++) {
236
- write(gray(` ${String(i + 1).padStart(4)} ${fileLines[i] ?? ''}\n`));
262
+ out.push(` ${gray(String(i + 1).padStart(4))} ${gray(fileLines[i] ?? '')}\n`);
237
263
  }
264
+ return out.join('');
238
265
  }
239
266
  function printEditPreview(content) {
240
267
  const lines = content.split('\n');
241
268
  const visible = lines.slice(0, DIFF_MAX);
242
269
  const hidden = lines.length - visible.length;
243
- write(gray(` └ ${lines.length} line${lines.length !== 1 ? 's' : ''}\n`));
270
+ const out = [` ${gray('')} ${green(`+${lines.length} line${lines.length !== 1 ? 's' : ''}`)}\n`];
244
271
  visible.forEach((line, i) => {
245
- write(` ${gray(String(i + 1).padStart(4))} ${green('+ ')}${green(line)}\n`);
272
+ out.push(` ${gray(String(i + 1).padStart(4))} ${bgGreen(`+ ${line}`)}\n`);
246
273
  });
247
274
  if (hidden > 0)
248
- write(gray(` …${hidden} more line${hidden !== 1 ? 's' : ''}\n`));
275
+ out.push(gray(` …${hidden} more line${hidden !== 1 ? 's' : ''}\n`));
276
+ return out.join('');
249
277
  }
250
278
  export function toolCallStart(name, args) {
251
279
  const dot = DELETE_TOOLS.has(name) ? red('●') : EDIT_TOOLS.has(name) ? green('●') : blue('●');
252
- write(`\n${dot} ${bold(toolLabel(name, args))}\n`);
280
+ let out = `\n${dot} ${bold(toolLabel(name, args))}\n`;
253
281
  const a = args;
254
282
  if (name === 'update_file' && a.old && a.new && a.path) {
255
- printUpdateDiff(a.path, a.old, a.new);
283
+ out += printUpdateDiff(a.path, a.old, a.new);
256
284
  }
257
- else if (name === 'edit_file' && a.content && a.path) {
258
- printEditPreview(a.content);
285
+ else if ((name === 'edit_file' || name === 'create_file') && a.content && a.path) {
286
+ out += printEditPreview(a.content);
259
287
  }
288
+ write(out);
260
289
  }
261
290
  export function toolResultSummary(name, args, result) {
262
291
  const a = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "description": "The high-performance local AI coding agent for your terminal. Automate complex workflows with local LLMs.",
6
6
  "license": "MIT",