miii-cli 0.2.4 → 0.2.5
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 +193 -152
- package/dist/__tests__/integration.test.js +50 -0
- package/dist/config.js +1 -1
- package/dist/init.js +67 -5
- package/dist/skills/loader.js +65 -1
- package/dist/tavily/client.js +64 -0
- package/dist/tools/index.js +34 -1
- package/dist/tui/InputBar.js +89 -316
- package/dist/tui/components/InputArea.js +3 -0
- package/dist/tui/git-context.js +59 -0
- package/dist/tui/hooks/useModelPicker.js +63 -0
- package/dist/tui/hooks/useRunLoop.js +137 -0
- package/dist/tui/hooks/useSession.js +50 -0
- package/dist/tui/printer.js +10 -3
- package/dist/tui/thinking.js +28 -0
- package/package.json +5 -3
- package/dist/tui/App.js +0 -285
- package/dist/tui/components/MessageList.js +0 -127
- package/dist/workers/context.worker.js +0 -71
- package/dist/workers/spawn.js +0 -17
package/dist/tui/InputBar.js
CHANGED
|
@@ -1,53 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useRef
|
|
2
|
+
import { useState, useCallback, useRef } from 'react';
|
|
3
3
|
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { InputArea } from './components/InputArea.js';
|
|
5
5
|
import { ModelPicker } from './components/ModelPicker.js';
|
|
6
6
|
import { Divider } from './components/StatusBar.js';
|
|
7
|
-
import {
|
|
8
|
-
import { listModels, pullModel } from '../llm/ollama.js';
|
|
9
|
-
import { StreamParser, extractBareToolCall } from '../parser/stream-parser.js';
|
|
10
|
-
import { tools, getSystemPrompt } from '../tools/index.js';
|
|
7
|
+
import { tools } from '../tools/index.js';
|
|
11
8
|
import { readFile } from '../files/ops.js';
|
|
12
|
-
import {
|
|
9
|
+
import { generateId } from '../types.js';
|
|
13
10
|
import * as printer from './printer.js';
|
|
14
11
|
import { loadSession, saveSession, listSessions } from '../sessions.js';
|
|
15
|
-
import { shouldCompact, compactContext, fileEditContext } from '../tasks/compactor.js';
|
|
16
12
|
import { MacroQueue, MicroQueue } from '../tasks/queue.js';
|
|
17
13
|
import { TaskExecutor } from '../tasks/executor.js';
|
|
18
|
-
import {
|
|
14
|
+
import { fileEditContext } from '../tasks/compactor.js';
|
|
15
|
+
import { StreamParser } from '../parser/stream-parser.js';
|
|
16
|
+
import { chat } from '../llm/stream.js';
|
|
19
17
|
import { exec } from 'child_process';
|
|
20
18
|
import { promisify } from 'util';
|
|
19
|
+
import { getTavilyKey, saveTavilyKey } from '../tavily/client.js';
|
|
20
|
+
import { getSystemPrompt } from '../tools/index.js';
|
|
21
|
+
import { THINKING_PHRASES, SPARKLE } from './thinking.js';
|
|
22
|
+
import { buildGitContext, looksCodeRelated } from './git-context.js';
|
|
23
|
+
import { useSession } from './hooks/useSession.js';
|
|
24
|
+
import { useModelPicker } from './hooks/useModelPicker.js';
|
|
25
|
+
import { useRunLoop } from './hooks/useRunLoop.js';
|
|
21
26
|
const gitRun = promisify(exec);
|
|
22
|
-
const MAX_TOOL_DEPTH = 6;
|
|
23
|
-
const THINKING_PHRASES = [
|
|
24
|
-
'oh wow, a question. let me pretend to care…',
|
|
25
|
-
'consulting the void…',
|
|
26
|
-
'making something up, just a sec…',
|
|
27
|
-
'definitely not hallucinating right now…',
|
|
28
|
-
'running 47 mental tabs…',
|
|
29
|
-
'staring into the abyss (it blinked)…',
|
|
30
|
-
'calculating your fate, no pressure…',
|
|
31
|
-
'doing the thinking you pay me for…',
|
|
32
|
-
'processing your questionable life choices…',
|
|
33
|
-
'summoning coherent thoughts, rarely works…',
|
|
34
|
-
'asking my imaginary friend for help…',
|
|
35
|
-
'pretending this is a hard problem…',
|
|
36
|
-
'yes, yes, very interesting. anyway…',
|
|
37
|
-
'googling it (not really, I can\'t)…',
|
|
38
|
-
'simulating intelligence… please wait…',
|
|
39
|
-
'having a brief existential crisis…',
|
|
40
|
-
'cross-referencing vibes…',
|
|
41
|
-
'totally not making this up…',
|
|
42
|
-
'the answer is 42. now finding the question…',
|
|
43
|
-
'my other tab is loading…',
|
|
44
|
-
'channelling the spirit of stack overflow…',
|
|
45
|
-
'trying not to confidently be wrong…',
|
|
46
|
-
'applying artificial to the intelligence…',
|
|
47
|
-
'phoning a friend who also doesn\'t know…',
|
|
48
|
-
'checking if this is even my problem to solve…',
|
|
49
|
-
];
|
|
50
|
-
const SPARKLE = ['✦', '✧', '✶', '✷', '✸', '✹'];
|
|
51
27
|
function buildAtContext(text) {
|
|
52
28
|
const refs = [...text.matchAll(/@([\w./\-]+)/g)];
|
|
53
29
|
if (!refs.length)
|
|
@@ -63,255 +39,21 @@ function buildAtContext(text) {
|
|
|
63
39
|
}
|
|
64
40
|
return parts.length ? parts.join('\n\n') + '\n\n' : '';
|
|
65
41
|
}
|
|
66
|
-
|
|
67
|
-
function looksCodeRelated(text) {
|
|
68
|
-
return text.length >= 10 && CODE_PATTERN.test(text);
|
|
69
|
-
}
|
|
70
|
-
async function buildGitContext(cwd, lastStatusRef) {
|
|
71
|
-
try {
|
|
72
|
-
const { stdout } = await gitRun('git status --short', { cwd, timeout: 5000 });
|
|
73
|
-
const status = stdout.trim();
|
|
74
|
-
if (!status || status === lastStatusRef.current)
|
|
75
|
-
return { prefix: '', label: '' };
|
|
76
|
-
lastStatusRef.current = status;
|
|
77
|
-
const MAX_TOTAL = 40_000;
|
|
78
|
-
const MAX_FILE = 15_000;
|
|
79
|
-
let total = 0;
|
|
80
|
-
const parts = [];
|
|
81
|
-
const skipped = [];
|
|
82
|
-
for (const line of status.split('\n')) {
|
|
83
|
-
const code = line.slice(0, 2);
|
|
84
|
-
if (code.includes('D'))
|
|
85
|
-
continue;
|
|
86
|
-
const raw = line.slice(3).trim().replace(/^"|"$/g, '');
|
|
87
|
-
const rel = raw.includes(' -> ') ? raw.split(' -> ')[1] : raw;
|
|
88
|
-
if (!rel)
|
|
89
|
-
continue;
|
|
90
|
-
try {
|
|
91
|
-
const content = readFile(resolve(cwd, rel));
|
|
92
|
-
if (!content || content.length > MAX_FILE) {
|
|
93
|
-
skipped.push(rel);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
total += content.length;
|
|
97
|
-
if (total > MAX_TOTAL) {
|
|
98
|
-
skipped.push(rel);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
parts.push(`<file path="${rel}">\n${content}\n</file>`);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
skipped.push(rel);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (!parts.length && !skipped.length)
|
|
108
|
-
return { prefix: '', label: '' };
|
|
109
|
-
let prefix = '[Auto-context: git-changed files]\n' + parts.join('\n') + '\n';
|
|
110
|
-
if (skipped.length)
|
|
111
|
-
prefix += `Files changed but too large to auto-load: ${skipped.join(', ')}\n`;
|
|
112
|
-
prefix += '\n';
|
|
113
|
-
const label = `auto-loaded ${parts.length} changed file(s)${skipped.length ? `, skipped ${skipped.length} (too large)` : ''}`;
|
|
114
|
-
return { prefix, label };
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return { prefix: '', label: '' };
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
export function InputBar({ config, skills, cwd, session }) {
|
|
42
|
+
export function InputBar({ config, skills, cwd, session, version }) {
|
|
121
43
|
const { stdout } = useStdout();
|
|
122
44
|
const cols = stdout.columns ?? 80;
|
|
123
|
-
const [status, setStatus] = useState('idle');
|
|
124
|
-
const [tick, setTick] = useState(0);
|
|
125
|
-
const [currentModel, setCurrentModel] = useState(config.model);
|
|
126
|
-
const [sessionName, setSessionName] = useState(session);
|
|
127
|
-
const [currentTool, setCurrentTool] = useState();
|
|
128
|
-
const [taskLabel, setTaskLabel] = useState();
|
|
129
45
|
const [planningMode, setPlanningMode] = useState(false);
|
|
130
|
-
// picker opens on mount — force model selection every launch
|
|
131
|
-
const [pickerOpen, setPickerOpen] = useState(true);
|
|
132
|
-
const [pickerModels, setPickerModels] = useState([]);
|
|
133
|
-
const [pickerLoading, setPickerLoading] = useState(false);
|
|
134
|
-
const [pickerError, setPickerError] = useState();
|
|
135
|
-
const [pullState, setPullState] = useState();
|
|
136
|
-
const abortRef = useRef(null);
|
|
137
|
-
const pullAbortRef = useRef(null);
|
|
138
|
-
const thinkingStartRef = useRef(0);
|
|
139
46
|
const macroQueueRef = useRef(new MacroQueue());
|
|
140
47
|
const executorRef = useRef(new TaskExecutor(tools));
|
|
141
|
-
const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`));
|
|
142
|
-
const currentModelRef = useRef(currentModel);
|
|
143
|
-
const sessionNameRef = useRef(sessionName);
|
|
144
|
-
const historyRef = useRef([]);
|
|
145
|
-
const saveTimerRef = useRef(null);
|
|
146
48
|
const lastGitStatusRef = useRef('');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
saveTimerRef.current = setTimeout(() => {
|
|
151
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
152
|
-
saveTimerRef.current = null;
|
|
153
|
-
}, 2000);
|
|
154
|
-
}
|
|
155
|
-
function pushHistory(msg) {
|
|
156
|
-
historyRef.current.push(msg);
|
|
157
|
-
if (historyRef.current.length > 100)
|
|
158
|
-
historyRef.current.splice(0, historyRef.current.length - 100);
|
|
159
|
-
scheduleSave();
|
|
160
|
-
}
|
|
161
|
-
useEffect(() => { currentModelRef.current = currentModel; }, [currentModel]);
|
|
162
|
-
useEffect(() => { sessionNameRef.current = sessionName; }, [sessionName]);
|
|
163
|
-
// mount: load session history + fetch models for initial picker
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
const history = loadSession(session);
|
|
166
|
-
historyRef.current = history;
|
|
167
|
-
if (history.length) {
|
|
168
|
-
printer.systemMsg(`resumed "${session}" — ${history.length} messages`);
|
|
169
|
-
}
|
|
170
|
-
setPickerLoading(true);
|
|
171
|
-
listModels(config.baseUrl)
|
|
172
|
-
.then(m => { setPickerModels(m); setPickerLoading(false); })
|
|
173
|
-
.catch(e => { setPickerError(String(e)); setPickerLoading(false); });
|
|
174
|
-
}, []);
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
if (status === 'idle')
|
|
177
|
-
return;
|
|
178
|
-
const t = setInterval(() => setTick(n => n + 1), 80);
|
|
179
|
-
return () => clearInterval(t);
|
|
180
|
-
}, [status]);
|
|
181
|
-
function buildContext(extra) {
|
|
182
|
-
const ctx = [{ role: 'system', content: systemPromptRef.current }];
|
|
183
|
-
ctx.push(...historyRef.current);
|
|
184
|
-
if (extra)
|
|
185
|
-
ctx.push(extra);
|
|
186
|
-
return ctx;
|
|
187
|
-
}
|
|
188
|
-
const runLoop = useCallback(async (contextMsgs, depth = 0, goal) => {
|
|
189
|
-
if (depth >= MAX_TOOL_DEPTH) {
|
|
190
|
-
setStatus('idle');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
setStatus('thinking');
|
|
194
|
-
if (depth === 0)
|
|
195
|
-
thinkingStartRef.current = Date.now();
|
|
196
|
-
// Auto-compact context when local model starts losing the thread
|
|
197
|
-
const msgs = shouldCompact(contextMsgs) ? compactContext(contextMsgs, goal) : contextMsgs;
|
|
198
|
-
abortRef.current = new AbortController();
|
|
199
|
-
await chat({
|
|
200
|
-
provider: config.provider,
|
|
201
|
-
model: currentModelRef.current,
|
|
202
|
-
baseUrl: config.baseUrl,
|
|
203
|
-
messages: msgs,
|
|
204
|
-
signal: abortRef.current.signal,
|
|
205
|
-
async onDone(fullText) {
|
|
206
|
-
const pendingTools = [];
|
|
207
|
-
const parser = new StreamParser();
|
|
208
|
-
for (const item of [...parser.feed(fullText), ...parser.flush()]) {
|
|
209
|
-
if (item.type === 'tool_call')
|
|
210
|
-
pendingTools.push({ name: item.toolName, args: item.toolArgs });
|
|
211
|
-
}
|
|
212
|
-
// Fallback: bare JSON tool call without <tool_call> wrapper
|
|
213
|
-
if (!pendingTools.length) {
|
|
214
|
-
const bare = extractBareToolCall(fullText);
|
|
215
|
-
if (bare)
|
|
216
|
-
pendingTools.push({ name: bare.name, args: bare.args });
|
|
217
|
-
}
|
|
218
|
-
printer.assistantMsg(fullText);
|
|
219
|
-
pushHistory({ role: 'assistant', content: fullText });
|
|
220
|
-
if (!pendingTools.length) {
|
|
221
|
-
// Model printed code as text instead of using tools — nudge it
|
|
222
|
-
const hasFencedCode = /```[\w]*\n[\s\S]{50,}?\n```/.test(fullText);
|
|
223
|
-
if (hasFencedCode && depth < MAX_TOOL_DEPTH - 1) {
|
|
224
|
-
const nudge = {
|
|
225
|
-
role: 'user',
|
|
226
|
-
content: 'You showed code in your response but did not use any file tools. Use edit_file or patch_file to actually write the changes to disk.',
|
|
227
|
-
};
|
|
228
|
-
const next = [...msgs, { role: 'assistant', content: fullText }, nudge];
|
|
229
|
-
await runLoop(next, depth + 1, goal);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
setStatus('idle');
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
setStatus('tool');
|
|
236
|
-
const next = [...msgs, { role: 'assistant', content: fullText }];
|
|
237
|
-
try {
|
|
238
|
-
for (const tc of pendingTools) {
|
|
239
|
-
const tool = tools.find(t => t.name === tc.name);
|
|
240
|
-
setCurrentTool(tc.name);
|
|
241
|
-
if (tool) {
|
|
242
|
-
try {
|
|
243
|
-
const result = await tool.execute(tc.args);
|
|
244
|
-
printer.toolMsg(tc.name, result);
|
|
245
|
-
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
246
|
-
}
|
|
247
|
-
catch (e) {
|
|
248
|
-
const err = `Tool ${tc.name} error: ${e}`;
|
|
249
|
-
printer.errorMsg(err);
|
|
250
|
-
next.push({ role: 'user', content: err });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
255
|
-
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
finally {
|
|
260
|
-
setCurrentTool(undefined);
|
|
261
|
-
}
|
|
262
|
-
await runLoop(next, depth + 1, goal);
|
|
263
|
-
},
|
|
264
|
-
onError(err) {
|
|
265
|
-
if (err.name !== 'AbortError')
|
|
266
|
-
printer.errorMsg(err.message);
|
|
267
|
-
setStatus('idle');
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
}, [config]);
|
|
271
|
-
// ─── model picker ──────────────────────────────────────────────────────────
|
|
272
|
-
const openPicker = useCallback(async () => {
|
|
273
|
-
setPickerOpen(true);
|
|
274
|
-
setPickerLoading(true);
|
|
275
|
-
setPickerError(undefined);
|
|
276
|
-
try {
|
|
277
|
-
setPickerModels(await listModels(config.baseUrl));
|
|
278
|
-
}
|
|
279
|
-
catch (e) {
|
|
280
|
-
setPickerError(String(e));
|
|
281
|
-
}
|
|
282
|
-
finally {
|
|
283
|
-
setPickerLoading(false);
|
|
284
|
-
}
|
|
285
|
-
}, [config.baseUrl]);
|
|
286
|
-
const handleModelSelect = useCallback((name) => {
|
|
287
|
-
setCurrentModel(name);
|
|
288
|
-
currentModelRef.current = name;
|
|
289
|
-
setPickerOpen(false);
|
|
290
|
-
printer.systemMsg(`model → ${name}`);
|
|
291
|
-
}, []);
|
|
292
|
-
const handleModelPull = useCallback(async (name) => {
|
|
293
|
-
setPullState({ name, status: 'starting...', pct: undefined });
|
|
294
|
-
pullAbortRef.current = new AbortController();
|
|
295
|
-
try {
|
|
296
|
-
await pullModel(config.baseUrl, name, (s, p) => setPullState({ name, status: s, pct: p }), pullAbortRef.current.signal);
|
|
297
|
-
setPickerModels(await listModels(config.baseUrl));
|
|
298
|
-
setPullState(undefined);
|
|
299
|
-
setCurrentModel(name);
|
|
300
|
-
currentModelRef.current = name;
|
|
301
|
-
setPickerOpen(false);
|
|
302
|
-
printer.systemMsg(`pulled ${name} → active`);
|
|
303
|
-
}
|
|
304
|
-
catch (e) {
|
|
305
|
-
setPullState(undefined);
|
|
306
|
-
setPickerError(`pull failed: ${e}`);
|
|
307
|
-
}
|
|
308
|
-
}, [config.baseUrl]);
|
|
49
|
+
const { setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, buildContext, } = useSession(session, cwd, config);
|
|
50
|
+
const { currentModel, setCurrentModel, currentModelRef, pickerOpen, setPickerOpen, pickerModels, pickerLoading, pickerError, pullState, openPicker, handleModelSelect, handleModelPull, } = useModelPicker(config);
|
|
51
|
+
const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, abortRef, runLoop, handleAbort, } = useRunLoop(config, currentModelRef, pushHistory);
|
|
309
52
|
// ─── refactor ─────────────────────────────────────────────────────────────
|
|
310
53
|
const runRefactor = useCallback(async (goal) => {
|
|
311
54
|
printer.systemMsg(`refactor: ${goal}`);
|
|
312
55
|
setTaskLabel(`planning: ${goal}`);
|
|
313
56
|
setStatus('thinking');
|
|
314
|
-
// Phase 1 — planning: ask model to list files and describe changes
|
|
315
57
|
const planCtx = [
|
|
316
58
|
{ role: 'system', content: systemPromptRef.current },
|
|
317
59
|
{
|
|
@@ -336,7 +78,6 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
336
78
|
return;
|
|
337
79
|
}
|
|
338
80
|
printer.assistantMsg(planText);
|
|
339
|
-
// Parse FILE:/CHANGE: pairs from plan
|
|
340
81
|
const filePlan = [];
|
|
341
82
|
const lines = planText.split('\n');
|
|
342
83
|
let lastPath = '';
|
|
@@ -357,9 +98,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
357
98
|
return;
|
|
358
99
|
}
|
|
359
100
|
printer.systemMsg(`plan: ${filePlan.length} file(s) to change`);
|
|
360
|
-
// Phase 2 — execute via macro/micro queue
|
|
361
101
|
const micro = new MicroQueue();
|
|
362
|
-
// P1: read all files in parallel
|
|
363
102
|
for (const fp of filePlan) {
|
|
364
103
|
const t = { id: `read:${fp.path}`, priority: 1, tool: 'read_file', args: { path: fp.path }, deps: [], status: 'pending' };
|
|
365
104
|
micro.push(t);
|
|
@@ -373,13 +112,12 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
373
112
|
};
|
|
374
113
|
macroQueueRef.current.enqueue(macro);
|
|
375
114
|
setTaskLabel(`reading ${filePlan.length} file(s)…`);
|
|
376
|
-
const readResults = await executorRef.current.drain(micro, ({ task,
|
|
115
|
+
const readResults = await executorRef.current.drain(micro, ({ task, error }) => {
|
|
377
116
|
if (error)
|
|
378
117
|
printer.errorMsg(`read failed: ${task.args.path} — ${error}`);
|
|
379
118
|
else
|
|
380
119
|
printer.systemMsg(`read: ${task.args.path}`);
|
|
381
120
|
});
|
|
382
|
-
// Phase 3 — per-file LLM call with isolated context → patch
|
|
383
121
|
setTaskLabel(`applying changes…`);
|
|
384
122
|
const writeMicro = new MicroQueue();
|
|
385
123
|
for (const fp of filePlan) {
|
|
@@ -391,7 +129,6 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
391
129
|
}
|
|
392
130
|
setCurrentTool(`edit ${fp.path}`);
|
|
393
131
|
setTaskLabel(`editing: ${fp.path}`);
|
|
394
|
-
// Isolated context per file keeps model focused
|
|
395
132
|
const editCtx = fileEditContext(systemPromptRef.current, goal, fp.path, fileContent, fp.change);
|
|
396
133
|
let editText = '';
|
|
397
134
|
await chat({
|
|
@@ -406,7 +143,6 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
406
143
|
if (!editText)
|
|
407
144
|
continue;
|
|
408
145
|
printer.assistantMsg(editText);
|
|
409
|
-
// Queue write tasks from LLM's tool calls (P2)
|
|
410
146
|
const parser = new StreamParser();
|
|
411
147
|
for (const item of [...parser.feed(editText), ...parser.flush()]) {
|
|
412
148
|
if (item.type === 'tool_call') {
|
|
@@ -414,7 +150,6 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
414
150
|
}
|
|
415
151
|
}
|
|
416
152
|
}
|
|
417
|
-
// Execute all writes
|
|
418
153
|
if (writeMicro.size > 0) {
|
|
419
154
|
setTaskLabel(`writing ${writeMicro.size} change(s)…`);
|
|
420
155
|
await executorRef.current.drain(writeMicro, ({ task, result, error }) => {
|
|
@@ -444,28 +179,21 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
444
179
|
return e.message ?? String(e);
|
|
445
180
|
}
|
|
446
181
|
};
|
|
447
|
-
// /git or /git status
|
|
448
182
|
if (!sub || sub === 'status') {
|
|
449
|
-
|
|
450
|
-
printer.systemMsg(out);
|
|
183
|
+
printer.systemMsg(await git('status'));
|
|
451
184
|
return;
|
|
452
185
|
}
|
|
453
|
-
// /git log [n]
|
|
454
186
|
if (sub === 'log' || sub.startsWith('log ')) {
|
|
455
187
|
const n = parseInt(sub.split(' ')[1] ?? '10', 10) || 10;
|
|
456
|
-
|
|
457
|
-
printer.systemMsg(out);
|
|
188
|
+
printer.systemMsg(await git(`log --oneline --decorate -${Math.min(n, 50)}`));
|
|
458
189
|
return;
|
|
459
190
|
}
|
|
460
|
-
// /git diff [--staged] [file]
|
|
461
191
|
if (sub === 'diff' || sub.startsWith('diff ')) {
|
|
462
192
|
const args = sub.slice(4).trim();
|
|
463
193
|
const out = await git(`diff ${args}`.trim());
|
|
464
|
-
|
|
465
|
-
printer.systemMsg(display || '(no diff)');
|
|
194
|
+
printer.systemMsg(out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out || '(no diff)');
|
|
466
195
|
return;
|
|
467
196
|
}
|
|
468
|
-
// /git review — inject diff into context, ask model to review
|
|
469
197
|
if (sub === 'review') {
|
|
470
198
|
const diff = await git('diff HEAD');
|
|
471
199
|
const staged = await git('diff --staged');
|
|
@@ -481,40 +209,91 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
481
209
|
await runLoop(buildContext());
|
|
482
210
|
return;
|
|
483
211
|
}
|
|
484
|
-
// /git branch
|
|
485
212
|
if (sub === 'branch' || sub.startsWith('branch ')) {
|
|
486
|
-
|
|
487
|
-
const out = await git(`branch ${args}`.trim());
|
|
488
|
-
printer.systemMsg(out || '(done)');
|
|
213
|
+
printer.systemMsg(await git(`branch ${sub.slice(6).trim()}`.trim()) || '(done)');
|
|
489
214
|
return;
|
|
490
215
|
}
|
|
491
|
-
// /git commit <msg>
|
|
492
216
|
if (sub.startsWith('commit ')) {
|
|
493
217
|
const msg = sub.slice(7).trim();
|
|
494
218
|
if (!msg) {
|
|
495
219
|
printer.systemMsg('usage: /git commit <message>');
|
|
496
220
|
return;
|
|
497
221
|
}
|
|
498
|
-
const
|
|
499
|
-
if (!
|
|
222
|
+
const gitStatus = await git('status --short');
|
|
223
|
+
if (!gitStatus || gitStatus === '(clean — no changes)') {
|
|
500
224
|
printer.systemMsg('nothing to commit — working tree clean');
|
|
501
225
|
return;
|
|
502
226
|
}
|
|
503
|
-
printer.systemMsg(`staging and committing:\n${
|
|
227
|
+
printer.systemMsg(`staging and committing:\n${gitStatus}`);
|
|
504
228
|
const stageOut = await git('add -A');
|
|
505
229
|
if (stageOut)
|
|
506
230
|
printer.systemMsg(stageOut);
|
|
507
|
-
|
|
508
|
-
printer.systemMsg(commitOut);
|
|
231
|
+
printer.systemMsg(await git(`commit -m ${JSON.stringify(msg)}`));
|
|
509
232
|
return;
|
|
510
233
|
}
|
|
511
|
-
|
|
512
|
-
const out = await git(sub);
|
|
513
|
-
printer.systemMsg(out || '(done)');
|
|
234
|
+
printer.systemMsg(await git(sub) || '(done)');
|
|
514
235
|
}, []);
|
|
515
236
|
// ─── submit ────────────────────────────────────────────────────────────────
|
|
516
237
|
const handleSubmit = useCallback(async (text) => {
|
|
517
238
|
const cmd = text.trim();
|
|
239
|
+
if (cmd === '/version') {
|
|
240
|
+
printer.systemMsg(`miii v${version ?? 'unknown'}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (cmd === '/tavily-key' || cmd.startsWith('/tavily-key ')) {
|
|
244
|
+
const key = cmd.slice(11).trim();
|
|
245
|
+
if (!key) {
|
|
246
|
+
const existing = getTavilyKey();
|
|
247
|
+
printer.systemMsg(existing ? 'Tavily key set (use /tavily-key <key> to update)' : 'No Tavily key set. Usage: /tavily-key tvly-...');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!key.startsWith('tvly-')) {
|
|
251
|
+
printer.systemMsg('Key should start with tvly-. Get yours at https://tavily.com');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
saveTavilyKey(key);
|
|
255
|
+
printer.systemMsg('Tavily API key saved to ~/.config/miii/tavily.key (mode 600)');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (cmd === '/skills' || cmd.startsWith('/skills ')) {
|
|
259
|
+
const sub = cmd.slice(7).trim();
|
|
260
|
+
if (!sub || sub === 'list') {
|
|
261
|
+
const pkgs = skills.listNpmSkills();
|
|
262
|
+
printer.systemMsg(pkgs.length ? `installed npm skills:\n${pkgs.map(p => ` ${p}`).join('\n')}` : 'no npm skills installed — try /skills install <name>');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (sub.startsWith('install ')) {
|
|
266
|
+
const pkg = sub.slice(8).trim();
|
|
267
|
+
if (!pkg) {
|
|
268
|
+
printer.systemMsg('usage: /skills install <name> (e.g. /skills install git-summary)');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
printer.systemMsg(`installing miii-skill-${pkg}…`);
|
|
272
|
+
try {
|
|
273
|
+
printer.systemMsg(await skills.installSkill(pkg));
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
printer.errorMsg(String(e));
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (sub.startsWith('uninstall ')) {
|
|
281
|
+
const pkg = sub.slice(10).trim();
|
|
282
|
+
if (!pkg) {
|
|
283
|
+
printer.systemMsg('usage: /skills uninstall <name>');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
printer.systemMsg(await skills.uninstallSkill(pkg));
|
|
288
|
+
}
|
|
289
|
+
catch (e) {
|
|
290
|
+
printer.errorMsg(String(e));
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
printer.systemMsg('usage: /skills install <name> | /skills uninstall <name> | /skills list');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
518
297
|
if (cmd === '/model' || cmd.startsWith('/model ')) {
|
|
519
298
|
const name = cmd.slice(6).trim();
|
|
520
299
|
if (!name) {
|
|
@@ -522,6 +301,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
522
301
|
return;
|
|
523
302
|
}
|
|
524
303
|
setCurrentModel(name);
|
|
304
|
+
currentModelRef.current = name;
|
|
525
305
|
printer.systemMsg(`model → ${name}`);
|
|
526
306
|
return;
|
|
527
307
|
}
|
|
@@ -555,8 +335,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
555
335
|
process.exit(0);
|
|
556
336
|
}
|
|
557
337
|
if (cmd === '/git' || cmd.startsWith('/git ')) {
|
|
558
|
-
|
|
559
|
-
await handleGit(sub);
|
|
338
|
+
await handleGit(cmd.slice(4).trim());
|
|
560
339
|
return;
|
|
561
340
|
}
|
|
562
341
|
if (cmd.startsWith('/refactor ') || cmd === '/refactor') {
|
|
@@ -572,9 +351,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
572
351
|
const topic = cmd.slice(5).trim();
|
|
573
352
|
setPlanningMode(true);
|
|
574
353
|
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}\n- MODE: Planning assistant. Help the user plan step by step. Ask clarifying questions. Suggest concrete next steps. Use plain text only — no markdown, no headers, no bold, no bullets with asterisks, no backtick blocks. Use numbered lists and plain indentation for structure.`);
|
|
575
|
-
const msg = topic
|
|
576
|
-
? `I want to plan: ${topic}`
|
|
577
|
-
: 'I want to start planning. Help me think through my goals step by step.';
|
|
354
|
+
const msg = topic ? `I want to plan: ${topic}` : 'I want to start planning. Help me think through my goals step by step.';
|
|
578
355
|
printer.userMsg(msg);
|
|
579
356
|
pushHistory({ role: 'user', content: msg });
|
|
580
357
|
await runLoop(buildContext());
|
|
@@ -667,13 +444,9 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
667
444
|
pushHistory({ role: 'user', content: gitPrefix + contextPrefix + text });
|
|
668
445
|
await runLoop(buildContext());
|
|
669
446
|
}, [skills, runLoop, openPicker]);
|
|
670
|
-
const handleAbort = useCallback(() => {
|
|
671
|
-
abortRef.current?.abort();
|
|
672
|
-
setStatus('idle');
|
|
673
|
-
}, []);
|
|
674
447
|
const skillList = skills.list();
|
|
675
448
|
// ─── render ────────────────────────────────────────────────────────────────
|
|
676
|
-
return (_jsxs(Box, { flexDirection: "column", children: [pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false);
|
|
449
|
+
return (_jsxs(Box, { flexDirection: "column", children: [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 })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "miii" }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
|
|
677
450
|
? _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: [SPARKLE[tick % SPARKLE.length], " "] }), _jsx(Text, { color: "gray", dimColor: true, italic: true, children: THINKING_PHRASES[Math.floor(tick / 62) % THINKING_PHRASES.length] })] })
|
|
678
451
|
: _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [Math.floor((Date.now() - thinkingStartRef.current) / 1000), "s"] }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] })] }), _jsx(Divider, { cols: cols })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, onSubmit: handleSubmit, onAbort: handleAbort })] }));
|
|
679
452
|
}
|
|
@@ -12,6 +12,9 @@ const BUILTIN_COMMANDS = [
|
|
|
12
12
|
{ ns: 'builtin', name: 'session', description: 'switch session /session <name>' },
|
|
13
13
|
{ ns: 'builtin', name: 'exit', description: 'exit miii' },
|
|
14
14
|
{ ns: 'builtin', name: 'model', description: 'switch model mid-session /model <name>' },
|
|
15
|
+
{ ns: 'builtin', name: 'version', description: 'show current miii version' },
|
|
16
|
+
{ ns: 'builtin', name: 'tavily-key', description: 'set Tavily API key for web search /tavily-key tvly-...' },
|
|
17
|
+
{ ns: 'builtin', name: 'skills', description: 'install/uninstall/list npm skills /skills install <name>' },
|
|
15
18
|
{ ns: 'builtin', name: 'list', description: 'list all loaded skills' },
|
|
16
19
|
{ ns: 'builtin', name: 'plan', description: 'start planning mode /plan [topic]' },
|
|
17
20
|
{ ns: 'builtin', name: 'refactor', description: 'multi-file AI refactor /refactor <goal>' },
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFile } from '../files/ops.js';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
const gitRun = promisify(exec);
|
|
6
|
+
const CODE_PATTERN = /\.(ts|js|tsx|jsx|py|go|rs|java|rb|sh|css|html|json|yaml|yml)\b|function|class|import|export|const|let|var|def |async|await|error|bug|fix|refactor|implement|`[^`]+`/i;
|
|
7
|
+
export function looksCodeRelated(text) {
|
|
8
|
+
return text.length >= 10 && CODE_PATTERN.test(text);
|
|
9
|
+
}
|
|
10
|
+
export async function buildGitContext(cwd, lastStatusRef) {
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await gitRun('git status --short', { cwd, timeout: 5000 });
|
|
13
|
+
const status = stdout.trim();
|
|
14
|
+
if (!status || status === lastStatusRef.current)
|
|
15
|
+
return { prefix: '', label: '' };
|
|
16
|
+
lastStatusRef.current = status;
|
|
17
|
+
const MAX_TOTAL = 40_000;
|
|
18
|
+
const MAX_FILE = 15_000;
|
|
19
|
+
let total = 0;
|
|
20
|
+
const parts = [];
|
|
21
|
+
const skipped = [];
|
|
22
|
+
for (const line of status.split('\n')) {
|
|
23
|
+
const code = line.slice(0, 2);
|
|
24
|
+
if (code.includes('D'))
|
|
25
|
+
continue;
|
|
26
|
+
const raw = line.slice(3).trim().replace(/^"|"$/g, '');
|
|
27
|
+
const rel = raw.includes(' -> ') ? raw.split(' -> ')[1] : raw;
|
|
28
|
+
if (!rel)
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
const content = readFile(resolve(cwd, rel));
|
|
32
|
+
if (!content || content.length > MAX_FILE) {
|
|
33
|
+
skipped.push(rel);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
total += content.length;
|
|
37
|
+
if (total > MAX_TOTAL) {
|
|
38
|
+
skipped.push(rel);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
parts.push(`<file path="${rel}">\n${content}\n</file>`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
skipped.push(rel);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!parts.length && !skipped.length)
|
|
48
|
+
return { prefix: '', label: '' };
|
|
49
|
+
let prefix = '[Auto-context: git-changed files]\n' + parts.join('\n') + '\n';
|
|
50
|
+
if (skipped.length)
|
|
51
|
+
prefix += `Files changed but too large to auto-load: ${skipped.join(', ')}\n`;
|
|
52
|
+
prefix += '\n';
|
|
53
|
+
const label = `auto-loaded ${parts.length} changed file(s)${skipped.length ? `, skipped ${skipped.length} (too large)` : ''}`;
|
|
54
|
+
return { prefix, label };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { prefix: '', label: '' };
|
|
58
|
+
}
|
|
59
|
+
}
|