centaurus-cli 2.8.6 → 2.8.7

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 (115) 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 +769 -28
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/slash-commands.d.ts +2 -0
  6. package/dist/config/slash-commands.d.ts.map +1 -1
  7. package/dist/config/slash-commands.js +31 -1
  8. package/dist/config/slash-commands.js.map +1 -1
  9. package/dist/context/handlers/docker-handler.js.map +1 -1
  10. package/dist/context/handlers/ssh-handler.d.ts +16 -1
  11. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  12. package/dist/context/handlers/ssh-handler.js +57 -12
  13. package/dist/context/handlers/ssh-handler.js.map +1 -1
  14. package/dist/context/subshell-handler.d.ts +14 -0
  15. package/dist/context/subshell-handler.d.ts.map +1 -1
  16. package/dist/hooks/useTerminalDimensions.d.ts +41 -0
  17. package/dist/hooks/useTerminalDimensions.d.ts.map +1 -0
  18. package/dist/hooks/useTerminalDimensions.js +84 -0
  19. package/dist/hooks/useTerminalDimensions.js.map +1 -0
  20. package/dist/index.js +27 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/services/api-client.d.ts +24 -0
  23. package/dist/services/api-client.d.ts.map +1 -1
  24. package/dist/services/api-client.js +27 -0
  25. package/dist/services/api-client.js.map +1 -1
  26. package/dist/services/auth-handler.js +1 -1
  27. package/dist/services/auth-handler.js.map +1 -1
  28. package/dist/services/clipboard-service.d.ts +42 -0
  29. package/dist/services/clipboard-service.d.ts.map +1 -0
  30. package/dist/services/clipboard-service.js +217 -0
  31. package/dist/services/clipboard-service.js.map +1 -0
  32. package/dist/services/local-chat-storage.d.ts +154 -0
  33. package/dist/services/local-chat-storage.d.ts.map +1 -0
  34. package/dist/services/local-chat-storage.js +258 -0
  35. package/dist/services/local-chat-storage.js.map +1 -0
  36. package/dist/tools/grep-search.d.ts +5 -0
  37. package/dist/tools/grep-search.d.ts.map +1 -1
  38. package/dist/tools/grep-search.js +68 -16
  39. package/dist/tools/grep-search.js.map +1 -1
  40. package/dist/tools/plan-mode.d.ts +57 -6
  41. package/dist/tools/plan-mode.d.ts.map +1 -1
  42. package/dist/tools/plan-mode.js +297 -46
  43. package/dist/tools/plan-mode.js.map +1 -1
  44. package/dist/tools/read-binary-file.d.ts +10 -0
  45. package/dist/tools/read-binary-file.d.ts.map +1 -0
  46. package/dist/tools/read-binary-file.js +210 -0
  47. package/dist/tools/read-binary-file.js.map +1 -0
  48. package/dist/types/index.d.ts +7 -1
  49. package/dist/types/index.d.ts.map +1 -1
  50. package/dist/ui/components/App.d.ts +35 -0
  51. package/dist/ui/components/App.d.ts.map +1 -1
  52. package/dist/ui/components/App.js +608 -16
  53. package/dist/ui/components/App.js.map +1 -1
  54. package/dist/ui/components/ClipboardImageAutocomplete.d.ts +14 -0
  55. package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +1 -0
  56. package/dist/ui/components/ClipboardImageAutocomplete.js +39 -0
  57. package/dist/ui/components/ClipboardImageAutocomplete.js.map +1 -0
  58. package/dist/ui/components/ConnectionStatusMessage.d.ts +1 -1
  59. package/dist/ui/components/ConnectionStatusMessage.d.ts.map +1 -1
  60. package/dist/ui/components/ConnectionStatusMessage.js +21 -0
  61. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  62. package/dist/ui/components/DetailedPlanReviewScreen.d.ts +17 -0
  63. package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +1 -0
  64. package/dist/ui/components/DetailedPlanReviewScreen.js +110 -0
  65. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -0
  66. package/dist/ui/components/InputBox.d.ts +2 -1
  67. package/dist/ui/components/InputBox.d.ts.map +1 -1
  68. package/dist/ui/components/InputBox.js +399 -28
  69. package/dist/ui/components/InputBox.js.map +1 -1
  70. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  71. package/dist/ui/components/InteractiveShell.js +20 -6
  72. package/dist/ui/components/InteractiveShell.js.map +1 -1
  73. package/dist/ui/components/MessageDisplay.d.ts +6 -0
  74. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  75. package/dist/ui/components/MessageDisplay.js +66 -3
  76. package/dist/ui/components/MessageDisplay.js.map +1 -1
  77. package/dist/ui/components/PlanAcceptedMessage.d.ts +8 -0
  78. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -1
  79. package/dist/ui/components/PlanAcceptedMessage.js +26 -8
  80. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  81. package/dist/ui/components/StreamingMessageDisplay.d.ts +3 -0
  82. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  83. package/dist/ui/components/StreamingMessageDisplay.js +10 -6
  84. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  85. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -1
  86. package/dist/ui/components/TaskCompletedMessage.js +4 -4
  87. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  88. package/dist/ui/components/TaskProgressIndicator.d.ts +18 -0
  89. package/dist/ui/components/TaskProgressIndicator.d.ts.map +1 -0
  90. package/dist/ui/components/TaskProgressIndicator.js +72 -0
  91. package/dist/ui/components/TaskProgressIndicator.js.map +1 -0
  92. package/dist/ui/components/ThinkingDisplay.d.ts +3 -0
  93. package/dist/ui/components/ThinkingDisplay.d.ts.map +1 -1
  94. package/dist/ui/components/ThinkingDisplay.js +6 -4
  95. package/dist/ui/components/ThinkingDisplay.js.map +1 -1
  96. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  97. package/dist/ui/components/ToolExecutionMessage.js +85 -15
  98. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  99. package/dist/utils/custom-commands-manager.d.ts +59 -0
  100. package/dist/utils/custom-commands-manager.d.ts.map +1 -0
  101. package/dist/utils/custom-commands-manager.js +142 -0
  102. package/dist/utils/custom-commands-manager.js.map +1 -0
  103. package/dist/utils/input-classifier.d.ts +10 -11
  104. package/dist/utils/input-classifier.d.ts.map +1 -1
  105. package/dist/utils/input-classifier.js +299 -75
  106. package/dist/utils/input-classifier.js.map +1 -1
  107. package/dist/utils/terminal-output.d.ts.map +1 -1
  108. package/dist/utils/terminal-output.js +110 -14
  109. package/dist/utils/terminal-output.js.map +1 -1
  110. package/dist/utils/unicode-sanitizer.d.ts +44 -0
  111. package/dist/utils/unicode-sanitizer.d.ts.map +1 -0
  112. package/dist/utils/unicode-sanitizer.js +211 -0
  113. package/dist/utils/unicode-sanitizer.js.map +1 -0
  114. package/models-config.json +2 -3
  115. 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 = [];
@@ -76,6 +79,18 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
76
79
  const [fileTagSelectedIndex, setFileTagSelectedIndex] = useState(0);
77
80
  const [activeFileTagStart, setActiveFileTagStart] = useState(null);
78
81
  const [confirmedFileTags, setConfirmedFileTags] = useState([]);
82
+ // Clipboard Image State (#image command)
83
+ const [clipboardAutocompleteVisible, setClipboardAutocompleteVisible] = useState(false);
84
+ const [clipboardImages, setClipboardImages] = useState([]);
85
+ const [clipboardSelectedIndex, setClipboardSelectedIndex] = useState(0);
86
+ const [clipboardLoading, setClipboardLoading] = useState(false);
87
+ const [activeClipboardStart, setActiveClipboardStart] = useState(null);
88
+ const [confirmedClipboardImages, setConfirmedClipboardImages] = useState([]);
89
+ // Track positions of validated #image commands (for pink highlighting)
90
+ const [validatedImagePositions, setValidatedImagePositions] = useState([]);
91
+ // Track visual line count to force re-renders when text wraps
92
+ // This is necessary because Ink doesn't automatically update layout when text wraps
93
+ const [visualLineCount, setVisualLineCount] = useState(1);
79
94
  // Configuration for scrolling
80
95
  const MAX_VISIBLE_LINES = 9;
81
96
  // Load history on mount
@@ -89,6 +104,16 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
89
104
  setCursorOffset(0);
90
105
  }
91
106
  }, [value, cursorOffset]);
107
+ // Track visual line count changes to force re-renders when text wraps
108
+ // This is crucial for the input box height to update correctly
109
+ useEffect(() => {
110
+ const termWidth = (process.stdout.columns || 80) - 6;
111
+ const visualLines = getVisualLines(value, termWidth);
112
+ const newLineCount = Math.max(1, visualLines.length);
113
+ if (newLineCount !== visualLineCount) {
114
+ setVisualLineCount(newLineCount);
115
+ }
116
+ }, [value, visualLineCount]);
92
117
  // Determine current working directory
93
118
  const currentDir = useMemo(() => {
94
119
  const cwd = currentWorkingDirectory || process.cwd();
@@ -204,7 +229,9 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
204
229
  break;
205
230
  }
206
231
  }
207
- if (atPosition === -1) {
232
+ // Only treat @ as file tag if it's at start of input OR preceded by whitespace
233
+ // This prevents triggering on things like "rohan@localhost" or "email@domain.com"
234
+ if (atPosition === -1 || (atPosition > 0 && !/[\s\n]/.test(value[atPosition - 1]))) {
208
235
  setFileTagAutocompleteVisible(false);
209
236
  setActiveFileTagStart(null);
210
237
  return;
@@ -224,6 +251,122 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
224
251
  setActiveFileTagStart(null);
225
252
  }
226
253
  }, [value, cursorOffset, commandMode, currentWorkingDirectory]);
254
+ // Hash (#) symbol detection effect for clipboard image autocomplete
255
+ // When user types # in Agent mode, check clipboard for images and show dropdown
256
+ useEffect(() => {
257
+ // Don't show in command mode
258
+ if (commandMode) {
259
+ setClipboardAutocompleteVisible(false);
260
+ setActiveClipboardStart(null);
261
+ return;
262
+ }
263
+ // Find if cursor is right after a # character (or typing after #)
264
+ let hashPosition = -1;
265
+ for (let i = cursorOffset - 1; i >= 0; i--) {
266
+ const char = value[i];
267
+ // Stop if we hit whitespace
268
+ if (/[\s\n]/.test(char))
269
+ break;
270
+ if (char === '#') {
271
+ hashPosition = i;
272
+ break;
273
+ }
274
+ }
275
+ // Only treat # as image trigger if it's at start of input OR preceded by whitespace
276
+ if (hashPosition === -1 || (hashPosition > 0 && !/[\s\n]/.test(value[hashPosition - 1]))) {
277
+ setClipboardAutocompleteVisible(false);
278
+ setActiveClipboardStart(null);
279
+ return;
280
+ }
281
+ // Extract what's typed after #
282
+ const query = value.slice(hashPosition + 1, cursorOffset).toLowerCase();
283
+ // If #image is fully typed (with space or at end), hide dropdown (validation will handle it)
284
+ if (query === 'image' || query.startsWith('image ')) {
285
+ setClipboardAutocompleteVisible(false);
286
+ setActiveClipboardStart(null);
287
+ return;
288
+ }
289
+ // If user has typed something that doesn't match "image" prefix, hide dropdown
290
+ if (query.length > 0 && !'image'.startsWith(query)) {
291
+ setClipboardAutocompleteVisible(false);
292
+ setActiveClipboardStart(null);
293
+ return;
294
+ }
295
+ // Show dropdown and check clipboard
296
+ setActiveClipboardStart(hashPosition);
297
+ setClipboardLoading(true);
298
+ setClipboardAutocompleteVisible(true);
299
+ // Check clipboard asynchronously
300
+ const checkClipboard = async () => {
301
+ try {
302
+ const images = await getClipboardImages();
303
+ setClipboardImages(images);
304
+ setClipboardLoading(false);
305
+ setClipboardSelectedIndex(0);
306
+ }
307
+ catch (error) {
308
+ logDebug(`Failed to check clipboard: ${error instanceof Error ? error.message : 'Unknown error'}`);
309
+ setClipboardImages([]);
310
+ setClipboardLoading(false);
311
+ }
312
+ };
313
+ checkClipboard();
314
+ }, [value, cursorOffset, commandMode]);
315
+ // #image command detection effect
316
+ // When user types #image, check clipboard for images and validate
317
+ useEffect(() => {
318
+ // Don't check in command mode
319
+ if (commandMode)
320
+ return;
321
+ // Find all #image occurrences in the text
322
+ const regex = /#image\b/gi;
323
+ const matches = [];
324
+ let match;
325
+ while ((match = regex.exec(value)) !== null) {
326
+ matches.push({ start: match.index, end: match.index + match[0].length });
327
+ }
328
+ // If no #image in text, clear validated positions
329
+ if (matches.length === 0) {
330
+ if (validatedImagePositions.length > 0) {
331
+ setValidatedImagePositions([]);
332
+ setConfirmedClipboardImages([]);
333
+ }
334
+ return;
335
+ }
336
+ // Check for new #image occurrences that haven't been validated yet
337
+ const unvalidatedMatches = matches.filter(m => !validatedImagePositions.some(v => v.start === m.start && v.end === m.end));
338
+ if (unvalidatedMatches.length === 0)
339
+ return;
340
+ // Check clipboard and validate new #image occurrences
341
+ const validateImages = async () => {
342
+ logDebug(`Found ${unvalidatedMatches.length} new #image occurrences to validate`);
343
+ try {
344
+ const images = await getClipboardImages();
345
+ logDebug(`Clipboard check returned ${images.length} images`);
346
+ if (images.length > 0) {
347
+ const image = images[0];
348
+ logDebug(`Image found in clipboard: ${image.displayName}, ${image.sizeBytes} bytes`);
349
+ // Add to confirmed clipboard images (avoid duplicates)
350
+ setConfirmedClipboardImages(prev => {
351
+ const exists = prev.some(img => img.id === image.id);
352
+ if (exists)
353
+ return prev;
354
+ return [...prev, image];
355
+ });
356
+ // Mark all unvalidated positions as validated
357
+ setValidatedImagePositions(prev => [...prev, ...unvalidatedMatches]);
358
+ logDebug(`Validated ${unvalidatedMatches.length} #image occurrences`);
359
+ }
360
+ else {
361
+ logDebug('No image in clipboard - #image command not validated');
362
+ }
363
+ }
364
+ catch (error) {
365
+ logDebug(`Failed to check clipboard: ${error instanceof Error ? error.message : 'Unknown error'}`);
366
+ }
367
+ };
368
+ validateImages();
369
+ }, [value, commandMode]);
227
370
  const pushToUndoStack = () => {
228
371
  setUndoStack(prev => [...prev, { value, cursorOffset }]);
229
372
  setRedoStack([]); // Clear redo stack on new action
@@ -265,12 +408,27 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
265
408
  setCursorOffset(newValue.length);
266
409
  setSlashAutocompleteVisible(false);
267
410
  }
411
+ else if (value.startsWith('/chat ')) {
412
+ // We're selecting a chat subcommand, keep "/chat " and append the subcommand
413
+ const newValue = `/chat ${selected.name} `;
414
+ setValue(newValue);
415
+ setCursorOffset(newValue.length);
416
+ setSlashAutocompleteVisible(false);
417
+ }
418
+ else if (value.startsWith('/add-command ') || value.startsWith('/add-command-auto-detect ')) {
419
+ // We're selecting an add-command subcommand
420
+ const prefix = value.startsWith('/add-command-auto-detect ') ? '/add-command-auto-detect ' : '/add-command ';
421
+ const newValue = `${prefix}${selected.name} `;
422
+ setValue(newValue);
423
+ setCursorOffset(newValue.length);
424
+ setSlashAutocompleteVisible(false);
425
+ }
268
426
  else {
269
427
  // Regular slash command, replace everything
270
428
  const newValue = `/${selected.name} `;
271
429
  setValue(newValue);
272
430
  setCursorOffset(newValue.length);
273
- // Check if this command has subcommands (e.g., /mcp)
431
+ // Check if this command has subcommands (e.g., /mcp, /chat, /add-command)
274
432
  // If so, immediately show the subcommand list
275
433
  if (selected.name === 'mcp') {
276
434
  const subcommandMatches = filterCommands('mcp ');
@@ -283,6 +441,28 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
283
441
  setSlashAutocompleteVisible(false);
284
442
  }
285
443
  }
444
+ else if (selected.name === 'chat') {
445
+ const subcommandMatches = filterCommands('chat ');
446
+ if (subcommandMatches.length > 0) {
447
+ setSlashAutocompleteCommands(subcommandMatches);
448
+ setSlashAutocompleteSelectedIndex(0);
449
+ // Keep autocomplete visible for subcommands
450
+ }
451
+ else {
452
+ setSlashAutocompleteVisible(false);
453
+ }
454
+ }
455
+ else if (selected.name === 'add-command' || selected.name === 'add-command-auto-detect') {
456
+ const subcommandMatches = filterCommands('add-command ');
457
+ if (subcommandMatches.length > 0) {
458
+ setSlashAutocompleteCommands(subcommandMatches);
459
+ setSlashAutocompleteSelectedIndex(0);
460
+ // Keep autocomplete visible for subcommands
461
+ }
462
+ else {
463
+ setSlashAutocompleteVisible(false);
464
+ }
465
+ }
286
466
  else {
287
467
  setSlashAutocompleteVisible(false);
288
468
  }
@@ -367,6 +547,54 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
367
547
  return;
368
548
  }
369
549
  }
550
+ // Handle clipboard image (#) autocomplete navigation
551
+ if (clipboardAutocompleteVisible) {
552
+ if (key.downArrow) {
553
+ setClipboardSelectedIndex(prev => Math.min(prev + 1, Math.max(clipboardImages.length - 1, 0)));
554
+ return;
555
+ }
556
+ if (key.upArrow) {
557
+ setClipboardSelectedIndex(prev => Math.max(prev - 1, 0));
558
+ return;
559
+ }
560
+ if (key.return && input.length <= 1 && !key.shift && !key.ctrl) {
561
+ // Select the image option and autocomplete #image
562
+ if (clipboardImages.length > 0 && activeClipboardStart !== null) {
563
+ pushToUndoStack();
564
+ // Replace # with #image (and add space after)
565
+ const beforeHash = value.slice(0, activeClipboardStart);
566
+ const afterCursor = value.slice(cursorOffset);
567
+ const newValue = beforeHash + '#image ' + afterCursor;
568
+ const newCursorPos = activeClipboardStart + 7; // length of "#image "
569
+ setValue(newValue);
570
+ setCursorOffset(newCursorPos);
571
+ setClipboardAutocompleteVisible(false);
572
+ setActiveClipboardStart(null);
573
+ }
574
+ return;
575
+ }
576
+ if (key.escape) {
577
+ setClipboardAutocompleteVisible(false);
578
+ setActiveClipboardStart(null);
579
+ return;
580
+ }
581
+ // Tab also selects
582
+ if (key.tab && !key.shift) {
583
+ if (clipboardImages.length > 0 && activeClipboardStart !== null) {
584
+ pushToUndoStack();
585
+ const beforeHash = value.slice(0, activeClipboardStart);
586
+ const afterCursor = value.slice(cursorOffset);
587
+ const newValue = beforeHash + '#image ' + afterCursor;
588
+ const newCursorPos = activeClipboardStart + 7;
589
+ setValue(newValue);
590
+ setCursorOffset(newCursorPos);
591
+ setClipboardAutocompleteVisible(false);
592
+ setActiveClipboardStart(null);
593
+ }
594
+ return;
595
+ }
596
+ }
597
+ // Clipboard image paste is handled via Ctrl+V below
370
598
  // DELETE WORD BACKWARDS - Check this FIRST before standard backspace/delete
371
599
  // Triggers on any of these conditions:
372
600
  // 1. Ctrl+W (char code 23) - Standard Unix terminal shortcut
@@ -418,6 +646,32 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
418
646
  setSlashAutocompleteVisible(false);
419
647
  }
420
648
  }
649
+ else if (newValue.startsWith('/chat ')) {
650
+ // Chat subcommands
651
+ const fullQuery = newValue.slice(1);
652
+ const matches = filterCommands(fullQuery);
653
+ if (matches.length > 0) {
654
+ setSlashAutocompleteCommands(matches);
655
+ setSlashAutocompleteVisible(true);
656
+ setSlashAutocompleteSelectedIndex(0);
657
+ }
658
+ else {
659
+ setSlashAutocompleteVisible(false);
660
+ }
661
+ }
662
+ else if (newValue.startsWith('/add-command ') || newValue.startsWith('/add-command-auto-detect ')) {
663
+ // Add-command subcommands
664
+ const fullQuery = newValue.slice(1);
665
+ const matches = filterCommands(fullQuery);
666
+ if (matches.length > 0) {
667
+ setSlashAutocompleteCommands(matches);
668
+ setSlashAutocompleteVisible(true);
669
+ setSlashAutocompleteSelectedIndex(0);
670
+ }
671
+ else {
672
+ setSlashAutocompleteVisible(false);
673
+ }
674
+ }
421
675
  else {
422
676
  setSlashAutocompleteVisible(false);
423
677
  }
@@ -475,6 +729,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
475
729
  setCursorOffset(value.length);
476
730
  return;
477
731
  }
732
+ // Note: Clipboard images are handled via #image command detection effect
478
733
  // DELETE CHAR - Only runs if Delete Word did NOT trigger
479
734
  // Triggers on:
480
735
  // 1. Backspace or Delete key flag is present
@@ -532,6 +787,32 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
532
787
  setSlashAutocompleteVisible(false);
533
788
  }
534
789
  }
790
+ else if (newValue.startsWith('/chat ')) {
791
+ // Chat subcommands
792
+ const fullQuery = newValue.slice(1);
793
+ const matches = filterCommands(fullQuery);
794
+ if (matches.length > 0) {
795
+ setSlashAutocompleteCommands(matches);
796
+ setSlashAutocompleteVisible(true);
797
+ setSlashAutocompleteSelectedIndex(0);
798
+ }
799
+ else {
800
+ setSlashAutocompleteVisible(false);
801
+ }
802
+ }
803
+ else if (newValue.startsWith('/add-command ') || newValue.startsWith('/add-command-auto-detect ')) {
804
+ // Add-command subcommands
805
+ const fullQuery = newValue.slice(1);
806
+ const matches = filterCommands(fullQuery);
807
+ if (matches.length > 0) {
808
+ setSlashAutocompleteCommands(matches);
809
+ setSlashAutocompleteVisible(true);
810
+ setSlashAutocompleteSelectedIndex(0);
811
+ }
812
+ else {
813
+ setSlashAutocompleteVisible(false);
814
+ }
815
+ }
535
816
  else {
536
817
  setSlashAutocompleteVisible(false);
537
818
  }
@@ -805,6 +1086,32 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
805
1086
  setSlashAutocompleteVisible(false);
806
1087
  }
807
1088
  }
1089
+ else if (newValue.startsWith('/chat ')) {
1090
+ // Chat subcommands (when user types "/chat ")
1091
+ const fullQuery = newValue.slice(1); // Remove leading "/", pass "chat <subquery>" to filterCommands
1092
+ const matches = filterCommands(fullQuery);
1093
+ if (matches.length > 0) {
1094
+ setSlashAutocompleteCommands(matches);
1095
+ setSlashAutocompleteVisible(true);
1096
+ setSlashAutocompleteSelectedIndex(0);
1097
+ }
1098
+ else {
1099
+ setSlashAutocompleteVisible(false);
1100
+ }
1101
+ }
1102
+ else if (newValue.startsWith('/add-command ') || newValue.startsWith('/add-command-auto-detect ')) {
1103
+ // Add-command subcommands (when user types "/add-command ")
1104
+ const fullQuery = newValue.slice(1); // Remove leading "/", pass "add-command <subquery>" to filterCommands
1105
+ const matches = filterCommands(fullQuery);
1106
+ if (matches.length > 0) {
1107
+ setSlashAutocompleteCommands(matches);
1108
+ setSlashAutocompleteVisible(true);
1109
+ setSlashAutocompleteSelectedIndex(0);
1110
+ }
1111
+ else {
1112
+ setSlashAutocompleteVisible(false);
1113
+ }
1114
+ }
808
1115
  else {
809
1116
  setSlashAutocompleteVisible(false);
810
1117
  }
@@ -907,7 +1214,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
907
1214
  return match;
908
1215
  });
909
1216
  }
910
- onSubmit(resolvedValue);
1217
+ onSubmit(resolvedValue, confirmedClipboardImages.length > 0 ? confirmedClipboardImages : undefined);
911
1218
  setValue('');
912
1219
  setCursorOffset(0);
913
1220
  setCompletions([]);
@@ -919,16 +1226,25 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
919
1226
  setSelection(null);
920
1227
  setAutocompleteSuggestion(null);
921
1228
  setConfirmedFileTags([]); // Clear confirmed tags on submit
1229
+ setConfirmedClipboardImages([]); // Clear confirmed clipboard images on submit
922
1230
  }
923
1231
  };
924
1232
  // Rendering Logic with Scrolling
925
1233
  const renderInput = () => {
926
- const lines = value.split('\n');
1234
+ // Get terminal width for visual line calculation
1235
+ // Account for borders (2) + padding (2) + prompt "> " (2) = 6 chars
1236
+ const termWidth = (process.stdout.columns || 80) - 6;
1237
+ // Use getVisualLines to properly calculate wrapped lines
1238
+ const visualLines = getVisualLines(value, termWidth);
1239
+ const logicalLines = value.split('\n');
927
1240
  // If empty, show placeholder
928
- if (lines.length === 1 && lines[0] === '') {
1241
+ if (logicalLines.length === 1 && logicalLines[0] === '') {
929
1242
  return React.createElement(Text, { color: "gray" }, placeholder);
930
1243
  }
931
- // Calculate cursor line and column
1244
+ // For rendering, we still use logical lines (split by \n) since we render character-by-character
1245
+ // But for height calculation, we use visualLines.length
1246
+ const lines = logicalLines;
1247
+ // Calculate cursor line and column (based on logical lines)
932
1248
  let currentPos = 0;
933
1249
  let cursorLine = 0;
934
1250
  let cursorCol = 0;
@@ -945,47 +1261,90 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
945
1261
  cursorLine = lines.length - 1;
946
1262
  cursorCol = lines[lines.length - 1].length;
947
1263
  }
948
- // Calculate visible range
1264
+ // For scrolling, calculate which VISUAL line the cursor is on
1265
+ let cursorVisualLine = 0;
1266
+ for (let i = 0; i < visualLines.length; i++) {
1267
+ if (cursorOffset >= visualLines[i].start && cursorOffset <= visualLines[i].end) {
1268
+ cursorVisualLine = i;
1269
+ break;
1270
+ }
1271
+ }
1272
+ // Handle cursor at very end
1273
+ if (cursorOffset === value.length && visualLines.length > 0) {
1274
+ cursorVisualLine = visualLines.length - 1;
1275
+ }
1276
+ // Calculate visible range using VISUAL lines count for proper scrolling
1277
+ const totalVisualLines = visualLines.length;
949
1278
  let startLine = 0;
950
- if (lines.length > MAX_VISIBLE_LINES) {
951
- if (cursorLine < MAX_VISIBLE_LINES) {
1279
+ if (totalVisualLines > MAX_VISIBLE_LINES) {
1280
+ // Use visual line position for scrolling calculation
1281
+ if (cursorVisualLine < MAX_VISIBLE_LINES) {
952
1282
  startLine = 0;
953
1283
  }
954
1284
  else {
955
- startLine = cursorLine - MAX_VISIBLE_LINES + 1;
1285
+ startLine = cursorVisualLine - MAX_VISIBLE_LINES + 1;
956
1286
  }
957
1287
  }
958
- const endLine = Math.min(startLine + MAX_VISIBLE_LINES, lines.length);
959
- const visibleLines = lines.slice(startLine, endLine);
1288
+ const endLine = Math.min(startLine + MAX_VISIBLE_LINES, totalVisualLines);
1289
+ // Get the text content for each visual line
1290
+ const visibleVisualLines = visualLines.slice(startLine, endLine);
960
1291
  return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
961
1292
  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;
1293
+ visibleVisualLines.map((vLine, idx) => {
1294
+ const actualVisualLineIndex = startLine + idx;
1295
+ // Extract the text content for this visual line
1296
+ const lineText = value.slice(vLine.start, vLine.end);
1297
+ const lineStartPos = vLine.start;
1298
+ // Determine if cursor is on this visual line
1299
+ const isCursorLine = cursorOffset >= vLine.start && cursorOffset <= vLine.end;
1300
+ const cursorCol = isCursorLine ? cursorOffset - vLine.start : -1;
1301
+ // Is this the last visual line?
1302
+ const isLastLine = actualVisualLineIndex === totalVisualLines - 1;
970
1303
  if (!isActive) {
971
- if (line.length === 0) {
1304
+ if (lineText.length === 0) {
972
1305
  return React.createElement(Text, { key: idx }, " ");
973
1306
  }
974
- return React.createElement(Text, { key: idx }, line);
1307
+ return React.createElement(Text, { key: idx }, lineText);
975
1308
  }
976
1309
  // Render with selection and cursor
977
- const chars = line.split('');
1310
+ const chars = lineText.split('');
978
1311
  const renderedChars = chars.map((char, charIdx) => {
979
1312
  const absPos = lineStartPos + charIdx;
980
1313
  const isSelected = selection &&
981
1314
  absPos >= Math.min(selection.start, selection.end) &&
982
1315
  absPos < Math.max(selection.start, selection.end);
983
1316
  // Check if this character is part of an active file tag (being typed after @)
1317
+ let activeFileTagEnd = activeFileTagStart !== null ? activeFileTagStart : 0;
1318
+ if (activeFileTagStart !== null) {
1319
+ for (let j = activeFileTagStart; j < value.length && j < cursorOffset; j++) {
1320
+ if (/[\s\n]/.test(value[j])) {
1321
+ break;
1322
+ }
1323
+ activeFileTagEnd = j + 1;
1324
+ }
1325
+ }
984
1326
  const isInActiveFileTag = activeFileTagStart !== null &&
985
1327
  absPos >= activeFileTagStart &&
986
- absPos < cursorOffset;
1328
+ absPos < activeFileTagEnd;
987
1329
  // Check if this character is part of a confirmed file tag
988
- const isInConfirmedFileTag = confirmedFileTags.some(tag => absPos >= tag.start && absPos < tag.end);
1330
+ let isInConfirmedFileTag = false;
1331
+ if (!commandMode) {
1332
+ const fileTagRegex = /(?:^|[\s\n])(@[^\s@]+)/g;
1333
+ let match;
1334
+ while ((match = fileTagRegex.exec(value)) !== null) {
1335
+ const fullMatch = match[0];
1336
+ const tagContent = match[1];
1337
+ const tagStart = match.index + (fullMatch.length - tagContent.length);
1338
+ const tagEnd = tagStart + tagContent.length;
1339
+ if (activeFileTagStart !== null && tagStart === activeFileTagStart) {
1340
+ continue;
1341
+ }
1342
+ if (absPos >= tagStart && absPos < tagEnd) {
1343
+ isInConfirmedFileTag = true;
1344
+ break;
1345
+ }
1346
+ }
1347
+ }
989
1348
  const isCursor = isCursorLine && charIdx === cursorCol;
990
1349
  if (isCursor) {
991
1350
  return React.createElement(Text, { key: charIdx, inverse: true, color: isSelected ? "yellow" : undefined }, char);
@@ -997,10 +1356,21 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
997
1356
  if (isInConfirmedFileTag || isInActiveFileTag) {
998
1357
  return React.createElement(Text, { key: charIdx, color: "#00ccff", bold: true }, char);
999
1358
  }
1359
+ // Check if this character is part of a validated #image command
1360
+ let isInValidatedImage = false;
1361
+ for (const pos of validatedImagePositions) {
1362
+ if (absPos >= pos.start && absPos < pos.end) {
1363
+ isInValidatedImage = true;
1364
+ break;
1365
+ }
1366
+ }
1367
+ if (isInValidatedImage) {
1368
+ return React.createElement(Text, { key: charIdx, color: "#ff69b4", bold: true }, char);
1369
+ }
1000
1370
  return React.createElement(Text, { key: charIdx }, char);
1001
1371
  });
1002
1372
  // Handle cursor at end of line
1003
- if (isCursorLine && cursorCol === line.length) {
1373
+ if (isCursorLine && cursorCol === lineText.length) {
1004
1374
  renderedChars.push(React.createElement(Text, { key: "cursor", inverse: true }, " "));
1005
1375
  }
1006
1376
  // Render Autocomplete Ghost Text
@@ -1016,7 +1386,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1016
1386
  }
1017
1387
  return React.createElement(Text, { key: idx }, renderedChars);
1018
1388
  }),
1019
- endLine < lines.length && React.createElement(Text, { color: "gray" }, "\u2193 ...")));
1389
+ endLine < totalVisualLines && React.createElement(Text, { color: "gray" }, "\u2193 ...")));
1020
1390
  };
1021
1391
  return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: rejectFlash ? "#ff3366" : (commandMode ? "#00cc66" : "#257aa5ff"), paddingX: 1, paddingY: 0, width: "100%" },
1022
1392
  React.createElement(Box, { marginY: 1, justifyContent: "space-between", width: "100%" },
@@ -1045,6 +1415,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1045
1415
  !commandMode && (React.createElement(Box, { marginLeft: 1 },
1046
1416
  React.createElement(ContextWindowIndicator, { currentTokens: currentTokens, maxTokens: maxTokens }))))),
1047
1417
  slashAutocompleteVisible && (React.createElement(SlashCommandAutocomplete, { commands: slashAutocompleteCommands, selectedIndex: slashAutocompleteSelectedIndex })),
1048
- fileTagAutocompleteVisible && (React.createElement(FileTagAutocomplete, { files: fileTagSuggestions, selectedIndex: fileTagSelectedIndex }))));
1418
+ fileTagAutocompleteVisible && (React.createElement(FileTagAutocomplete, { files: fileTagSuggestions, selectedIndex: fileTagSelectedIndex })),
1419
+ clipboardAutocompleteVisible && (React.createElement(ClipboardImageAutocomplete, { images: clipboardImages, selectedIndex: clipboardSelectedIndex, isLoading: clipboardLoading }))));
1049
1420
  });
1050
1421
  //# sourceMappingURL=InputBox.js.map