centaurus-cli 2.9.2 → 2.9.4

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 (121) hide show
  1. package/dist/cli-adapter.d.ts +78 -11
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +851 -215
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/commands/CommandParser.d.ts +1 -1
  6. package/dist/commands/CommandParser.d.ts.map +1 -1
  7. package/dist/commands/CommandParser.js +113 -0
  8. package/dist/commands/CommandParser.js.map +1 -1
  9. package/dist/config/models.d.ts.map +1 -1
  10. package/dist/config/models.js +2 -0
  11. package/dist/config/models.js.map +1 -1
  12. package/dist/config/slash-commands.d.ts +5 -0
  13. package/dist/config/slash-commands.d.ts.map +1 -1
  14. package/dist/config/slash-commands.js +63 -1
  15. package/dist/config/slash-commands.js.map +1 -1
  16. package/dist/config/types.d.ts +2 -0
  17. package/dist/config/types.d.ts.map +1 -1
  18. package/dist/config/types.js +1 -0
  19. package/dist/config/types.js.map +1 -1
  20. package/dist/context/context-manager.d.ts +1 -1
  21. package/dist/context/context-manager.d.ts.map +1 -1
  22. package/dist/context/context-manager.js +3 -1
  23. package/dist/context/context-manager.js.map +1 -1
  24. package/dist/context/handlers/docker-handler.d.ts +9 -0
  25. package/dist/context/handlers/docker-handler.d.ts.map +1 -1
  26. package/dist/context/handlers/docker-handler.js +99 -10
  27. package/dist/context/handlers/docker-handler.js.map +1 -1
  28. package/dist/context/handlers/ssh-handler.d.ts +20 -0
  29. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  30. package/dist/context/handlers/ssh-handler.js +129 -1
  31. package/dist/context/handlers/ssh-handler.js.map +1 -1
  32. package/dist/context/subshell-handler.d.ts +15 -0
  33. package/dist/context/subshell-handler.d.ts.map +1 -1
  34. package/dist/index.js +10 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/services/ai-autocomplete-agent.d.ts +39 -0
  37. package/dist/services/ai-autocomplete-agent.d.ts.map +1 -0
  38. package/dist/services/ai-autocomplete-agent.js +189 -0
  39. package/dist/services/ai-autocomplete-agent.js.map +1 -0
  40. package/dist/services/ai-service-client.d.ts +25 -0
  41. package/dist/services/ai-service-client.d.ts.map +1 -1
  42. package/dist/services/ai-service-client.js +195 -12
  43. package/dist/services/ai-service-client.js.map +1 -1
  44. package/dist/services/api-client.js +1 -1
  45. package/dist/services/api-client.js.map +1 -1
  46. package/dist/services/auth-handler.js +1 -1
  47. package/dist/services/auth-handler.js.map +1 -1
  48. package/dist/services/local-chat-storage.d.ts +21 -0
  49. package/dist/services/local-chat-storage.d.ts.map +1 -1
  50. package/dist/services/local-chat-storage.js +138 -43
  51. package/dist/services/local-chat-storage.js.map +1 -1
  52. package/dist/services/ollama-service.d.ts +197 -0
  53. package/dist/services/ollama-service.d.ts.map +1 -0
  54. package/dist/services/ollama-service.js +324 -0
  55. package/dist/services/ollama-service.js.map +1 -0
  56. package/dist/services/warpify-detector.d.ts +43 -0
  57. package/dist/services/warpify-detector.d.ts.map +1 -0
  58. package/dist/services/warpify-detector.js +203 -0
  59. package/dist/services/warpify-detector.js.map +1 -0
  60. package/dist/services/workflow-storage.d.ts +72 -0
  61. package/dist/services/workflow-storage.d.ts.map +1 -0
  62. package/dist/services/workflow-storage.js +239 -0
  63. package/dist/services/workflow-storage.js.map +1 -0
  64. package/dist/tools/command.d.ts.map +1 -1
  65. package/dist/tools/command.js +14 -0
  66. package/dist/tools/command.js.map +1 -1
  67. package/dist/tools/enter-remote-session.d.ts +13 -0
  68. package/dist/tools/enter-remote-session.d.ts.map +1 -0
  69. package/dist/tools/enter-remote-session.js +226 -0
  70. package/dist/tools/enter-remote-session.js.map +1 -0
  71. package/dist/tools/find-files.d.ts.map +1 -1
  72. package/dist/tools/find-files.js +9 -2
  73. package/dist/tools/find-files.js.map +1 -1
  74. package/dist/tools/grep-search.d.ts +104 -31
  75. package/dist/tools/grep-search.d.ts.map +1 -1
  76. package/dist/tools/grep-search.js +699 -430
  77. package/dist/tools/grep-search.js.map +1 -1
  78. package/dist/tools/workflow-tool.d.ts +11 -0
  79. package/dist/tools/workflow-tool.d.ts.map +1 -0
  80. package/dist/tools/workflow-tool.js +87 -0
  81. package/dist/tools/workflow-tool.js.map +1 -0
  82. package/dist/types/workflow.d.ts +110 -0
  83. package/dist/types/workflow.d.ts.map +1 -0
  84. package/dist/types/workflow.js +8 -0
  85. package/dist/types/workflow.js.map +1 -0
  86. package/dist/ui/components/App.d.ts +12 -3
  87. package/dist/ui/components/App.d.ts.map +1 -1
  88. package/dist/ui/components/App.js +162 -6
  89. package/dist/ui/components/App.js.map +1 -1
  90. package/dist/ui/components/Breadcrumbs.d.ts +4 -3
  91. package/dist/ui/components/Breadcrumbs.d.ts.map +1 -1
  92. package/dist/ui/components/Breadcrumbs.js +60 -54
  93. package/dist/ui/components/Breadcrumbs.js.map +1 -1
  94. package/dist/ui/components/ConnectionStatusMessage.js +2 -2
  95. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  96. package/dist/ui/components/InputBox.d.ts +3 -0
  97. package/dist/ui/components/InputBox.d.ts.map +1 -1
  98. package/dist/ui/components/InputBox.js +488 -20
  99. package/dist/ui/components/InputBox.js.map +1 -1
  100. package/dist/ui/components/InteractiveShell.d.ts +2 -0
  101. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  102. package/dist/ui/components/InteractiveShell.js +13 -3
  103. package/dist/ui/components/InteractiveShell.js.map +1 -1
  104. package/dist/ui/components/MultiLineInput.d.ts.map +1 -1
  105. package/dist/ui/components/MultiLineInput.js +68 -2
  106. package/dist/ui/components/MultiLineInput.js.map +1 -1
  107. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  108. package/dist/ui/components/ToolExecutionMessage.js +169 -26
  109. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  110. package/dist/ui/components/WorkflowCreatorScreen.d.ts +25 -0
  111. package/dist/ui/components/WorkflowCreatorScreen.d.ts.map +1 -0
  112. package/dist/ui/components/WorkflowCreatorScreen.js +164 -0
  113. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -0
  114. package/dist/utils/command-history.d.ts +12 -2
  115. package/dist/utils/command-history.d.ts.map +1 -1
  116. package/dist/utils/command-history.js +57 -13
  117. package/dist/utils/command-history.js.map +1 -1
  118. package/dist/utils/input-classifier.d.ts.map +1 -1
  119. package/dist/utils/input-classifier.js +3 -2
  120. package/dist/utils/input-classifier.js.map +1 -1
  121. package/package.json +1 -1
@@ -13,6 +13,8 @@ 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';
17
+ import { workflowStorage } from '../../services/workflow-storage.js';
16
18
  const getVisualLines = (text, width) => {
17
19
  const logicalLines = text.split('\n');
18
20
  const visualLines = [];
@@ -50,7 +52,7 @@ const getVisualLines = (text, width) => {
50
52
  });
51
53
  return visualLines;
52
54
  };
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 }) => {
55
+ 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, subshellContextStack, currentTokens = 0, maxTokens = 1000000, contextLimitReached = false, isShellRunning = false, backgroundTaskCount = 0, initialValue = '', onValueChange, onSetAutoModeSetup, sessionQuotaExhausted = false, sessionQuotaTimeRemaining = '', subAgentCount = 0, aiAutoSuggestEnabled = false, sessionCommands = [] }) => {
54
56
  // Use initialValue for first mount, but manage state internally after that
55
57
  const [value, setValueInternal] = useState(initialValue);
56
58
  const [cursorOffset, setCursorOffset] = useState(0);
@@ -68,11 +70,18 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
68
70
  const [detectedIntent, setDetectedIntent] = useState('ai');
69
71
  // Autocomplete State
70
72
  const [autocompleteSuggestion, setAutocompleteSuggestion] = useState(null);
73
+ // AI Autocomplete State
74
+ const [aiAutocompleteSuggestion, setAiAutocompleteSuggestion] = useState(null);
75
+ const [isAiAutocompleteLoading, setIsAiAutocompleteLoading] = useState(false);
76
+ const aiAutocompleteDebounceRef = useRef(null);
77
+ const aiAutocompleteAbortRef = useRef(null);
71
78
  // Undo/Redo State
72
79
  const [undoStack, setUndoStack] = useState([]);
73
80
  const [redoStack, setRedoStack] = useState([]);
74
81
  // Selection State
75
82
  const [selection, setSelection] = useState(null);
83
+ // Platform Detection
84
+ const isMac = process.platform === 'darwin';
76
85
  // Reject Flash State (turns border red when submission is blocked)
77
86
  const [rejectFlash, setRejectFlash] = useState(false);
78
87
  // Session Quota Message State (shows quota exhausted message)
@@ -188,6 +197,26 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
188
197
  }
189
198
  return cwd;
190
199
  }, [currentWorkingDirectory]);
200
+ // Determine current environment for command history isolation
201
+ // Format: 'local', 'ssh:user@host', or 'wsl:distroName'
202
+ const currentEnvironment = useMemo(() => {
203
+ if (!subshellContext)
204
+ return 'local';
205
+ if (subshellContext.type === 'ssh') {
206
+ const user = subshellContext.metadata?.username || 'user';
207
+ const host = subshellContext.metadata?.hostname || 'host';
208
+ return `ssh:${user}@${host}`;
209
+ }
210
+ if (subshellContext.type === 'wsl') {
211
+ const distro = subshellContext.metadata?.distroName || 'Ubuntu';
212
+ return `wsl:${distro}`;
213
+ }
214
+ if (subshellContext.type === 'docker') {
215
+ const container = subshellContext.metadata?.containerId || 'container';
216
+ return `docker:${container}`;
217
+ }
218
+ return 'local';
219
+ }, [subshellContext]);
191
220
  // Autocomplete Logic
192
221
  useEffect(() => {
193
222
  if (!value || value.trim() === '') {
@@ -198,7 +227,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
198
227
  // OR if we are in Auto mode and it looks like a command
199
228
  const shouldSuggest = commandMode || (isAutoMode && detectedIntent === 'command');
200
229
  if (shouldSuggest) {
201
- const matches = CommandHistoryManager.getInstance().getMatches(value, currentDir);
230
+ const matches = CommandHistoryManager.getInstance().getMatches(value, currentDir, currentEnvironment);
202
231
  if (matches.length > 0) {
203
232
  setAutocompleteSuggestion(matches[0]);
204
233
  }
@@ -209,7 +238,137 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
209
238
  else {
210
239
  setAutocompleteSuggestion(null);
211
240
  }
212
- }, [value, commandMode, isAutoMode, detectedIntent, currentDir]);
241
+ }, [value, commandMode, isAutoMode, detectedIntent, currentDir, currentEnvironment]);
242
+ // AI Autocomplete Logic (5-second debounce)
243
+ useEffect(() => {
244
+ // Clear AI suggestion when value changes
245
+ setAiAutocompleteSuggestion(null);
246
+ // Clear any existing debounce timer
247
+ if (aiAutocompleteDebounceRef.current) {
248
+ clearTimeout(aiAutocompleteDebounceRef.current);
249
+ aiAutocompleteDebounceRef.current = null;
250
+ }
251
+ // Abort any pending AI request
252
+ if (aiAutocompleteAbortRef.current) {
253
+ aiAutocompleteAbortRef.current.abort();
254
+ aiAutocompleteAbortRef.current = null;
255
+ }
256
+ // Debug logging for trigger ref
257
+ /*
258
+ try {
259
+ if (detectedIntent !== 'ai' || value.length > 5) {
260
+ quickLog(`[InputBox] AI Effect: val="${value}", enabled=${aiAutoSuggestEnabled}, mode=${commandMode}, auto=${isAutoMode}, intent=${detectedIntent}`);
261
+ }
262
+ } catch (e) {}
263
+ */
264
+ // Don't trigger AI autocomplete if disabled or not in command mode
265
+ if (!aiAutoSuggestEnabled) {
266
+ // quickLog('[InputBox] AI skipped: disabled');
267
+ return;
268
+ }
269
+ const shouldSuggest = commandMode || (isAutoMode && detectedIntent === 'command');
270
+ if (!shouldSuggest) {
271
+ // if (value.length > 3) quickLog(`[InputBox] AI skipped: !shouldSuggest (cmd=${commandMode}, auto=${isAutoMode}, intent=${detectedIntent})`);
272
+ return;
273
+ }
274
+ // Don't suggest for empty, very short, or slash commands
275
+ if (!value || value.trim().length < 2 || value.startsWith('/')) {
276
+ // quickLog('[InputBox] AI skipped: too short or slash');
277
+ return;
278
+ }
279
+ // quickLog('[InputBox] AI Triggering debounce...');
280
+ // Set up 5-second debounce timer
281
+ aiAutocompleteDebounceRef.current = setTimeout(async () => {
282
+ setIsAiAutocompleteLoading(true);
283
+ // Create abort controller for this request
284
+ const abortController = new AbortController();
285
+ aiAutocompleteAbortRef.current = abortController;
286
+ try {
287
+ if (!aiAutoSuggestEnabled)
288
+ return; // double check inside timeout
289
+ // Get directory history using the correct method
290
+ const directoryHistory = CommandHistoryManager.getInstance().getDirectoryHistory(currentDir, currentEnvironment);
291
+ // Get files in current directory (local or remote)
292
+ let files = [];
293
+ try {
294
+ if (subshellContext && subshellContext.type !== 'local' && subshellContext.handler) {
295
+ // Remote/Subshell environment
296
+ // Check if we can list files
297
+ try {
298
+ const dirEntries = await subshellContext.handler.listDirectory(currentDir);
299
+ files = dirEntries
300
+ .slice(0, 50)
301
+ .map((e) => e.name + (e.type === 'directory' ? '/' : ''));
302
+ }
303
+ catch (remoteErr) {
304
+ // quickLog('Remote list error: ' + remoteErr);
305
+ }
306
+ }
307
+ else {
308
+ // Local environment
309
+ const dir = currentDir || process.cwd();
310
+ if (fs.existsSync(dir)) {
311
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
312
+ files = entries
313
+ .slice(0, 50)
314
+ .map(e => e.name + (e.isDirectory() ? '/' : ''));
315
+ }
316
+ }
317
+ }
318
+ catch (e) {
319
+ // Ignore file access errors
320
+ }
321
+ // Determine OS and Platform from subshell context if available
322
+ let osContext = process.platform;
323
+ let platformContext = process.platform;
324
+ if (subshellContext && subshellContext.type !== 'local' && subshellContext.metadata) {
325
+ const metaOs = subshellContext.metadata.os;
326
+ if (metaOs === 'windows') {
327
+ osContext = 'win32';
328
+ platformContext = 'win32';
329
+ }
330
+ else if (metaOs === 'macos') {
331
+ osContext = 'darwin';
332
+ platformContext = 'darwin';
333
+ }
334
+ else if (metaOs === 'linux') {
335
+ osContext = 'linux';
336
+ platformContext = 'linux';
337
+ }
338
+ }
339
+ const context = {
340
+ os: osContext,
341
+ platform: platformContext,
342
+ cwd: currentDir || process.cwd(),
343
+ directoryHistory: directoryHistory.slice(0, 10),
344
+ sessionCommands: sessionCommands.slice(-10),
345
+ files: files,
346
+ currentInput: value
347
+ };
348
+ const prediction = await AIAutocompleteAgent.predictCommand(context, abortController.signal);
349
+ if (prediction && !abortController.signal.aborted) {
350
+ setAiAutocompleteSuggestion(prediction);
351
+ }
352
+ }
353
+ catch (error) {
354
+ // Ignore errors (likely aborted)
355
+ }
356
+ finally {
357
+ setIsAiAutocompleteLoading(false);
358
+ if (aiAutocompleteAbortRef.current === abortController) {
359
+ aiAutocompleteAbortRef.current = null;
360
+ }
361
+ }
362
+ }, AI_AUTOCOMPLETE_DEBOUNCE_MS);
363
+ return () => {
364
+ if (aiAutocompleteDebounceRef.current) {
365
+ clearTimeout(aiAutocompleteDebounceRef.current);
366
+ }
367
+ if (aiAutocompleteAbortRef.current) {
368
+ aiAutocompleteAbortRef.current.abort();
369
+ }
370
+ };
371
+ }, [value, commandMode, isAutoMode, detectedIntent, aiAutoSuggestEnabled, currentDir, currentEnvironment, sessionCommands]);
213
372
  // Auto-classification effect (Synchronous Heuristics Only)
214
373
  useEffect(() => {
215
374
  // Only run classification if in Auto Mode
@@ -398,6 +557,104 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
398
557
  setCursorOffset(newValue.length);
399
558
  setSlashAutocompleteVisible(false);
400
559
  }
560
+ else if (value.startsWith('/models ') || value.startsWith('/model ')) {
561
+ // We're selecting a models subcommand
562
+ const prefix = value.startsWith('/models ') ? '/models ' : '/model ';
563
+ const newValue = `${prefix}${selected.name}`;
564
+ setValue(newValue);
565
+ setCursorOffset(newValue.length);
566
+ setSlashAutocompleteVisible(false);
567
+ }
568
+ else if (value.startsWith('/workflow new ') || value.startsWith('/wf new ')) {
569
+ // We're in workflow new subcommand mode (manual or learn-workflow)
570
+ const prefix = value.startsWith('/workflow new ') ? '/workflow new ' : '/wf new ';
571
+ const newValue = `${prefix}${selected.name}`;
572
+ setValue(newValue);
573
+ setCursorOffset(newValue.length);
574
+ setSlashAutocompleteVisible(false);
575
+ }
576
+ else if (value.startsWith('/workflow ') || value.startsWith('/wf ')) {
577
+ // Check if we're in workflow name selection mode (after run/view/delete)
578
+ const workflowNameMatch = value.match(/^\/(?:workflow|wf)\s+(run|view|delete)\s+/);
579
+ if (workflowNameMatch) {
580
+ // We're selecting a workflow name
581
+ const prefix = value.match(/^\/(?:workflow|wf)\s+(?:run|view|delete)\s+/)?.[0] || '';
582
+ const newValue = `${prefix}${selected.name}`;
583
+ setValue(newValue);
584
+ setCursorOffset(newValue.length);
585
+ setSlashAutocompleteVisible(false);
586
+ }
587
+ else {
588
+ // We're selecting a workflow subcommand
589
+ const prefix = value.startsWith('/workflow ') ? '/workflow ' : '/wf ';
590
+ const newValue = `${prefix}${selected.name} `;
591
+ setValue(newValue);
592
+ setCursorOffset(newValue.length);
593
+ // For run/view/delete, show workflow names immediately
594
+ if (selected.name === 'run' || selected.name === 'view' || selected.name === 'delete') {
595
+ const workflows = workflowStorage.list();
596
+ const matchingWorkflows = workflows
597
+ .slice(0, 10)
598
+ .map(wf => ({
599
+ name: wf.name,
600
+ description: wf.description || `${wf.stepCount} step${wf.stepCount !== 1 ? 's' : ''}`
601
+ }));
602
+ if (matchingWorkflows.length > 0) {
603
+ setSlashAutocompleteCommands(matchingWorkflows);
604
+ setSlashAutocompleteSelectedIndex(0);
605
+ setSlashAutocompleteScrollOffset(0);
606
+ // Keep autocomplete visible
607
+ }
608
+ else {
609
+ setSlashAutocompleteVisible(false);
610
+ }
611
+ }
612
+ else if (selected.name === 'new') {
613
+ // For 'new' subcommand, show manual/learn-workflow options
614
+ const subcommandMatches = filterCommands('workflow new ');
615
+ if (subcommandMatches.length > 0) {
616
+ setSlashAutocompleteCommands(subcommandMatches);
617
+ setSlashAutocompleteSelectedIndex(0);
618
+ setSlashAutocompleteScrollOffset(0);
619
+ // Keep autocomplete visible for next level
620
+ }
621
+ else {
622
+ setSlashAutocompleteVisible(false);
623
+ }
624
+ }
625
+ else {
626
+ setSlashAutocompleteVisible(false);
627
+ }
628
+ }
629
+ }
630
+ else if (value.startsWith('/settings auto-suggest ')) {
631
+ // We're selecting an auto-suggest option (on/off)
632
+ const newValue = `/settings auto-suggest ${selected.name}`;
633
+ setValue(newValue);
634
+ setCursorOffset(newValue.length);
635
+ setSlashAutocompleteVisible(false);
636
+ }
637
+ else if (value.startsWith('/settings ')) {
638
+ // We're selecting a settings subcommand (e.g., auto-suggest)
639
+ const newValue = `/settings ${selected.name} `;
640
+ setValue(newValue);
641
+ setCursorOffset(newValue.length);
642
+ // Show the next level options (on/off for auto-suggest)
643
+ if (selected.name === 'auto-suggest') {
644
+ const optionMatches = filterCommands('settings auto-suggest ');
645
+ if (optionMatches.length > 0) {
646
+ setSlashAutocompleteCommands(optionMatches);
647
+ setSlashAutocompleteSelectedIndex(0);
648
+ setSlashAutocompleteScrollOffset(0);
649
+ }
650
+ else {
651
+ setSlashAutocompleteVisible(false);
652
+ }
653
+ }
654
+ else {
655
+ setSlashAutocompleteVisible(false);
656
+ }
657
+ }
401
658
  else {
402
659
  // Regular slash command, replace everything
403
660
  const newValue = `/${selected.name} `;
@@ -465,6 +722,42 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
465
722
  setSlashAutocompleteVisible(false);
466
723
  }
467
724
  }
725
+ else if (selected.name === 'models' || selected.name === 'model') {
726
+ const subcommandMatches = filterCommands('models ');
727
+ if (subcommandMatches.length > 0) {
728
+ setSlashAutocompleteCommands(subcommandMatches);
729
+ setSlashAutocompleteSelectedIndex(0);
730
+ setSlashAutocompleteScrollOffset(0);
731
+ // Keep autocomplete visible for subcommands
732
+ }
733
+ else {
734
+ setSlashAutocompleteVisible(false);
735
+ }
736
+ }
737
+ else if (selected.name === 'settings') {
738
+ const subcommandMatches = filterCommands('settings ');
739
+ if (subcommandMatches.length > 0) {
740
+ setSlashAutocompleteCommands(subcommandMatches);
741
+ setSlashAutocompleteSelectedIndex(0);
742
+ setSlashAutocompleteScrollOffset(0);
743
+ // Keep autocomplete visible for subcommands
744
+ }
745
+ else {
746
+ setSlashAutocompleteVisible(false);
747
+ }
748
+ }
749
+ else if (selected.name === 'workflow' || selected.name === 'wf') {
750
+ const subcommandMatches = filterCommands('workflow ');
751
+ if (subcommandMatches.length > 0) {
752
+ setSlashAutocompleteCommands(subcommandMatches);
753
+ setSlashAutocompleteSelectedIndex(0);
754
+ setSlashAutocompleteScrollOffset(0);
755
+ // Keep autocomplete visible for subcommands
756
+ }
757
+ else {
758
+ setSlashAutocompleteVisible(false);
759
+ }
760
+ }
468
761
  else {
469
762
  setSlashAutocompleteVisible(false);
470
763
  }
@@ -688,6 +981,73 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
688
981
  setSlashAutocompleteVisible(false);
689
982
  }
690
983
  }
984
+ else if (newValue.startsWith('/models ') || newValue.startsWith('/model ')) {
985
+ // Models subcommands
986
+ const fullQuery = newValue.slice(1);
987
+ const matches = filterCommands(fullQuery);
988
+ if (matches.length > 0) {
989
+ setSlashAutocompleteCommands(matches);
990
+ setSlashAutocompleteVisible(true);
991
+ setSlashAutocompleteSelectedIndex(0);
992
+ setSlashAutocompleteScrollOffset(0);
993
+ }
994
+ else {
995
+ setSlashAutocompleteVisible(false);
996
+ }
997
+ }
998
+ else if (newValue.match(/^\/workflow\s+(run|view|delete)\s+/) ||
999
+ newValue.match(/^\/wf\s+(run|view|delete)\s+/)) {
1000
+ // Workflow name autocomplete (when user types "/workflow run " or similar)
1001
+ // This MUST come before the /workflow subcommand check since it's more specific
1002
+ const match = newValue.match(/^\/(?:workflow|wf)\s+(?:run|view|delete)\s+(.*)$/);
1003
+ const partialName = match ? match[1].toLowerCase() : '';
1004
+ const workflows = workflowStorage.list();
1005
+ const matchingWorkflows = workflows
1006
+ .filter(wf => wf.name.toLowerCase().includes(partialName))
1007
+ .slice(0, 10)
1008
+ .map(wf => ({
1009
+ name: wf.name,
1010
+ description: wf.description || `${wf.stepCount} step${wf.stepCount !== 1 ? 's' : ''}`
1011
+ }));
1012
+ if (matchingWorkflows.length > 0) {
1013
+ setSlashAutocompleteCommands(matchingWorkflows);
1014
+ setSlashAutocompleteVisible(true);
1015
+ setSlashAutocompleteSelectedIndex(0);
1016
+ setSlashAutocompleteScrollOffset(0);
1017
+ }
1018
+ else {
1019
+ setSlashAutocompleteVisible(false);
1020
+ }
1021
+ }
1022
+ else if (newValue.match(/^\/workflow\s+new\s+/) ||
1023
+ newValue.match(/^\/wf\s+new\s+/)) {
1024
+ // Workflow new subcommand autocomplete (manual, learn-workflow)
1025
+ const fullQuery = newValue.slice(1);
1026
+ const matches = filterCommands(fullQuery);
1027
+ if (matches.length > 0) {
1028
+ setSlashAutocompleteCommands(matches);
1029
+ setSlashAutocompleteVisible(true);
1030
+ setSlashAutocompleteSelectedIndex(0);
1031
+ setSlashAutocompleteScrollOffset(0);
1032
+ }
1033
+ else {
1034
+ setSlashAutocompleteVisible(false);
1035
+ }
1036
+ }
1037
+ else if (newValue.startsWith('/workflow ') || newValue.startsWith('/wf ')) {
1038
+ // Workflow subcommands (when user types "/workflow " or "/wf ")
1039
+ const fullQuery = newValue.slice(1);
1040
+ const matches = filterCommands(fullQuery);
1041
+ if (matches.length > 0) {
1042
+ setSlashAutocompleteCommands(matches);
1043
+ setSlashAutocompleteVisible(true);
1044
+ setSlashAutocompleteSelectedIndex(0);
1045
+ setSlashAutocompleteScrollOffset(0);
1046
+ }
1047
+ else {
1048
+ setSlashAutocompleteVisible(false);
1049
+ }
1050
+ }
691
1051
  else {
692
1052
  setSlashAutocompleteVisible(false);
693
1053
  }
@@ -743,17 +1103,52 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
743
1103
  }
744
1104
  return;
745
1105
  }
746
- // Ctrl+Z: Undo
747
- if (key.ctrl && input.toLowerCase() === 'z') {
1106
+ // Ctrl+Z / Cmd+Z: Undo
1107
+ if ((key.ctrl && input.toLowerCase() === 'z') || (key.meta && input.toLowerCase() === 'z')) {
748
1108
  handleUndo();
749
1109
  return;
750
1110
  }
751
- // Ctrl+A: Select All
752
- if (key.ctrl && input.toLowerCase() === 'a') {
1111
+ // Ctrl+A / Cmd+A: Select All
1112
+ if ((key.ctrl && input.toLowerCase() === 'a') || (key.meta && input.toLowerCase() === 'a')) {
753
1113
  setSelection({ start: 0, end: value.length });
754
1114
  setCursorOffset(value.length);
755
1115
  return;
756
1116
  }
1117
+ // Home: Start of Line
1118
+ // Note: In single-line inputs, this goes to start of text.
1119
+ // In multi-line wrapped view, we ideally want start of logical line or start of text?
1120
+ // Standard terminal Home = Start of command. Editor Home = Start of line.
1121
+ // Let's stick to Start of Text for now as it's a single "input box".
1122
+ // Home: Start of Line
1123
+ // @ts-ignore
1124
+ if (key.home) {
1125
+ const width = (process.stdout.columns || 80) - 6;
1126
+ const visualLines = getVisualLines(value, width);
1127
+ const currentLine = visualLines.find(line => (cursorOffset >= line.start && cursorOffset < line.end) ||
1128
+ (cursorOffset === line.end && line.isHardEnd));
1129
+ if (currentLine) {
1130
+ setCursorOffset(currentLine.start);
1131
+ }
1132
+ else {
1133
+ setCursorOffset(0);
1134
+ }
1135
+ return;
1136
+ }
1137
+ // End: End of Line
1138
+ // @ts-ignore
1139
+ if (key.end) {
1140
+ const width = (process.stdout.columns || 80) - 6;
1141
+ const visualLines = getVisualLines(value, width);
1142
+ const currentLine = visualLines.find(line => (cursorOffset >= line.start && cursorOffset < line.end) ||
1143
+ (cursorOffset === line.end && line.isHardEnd));
1144
+ if (currentLine) {
1145
+ setCursorOffset(currentLine.end);
1146
+ }
1147
+ else {
1148
+ setCursorOffset(value.length);
1149
+ }
1150
+ return;
1151
+ }
757
1152
  // Note: Clipboard images are handled via Alt+V keyboard shortcut
758
1153
  // DELETE CHAR - Only runs if Delete Word did NOT trigger
759
1154
  // Triggers on:
@@ -870,6 +1265,20 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
870
1265
  setSlashAutocompleteVisible(false);
871
1266
  }
872
1267
  }
1268
+ else if (newValue.startsWith('/models ') || newValue.startsWith('/model ')) {
1269
+ // Models subcommands
1270
+ const fullQuery = newValue.slice(1);
1271
+ const matches = filterCommands(fullQuery);
1272
+ if (matches.length > 0) {
1273
+ setSlashAutocompleteCommands(matches);
1274
+ setSlashAutocompleteVisible(true);
1275
+ setSlashAutocompleteSelectedIndex(0);
1276
+ setSlashAutocompleteScrollOffset(0);
1277
+ }
1278
+ else {
1279
+ setSlashAutocompleteVisible(false);
1280
+ }
1281
+ }
873
1282
  else {
874
1283
  setSlashAutocompleteVisible(false);
875
1284
  }
@@ -1043,8 +1452,8 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1043
1452
  return;
1044
1453
  }
1045
1454
  if (key.leftArrow) {
1046
- if (key.ctrl) {
1047
- // Ctrl+Left: Move word backwards
1455
+ if (key.ctrl || key.meta) {
1456
+ // Ctrl+Left / Meta+Left (Option+Left): Move word backwards
1048
1457
  let newOffset = cursorOffset;
1049
1458
  if (newOffset > 0) {
1050
1459
  // Skip whitespace backwards
@@ -1065,17 +1474,20 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1065
1474
  }
1066
1475
  if (key.rightArrow) {
1067
1476
  // Autocomplete Logic (Only at end of line)
1068
- if (autocompleteSuggestion && cursorOffset === value.length) {
1477
+ // AI suggestion takes priority over passive suggestion
1478
+ const effectiveSuggestion = aiAutocompleteSuggestion || autocompleteSuggestion;
1479
+ if (effectiveSuggestion && cursorOffset === value.length) {
1069
1480
  if (key.ctrl) {
1070
1481
  // Ctrl+Right: Accept FULL suggestion
1071
- setValue(autocompleteSuggestion);
1072
- setCursorOffset(autocompleteSuggestion.length);
1482
+ setValue(effectiveSuggestion);
1483
+ setCursorOffset(effectiveSuggestion.length);
1073
1484
  setAutocompleteSuggestion(null);
1485
+ setAiAutocompleteSuggestion(null);
1074
1486
  return;
1075
1487
  }
1076
1488
  else {
1077
1489
  // Right: Accept NEXT WORD
1078
- const remaining = autocompleteSuggestion.slice(value.length);
1490
+ const remaining = effectiveSuggestion.slice(value.length);
1079
1491
  // Match next chunk of non-whitespace (including preceding whitespace)
1080
1492
  const match = remaining.match(/^(\s*\S+)/);
1081
1493
  if (match) {
@@ -1095,8 +1507,8 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1095
1507
  }
1096
1508
  }
1097
1509
  // Navigation Logic (if not completing)
1098
- if (key.ctrl) {
1099
- // Ctrl+Right: Move word forwards
1510
+ if (key.ctrl || key.meta) {
1511
+ // Ctrl+Right / Meta+Right (Option+Right): Move word forwards
1100
1512
  let newOffset = cursorOffset;
1101
1513
  if (newOffset < value.length) {
1102
1514
  // Skip non-whitespace forwards
@@ -1239,6 +1651,58 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1239
1651
  setSlashAutocompleteVisible(false);
1240
1652
  }
1241
1653
  }
1654
+ else if (newValue.startsWith('/models ') || newValue.startsWith('/model ')) {
1655
+ // Models subcommands (when user types "/models " or "/model ")
1656
+ const fullQuery = newValue.slice(1); // Remove leading "/", pass "models <subquery>" to filterCommands
1657
+ const matches = filterCommands(fullQuery);
1658
+ if (matches.length > 0) {
1659
+ setSlashAutocompleteCommands(matches);
1660
+ setSlashAutocompleteVisible(true);
1661
+ setSlashAutocompleteSelectedIndex(0);
1662
+ setSlashAutocompleteScrollOffset(0);
1663
+ }
1664
+ else {
1665
+ setSlashAutocompleteVisible(false);
1666
+ }
1667
+ }
1668
+ else if (newValue.match(/^\/workflow\s+(run|view|delete)\s+/) ||
1669
+ newValue.match(/^\/wf\s+(run|view|delete)\s+/)) {
1670
+ // Workflow name autocomplete (when user types "/workflow run " or similar)
1671
+ // This MUST come before the /workflow subcommand check since it's more specific
1672
+ const match = newValue.match(/^\/(?:workflow|wf)\s+(?:run|view|delete)\s+(.*)$/);
1673
+ const partialName = match ? match[1].toLowerCase() : '';
1674
+ const workflows = workflowStorage.list();
1675
+ const matchingWorkflows = workflows
1676
+ .filter(wf => wf.name.toLowerCase().includes(partialName))
1677
+ .slice(0, 10)
1678
+ .map(wf => ({
1679
+ name: wf.name,
1680
+ description: wf.description || `${wf.stepCount} step${wf.stepCount !== 1 ? 's' : ''}`
1681
+ }));
1682
+ if (matchingWorkflows.length > 0) {
1683
+ setSlashAutocompleteCommands(matchingWorkflows);
1684
+ setSlashAutocompleteVisible(true);
1685
+ setSlashAutocompleteSelectedIndex(0);
1686
+ setSlashAutocompleteScrollOffset(0);
1687
+ }
1688
+ else {
1689
+ setSlashAutocompleteVisible(false);
1690
+ }
1691
+ }
1692
+ else if (newValue.startsWith('/workflow ') || newValue.startsWith('/wf ')) {
1693
+ // Workflow subcommands (when user types "/workflow " or "/wf ")
1694
+ const fullQuery = newValue.slice(1); // Remove leading "/", pass "workflow <subquery>" to filterCommands
1695
+ const matches = filterCommands(fullQuery);
1696
+ if (matches.length > 0) {
1697
+ setSlashAutocompleteCommands(matches);
1698
+ setSlashAutocompleteVisible(true);
1699
+ setSlashAutocompleteSelectedIndex(0);
1700
+ setSlashAutocompleteScrollOffset(0);
1701
+ }
1702
+ else {
1703
+ setSlashAutocompleteVisible(false);
1704
+ }
1705
+ }
1242
1706
  else {
1243
1707
  setSlashAutocompleteVisible(false);
1244
1708
  }
@@ -1324,7 +1788,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1324
1788
  if (trimmedValue) {
1325
1789
  // Save to history if it was a command
1326
1790
  if (commandMode) {
1327
- CommandHistoryManager.getInstance().addCommand(trimmedValue, currentDir);
1791
+ CommandHistoryManager.getInstance().addCommand(trimmedValue, currentDir, currentEnvironment);
1328
1792
  }
1329
1793
  // Resolve file tags (@filename -> absolute path)
1330
1794
  let resolvedValue = trimmedValue;
@@ -1491,10 +1955,14 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1491
1955
  }
1492
1956
  // Render Autocomplete Ghost Text
1493
1957
  // 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);
1958
+ // AI suggestion takes priority over passive suggestion
1959
+ const effectiveSuggestion = aiAutocompleteSuggestion || autocompleteSuggestion;
1960
+ if (isLastLine && effectiveSuggestion && effectiveSuggestion.startsWith(value)) {
1961
+ const suffix = effectiveSuggestion.slice(value.length);
1496
1962
  if (suffix) {
1497
- renderedChars.push(React.createElement(Text, { key: "ghost", color: "gray" }, suffix));
1963
+ // Use slightly different color for AI suggestion to differentiate
1964
+ const ghostColor = aiAutocompleteSuggestion ? '#888888' : 'gray';
1965
+ renderedChars.push(React.createElement(Text, { key: "ghost", color: ghostColor }, suffix));
1498
1966
  }
1499
1967
  }
1500
1968
  if (renderedChars.length === 0) {
@@ -1512,7 +1980,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1512
1980
  "#257aa5ff", paddingX: 1, paddingY: 0, width: "100%" },
1513
1981
  React.createElement(Box, { marginY: 1, justifyContent: "space-between", width: "100%" },
1514
1982
  React.createElement(Box, null,
1515
- subshellContext && subshellContext.type !== 'local' && (React.createElement(Breadcrumbs, { context: subshellContext })),
1983
+ subshellContext && subshellContext.type !== 'local' && (React.createElement(Breadcrumbs, { context: subshellContext, stack: subshellContextStack })),
1516
1984
  React.createElement(Text, { color: "#666666" }, "CWD: "),
1517
1985
  React.createElement(Text, { color: "#00ccff", bold: true }, currentDir)),
1518
1986
  React.createElement(Box, null,