centaurus-cli 2.8.6 → 2.8.8

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 (128) hide show
  1. package/dist/cli-adapter.d.ts +85 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +773 -31
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  6. package/dist/config/mcp-config-manager.js +9 -8
  7. package/dist/config/mcp-config-manager.js.map +1 -1
  8. package/dist/config/slash-commands.d.ts +2 -0
  9. package/dist/config/slash-commands.d.ts.map +1 -1
  10. package/dist/config/slash-commands.js +31 -1
  11. package/dist/config/slash-commands.js.map +1 -1
  12. package/dist/context/handlers/docker-handler.js.map +1 -1
  13. package/dist/context/handlers/ssh-handler.d.ts +16 -1
  14. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  15. package/dist/context/handlers/ssh-handler.js +57 -12
  16. package/dist/context/handlers/ssh-handler.js.map +1 -1
  17. package/dist/context/subshell-handler.d.ts +14 -0
  18. package/dist/context/subshell-handler.d.ts.map +1 -1
  19. package/dist/hooks/useTerminalDimensions.d.ts +41 -0
  20. package/dist/hooks/useTerminalDimensions.d.ts.map +1 -0
  21. package/dist/hooks/useTerminalDimensions.js +84 -0
  22. package/dist/hooks/useTerminalDimensions.js.map +1 -0
  23. package/dist/index.js +57 -5
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  26. package/dist/mcp/mcp-command-handler.js +3 -1
  27. package/dist/mcp/mcp-command-handler.js.map +1 -1
  28. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  29. package/dist/mcp/mcp-server-manager.js +5 -3
  30. package/dist/mcp/mcp-server-manager.js.map +1 -1
  31. package/dist/services/api-client.d.ts +24 -0
  32. package/dist/services/api-client.d.ts.map +1 -1
  33. package/dist/services/api-client.js +27 -0
  34. package/dist/services/api-client.js.map +1 -1
  35. package/dist/services/auth-handler.js +1 -1
  36. package/dist/services/auth-handler.js.map +1 -1
  37. package/dist/services/clipboard-service.d.ts +42 -0
  38. package/dist/services/clipboard-service.d.ts.map +1 -0
  39. package/dist/services/clipboard-service.js +217 -0
  40. package/dist/services/clipboard-service.js.map +1 -0
  41. package/dist/services/local-chat-storage.d.ts +154 -0
  42. package/dist/services/local-chat-storage.d.ts.map +1 -0
  43. package/dist/services/local-chat-storage.js +258 -0
  44. package/dist/services/local-chat-storage.js.map +1 -0
  45. package/dist/tools/grep-search.d.ts +5 -0
  46. package/dist/tools/grep-search.d.ts.map +1 -1
  47. package/dist/tools/grep-search.js +68 -16
  48. package/dist/tools/grep-search.js.map +1 -1
  49. package/dist/tools/plan-mode.d.ts +57 -6
  50. package/dist/tools/plan-mode.d.ts.map +1 -1
  51. package/dist/tools/plan-mode.js +297 -46
  52. package/dist/tools/plan-mode.js.map +1 -1
  53. package/dist/tools/read-binary-file.d.ts +10 -0
  54. package/dist/tools/read-binary-file.d.ts.map +1 -0
  55. package/dist/tools/read-binary-file.js +210 -0
  56. package/dist/tools/read-binary-file.js.map +1 -0
  57. package/dist/types/index.d.ts +7 -1
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/ui/components/App.d.ts +35 -0
  60. package/dist/ui/components/App.d.ts.map +1 -1
  61. package/dist/ui/components/App.js +622 -43
  62. package/dist/ui/components/App.js.map +1 -1
  63. package/dist/ui/components/ClipboardImageAutocomplete.d.ts +14 -0
  64. package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +1 -0
  65. package/dist/ui/components/ClipboardImageAutocomplete.js +39 -0
  66. package/dist/ui/components/ClipboardImageAutocomplete.js.map +1 -0
  67. package/dist/ui/components/ConnectionStatusMessage.d.ts +1 -1
  68. package/dist/ui/components/ConnectionStatusMessage.d.ts.map +1 -1
  69. package/dist/ui/components/ConnectionStatusMessage.js +21 -0
  70. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  71. package/dist/ui/components/DetailedPlanReviewScreen.d.ts +17 -0
  72. package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +1 -0
  73. package/dist/ui/components/DetailedPlanReviewScreen.js +110 -0
  74. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -0
  75. package/dist/ui/components/InputBox.d.ts +4 -1
  76. package/dist/ui/components/InputBox.d.ts.map +1 -1
  77. package/dist/ui/components/InputBox.js +419 -30
  78. package/dist/ui/components/InputBox.js.map +1 -1
  79. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  80. package/dist/ui/components/InteractiveShell.js +20 -6
  81. package/dist/ui/components/InteractiveShell.js.map +1 -1
  82. package/dist/ui/components/MessageDisplay.d.ts +6 -0
  83. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  84. package/dist/ui/components/MessageDisplay.js +66 -3
  85. package/dist/ui/components/MessageDisplay.js.map +1 -1
  86. package/dist/ui/components/PlanAcceptedMessage.d.ts +8 -0
  87. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -1
  88. package/dist/ui/components/PlanAcceptedMessage.js +26 -8
  89. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  90. package/dist/ui/components/StreamingMessageDisplay.d.ts +3 -0
  91. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  92. package/dist/ui/components/StreamingMessageDisplay.js +10 -6
  93. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  94. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -1
  95. package/dist/ui/components/TaskCompletedMessage.js +4 -4
  96. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  97. package/dist/ui/components/TaskProgressIndicator.d.ts +18 -0
  98. package/dist/ui/components/TaskProgressIndicator.d.ts.map +1 -0
  99. package/dist/ui/components/TaskProgressIndicator.js +72 -0
  100. package/dist/ui/components/TaskProgressIndicator.js.map +1 -0
  101. package/dist/ui/components/ThinkingDisplay.d.ts +3 -0
  102. package/dist/ui/components/ThinkingDisplay.d.ts.map +1 -1
  103. package/dist/ui/components/ThinkingDisplay.js +6 -4
  104. package/dist/ui/components/ThinkingDisplay.js.map +1 -1
  105. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  106. package/dist/ui/components/ToolExecutionMessage.js +85 -15
  107. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  108. package/dist/ui/components/VersionUpdatePrompt.d.ts +1 -2
  109. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  110. package/dist/ui/components/VersionUpdatePrompt.js +108 -27
  111. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  112. package/dist/utils/custom-commands-manager.d.ts +59 -0
  113. package/dist/utils/custom-commands-manager.d.ts.map +1 -0
  114. package/dist/utils/custom-commands-manager.js +142 -0
  115. package/dist/utils/custom-commands-manager.js.map +1 -0
  116. package/dist/utils/input-classifier.d.ts +10 -11
  117. package/dist/utils/input-classifier.d.ts.map +1 -1
  118. package/dist/utils/input-classifier.js +299 -75
  119. package/dist/utils/input-classifier.js.map +1 -1
  120. package/dist/utils/terminal-output.d.ts.map +1 -1
  121. package/dist/utils/terminal-output.js +110 -14
  122. package/dist/utils/terminal-output.js.map +1 -1
  123. package/dist/utils/unicode-sanitizer.d.ts +44 -0
  124. package/dist/utils/unicode-sanitizer.d.ts.map +1 -0
  125. package/dist/utils/unicode-sanitizer.js +211 -0
  126. package/dist/utils/unicode-sanitizer.js.map +1 -0
  127. package/models-config.json +2 -3
  128. package/package.json +4 -1
@@ -4,11 +4,14 @@ import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { Breadcrumbs } from './Breadcrumbs.js';
6
6
  import { ContextWindowIndicator } from './ContextWindowIndicator.js';
7
+ import { logDebug } from '../../utils/logger.js';
7
8
  import { detectIntent } from '../../utils/input-classifier.js';
8
9
  import { CommandHistoryManager } from '../../utils/command-history.js';
9
10
  import { SlashCommandAutocomplete } from './SlashCommandAutocomplete.js';
10
11
  import { FileTagAutocomplete } from './FileTagAutocomplete.js';
12
+ import { ClipboardImageAutocomplete } from './ClipboardImageAutocomplete.js';
11
13
  import { filterCommands } from '../../config/slash-commands.js';
14
+ import { getClipboardImages } from '../../services/clipboard-service.js';
12
15
  const getVisualLines = (text, width) => {
13
16
  const logicalLines = text.split('\n');
14
17
  const visualLines = [];
@@ -46,8 +49,9 @@ const getVisualLines = (text, width) => {
46
49
  });
47
50
  return visualLines;
48
51
  };
49
- export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...', autoAcceptMode, model, planMode = false, commandMode = false, currentWorkingDirectory, commandHistory = [], onToggleAutoAccept, onToggleCommandMode, isActive = true, subshellContext, currentTokens = 0, maxTokens = 1000000, isShellRunning = false }) => {
50
- const [value, setValue] = useState('');
52
+ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...', autoAcceptMode, model, planMode = false, commandMode = false, currentWorkingDirectory, commandHistory = [], onToggleAutoAccept, onToggleCommandMode, isActive = true, subshellContext, currentTokens = 0, maxTokens = 1000000, isShellRunning = false, initialValue = '', onValueChange }) => {
53
+ // Use initialValue for first mount, but manage state internally after that
54
+ const [value, setValueInternal] = useState(initialValue);
51
55
  const [cursorOffset, setCursorOffset] = useState(0);
52
56
  const [completions, setCompletions] = useState([]);
53
57
  const [completionIndex, setCompletionIndex] = useState(0);
@@ -76,8 +80,37 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
76
80
  const [fileTagSelectedIndex, setFileTagSelectedIndex] = useState(0);
77
81
  const [activeFileTagStart, setActiveFileTagStart] = useState(null);
78
82
  const [confirmedFileTags, setConfirmedFileTags] = useState([]);
83
+ // Clipboard Image State (#image command)
84
+ const [clipboardAutocompleteVisible, setClipboardAutocompleteVisible] = useState(false);
85
+ const [clipboardImages, setClipboardImages] = useState([]);
86
+ const [clipboardSelectedIndex, setClipboardSelectedIndex] = useState(0);
87
+ const [clipboardLoading, setClipboardLoading] = useState(false);
88
+ const [activeClipboardStart, setActiveClipboardStart] = useState(null);
89
+ const [confirmedClipboardImages, setConfirmedClipboardImages] = useState([]);
90
+ // Track positions of validated #image commands (for pink highlighting)
91
+ const [validatedImagePositions, setValidatedImagePositions] = useState([]);
92
+ // Track visual line count to force re-renders when text wraps
93
+ // This is necessary because Ink doesn't automatically update layout when text wraps
94
+ const [visualLineCount, setVisualLineCount] = useState(1);
79
95
  // Configuration for scrolling
80
96
  const MAX_VISIBLE_LINES = 9;
97
+ // Wrapper for setValue that also notifies parent of changes
98
+ const setValue = React.useCallback((newValue) => {
99
+ setValueInternal(prev => {
100
+ const resolvedValue = typeof newValue === 'function' ? newValue(prev) : newValue;
101
+ // Notify parent of value change for preservation across screen transitions
102
+ if (onValueChange) {
103
+ onValueChange(resolvedValue);
104
+ }
105
+ return resolvedValue;
106
+ });
107
+ }, [onValueChange]);
108
+ // Initialize cursor position when initialValue is provided
109
+ useEffect(() => {
110
+ if (initialValue && initialValue.length > 0) {
111
+ setCursorOffset(initialValue.length);
112
+ }
113
+ }, []); // Only run on mount
81
114
  // Load history on mount
82
115
  useEffect(() => {
83
116
  CommandHistoryManager.getInstance().load();
@@ -89,6 +122,16 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
89
122
  setCursorOffset(0);
90
123
  }
91
124
  }, [value, cursorOffset]);
125
+ // Track visual line count changes to force re-renders when text wraps
126
+ // This is crucial for the input box height to update correctly
127
+ useEffect(() => {
128
+ const termWidth = (process.stdout.columns || 80) - 6;
129
+ const visualLines = getVisualLines(value, termWidth);
130
+ const newLineCount = Math.max(1, visualLines.length);
131
+ if (newLineCount !== visualLineCount) {
132
+ setVisualLineCount(newLineCount);
133
+ }
134
+ }, [value, visualLineCount]);
92
135
  // Determine current working directory
93
136
  const currentDir = useMemo(() => {
94
137
  const cwd = currentWorkingDirectory || process.cwd();
@@ -204,7 +247,9 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
204
247
  break;
205
248
  }
206
249
  }
207
- if (atPosition === -1) {
250
+ // Only treat @ as file tag if it's at start of input OR preceded by whitespace
251
+ // This prevents triggering on things like "rohan@localhost" or "email@domain.com"
252
+ if (atPosition === -1 || (atPosition > 0 && !/[\s\n]/.test(value[atPosition - 1]))) {
208
253
  setFileTagAutocompleteVisible(false);
209
254
  setActiveFileTagStart(null);
210
255
  return;
@@ -224,6 +269,122 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
224
269
  setActiveFileTagStart(null);
225
270
  }
226
271
  }, [value, cursorOffset, commandMode, currentWorkingDirectory]);
272
+ // Hash (#) symbol detection effect for clipboard image autocomplete
273
+ // When user types # in Agent mode, check clipboard for images and show dropdown
274
+ useEffect(() => {
275
+ // Don't show in command mode
276
+ if (commandMode) {
277
+ setClipboardAutocompleteVisible(false);
278
+ setActiveClipboardStart(null);
279
+ return;
280
+ }
281
+ // Find if cursor is right after a # character (or typing after #)
282
+ let hashPosition = -1;
283
+ for (let i = cursorOffset - 1; i >= 0; i--) {
284
+ const char = value[i];
285
+ // Stop if we hit whitespace
286
+ if (/[\s\n]/.test(char))
287
+ break;
288
+ if (char === '#') {
289
+ hashPosition = i;
290
+ break;
291
+ }
292
+ }
293
+ // Only treat # as image trigger if it's at start of input OR preceded by whitespace
294
+ if (hashPosition === -1 || (hashPosition > 0 && !/[\s\n]/.test(value[hashPosition - 1]))) {
295
+ setClipboardAutocompleteVisible(false);
296
+ setActiveClipboardStart(null);
297
+ return;
298
+ }
299
+ // Extract what's typed after #
300
+ const query = value.slice(hashPosition + 1, cursorOffset).toLowerCase();
301
+ // If #image is fully typed (with space or at end), hide dropdown (validation will handle it)
302
+ if (query === 'image' || query.startsWith('image ')) {
303
+ setClipboardAutocompleteVisible(false);
304
+ setActiveClipboardStart(null);
305
+ return;
306
+ }
307
+ // If user has typed something that doesn't match "image" prefix, hide dropdown
308
+ if (query.length > 0 && !'image'.startsWith(query)) {
309
+ setClipboardAutocompleteVisible(false);
310
+ setActiveClipboardStart(null);
311
+ return;
312
+ }
313
+ // Show dropdown and check clipboard
314
+ setActiveClipboardStart(hashPosition);
315
+ setClipboardLoading(true);
316
+ setClipboardAutocompleteVisible(true);
317
+ // Check clipboard asynchronously
318
+ const checkClipboard = async () => {
319
+ try {
320
+ const images = await getClipboardImages();
321
+ setClipboardImages(images);
322
+ setClipboardLoading(false);
323
+ setClipboardSelectedIndex(0);
324
+ }
325
+ catch (error) {
326
+ logDebug(`Failed to check clipboard: ${error instanceof Error ? error.message : 'Unknown error'}`);
327
+ setClipboardImages([]);
328
+ setClipboardLoading(false);
329
+ }
330
+ };
331
+ checkClipboard();
332
+ }, [value, cursorOffset, commandMode]);
333
+ // #image command detection effect
334
+ // When user types #image, check clipboard for images and validate
335
+ useEffect(() => {
336
+ // Don't check in command mode
337
+ if (commandMode)
338
+ return;
339
+ // Find all #image occurrences in the text
340
+ const regex = /#image\b/gi;
341
+ const matches = [];
342
+ let match;
343
+ while ((match = regex.exec(value)) !== null) {
344
+ matches.push({ start: match.index, end: match.index + match[0].length });
345
+ }
346
+ // If no #image in text, clear validated positions
347
+ if (matches.length === 0) {
348
+ if (validatedImagePositions.length > 0) {
349
+ setValidatedImagePositions([]);
350
+ setConfirmedClipboardImages([]);
351
+ }
352
+ return;
353
+ }
354
+ // Check for new #image occurrences that haven't been validated yet
355
+ const unvalidatedMatches = matches.filter(m => !validatedImagePositions.some(v => v.start === m.start && v.end === m.end));
356
+ if (unvalidatedMatches.length === 0)
357
+ return;
358
+ // Check clipboard and validate new #image occurrences
359
+ const validateImages = async () => {
360
+ logDebug(`Found ${unvalidatedMatches.length} new #image occurrences to validate`);
361
+ try {
362
+ const images = await getClipboardImages();
363
+ logDebug(`Clipboard check returned ${images.length} images`);
364
+ if (images.length > 0) {
365
+ const image = images[0];
366
+ logDebug(`Image found in clipboard: ${image.displayName}, ${image.sizeBytes} bytes`);
367
+ // Add to confirmed clipboard images (avoid duplicates)
368
+ setConfirmedClipboardImages(prev => {
369
+ const exists = prev.some(img => img.id === image.id);
370
+ if (exists)
371
+ return prev;
372
+ return [...prev, image];
373
+ });
374
+ // Mark all unvalidated positions as validated
375
+ setValidatedImagePositions(prev => [...prev, ...unvalidatedMatches]);
376
+ logDebug(`Validated ${unvalidatedMatches.length} #image occurrences`);
377
+ }
378
+ else {
379
+ logDebug('No image in clipboard - #image command not validated');
380
+ }
381
+ }
382
+ catch (error) {
383
+ logDebug(`Failed to check clipboard: ${error instanceof Error ? error.message : 'Unknown error'}`);
384
+ }
385
+ };
386
+ validateImages();
387
+ }, [value, commandMode]);
227
388
  const pushToUndoStack = () => {
228
389
  setUndoStack(prev => [...prev, { value, cursorOffset }]);
229
390
  setRedoStack([]); // Clear redo stack on new action
@@ -265,12 +426,27 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
265
426
  setCursorOffset(newValue.length);
266
427
  setSlashAutocompleteVisible(false);
267
428
  }
429
+ else if (value.startsWith('/chat ')) {
430
+ // We're selecting a chat subcommand, keep "/chat " and append the subcommand
431
+ const newValue = `/chat ${selected.name} `;
432
+ setValue(newValue);
433
+ setCursorOffset(newValue.length);
434
+ setSlashAutocompleteVisible(false);
435
+ }
436
+ else if (value.startsWith('/add-command ') || value.startsWith('/add-command-auto-detect ')) {
437
+ // We're selecting an add-command subcommand
438
+ const prefix = value.startsWith('/add-command-auto-detect ') ? '/add-command-auto-detect ' : '/add-command ';
439
+ const newValue = `${prefix}${selected.name} `;
440
+ setValue(newValue);
441
+ setCursorOffset(newValue.length);
442
+ setSlashAutocompleteVisible(false);
443
+ }
268
444
  else {
269
445
  // Regular slash command, replace everything
270
446
  const newValue = `/${selected.name} `;
271
447
  setValue(newValue);
272
448
  setCursorOffset(newValue.length);
273
- // Check if this command has subcommands (e.g., /mcp)
449
+ // Check if this command has subcommands (e.g., /mcp, /chat, /add-command)
274
450
  // If so, immediately show the subcommand list
275
451
  if (selected.name === 'mcp') {
276
452
  const subcommandMatches = filterCommands('mcp ');
@@ -283,6 +459,28 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
283
459
  setSlashAutocompleteVisible(false);
284
460
  }
285
461
  }
462
+ else if (selected.name === 'chat') {
463
+ const subcommandMatches = filterCommands('chat ');
464
+ if (subcommandMatches.length > 0) {
465
+ setSlashAutocompleteCommands(subcommandMatches);
466
+ setSlashAutocompleteSelectedIndex(0);
467
+ // Keep autocomplete visible for subcommands
468
+ }
469
+ else {
470
+ setSlashAutocompleteVisible(false);
471
+ }
472
+ }
473
+ else if (selected.name === 'add-command' || selected.name === 'add-command-auto-detect') {
474
+ const subcommandMatches = filterCommands('add-command ');
475
+ if (subcommandMatches.length > 0) {
476
+ setSlashAutocompleteCommands(subcommandMatches);
477
+ setSlashAutocompleteSelectedIndex(0);
478
+ // Keep autocomplete visible for subcommands
479
+ }
480
+ else {
481
+ setSlashAutocompleteVisible(false);
482
+ }
483
+ }
286
484
  else {
287
485
  setSlashAutocompleteVisible(false);
288
486
  }
@@ -367,6 +565,54 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
367
565
  return;
368
566
  }
369
567
  }
568
+ // Handle clipboard image (#) autocomplete navigation
569
+ if (clipboardAutocompleteVisible) {
570
+ if (key.downArrow) {
571
+ setClipboardSelectedIndex(prev => Math.min(prev + 1, Math.max(clipboardImages.length - 1, 0)));
572
+ return;
573
+ }
574
+ if (key.upArrow) {
575
+ setClipboardSelectedIndex(prev => Math.max(prev - 1, 0));
576
+ return;
577
+ }
578
+ if (key.return && input.length <= 1 && !key.shift && !key.ctrl) {
579
+ // Select the image option and autocomplete #image
580
+ if (clipboardImages.length > 0 && activeClipboardStart !== null) {
581
+ pushToUndoStack();
582
+ // Replace # with #image (and add space after)
583
+ const beforeHash = value.slice(0, activeClipboardStart);
584
+ const afterCursor = value.slice(cursorOffset);
585
+ const newValue = beforeHash + '#image ' + afterCursor;
586
+ const newCursorPos = activeClipboardStart + 7; // length of "#image "
587
+ setValue(newValue);
588
+ setCursorOffset(newCursorPos);
589
+ setClipboardAutocompleteVisible(false);
590
+ setActiveClipboardStart(null);
591
+ }
592
+ return;
593
+ }
594
+ if (key.escape) {
595
+ setClipboardAutocompleteVisible(false);
596
+ setActiveClipboardStart(null);
597
+ return;
598
+ }
599
+ // Tab also selects
600
+ if (key.tab && !key.shift) {
601
+ if (clipboardImages.length > 0 && activeClipboardStart !== null) {
602
+ pushToUndoStack();
603
+ const beforeHash = value.slice(0, activeClipboardStart);
604
+ const afterCursor = value.slice(cursorOffset);
605
+ const newValue = beforeHash + '#image ' + afterCursor;
606
+ const newCursorPos = activeClipboardStart + 7;
607
+ setValue(newValue);
608
+ setCursorOffset(newCursorPos);
609
+ setClipboardAutocompleteVisible(false);
610
+ setActiveClipboardStart(null);
611
+ }
612
+ return;
613
+ }
614
+ }
615
+ // Clipboard image paste is handled via Ctrl+V below
370
616
  // DELETE WORD BACKWARDS - Check this FIRST before standard backspace/delete
371
617
  // Triggers on any of these conditions:
372
618
  // 1. Ctrl+W (char code 23) - Standard Unix terminal shortcut
@@ -418,6 +664,32 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
418
664
  setSlashAutocompleteVisible(false);
419
665
  }
420
666
  }
667
+ else if (newValue.startsWith('/chat ')) {
668
+ // Chat subcommands
669
+ const fullQuery = newValue.slice(1);
670
+ const matches = filterCommands(fullQuery);
671
+ if (matches.length > 0) {
672
+ setSlashAutocompleteCommands(matches);
673
+ setSlashAutocompleteVisible(true);
674
+ setSlashAutocompleteSelectedIndex(0);
675
+ }
676
+ else {
677
+ setSlashAutocompleteVisible(false);
678
+ }
679
+ }
680
+ else if (newValue.startsWith('/add-command ') || newValue.startsWith('/add-command-auto-detect ')) {
681
+ // Add-command subcommands
682
+ const fullQuery = newValue.slice(1);
683
+ const matches = filterCommands(fullQuery);
684
+ if (matches.length > 0) {
685
+ setSlashAutocompleteCommands(matches);
686
+ setSlashAutocompleteVisible(true);
687
+ setSlashAutocompleteSelectedIndex(0);
688
+ }
689
+ else {
690
+ setSlashAutocompleteVisible(false);
691
+ }
692
+ }
421
693
  else {
422
694
  setSlashAutocompleteVisible(false);
423
695
  }
@@ -475,6 +747,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
475
747
  setCursorOffset(value.length);
476
748
  return;
477
749
  }
750
+ // Note: Clipboard images are handled via #image command detection effect
478
751
  // DELETE CHAR - Only runs if Delete Word did NOT trigger
479
752
  // Triggers on:
480
753
  // 1. Backspace or Delete key flag is present
@@ -532,6 +805,32 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
532
805
  setSlashAutocompleteVisible(false);
533
806
  }
534
807
  }
808
+ else if (newValue.startsWith('/chat ')) {
809
+ // Chat subcommands
810
+ const fullQuery = newValue.slice(1);
811
+ const matches = filterCommands(fullQuery);
812
+ if (matches.length > 0) {
813
+ setSlashAutocompleteCommands(matches);
814
+ setSlashAutocompleteVisible(true);
815
+ setSlashAutocompleteSelectedIndex(0);
816
+ }
817
+ else {
818
+ setSlashAutocompleteVisible(false);
819
+ }
820
+ }
821
+ else if (newValue.startsWith('/add-command ') || newValue.startsWith('/add-command-auto-detect ')) {
822
+ // Add-command subcommands
823
+ const fullQuery = newValue.slice(1);
824
+ const matches = filterCommands(fullQuery);
825
+ if (matches.length > 0) {
826
+ setSlashAutocompleteCommands(matches);
827
+ setSlashAutocompleteVisible(true);
828
+ setSlashAutocompleteSelectedIndex(0);
829
+ }
830
+ else {
831
+ setSlashAutocompleteVisible(false);
832
+ }
833
+ }
535
834
  else {
536
835
  setSlashAutocompleteVisible(false);
537
836
  }
@@ -805,6 +1104,32 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
805
1104
  setSlashAutocompleteVisible(false);
806
1105
  }
807
1106
  }
1107
+ else if (newValue.startsWith('/chat ')) {
1108
+ // Chat subcommands (when user types "/chat ")
1109
+ const fullQuery = newValue.slice(1); // Remove leading "/", pass "chat <subquery>" to filterCommands
1110
+ const matches = filterCommands(fullQuery);
1111
+ if (matches.length > 0) {
1112
+ setSlashAutocompleteCommands(matches);
1113
+ setSlashAutocompleteVisible(true);
1114
+ setSlashAutocompleteSelectedIndex(0);
1115
+ }
1116
+ else {
1117
+ setSlashAutocompleteVisible(false);
1118
+ }
1119
+ }
1120
+ else if (newValue.startsWith('/add-command ') || newValue.startsWith('/add-command-auto-detect ')) {
1121
+ // Add-command subcommands (when user types "/add-command ")
1122
+ const fullQuery = newValue.slice(1); // Remove leading "/", pass "add-command <subquery>" to filterCommands
1123
+ const matches = filterCommands(fullQuery);
1124
+ if (matches.length > 0) {
1125
+ setSlashAutocompleteCommands(matches);
1126
+ setSlashAutocompleteVisible(true);
1127
+ setSlashAutocompleteSelectedIndex(0);
1128
+ }
1129
+ else {
1130
+ setSlashAutocompleteVisible(false);
1131
+ }
1132
+ }
808
1133
  else {
809
1134
  setSlashAutocompleteVisible(false);
810
1135
  }
@@ -907,7 +1232,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
907
1232
  return match;
908
1233
  });
909
1234
  }
910
- onSubmit(resolvedValue);
1235
+ onSubmit(resolvedValue, confirmedClipboardImages.length > 0 ? confirmedClipboardImages : undefined);
911
1236
  setValue('');
912
1237
  setCursorOffset(0);
913
1238
  setCompletions([]);
@@ -919,16 +1244,25 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
919
1244
  setSelection(null);
920
1245
  setAutocompleteSuggestion(null);
921
1246
  setConfirmedFileTags([]); // Clear confirmed tags on submit
1247
+ setConfirmedClipboardImages([]); // Clear confirmed clipboard images on submit
922
1248
  }
923
1249
  };
924
1250
  // Rendering Logic with Scrolling
925
1251
  const renderInput = () => {
926
- const lines = value.split('\n');
1252
+ // Get terminal width for visual line calculation
1253
+ // Account for borders (2) + padding (2) + prompt "> " (2) = 6 chars
1254
+ const termWidth = (process.stdout.columns || 80) - 6;
1255
+ // Use getVisualLines to properly calculate wrapped lines
1256
+ const visualLines = getVisualLines(value, termWidth);
1257
+ const logicalLines = value.split('\n');
927
1258
  // If empty, show placeholder
928
- if (lines.length === 1 && lines[0] === '') {
1259
+ if (logicalLines.length === 1 && logicalLines[0] === '') {
929
1260
  return React.createElement(Text, { color: "gray" }, placeholder);
930
1261
  }
931
- // Calculate cursor line and column
1262
+ // For rendering, we still use logical lines (split by \n) since we render character-by-character
1263
+ // But for height calculation, we use visualLines.length
1264
+ const lines = logicalLines;
1265
+ // Calculate cursor line and column (based on logical lines)
932
1266
  let currentPos = 0;
933
1267
  let cursorLine = 0;
934
1268
  let cursorCol = 0;
@@ -945,47 +1279,90 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
945
1279
  cursorLine = lines.length - 1;
946
1280
  cursorCol = lines[lines.length - 1].length;
947
1281
  }
948
- // Calculate visible range
1282
+ // For scrolling, calculate which VISUAL line the cursor is on
1283
+ let cursorVisualLine = 0;
1284
+ for (let i = 0; i < visualLines.length; i++) {
1285
+ if (cursorOffset >= visualLines[i].start && cursorOffset <= visualLines[i].end) {
1286
+ cursorVisualLine = i;
1287
+ break;
1288
+ }
1289
+ }
1290
+ // Handle cursor at very end
1291
+ if (cursorOffset === value.length && visualLines.length > 0) {
1292
+ cursorVisualLine = visualLines.length - 1;
1293
+ }
1294
+ // Calculate visible range using VISUAL lines count for proper scrolling
1295
+ const totalVisualLines = visualLines.length;
949
1296
  let startLine = 0;
950
- if (lines.length > MAX_VISIBLE_LINES) {
951
- if (cursorLine < MAX_VISIBLE_LINES) {
1297
+ if (totalVisualLines > MAX_VISIBLE_LINES) {
1298
+ // Use visual line position for scrolling calculation
1299
+ if (cursorVisualLine < MAX_VISIBLE_LINES) {
952
1300
  startLine = 0;
953
1301
  }
954
1302
  else {
955
- startLine = cursorLine - MAX_VISIBLE_LINES + 1;
1303
+ startLine = cursorVisualLine - MAX_VISIBLE_LINES + 1;
956
1304
  }
957
1305
  }
958
- const endLine = Math.min(startLine + MAX_VISIBLE_LINES, lines.length);
959
- const visibleLines = lines.slice(startLine, endLine);
1306
+ const endLine = Math.min(startLine + MAX_VISIBLE_LINES, totalVisualLines);
1307
+ // Get the text content for each visual line
1308
+ const visibleVisualLines = visualLines.slice(startLine, endLine);
960
1309
  return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
961
1310
  startLine > 0 && React.createElement(Text, { color: "gray" }, "\u2191 ..."),
962
- visibleLines.map((line, idx) => {
963
- const actualLineIndex = startLine + idx;
964
- const isCursorLine = actualLineIndex === cursorLine;
965
- const isLastLine = actualLineIndex === lines.length - 1;
966
- // Calculate absolute position of this line start
967
- let lineStartPos = 0;
968
- for (let k = 0; k < actualLineIndex; k++)
969
- lineStartPos += lines[k].length + 1;
1311
+ visibleVisualLines.map((vLine, idx) => {
1312
+ const actualVisualLineIndex = startLine + idx;
1313
+ // Extract the text content for this visual line
1314
+ const lineText = value.slice(vLine.start, vLine.end);
1315
+ const lineStartPos = vLine.start;
1316
+ // Determine if cursor is on this visual line
1317
+ const isCursorLine = cursorOffset >= vLine.start && cursorOffset <= vLine.end;
1318
+ const cursorCol = isCursorLine ? cursorOffset - vLine.start : -1;
1319
+ // Is this the last visual line?
1320
+ const isLastLine = actualVisualLineIndex === totalVisualLines - 1;
970
1321
  if (!isActive) {
971
- if (line.length === 0) {
1322
+ if (lineText.length === 0) {
972
1323
  return React.createElement(Text, { key: idx }, " ");
973
1324
  }
974
- return React.createElement(Text, { key: idx }, line);
1325
+ return React.createElement(Text, { key: idx }, lineText);
975
1326
  }
976
1327
  // Render with selection and cursor
977
- const chars = line.split('');
1328
+ const chars = lineText.split('');
978
1329
  const renderedChars = chars.map((char, charIdx) => {
979
1330
  const absPos = lineStartPos + charIdx;
980
1331
  const isSelected = selection &&
981
1332
  absPos >= Math.min(selection.start, selection.end) &&
982
1333
  absPos < Math.max(selection.start, selection.end);
983
1334
  // Check if this character is part of an active file tag (being typed after @)
1335
+ let activeFileTagEnd = activeFileTagStart !== null ? activeFileTagStart : 0;
1336
+ if (activeFileTagStart !== null) {
1337
+ for (let j = activeFileTagStart; j < value.length && j < cursorOffset; j++) {
1338
+ if (/[\s\n]/.test(value[j])) {
1339
+ break;
1340
+ }
1341
+ activeFileTagEnd = j + 1;
1342
+ }
1343
+ }
984
1344
  const isInActiveFileTag = activeFileTagStart !== null &&
985
1345
  absPos >= activeFileTagStart &&
986
- absPos < cursorOffset;
1346
+ absPos < activeFileTagEnd;
987
1347
  // Check if this character is part of a confirmed file tag
988
- const isInConfirmedFileTag = confirmedFileTags.some(tag => absPos >= tag.start && absPos < tag.end);
1348
+ let isInConfirmedFileTag = false;
1349
+ if (!commandMode) {
1350
+ const fileTagRegex = /(?:^|[\s\n])(@[^\s@]+)/g;
1351
+ let match;
1352
+ while ((match = fileTagRegex.exec(value)) !== null) {
1353
+ const fullMatch = match[0];
1354
+ const tagContent = match[1];
1355
+ const tagStart = match.index + (fullMatch.length - tagContent.length);
1356
+ const tagEnd = tagStart + tagContent.length;
1357
+ if (activeFileTagStart !== null && tagStart === activeFileTagStart) {
1358
+ continue;
1359
+ }
1360
+ if (absPos >= tagStart && absPos < tagEnd) {
1361
+ isInConfirmedFileTag = true;
1362
+ break;
1363
+ }
1364
+ }
1365
+ }
989
1366
  const isCursor = isCursorLine && charIdx === cursorCol;
990
1367
  if (isCursor) {
991
1368
  return React.createElement(Text, { key: charIdx, inverse: true, color: isSelected ? "yellow" : undefined }, char);
@@ -997,10 +1374,21 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
997
1374
  if (isInConfirmedFileTag || isInActiveFileTag) {
998
1375
  return React.createElement(Text, { key: charIdx, color: "#00ccff", bold: true }, char);
999
1376
  }
1377
+ // Check if this character is part of a validated #image command
1378
+ let isInValidatedImage = false;
1379
+ for (const pos of validatedImagePositions) {
1380
+ if (absPos >= pos.start && absPos < pos.end) {
1381
+ isInValidatedImage = true;
1382
+ break;
1383
+ }
1384
+ }
1385
+ if (isInValidatedImage) {
1386
+ return React.createElement(Text, { key: charIdx, color: "#ff69b4", bold: true }, char);
1387
+ }
1000
1388
  return React.createElement(Text, { key: charIdx }, char);
1001
1389
  });
1002
1390
  // Handle cursor at end of line
1003
- if (isCursorLine && cursorCol === line.length) {
1391
+ if (isCursorLine && cursorCol === lineText.length) {
1004
1392
  renderedChars.push(React.createElement(Text, { key: "cursor", inverse: true }, " "));
1005
1393
  }
1006
1394
  // Render Autocomplete Ghost Text
@@ -1016,7 +1404,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1016
1404
  }
1017
1405
  return React.createElement(Text, { key: idx }, renderedChars);
1018
1406
  }),
1019
- endLine < lines.length && React.createElement(Text, { color: "gray" }, "\u2193 ...")));
1407
+ endLine < totalVisualLines && React.createElement(Text, { color: "gray" }, "\u2193 ...")));
1020
1408
  };
1021
1409
  return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: rejectFlash ? "#ff3366" : (commandMode ? "#00cc66" : "#257aa5ff"), paddingX: 1, paddingY: 0, width: "100%" },
1022
1410
  React.createElement(Box, { marginY: 1, justifyContent: "space-between", width: "100%" },
@@ -1045,6 +1433,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1045
1433
  !commandMode && (React.createElement(Box, { marginLeft: 1 },
1046
1434
  React.createElement(ContextWindowIndicator, { currentTokens: currentTokens, maxTokens: maxTokens }))))),
1047
1435
  slashAutocompleteVisible && (React.createElement(SlashCommandAutocomplete, { commands: slashAutocompleteCommands, selectedIndex: slashAutocompleteSelectedIndex })),
1048
- fileTagAutocompleteVisible && (React.createElement(FileTagAutocomplete, { files: fileTagSuggestions, selectedIndex: fileTagSelectedIndex }))));
1436
+ fileTagAutocompleteVisible && (React.createElement(FileTagAutocomplete, { files: fileTagSuggestions, selectedIndex: fileTagSelectedIndex })),
1437
+ clipboardAutocompleteVisible && (React.createElement(ClipboardImageAutocomplete, { images: clipboardImages, selectedIndex: clipboardSelectedIndex, isLoading: clipboardLoading }))));
1049
1438
  });
1050
1439
  //# sourceMappingURL=InputBox.js.map