miii-cli 0.3.4 → 0.3.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/dist/config.js +1 -1
- package/dist/index/embedder.js +28 -0
- package/dist/index/indexer.js +100 -0
- package/dist/index/search.js +19 -0
- package/dist/index/store.js +47 -0
- package/dist/index/tool.js +29 -0
- package/dist/init.js +14 -11
- package/dist/tools/index.js +2 -1
- package/dist/tui/InputBar.js +23 -490
- package/dist/tui/components/InputArea.js +12 -14
- package/dist/tui/hooks/useGit.js +75 -0
- package/dist/tui/hooks/useRefactor.js +131 -0
- package/dist/tui/hooks/useSubmit.js +382 -0
- package/dist/tui/printer.js +35 -6
- package/package.json +1 -1
package/dist/tui/InputBar.js
CHANGED
|
@@ -1,47 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState,
|
|
2
|
+
import { useState, useRef, useMemo, useEffect } 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
7
|
import { tools } from '../tools/index.js';
|
|
8
|
-
import { readFile } from '../files/ops.js';
|
|
9
|
-
import { generateId } from '../types.js';
|
|
10
|
-
import * as printer from './printer.js';
|
|
11
8
|
import { toolArgSummary } from './printer.js';
|
|
12
|
-
import {
|
|
13
|
-
import { MacroQueue, MicroQueue } from '../tasks/queue.js';
|
|
9
|
+
import { MacroQueue } from '../tasks/queue.js';
|
|
14
10
|
import { TaskExecutor } from '../tasks/executor.js';
|
|
15
|
-
import { fileEditContext } from '../tasks/compactor.js';
|
|
16
|
-
import { StreamParser } from '../parser/stream-parser.js';
|
|
17
|
-
import { chat } from '../llm/stream.js';
|
|
18
|
-
import { exec } from 'child_process';
|
|
19
|
-
import { promisify } from 'util';
|
|
20
|
-
import { getTavilyKey, saveTavilyKey } from '../tavily/client.js';
|
|
21
|
-
import { getSystemPrompt } from '../tools/index.js';
|
|
22
11
|
import { THINKING_PHRASES, SPARKLE } from './thinking.js';
|
|
23
|
-
import { buildGitContext, looksCodeRelated } from './git-context.js';
|
|
24
12
|
import { useSession } from './hooks/useSession.js';
|
|
25
13
|
import { useModelPicker } from './hooks/useModelPicker.js';
|
|
26
14
|
import { useRunLoop } from './hooks/useRunLoop.js';
|
|
15
|
+
import { useRefactor } from './hooks/useRefactor.js';
|
|
16
|
+
import { useGit } from './hooks/useGit.js';
|
|
17
|
+
import { useSubmit } from './hooks/useSubmit.js';
|
|
27
18
|
import { runDeepThink } from './deepThink.js';
|
|
28
19
|
import { setInkInstance } from './printer.js';
|
|
29
|
-
|
|
30
|
-
function buildAtContext(text) {
|
|
31
|
-
const refs = [...text.matchAll(/@([\w./\-]+)/g)];
|
|
32
|
-
if (!refs.length)
|
|
33
|
-
return '';
|
|
34
|
-
const parts = [];
|
|
35
|
-
for (const m of refs) {
|
|
36
|
-
try {
|
|
37
|
-
const content = readFile(m[1]);
|
|
38
|
-
if (content)
|
|
39
|
-
parts.push(`<file path="${m[1]}">\n${content}\n</file>`);
|
|
40
|
-
}
|
|
41
|
-
catch { }
|
|
42
|
-
}
|
|
43
|
-
return parts.length ? parts.join('\n\n') + '\n\n' : '';
|
|
44
|
-
}
|
|
20
|
+
import { createSearchCodebaseTool } from '../index/tool.js';
|
|
45
21
|
function formatElapsed(ms) {
|
|
46
22
|
const s = Math.floor(ms / 1000);
|
|
47
23
|
if (s < 60)
|
|
@@ -53,9 +29,7 @@ function formatElapsed(ms) {
|
|
|
53
29
|
export function InputBar({ config, skills, cwd, session, version }) {
|
|
54
30
|
const { stdout, write: stdoutWrite } = useStdout();
|
|
55
31
|
const cols = stdout.columns ?? 80;
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
setInkInstance(stdoutWrite);
|
|
58
|
-
}, []);
|
|
32
|
+
useEffect(() => { setInkInstance(stdoutWrite); }, []);
|
|
59
33
|
const phraseSeq = useMemo(() => Array.from({ length: 100 }, () => Math.floor(Math.random() * THINKING_PHRASES.length)), []);
|
|
60
34
|
const [planningMode, setPlanningMode] = useState(false);
|
|
61
35
|
const macroQueueRef = useRef(new MacroQueue());
|
|
@@ -73,465 +47,24 @@ export function InputBar({ config, skills, cwd, session, version }) {
|
|
|
73
47
|
return `Research complete (${result.toolCalls} tool calls, ${result.webCalls} web):\n\n${result.findings}`;
|
|
74
48
|
},
|
|
75
49
|
}), [config]);
|
|
76
|
-
const
|
|
50
|
+
const searchTool = useMemo(() => createSearchCodebaseTool(config, cwd), [config, cwd]);
|
|
51
|
+
const allTools = useMemo(() => [...tools, deepThinkTool, searchTool], [deepThinkTool, searchTool]);
|
|
77
52
|
const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, permissionRequest, resolvePermission, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
setTaskLabel
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await chat({
|
|
93
|
-
provider: config.provider,
|
|
94
|
-
model: currentModelRef.current,
|
|
95
|
-
baseUrl: config.baseUrl,
|
|
96
|
-
messages: planCtx,
|
|
97
|
-
signal: abortRef.current.signal,
|
|
98
|
-
async onDone(text) { planText = text; },
|
|
99
|
-
onError(err) { printer.errorMsg(err.message); },
|
|
100
|
-
});
|
|
101
|
-
if (!planText) {
|
|
102
|
-
setStatus('idle');
|
|
103
|
-
setTaskLabel(undefined);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
printer.assistantMsg(planText);
|
|
107
|
-
const filePlan = [];
|
|
108
|
-
const lines = planText.split('\n');
|
|
109
|
-
let lastPath = '';
|
|
110
|
-
for (const line of lines) {
|
|
111
|
-
const fm = line.match(/^FILE:\s*(.+)/);
|
|
112
|
-
const cm = line.match(/^CHANGE:\s*(.+)/);
|
|
113
|
-
if (fm)
|
|
114
|
-
lastPath = fm[1].trim();
|
|
115
|
-
if (cm && lastPath) {
|
|
116
|
-
filePlan.push({ path: lastPath, change: cm[1].trim() });
|
|
117
|
-
lastPath = '';
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (!filePlan.length) {
|
|
121
|
-
printer.systemMsg('no files identified in plan — done');
|
|
122
|
-
setStatus('idle');
|
|
123
|
-
setTaskLabel(undefined);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
printer.systemMsg(`plan: ${filePlan.length} file(s) to change`);
|
|
127
|
-
const micro = new MicroQueue();
|
|
128
|
-
for (const fp of filePlan) {
|
|
129
|
-
const t = { id: `read:${fp.path}`, priority: 1, tool: 'read_file', args: { path: fp.path }, deps: [], status: 'pending' };
|
|
130
|
-
micro.push(t);
|
|
131
|
-
}
|
|
132
|
-
const macro = {
|
|
133
|
-
id: generateId(),
|
|
134
|
-
goal,
|
|
135
|
-
priority: 0,
|
|
136
|
-
microtasks: micro.toArray(),
|
|
137
|
-
status: 'running',
|
|
138
|
-
};
|
|
139
|
-
macroQueueRef.current.enqueue(macro);
|
|
140
|
-
setTaskLabel(`reading ${filePlan.length} file(s)…`);
|
|
141
|
-
const readResults = await executorRef.current.drain(micro, ({ task, error }) => {
|
|
142
|
-
if (error)
|
|
143
|
-
printer.errorMsg(`read failed: ${task.args.path} — ${error}`);
|
|
144
|
-
else
|
|
145
|
-
printer.systemMsg(`read: ${task.args.path}`);
|
|
146
|
-
});
|
|
147
|
-
setTaskLabel(`applying changes…`);
|
|
148
|
-
const writeMicro = new MicroQueue();
|
|
149
|
-
for (const fp of filePlan) {
|
|
150
|
-
const readId = `read:${fp.path}`;
|
|
151
|
-
const fileContent = readResults.get(readId) ?? '';
|
|
152
|
-
if (!fileContent) {
|
|
153
|
-
printer.systemMsg(`skip (unreadable): ${fp.path}`);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
setCurrentTool(`edit ${fp.path}`);
|
|
157
|
-
setTaskLabel(`editing: ${fp.path}`);
|
|
158
|
-
const editCtx = fileEditContext(systemPromptRef.current, goal, fp.path, fileContent, fp.change);
|
|
159
|
-
let editText = '';
|
|
160
|
-
await chat({
|
|
161
|
-
provider: config.provider,
|
|
162
|
-
model: currentModelRef.current,
|
|
163
|
-
baseUrl: config.baseUrl,
|
|
164
|
-
messages: editCtx,
|
|
165
|
-
signal: abortRef.current?.signal,
|
|
166
|
-
async onDone(text) { editText = text; },
|
|
167
|
-
onError(err) { printer.errorMsg(`edit LLM error: ${err.message}`); },
|
|
168
|
-
});
|
|
169
|
-
if (!editText)
|
|
170
|
-
continue;
|
|
171
|
-
printer.assistantMsg(editText);
|
|
172
|
-
const parser = new StreamParser();
|
|
173
|
-
for (const item of [...parser.feed(editText), ...parser.flush()]) {
|
|
174
|
-
if (item.type === 'tool_call') {
|
|
175
|
-
writeMicro.push({ id: generateId(), priority: 2, tool: item.toolName, args: item.toolArgs, deps: [], status: 'pending' });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (writeMicro.size > 0) {
|
|
180
|
-
setTaskLabel(`writing ${writeMicro.size} change(s)…`);
|
|
181
|
-
await executorRef.current.drain(writeMicro, ({ task, result, error }) => {
|
|
182
|
-
if (error)
|
|
183
|
-
printer.errorMsg(`${task.tool} failed: ${error}`);
|
|
184
|
-
else
|
|
185
|
-
printer.toolMsg(task.tool, result ?? '');
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
macro.status = 'done';
|
|
189
|
-
macroQueueRef.current.dequeue();
|
|
190
|
-
setCurrentTool(undefined);
|
|
191
|
-
setTaskLabel(undefined);
|
|
192
|
-
setStatus('idle');
|
|
193
|
-
printer.systemMsg(`refactor done — ${filePlan.length} file(s) processed`);
|
|
194
|
-
pushHistory({ role: 'user', content: `[refactor] ${goal}` });
|
|
195
|
-
pushHistory({ role: 'assistant', content: planText });
|
|
196
|
-
}, [config]);
|
|
197
|
-
// ─── git ───────────────────────────────────────────────────────────────────
|
|
198
|
-
const handleGit = useCallback(async (sub) => {
|
|
199
|
-
const git = async (args) => {
|
|
200
|
-
try {
|
|
201
|
-
const { stdout, stderr } = await gitRun(`git ${args}`, { timeout: 15_000 });
|
|
202
|
-
return (stdout + stderr).trim();
|
|
203
|
-
}
|
|
204
|
-
catch (e) {
|
|
205
|
-
return e.message ?? String(e);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
if (!sub || sub === 'status') {
|
|
209
|
-
printer.systemMsg(await git('status'));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (sub === 'log' || sub.startsWith('log ')) {
|
|
213
|
-
const n = parseInt(sub.split(' ')[1] ?? '10', 10) || 10;
|
|
214
|
-
printer.systemMsg(await git(`log --oneline --decorate -${Math.min(n, 50)}`));
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (sub === 'diff' || sub.startsWith('diff ')) {
|
|
218
|
-
const args = sub.slice(4).trim();
|
|
219
|
-
const out = await git(`diff ${args}`.trim());
|
|
220
|
-
printer.systemMsg(out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out || '(no diff)');
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (sub === 'review') {
|
|
224
|
-
const diff = await git('diff HEAD');
|
|
225
|
-
const staged = await git('diff --staged');
|
|
226
|
-
const combined = [diff, staged].filter(Boolean).join('\n').trim();
|
|
227
|
-
if (!combined || combined === '(no diff)') {
|
|
228
|
-
printer.systemMsg('no changes to review');
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const truncated = combined.length > 8000 ? combined.slice(0, 8000) + '\n…[truncated]' : combined;
|
|
232
|
-
const userMsg = `Review these git changes for bugs, issues, and improvements:\n\n${truncated}`;
|
|
233
|
-
printer.userMsg('/git review');
|
|
234
|
-
pushHistory({ role: 'user', content: userMsg });
|
|
235
|
-
await runLoop(buildContext());
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (sub === 'branch' || sub.startsWith('branch ')) {
|
|
239
|
-
printer.systemMsg(await git(`branch ${sub.slice(6).trim()}`.trim()) || '(done)');
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
if (sub.startsWith('commit ')) {
|
|
243
|
-
const msg = sub.slice(7).trim();
|
|
244
|
-
if (!msg) {
|
|
245
|
-
printer.systemMsg('usage: /git commit <message>');
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const gitStatus = await git('status --short');
|
|
249
|
-
if (!gitStatus || gitStatus === '(clean — no changes)') {
|
|
250
|
-
printer.systemMsg('nothing to commit — working tree clean');
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
printer.systemMsg(`staging and committing:\n${gitStatus}`);
|
|
254
|
-
const stageOut = await git('add -A');
|
|
255
|
-
if (stageOut)
|
|
256
|
-
printer.systemMsg(stageOut);
|
|
257
|
-
printer.systemMsg(await git(`commit -m ${JSON.stringify(msg)}`));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
printer.systemMsg(await git(sub) || '(done)');
|
|
261
|
-
}, []);
|
|
262
|
-
// ─── submit ────────────────────────────────────────────────────────────────
|
|
263
|
-
const handleSubmit = useCallback(async (text) => {
|
|
264
|
-
const cmd = text.trim();
|
|
265
|
-
if (cmd === '/version') {
|
|
266
|
-
printer.systemMsg(`miii v${version ?? 'unknown'}`);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
if (cmd === '/tavily-key' || cmd.startsWith('/tavily-key ')) {
|
|
270
|
-
const key = cmd.slice(11).trim();
|
|
271
|
-
if (!key) {
|
|
272
|
-
const existing = getTavilyKey();
|
|
273
|
-
printer.systemMsg(existing ? 'Tavily key set (use /tavily-key <key> to update)' : 'No Tavily key set. Usage: /tavily-key tvly-...');
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (!key.startsWith('tvly-')) {
|
|
277
|
-
printer.systemMsg('Key should start with tvly-. Get yours at https://tavily.com');
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
saveTavilyKey(key);
|
|
281
|
-
printer.systemMsg('Tavily API key saved to ~/.config/miii/tavily.key (mode 600)');
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (cmd === '/skills' || cmd.startsWith('/skills ')) {
|
|
285
|
-
const sub = cmd.slice(7).trim();
|
|
286
|
-
if (!sub || sub === 'list') {
|
|
287
|
-
const pkgs = skills.listNpmSkills();
|
|
288
|
-
printer.systemMsg(pkgs.length ? `installed npm skills:\n${pkgs.map(p => ` ${p}`).join('\n')}` : 'no npm skills installed — try /skills install <name>');
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (sub.startsWith('install ')) {
|
|
292
|
-
const pkg = sub.slice(8).trim();
|
|
293
|
-
if (!pkg) {
|
|
294
|
-
printer.systemMsg('usage: /skills install <name> (e.g. /skills install git-summary)');
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
printer.systemMsg(`installing miii-skill-${pkg}…`);
|
|
298
|
-
try {
|
|
299
|
-
printer.systemMsg(await skills.installSkill(pkg));
|
|
300
|
-
}
|
|
301
|
-
catch (e) {
|
|
302
|
-
printer.errorMsg(String(e));
|
|
303
|
-
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (sub.startsWith('uninstall ')) {
|
|
307
|
-
const pkg = sub.slice(10).trim();
|
|
308
|
-
if (!pkg) {
|
|
309
|
-
printer.systemMsg('usage: /skills uninstall <name>');
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
printer.systemMsg(await skills.uninstallSkill(pkg));
|
|
314
|
-
}
|
|
315
|
-
catch (e) {
|
|
316
|
-
printer.errorMsg(String(e));
|
|
317
|
-
}
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
printer.systemMsg('usage: /skills install <name> | /skills uninstall <name> | /skills list');
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (cmd === '/model' || cmd.startsWith('/model ')) {
|
|
324
|
-
const name = cmd.slice(6).trim();
|
|
325
|
-
if (!name) {
|
|
326
|
-
printer.systemMsg(`current model: ${currentModelRef.current}`);
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
setCurrentModel(name);
|
|
330
|
-
currentModelRef.current = name;
|
|
331
|
-
printer.systemMsg(`model → ${name}`);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (cmd === '/models') {
|
|
335
|
-
await openPicker();
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (cmd === '/new') {
|
|
339
|
-
if (saveTimerRef.current) {
|
|
340
|
-
clearTimeout(saveTimerRef.current);
|
|
341
|
-
saveTimerRef.current = null;
|
|
342
|
-
}
|
|
343
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
344
|
-
const newName = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
345
|
-
historyRef.current = [];
|
|
346
|
-
setSessionName(newName);
|
|
347
|
-
setPlanningMode(false);
|
|
348
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`);
|
|
349
|
-
printer.systemMsg(`new session → ${newName}`);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (cmd === '/clear') {
|
|
353
|
-
historyRef.current = [];
|
|
354
|
-
saveSession(sessionNameRef.current, []);
|
|
355
|
-
setPlanningMode(false);
|
|
356
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`);
|
|
357
|
-
printer.systemMsg('chat cleared');
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (cmd === '/exit') {
|
|
361
|
-
process.exit(0);
|
|
362
|
-
}
|
|
363
|
-
if (cmd === '/git' || cmd.startsWith('/git ')) {
|
|
364
|
-
await handleGit(cmd.slice(4).trim());
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
if (cmd.startsWith('/refactor ') || cmd === '/refactor') {
|
|
368
|
-
const goal = cmd.slice(9).trim();
|
|
369
|
-
if (!goal) {
|
|
370
|
-
printer.systemMsg('usage: /refactor <goal>');
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
await runRefactor(goal);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (cmd.startsWith('/think ') || cmd === '/think') {
|
|
377
|
-
const query = cmd.slice(6).trim();
|
|
378
|
-
if (!query) {
|
|
379
|
-
printer.systemMsg('usage: /think <query>');
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
printer.userMsg(`/think ${query}`);
|
|
383
|
-
setStatus('thinking');
|
|
384
|
-
setTaskLabel(`gathering: ${query}`);
|
|
385
|
-
abortRef.current = new AbortController();
|
|
386
|
-
try {
|
|
387
|
-
const result = await runDeepThink(query, config, currentModelRef.current, abortRef.current.signal, (toolName) => setCurrentTool(`gather:${toolName}`));
|
|
388
|
-
setCurrentTool(undefined);
|
|
389
|
-
printer.systemMsg(`gathered: ${result.toolCalls} tool call(s), ${result.webCalls} web call(s)`);
|
|
390
|
-
if (result.findings) {
|
|
391
|
-
pushHistory({ role: 'user', content: `/think ${query}` });
|
|
392
|
-
pushHistory({ role: 'assistant', content: result.findings });
|
|
393
|
-
pushHistory({ role: 'user', content: `Based on your research above, give a complete answer to: ${query}` });
|
|
394
|
-
await runLoop(buildContext(), 0, query);
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
printer.systemMsg('nothing gathered — try rephrasing');
|
|
398
|
-
setStatus('idle');
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
catch (e) {
|
|
402
|
-
printer.errorMsg(`deep think failed: ${e}`);
|
|
403
|
-
setStatus('idle');
|
|
404
|
-
}
|
|
405
|
-
finally {
|
|
406
|
-
setCurrentTool(undefined);
|
|
407
|
-
setTaskLabel(undefined);
|
|
408
|
-
}
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
if (cmd === '/plan' || cmd.startsWith('/plan ')) {
|
|
412
|
-
const topic = cmd.slice(5).trim();
|
|
413
|
-
setPlanningMode(true);
|
|
414
|
-
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.`);
|
|
415
|
-
const msg = topic ? `I want to plan: ${topic}` : 'I want to start planning. Help me think through my goals step by step.';
|
|
416
|
-
printer.userMsg(msg);
|
|
417
|
-
pushHistory({ role: 'user', content: msg });
|
|
418
|
-
await runLoop(buildContext());
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (cmd === '/plan:done') {
|
|
422
|
-
setPlanningMode(false);
|
|
423
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`);
|
|
424
|
-
printer.systemMsg('planning mode off');
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (cmd.startsWith('/plan:')) {
|
|
428
|
-
const subCmd = cmd.slice(6);
|
|
429
|
-
const subPrompts = {
|
|
430
|
-
next: 'What are the next concrete steps I should take?',
|
|
431
|
-
breakdown: 'Can you break this down into specific subtasks?',
|
|
432
|
-
review: 'Please review and critique our plan so far. What are we missing?',
|
|
433
|
-
};
|
|
434
|
-
const msg = subPrompts[subCmd];
|
|
435
|
-
if (msg) {
|
|
436
|
-
printer.userMsg(msg);
|
|
437
|
-
pushHistory({ role: 'user', content: msg });
|
|
438
|
-
await runLoop(buildContext());
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (cmd === '/sessions') {
|
|
443
|
-
const sessions = listSessions();
|
|
444
|
-
if (!sessions.length) {
|
|
445
|
-
printer.systemMsg('no saved sessions');
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
printer.systemMsg(sessions.map(s => `${s.name === sessionNameRef.current ? '▶ ' : ' '}${s.name} (${s.messageCount} msgs)`).join('\n'));
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (cmd.startsWith('/session')) {
|
|
452
|
-
const arg = cmd.slice(8).trim();
|
|
453
|
-
if (!arg) {
|
|
454
|
-
printer.systemMsg(`current: ${sessionNameRef.current}`);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
if (arg.startsWith('delete ')) {
|
|
458
|
-
const target = arg.slice(7).trim();
|
|
459
|
-
if (!target) {
|
|
460
|
-
printer.systemMsg('usage: /session delete <name|all>');
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (target === 'all') {
|
|
464
|
-
const count = deleteAllSessions(sessionNameRef.current);
|
|
465
|
-
printer.systemMsg(`deleted ${count} session(s) — kept active: ${sessionNameRef.current}`);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (target === sessionNameRef.current) {
|
|
469
|
-
printer.systemMsg('cannot delete active session — switch first');
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
try {
|
|
473
|
-
deleteSession(target);
|
|
474
|
-
printer.systemMsg(`deleted: ${target}`);
|
|
475
|
-
}
|
|
476
|
-
catch (e) {
|
|
477
|
-
printer.errorMsg(`delete failed: ${String(e)}`);
|
|
478
|
-
}
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
if (saveTimerRef.current) {
|
|
482
|
-
clearTimeout(saveTimerRef.current);
|
|
483
|
-
saveTimerRef.current = null;
|
|
484
|
-
}
|
|
485
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
486
|
-
historyRef.current = loadSession(arg);
|
|
487
|
-
setSessionName(arg);
|
|
488
|
-
printer.systemMsg(`session → ${arg} (${historyRef.current.length} messages)`);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
if (text.startsWith('/')) {
|
|
492
|
-
const [slashCmd, ...rest] = text.slice(1).split(' ');
|
|
493
|
-
const skill = skills.get(slashCmd);
|
|
494
|
-
if (skill) {
|
|
495
|
-
if (skill.name === 'list') {
|
|
496
|
-
printer.systemMsg(skills.list().map(s => `/${s.ns === 'default' ? '' : s.ns + ':'}${s.name} — ${s.description}`).join('\n'));
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
if (skill.execute) {
|
|
500
|
-
const ctx = {
|
|
501
|
-
messages: historyRef.current.map(m => ({ role: m.role, content: m.content })),
|
|
502
|
-
appendMessage: (_role, content) => printer.systemMsg(content),
|
|
503
|
-
setSystemPrompt: (p) => { systemPromptRef.current = p; },
|
|
504
|
-
getSystemPrompt: () => systemPromptRef.current,
|
|
505
|
-
};
|
|
506
|
-
const result = await skill.execute(rest.join(' '), ctx);
|
|
507
|
-
if (result)
|
|
508
|
-
printer.systemMsg(result);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
if (skill.prompt) {
|
|
512
|
-
printer.userMsg(skill.prompt);
|
|
513
|
-
pushHistory({ role: 'user', content: skill.prompt });
|
|
514
|
-
await runLoop(buildContext());
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
printer.systemMsg(`unknown command: /${slashCmd} — try /list`);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
renameFromMessage(text);
|
|
522
|
-
const contextPrefix = buildAtContext(text);
|
|
523
|
-
const shouldInjectGit = config.gitContext !== false && looksCodeRelated(text);
|
|
524
|
-
const { prefix: gitPrefix, label: gitLabel } = shouldInjectGit
|
|
525
|
-
? await buildGitContext(cwd, lastGitStatusRef)
|
|
526
|
-
: { prefix: '', label: '' };
|
|
527
|
-
if (gitLabel)
|
|
528
|
-
printer.systemMsg(gitLabel);
|
|
529
|
-
printer.userMsg(text);
|
|
530
|
-
pushHistory({ role: 'user', content: gitPrefix + contextPrefix + text });
|
|
531
|
-
await runLoop(buildContext());
|
|
532
|
-
}, [skills, runLoop, openPicker]);
|
|
53
|
+
const { runRefactor } = useRefactor({
|
|
54
|
+
config, currentModelRef, systemPromptRef, abortRef,
|
|
55
|
+
macroQueueRef, executorRef,
|
|
56
|
+
setStatus, setTaskLabel, setCurrentTool, pushHistory,
|
|
57
|
+
});
|
|
58
|
+
const { handleGit } = useGit({ pushHistory, buildContext, runLoop });
|
|
59
|
+
const { handleSubmit } = useSubmit({
|
|
60
|
+
config, skills, cwd, version, currentModelRef, setCurrentModel,
|
|
61
|
+
historyRef, sessionNameRef, saveTimerRef, systemPromptRef, abortRef,
|
|
62
|
+
planningMode, setPlanningMode, runLoop, buildContext, pushHistory,
|
|
63
|
+
setSessionName, renameFromMessage, openPicker,
|
|
64
|
+
setStatus, setTaskLabel, setCurrentTool,
|
|
65
|
+
runRefactor, handleGit, lastGitStatusRef,
|
|
66
|
+
});
|
|
533
67
|
const skillList = skills.list();
|
|
534
|
-
// ─── render ────────────────────────────────────────────────────────────────
|
|
535
68
|
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 })] })) : permissionRequest ? (_jsxs(Box, { paddingX: 1, gap: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { color: "white", bold: true, children: permissionRequest.toolName }), _jsx(Text, { color: "gray", children: toolArgSummary(permissionRequest.args) })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { children: status === 'thinking'
|
|
536
69
|
? _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: [SPARKLE[tick % SPARKLE.length], " "] }), _jsx(Text, { color: "gray", dimColor: true, italic: true, children: THINKING_PHRASES[phraseSeq[Math.floor(tick / 62) % phraseSeq.length]] })] })
|
|
537
70
|
: _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "gray", dimColor: true, children: formatElapsed(Date.now() - thinkingStartRef.current) }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, permissionRequest: permissionRequest, onPermissionResponse: resolvePermission, onSubmit: handleSubmit, onAbort: handleAbort, history: historyRef.current.filter(m => m.role === 'user').map(m => m.content) })] }));
|
|
@@ -433,26 +433,24 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
|
|
|
433
433
|
const promptColor = permissionRequest ? 'yellow' : isProcessing ? 'yellow' : 'green';
|
|
434
434
|
const inHistory = historyIdx !== -1;
|
|
435
435
|
const hint = permissionRequest
|
|
436
|
-
? 'y
|
|
436
|
+
? 'y approve n deny'
|
|
437
437
|
: isProcessing
|
|
438
|
-
? 'esc
|
|
438
|
+
? 'esc interrupt'
|
|
439
439
|
: pasteLines > 0
|
|
440
|
-
? 'backspace
|
|
441
|
-
: overlay
|
|
442
|
-
? '↑↓
|
|
443
|
-
:
|
|
444
|
-
?
|
|
445
|
-
:
|
|
446
|
-
?
|
|
447
|
-
:
|
|
448
|
-
? 'planning mode · / suggestions · enter send · /plan:done exit'
|
|
449
|
-
: 'enter send · @ file · / cmd · ctrl+j newline · ↑ history';
|
|
440
|
+
? 'backspace remove paste enter send'
|
|
441
|
+
: overlay !== 'none'
|
|
442
|
+
? '↑↓ navigate enter select esc close'
|
|
443
|
+
: inHistory
|
|
444
|
+
? `history ${historyIdx + 1}/${history.length} ↑↓ navigate esc clear`
|
|
445
|
+
: planningMode
|
|
446
|
+
? 'planning mode /plan:done exit'
|
|
447
|
+
: '? for shortcuts';
|
|
450
448
|
const pastePreview = pasteRef.current
|
|
451
449
|
? pasteRef.current.split('\n')[0].slice(0, cols - 6)
|
|
452
450
|
: '';
|
|
453
|
-
return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: promptColor, bold: true, children: '> ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: permissionRequest ? (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "green", bold: true, children: "y yes" }), _jsx(Text, { color: "red", bold: true, children: "n no" })] })) : pasteLines > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " line", pasteLines !== 1 ? 's' : ''] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] }), pastePreview && (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", pastePreview, pasteRef.current.split('\n')[0].length > cols - 6 ? '…' : ''] }))] })) : lines.length === 1 && !lines[0] ? (
|
|
451
|
+
return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: promptColor, bold: true, children: '> ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: permissionRequest ? (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "green", bold: true, children: "y yes" }), _jsx(Text, { color: "red", bold: true, children: "n no" })] })) : pasteLines > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " line", pasteLines !== 1 ? 's' : ''] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] }), pastePreview && (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", pastePreview, pasteRef.current.split('\n')[0].length > cols - 6 ? '…' : ''] }))] })) : lines.length === 1 && !lines[0] ? (_jsx(Text, { children: isActive ? '█' : ' ' })) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
|
|
454
452
|
? renderLineWithCursor(line, cursor.col, isActive)
|
|
455
|
-
: line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: '─
|
|
453
|
+
: line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", hint] })] }));
|
|
456
454
|
}
|
|
457
455
|
function renderLineWithCursor(line, col, showCursor) {
|
|
458
456
|
return line.slice(0, col) + (showCursor ? '█' : '') + line.slice(col);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import * as printer from '../printer.js';
|
|
5
|
+
const gitRun = promisify(exec);
|
|
6
|
+
export function useGit(deps) {
|
|
7
|
+
const depsRef = useRef(deps);
|
|
8
|
+
depsRef.current = deps;
|
|
9
|
+
const handleGit = useCallback(async (sub) => {
|
|
10
|
+
const { pushHistory, buildContext, runLoop } = depsRef.current;
|
|
11
|
+
const git = async (args) => {
|
|
12
|
+
try {
|
|
13
|
+
const { stdout, stderr } = await gitRun(`git ${args}`, { timeout: 15_000 });
|
|
14
|
+
return (stdout + stderr).trim();
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return e.message ?? String(e);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (!sub || sub === 'status') {
|
|
21
|
+
printer.systemMsg(await git('status'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (sub === 'log' || sub.startsWith('log ')) {
|
|
25
|
+
const n = parseInt(sub.split(' ')[1] ?? '10', 10) || 10;
|
|
26
|
+
printer.systemMsg(await git(`log --oneline --decorate -${Math.min(n, 50)}`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (sub === 'diff' || sub.startsWith('diff ')) {
|
|
30
|
+
const args = sub.slice(4).trim();
|
|
31
|
+
const out = await git(`diff ${args}`.trim());
|
|
32
|
+
printer.systemMsg(out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out || '(no diff)');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (sub === 'review') {
|
|
36
|
+
const diff = await git('diff HEAD');
|
|
37
|
+
const staged = await git('diff --staged');
|
|
38
|
+
const combined = [diff, staged].filter(Boolean).join('\n').trim();
|
|
39
|
+
if (!combined || combined === '(no diff)') {
|
|
40
|
+
printer.systemMsg('no changes to review');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const truncated = combined.length > 8000 ? combined.slice(0, 8000) + '\n…[truncated]' : combined;
|
|
44
|
+
const userMsg = `Review these git changes for bugs, issues, and improvements:\n\n${truncated}`;
|
|
45
|
+
printer.userMsg('/git review');
|
|
46
|
+
pushHistory({ role: 'user', content: userMsg });
|
|
47
|
+
await runLoop(buildContext());
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (sub === 'branch' || sub.startsWith('branch ')) {
|
|
51
|
+
printer.systemMsg(await git(`branch ${sub.slice(6).trim()}`.trim()) || '(done)');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (sub.startsWith('commit ')) {
|
|
55
|
+
const msg = sub.slice(7).trim();
|
|
56
|
+
if (!msg) {
|
|
57
|
+
printer.systemMsg('usage: /git commit <message>');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const gitStatus = await git('status --short');
|
|
61
|
+
if (!gitStatus || gitStatus === '(clean — no changes)') {
|
|
62
|
+
printer.systemMsg('nothing to commit — working tree clean');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
printer.systemMsg(`staging and committing:\n${gitStatus}`);
|
|
66
|
+
const stageOut = await git('add -A');
|
|
67
|
+
if (stageOut)
|
|
68
|
+
printer.systemMsg(stageOut);
|
|
69
|
+
printer.systemMsg(await git(`commit -m ${JSON.stringify(msg)}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
printer.systemMsg(await git(sub) || '(done)');
|
|
73
|
+
}, []);
|
|
74
|
+
return { handleGit };
|
|
75
|
+
}
|