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.
Files changed (54) hide show
  1. package/dist/cli-adapter.d.ts +6 -3
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +244 -74
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/models.d.ts.map +1 -1
  6. package/dist/config/models.js +2 -0
  7. package/dist/config/models.js.map +1 -1
  8. package/dist/config/slash-commands.d.ts +3 -0
  9. package/dist/config/slash-commands.d.ts.map +1 -1
  10. package/dist/config/slash-commands.js +35 -1
  11. package/dist/config/slash-commands.js.map +1 -1
  12. package/dist/config/types.d.ts +2 -0
  13. package/dist/config/types.d.ts.map +1 -1
  14. package/dist/config/types.js +1 -0
  15. package/dist/config/types.js.map +1 -1
  16. package/dist/services/ai-autocomplete-agent.d.ts +39 -0
  17. package/dist/services/ai-autocomplete-agent.d.ts.map +1 -0
  18. package/dist/services/ai-autocomplete-agent.js +189 -0
  19. package/dist/services/ai-autocomplete-agent.js.map +1 -0
  20. package/dist/services/ai-service-client.d.ts +25 -0
  21. package/dist/services/ai-service-client.d.ts.map +1 -1
  22. package/dist/services/ai-service-client.js +162 -1
  23. package/dist/services/ai-service-client.js.map +1 -1
  24. package/dist/services/auth-handler.js +1 -1
  25. package/dist/services/auth-handler.js.map +1 -1
  26. package/dist/services/local-chat-storage.d.ts +21 -0
  27. package/dist/services/local-chat-storage.d.ts.map +1 -1
  28. package/dist/services/local-chat-storage.js +138 -43
  29. package/dist/services/local-chat-storage.js.map +1 -1
  30. package/dist/services/ollama-service.d.ts +197 -0
  31. package/dist/services/ollama-service.d.ts.map +1 -0
  32. package/dist/services/ollama-service.js +324 -0
  33. package/dist/services/ollama-service.js.map +1 -0
  34. package/dist/ui/components/App.d.ts +2 -2
  35. package/dist/ui/components/App.d.ts.map +1 -1
  36. package/dist/ui/components/App.js +45 -2
  37. package/dist/ui/components/App.js.map +1 -1
  38. package/dist/ui/components/InputBox.d.ts +2 -0
  39. package/dist/ui/components/InputBox.d.ts.map +1 -1
  40. package/dist/ui/components/InputBox.js +321 -19
  41. package/dist/ui/components/InputBox.js.map +1 -1
  42. package/dist/ui/components/MultiLineInput.d.ts.map +1 -1
  43. package/dist/ui/components/MultiLineInput.js +68 -2
  44. package/dist/ui/components/MultiLineInput.js.map +1 -1
  45. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  46. package/dist/ui/components/ToolExecutionMessage.js +5 -1
  47. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  48. package/dist/utils/command-history.d.ts +12 -2
  49. package/dist/utils/command-history.d.ts.map +1 -1
  50. package/dist/utils/command-history.js +57 -13
  51. package/dist/utils/command-history.js.map +1 -1
  52. package/dist/utils/input-classifier.js +1 -1
  53. package/dist/utils/input-classifier.js.map +1 -1
  54. 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
- if (autocompleteSuggestion && cursorOffset === value.length) {
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(autocompleteSuggestion);
1072
- setCursorOffset(autocompleteSuggestion.length);
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 = autocompleteSuggestion.slice(value.length);
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
- if (isLastLine && autocompleteSuggestion && autocompleteSuggestion.startsWith(value)) {
1495
- const suffix = autocompleteSuggestion.slice(value.length);
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
- renderedChars.push(React.createElement(Text, { key: "ghost", color: "gray" }, suffix));
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) {