miii-cli 0.2.2 → 0.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/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 +111 -6
- package/dist/tui/App.js +0 -1
- package/dist/tui/InputBar.js +397 -33
- 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 +1 -3
- 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/dist/{workers/context.worker.d.ts → tasks/types.js} +0 -0
package/dist/tui/InputBar.js
CHANGED
|
@@ -6,11 +6,19 @@ import { ModelPicker } from './components/ModelPicker.js';
|
|
|
6
6
|
import { Divider } from './components/StatusBar.js';
|
|
7
7
|
import { chat } from '../llm/stream.js';
|
|
8
8
|
import { listModels, pullModel } from '../llm/ollama.js';
|
|
9
|
-
import { StreamParser } from '../parser/stream-parser.js';
|
|
9
|
+
import { StreamParser, extractBareToolCall } 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 = [];
|
|
@@ -107,35 +209,57 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
107
209
|
if (item.type === 'tool_call')
|
|
108
210
|
pendingTools.push({ name: item.toolName, args: item.toolArgs });
|
|
109
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
|
+
}
|
|
110
218
|
printer.assistantMsg(fullText);
|
|
111
|
-
|
|
112
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
219
|
+
pushHistory({ role: 'assistant', content: fullText });
|
|
113
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
|
+
}
|
|
114
232
|
setStatus('idle');
|
|
115
233
|
return;
|
|
116
234
|
}
|
|
117
235
|
setStatus('tool');
|
|
118
|
-
const next = [...
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
|
126
252
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
next.push({ role: 'user', content: err });
|
|
253
|
+
else {
|
|
254
|
+
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
255
|
+
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
131
256
|
}
|
|
132
257
|
}
|
|
133
|
-
else {
|
|
134
|
-
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
135
|
-
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
136
|
-
}
|
|
137
258
|
}
|
|
138
|
-
|
|
259
|
+
finally {
|
|
260
|
+
setCurrentTool(undefined);
|
|
261
|
+
}
|
|
262
|
+
await runLoop(next, depth + 1, goal);
|
|
139
263
|
},
|
|
140
264
|
onError(err) {
|
|
141
265
|
if (err.name !== 'AbortError')
|
|
@@ -182,14 +306,234 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
182
306
|
setPickerError(`pull failed: ${e}`);
|
|
183
307
|
}
|
|
184
308
|
}, [config.baseUrl]);
|
|
309
|
+
// ─── refactor ─────────────────────────────────────────────────────────────
|
|
310
|
+
const runRefactor = useCallback(async (goal) => {
|
|
311
|
+
printer.systemMsg(`refactor: ${goal}`);
|
|
312
|
+
setTaskLabel(`planning: ${goal}`);
|
|
313
|
+
setStatus('thinking');
|
|
314
|
+
// Phase 1 — planning: ask model to list files and describe changes
|
|
315
|
+
const planCtx = [
|
|
316
|
+
{ role: 'system', content: systemPromptRef.current },
|
|
317
|
+
{
|
|
318
|
+
role: 'user',
|
|
319
|
+
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.`,
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
abortRef.current = new AbortController();
|
|
323
|
+
let planText = '';
|
|
324
|
+
await chat({
|
|
325
|
+
provider: config.provider,
|
|
326
|
+
model: currentModelRef.current,
|
|
327
|
+
baseUrl: config.baseUrl,
|
|
328
|
+
messages: planCtx,
|
|
329
|
+
signal: abortRef.current.signal,
|
|
330
|
+
async onDone(text) { planText = text; },
|
|
331
|
+
onError(err) { printer.errorMsg(err.message); },
|
|
332
|
+
});
|
|
333
|
+
if (!planText) {
|
|
334
|
+
setStatus('idle');
|
|
335
|
+
setTaskLabel(undefined);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
printer.assistantMsg(planText);
|
|
339
|
+
// Parse FILE:/CHANGE: pairs from plan
|
|
340
|
+
const filePlan = [];
|
|
341
|
+
const lines = planText.split('\n');
|
|
342
|
+
let lastPath = '';
|
|
343
|
+
for (const line of lines) {
|
|
344
|
+
const fm = line.match(/^FILE:\s*(.+)/);
|
|
345
|
+
const cm = line.match(/^CHANGE:\s*(.+)/);
|
|
346
|
+
if (fm)
|
|
347
|
+
lastPath = fm[1].trim();
|
|
348
|
+
if (cm && lastPath) {
|
|
349
|
+
filePlan.push({ path: lastPath, change: cm[1].trim() });
|
|
350
|
+
lastPath = '';
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (!filePlan.length) {
|
|
354
|
+
printer.systemMsg('no files identified in plan — done');
|
|
355
|
+
setStatus('idle');
|
|
356
|
+
setTaskLabel(undefined);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
printer.systemMsg(`plan: ${filePlan.length} file(s) to change`);
|
|
360
|
+
// Phase 2 — execute via macro/micro queue
|
|
361
|
+
const micro = new MicroQueue();
|
|
362
|
+
// P1: read all files in parallel
|
|
363
|
+
for (const fp of filePlan) {
|
|
364
|
+
const t = { id: `read:${fp.path}`, priority: 1, tool: 'read_file', args: { path: fp.path }, deps: [], status: 'pending' };
|
|
365
|
+
micro.push(t);
|
|
366
|
+
}
|
|
367
|
+
const macro = {
|
|
368
|
+
id: generateId(),
|
|
369
|
+
goal,
|
|
370
|
+
priority: 0,
|
|
371
|
+
microtasks: micro.toArray(),
|
|
372
|
+
status: 'running',
|
|
373
|
+
};
|
|
374
|
+
macroQueueRef.current.enqueue(macro);
|
|
375
|
+
setTaskLabel(`reading ${filePlan.length} file(s)…`);
|
|
376
|
+
const readResults = await executorRef.current.drain(micro, ({ task, result, error }) => {
|
|
377
|
+
if (error)
|
|
378
|
+
printer.errorMsg(`read failed: ${task.args.path} — ${error}`);
|
|
379
|
+
else
|
|
380
|
+
printer.systemMsg(`read: ${task.args.path}`);
|
|
381
|
+
});
|
|
382
|
+
// Phase 3 — per-file LLM call with isolated context → patch
|
|
383
|
+
setTaskLabel(`applying changes…`);
|
|
384
|
+
const writeMicro = new MicroQueue();
|
|
385
|
+
for (const fp of filePlan) {
|
|
386
|
+
const readId = `read:${fp.path}`;
|
|
387
|
+
const fileContent = readResults.get(readId) ?? '';
|
|
388
|
+
if (!fileContent) {
|
|
389
|
+
printer.systemMsg(`skip (unreadable): ${fp.path}`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
setCurrentTool(`edit ${fp.path}`);
|
|
393
|
+
setTaskLabel(`editing: ${fp.path}`);
|
|
394
|
+
// Isolated context per file keeps model focused
|
|
395
|
+
const editCtx = fileEditContext(systemPromptRef.current, goal, fp.path, fileContent, fp.change);
|
|
396
|
+
let editText = '';
|
|
397
|
+
await chat({
|
|
398
|
+
provider: config.provider,
|
|
399
|
+
model: currentModelRef.current,
|
|
400
|
+
baseUrl: config.baseUrl,
|
|
401
|
+
messages: editCtx,
|
|
402
|
+
signal: abortRef.current?.signal,
|
|
403
|
+
async onDone(text) { editText = text; },
|
|
404
|
+
onError(err) { printer.errorMsg(`edit LLM error: ${err.message}`); },
|
|
405
|
+
});
|
|
406
|
+
if (!editText)
|
|
407
|
+
continue;
|
|
408
|
+
printer.assistantMsg(editText);
|
|
409
|
+
// Queue write tasks from LLM's tool calls (P2)
|
|
410
|
+
const parser = new StreamParser();
|
|
411
|
+
for (const item of [...parser.feed(editText), ...parser.flush()]) {
|
|
412
|
+
if (item.type === 'tool_call') {
|
|
413
|
+
writeMicro.push({ id: generateId(), priority: 2, tool: item.toolName, args: item.toolArgs, deps: [], status: 'pending' });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Execute all writes
|
|
418
|
+
if (writeMicro.size > 0) {
|
|
419
|
+
setTaskLabel(`writing ${writeMicro.size} change(s)…`);
|
|
420
|
+
await executorRef.current.drain(writeMicro, ({ task, result, error }) => {
|
|
421
|
+
if (error)
|
|
422
|
+
printer.errorMsg(`${task.tool} failed: ${error}`);
|
|
423
|
+
else
|
|
424
|
+
printer.toolMsg(task.tool, result ?? '');
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
macro.status = 'done';
|
|
428
|
+
macroQueueRef.current.dequeue();
|
|
429
|
+
setCurrentTool(undefined);
|
|
430
|
+
setTaskLabel(undefined);
|
|
431
|
+
setStatus('idle');
|
|
432
|
+
printer.systemMsg(`refactor done — ${filePlan.length} file(s) processed`);
|
|
433
|
+
pushHistory({ role: 'user', content: `[refactor] ${goal}` });
|
|
434
|
+
pushHistory({ role: 'assistant', content: planText });
|
|
435
|
+
}, [config]);
|
|
436
|
+
// ─── git ───────────────────────────────────────────────────────────────────
|
|
437
|
+
const handleGit = useCallback(async (sub) => {
|
|
438
|
+
const git = async (args) => {
|
|
439
|
+
try {
|
|
440
|
+
const { stdout, stderr } = await gitRun(`git ${args}`, { timeout: 15_000 });
|
|
441
|
+
return (stdout + stderr).trim();
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
return e.message ?? String(e);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
// /git or /git status
|
|
448
|
+
if (!sub || sub === 'status') {
|
|
449
|
+
const out = await git('status');
|
|
450
|
+
printer.systemMsg(out);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// /git log [n]
|
|
454
|
+
if (sub === 'log' || sub.startsWith('log ')) {
|
|
455
|
+
const n = parseInt(sub.split(' ')[1] ?? '10', 10) || 10;
|
|
456
|
+
const out = await git(`log --oneline --decorate -${Math.min(n, 50)}`);
|
|
457
|
+
printer.systemMsg(out);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// /git diff [--staged] [file]
|
|
461
|
+
if (sub === 'diff' || sub.startsWith('diff ')) {
|
|
462
|
+
const args = sub.slice(4).trim();
|
|
463
|
+
const out = await git(`diff ${args}`.trim());
|
|
464
|
+
const display = out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out;
|
|
465
|
+
printer.systemMsg(display || '(no diff)');
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// /git review — inject diff into context, ask model to review
|
|
469
|
+
if (sub === 'review') {
|
|
470
|
+
const diff = await git('diff HEAD');
|
|
471
|
+
const staged = await git('diff --staged');
|
|
472
|
+
const combined = [diff, staged].filter(Boolean).join('\n').trim();
|
|
473
|
+
if (!combined || combined === '(no diff)') {
|
|
474
|
+
printer.systemMsg('no changes to review');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const truncated = combined.length > 8000 ? combined.slice(0, 8000) + '\n…[truncated]' : combined;
|
|
478
|
+
const userMsg = `Review these git changes for bugs, issues, and improvements:\n\n${truncated}`;
|
|
479
|
+
printer.userMsg('/git review');
|
|
480
|
+
pushHistory({ role: 'user', content: userMsg });
|
|
481
|
+
await runLoop(buildContext());
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
// /git branch
|
|
485
|
+
if (sub === 'branch' || sub.startsWith('branch ')) {
|
|
486
|
+
const args = sub.slice(6).trim();
|
|
487
|
+
const out = await git(`branch ${args}`.trim());
|
|
488
|
+
printer.systemMsg(out || '(done)');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
// /git commit <msg>
|
|
492
|
+
if (sub.startsWith('commit ')) {
|
|
493
|
+
const msg = sub.slice(7).trim();
|
|
494
|
+
if (!msg) {
|
|
495
|
+
printer.systemMsg('usage: /git commit <message>');
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const status = await git('status --short');
|
|
499
|
+
if (!status || status === '(clean — no changes)') {
|
|
500
|
+
printer.systemMsg('nothing to commit — working tree clean');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
printer.systemMsg(`staging and committing:\n${status}`);
|
|
504
|
+
const stageOut = await git('add -A');
|
|
505
|
+
if (stageOut)
|
|
506
|
+
printer.systemMsg(stageOut);
|
|
507
|
+
const commitOut = await git(`commit -m ${JSON.stringify(msg)}`);
|
|
508
|
+
printer.systemMsg(commitOut);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// fallthrough — run arbitrary git subcommand
|
|
512
|
+
const out = await git(sub);
|
|
513
|
+
printer.systemMsg(out || '(done)');
|
|
514
|
+
}, []);
|
|
185
515
|
// ─── submit ────────────────────────────────────────────────────────────────
|
|
186
516
|
const handleSubmit = useCallback(async (text) => {
|
|
187
517
|
const cmd = text.trim();
|
|
518
|
+
if (cmd === '/model' || cmd.startsWith('/model ')) {
|
|
519
|
+
const name = cmd.slice(6).trim();
|
|
520
|
+
if (!name) {
|
|
521
|
+
printer.systemMsg(`current model: ${currentModelRef.current}`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
setCurrentModel(name);
|
|
525
|
+
printer.systemMsg(`model → ${name}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
188
528
|
if (cmd === '/models') {
|
|
189
529
|
await openPicker();
|
|
190
530
|
return;
|
|
191
531
|
}
|
|
192
532
|
if (cmd === '/new') {
|
|
533
|
+
if (saveTimerRef.current) {
|
|
534
|
+
clearTimeout(saveTimerRef.current);
|
|
535
|
+
saveTimerRef.current = null;
|
|
536
|
+
}
|
|
193
537
|
saveSession(sessionNameRef.current, historyRef.current);
|
|
194
538
|
const newName = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
195
539
|
historyRef.current = [];
|
|
@@ -210,6 +554,20 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
210
554
|
if (cmd === '/exit') {
|
|
211
555
|
process.exit(0);
|
|
212
556
|
}
|
|
557
|
+
if (cmd === '/git' || cmd.startsWith('/git ')) {
|
|
558
|
+
const sub = cmd.slice(4).trim();
|
|
559
|
+
await handleGit(sub);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (cmd.startsWith('/refactor ') || cmd === '/refactor') {
|
|
563
|
+
const goal = cmd.slice(9).trim();
|
|
564
|
+
if (!goal) {
|
|
565
|
+
printer.systemMsg('usage: /refactor <goal>');
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
await runRefactor(goal);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
213
571
|
if (cmd === '/plan' || cmd.startsWith('/plan ')) {
|
|
214
572
|
const topic = cmd.slice(5).trim();
|
|
215
573
|
setPlanningMode(true);
|
|
@@ -218,8 +576,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
218
576
|
? `I want to plan: ${topic}`
|
|
219
577
|
: 'I want to start planning. Help me think through my goals step by step.';
|
|
220
578
|
printer.userMsg(msg);
|
|
221
|
-
|
|
222
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
579
|
+
pushHistory({ role: 'user', content: msg });
|
|
223
580
|
await runLoop(buildContext());
|
|
224
581
|
return;
|
|
225
582
|
}
|
|
@@ -239,8 +596,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
239
596
|
const msg = subPrompts[subCmd];
|
|
240
597
|
if (msg) {
|
|
241
598
|
printer.userMsg(msg);
|
|
242
|
-
|
|
243
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
599
|
+
pushHistory({ role: 'user', content: msg });
|
|
244
600
|
await runLoop(buildContext());
|
|
245
601
|
return;
|
|
246
602
|
}
|
|
@@ -260,6 +616,10 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
260
616
|
printer.systemMsg(`current: ${sessionNameRef.current}`);
|
|
261
617
|
return;
|
|
262
618
|
}
|
|
619
|
+
if (saveTimerRef.current) {
|
|
620
|
+
clearTimeout(saveTimerRef.current);
|
|
621
|
+
saveTimerRef.current = null;
|
|
622
|
+
}
|
|
263
623
|
saveSession(sessionNameRef.current, historyRef.current);
|
|
264
624
|
historyRef.current = loadSession(arg);
|
|
265
625
|
setSessionName(arg);
|
|
@@ -288,7 +648,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
288
648
|
}
|
|
289
649
|
if (skill.prompt) {
|
|
290
650
|
printer.userMsg(skill.prompt);
|
|
291
|
-
|
|
651
|
+
pushHistory({ role: 'user', content: skill.prompt });
|
|
292
652
|
await runLoop(buildContext());
|
|
293
653
|
return;
|
|
294
654
|
}
|
|
@@ -297,9 +657,14 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
297
657
|
return;
|
|
298
658
|
}
|
|
299
659
|
const contextPrefix = buildAtContext(text);
|
|
660
|
+
const shouldInjectGit = config.gitContext !== false && looksCodeRelated(text);
|
|
661
|
+
const { prefix: gitPrefix, label: gitLabel } = shouldInjectGit
|
|
662
|
+
? await buildGitContext(cwd, lastGitStatusRef)
|
|
663
|
+
: { prefix: '', label: '' };
|
|
664
|
+
if (gitLabel)
|
|
665
|
+
printer.systemMsg(gitLabel);
|
|
300
666
|
printer.userMsg(text);
|
|
301
|
-
|
|
302
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
667
|
+
pushHistory({ role: 'user', content: gitPrefix + contextPrefix + text });
|
|
303
668
|
await runLoop(buildContext());
|
|
304
669
|
}, [skills, runLoop, openPicker]);
|
|
305
670
|
const handleAbort = useCallback(() => {
|
|
@@ -308,8 +673,7 @@ export function InputBar({ config, skills, cwd, session }) {
|
|
|
308
673
|
}, []);
|
|
309
674
|
const skillList = skills.list();
|
|
310
675
|
// ─── 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
|
-
|
|
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); 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'
|
|
677
|
+
? _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
|
+
: _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
679
|
}
|
|
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
|