miii-cli 0.3.3 → 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.
@@ -1,47 +1,23 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
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 { loadSession, saveSession, listSessions, deleteSession, deleteAllSessions } from '../sessions.js';
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
- const gitRun = promisify(exec);
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 allTools = useMemo(() => [...tools, deepThinkTool], [deepThinkTool]);
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
- // ─── refactor ─────────────────────────────────────────────────────────────
79
- const runRefactor = useCallback(async (goal) => {
80
- printer.systemMsg(`refactor: ${goal}`);
81
- setTaskLabel(`planning: ${goal}`);
82
- setStatus('thinking');
83
- const planCtx = [
84
- { role: 'system', content: systemPromptRef.current },
85
- {
86
- role: 'user',
87
- 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.`,
88
- },
89
- ];
90
- abortRef.current = new AbortController();
91
- let planText = '';
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 approve · n deny'
436
+ ? 'y approve n deny'
437
437
  : isProcessing
438
- ? 'esc to interrupt'
438
+ ? 'esc interrupt'
439
439
  : pasteLines > 0
440
- ? 'backspace removes paste · enter to send'
441
- : overlay === 'command' && !commandQuery.includes(' ')
442
- ? '↑↓ navigate · enter select · esc close'
443
- : overlay === 'at'
444
- ? '↑↓ navigate · enter select · esc close'
445
- : inHistory
446
- ? `history [${historyIdx + 1}/${history.length}] · ↑↓ navigate · enter to send · esc clear`
447
- : planningMode
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] ? (isActive ? (_jsxs(Text, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "How can I help you? " }), _jsx(Text, { children: "\u2588" })] })) : (_jsx(Text, { color: "gray", dimColor: true, children: " " }))) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
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: '─ ' + hint + ' ' + '─'.repeat(Math.max(0, cols - hint.length - 3)) })] }));
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
+ }