miii-cli 0.2.1 → 0.2.3
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 +190 -83
- package/dist/config.js +0 -1
- package/dist/files/ops.js +22 -4
- package/dist/index.js +0 -1
- package/dist/init.js +0 -1
- package/dist/llm/ollama.js +0 -1
- package/dist/llm/stream.js +4 -3
- package/dist/parser/stream-parser.js +1 -13
- package/dist/sessions.js +0 -1
- package/dist/skills/loader.js +0 -1
- package/dist/tasks/compactor.js +68 -0
- package/dist/tasks/executor.js +88 -0
- package/dist/tasks/queue.js +72 -0
- package/dist/tools/index.js +108 -5
- package/dist/tui/App.js +0 -1
- package/dist/tui/InputBar.js +379 -32
- package/dist/tui/components/AtPicker.js +0 -1
- package/dist/tui/components/CommandPalette.js +4 -3
- package/dist/tui/components/InputArea.js +25 -13
- package/dist/tui/components/MessageList.js +12 -1
- package/dist/tui/components/ModelPicker.js +0 -1
- package/dist/tui/components/StatusBar.js +0 -1
- package/dist/tui/printer.js +0 -1
- package/dist/types.js +0 -1
- package/dist/workers/context.worker.js +0 -1
- package/dist/workers/spawn.js +0 -1
- package/package.json +6 -3
- package/.claude/settings.local.json +0 -28
- package/CONTRIBUTING.md +0 -55
- package/Makefile +0 -13
- package/dist/config.d.ts +0 -2
- package/dist/config.js.map +0 -1
- package/dist/files/ops.d.ts +0 -14
- package/dist/files/ops.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/init.d.ts +0 -1
- package/dist/init.js.map +0 -1
- package/dist/llm/ollama.d.ts +0 -10
- package/dist/llm/ollama.js.map +0 -1
- package/dist/llm/stream.d.ts +0 -12
- package/dist/llm/stream.js.map +0 -1
- package/dist/parser/stream-parser.d.ts +0 -21
- package/dist/parser/stream-parser.js.map +0 -1
- package/dist/sessions.d.ts +0 -9
- package/dist/sessions.js.map +0 -1
- package/dist/skills/loader.d.ts +0 -23
- package/dist/skills/loader.js.map +0 -1
- package/dist/tools/index.d.ts +0 -8
- package/dist/tools/index.js.map +0 -1
- package/dist/tui/App.d.ts +0 -9
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/InputBar.d.ts +0 -10
- package/dist/tui/InputBar.js.map +0 -1
- package/dist/tui/components/AtPicker.d.ts +0 -8
- package/dist/tui/components/AtPicker.js.map +0 -1
- package/dist/tui/components/CommandPalette.d.ts +0 -8
- package/dist/tui/components/CommandPalette.js.map +0 -1
- package/dist/tui/components/InputArea.d.ts +0 -12
- package/dist/tui/components/InputArea.js.map +0 -1
- package/dist/tui/components/MessageList.d.ts +0 -11
- package/dist/tui/components/MessageList.js.map +0 -1
- package/dist/tui/components/ModelPicker.d.ts +0 -18
- package/dist/tui/components/ModelPicker.js.map +0 -1
- package/dist/tui/components/StatusBar.d.ts +0 -12
- package/dist/tui/components/StatusBar.js.map +0 -1
- package/dist/tui/printer.d.ts +0 -7
- package/dist/tui/printer.js.map +0 -1
- package/dist/types.d.ts +0 -20
- package/dist/types.js.map +0 -1
- package/dist/workers/context.worker.js.map +0 -1
- package/dist/workers/diff.worker.d.ts +0 -1
- package/dist/workers/diff.worker.js +0 -12
- package/dist/workers/diff.worker.js.map +0 -1
- package/dist/workers/spawn.d.ts +0 -1
- package/dist/workers/spawn.js.map +0 -1
- package/install.sh +0 -6
- package/mii-cli.gif +0 -0
- package/src/config.ts +0 -32
- package/src/files/ops.ts +0 -89
- package/src/index.ts +0 -11
- package/src/init.ts +0 -41
- package/src/llm/ollama.ts +0 -110
- package/src/llm/stream.ts +0 -55
- package/src/parser/stream-parser.ts +0 -196
- package/src/sessions.ts +0 -54
- package/src/skills/loader.ts +0 -144
- package/src/tools/index.ts +0 -151
- package/src/tui/App.tsx +0 -355
- package/src/tui/InputBar.tsx +0 -381
- package/src/tui/components/AtPicker.tsx +0 -49
- package/src/tui/components/CommandPalette.tsx +0 -50
- package/src/tui/components/InputArea.tsx +0 -297
- package/src/tui/components/MessageList.tsx +0 -219
- package/src/tui/components/ModelPicker.tsx +0 -134
- package/src/tui/components/StatusBar.tsx +0 -36
- package/src/tui/printer.ts +0 -130
- package/src/types.ts +0 -26
- package/src/workers/context.worker.ts +0 -66
- package/src/workers/diff.worker.ts +0 -20
- package/src/workers/spawn.ts +0 -19
- package/tsconfig.json +0 -18
- /package/dist/{workers/context.worker.d.ts → tasks/types.js} +0 -0
package/dist/tui/InputBar.js
CHANGED
|
@@ -9,8 +9,16 @@ import { listModels, pullModel } from '../llm/ollama.js';
|
|
|
9
9
|
import { StreamParser } from '../parser/stream-parser.js';
|
|
10
10
|
import { tools, getSystemPrompt } from '../tools/index.js';
|
|
11
11
|
import { readFile } from '../files/ops.js';
|
|
12
|
+
import { resolve } from 'path';
|
|
12
13
|
import * as printer from './printer.js';
|
|
13
14
|
import { loadSession, saveSession, listSessions } from '../sessions.js';
|
|
15
|
+
import { shouldCompact, compactContext, fileEditContext } from '../tasks/compactor.js';
|
|
16
|
+
import { MacroQueue, MicroQueue } from '../tasks/queue.js';
|
|
17
|
+
import { TaskExecutor } from '../tasks/executor.js';
|
|
18
|
+
import { generateId } from '../types.js';
|
|
19
|
+
import { exec } from 'child_process';
|
|
20
|
+
import { promisify } from 'util';
|
|
21
|
+
const gitRun = promisify(exec);
|
|
14
22
|
const MAX_TOOL_DEPTH = 6;
|
|
15
23
|
const THINKING_PHRASES = [
|
|
16
24
|
'oh wow, a question. let me pretend to care…',
|
|
@@ -23,6 +31,21 @@ const THINKING_PHRASES = [
|
|
|
23
31
|
'doing the thinking you pay me for…',
|
|
24
32
|
'processing your questionable life choices…',
|
|
25
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…',
|
|
26
49
|
];
|
|
27
50
|
const SPARKLE = ['✦', '✧', '✶', '✷', '✸', '✹'];
|
|
28
51
|
function buildAtContext(text) {
|
|
@@ -40,6 +63,60 @@ function buildAtContext(text) {
|
|
|
40
63
|
}
|
|
41
64
|
return parts.length ? parts.join('\n\n') + '\n\n' : '';
|
|
42
65
|
}
|
|
66
|
+
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;
|
|
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
|
+
}
|
|
43
120
|
export function InputBar({ config, skills, cwd, session }) {
|
|
44
121
|
const { stdout } = useStdout();
|
|
45
122
|
const cols = stdout.columns ?? 80;
|
|
@@ -47,6 +124,8 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
47
124
|
const [tick, setTick] = useState(0);
|
|
48
125
|
const [currentModel, setCurrentModel] = useState(config.model);
|
|
49
126
|
const [sessionName, setSessionName] = useState(session);
|
|
127
|
+
const [currentTool, setCurrentTool] = useState();
|
|
128
|
+
const [taskLabel, setTaskLabel] = useState();
|
|
50
129
|
const [planningMode, setPlanningMode] = useState(false);
|
|
51
130
|
// picker opens on mount — force model selection every launch
|
|
52
131
|
const [pickerOpen, setPickerOpen] = useState(true);
|
|
@@ -56,10 +135,29 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
56
135
|
const [pullState, setPullState] = useState();
|
|
57
136
|
const abortRef = useRef(null);
|
|
58
137
|
const pullAbortRef = useRef(null);
|
|
138
|
+
const thinkingStartRef = useRef(0);
|
|
139
|
+
const macroQueueRef = useRef(new MacroQueue());
|
|
140
|
+
const executorRef = useRef(new TaskExecutor(tools));
|
|
59
141
|
const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`));
|
|
60
142
|
const currentModelRef = useRef(currentModel);
|
|
61
143
|
const sessionNameRef = useRef(sessionName);
|
|
62
144
|
const historyRef = useRef([]);
|
|
145
|
+
const saveTimerRef = useRef(null);
|
|
146
|
+
const lastGitStatusRef = useRef('');
|
|
147
|
+
function scheduleSave() {
|
|
148
|
+
if (saveTimerRef.current)
|
|
149
|
+
clearTimeout(saveTimerRef.current);
|
|
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
|
+
}
|
|
63
161
|
useEffect(() => { currentModelRef.current = currentModel; }, [currentModel]);
|
|
64
162
|
useEffect(() => { sessionNameRef.current = sessionName; }, [sessionName]);
|
|
65
163
|
// mount: load session history + fetch models for initial picker
|
|
@@ -87,18 +185,22 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
87
185
|
ctx.push(extra);
|
|
88
186
|
return ctx;
|
|
89
187
|
}
|
|
90
|
-
const runLoop = useCallback(async (contextMsgs, depth = 0) => {
|
|
188
|
+
const runLoop = useCallback(async (contextMsgs, depth = 0, goal) => {
|
|
91
189
|
if (depth >= MAX_TOOL_DEPTH) {
|
|
92
190
|
setStatus('idle');
|
|
93
191
|
return;
|
|
94
192
|
}
|
|
95
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;
|
|
96
198
|
abortRef.current = new AbortController();
|
|
97
199
|
await chat({
|
|
98
200
|
provider: config.provider,
|
|
99
201
|
model: currentModelRef.current,
|
|
100
202
|
baseUrl: config.baseUrl,
|
|
101
|
-
messages:
|
|
203
|
+
messages: msgs,
|
|
102
204
|
signal: abortRef.current.signal,
|
|
103
205
|
async onDone(fullText) {
|
|
104
206
|
const pendingTools = [];
|
|
@@ -108,34 +210,39 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
108
210
|
pendingTools.push({ name: item.toolName, args: item.toolArgs });
|
|
109
211
|
}
|
|
110
212
|
printer.assistantMsg(fullText);
|
|
111
|
-
|
|
112
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
213
|
+
pushHistory({ role: 'assistant', content: fullText });
|
|
113
214
|
if (!pendingTools.length) {
|
|
114
215
|
setStatus('idle');
|
|
115
216
|
return;
|
|
116
217
|
}
|
|
117
218
|
setStatus('tool');
|
|
118
|
-
const next = [...
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
219
|
+
const next = [...msgs, { role: 'assistant', content: fullText }];
|
|
220
|
+
try {
|
|
221
|
+
for (const tc of pendingTools) {
|
|
222
|
+
const tool = tools.find(t => t.name === tc.name);
|
|
223
|
+
setCurrentTool(tc.name);
|
|
224
|
+
if (tool) {
|
|
225
|
+
try {
|
|
226
|
+
const result = await tool.execute(tc.args);
|
|
227
|
+
printer.toolMsg(tc.name, result);
|
|
228
|
+
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
const err = `Tool ${tc.name} error: ${e}`;
|
|
232
|
+
printer.errorMsg(err);
|
|
233
|
+
next.push({ role: 'user', content: err });
|
|
234
|
+
}
|
|
126
235
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
next.push({ role: 'user', content: err });
|
|
236
|
+
else {
|
|
237
|
+
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
238
|
+
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
131
239
|
}
|
|
132
240
|
}
|
|
133
|
-
else {
|
|
134
|
-
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
135
|
-
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
136
|
-
}
|
|
137
241
|
}
|
|
138
|
-
|
|
242
|
+
finally {
|
|
243
|
+
setCurrentTool(undefined);
|
|
244
|
+
}
|
|
245
|
+
await runLoop(next, depth + 1, goal);
|
|
139
246
|
},
|
|
140
247
|
onError(err) {
|
|
141
248
|
if (err.name !== 'AbortError')
|
|
@@ -182,14 +289,234 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
182
289
|
setPickerError(`pull failed: ${e}`);
|
|
183
290
|
}
|
|
184
291
|
}, [config.baseUrl]);
|
|
292
|
+
// ─── refactor ─────────────────────────────────────────────────────────────
|
|
293
|
+
const runRefactor = useCallback(async (goal) => {
|
|
294
|
+
printer.systemMsg(`refactor: ${goal}`);
|
|
295
|
+
setTaskLabel(`planning: ${goal}`);
|
|
296
|
+
setStatus('thinking');
|
|
297
|
+
// Phase 1 — planning: ask model to list files and describe changes
|
|
298
|
+
const planCtx = [
|
|
299
|
+
{ role: 'system', content: systemPromptRef.current },
|
|
300
|
+
{
|
|
301
|
+
role: 'user',
|
|
302
|
+
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.`,
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
abortRef.current = new AbortController();
|
|
306
|
+
let planText = '';
|
|
307
|
+
await chat({
|
|
308
|
+
provider: config.provider,
|
|
309
|
+
model: currentModelRef.current,
|
|
310
|
+
baseUrl: config.baseUrl,
|
|
311
|
+
messages: planCtx,
|
|
312
|
+
signal: abortRef.current.signal,
|
|
313
|
+
async onDone(text) { planText = text; },
|
|
314
|
+
onError(err) { printer.errorMsg(err.message); },
|
|
315
|
+
});
|
|
316
|
+
if (!planText) {
|
|
317
|
+
setStatus('idle');
|
|
318
|
+
setTaskLabel(undefined);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
printer.assistantMsg(planText);
|
|
322
|
+
// Parse FILE:/CHANGE: pairs from plan
|
|
323
|
+
const filePlan = [];
|
|
324
|
+
const lines = planText.split('\n');
|
|
325
|
+
let lastPath = '';
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
const fm = line.match(/^FILE:\s*(.+)/);
|
|
328
|
+
const cm = line.match(/^CHANGE:\s*(.+)/);
|
|
329
|
+
if (fm)
|
|
330
|
+
lastPath = fm[1].trim();
|
|
331
|
+
if (cm && lastPath) {
|
|
332
|
+
filePlan.push({ path: lastPath, change: cm[1].trim() });
|
|
333
|
+
lastPath = '';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!filePlan.length) {
|
|
337
|
+
printer.systemMsg('no files identified in plan — done');
|
|
338
|
+
setStatus('idle');
|
|
339
|
+
setTaskLabel(undefined);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
printer.systemMsg(`plan: ${filePlan.length} file(s) to change`);
|
|
343
|
+
// Phase 2 — execute via macro/micro queue
|
|
344
|
+
const micro = new MicroQueue();
|
|
345
|
+
// P1: read all files in parallel
|
|
346
|
+
for (const fp of filePlan) {
|
|
347
|
+
const t = { id: `read:${fp.path}`, priority: 1, tool: 'read_file', args: { path: fp.path }, deps: [], status: 'pending' };
|
|
348
|
+
micro.push(t);
|
|
349
|
+
}
|
|
350
|
+
const macro = {
|
|
351
|
+
id: generateId(),
|
|
352
|
+
goal,
|
|
353
|
+
priority: 0,
|
|
354
|
+
microtasks: micro.toArray(),
|
|
355
|
+
status: 'running',
|
|
356
|
+
};
|
|
357
|
+
macroQueueRef.current.enqueue(macro);
|
|
358
|
+
setTaskLabel(`reading ${filePlan.length} file(s)…`);
|
|
359
|
+
const readResults = await executorRef.current.drain(micro, ({ task, result, error }) => {
|
|
360
|
+
if (error)
|
|
361
|
+
printer.errorMsg(`read failed: ${task.args.path} — ${error}`);
|
|
362
|
+
else
|
|
363
|
+
printer.systemMsg(`read: ${task.args.path}`);
|
|
364
|
+
});
|
|
365
|
+
// Phase 3 — per-file LLM call with isolated context → patch
|
|
366
|
+
setTaskLabel(`applying changes…`);
|
|
367
|
+
const writeMicro = new MicroQueue();
|
|
368
|
+
for (const fp of filePlan) {
|
|
369
|
+
const readId = `read:${fp.path}`;
|
|
370
|
+
const fileContent = readResults.get(readId) ?? '';
|
|
371
|
+
if (!fileContent) {
|
|
372
|
+
printer.systemMsg(`skip (unreadable): ${fp.path}`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
setCurrentTool(`edit ${fp.path}`);
|
|
376
|
+
setTaskLabel(`editing: ${fp.path}`);
|
|
377
|
+
// Isolated context per file keeps model focused
|
|
378
|
+
const editCtx = fileEditContext(systemPromptRef.current, goal, fp.path, fileContent, fp.change);
|
|
379
|
+
let editText = '';
|
|
380
|
+
await chat({
|
|
381
|
+
provider: config.provider,
|
|
382
|
+
model: currentModelRef.current,
|
|
383
|
+
baseUrl: config.baseUrl,
|
|
384
|
+
messages: editCtx,
|
|
385
|
+
signal: abortRef.current?.signal,
|
|
386
|
+
async onDone(text) { editText = text; },
|
|
387
|
+
onError(err) { printer.errorMsg(`edit LLM error: ${err.message}`); },
|
|
388
|
+
});
|
|
389
|
+
if (!editText)
|
|
390
|
+
continue;
|
|
391
|
+
printer.assistantMsg(editText);
|
|
392
|
+
// Queue write tasks from LLM's tool calls (P2)
|
|
393
|
+
const parser = new StreamParser();
|
|
394
|
+
for (const item of [...parser.feed(editText), ...parser.flush()]) {
|
|
395
|
+
if (item.type === 'tool_call') {
|
|
396
|
+
writeMicro.push({ id: generateId(), priority: 2, tool: item.toolName, args: item.toolArgs, deps: [], status: 'pending' });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Execute all writes
|
|
401
|
+
if (writeMicro.size > 0) {
|
|
402
|
+
setTaskLabel(`writing ${writeMicro.size} change(s)…`);
|
|
403
|
+
await executorRef.current.drain(writeMicro, ({ task, result, error }) => {
|
|
404
|
+
if (error)
|
|
405
|
+
printer.errorMsg(`${task.tool} failed: ${error}`);
|
|
406
|
+
else
|
|
407
|
+
printer.toolMsg(task.tool, result ?? '');
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
macro.status = 'done';
|
|
411
|
+
macroQueueRef.current.dequeue();
|
|
412
|
+
setCurrentTool(undefined);
|
|
413
|
+
setTaskLabel(undefined);
|
|
414
|
+
setStatus('idle');
|
|
415
|
+
printer.systemMsg(`refactor done — ${filePlan.length} file(s) processed`);
|
|
416
|
+
pushHistory({ role: 'user', content: `[refactor] ${goal}` });
|
|
417
|
+
pushHistory({ role: 'assistant', content: planText });
|
|
418
|
+
}, [config]);
|
|
419
|
+
// ─── git ───────────────────────────────────────────────────────────────────
|
|
420
|
+
const handleGit = useCallback(async (sub) => {
|
|
421
|
+
const git = async (args) => {
|
|
422
|
+
try {
|
|
423
|
+
const { stdout, stderr } = await gitRun(`git ${args}`, { timeout: 15_000 });
|
|
424
|
+
return (stdout + stderr).trim();
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
return e.message ?? String(e);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
// /git or /git status
|
|
431
|
+
if (!sub || sub === 'status') {
|
|
432
|
+
const out = await git('status');
|
|
433
|
+
printer.systemMsg(out);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// /git log [n]
|
|
437
|
+
if (sub === 'log' || sub.startsWith('log ')) {
|
|
438
|
+
const n = parseInt(sub.split(' ')[1] ?? '10', 10) || 10;
|
|
439
|
+
const out = await git(`log --oneline --decorate -${Math.min(n, 50)}`);
|
|
440
|
+
printer.systemMsg(out);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
// /git diff [--staged] [file]
|
|
444
|
+
if (sub === 'diff' || sub.startsWith('diff ')) {
|
|
445
|
+
const args = sub.slice(4).trim();
|
|
446
|
+
const out = await git(`diff ${args}`.trim());
|
|
447
|
+
const display = out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out;
|
|
448
|
+
printer.systemMsg(display || '(no diff)');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
// /git review — inject diff into context, ask model to review
|
|
452
|
+
if (sub === 'review') {
|
|
453
|
+
const diff = await git('diff HEAD');
|
|
454
|
+
const staged = await git('diff --staged');
|
|
455
|
+
const combined = [diff, staged].filter(Boolean).join('\n').trim();
|
|
456
|
+
if (!combined || combined === '(no diff)') {
|
|
457
|
+
printer.systemMsg('no changes to review');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const truncated = combined.length > 8000 ? combined.slice(0, 8000) + '\n…[truncated]' : combined;
|
|
461
|
+
const userMsg = `Review these git changes for bugs, issues, and improvements:\n\n${truncated}`;
|
|
462
|
+
printer.userMsg('/git review');
|
|
463
|
+
pushHistory({ role: 'user', content: userMsg });
|
|
464
|
+
await runLoop(buildContext());
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// /git branch
|
|
468
|
+
if (sub === 'branch' || sub.startsWith('branch ')) {
|
|
469
|
+
const args = sub.slice(6).trim();
|
|
470
|
+
const out = await git(`branch ${args}`.trim());
|
|
471
|
+
printer.systemMsg(out || '(done)');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// /git commit <msg>
|
|
475
|
+
if (sub.startsWith('commit ')) {
|
|
476
|
+
const msg = sub.slice(7).trim();
|
|
477
|
+
if (!msg) {
|
|
478
|
+
printer.systemMsg('usage: /git commit <message>');
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const status = await git('status --short');
|
|
482
|
+
if (!status || status === '(clean — no changes)') {
|
|
483
|
+
printer.systemMsg('nothing to commit — working tree clean');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
printer.systemMsg(`staging and committing:\n${status}`);
|
|
487
|
+
const stageOut = await git('add -A');
|
|
488
|
+
if (stageOut)
|
|
489
|
+
printer.systemMsg(stageOut);
|
|
490
|
+
const commitOut = await git(`commit -m ${JSON.stringify(msg)}`);
|
|
491
|
+
printer.systemMsg(commitOut);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// fallthrough — run arbitrary git subcommand
|
|
495
|
+
const out = await git(sub);
|
|
496
|
+
printer.systemMsg(out || '(done)');
|
|
497
|
+
}, []);
|
|
185
498
|
// ─── submit ────────────────────────────────────────────────────────────────
|
|
186
499
|
const handleSubmit = useCallback(async (text) => {
|
|
187
500
|
const cmd = text.trim();
|
|
501
|
+
if (cmd === '/model' || cmd.startsWith('/model ')) {
|
|
502
|
+
const name = cmd.slice(6).trim();
|
|
503
|
+
if (!name) {
|
|
504
|
+
printer.systemMsg(`current model: ${currentModelRef.current}`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
setCurrentModel(name);
|
|
508
|
+
printer.systemMsg(`model → ${name}`);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
188
511
|
if (cmd === '/models') {
|
|
189
512
|
await openPicker();
|
|
190
513
|
return;
|
|
191
514
|
}
|
|
192
515
|
if (cmd === '/new') {
|
|
516
|
+
if (saveTimerRef.current) {
|
|
517
|
+
clearTimeout(saveTimerRef.current);
|
|
518
|
+
saveTimerRef.current = null;
|
|
519
|
+
}
|
|
193
520
|
saveSession(sessionNameRef.current, historyRef.current);
|
|
194
521
|
const newName = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
195
522
|
historyRef.current = [];
|
|
@@ -210,6 +537,20 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
210
537
|
if (cmd === '/exit') {
|
|
211
538
|
process.exit(0);
|
|
212
539
|
}
|
|
540
|
+
if (cmd === '/git' || cmd.startsWith('/git ')) {
|
|
541
|
+
const sub = cmd.slice(4).trim();
|
|
542
|
+
await handleGit(sub);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (cmd.startsWith('/refactor ') || cmd === '/refactor') {
|
|
546
|
+
const goal = cmd.slice(9).trim();
|
|
547
|
+
if (!goal) {
|
|
548
|
+
printer.systemMsg('usage: /refactor <goal>');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
await runRefactor(goal);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
213
554
|
if (cmd === '/plan' || cmd.startsWith('/plan ')) {
|
|
214
555
|
const topic = cmd.slice(5).trim();
|
|
215
556
|
setPlanningMode(true);
|
|
@@ -218,8 +559,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
218
559
|
? `I want to plan: ${topic}`
|
|
219
560
|
: 'I want to start planning. Help me think through my goals step by step.';
|
|
220
561
|
printer.userMsg(msg);
|
|
221
|
-
|
|
222
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
562
|
+
pushHistory({ role: 'user', content: msg });
|
|
223
563
|
await runLoop(buildContext());
|
|
224
564
|
return;
|
|
225
565
|
}
|
|
@@ -239,8 +579,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
239
579
|
const msg = subPrompts[subCmd];
|
|
240
580
|
if (msg) {
|
|
241
581
|
printer.userMsg(msg);
|
|
242
|
-
|
|
243
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
582
|
+
pushHistory({ role: 'user', content: msg });
|
|
244
583
|
await runLoop(buildContext());
|
|
245
584
|
return;
|
|
246
585
|
}
|
|
@@ -260,6 +599,10 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
260
599
|
printer.systemMsg(`current: ${sessionNameRef.current}`);
|
|
261
600
|
return;
|
|
262
601
|
}
|
|
602
|
+
if (saveTimerRef.current) {
|
|
603
|
+
clearTimeout(saveTimerRef.current);
|
|
604
|
+
saveTimerRef.current = null;
|
|
605
|
+
}
|
|
263
606
|
saveSession(sessionNameRef.current, historyRef.current);
|
|
264
607
|
historyRef.current = loadSession(arg);
|
|
265
608
|
setSessionName(arg);
|
|
@@ -288,7 +631,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
288
631
|
}
|
|
289
632
|
if (skill.prompt) {
|
|
290
633
|
printer.userMsg(skill.prompt);
|
|
291
|
-
|
|
634
|
+
pushHistory({ role: 'user', content: skill.prompt });
|
|
292
635
|
await runLoop(buildContext());
|
|
293
636
|
return;
|
|
294
637
|
}
|
|
@@ -297,9 +640,14 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
297
640
|
return;
|
|
298
641
|
}
|
|
299
642
|
const contextPrefix = buildAtContext(text);
|
|
643
|
+
const shouldInjectGit = config.gitContext !== false && looksCodeRelated(text);
|
|
644
|
+
const { prefix: gitPrefix, label: gitLabel } = shouldInjectGit
|
|
645
|
+
? await buildGitContext(cwd, lastGitStatusRef)
|
|
646
|
+
: { prefix: '', label: '' };
|
|
647
|
+
if (gitLabel)
|
|
648
|
+
printer.systemMsg(gitLabel);
|
|
300
649
|
printer.userMsg(text);
|
|
301
|
-
|
|
302
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
650
|
+
pushHistory({ role: 'user', content: gitPrefix + contextPrefix + text });
|
|
303
651
|
await runLoop(buildContext());
|
|
304
652
|
}, [skills, runLoop, openPicker]);
|
|
305
653
|
const handleAbort = useCallback(() => {
|
|
@@ -308,8 +656,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
308
656
|
}, []);
|
|
309
657
|
const skillList = skills.list();
|
|
310
658
|
// ─── render ────────────────────────────────────────────────────────────────
|
|
311
|
-
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); setPullState(undefined); } }), _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" }),
|
|
312
|
-
|
|
313
|
-
|
|
659
|
+
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); setPullState(undefined); } }), _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'
|
|
660
|
+
? _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] })] })
|
|
661
|
+
: _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 })] }));
|
|
314
662
|
}
|
|
315
|
-
//# sourceMappingURL=InputBar.js.map
|
|
@@ -16,4 +16,3 @@ export function AtPicker({ files, query, idx }) {
|
|
|
16
16
|
return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: active ? 'cyan' : 'white', bold: active, children: [active ? '▶' : ' ', icon] }), _jsxs(Text, { color: active ? 'cyan' : f.type === 'dir' ? 'blue' : 'white', children: [' ', f.rel] }), f.size !== undefined && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', f.size > 1024 ? `${(f.size / 1024).toFixed(0)}k` : `${f.size}b`] }))] }, f.path));
|
|
17
17
|
}) }));
|
|
18
18
|
}
|
|
19
|
-
//# sourceMappingURL=AtPicker.js.map
|
|
@@ -15,11 +15,12 @@ export function CommandPalette({ skills, query, idx }) {
|
|
|
15
15
|
}
|
|
16
16
|
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", marginX: 1, children: filtered.map((s, i) => {
|
|
17
17
|
const active = i === idx;
|
|
18
|
-
const isBuiltin = s.ns === 'builtin';
|
|
18
|
+
const isBuiltin = s.ns === 'builtin' || s.ns === 'git';
|
|
19
19
|
const name = (s.ns === 'default' || s.ns === 'builtin')
|
|
20
20
|
? `/${s.name}`
|
|
21
|
-
:
|
|
21
|
+
: s.ns === 'git'
|
|
22
|
+
? `/git ${s.name}`
|
|
23
|
+
: `/${s.ns}:${s.name}`;
|
|
22
24
|
return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: active ? 'cyan' : isBuiltin ? 'white' : 'magenta', bold: active, children: [active ? '▶ ' : ' ', name.padEnd(20)] }), _jsx(Text, { color: "gray", dimColor: true, children: s.description })] }, `${s.ns}:${s.name}`));
|
|
23
25
|
}) }));
|
|
24
26
|
}
|
|
25
|
-
//# sourceMappingURL=CommandPalette.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useMemo } from 'react';
|
|
2
|
+
import { useState, useMemo, useRef } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { listFiles } from '../../files/ops.js';
|
|
5
5
|
import { CommandPalette } from './CommandPalette.js';
|
|
@@ -11,8 +11,17 @@ const BUILTIN_COMMANDS = [
|
|
|
11
11
|
{ ns: 'builtin', name: 'sessions', description: 'list all saved sessions' },
|
|
12
12
|
{ ns: 'builtin', name: 'session', description: 'switch session /session <name>' },
|
|
13
13
|
{ ns: 'builtin', name: 'exit', description: 'exit miii' },
|
|
14
|
+
{ ns: 'builtin', name: 'model', description: 'switch model mid-session /model <name>' },
|
|
14
15
|
{ ns: 'builtin', name: 'list', description: 'list all loaded skills' },
|
|
15
16
|
{ ns: 'builtin', name: 'plan', description: 'start planning mode /plan [topic]' },
|
|
17
|
+
{ ns: 'builtin', name: 'refactor', description: 'multi-file AI refactor /refactor <goal>' },
|
|
18
|
+
{ ns: 'git', name: 'status', description: 'show git working tree status' },
|
|
19
|
+
{ ns: 'git', name: 'diff', description: 'show unstaged diff' },
|
|
20
|
+
{ ns: 'git', name: 'diff --staged', description: 'show staged diff' },
|
|
21
|
+
{ ns: 'git', name: 'log', description: 'show recent commits' },
|
|
22
|
+
{ ns: 'git', name: 'review', description: 'review current changes with AI' },
|
|
23
|
+
{ ns: 'git', name: 'branch', description: 'list branches' },
|
|
24
|
+
{ ns: 'git', name: 'commit', description: 'stage all and commit /git commit <msg>' },
|
|
16
25
|
];
|
|
17
26
|
const PLANNING_COMMANDS = [
|
|
18
27
|
{ ns: 'plan', name: 'next', description: 'suggest next concrete steps' },
|
|
@@ -25,14 +34,8 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
25
34
|
const [cursor, setCursor] = useState({ row: 0, col: 0 });
|
|
26
35
|
const [overlay, setOverlay] = useState('none');
|
|
27
36
|
const [overlayIdx, setOverlayIdx] = useState(0);
|
|
28
|
-
const [files] = useState(
|
|
29
|
-
|
|
30
|
-
return listFiles(cwd, true);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
});
|
|
37
|
+
const [files, setFiles] = useState([]);
|
|
38
|
+
const filesLoadedRef = useRef(false);
|
|
36
39
|
// built-ins first, then loaded skills (deduplicated by name)
|
|
37
40
|
const allCommands = useMemo(() => {
|
|
38
41
|
const builtinNames = new Set(BUILTIN_COMMANDS.map(b => b.name));
|
|
@@ -64,9 +67,17 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
64
67
|
}, [commandQuery, allCommands]);
|
|
65
68
|
const filteredFiles = useMemo(() => {
|
|
66
69
|
if (!atQuery)
|
|
67
|
-
return
|
|
70
|
+
return [];
|
|
71
|
+
if (!filesLoadedRef.current) {
|
|
72
|
+
filesLoadedRef.current = true;
|
|
73
|
+
setTimeout(() => { try {
|
|
74
|
+
setFiles(listFiles(cwd, true));
|
|
75
|
+
}
|
|
76
|
+
catch { } }, 0);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
68
79
|
return files.filter(f => f.rel.toLowerCase().includes(atQuery.toLowerCase())).slice(0, 8);
|
|
69
|
-
}, [atQuery, files]);
|
|
80
|
+
}, [atQuery, files, cwd]);
|
|
70
81
|
const overlayCount = overlay === 'command' ? filteredCommands.length : filteredFiles.length;
|
|
71
82
|
function clearInput() {
|
|
72
83
|
setLines(['']);
|
|
@@ -104,7 +115,9 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
104
115
|
function selectCommand(skill) {
|
|
105
116
|
const name = (skill.ns === 'default' || skill.ns === 'builtin')
|
|
106
117
|
? `/${skill.name}`
|
|
107
|
-
:
|
|
118
|
+
: skill.ns === 'git'
|
|
119
|
+
? `/git ${skill.name}`
|
|
120
|
+
: `/${skill.ns}:${skill.name}`;
|
|
108
121
|
clearInput();
|
|
109
122
|
onSubmit(name);
|
|
110
123
|
}
|
|
@@ -275,4 +288,3 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
|
|
|
275
288
|
function renderLineWithCursor(line, col, showCursor) {
|
|
276
289
|
return line.slice(0, col) + (showCursor ? '█' : '') + line.slice(col);
|
|
277
290
|
}
|
|
278
|
-
//# sourceMappingURL=InputArea.js.map
|