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.
Files changed (42) hide show
  1. package/README.md +319 -0
  2. package/dist/agent/orchestrator.js +141 -0
  3. package/dist/agent/types.js +1 -0
  4. package/dist/config/configManager.js +62 -0
  5. package/dist/config/keychain.js +20 -0
  6. package/dist/context/ignore.js +27 -0
  7. package/dist/context/manager.js +62 -0
  8. package/dist/context/scanner.js +41 -0
  9. package/dist/context/types.js +1 -0
  10. package/dist/editing/patcher.js +37 -0
  11. package/dist/index.js +401 -0
  12. package/dist/llm/claudeAdapter.js +47 -0
  13. package/dist/llm/cohereAdapter.js +42 -0
  14. package/dist/llm/factory.js +30 -0
  15. package/dist/llm/geminiAdapter.js +55 -0
  16. package/dist/llm/openAIAdapter.js +45 -0
  17. package/dist/llm/types.js +1 -0
  18. package/dist/planning/todoManager.js +23 -0
  19. package/dist/tools/definitions.js +187 -0
  20. package/dist/tools/factory.js +196 -0
  21. package/dist/tools/registry.js +21 -0
  22. package/dist/tools/types.js +1 -0
  23. package/dist/ui/App.js +387 -0
  24. package/dist/ui/ProfileManager.js +51 -0
  25. package/dist/ui/components/CommandPalette.js +29 -0
  26. package/dist/ui/components/CustomTextInput.js +75 -0
  27. package/dist/ui/components/FileAutocomplete.js +16 -0
  28. package/dist/ui/components/FileChip.js +8 -0
  29. package/dist/ui/components/FlameLogo.js +40 -0
  30. package/dist/ui/components/WordFlame.js +10 -0
  31. package/dist/ui/components/WordMoth.js +10 -0
  32. package/dist/ui/wizards/LLMRemover.js +68 -0
  33. package/dist/ui/wizards/LLMWizard.js +149 -0
  34. package/dist/utils/fileUtils.js +67 -0
  35. package/dist/utils/paths.js +22 -0
  36. package/dist/utils/text.js +49 -0
  37. package/docs/architecture.md +63 -0
  38. package/docs/core_logic.md +53 -0
  39. package/docs/index.md +30 -0
  40. package/docs/llm_integration.md +49 -0
  41. package/docs/ui_components.md +44 -0
  42. 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
+ };