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.
- package/dist/tools/index.js +20 -12
- package/dist/tui/InputBar.js +2 -2
- package/dist/tui/components/InputArea.js +16 -10
- package/dist/tui/hooks/useRunLoop.js +4 -4
- package/dist/tui/hooks/useSubmit.js +18 -6
- package/dist/tui/printer.js +67 -38
- package/package.json +1 -1
package/dist/tools/index.js
CHANGED
|
@@ -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
|
},
|
package/dist/tui/InputBar.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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 !==
|
|
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
|
|
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}.
|
|
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
|
|
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: ${
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
528
|
-
|
|
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
|
}
|
package/dist/tui/printer.js
CHANGED
|
@@ -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
|
-
|
|
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 `
|
|
169
|
-
case 'list_files': return `
|
|
170
|
-
case 'create_file': return `
|
|
171
|
-
case 'edit_file': return `
|
|
172
|
-
case 'update_file': return `
|
|
173
|
-
case 'delete_file': return `
|
|
174
|
-
case 'move_file': return `
|
|
175
|
-
case 'create_folder': return `
|
|
176
|
-
case 'run_command': return `
|
|
177
|
-
case 'git_status': return '
|
|
178
|
-
case 'git_diff': return '
|
|
179
|
-
case 'git_log': return '
|
|
180
|
-
case 'git_commit': return `
|
|
181
|
-
case 'run_tests': return a.path ? `
|
|
182
|
-
case 'web_search': return `
|
|
183
|
-
case 'web_extract': return `
|
|
184
|
-
case 'deep_think': return `
|
|
185
|
-
case 'search_codebase': return `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 = [];
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
const out = [` ${gray('└')} ${green(`+${lines.length} line${lines.length !== 1 ? 's' : ''}`)}\n`];
|
|
244
271
|
visible.forEach((line, i) => {
|
|
245
|
-
|
|
272
|
+
out.push(` ${gray(String(i + 1).padStart(4))} ${bgGreen(`+ ${line}`)}\n`);
|
|
246
273
|
});
|
|
247
274
|
if (hidden > 0)
|
|
248
|
-
|
|
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
|
-
|
|
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;
|