centaurus-cli 2.9.2 → 2.9.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/dist/cli-adapter.d.ts +6 -3
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +244 -74
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +2 -0
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.d.ts +3 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +35 -1
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/services/ai-autocomplete-agent.d.ts +39 -0
- package/dist/services/ai-autocomplete-agent.d.ts.map +1 -0
- package/dist/services/ai-autocomplete-agent.js +189 -0
- package/dist/services/ai-autocomplete-agent.js.map +1 -0
- package/dist/services/ai-service-client.d.ts +25 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +162 -1
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/auth-handler.js +1 -1
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/local-chat-storage.d.ts +21 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -1
- package/dist/services/local-chat-storage.js +138 -43
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/ollama-service.d.ts +197 -0
- package/dist/services/ollama-service.d.ts.map +1 -0
- package/dist/services/ollama-service.js +324 -0
- package/dist/services/ollama-service.js.map +1 -0
- package/dist/ui/components/App.d.ts +2 -2
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +45 -2
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +2 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +321 -19
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/MultiLineInput.d.ts.map +1 -1
- package/dist/ui/components/MultiLineInput.js +68 -2
- package/dist/ui/components/MultiLineInput.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +5 -1
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/utils/command-history.d.ts +12 -2
- package/dist/utils/command-history.d.ts.map +1 -1
- package/dist/utils/command-history.js +57 -13
- package/dist/utils/command-history.js.map +1 -1
- package/dist/utils/input-classifier.js +1 -1
- package/dist/utils/input-classifier.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import { FileTagAutocomplete } from './FileTagAutocomplete.js';
|
|
|
13
13
|
import { filterCommands } from '../../config/slash-commands.js';
|
|
14
14
|
import { getClipboardImages } from '../../services/clipboard-service.js';
|
|
15
15
|
import { useTerminalDimensions, TERMINAL_HEIGHT_CONSTANTS } from '../../hooks/useTerminalDimensions.js';
|
|
16
|
+
import { AIAutocompleteAgent, AI_AUTOCOMPLETE_DEBOUNCE_MS } from '../../services/ai-autocomplete-agent.js';
|
|
16
17
|
const getVisualLines = (text, width) => {
|
|
17
18
|
const logicalLines = text.split('\n');
|
|
18
19
|
const visualLines = [];
|
|
@@ -50,7 +51,7 @@ const getVisualLines = (text, width) => {
|
|
|
50
51
|
});
|
|
51
52
|
return visualLines;
|
|
52
53
|
};
|
|
53
|
-
export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...', autoAcceptMode, model, planMode = false, commandMode = false, backgroundMode = false, currentWorkingDirectory, commandHistory = [], onToggleAutoAccept, onToggleCommandMode, onToggleBackgroundMode, isActive = true, subshellContext, currentTokens = 0, maxTokens = 1000000, contextLimitReached = false, isShellRunning = false, backgroundTaskCount = 0, initialValue = '', onValueChange, onSetAutoModeSetup, sessionQuotaExhausted = false, sessionQuotaTimeRemaining = '', subAgentCount = 0 }) => {
|
|
54
|
+
export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...', autoAcceptMode, model, planMode = false, commandMode = false, backgroundMode = false, currentWorkingDirectory, commandHistory = [], onToggleAutoAccept, onToggleCommandMode, onToggleBackgroundMode, isActive = true, subshellContext, currentTokens = 0, maxTokens = 1000000, contextLimitReached = false, isShellRunning = false, backgroundTaskCount = 0, initialValue = '', onValueChange, onSetAutoModeSetup, sessionQuotaExhausted = false, sessionQuotaTimeRemaining = '', subAgentCount = 0, aiAutoSuggestEnabled = false, sessionCommands = [] }) => {
|
|
54
55
|
// Use initialValue for first mount, but manage state internally after that
|
|
55
56
|
const [value, setValueInternal] = useState(initialValue);
|
|
56
57
|
const [cursorOffset, setCursorOffset] = useState(0);
|
|
@@ -68,11 +69,18 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
68
69
|
const [detectedIntent, setDetectedIntent] = useState('ai');
|
|
69
70
|
// Autocomplete State
|
|
70
71
|
const [autocompleteSuggestion, setAutocompleteSuggestion] = useState(null);
|
|
72
|
+
// AI Autocomplete State
|
|
73
|
+
const [aiAutocompleteSuggestion, setAiAutocompleteSuggestion] = useState(null);
|
|
74
|
+
const [isAiAutocompleteLoading, setIsAiAutocompleteLoading] = useState(false);
|
|
75
|
+
const aiAutocompleteDebounceRef = useRef(null);
|
|
76
|
+
const aiAutocompleteAbortRef = useRef(null);
|
|
71
77
|
// Undo/Redo State
|
|
72
78
|
const [undoStack, setUndoStack] = useState([]);
|
|
73
79
|
const [redoStack, setRedoStack] = useState([]);
|
|
74
80
|
// Selection State
|
|
75
81
|
const [selection, setSelection] = useState(null);
|
|
82
|
+
// Platform Detection
|
|
83
|
+
const isMac = process.platform === 'darwin';
|
|
76
84
|
// Reject Flash State (turns border red when submission is blocked)
|
|
77
85
|
const [rejectFlash, setRejectFlash] = useState(false);
|
|
78
86
|
// Session Quota Message State (shows quota exhausted message)
|
|
@@ -188,6 +196,26 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
188
196
|
}
|
|
189
197
|
return cwd;
|
|
190
198
|
}, [currentWorkingDirectory]);
|
|
199
|
+
// Determine current environment for command history isolation
|
|
200
|
+
// Format: 'local', 'ssh:user@host', or 'wsl:distroName'
|
|
201
|
+
const currentEnvironment = useMemo(() => {
|
|
202
|
+
if (!subshellContext)
|
|
203
|
+
return 'local';
|
|
204
|
+
if (subshellContext.type === 'ssh') {
|
|
205
|
+
const user = subshellContext.metadata?.username || 'user';
|
|
206
|
+
const host = subshellContext.metadata?.hostname || 'host';
|
|
207
|
+
return `ssh:${user}@${host}`;
|
|
208
|
+
}
|
|
209
|
+
if (subshellContext.type === 'wsl') {
|
|
210
|
+
const distro = subshellContext.metadata?.distroName || 'Ubuntu';
|
|
211
|
+
return `wsl:${distro}`;
|
|
212
|
+
}
|
|
213
|
+
if (subshellContext.type === 'docker') {
|
|
214
|
+
const container = subshellContext.metadata?.containerId || 'container';
|
|
215
|
+
return `docker:${container}`;
|
|
216
|
+
}
|
|
217
|
+
return 'local';
|
|
218
|
+
}, [subshellContext]);
|
|
191
219
|
// Autocomplete Logic
|
|
192
220
|
useEffect(() => {
|
|
193
221
|
if (!value || value.trim() === '') {
|
|
@@ -198,7 +226,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
198
226
|
// OR if we are in Auto mode and it looks like a command
|
|
199
227
|
const shouldSuggest = commandMode || (isAutoMode && detectedIntent === 'command');
|
|
200
228
|
if (shouldSuggest) {
|
|
201
|
-
const matches = CommandHistoryManager.getInstance().getMatches(value, currentDir);
|
|
229
|
+
const matches = CommandHistoryManager.getInstance().getMatches(value, currentDir, currentEnvironment);
|
|
202
230
|
if (matches.length > 0) {
|
|
203
231
|
setAutocompleteSuggestion(matches[0]);
|
|
204
232
|
}
|
|
@@ -209,7 +237,137 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
209
237
|
else {
|
|
210
238
|
setAutocompleteSuggestion(null);
|
|
211
239
|
}
|
|
212
|
-
}, [value, commandMode, isAutoMode, detectedIntent, currentDir]);
|
|
240
|
+
}, [value, commandMode, isAutoMode, detectedIntent, currentDir, currentEnvironment]);
|
|
241
|
+
// AI Autocomplete Logic (5-second debounce)
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
// Clear AI suggestion when value changes
|
|
244
|
+
setAiAutocompleteSuggestion(null);
|
|
245
|
+
// Clear any existing debounce timer
|
|
246
|
+
if (aiAutocompleteDebounceRef.current) {
|
|
247
|
+
clearTimeout(aiAutocompleteDebounceRef.current);
|
|
248
|
+
aiAutocompleteDebounceRef.current = null;
|
|
249
|
+
}
|
|
250
|
+
// Abort any pending AI request
|
|
251
|
+
if (aiAutocompleteAbortRef.current) {
|
|
252
|
+
aiAutocompleteAbortRef.current.abort();
|
|
253
|
+
aiAutocompleteAbortRef.current = null;
|
|
254
|
+
}
|
|
255
|
+
// Debug logging for trigger ref
|
|
256
|
+
/*
|
|
257
|
+
try {
|
|
258
|
+
if (detectedIntent !== 'ai' || value.length > 5) {
|
|
259
|
+
quickLog(`[InputBox] AI Effect: val="${value}", enabled=${aiAutoSuggestEnabled}, mode=${commandMode}, auto=${isAutoMode}, intent=${detectedIntent}`);
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {}
|
|
262
|
+
*/
|
|
263
|
+
// Don't trigger AI autocomplete if disabled or not in command mode
|
|
264
|
+
if (!aiAutoSuggestEnabled) {
|
|
265
|
+
// quickLog('[InputBox] AI skipped: disabled');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const shouldSuggest = commandMode || (isAutoMode && detectedIntent === 'command');
|
|
269
|
+
if (!shouldSuggest) {
|
|
270
|
+
// if (value.length > 3) quickLog(`[InputBox] AI skipped: !shouldSuggest (cmd=${commandMode}, auto=${isAutoMode}, intent=${detectedIntent})`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// Don't suggest for empty, very short, or slash commands
|
|
274
|
+
if (!value || value.trim().length < 2 || value.startsWith('/')) {
|
|
275
|
+
// quickLog('[InputBox] AI skipped: too short or slash');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// quickLog('[InputBox] AI Triggering debounce...');
|
|
279
|
+
// Set up 5-second debounce timer
|
|
280
|
+
aiAutocompleteDebounceRef.current = setTimeout(async () => {
|
|
281
|
+
setIsAiAutocompleteLoading(true);
|
|
282
|
+
// Create abort controller for this request
|
|
283
|
+
const abortController = new AbortController();
|
|
284
|
+
aiAutocompleteAbortRef.current = abortController;
|
|
285
|
+
try {
|
|
286
|
+
if (!aiAutoSuggestEnabled)
|
|
287
|
+
return; // double check inside timeout
|
|
288
|
+
// Get directory history using the correct method
|
|
289
|
+
const directoryHistory = CommandHistoryManager.getInstance().getDirectoryHistory(currentDir, currentEnvironment);
|
|
290
|
+
// Get files in current directory (local or remote)
|
|
291
|
+
let files = [];
|
|
292
|
+
try {
|
|
293
|
+
if (subshellContext && subshellContext.type !== 'local' && subshellContext.handler) {
|
|
294
|
+
// Remote/Subshell environment
|
|
295
|
+
// Check if we can list files
|
|
296
|
+
try {
|
|
297
|
+
const dirEntries = await subshellContext.handler.listDirectory(currentDir);
|
|
298
|
+
files = dirEntries
|
|
299
|
+
.slice(0, 50)
|
|
300
|
+
.map((e) => e.name + (e.type === 'directory' ? '/' : ''));
|
|
301
|
+
}
|
|
302
|
+
catch (remoteErr) {
|
|
303
|
+
// quickLog('Remote list error: ' + remoteErr);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// Local environment
|
|
308
|
+
const dir = currentDir || process.cwd();
|
|
309
|
+
if (fs.existsSync(dir)) {
|
|
310
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
311
|
+
files = entries
|
|
312
|
+
.slice(0, 50)
|
|
313
|
+
.map(e => e.name + (e.isDirectory() ? '/' : ''));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
// Ignore file access errors
|
|
319
|
+
}
|
|
320
|
+
// Determine OS and Platform from subshell context if available
|
|
321
|
+
let osContext = process.platform;
|
|
322
|
+
let platformContext = process.platform;
|
|
323
|
+
if (subshellContext && subshellContext.type !== 'local' && subshellContext.metadata) {
|
|
324
|
+
const metaOs = subshellContext.metadata.os;
|
|
325
|
+
if (metaOs === 'windows') {
|
|
326
|
+
osContext = 'win32';
|
|
327
|
+
platformContext = 'win32';
|
|
328
|
+
}
|
|
329
|
+
else if (metaOs === 'macos') {
|
|
330
|
+
osContext = 'darwin';
|
|
331
|
+
platformContext = 'darwin';
|
|
332
|
+
}
|
|
333
|
+
else if (metaOs === 'linux') {
|
|
334
|
+
osContext = 'linux';
|
|
335
|
+
platformContext = 'linux';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const context = {
|
|
339
|
+
os: osContext,
|
|
340
|
+
platform: platformContext,
|
|
341
|
+
cwd: currentDir || process.cwd(),
|
|
342
|
+
directoryHistory: directoryHistory.slice(0, 10),
|
|
343
|
+
sessionCommands: sessionCommands.slice(-10),
|
|
344
|
+
files: files,
|
|
345
|
+
currentInput: value
|
|
346
|
+
};
|
|
347
|
+
const prediction = await AIAutocompleteAgent.predictCommand(context, abortController.signal);
|
|
348
|
+
if (prediction && !abortController.signal.aborted) {
|
|
349
|
+
setAiAutocompleteSuggestion(prediction);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
// Ignore errors (likely aborted)
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
setIsAiAutocompleteLoading(false);
|
|
357
|
+
if (aiAutocompleteAbortRef.current === abortController) {
|
|
358
|
+
aiAutocompleteAbortRef.current = null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}, AI_AUTOCOMPLETE_DEBOUNCE_MS);
|
|
362
|
+
return () => {
|
|
363
|
+
if (aiAutocompleteDebounceRef.current) {
|
|
364
|
+
clearTimeout(aiAutocompleteDebounceRef.current);
|
|
365
|
+
}
|
|
366
|
+
if (aiAutocompleteAbortRef.current) {
|
|
367
|
+
aiAutocompleteAbortRef.current.abort();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}, [value, commandMode, isAutoMode, detectedIntent, aiAutoSuggestEnabled, currentDir, currentEnvironment, sessionCommands]);
|
|
213
371
|
// Auto-classification effect (Synchronous Heuristics Only)
|
|
214
372
|
useEffect(() => {
|
|
215
373
|
// Only run classification if in Auto Mode
|
|
@@ -398,6 +556,42 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
398
556
|
setCursorOffset(newValue.length);
|
|
399
557
|
setSlashAutocompleteVisible(false);
|
|
400
558
|
}
|
|
559
|
+
else if (value.startsWith('/models ') || value.startsWith('/model ')) {
|
|
560
|
+
// We're selecting a models subcommand
|
|
561
|
+
const prefix = value.startsWith('/models ') ? '/models ' : '/model ';
|
|
562
|
+
const newValue = `${prefix}${selected.name}`;
|
|
563
|
+
setValue(newValue);
|
|
564
|
+
setCursorOffset(newValue.length);
|
|
565
|
+
setSlashAutocompleteVisible(false);
|
|
566
|
+
}
|
|
567
|
+
else if (value.startsWith('/settings auto-suggest ')) {
|
|
568
|
+
// We're selecting an auto-suggest option (on/off)
|
|
569
|
+
const newValue = `/settings auto-suggest ${selected.name}`;
|
|
570
|
+
setValue(newValue);
|
|
571
|
+
setCursorOffset(newValue.length);
|
|
572
|
+
setSlashAutocompleteVisible(false);
|
|
573
|
+
}
|
|
574
|
+
else if (value.startsWith('/settings ')) {
|
|
575
|
+
// We're selecting a settings subcommand (e.g., auto-suggest)
|
|
576
|
+
const newValue = `/settings ${selected.name} `;
|
|
577
|
+
setValue(newValue);
|
|
578
|
+
setCursorOffset(newValue.length);
|
|
579
|
+
// Show the next level options (on/off for auto-suggest)
|
|
580
|
+
if (selected.name === 'auto-suggest') {
|
|
581
|
+
const optionMatches = filterCommands('settings auto-suggest ');
|
|
582
|
+
if (optionMatches.length > 0) {
|
|
583
|
+
setSlashAutocompleteCommands(optionMatches);
|
|
584
|
+
setSlashAutocompleteSelectedIndex(0);
|
|
585
|
+
setSlashAutocompleteScrollOffset(0);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
setSlashAutocompleteVisible(false);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
setSlashAutocompleteVisible(false);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
401
595
|
else {
|
|
402
596
|
// Regular slash command, replace everything
|
|
403
597
|
const newValue = `/${selected.name} `;
|
|
@@ -465,6 +659,30 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
465
659
|
setSlashAutocompleteVisible(false);
|
|
466
660
|
}
|
|
467
661
|
}
|
|
662
|
+
else if (selected.name === 'models' || selected.name === 'model') {
|
|
663
|
+
const subcommandMatches = filterCommands('models ');
|
|
664
|
+
if (subcommandMatches.length > 0) {
|
|
665
|
+
setSlashAutocompleteCommands(subcommandMatches);
|
|
666
|
+
setSlashAutocompleteSelectedIndex(0);
|
|
667
|
+
setSlashAutocompleteScrollOffset(0);
|
|
668
|
+
// Keep autocomplete visible for subcommands
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
setSlashAutocompleteVisible(false);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else if (selected.name === 'settings') {
|
|
675
|
+
const subcommandMatches = filterCommands('settings ');
|
|
676
|
+
if (subcommandMatches.length > 0) {
|
|
677
|
+
setSlashAutocompleteCommands(subcommandMatches);
|
|
678
|
+
setSlashAutocompleteSelectedIndex(0);
|
|
679
|
+
setSlashAutocompleteScrollOffset(0);
|
|
680
|
+
// Keep autocomplete visible for subcommands
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
setSlashAutocompleteVisible(false);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
468
686
|
else {
|
|
469
687
|
setSlashAutocompleteVisible(false);
|
|
470
688
|
}
|
|
@@ -688,6 +906,20 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
688
906
|
setSlashAutocompleteVisible(false);
|
|
689
907
|
}
|
|
690
908
|
}
|
|
909
|
+
else if (newValue.startsWith('/models ') || newValue.startsWith('/model ')) {
|
|
910
|
+
// Models subcommands
|
|
911
|
+
const fullQuery = newValue.slice(1);
|
|
912
|
+
const matches = filterCommands(fullQuery);
|
|
913
|
+
if (matches.length > 0) {
|
|
914
|
+
setSlashAutocompleteCommands(matches);
|
|
915
|
+
setSlashAutocompleteVisible(true);
|
|
916
|
+
setSlashAutocompleteSelectedIndex(0);
|
|
917
|
+
setSlashAutocompleteScrollOffset(0);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
setSlashAutocompleteVisible(false);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
691
923
|
else {
|
|
692
924
|
setSlashAutocompleteVisible(false);
|
|
693
925
|
}
|
|
@@ -743,17 +975,52 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
743
975
|
}
|
|
744
976
|
return;
|
|
745
977
|
}
|
|
746
|
-
// Ctrl+Z: Undo
|
|
747
|
-
if (key.ctrl && input.toLowerCase() === 'z') {
|
|
978
|
+
// Ctrl+Z / Cmd+Z: Undo
|
|
979
|
+
if ((key.ctrl && input.toLowerCase() === 'z') || (key.meta && input.toLowerCase() === 'z')) {
|
|
748
980
|
handleUndo();
|
|
749
981
|
return;
|
|
750
982
|
}
|
|
751
|
-
// Ctrl+A: Select All
|
|
752
|
-
if (key.ctrl && input.toLowerCase() === 'a') {
|
|
983
|
+
// Ctrl+A / Cmd+A: Select All
|
|
984
|
+
if ((key.ctrl && input.toLowerCase() === 'a') || (key.meta && input.toLowerCase() === 'a')) {
|
|
753
985
|
setSelection({ start: 0, end: value.length });
|
|
754
986
|
setCursorOffset(value.length);
|
|
755
987
|
return;
|
|
756
988
|
}
|
|
989
|
+
// Home: Start of Line
|
|
990
|
+
// Note: In single-line inputs, this goes to start of text.
|
|
991
|
+
// In multi-line wrapped view, we ideally want start of logical line or start of text?
|
|
992
|
+
// Standard terminal Home = Start of command. Editor Home = Start of line.
|
|
993
|
+
// Let's stick to Start of Text for now as it's a single "input box".
|
|
994
|
+
// Home: Start of Line
|
|
995
|
+
// @ts-ignore
|
|
996
|
+
if (key.home) {
|
|
997
|
+
const width = (process.stdout.columns || 80) - 6;
|
|
998
|
+
const visualLines = getVisualLines(value, width);
|
|
999
|
+
const currentLine = visualLines.find(line => (cursorOffset >= line.start && cursorOffset < line.end) ||
|
|
1000
|
+
(cursorOffset === line.end && line.isHardEnd));
|
|
1001
|
+
if (currentLine) {
|
|
1002
|
+
setCursorOffset(currentLine.start);
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
setCursorOffset(0);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
// End: End of Line
|
|
1010
|
+
// @ts-ignore
|
|
1011
|
+
if (key.end) {
|
|
1012
|
+
const width = (process.stdout.columns || 80) - 6;
|
|
1013
|
+
const visualLines = getVisualLines(value, width);
|
|
1014
|
+
const currentLine = visualLines.find(line => (cursorOffset >= line.start && cursorOffset < line.end) ||
|
|
1015
|
+
(cursorOffset === line.end && line.isHardEnd));
|
|
1016
|
+
if (currentLine) {
|
|
1017
|
+
setCursorOffset(currentLine.end);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
setCursorOffset(value.length);
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
757
1024
|
// Note: Clipboard images are handled via Alt+V keyboard shortcut
|
|
758
1025
|
// DELETE CHAR - Only runs if Delete Word did NOT trigger
|
|
759
1026
|
// Triggers on:
|
|
@@ -870,6 +1137,20 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
870
1137
|
setSlashAutocompleteVisible(false);
|
|
871
1138
|
}
|
|
872
1139
|
}
|
|
1140
|
+
else if (newValue.startsWith('/models ') || newValue.startsWith('/model ')) {
|
|
1141
|
+
// Models subcommands
|
|
1142
|
+
const fullQuery = newValue.slice(1);
|
|
1143
|
+
const matches = filterCommands(fullQuery);
|
|
1144
|
+
if (matches.length > 0) {
|
|
1145
|
+
setSlashAutocompleteCommands(matches);
|
|
1146
|
+
setSlashAutocompleteVisible(true);
|
|
1147
|
+
setSlashAutocompleteSelectedIndex(0);
|
|
1148
|
+
setSlashAutocompleteScrollOffset(0);
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
setSlashAutocompleteVisible(false);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
873
1154
|
else {
|
|
874
1155
|
setSlashAutocompleteVisible(false);
|
|
875
1156
|
}
|
|
@@ -1043,8 +1324,8 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
1043
1324
|
return;
|
|
1044
1325
|
}
|
|
1045
1326
|
if (key.leftArrow) {
|
|
1046
|
-
if (key.ctrl) {
|
|
1047
|
-
// Ctrl+Left: Move word backwards
|
|
1327
|
+
if (key.ctrl || key.meta) {
|
|
1328
|
+
// Ctrl+Left / Meta+Left (Option+Left): Move word backwards
|
|
1048
1329
|
let newOffset = cursorOffset;
|
|
1049
1330
|
if (newOffset > 0) {
|
|
1050
1331
|
// Skip whitespace backwards
|
|
@@ -1065,17 +1346,20 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
1065
1346
|
}
|
|
1066
1347
|
if (key.rightArrow) {
|
|
1067
1348
|
// Autocomplete Logic (Only at end of line)
|
|
1068
|
-
|
|
1349
|
+
// AI suggestion takes priority over passive suggestion
|
|
1350
|
+
const effectiveSuggestion = aiAutocompleteSuggestion || autocompleteSuggestion;
|
|
1351
|
+
if (effectiveSuggestion && cursorOffset === value.length) {
|
|
1069
1352
|
if (key.ctrl) {
|
|
1070
1353
|
// Ctrl+Right: Accept FULL suggestion
|
|
1071
|
-
setValue(
|
|
1072
|
-
setCursorOffset(
|
|
1354
|
+
setValue(effectiveSuggestion);
|
|
1355
|
+
setCursorOffset(effectiveSuggestion.length);
|
|
1073
1356
|
setAutocompleteSuggestion(null);
|
|
1357
|
+
setAiAutocompleteSuggestion(null);
|
|
1074
1358
|
return;
|
|
1075
1359
|
}
|
|
1076
1360
|
else {
|
|
1077
1361
|
// Right: Accept NEXT WORD
|
|
1078
|
-
const remaining =
|
|
1362
|
+
const remaining = effectiveSuggestion.slice(value.length);
|
|
1079
1363
|
// Match next chunk of non-whitespace (including preceding whitespace)
|
|
1080
1364
|
const match = remaining.match(/^(\s*\S+)/);
|
|
1081
1365
|
if (match) {
|
|
@@ -1095,8 +1379,8 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
1095
1379
|
}
|
|
1096
1380
|
}
|
|
1097
1381
|
// Navigation Logic (if not completing)
|
|
1098
|
-
if (key.ctrl) {
|
|
1099
|
-
// Ctrl+Right: Move word forwards
|
|
1382
|
+
if (key.ctrl || key.meta) {
|
|
1383
|
+
// Ctrl+Right / Meta+Right (Option+Right): Move word forwards
|
|
1100
1384
|
let newOffset = cursorOffset;
|
|
1101
1385
|
if (newOffset < value.length) {
|
|
1102
1386
|
// Skip non-whitespace forwards
|
|
@@ -1239,6 +1523,20 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
1239
1523
|
setSlashAutocompleteVisible(false);
|
|
1240
1524
|
}
|
|
1241
1525
|
}
|
|
1526
|
+
else if (newValue.startsWith('/models ') || newValue.startsWith('/model ')) {
|
|
1527
|
+
// Models subcommands (when user types "/models " or "/model ")
|
|
1528
|
+
const fullQuery = newValue.slice(1); // Remove leading "/", pass "models <subquery>" to filterCommands
|
|
1529
|
+
const matches = filterCommands(fullQuery);
|
|
1530
|
+
if (matches.length > 0) {
|
|
1531
|
+
setSlashAutocompleteCommands(matches);
|
|
1532
|
+
setSlashAutocompleteVisible(true);
|
|
1533
|
+
setSlashAutocompleteSelectedIndex(0);
|
|
1534
|
+
setSlashAutocompleteScrollOffset(0);
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
setSlashAutocompleteVisible(false);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1242
1540
|
else {
|
|
1243
1541
|
setSlashAutocompleteVisible(false);
|
|
1244
1542
|
}
|
|
@@ -1324,7 +1622,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
1324
1622
|
if (trimmedValue) {
|
|
1325
1623
|
// Save to history if it was a command
|
|
1326
1624
|
if (commandMode) {
|
|
1327
|
-
CommandHistoryManager.getInstance().addCommand(trimmedValue, currentDir);
|
|
1625
|
+
CommandHistoryManager.getInstance().addCommand(trimmedValue, currentDir, currentEnvironment);
|
|
1328
1626
|
}
|
|
1329
1627
|
// Resolve file tags (@filename -> absolute path)
|
|
1330
1628
|
let resolvedValue = trimmedValue;
|
|
@@ -1491,10 +1789,14 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
|
|
|
1491
1789
|
}
|
|
1492
1790
|
// Render Autocomplete Ghost Text
|
|
1493
1791
|
// Only on the last line, if suggestion exists and matches start
|
|
1494
|
-
|
|
1495
|
-
|
|
1792
|
+
// AI suggestion takes priority over passive suggestion
|
|
1793
|
+
const effectiveSuggestion = aiAutocompleteSuggestion || autocompleteSuggestion;
|
|
1794
|
+
if (isLastLine && effectiveSuggestion && effectiveSuggestion.startsWith(value)) {
|
|
1795
|
+
const suffix = effectiveSuggestion.slice(value.length);
|
|
1496
1796
|
if (suffix) {
|
|
1497
|
-
|
|
1797
|
+
// Use slightly different color for AI suggestion to differentiate
|
|
1798
|
+
const ghostColor = aiAutocompleteSuggestion ? '#888888' : 'gray';
|
|
1799
|
+
renderedChars.push(React.createElement(Text, { key: "ghost", color: ghostColor }, suffix));
|
|
1498
1800
|
}
|
|
1499
1801
|
}
|
|
1500
1802
|
if (renderedChars.length === 0) {
|