miii-cli 1.2.4 → 1.3.1
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/README.md +159 -127
- package/dist/config.js +1 -1
- package/dist/init.js +47 -2
- package/dist/llm/stream.js +181 -18
- package/dist/mcp/client.js +8 -1
- package/dist/memory/extractor.js +34 -3
- package/dist/skills/loader.js +6 -2
- package/dist/tasks/compactor.js +4 -1
- package/dist/tools/index.js +73 -20
- package/dist/tui/InputBar.js +2 -2
- package/dist/tui/components/ConfigPicker.js +12 -2
- package/dist/tui/components/InputArea.js +15 -4
- package/dist/tui/deepThink.js +0 -1
- package/dist/tui/hooks/useRefactor.js +4 -3
- package/dist/tui/hooks/useRunLoop.js +158 -78
- package/dist/tui/hooks/useSession.js +2 -2
- package/dist/tui/hooks/useSubmit.js +12 -0
- package/dist/tui/printer.js +7 -6
- package/package.json +1 -1
|
@@ -20,14 +20,15 @@ export function useRefactor(deps) {
|
|
|
20
20
|
content: `Refactor goal: ${goal}\n\nList every file that needs to change. For each file output:\nFILE: <path>\nCHANGE: <one sentence describing the edit>\n\nUse list_files and read_file to discover relevant files first. Only list files that genuinely need changes.`,
|
|
21
21
|
},
|
|
22
22
|
];
|
|
23
|
-
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
abortRef.current = controller;
|
|
24
25
|
let planText = '';
|
|
25
26
|
await chat({
|
|
26
27
|
provider: config.provider,
|
|
27
28
|
model: currentModelRef.current,
|
|
28
29
|
baseUrl: config.baseUrl,
|
|
29
30
|
messages: planCtx,
|
|
30
|
-
signal:
|
|
31
|
+
signal: controller.signal,
|
|
31
32
|
async onDone(text) { planText = text; },
|
|
32
33
|
onError(err) { printer.errorMsg(err.message); },
|
|
33
34
|
});
|
|
@@ -95,7 +96,7 @@ export function useRefactor(deps) {
|
|
|
95
96
|
model: currentModelRef.current,
|
|
96
97
|
baseUrl: config.baseUrl,
|
|
97
98
|
messages: editCtx,
|
|
98
|
-
signal:
|
|
99
|
+
signal: controller.signal,
|
|
99
100
|
async onDone(text) { editText = text; },
|
|
100
101
|
onError(err) { printer.errorMsg(`edit LLM error: ${err.message}`); },
|
|
101
102
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
const runCmd = promisify(exec);
|
|
3
6
|
import { chat } from '../../llm/stream.js';
|
|
4
7
|
import { tools as staticTools } from '../../tools/index.js';
|
|
5
8
|
import { StreamParser, extractBareToolCall } from '../../parser/stream-parser.js';
|
|
@@ -10,6 +13,7 @@ const FILE_EDIT_TOOLS = new Set(['edit_file', 'create_file', 'update_file', 'del
|
|
|
10
13
|
const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
|
|
11
14
|
const PERMISSION_TOOLS = new Set(['edit_file', 'update_file', 'delete_file', 'create_file', 'move_file', 'run_command', 'git_commit']);
|
|
12
15
|
const CHECKPOINT_TOOLS = new Set(['edit_file', 'update_file', 'create_file', 'delete_file']);
|
|
16
|
+
const PARALLEL_SAFE = new Set(['read_file', 'list_files', 'git_status', 'git_log', 'git_diff', 'web_search', 'web_extract']);
|
|
13
17
|
// Tool result messages that are ephemeral — never worth storing in memory or compact summaries
|
|
14
18
|
const EPHEMERAL_PATTERN = /^Tool (read_file|list_files|run_tests) result:|^\[current state of|^\[Context compacted|^\[file updated:/;
|
|
15
19
|
export function stripEphemeral(messages) {
|
|
@@ -23,6 +27,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
23
27
|
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
24
28
|
const permissionResolveRef = useRef(null);
|
|
25
29
|
const checkpointRef = useRef(new Map());
|
|
30
|
+
const autoBranchedRef = useRef(null);
|
|
26
31
|
const sessionApprovedRef = useRef(new Set());
|
|
27
32
|
const thinkingStartRef = useRef(0);
|
|
28
33
|
const extraToolsRef = useRef(extraTools);
|
|
@@ -42,7 +47,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
42
47
|
const t = setInterval(() => setTick(n => n + 1), 80);
|
|
43
48
|
return () => clearInterval(t);
|
|
44
49
|
}, [status]);
|
|
45
|
-
const runLoop = useCallback(async (contextMsgs, depth = 0, goal) => {
|
|
50
|
+
const runLoop = useCallback(async (contextMsgs, depth = 0, goal, options) => {
|
|
46
51
|
if (depth >= MAX_TOOL_DEPTH) {
|
|
47
52
|
abortRef.current = null;
|
|
48
53
|
setStatus('idle');
|
|
@@ -52,7 +57,9 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
52
57
|
if (depth === 0) {
|
|
53
58
|
thinkingStartRef.current = Date.now();
|
|
54
59
|
checkpointRef.current.clear();
|
|
60
|
+
autoBranchedRef.current = null;
|
|
55
61
|
}
|
|
62
|
+
abortRef.current = new AbortController();
|
|
56
63
|
let msgs = contextMsgs;
|
|
57
64
|
if (shouldCompact(contextMsgs)) {
|
|
58
65
|
printer.systemMsg('compacting context…');
|
|
@@ -62,17 +69,31 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
62
69
|
model: currentModelRef.current,
|
|
63
70
|
baseUrl: config.baseUrl,
|
|
64
71
|
apiKey: config.apiKey,
|
|
65
|
-
}, goal);
|
|
72
|
+
}, goal, abortRef.current.signal);
|
|
73
|
+
if (abortRef.current.signal.aborted) {
|
|
74
|
+
setStatus('idle');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
66
77
|
printer.systemMsg(`compacted: ${contextMsgs.length} → ${msgs.length} messages`);
|
|
67
78
|
replaceHistoryRef.current?.(msgs.filter(m => m.role !== 'system'));
|
|
68
79
|
}
|
|
69
|
-
|
|
80
|
+
let didStream = false;
|
|
70
81
|
await chat({
|
|
71
82
|
provider: config.provider,
|
|
72
83
|
model: currentModelRef.current,
|
|
73
84
|
baseUrl: config.baseUrl,
|
|
85
|
+
apiKey: config.apiKey,
|
|
74
86
|
messages: msgs,
|
|
87
|
+
tools: config.provider !== 'ollama' && !(options?.noTools && depth === 0) ? [...staticTools, ...extraToolsRef.current] : undefined,
|
|
88
|
+
toolChoice: (options?.noTools && depth === 0) ? 'none' : undefined,
|
|
75
89
|
signal: abortRef.current.signal,
|
|
90
|
+
onChunk: config.streaming ? (chunk) => {
|
|
91
|
+
if (!didStream) {
|
|
92
|
+
printer.streamStart();
|
|
93
|
+
didStream = true;
|
|
94
|
+
}
|
|
95
|
+
printer.streamChunk(chunk);
|
|
96
|
+
} : undefined,
|
|
76
97
|
onRetry(attempt, max, delayMs) {
|
|
77
98
|
printer.systemMsg(`retry ${attempt}/${max} — waiting ${Math.round(delayMs / 1000)}s`);
|
|
78
99
|
},
|
|
@@ -91,8 +112,10 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
91
112
|
if (bare)
|
|
92
113
|
pendingTools.push({ name: bare.name, args: bare.args });
|
|
93
114
|
}
|
|
115
|
+
if (didStream)
|
|
116
|
+
printer.streamEnd();
|
|
94
117
|
const displayText = textParts.join('').trim();
|
|
95
|
-
if (displayText)
|
|
118
|
+
if (displayText && !didStream)
|
|
96
119
|
printer.assistantMsg(displayText);
|
|
97
120
|
pushHistoryRef.current({ role: 'assistant', content: fullText });
|
|
98
121
|
if (pendingTools.length)
|
|
@@ -107,108 +130,161 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
107
130
|
await runLoop([...msgs, { role: 'assistant', content: fullText }, nudge], depth + 1, goal);
|
|
108
131
|
return;
|
|
109
132
|
}
|
|
133
|
+
if (autoBranchedRef.current)
|
|
134
|
+
printer.systemMsg(`branch: ${autoBranchedRef.current} (git checkout main when done)`);
|
|
110
135
|
printer.systemMsg(`done in ${printer.formatElapsed(Date.now() - thinkingStartRef.current)}`);
|
|
111
136
|
setStatus('idle');
|
|
112
137
|
return;
|
|
113
138
|
}
|
|
114
139
|
setStatus('tool');
|
|
115
140
|
const next = [...msgs, { role: 'assistant', content: fullText }];
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
const allParallelSafe = pendingTools.every(tc => PARALLEL_SAFE.has(tc.name));
|
|
142
|
+
if (allParallelSafe && pendingTools.length > 1) {
|
|
143
|
+
try {
|
|
144
|
+
setCurrentTool(pendingTools[0].name);
|
|
118
145
|
const allTools = [...staticTools, ...extraToolsRef.current];
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
const settled = await Promise.allSettled(pendingTools.map(async (tc) => {
|
|
147
|
+
const tool = allTools.find(t => t.name === tc.name);
|
|
148
|
+
printer.toolCallStart(tc.name, tc.args);
|
|
149
|
+
if (!tool)
|
|
150
|
+
throw new Error(`unknown tool: ${tc.name}`);
|
|
151
|
+
const result = await tool.execute(tc.args);
|
|
152
|
+
printer.toolResultSummary(tc.name, tc.args, result);
|
|
153
|
+
if (SHOW_RESULT_TOOLS.has(tc.name))
|
|
154
|
+
printer.toolMsg(tc.name, result);
|
|
155
|
+
return { tc, result };
|
|
156
|
+
}));
|
|
157
|
+
for (const r of settled) {
|
|
158
|
+
if (r.status === 'fulfilled') {
|
|
159
|
+
next.push({ role: 'user', content: `Tool ${r.value.tc.name} result:\n${r.value.result}` });
|
|
126
160
|
}
|
|
127
161
|
else {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
if (decision === 'session')
|
|
134
|
-
sessionApprovedRef.current.add(sessionKey);
|
|
135
|
-
if (decision === 'no') {
|
|
136
|
-
printer.systemMsg(`denied: ${tc.name}`);
|
|
137
|
-
next.push({ role: 'user', content: `Tool ${tc.name} was denied by the user` });
|
|
138
|
-
break;
|
|
162
|
+
const err = `Tool error: ${r.reason}`;
|
|
163
|
+
printer.errorMsg(err);
|
|
164
|
+
next.push({ role: 'user', content: err });
|
|
139
165
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
setCurrentTool(undefined);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
try {
|
|
174
|
+
for (const tc of pendingTools) {
|
|
175
|
+
const allTools = [...staticTools, ...extraToolsRef.current];
|
|
176
|
+
const tool = allTools.find(t => t.name === tc.name);
|
|
177
|
+
setCurrentTool(tc.name);
|
|
178
|
+
if (PERMISSION_TOOLS.has(tc.name)) {
|
|
179
|
+
const sessionKey = tc.name;
|
|
180
|
+
let decision;
|
|
181
|
+
if (sessionApprovedRef.current.has(sessionKey)) {
|
|
182
|
+
decision = 'yes';
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
decision = await new Promise(resolve => {
|
|
186
|
+
permissionResolveRef.current = resolve;
|
|
187
|
+
setPermissionRequest({ toolName: tc.name, args: tc.args });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (decision === 'session')
|
|
191
|
+
sessionApprovedRef.current.add(sessionKey);
|
|
192
|
+
if (decision === 'no') {
|
|
193
|
+
printer.systemMsg(`denied: ${tc.name}`);
|
|
194
|
+
const remaining = pendingTools.slice(pendingTools.indexOf(tc) + 1).map(t => t.name);
|
|
195
|
+
const skippedNote = remaining.length ? ` The following tools were also skipped: ${remaining.join(', ')}.` : '';
|
|
196
|
+
next.push({ role: 'user', content: `Tool ${tc.name} was denied by the user.${skippedNote} Do not retry these tools unless the user explicitly asks.` });
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
// Checkpoint: store pre-execution file state + auto-branch on first edit
|
|
200
|
+
if (CHECKPOINT_TOOLS.has(tc.name)) {
|
|
201
|
+
const path = tc.args.path;
|
|
202
|
+
if (path && !checkpointRef.current.has(path)) {
|
|
203
|
+
try {
|
|
204
|
+
checkpointRef.current.set(path, readFileSync(path, 'utf-8'));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
checkpointRef.current.set(path, null);
|
|
208
|
+
}
|
|
146
209
|
}
|
|
147
|
-
|
|
148
|
-
|
|
210
|
+
if (!autoBranchedRef.current) {
|
|
211
|
+
try {
|
|
212
|
+
const { stdout } = await runCmd('git rev-parse --abbrev-ref HEAD', { timeout: 3000 });
|
|
213
|
+
const branch = stdout.trim();
|
|
214
|
+
if (branch === 'main' || branch === 'master') {
|
|
215
|
+
const ts = new Date().toISOString().slice(0, 16).replace(/[T:]/g, '-');
|
|
216
|
+
const newBranch = `miii/task-${ts}`;
|
|
217
|
+
await runCmd(`git checkout -b ${newBranch}`, { timeout: 5000 });
|
|
218
|
+
autoBranchedRef.current = newBranch;
|
|
219
|
+
printer.systemMsg(`auto-branched: ${newBranch}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
149
223
|
}
|
|
150
224
|
}
|
|
151
225
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
226
|
+
if (tool) {
|
|
227
|
+
try {
|
|
228
|
+
// Guard: for update_file, verify old text still matches before executing.
|
|
229
|
+
// If stale, inject fresh file content and skip — model will retry.
|
|
230
|
+
if (tc.name === 'update_file') {
|
|
231
|
+
const filePath = tc.args.path;
|
|
232
|
+
const oldText = tc.args.old;
|
|
233
|
+
if (filePath && oldText && existsSync(filePath)) {
|
|
234
|
+
const norm = (s) => s.replace(/\r\n/g, '\n');
|
|
235
|
+
const current = readFileSync(filePath, 'utf-8');
|
|
236
|
+
const occurrences = norm(current).split(norm(oldText)).length - 1;
|
|
237
|
+
if (occurrences === 0) {
|
|
238
|
+
printer.errorMsg(`patch stale: old text not found in ${filePath} — injecting fresh content`);
|
|
239
|
+
next.push({ role: 'user', content: `Tool read_file result:\n${current}` });
|
|
240
|
+
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.` });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (occurrences > 1) {
|
|
244
|
+
printer.errorMsg(`patch ambiguous: old text matches ${occurrences} locations in ${filePath} — injecting fresh content`);
|
|
245
|
+
next.push({ role: 'user', content: `Tool read_file result:\n${current}` });
|
|
246
|
+
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.` });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
168
249
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
250
|
+
}
|
|
251
|
+
printer.toolCallStart(tc.name, tc.args);
|
|
252
|
+
const result = await tool.execute(tc.args);
|
|
253
|
+
printer.toolResultSummary(tc.name, tc.args, result);
|
|
254
|
+
if (SHOW_RESULT_TOOLS.has(tc.name))
|
|
255
|
+
printer.toolMsg(tc.name, result);
|
|
256
|
+
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
257
|
+
if (FILE_EDIT_TOOLS.has(tc.name)) {
|
|
258
|
+
const filePath = tc.args.path;
|
|
259
|
+
if (filePath && existsSync(filePath)) {
|
|
260
|
+
const lineCount = readFileSync(filePath, 'utf-8').split('\n').length;
|
|
261
|
+
next.push({ role: 'user', content: `[file updated: ${filePath} — ${lineCount} lines]` });
|
|
174
262
|
}
|
|
175
263
|
}
|
|
176
264
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
printer.toolMsg(tc.name, result);
|
|
182
|
-
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
183
|
-
if (FILE_EDIT_TOOLS.has(tc.name)) {
|
|
184
|
-
const filePath = tc.args.path;
|
|
185
|
-
if (filePath && existsSync(filePath)) {
|
|
186
|
-
const lineCount = readFileSync(filePath, 'utf-8').split('\n').length;
|
|
187
|
-
next.push({ role: 'user', content: `[file updated: ${filePath} — ${lineCount} lines]` });
|
|
188
|
-
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
const err = `Tool ${tc.name} error: ${e}`;
|
|
267
|
+
printer.errorMsg(err);
|
|
268
|
+
next.push({ role: 'user', content: err });
|
|
189
269
|
}
|
|
190
270
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
next.push({ role: 'user', content: err });
|
|
271
|
+
else {
|
|
272
|
+
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
273
|
+
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
195
274
|
}
|
|
196
275
|
}
|
|
197
|
-
else {
|
|
198
|
-
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
199
|
-
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
200
|
-
}
|
|
201
276
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
277
|
+
finally {
|
|
278
|
+
setCurrentTool(undefined);
|
|
279
|
+
}
|
|
280
|
+
} // end sequential else
|
|
206
281
|
// For file-edit turns: slim context (system + goal + fresh file states + recent results)
|
|
207
282
|
// For non-edit turns: full next (model needs full conversational context)
|
|
208
283
|
const didEditFiles = pendingTools.some(tc => FILE_EDIT_TOOLS.has(tc.name));
|
|
209
284
|
if (didEditFiles) {
|
|
210
285
|
const systemMsg = msgs.find(m => m.role === 'system');
|
|
211
|
-
const goalMsg = msgs.find(m => m.role === 'user' && !m.content.startsWith('[') && !m.content.startsWith('Tool '))
|
|
286
|
+
const goalMsg = msgs.find(m => m.role === 'user' && !m.content.startsWith('[') && !m.content.startsWith('Tool '))
|
|
287
|
+
?? (goal ? { role: 'user', content: goal } : undefined);
|
|
212
288
|
const batchStart = msgs.length; // include assistant message so model sees its own tool call on retry
|
|
213
289
|
const batchMsgs = next.slice(batchStart);
|
|
214
290
|
const slimCtx = [
|
|
@@ -257,6 +333,10 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
257
333
|
if (restored > 0)
|
|
258
334
|
printer.systemMsg(`restored ${restored} file(s) to pre-session state`);
|
|
259
335
|
}
|
|
336
|
+
if (autoBranchedRef.current) {
|
|
337
|
+
printer.systemMsg(`task branch preserved: ${autoBranchedRef.current}`);
|
|
338
|
+
autoBranchedRef.current = null;
|
|
339
|
+
}
|
|
260
340
|
setStatus('idle');
|
|
261
341
|
}, []);
|
|
262
342
|
return {
|
|
@@ -9,7 +9,7 @@ const SHORT_MEMORY_SIZE = 50;
|
|
|
9
9
|
function buildSystemPrompt(cwd, facts, extraTools = []) {
|
|
10
10
|
return getSystemPrompt(`\n- CWD: ${cwd}`, extraTools) + formatMemoryBlock(facts);
|
|
11
11
|
}
|
|
12
|
-
export function useSession(initialSession, cwd, config, extraTools = []) {
|
|
12
|
+
export function useSession(initialSession, cwd, config, extraTools = [], currentModelRef) {
|
|
13
13
|
const projectDir = getProjectDir(cwd);
|
|
14
14
|
const [sessionName, setSessionName] = useState(initialSession);
|
|
15
15
|
const sessionNameRef = useRef(initialSession);
|
|
@@ -49,7 +49,7 @@ export function useSession(initialSession, cwd, config, extraTools = []) {
|
|
|
49
49
|
if (historyRef.current.length > SHORT_MEMORY_SIZE && !extractingRef.current) {
|
|
50
50
|
const dropped = historyRef.current.splice(0, historyRef.current.length - SHORT_MEMORY_SIZE);
|
|
51
51
|
extractingRef.current = true;
|
|
52
|
-
extractFacts(dropped, config, config.model).then(newFacts => {
|
|
52
|
+
extractFacts(dropped, config, currentModelRef?.current ?? config.model).then(newFacts => {
|
|
53
53
|
if (newFacts.length) {
|
|
54
54
|
const updated = mergeFacts(longMemoryRef.current, newFacts);
|
|
55
55
|
longMemoryRef.current = updated;
|
|
@@ -326,6 +326,18 @@ Analyze what exists, then implement the design. Use the design system above if a
|
|
|
326
326
|
}
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
|
+
if (cmd.startsWith('/plan exec ')) {
|
|
330
|
+
const task = cmd.slice(11).trim();
|
|
331
|
+
if (!task) {
|
|
332
|
+
printer.systemMsg('usage: /plan exec <task>');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const planPrompt = `PLANNING TURN — output a numbered plan of exactly what you will do to accomplish this task. List which files to read, which to edit, and what changes to make. Do NOT call any tools in this response. After I review the plan and respond "go", you will execute.\n\nTask: ${task}`;
|
|
336
|
+
printer.userMsg(`/plan exec ${task}`);
|
|
337
|
+
pushHistory({ role: 'user', content: planPrompt });
|
|
338
|
+
await runLoop(buildContext(), 0, task, { noTools: true });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
329
341
|
if (cmd === '/plan' || cmd.startsWith('/plan ')) {
|
|
330
342
|
const topic = cmd.slice(5).trim();
|
|
331
343
|
setPlanningMode(true);
|
package/dist/tui/printer.js
CHANGED
|
@@ -81,7 +81,7 @@ export function toolArgSummary(args) {
|
|
|
81
81
|
const first = Object.values(args)[0];
|
|
82
82
|
return first ? truncate(String(first), 60) : '';
|
|
83
83
|
}
|
|
84
|
-
export function welcome(
|
|
84
|
+
export function welcome(cwd, version, updateAvailable, linked) {
|
|
85
85
|
const cols = Math.min(process.stdout.columns ?? 80, 100);
|
|
86
86
|
const innerW = cols - 2;
|
|
87
87
|
const leftW = Math.floor(innerW * 0.44);
|
|
@@ -114,7 +114,6 @@ export function welcome(provider, model, cwd, version, updateAvailable, linked)
|
|
|
114
114
|
'',
|
|
115
115
|
...miniArt,
|
|
116
116
|
'',
|
|
117
|
-
` ${gray(model + ' · ' + provider)}`,
|
|
118
117
|
` ${gray(shortCwd)}`,
|
|
119
118
|
'',
|
|
120
119
|
];
|
|
@@ -162,8 +161,11 @@ export function assistantMsg(text) {
|
|
|
162
161
|
const tail = lines.slice(idx + 1).join('\n');
|
|
163
162
|
write(`\n${blue('●')} ${head}${tail ? '\n' + tail : ''}\n`);
|
|
164
163
|
}
|
|
165
|
-
export
|
|
166
|
-
export
|
|
164
|
+
export function streamStart() { write(`\n${blue('●')} `); }
|
|
165
|
+
export function streamChunk(s) { write(s); }
|
|
166
|
+
export function streamEnd() { write('\n'); }
|
|
167
|
+
export const EDIT_TOOLS = new Set(['edit_file', 'update_file', 'create_file']);
|
|
168
|
+
export const DELETE_TOOLS = new Set(['delete_file']);
|
|
167
169
|
const PERM_DESC = {
|
|
168
170
|
delete_file: 'delete this file',
|
|
169
171
|
update_file: 'edit this file',
|
|
@@ -292,8 +294,7 @@ export function toolResultSummary(name, args, result) {
|
|
|
292
294
|
const lines = result.trim().split('\n').filter(Boolean);
|
|
293
295
|
let summary = '';
|
|
294
296
|
switch (name) {
|
|
295
|
-
case 'edit_file':
|
|
296
|
-
case 'write_file': {
|
|
297
|
+
case 'edit_file': {
|
|
297
298
|
const n = (a.content ?? '').split('\n').length;
|
|
298
299
|
summary = `Wrote ${n} line${n === 1 ? '' : 's'}`;
|
|
299
300
|
break;
|