moth-ai 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +319 -0
- package/dist/agent/orchestrator.js +141 -0
- package/dist/agent/types.js +1 -0
- package/dist/config/configManager.js +62 -0
- package/dist/config/keychain.js +20 -0
- package/dist/context/ignore.js +27 -0
- package/dist/context/manager.js +62 -0
- package/dist/context/scanner.js +41 -0
- package/dist/context/types.js +1 -0
- package/dist/editing/patcher.js +37 -0
- package/dist/index.js +401 -0
- package/dist/llm/claudeAdapter.js +47 -0
- package/dist/llm/cohereAdapter.js +42 -0
- package/dist/llm/factory.js +30 -0
- package/dist/llm/geminiAdapter.js +55 -0
- package/dist/llm/openAIAdapter.js +45 -0
- package/dist/llm/types.js +1 -0
- package/dist/planning/todoManager.js +23 -0
- package/dist/tools/definitions.js +187 -0
- package/dist/tools/factory.js +196 -0
- package/dist/tools/registry.js +21 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/App.js +387 -0
- package/dist/ui/ProfileManager.js +51 -0
- package/dist/ui/components/CommandPalette.js +29 -0
- package/dist/ui/components/CustomTextInput.js +75 -0
- package/dist/ui/components/FileAutocomplete.js +16 -0
- package/dist/ui/components/FileChip.js +8 -0
- package/dist/ui/components/FlameLogo.js +40 -0
- package/dist/ui/components/WordFlame.js +10 -0
- package/dist/ui/components/WordMoth.js +10 -0
- package/dist/ui/wizards/LLMRemover.js +68 -0
- package/dist/ui/wizards/LLMWizard.js +149 -0
- package/dist/utils/fileUtils.js +67 -0
- package/dist/utils/paths.js +22 -0
- package/dist/utils/text.js +49 -0
- package/docs/architecture.md +63 -0
- package/docs/core_logic.md +53 -0
- package/docs/index.md +30 -0
- package/docs/llm_integration.md +49 -0
- package/docs/ui_components.md +44 -0
- package/package.json +70 -0
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput, Newline } from 'ink';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { loadConfig, saveConfig, addProfile } from '../config/configManager.js';
|
|
6
|
+
import { TodoManager } from '../planning/todoManager.js';
|
|
7
|
+
import { AgentOrchestrator } from '../agent/orchestrator.js';
|
|
8
|
+
import { createToolRegistry } from '../tools/factory.js';
|
|
9
|
+
import { findProjectRoot } from '../utils/paths.js';
|
|
10
|
+
import { ProjectScanner } from '../context/scanner.js';
|
|
11
|
+
import { CustomTextInput } from './components/CustomTextInput.js';
|
|
12
|
+
import { WordMoth } from './components/WordMoth.js';
|
|
13
|
+
import { FileChip } from './components/FileChip.js';
|
|
14
|
+
import { FileAutocomplete } from './components/FileAutocomplete.js';
|
|
15
|
+
import { CommandPalette } from './components/CommandPalette.js';
|
|
16
|
+
import { ProfileManager } from './ProfileManager.js';
|
|
17
|
+
import { LLMWizard } from './wizards/LLMWizard.js';
|
|
18
|
+
import { LLMRemover } from './wizards/LLMRemover.js';
|
|
19
|
+
export const App = ({ command, args, todoManager: propTodoManager, username }) => {
|
|
20
|
+
const [messages, setMessages] = useState([]);
|
|
21
|
+
const [inputVal, setInputVal] = useState('');
|
|
22
|
+
const [status, setStatus] = useState('Ready');
|
|
23
|
+
// UX State
|
|
24
|
+
const [showWelcome, setShowWelcome] = useState(true);
|
|
25
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
26
|
+
const [autopilot, setAutopilot] = useState(false);
|
|
27
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
28
|
+
const [thinkingText, setThinkingText] = useState('Sauting...');
|
|
29
|
+
// File Reference State
|
|
30
|
+
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
31
|
+
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
|
32
|
+
const [autocompleteQuery, setAutocompleteQuery] = useState('');
|
|
33
|
+
const [autocompleteIndex, setAutocompleteIndex] = useState(0);
|
|
34
|
+
const [availableFiles, setAvailableFiles] = useState([]);
|
|
35
|
+
const [showFileChips, setShowFileChips] = useState(false); // Collapsed by default
|
|
36
|
+
// Command Palette State
|
|
37
|
+
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
|
38
|
+
// Active Wizard State (for command palette execution)
|
|
39
|
+
const [activeWizard, setActiveWizard] = useState(null);
|
|
40
|
+
// Effect for cycling thinking text
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (isProcessing) {
|
|
43
|
+
const words = ['Sauting...', 'Bubbling...', 'Cooking...', 'Chopping...', 'Simmering...', 'Whisking...', 'Seasoning...'];
|
|
44
|
+
const interval = setInterval(() => {
|
|
45
|
+
setThinkingText(words[Math.floor(Math.random() * words.length)]);
|
|
46
|
+
}, 800);
|
|
47
|
+
return () => clearInterval(interval);
|
|
48
|
+
}
|
|
49
|
+
}, [isProcessing]);
|
|
50
|
+
// Load available files on mount
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const loadFiles = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const root = findProjectRoot() || process.cwd();
|
|
55
|
+
const scanner = new ProjectScanner(root);
|
|
56
|
+
const files = await scanner.scan();
|
|
57
|
+
setAvailableFiles(files);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
console.error('Failed to load files:', e);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
loadFiles();
|
|
64
|
+
}, []);
|
|
65
|
+
// Permission State
|
|
66
|
+
const [pendingPermission, setPendingPermission] = useState(null);
|
|
67
|
+
const [feedbackMode, setFeedbackMode] = useState(false);
|
|
68
|
+
const [permissionSelection, setPermissionSelection] = useState(0);
|
|
69
|
+
// Initialize TodoManager
|
|
70
|
+
const [todoManager] = useState(() => propTodoManager || new TodoManager());
|
|
71
|
+
// --- Run Command Logic ---
|
|
72
|
+
const client = args.client;
|
|
73
|
+
const initialPrompt = args.prompt;
|
|
74
|
+
const activeProfile = args.profile;
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (initialPrompt && messages.length === 0) {
|
|
77
|
+
if (showWelcome)
|
|
78
|
+
setShowWelcome(false);
|
|
79
|
+
runAgent(initialPrompt);
|
|
80
|
+
}
|
|
81
|
+
}, [initialPrompt]);
|
|
82
|
+
const runAgent = async (userPrompt, attachedFiles) => {
|
|
83
|
+
if (showWelcome)
|
|
84
|
+
setShowWelcome(false);
|
|
85
|
+
setIsProcessing(true);
|
|
86
|
+
// Create new user message with attached files
|
|
87
|
+
const userMsg = {
|
|
88
|
+
role: 'user',
|
|
89
|
+
content: userPrompt,
|
|
90
|
+
attachedFiles: attachedFiles && attachedFiles.length > 0 ? attachedFiles : undefined
|
|
91
|
+
};
|
|
92
|
+
setMessages(prev => [...prev, userMsg]);
|
|
93
|
+
try {
|
|
94
|
+
const root = findProjectRoot() || process.cwd();
|
|
95
|
+
// Permission Callback
|
|
96
|
+
const checkPermission = async (toolName, args) => {
|
|
97
|
+
if (autopilot) {
|
|
98
|
+
return { allowed: true };
|
|
99
|
+
}
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
setPendingPermission({
|
|
102
|
+
id: Date.now().toString(),
|
|
103
|
+
toolName,
|
|
104
|
+
args,
|
|
105
|
+
resolve: (response) => {
|
|
106
|
+
setPendingPermission(null);
|
|
107
|
+
resolve(response);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const registry = createToolRegistry(root, todoManager, checkPermission);
|
|
113
|
+
const orchestrator = new AgentOrchestrator({
|
|
114
|
+
model: client,
|
|
115
|
+
tools: registry.getDefinitions(),
|
|
116
|
+
maxSteps: 10
|
|
117
|
+
}, registry, root);
|
|
118
|
+
let finalAnswer = "";
|
|
119
|
+
// Pass accumulated history to the agent
|
|
120
|
+
// 'messages' state contains history prior to this turn.
|
|
121
|
+
// We do NOT include the pending userMsg in the 'history' arg because orchestrator.run
|
|
122
|
+
// treats the prompt separately. Or we can include it in history and pass empty prompt?
|
|
123
|
+
// Orchestrator.run logic: [System, ...history, CurrentPrompt]
|
|
124
|
+
// So we pass 'messages' (which excludes current UserPrompt yet in this render cycle,
|
|
125
|
+
// but wait, we called setMessages above).
|
|
126
|
+
// Actually, setMessages update is invisible in this render closure.
|
|
127
|
+
// So 'messages' here IS the history before this turn. Perfect.
|
|
128
|
+
for await (const step of orchestrator.run(userPrompt, messages)) {
|
|
129
|
+
if (isPaused)
|
|
130
|
+
break; // Basic pause handling (not perfect resonance)
|
|
131
|
+
if (step.thought) {
|
|
132
|
+
// Ambient status update
|
|
133
|
+
setStatus(step.thought);
|
|
134
|
+
}
|
|
135
|
+
if (step.toolCall) {
|
|
136
|
+
setStatus(`Act: ${step.toolCall.name}`);
|
|
137
|
+
}
|
|
138
|
+
if (step.finalAnswer)
|
|
139
|
+
finalAnswer = step.finalAnswer;
|
|
140
|
+
}
|
|
141
|
+
const assistantMsg = { role: 'assistant', content: finalAnswer || "Done." };
|
|
142
|
+
setMessages(prev => [...prev, assistantMsg]);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
setMessages(prev => [...prev, { role: 'assistant', content: `Error: ${e.message}` }]);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
setStatus('Ready');
|
|
149
|
+
setIsProcessing(false);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
// Handle input changes to detect @ for autocomplete
|
|
153
|
+
const handleInputChange = (value) => {
|
|
154
|
+
setInputVal(value);
|
|
155
|
+
// Extract and track file references from @ mentions
|
|
156
|
+
const referencedFiles = extractFileReferences(value);
|
|
157
|
+
setSelectedFiles(referencedFiles);
|
|
158
|
+
// Detect @ symbol for file autocomplete
|
|
159
|
+
const atIndex = value.lastIndexOf('@');
|
|
160
|
+
if (atIndex !== -1) {
|
|
161
|
+
const afterAt = value.slice(atIndex + 1);
|
|
162
|
+
// Close autocomplete if there's a space after @ (end of filename)
|
|
163
|
+
if (afterAt.includes(' ')) {
|
|
164
|
+
setShowAutocomplete(false);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const query = afterAt;
|
|
168
|
+
// Only show autocomplete if @ is at start or after a space
|
|
169
|
+
if (atIndex === 0 || value[atIndex - 1] === ' ') {
|
|
170
|
+
setAutocompleteQuery(query);
|
|
171
|
+
setShowAutocomplete(true);
|
|
172
|
+
setAutocompleteIndex(0);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
setShowAutocomplete(false);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
// Get filtered files for autocomplete
|
|
180
|
+
const getFilteredFiles = () => {
|
|
181
|
+
return availableFiles
|
|
182
|
+
.filter(file => {
|
|
183
|
+
const lowerFile = file.toLowerCase();
|
|
184
|
+
const lowerQuery = autocompleteQuery.toLowerCase();
|
|
185
|
+
return lowerFile.includes(lowerQuery);
|
|
186
|
+
})
|
|
187
|
+
.filter(file => !selectedFiles.includes(file)) // Exclude already selected
|
|
188
|
+
.slice(0, 10);
|
|
189
|
+
};
|
|
190
|
+
const handleFileSelect = (file) => {
|
|
191
|
+
// Replace the entire input value to ensure cursor ends up at the end
|
|
192
|
+
const atIndex = inputVal.lastIndexOf('@');
|
|
193
|
+
if (atIndex !== -1) {
|
|
194
|
+
const beforeAt = inputVal.slice(0, atIndex);
|
|
195
|
+
// Set the complete new value - this places cursor at the end automatically
|
|
196
|
+
const newInput = beforeAt + '@' + file + ' ';
|
|
197
|
+
setInputVal(newInput);
|
|
198
|
+
}
|
|
199
|
+
setShowAutocomplete(false);
|
|
200
|
+
setAutocompleteQuery('');
|
|
201
|
+
};
|
|
202
|
+
const handleFileRemove = (file) => {
|
|
203
|
+
setSelectedFiles(prev => prev.filter(f => f !== file));
|
|
204
|
+
};
|
|
205
|
+
// Extract file references from input text
|
|
206
|
+
const extractFileReferences = (text) => {
|
|
207
|
+
const atMatches = text.match(/@[\w\/.\-]+/g) || [];
|
|
208
|
+
return atMatches.map(match => match.slice(1)); // Remove @ prefix
|
|
209
|
+
};
|
|
210
|
+
// Execute command from command palette
|
|
211
|
+
const executeCommand = (action) => {
|
|
212
|
+
setShowCommandPalette(false);
|
|
213
|
+
switch (action) {
|
|
214
|
+
case 'autopilot':
|
|
215
|
+
setAutopilot(prev => !prev);
|
|
216
|
+
setMessages(prev => [...prev, {
|
|
217
|
+
role: 'assistant',
|
|
218
|
+
content: `Autopilot ${!autopilot ? 'enabled' : 'disabled'}.`
|
|
219
|
+
}]);
|
|
220
|
+
break;
|
|
221
|
+
case 'llm-list':
|
|
222
|
+
setActiveWizard('llm-list');
|
|
223
|
+
break;
|
|
224
|
+
case 'llm-add':
|
|
225
|
+
setActiveWizard('llm-add');
|
|
226
|
+
break;
|
|
227
|
+
case 'llm-use':
|
|
228
|
+
setActiveWizard('llm-use');
|
|
229
|
+
break;
|
|
230
|
+
case 'llm-remove':
|
|
231
|
+
setActiveWizard('llm-remove');
|
|
232
|
+
break;
|
|
233
|
+
case 'help':
|
|
234
|
+
setMessages(prev => [...prev, {
|
|
235
|
+
role: 'assistant',
|
|
236
|
+
content: 'Moth AI - Your terminal coding assistant.\n\nKeyboard Shortcuts:\n- Ctrl+U: Open command palette\n- Ctrl+O: Toggle file references\n- Esc: Pause/Resume\n\nFor full help, run: moth --help'
|
|
237
|
+
}]);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
useInput((input, key) => {
|
|
242
|
+
// Autocomplete Navigation
|
|
243
|
+
if (showAutocomplete && !pendingPermission) {
|
|
244
|
+
const filteredFiles = getFilteredFiles();
|
|
245
|
+
if (key.escape) {
|
|
246
|
+
setShowAutocomplete(false);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (key.upArrow) {
|
|
250
|
+
setAutocompleteIndex(prev => (prev - 1 + filteredFiles.length) % filteredFiles.length);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (key.downArrow) {
|
|
254
|
+
setAutocompleteIndex(prev => (prev + 1) % filteredFiles.length);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Use Enter to select file from autocomplete
|
|
258
|
+
if (key.return && filteredFiles.length > 0) {
|
|
259
|
+
handleFileSelect(filteredFiles[autocompleteIndex]);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Ctrl+O to toggle file chips visibility
|
|
264
|
+
if (input === 'o' && key.ctrl) {
|
|
265
|
+
setShowFileChips(prev => !prev);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Ctrl+U to toggle command palette
|
|
269
|
+
if (input === 'u' && key.ctrl) {
|
|
270
|
+
setShowCommandPalette(prev => !prev);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// ESC Pause
|
|
274
|
+
if (key.escape && !showAutocomplete) {
|
|
275
|
+
setIsPaused(prev => !prev);
|
|
276
|
+
setStatus(prev => prev === 'Paused' ? 'Resumed' : 'Paused');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Permission Handling
|
|
280
|
+
if (pendingPermission) {
|
|
281
|
+
if (feedbackMode) {
|
|
282
|
+
if (key.return) {
|
|
283
|
+
pendingPermission.resolve({ allowed: false, feedback: inputVal });
|
|
284
|
+
setInputVal('');
|
|
285
|
+
setFeedbackMode(false);
|
|
286
|
+
}
|
|
287
|
+
else if (key.backspace || key.delete) {
|
|
288
|
+
setInputVal(prev => prev.slice(0, -1));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
setInputVal(prev => prev + input);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Arrow Key Navigation
|
|
296
|
+
if (key.upArrow) {
|
|
297
|
+
setPermissionSelection(prev => (prev - 1 + 3) % 3);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (key.downArrow) {
|
|
301
|
+
setPermissionSelection(prev => (prev + 1) % 3);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (key.return) {
|
|
305
|
+
if (permissionSelection === 0) {
|
|
306
|
+
pendingPermission.resolve({ allowed: true });
|
|
307
|
+
}
|
|
308
|
+
else if (permissionSelection === 1) {
|
|
309
|
+
setAutopilot(true);
|
|
310
|
+
pendingPermission.resolve({ allowed: true });
|
|
311
|
+
}
|
|
312
|
+
else if (permissionSelection === 2) {
|
|
313
|
+
setFeedbackMode(true);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (input === 'a' || input === 'A') {
|
|
318
|
+
// Yes - execute once
|
|
319
|
+
pendingPermission.resolve({ allowed: true });
|
|
320
|
+
}
|
|
321
|
+
else if (input === 'b' || input === 'B') {
|
|
322
|
+
// Yes - autopilot
|
|
323
|
+
setAutopilot(true);
|
|
324
|
+
pendingPermission.resolve({ allowed: true });
|
|
325
|
+
}
|
|
326
|
+
else if (input === 'c' || input === 'C') {
|
|
327
|
+
// Tell Moth what to do instead
|
|
328
|
+
setFeedbackMode(true);
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
// --- RENDER ---
|
|
334
|
+
// Render wizards if active
|
|
335
|
+
if (activeWizard === 'llm-list' || activeWizard === 'llm-use') {
|
|
336
|
+
const config = loadConfig();
|
|
337
|
+
return (_jsx(ProfileManager, { config: config, onSelect: (profile) => {
|
|
338
|
+
setActiveWizard(null);
|
|
339
|
+
setMessages(prev => [...prev, {
|
|
340
|
+
role: 'assistant',
|
|
341
|
+
content: `Switched to profile '${profile.name}'.`
|
|
342
|
+
}]);
|
|
343
|
+
} }));
|
|
344
|
+
}
|
|
345
|
+
if (activeWizard === 'llm-add') {
|
|
346
|
+
return (_jsx(LLMWizard, { onComplete: (newProfile) => {
|
|
347
|
+
const config = loadConfig();
|
|
348
|
+
const updatedConfig = addProfile(config, newProfile);
|
|
349
|
+
saveConfig(updatedConfig);
|
|
350
|
+
setActiveWizard(null);
|
|
351
|
+
setMessages(prev => [...prev, {
|
|
352
|
+
role: 'assistant',
|
|
353
|
+
content: `Profile '${newProfile.name}' added successfully!`
|
|
354
|
+
}]);
|
|
355
|
+
}, onCancel: () => {
|
|
356
|
+
setActiveWizard(null);
|
|
357
|
+
setMessages(prev => [...prev, {
|
|
358
|
+
role: 'assistant',
|
|
359
|
+
content: 'Cancelled.'
|
|
360
|
+
}]);
|
|
361
|
+
} }));
|
|
362
|
+
}
|
|
363
|
+
if (activeWizard === 'llm-remove') {
|
|
364
|
+
const config = loadConfig();
|
|
365
|
+
return (_jsx(LLMRemover, { config: config, onExit: () => {
|
|
366
|
+
setActiveWizard(null);
|
|
367
|
+
setMessages(prev => [...prev, {
|
|
368
|
+
role: 'assistant',
|
|
369
|
+
content: 'Returned to chat.'
|
|
370
|
+
}]);
|
|
371
|
+
} }));
|
|
372
|
+
}
|
|
373
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [command === 'run' && (_jsxs(Box, { flexDirection: "row", paddingX: 1, paddingY: 0, borderStyle: "round", borderColor: "#0192e5", children: [_jsxs(Box, { flexDirection: "column", marginTop: -1, paddingRight: 1, children: [_jsx(WordMoth, { text: "MOTH", big: true }), _jsx(Box, { marginTop: -1, children: _jsx(Text, { dimColor: true, children: "v1.0.0" }) }), _jsxs(Text, { color: "#3EA0C3", children: ["Welcome, ", username || os.userInfo().username] })] }), _jsxs(Box, { flexDirection: "column", alignItems: "flex-start", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: ["Active_AI: ", activeProfile?.name || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Path: ", process.cwd()] }), _jsx(Text, { dimColor: true, children: "Use Ctrl+U to view commands" })] })] })), messages.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: messages.map((m, i) => (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsxs(Text, { color: m.role === 'user' ? 'blue' : 'green', bold: true, children: [m.role === 'user' ? 'You' : 'Moth', ":"] }), _jsxs(Text, { children: [" ", m.content] })] }, i))) })), pendingPermission && (_jsxs(Box, { borderStyle: "double", borderColor: "red", flexDirection: "column", padding: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "red", children: "PERMISSION REQUIRED" }), _jsxs(Text, { children: ["Tool: ", pendingPermission.toolName] }), _jsxs(Text, { children: ["Args: ", JSON.stringify(pendingPermission.args)] }), _jsx(Newline, {}), !feedbackMode ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: _jsxs(Text, { color: permissionSelection === 0 ? "green" : undefined, bold: permissionSelection === 0, children: [permissionSelection === 0 ? "> " : " ", " [a] Yes - execute this action"] }) }), _jsx(Text, { children: _jsxs(Text, { color: permissionSelection === 1 ? "green" : undefined, bold: permissionSelection === 1, children: [permissionSelection === 1 ? "> " : " ", " [b] Yes - enable autopilot (approve all)"] }) }), _jsx(Text, { children: _jsxs(Text, { color: permissionSelection === 2 ? "green" : undefined, bold: permissionSelection === 2, children: [permissionSelection === 2 ? "> " : " ", " [c] Tell Moth what to do instead"] }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Feedback: " }), _jsx(Text, { children: inputVal })] }))] })), !pendingPermission && (_jsxs(Box, { flexDirection: "column", children: [isProcessing && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsx(Text, { color: "yellow", italic: true, children: thinkingText }), status !== 'Ready' && _jsxs(Text, { color: "gray", dimColor: true, children: [" ", status] })] })), autopilot && (_jsx(Text, { color: "magenta", children: "AUTOPILOT MODE" })), selectedFiles.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { dimColor: true, children: ["Referenced Files (", selectedFiles.length, ") "] }), _jsxs(Text, { color: "gray", dimColor: true, children: ["[Ctrl+O to ", showFileChips ? 'hide' : 'show', "]"] })] }), showFileChips && (_jsx(Box, { flexDirection: "row", flexWrap: "wrap", marginTop: 1, children: selectedFiles.map((file) => (_jsx(FileChip, { filePath: file, onRemove: () => handleFileRemove(file) }, file))) }))] })), _jsxs(Box, { borderStyle: "round", borderColor: isProcessing ? "gray" : "blue", paddingX: 1, children: [_jsx(Text, { color: isProcessing ? "gray" : "cyan", children: '> ' }), _jsx(CustomTextInput, { value: inputVal, onChange: handleInputChange, onSubmit: (val) => {
|
|
374
|
+
// Don't submit if autocomplete is open
|
|
375
|
+
if (showAutocomplete) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (val.trim() && !isProcessing) {
|
|
379
|
+
// Extract file references from @ mentions in text
|
|
380
|
+
const referencedFiles = extractFileReferences(val);
|
|
381
|
+
const allFiles = [...new Set([...selectedFiles, ...referencedFiles])];
|
|
382
|
+
runAgent(val, allFiles.length > 0 ? allFiles : undefined);
|
|
383
|
+
setInputVal('');
|
|
384
|
+
setSelectedFiles([]);
|
|
385
|
+
}
|
|
386
|
+
}, focus: !isProcessing && !pendingPermission })] }), showAutocomplete && (_jsx(FileAutocomplete, { files: availableFiles, query: autocompleteQuery, selectedIndex: autocompleteIndex, onSelect: handleFileSelect })), showCommandPalette && (_jsx(CommandPalette, { onExecute: executeCommand, onClose: () => setShowCommandPalette(false) })), _jsx(Box, { flexDirection: "row", justifyContent: "flex-end", children: _jsx(Text, { color: "gray", dimColor: true, children: activeProfile?.name }) })] }))] }));
|
|
387
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput, Newline, useApp } from 'ink';
|
|
4
|
+
import { setActiveProfile, saveConfig } from '../config/configManager.js';
|
|
5
|
+
export const ProfileManager = ({ config: initialConfig, onSelect }) => {
|
|
6
|
+
const { exit } = useApp();
|
|
7
|
+
const [localConfig, setLocalConfig] = useState(initialConfig);
|
|
8
|
+
const [selectionIndex, setSelectionIndex] = useState(() => {
|
|
9
|
+
const idx = initialConfig.profiles.findIndex(p => p.name === initialConfig.activeProfile);
|
|
10
|
+
return idx >= 0 ? idx : 0;
|
|
11
|
+
});
|
|
12
|
+
const [message, setMessage] = useState('');
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
// Ctrl+X to Exit
|
|
15
|
+
if (input === '\x18' || (key.ctrl && input === 'x')) {
|
|
16
|
+
exit();
|
|
17
|
+
// Only hard exit if we're not in a larger flow, but for now safe to exit app
|
|
18
|
+
if (!onSelect)
|
|
19
|
+
process.exit(0);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (key.upArrow) {
|
|
23
|
+
setSelectionIndex(prev => (prev - 1 + localConfig.profiles.length) % localConfig.profiles.length);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.downArrow) {
|
|
27
|
+
setSelectionIndex(prev => (prev + 1) % localConfig.profiles.length);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (key.return) {
|
|
31
|
+
const selectedProfile = localConfig.profiles[selectionIndex];
|
|
32
|
+
let newConfig = setActiveProfile(localConfig, selectedProfile.name);
|
|
33
|
+
saveConfig(newConfig);
|
|
34
|
+
setLocalConfig(newConfig);
|
|
35
|
+
if (onSelect) {
|
|
36
|
+
// Give a tiny visual feedback before unmounting?
|
|
37
|
+
// Or just instant. Instant is better for "flow".
|
|
38
|
+
onSelect(selectedProfile);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
setMessage(`Active profile set to '${selectedProfile.name}'`);
|
|
42
|
+
setTimeout(() => setMessage(''), 3000);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsx(Text, { bold: true, children: "MOTH LLM PROFILES" }), _jsx(Text, { color: "green", children: "Hint: Press Enter to select new model or Ctrl+X to exit nav." }), _jsx(Newline, {}), localConfig.profiles.map((p, i) => {
|
|
47
|
+
const isSelected = i === selectionIndex;
|
|
48
|
+
const isActive = p.name === localConfig.activeProfile;
|
|
49
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: isSelected ? "green" : undefined, bold: isSelected, children: isSelected ? "* " : " " }), _jsx(Text, { bold: isActive, color: isActive ? "green" : undefined, children: p.name }), _jsxs(Text, { children: [" (", p.provider, " / ", p.model, ")"] }), isActive && _jsx(Text, { color: "green", dimColor: true, children: " (active)" })] }, i));
|
|
50
|
+
}), message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: message }) }))] }));
|
|
51
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
const commands = [
|
|
5
|
+
{ name: 'LLM: List Profiles', description: 'Show all configured LLM profiles', action: 'llm-list' },
|
|
6
|
+
{ name: 'LLM: Add Profile', description: 'Add a new LLM profile', action: 'llm-add' },
|
|
7
|
+
{ name: 'LLM: Switch Profile', description: 'Switch to a different LLM profile', action: 'llm-use' },
|
|
8
|
+
{ name: 'LLM: Remove Profile', description: 'Remove an LLM profile', action: 'llm-remove' },
|
|
9
|
+
{ name: 'Toggle Autopilot', description: 'Enable/disable autopilot mode', action: 'autopilot' },
|
|
10
|
+
{ name: 'Show Help', description: 'Display help information', action: 'help' },
|
|
11
|
+
];
|
|
12
|
+
export const CommandPalette = ({ onExecute, onClose }) => {
|
|
13
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (key.upArrow) {
|
|
16
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
17
|
+
}
|
|
18
|
+
else if (key.downArrow) {
|
|
19
|
+
setSelectedIndex(prev => Math.min(commands.length - 1, prev + 1));
|
|
20
|
+
}
|
|
21
|
+
else if (key.return) {
|
|
22
|
+
onExecute(commands[selectedIndex].action);
|
|
23
|
+
}
|
|
24
|
+
else if (key.escape || (input === 'u' && key.ctrl)) {
|
|
25
|
+
onClose();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Command Palette (Ctrl+U to close)" }), _jsx(Text, { dimColor: true, children: "Select a command and press Enter" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: commands.map((cmd, index) => (_jsxs(Box, { marginY: 0, children: [_jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, bold: index === selectedIndex, children: [index === selectedIndex ? '> ' : ' ', cmd.name] }), _jsxs(Text, { dimColor: true, children: [" - ", cmd.description] })] }, cmd.action))) })] }) }));
|
|
29
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
export const CustomTextInput = ({ value: externalValue, onChange, onSubmit, focus = true, placeholder = '' }) => {
|
|
5
|
+
const [internalValue, setInternalValue] = useState(externalValue);
|
|
6
|
+
const [cursorPos, setCursorPos] = useState(externalValue.length);
|
|
7
|
+
// Sync with external value changes (e.g., from file path insertion)
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setInternalValue(externalValue);
|
|
10
|
+
// Place cursor at end when value changes externally
|
|
11
|
+
setCursorPos(externalValue.length);
|
|
12
|
+
}, [externalValue]);
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
if (!focus)
|
|
15
|
+
return;
|
|
16
|
+
// Submit on Enter
|
|
17
|
+
if (key.return) {
|
|
18
|
+
onSubmit(internalValue);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Backspace - delete character before cursor
|
|
22
|
+
if (key.backspace) {
|
|
23
|
+
if (cursorPos > 0) {
|
|
24
|
+
const newValue = internalValue.slice(0, cursorPos - 1) + internalValue.slice(cursorPos);
|
|
25
|
+
setInternalValue(newValue);
|
|
26
|
+
setCursorPos(cursorPos - 1);
|
|
27
|
+
onChange(newValue);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Delete - delete character after cursor
|
|
32
|
+
if (key.delete) {
|
|
33
|
+
if (cursorPos < internalValue.length) {
|
|
34
|
+
const newValue = internalValue.slice(0, cursorPos) + internalValue.slice(cursorPos + 1);
|
|
35
|
+
setInternalValue(newValue);
|
|
36
|
+
onChange(newValue);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Left arrow - move cursor left
|
|
41
|
+
if (key.leftArrow) {
|
|
42
|
+
setCursorPos(Math.max(0, cursorPos - 1));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Right arrow - move cursor right
|
|
46
|
+
if (key.rightArrow) {
|
|
47
|
+
setCursorPos(Math.min(internalValue.length, cursorPos + 1));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Home - move to start
|
|
51
|
+
if (key.home) {
|
|
52
|
+
setCursorPos(0);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// End - move to end
|
|
56
|
+
if (key.end) {
|
|
57
|
+
setCursorPos(internalValue.length);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Regular character input (ignore ctrl/meta combinations)
|
|
61
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
62
|
+
const newValue = internalValue.slice(0, cursorPos) + input + internalValue.slice(cursorPos);
|
|
63
|
+
setInternalValue(newValue);
|
|
64
|
+
setCursorPos(cursorPos + 1);
|
|
65
|
+
onChange(newValue);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}, { isActive: focus });
|
|
69
|
+
// Render the input with cursor
|
|
70
|
+
const displayValue = internalValue;
|
|
71
|
+
const beforeCursor = displayValue.slice(0, cursorPos);
|
|
72
|
+
const cursorChar = displayValue[cursorPos] || ' '; // Always show space if nothing there
|
|
73
|
+
const afterCursor = displayValue.slice(cursorPos + 1);
|
|
74
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: beforeCursor }), _jsx(Text, { inverse: true, children: cursorChar }), _jsx(Text, { children: afterCursor })] }));
|
|
75
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export const FileAutocomplete = ({ files, query, selectedIndex, onSelect }) => {
|
|
4
|
+
// Filter files based on query
|
|
5
|
+
const filteredFiles = files
|
|
6
|
+
.filter(file => {
|
|
7
|
+
const lowerFile = file.toLowerCase();
|
|
8
|
+
const lowerQuery = query.toLowerCase();
|
|
9
|
+
return lowerFile.includes(lowerQuery);
|
|
10
|
+
})
|
|
11
|
+
.slice(0, 10); // Limit to 10 results
|
|
12
|
+
if (filteredFiles.length === 0) {
|
|
13
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: _jsxs(Text, { dimColor: true, children: ["No files found matching \"", query, "\""] }) }));
|
|
14
|
+
}
|
|
15
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "\u2191\u2193 navigate, Enter inserts path, Esc cancels" }), filteredFiles.map((file, index) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, bold: index === selectedIndex, children: [index === selectedIndex ? '> ' : ' ', file] }) }, file)))] }));
|
|
16
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export const FileChip = ({ filePath, onRemove }) => {
|
|
5
|
+
const basename = path.basename(filePath);
|
|
6
|
+
const dirname = path.dirname(filePath);
|
|
7
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: basename }), _jsxs(Text, { dimColor: true, children: [" ", dirname !== '.' ? `(${dirname})` : ''] }), _jsx(Text, { color: "red", bold: true, children: " [x]" })] }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
// Palette mapping: Character -> { color: Hex, symbol: Char }
|
|
4
|
+
const PALETTE = {
|
|
5
|
+
// Deep Blue / Background blend
|
|
6
|
+
'1': { color: '#0f20a2', char: '░' },
|
|
7
|
+
// Outer Blue
|
|
8
|
+
'2': { color: '#0020ef', char: '▒' },
|
|
9
|
+
// Mid Blue
|
|
10
|
+
'3': { color: '#002dff', char: '▓' },
|
|
11
|
+
// Bright Blue
|
|
12
|
+
'4': { color: '#0088af', char: '█' },
|
|
13
|
+
// Cyan
|
|
14
|
+
'5': { color: '#00c7f7', char: '█' },
|
|
15
|
+
// Light Cyan
|
|
16
|
+
'6': { color: '#00ffff', char: '█' },
|
|
17
|
+
// White Core
|
|
18
|
+
'7': { color: '#ffffff', char: '█' },
|
|
19
|
+
};
|
|
20
|
+
const HD_FLAME_ART = [
|
|
21
|
+
" 333 ",
|
|
22
|
+
" 2333332 ",
|
|
23
|
+
" 233444332 ",
|
|
24
|
+
" 2334555554332 ",
|
|
25
|
+
" 234556666655432 ",
|
|
26
|
+
" 34567777777776543 ",
|
|
27
|
+
" 2456777777777776542 ",
|
|
28
|
+
" 235677766666667776532 ",
|
|
29
|
+
" 345677655555556776543 ",
|
|
30
|
+
" 23456765444444456765432 ",
|
|
31
|
+
" 23455654333333345655432 ",
|
|
32
|
+
" 12344543222222234544321 ",
|
|
33
|
+
" 11233432111111123433211 "
|
|
34
|
+
];
|
|
35
|
+
export const FlameLogo = () => {
|
|
36
|
+
return (_jsx(Box, { flexDirection: "column", marginRight: 2, children: HD_FLAME_ART.map((row, rowIndex) => (_jsx(Box, { children: row.split('').map((char, colIndex) => {
|
|
37
|
+
const pixel = PALETTE[char];
|
|
38
|
+
return (_jsx(Text, { color: pixel?.color || undefined, children: pixel ? pixel.char : ' ' }, `${rowIndex}-${colIndex}`));
|
|
39
|
+
}) }, rowIndex))) }));
|
|
40
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import BigText from 'ink-big-text';
|
|
4
|
+
export const WordFlame = ({ text, big = false }) => {
|
|
5
|
+
if (big) {
|
|
6
|
+
return (_jsx(Text, { color: "#3EA0C3", children: _jsx(BigText, { text: text, font: "block", colors: ['#0192e5', '#0192e5'] }) }) // BigText needs specific handling or Text wrapper
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return (_jsx(Text, { color: "#0192e5", children: text }));
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import BigText from 'ink-big-text';
|
|
4
|
+
export const WordMoth = ({ text, big = false }) => {
|
|
5
|
+
if (big) {
|
|
6
|
+
return (_jsx(Text, { color: "#3EA0C3", children: _jsx(BigText, { text: text, font: "block", colors: ['#0192e5', '#0192e5'] }) }) // BigText needs specific handling or Text wrapper
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return (_jsx(Text, { color: "#0192e5", children: text }));
|
|
10
|
+
};
|