centaurus-cli 2.8.9 → 2.9.0

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 (88) hide show
  1. package/dist/cli-adapter.d.ts +21 -2
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +302 -68
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/models.d.ts +8 -0
  6. package/dist/config/models.d.ts.map +1 -1
  7. package/dist/config/models.js +29 -0
  8. package/dist/config/models.js.map +1 -1
  9. package/dist/config/slash-commands.d.ts +1 -0
  10. package/dist/config/slash-commands.d.ts.map +1 -1
  11. package/dist/config/slash-commands.js +13 -1
  12. package/dist/config/slash-commands.js.map +1 -1
  13. package/dist/hooks/useConnectivity.d.ts +2 -0
  14. package/dist/hooks/useConnectivity.d.ts.map +1 -0
  15. package/dist/hooks/useConnectivity.js +12 -0
  16. package/dist/hooks/useConnectivity.js.map +1 -0
  17. package/dist/index.js +3 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  20. package/dist/mcp/mcp-command-handler.js +0 -3
  21. package/dist/mcp/mcp-command-handler.js.map +1 -1
  22. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  23. package/dist/mcp/mcp-tool-wrapper.js +8 -0
  24. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  25. package/dist/services/ai-service-client.d.ts +1 -0
  26. package/dist/services/ai-service-client.d.ts.map +1 -1
  27. package/dist/services/ai-service-client.js.map +1 -1
  28. package/dist/services/api-client.d.ts +26 -40
  29. package/dist/services/api-client.d.ts.map +1 -1
  30. package/dist/services/api-client.js +32 -34
  31. package/dist/services/api-client.js.map +1 -1
  32. package/dist/services/connectivity-manager.d.ts +18 -0
  33. package/dist/services/connectivity-manager.d.ts.map +1 -0
  34. package/dist/services/connectivity-manager.js +72 -0
  35. package/dist/services/connectivity-manager.js.map +1 -0
  36. package/dist/services/local-chat-storage.d.ts +5 -0
  37. package/dist/services/local-chat-storage.d.ts.map +1 -1
  38. package/dist/services/local-chat-storage.js +33 -0
  39. package/dist/services/local-chat-storage.js.map +1 -1
  40. package/dist/tools/background-command.d.ts +11 -0
  41. package/dist/tools/background-command.d.ts.map +1 -0
  42. package/dist/tools/background-command.js +162 -0
  43. package/dist/tools/background-command.js.map +1 -0
  44. package/dist/tools/command.d.ts.map +1 -1
  45. package/dist/tools/command.js +6 -3
  46. package/dist/tools/command.js.map +1 -1
  47. package/dist/tools/create-image.d.ts +10 -0
  48. package/dist/tools/create-image.d.ts.map +1 -0
  49. package/dist/tools/create-image.js +189 -0
  50. package/dist/tools/create-image.js.map +1 -0
  51. package/dist/ui/components/App.d.ts +3 -2
  52. package/dist/ui/components/App.d.ts.map +1 -1
  53. package/dist/ui/components/App.js +127 -44
  54. package/dist/ui/components/App.js.map +1 -1
  55. package/dist/ui/components/ContextWindowIndicator.d.ts.map +1 -1
  56. package/dist/ui/components/ContextWindowIndicator.js +43 -22
  57. package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
  58. package/dist/ui/components/InputBox.d.ts.map +1 -1
  59. package/dist/ui/components/InputBox.js +198 -199
  60. package/dist/ui/components/InputBox.js.map +1 -1
  61. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  62. package/dist/ui/components/MessageDisplay.js +8 -15
  63. package/dist/ui/components/MessageDisplay.js.map +1 -1
  64. package/dist/ui/components/SlashCommandAutocomplete.d.ts +2 -0
  65. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -1
  66. package/dist/ui/components/SlashCommandAutocomplete.js +19 -10
  67. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  68. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  69. package/dist/ui/components/StatusBar.js +4 -0
  70. package/dist/ui/components/StatusBar.js.map +1 -1
  71. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  72. package/dist/ui/components/ToolExecutionMessage.js +153 -39
  73. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  74. package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
  75. package/dist/ui/components/ToolExecutionStatus.js +1 -0
  76. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  77. package/dist/utils/chat-formatter.d.ts +12 -0
  78. package/dist/utils/chat-formatter.d.ts.map +1 -0
  79. package/dist/utils/chat-formatter.js +326 -0
  80. package/dist/utils/chat-formatter.js.map +1 -0
  81. package/dist/utils/input-classifier.d.ts.map +1 -1
  82. package/dist/utils/input-classifier.js +139 -20
  83. package/dist/utils/input-classifier.js.map +1 -1
  84. package/dist/utils/text-clipboard.d.ts +12 -0
  85. package/dist/utils/text-clipboard.d.ts.map +1 -0
  86. package/dist/utils/text-clipboard.js +63 -0
  87. package/dist/utils/text-clipboard.js.map +1 -0
  88. package/package.json +1 -1
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
+ import { useConnectivity } from '../../hooks/useConnectivity.js';
5
6
  import { Breadcrumbs } from './Breadcrumbs.js';
6
7
  import { ContextWindowIndicator } from './ContextWindowIndicator.js';
7
8
  import { logDebug } from '../../utils/logger.js';
@@ -9,9 +10,9 @@ import { detectIntent } from '../../utils/input-classifier.js';
9
10
  import { CommandHistoryManager } from '../../utils/command-history.js';
10
11
  import { SlashCommandAutocomplete } from './SlashCommandAutocomplete.js';
11
12
  import { FileTagAutocomplete } from './FileTagAutocomplete.js';
12
- import { ClipboardImageAutocomplete } from './ClipboardImageAutocomplete.js';
13
13
  import { filterCommands } from '../../config/slash-commands.js';
14
14
  import { getClipboardImages } from '../../services/clipboard-service.js';
15
+ import { useTerminalDimensions, TERMINAL_HEIGHT_CONSTANTS } from '../../hooks/useTerminalDimensions.js';
15
16
  const getVisualLines = (text, width) => {
16
17
  const logicalLines = text.split('\n');
17
18
  const visualLines = [];
@@ -58,6 +59,10 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
58
59
  const [historyIndex, setHistoryIndex] = useState(-1);
59
60
  const [tempValue, setTempValue] = useState('');
60
61
  const ignoreNextChangeRef = useRef(false);
62
+ // Refs to track current value and cursor for paste handling
63
+ // This prevents stale closure issues when Ink calls useInput multiple times during paste
64
+ const valueRef = useRef(initialValue);
65
+ const cursorOffsetRef = useRef(0);
61
66
  // Auto Mode State
62
67
  const [isAutoMode, setIsAutoMode] = useState(true);
63
68
  const [detectedIntent, setDetectedIntent] = useState('ai');
@@ -74,30 +79,34 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
74
79
  const [slashAutocompleteVisible, setSlashAutocompleteVisible] = useState(false);
75
80
  const [slashAutocompleteCommands, setSlashAutocompleteCommands] = useState([]);
76
81
  const [slashAutocompleteSelectedIndex, setSlashAutocompleteSelectedIndex] = useState(0);
82
+ const [slashAutocompleteScrollOffset, setSlashAutocompleteScrollOffset] = useState(0);
83
+ // Connectivity State
84
+ const isConnected = useConnectivity();
85
+ // Terminal dimensions for height-aware autocomplete
86
+ const dimensions = useTerminalDimensions();
87
+ // Max 5 items, min 0 for very small terminals (use MIN_ROWS_FOR_STREAMING as threshold)
88
+ const slashMaxVisibleItems = dimensions.rows < TERMINAL_HEIGHT_CONSTANTS.MIN_ROWS_FOR_STREAMING
89
+ ? 0
90
+ : Math.min(5, Math.max(1, Math.floor((dimensions.rows - 20) / 3)));
77
91
  // File Tag Autocomplete State (@ symbol)
78
92
  const [fileTagAutocompleteVisible, setFileTagAutocompleteVisible] = useState(false);
79
93
  const [fileTagSuggestions, setFileTagSuggestions] = useState([]);
80
94
  const [fileTagSelectedIndex, setFileTagSelectedIndex] = useState(0);
81
95
  const [activeFileTagStart, setActiveFileTagStart] = useState(null);
82
96
  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);
97
+ // Clipboard Image State (Alt+V paste)
89
98
  const [confirmedClipboardImages, setConfirmedClipboardImages] = useState([]);
90
- // Track positions of validated #image commands (for pink highlighting)
91
- const [validatedImagePositions, setValidatedImagePositions] = useState([]);
92
99
  // Track visual line count to force re-renders when text wraps
93
100
  // This is necessary because Ink doesn't automatically update layout when text wraps
94
101
  const [visualLineCount, setVisualLineCount] = useState(1);
95
102
  // Configuration for scrolling
96
103
  const MAX_VISIBLE_LINES = 9;
97
- // Wrapper for setValue that also notifies parent of changes
104
+ // Wrapper for setValue that also notifies parent of changes and updates ref
98
105
  const setValue = React.useCallback((newValue) => {
99
106
  setValueInternal(prev => {
100
107
  const resolvedValue = typeof newValue === 'function' ? newValue(prev) : newValue;
108
+ // Update ref synchronously for paste handling
109
+ valueRef.current = resolvedValue;
101
110
  // Notify parent of value change for preservation across screen transitions
102
111
  if (onValueChange) {
103
112
  onValueChange(resolvedValue);
@@ -105,12 +114,29 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
105
114
  return resolvedValue;
106
115
  });
107
116
  }, [onValueChange]);
117
+ // Wrapper for setCursorOffset that also updates ref
118
+ const setCursorOffsetWithRef = React.useCallback((newOffset) => {
119
+ setCursorOffset(prev => {
120
+ const resolvedOffset = typeof newOffset === 'function' ? newOffset(prev) : newOffset;
121
+ // Update ref synchronously for paste handling
122
+ cursorOffsetRef.current = resolvedOffset;
123
+ return resolvedOffset;
124
+ });
125
+ }, []);
108
126
  // Initialize cursor position when initialValue is provided
109
127
  useEffect(() => {
110
128
  if (initialValue && initialValue.length > 0) {
111
129
  setCursorOffset(initialValue.length);
130
+ cursorOffsetRef.current = initialValue.length;
112
131
  }
113
132
  }, []); // Only run on mount
133
+ // Keep refs in sync with state (for cases where state is updated directly)
134
+ useEffect(() => {
135
+ valueRef.current = value;
136
+ }, [value]);
137
+ useEffect(() => {
138
+ cursorOffsetRef.current = cursorOffset;
139
+ }, [cursorOffset]);
114
140
  // Load history on mount
115
141
  useEffect(() => {
116
142
  CommandHistoryManager.getInstance().load();
@@ -289,127 +315,6 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
289
315
  setActiveFileTagStart(null);
290
316
  }
291
317
  }, [value, cursorOffset, commandMode, currentWorkingDirectory]);
292
- // Hash (#) symbol detection effect for clipboard image autocomplete
293
- // When user types # in Agent mode, check clipboard for images and show dropdown
294
- useEffect(() => {
295
- // Don't show in command mode
296
- if (commandMode) {
297
- setClipboardAutocompleteVisible(false);
298
- setActiveClipboardStart(null);
299
- return;
300
- }
301
- // Find if cursor is right after a # character (or typing after #)
302
- let hashPosition = -1;
303
- for (let i = cursorOffset - 1; i >= 0; i--) {
304
- const char = value[i];
305
- // Stop if we hit whitespace
306
- if (/[\s\n]/.test(char))
307
- break;
308
- if (char === '#') {
309
- hashPosition = i;
310
- break;
311
- }
312
- }
313
- // Only treat # as image trigger if it's at start of input OR preceded by whitespace
314
- if (hashPosition === -1 || (hashPosition > 0 && !/[\s\n]/.test(value[hashPosition - 1]))) {
315
- setClipboardAutocompleteVisible(false);
316
- setActiveClipboardStart(null);
317
- return;
318
- }
319
- // Extract what's typed after #
320
- const query = value.slice(hashPosition + 1, cursorOffset).toLowerCase();
321
- // If #image is fully typed (with space or at end), hide dropdown (validation will handle it)
322
- if (query === 'image' || query.startsWith('image ')) {
323
- setClipboardAutocompleteVisible(false);
324
- setActiveClipboardStart(null);
325
- return;
326
- }
327
- // If user has typed something that doesn't match "image" prefix, hide dropdown
328
- if (query.length > 0 && !'image'.startsWith(query)) {
329
- setClipboardAutocompleteVisible(false);
330
- setActiveClipboardStart(null);
331
- return;
332
- }
333
- // Show dropdown and check clipboard
334
- setActiveClipboardStart(hashPosition);
335
- setClipboardLoading(true);
336
- setClipboardAutocompleteVisible(true);
337
- // Check clipboard asynchronously
338
- const checkClipboard = async () => {
339
- try {
340
- const images = await getClipboardImages();
341
- setClipboardImages(images);
342
- setClipboardLoading(false);
343
- setClipboardSelectedIndex(0);
344
- }
345
- catch (error) {
346
- logDebug(`Failed to check clipboard: ${error instanceof Error ? error.message : 'Unknown error'}`);
347
- setClipboardImages([]);
348
- setClipboardLoading(false);
349
- }
350
- };
351
- checkClipboard();
352
- }, [value, cursorOffset, commandMode]);
353
- // #image command detection effect
354
- // When user types #image, check clipboard for images and validate
355
- useEffect(() => {
356
- // Don't check in command mode
357
- if (commandMode)
358
- return;
359
- // Find all #image occurrences in the text
360
- const regex = /#image\b/gi;
361
- const matches = [];
362
- let match;
363
- while ((match = regex.exec(value)) !== null) {
364
- matches.push({ start: match.index, end: match.index + match[0].length });
365
- }
366
- // If no #image in text, clear validated positions
367
- if (matches.length === 0) {
368
- if (validatedImagePositions.length > 0) {
369
- setValidatedImagePositions([]);
370
- setConfirmedClipboardImages([]);
371
- }
372
- return;
373
- }
374
- // Check for new #image occurrences that haven't been validated yet
375
- const unvalidatedMatches = matches.filter(m => !validatedImagePositions.some(v => v.start === m.start && v.end === m.end));
376
- if (unvalidatedMatches.length === 0)
377
- return;
378
- // Check clipboard and validate new #image occurrences
379
- const validateImages = async () => {
380
- logDebug(`Found ${unvalidatedMatches.length} new #image occurrences to validate`);
381
- try {
382
- const images = await getClipboardImages();
383
- logDebug(`Clipboard check returned ${images.length} images`);
384
- if (images.length > 0) {
385
- const image = images[0];
386
- logDebug(`Image found in clipboard: ${image.displayName}, ${image.sizeBytes} bytes`);
387
- // Add the clipboard image once for EACH #image occurrence
388
- // This allows multiple #image tags in the same prompt to each attach the clipboard image
389
- // Each #image tag represents an intent to include the image at that location
390
- setConfirmedClipboardImages(prev => {
391
- // Create a unique copy of the image for each #image occurrence
392
- const newImages = unvalidatedMatches.map((_, index) => ({
393
- ...image,
394
- // Give each a unique id so they're treated as separate entries for upload
395
- id: `${image.id}_${Date.now()}_${index}`
396
- }));
397
- return [...prev, ...newImages];
398
- });
399
- // Mark all unvalidated positions as validated
400
- setValidatedImagePositions(prev => [...prev, ...unvalidatedMatches]);
401
- logDebug(`Validated ${unvalidatedMatches.length} #image occurrences, added ${unvalidatedMatches.length} images to queue`);
402
- }
403
- else {
404
- logDebug('No image in clipboard - #image command not validated');
405
- }
406
- }
407
- catch (error) {
408
- logDebug(`Failed to check clipboard: ${error instanceof Error ? error.message : 'Unknown error'}`);
409
- }
410
- };
411
- validateImages();
412
- }, [value, commandMode]);
413
318
  const pushToUndoStack = () => {
414
319
  setUndoStack(prev => [...prev, { value, cursorOffset }]);
415
320
  setRedoStack([]); // Clear redo stack on new action
@@ -433,11 +338,21 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
433
338
  // Handle slash command autocomplete navigation
434
339
  if (slashAutocompleteVisible) {
435
340
  if (key.downArrow) {
436
- setSlashAutocompleteSelectedIndex(prev => Math.min(prev + 1, slashAutocompleteCommands.length - 1));
341
+ const newIndex = Math.min(slashAutocompleteSelectedIndex + 1, slashAutocompleteCommands.length - 1);
342
+ setSlashAutocompleteSelectedIndex(newIndex);
343
+ // Scroll down if selected is below visible window
344
+ if (newIndex >= slashAutocompleteScrollOffset + slashMaxVisibleItems) {
345
+ setSlashAutocompleteScrollOffset(newIndex - slashMaxVisibleItems + 1);
346
+ }
437
347
  return;
438
348
  }
439
349
  if (key.upArrow) {
440
- setSlashAutocompleteSelectedIndex(prev => Math.max(prev - 1, 0));
350
+ const newIndex = Math.max(slashAutocompleteSelectedIndex - 1, 0);
351
+ setSlashAutocompleteSelectedIndex(newIndex);
352
+ // Scroll up if selected is above visible window
353
+ if (newIndex < slashAutocompleteScrollOffset) {
354
+ setSlashAutocompleteScrollOffset(newIndex);
355
+ }
441
356
  return;
442
357
  }
443
358
  if (key.return && input.length <= 1 && !key.shift && !key.ctrl) {
@@ -474,6 +389,13 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
474
389
  setCursorOffset(newValue.length);
475
390
  setSlashAutocompleteVisible(false);
476
391
  }
392
+ else if (value.startsWith('/sync ')) {
393
+ // We're selecting a sync subcommand
394
+ const newValue = `/sync ${selected.name} `;
395
+ setValue(newValue);
396
+ setCursorOffset(newValue.length);
397
+ setSlashAutocompleteVisible(false);
398
+ }
477
399
  else {
478
400
  // Regular slash command, replace everything
479
401
  const newValue = `/${selected.name} `;
@@ -486,6 +408,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
486
408
  if (subcommandMatches.length > 0) {
487
409
  setSlashAutocompleteCommands(subcommandMatches);
488
410
  setSlashAutocompleteSelectedIndex(0);
411
+ setSlashAutocompleteScrollOffset(0);
489
412
  // Keep autocomplete visible for subcommands
490
413
  }
491
414
  else {
@@ -497,6 +420,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
497
420
  if (subcommandMatches.length > 0) {
498
421
  setSlashAutocompleteCommands(subcommandMatches);
499
422
  setSlashAutocompleteSelectedIndex(0);
423
+ setSlashAutocompleteScrollOffset(0);
500
424
  // Keep autocomplete visible for subcommands
501
425
  }
502
426
  else {
@@ -508,6 +432,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
508
432
  if (subcommandMatches.length > 0) {
509
433
  setSlashAutocompleteCommands(subcommandMatches);
510
434
  setSlashAutocompleteSelectedIndex(0);
435
+ setSlashAutocompleteScrollOffset(0);
511
436
  // Keep autocomplete visible for subcommands
512
437
  }
513
438
  else {
@@ -519,6 +444,19 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
519
444
  if (subcommandMatches.length > 0) {
520
445
  setSlashAutocompleteCommands(subcommandMatches);
521
446
  setSlashAutocompleteSelectedIndex(0);
447
+ setSlashAutocompleteScrollOffset(0);
448
+ // Keep autocomplete visible for subcommands
449
+ }
450
+ else {
451
+ setSlashAutocompleteVisible(false);
452
+ }
453
+ }
454
+ else if (selected.name === 'sync') {
455
+ const subcommandMatches = filterCommands('sync ');
456
+ if (subcommandMatches.length > 0) {
457
+ setSlashAutocompleteCommands(subcommandMatches);
458
+ setSlashAutocompleteSelectedIndex(0);
459
+ setSlashAutocompleteScrollOffset(0);
522
460
  // Keep autocomplete visible for subcommands
523
461
  }
524
462
  else {
@@ -609,54 +547,36 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
609
547
  return;
610
548
  }
611
549
  }
612
- // Handle clipboard image (#) autocomplete navigation
613
- if (clipboardAutocompleteVisible) {
614
- if (key.downArrow) {
615
- setClipboardSelectedIndex(prev => Math.min(prev + 1, Math.max(clipboardImages.length - 1, 0)));
616
- return;
617
- }
618
- if (key.upArrow) {
619
- setClipboardSelectedIndex(prev => Math.max(prev - 1, 0));
620
- return;
621
- }
622
- if (key.return && input.length <= 1 && !key.shift && !key.ctrl) {
623
- // Select the image option and autocomplete #image
624
- if (clipboardImages.length > 0 && activeClipboardStart !== null) {
625
- pushToUndoStack();
626
- // Replace # with #image (and add space after)
627
- const beforeHash = value.slice(0, activeClipboardStart);
628
- const afterCursor = value.slice(cursorOffset);
629
- const newValue = beforeHash + '#image ' + afterCursor;
630
- const newCursorPos = activeClipboardStart + 7; // length of "#image "
631
- setValue(newValue);
632
- setCursorOffset(newCursorPos);
633
- setClipboardAutocompleteVisible(false);
634
- setActiveClipboardStart(null);
550
+ // Alt+V: Paste image from clipboard
551
+ // Detect Alt+V on Windows/Linux (key.meta is often Alt on Windows in Ink)
552
+ const isAltV = (key.meta && input === 'v') || (input === '√'); // '√' is Alt+V on some systems
553
+ if (isAltV && !commandMode) {
554
+ // Check clipboard for images asynchronously
555
+ (async () => {
556
+ try {
557
+ const images = await getClipboardImages();
558
+ if (images.length > 0) {
559
+ const image = images[0];
560
+ logDebug(`Alt+V: Image found in clipboard: ${image.displayName}, ${image.sizeBytes} bytes`);
561
+ // Add image to confirmed list with unique ID
562
+ setConfirmedClipboardImages(prev => [
563
+ ...prev,
564
+ {
565
+ ...image,
566
+ id: `${image.id}_${Date.now()}`
567
+ }
568
+ ]);
569
+ }
570
+ else {
571
+ logDebug('Alt+V: No image in clipboard');
572
+ }
635
573
  }
636
- return;
637
- }
638
- if (key.escape) {
639
- setClipboardAutocompleteVisible(false);
640
- setActiveClipboardStart(null);
641
- return;
642
- }
643
- // Tab also selects
644
- if (key.tab && !key.shift) {
645
- if (clipboardImages.length > 0 && activeClipboardStart !== null) {
646
- pushToUndoStack();
647
- const beforeHash = value.slice(0, activeClipboardStart);
648
- const afterCursor = value.slice(cursorOffset);
649
- const newValue = beforeHash + '#image ' + afterCursor;
650
- const newCursorPos = activeClipboardStart + 7;
651
- setValue(newValue);
652
- setCursorOffset(newCursorPos);
653
- setClipboardAutocompleteVisible(false);
654
- setActiveClipboardStart(null);
574
+ catch (error) {
575
+ logDebug(`Alt+V: Failed to check clipboard: ${error instanceof Error ? error.message : 'Unknown error'}`);
655
576
  }
656
- return;
657
- }
577
+ })();
578
+ return;
658
579
  }
659
- // Clipboard image paste is handled via Ctrl+V below
660
580
  // DELETE WORD BACKWARDS - Check this FIRST before standard backspace/delete
661
581
  // Triggers on any of these conditions:
662
582
  // 1. Ctrl+W (char code 23) - Standard Unix terminal shortcut
@@ -690,6 +610,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
690
610
  setSlashAutocompleteCommands(matches);
691
611
  setSlashAutocompleteVisible(true);
692
612
  setSlashAutocompleteSelectedIndex(0);
613
+ setSlashAutocompleteScrollOffset(0);
693
614
  }
694
615
  else {
695
616
  setSlashAutocompleteVisible(false);
@@ -703,6 +624,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
703
624
  setSlashAutocompleteCommands(matches);
704
625
  setSlashAutocompleteVisible(true);
705
626
  setSlashAutocompleteSelectedIndex(0);
627
+ setSlashAutocompleteScrollOffset(0);
706
628
  }
707
629
  else {
708
630
  setSlashAutocompleteVisible(false);
@@ -716,6 +638,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
716
638
  setSlashAutocompleteCommands(matches);
717
639
  setSlashAutocompleteVisible(true);
718
640
  setSlashAutocompleteSelectedIndex(0);
641
+ setSlashAutocompleteScrollOffset(0);
719
642
  }
720
643
  else {
721
644
  setSlashAutocompleteVisible(false);
@@ -729,6 +652,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
729
652
  setSlashAutocompleteCommands(matches);
730
653
  setSlashAutocompleteVisible(true);
731
654
  setSlashAutocompleteSelectedIndex(0);
655
+ setSlashAutocompleteScrollOffset(0);
732
656
  }
733
657
  else {
734
658
  setSlashAutocompleteVisible(false);
@@ -742,6 +666,21 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
742
666
  setSlashAutocompleteCommands(matches);
743
667
  setSlashAutocompleteVisible(true);
744
668
  setSlashAutocompleteSelectedIndex(0);
669
+ setSlashAutocompleteScrollOffset(0);
670
+ }
671
+ else {
672
+ setSlashAutocompleteVisible(false);
673
+ }
674
+ }
675
+ else if (newValue.startsWith('/sync ')) {
676
+ // Sync subcommands
677
+ const fullQuery = newValue.slice(1);
678
+ const matches = filterCommands(fullQuery);
679
+ if (matches.length > 0) {
680
+ setSlashAutocompleteCommands(matches);
681
+ setSlashAutocompleteVisible(true);
682
+ setSlashAutocompleteSelectedIndex(0);
683
+ setSlashAutocompleteScrollOffset(0);
745
684
  }
746
685
  else {
747
686
  setSlashAutocompleteVisible(false);
@@ -813,7 +752,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
813
752
  setCursorOffset(value.length);
814
753
  return;
815
754
  }
816
- // Note: Clipboard images are handled via #image command detection effect
755
+ // Note: Clipboard images are handled via Alt+V keyboard shortcut
817
756
  // DELETE CHAR - Only runs if Delete Word did NOT trigger
818
757
  // Triggers on:
819
758
  // 1. Backspace or Delete key flag is present
@@ -853,6 +792,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
853
792
  setSlashAutocompleteCommands(matches);
854
793
  setSlashAutocompleteVisible(true);
855
794
  setSlashAutocompleteSelectedIndex(0);
795
+ setSlashAutocompleteScrollOffset(0);
856
796
  }
857
797
  else {
858
798
  setSlashAutocompleteVisible(false);
@@ -866,6 +806,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
866
806
  setSlashAutocompleteCommands(matches);
867
807
  setSlashAutocompleteVisible(true);
868
808
  setSlashAutocompleteSelectedIndex(0);
809
+ setSlashAutocompleteScrollOffset(0);
869
810
  }
870
811
  else {
871
812
  setSlashAutocompleteVisible(false);
@@ -879,6 +820,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
879
820
  setSlashAutocompleteCommands(matches);
880
821
  setSlashAutocompleteVisible(true);
881
822
  setSlashAutocompleteSelectedIndex(0);
823
+ setSlashAutocompleteScrollOffset(0);
882
824
  }
883
825
  else {
884
826
  setSlashAutocompleteVisible(false);
@@ -892,6 +834,35 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
892
834
  setSlashAutocompleteCommands(matches);
893
835
  setSlashAutocompleteVisible(true);
894
836
  setSlashAutocompleteSelectedIndex(0);
837
+ setSlashAutocompleteScrollOffset(0);
838
+ }
839
+ else {
840
+ setSlashAutocompleteVisible(false);
841
+ }
842
+ }
843
+ else if (newValue.startsWith('/background-task ') || newValue.startsWith('/bkg ') || newValue.startsWith('/bg-task ')) {
844
+ // Background-task subcommands
845
+ const fullQuery = newValue.slice(1);
846
+ const matches = filterCommands(fullQuery);
847
+ if (matches.length > 0) {
848
+ setSlashAutocompleteCommands(matches);
849
+ setSlashAutocompleteVisible(true);
850
+ setSlashAutocompleteSelectedIndex(0);
851
+ setSlashAutocompleteScrollOffset(0);
852
+ }
853
+ else {
854
+ setSlashAutocompleteVisible(false);
855
+ }
856
+ }
857
+ else if (newValue.startsWith('/sync ')) {
858
+ // Sync subcommands
859
+ const fullQuery = newValue.slice(1);
860
+ const matches = filterCommands(fullQuery);
861
+ if (matches.length > 0) {
862
+ setSlashAutocompleteCommands(matches);
863
+ setSlashAutocompleteVisible(true);
864
+ setSlashAutocompleteSelectedIndex(0);
865
+ setSlashAutocompleteScrollOffset(0);
895
866
  }
896
867
  else {
897
868
  setSlashAutocompleteVisible(false);
@@ -930,6 +901,16 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
930
901
  }
931
902
  else {
932
903
  // Enter: Submit
904
+ // Check if submission is allowed when disconnected
905
+ // Allowed: /exit command, OR Command Mode is active, OR detected intent is 'command'
906
+ const isExitCommand = value.trim() === '/exit';
907
+ const isCommandIntent = detectedIntent === 'command';
908
+ const isAllowedOffline = isConnected || isExitCommand || commandMode || isCommandIntent;
909
+ if (!isAllowedOffline) {
910
+ setRejectFlash(true);
911
+ setTimeout(() => setRejectFlash(false), 1000);
912
+ return;
913
+ }
933
914
  handleSubmit();
934
915
  }
935
916
  return;
@@ -1125,21 +1106,28 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1125
1106
  pushToUndoStack();
1126
1107
  // Handle paste with newlines
1127
1108
  const cleanedInput = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
1128
- let newValue = value;
1129
- let newOffset = cursorOffset;
1109
+ // Use refs to get the latest value and cursor position
1110
+ // This prevents stale closure issues when Ink calls useInput multiple times during paste
1111
+ const currentValue = valueRef.current;
1112
+ const currentCursorOffset = cursorOffsetRef.current;
1113
+ let newValue = currentValue;
1114
+ let newOffset = currentCursorOffset;
1130
1115
  if (selection) {
1131
1116
  const start = Math.min(selection.start, selection.end);
1132
1117
  const end = Math.max(selection.start, selection.end);
1133
- newValue = value.slice(0, start) + cleanedInput + value.slice(end);
1118
+ newValue = currentValue.slice(0, start) + cleanedInput + currentValue.slice(end);
1134
1119
  newOffset = start + cleanedInput.length;
1135
1120
  setSelection(null);
1136
1121
  }
1137
1122
  else {
1138
- newValue = value.slice(0, cursorOffset) + cleanedInput + value.slice(cursorOffset);
1139
- newOffset = cursorOffset + cleanedInput.length;
1123
+ newValue = currentValue.slice(0, currentCursorOffset) + cleanedInput + currentValue.slice(currentCursorOffset);
1124
+ newOffset = currentCursorOffset + cleanedInput.length;
1140
1125
  }
1126
+ // Update refs immediately for subsequent paste chunks
1127
+ valueRef.current = newValue;
1128
+ cursorOffsetRef.current = newOffset;
1141
1129
  setValue(newValue);
1142
- setCursorOffset(newOffset);
1130
+ setCursorOffsetWithRef(newOffset);
1143
1131
  // Reset history/completions
1144
1132
  setHistoryIndex(-1);
1145
1133
  setCompletions([]);
@@ -1152,6 +1140,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1152
1140
  setSlashAutocompleteCommands(matches);
1153
1141
  setSlashAutocompleteVisible(true);
1154
1142
  setSlashAutocompleteSelectedIndex(0);
1143
+ setSlashAutocompleteScrollOffset(0);
1155
1144
  }
1156
1145
  else {
1157
1146
  setSlashAutocompleteVisible(false);
@@ -1165,6 +1154,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1165
1154
  setSlashAutocompleteCommands(matches);
1166
1155
  setSlashAutocompleteVisible(true);
1167
1156
  setSlashAutocompleteSelectedIndex(0);
1157
+ setSlashAutocompleteScrollOffset(0);
1168
1158
  }
1169
1159
  else {
1170
1160
  setSlashAutocompleteVisible(false);
@@ -1178,6 +1168,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1178
1168
  setSlashAutocompleteCommands(matches);
1179
1169
  setSlashAutocompleteVisible(true);
1180
1170
  setSlashAutocompleteSelectedIndex(0);
1171
+ setSlashAutocompleteScrollOffset(0);
1181
1172
  }
1182
1173
  else {
1183
1174
  setSlashAutocompleteVisible(false);
@@ -1191,6 +1182,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1191
1182
  setSlashAutocompleteCommands(matches);
1192
1183
  setSlashAutocompleteVisible(true);
1193
1184
  setSlashAutocompleteSelectedIndex(0);
1185
+ setSlashAutocompleteScrollOffset(0);
1194
1186
  }
1195
1187
  else {
1196
1188
  setSlashAutocompleteVisible(false);
@@ -1204,6 +1196,21 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1204
1196
  setSlashAutocompleteCommands(matches);
1205
1197
  setSlashAutocompleteVisible(true);
1206
1198
  setSlashAutocompleteSelectedIndex(0);
1199
+ setSlashAutocompleteScrollOffset(0);
1200
+ }
1201
+ else {
1202
+ setSlashAutocompleteVisible(false);
1203
+ }
1204
+ }
1205
+ else if (newValue.startsWith('/sync ')) {
1206
+ // Sync subcommands (when user types "/sync ")
1207
+ const fullQuery = newValue.slice(1); // Remove leading "/", pass "sync <subquery>" to filterCommands
1208
+ const matches = filterCommands(fullQuery);
1209
+ if (matches.length > 0) {
1210
+ setSlashAutocompleteCommands(matches);
1211
+ setSlashAutocompleteVisible(true);
1212
+ setSlashAutocompleteSelectedIndex(0);
1213
+ setSlashAutocompleteScrollOffset(0);
1207
1214
  }
1208
1215
  else {
1209
1216
  setSlashAutocompleteVisible(false);
@@ -1453,17 +1460,6 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1453
1460
  if (isInConfirmedFileTag || isInActiveFileTag) {
1454
1461
  return React.createElement(Text, { key: charIdx, color: "#00ccff", bold: true }, char);
1455
1462
  }
1456
- // Check if this character is part of a validated #image command
1457
- let isInValidatedImage = false;
1458
- for (const pos of validatedImagePositions) {
1459
- if (absPos >= pos.start && absPos < pos.end) {
1460
- isInValidatedImage = true;
1461
- break;
1462
- }
1463
- }
1464
- if (isInValidatedImage) {
1465
- return React.createElement(Text, { key: charIdx, color: "#ff69b4", bold: true }, char);
1466
- }
1467
1463
  return React.createElement(Text, { key: charIdx }, char);
1468
1464
  });
1469
1465
  // Handle cursor at end of line
@@ -1501,6 +1497,10 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1501
1497
  React.createElement(Text, { color: "#00ccff", bold: true }, "Auto ["),
1502
1498
  detectedIntent === 'command' ? (React.createElement(Text, { color: "#00cc66", bold: true }, "Terminal")) : (React.createElement(Text, { color: "#00ccff" }, "Agent")),
1503
1499
  React.createElement(Text, { color: "#00ccff", bold: true }, "]"))) : backgroundMode ? (React.createElement(Text, { color: "#9966ff", bold: true }, "Background")) : commandMode ? (React.createElement(Text, { color: "#00cc66", bold: true }, "Terminal")) : planMode ? (React.createElement(Text, { color: "#ffaa00", bold: true }, "Plan")) : (React.createElement(Text, { color: "#00ccff" }, "Agent"))))),
1500
+ confirmedClipboardImages.length > 0 && (React.createElement(Box, { marginBottom: 1, flexDirection: "row", flexWrap: "wrap", gap: 1 }, confirmedClipboardImages.map((img, index) => (React.createElement(Box, { key: img.id, borderStyle: "round", borderColor: "#ff69b4", paddingX: 1 },
1501
+ React.createElement(Text, { color: "#ff69b4", bold: true },
1502
+ "image_",
1503
+ index + 1)))))),
1504
1504
  React.createElement(Box, { flexDirection: "row", width: "100%" },
1505
1505
  React.createElement(Text, { color: "#666666" }, "> "),
1506
1506
  renderInput()),
@@ -1515,8 +1515,7 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
1515
1515
  !commandMode && autoAcceptMode ? (React.createElement(Text, { color: "#00cc66", bold: true }, "[AUTO-ACCEPT: ON]")) : !commandMode ? (React.createElement(Text, { color: "#666666", dimColor: true }, "[AUTO-ACCEPT: OFF]")) : null,
1516
1516
  !commandMode && (React.createElement(Box, { marginLeft: 1 },
1517
1517
  React.createElement(ContextWindowIndicator, { currentTokens: currentTokens, maxTokens: maxTokens }))))),
1518
- slashAutocompleteVisible && (React.createElement(SlashCommandAutocomplete, { commands: slashAutocompleteCommands, selectedIndex: slashAutocompleteSelectedIndex })),
1519
- fileTagAutocompleteVisible && (React.createElement(FileTagAutocomplete, { files: fileTagSuggestions, selectedIndex: fileTagSelectedIndex })),
1520
- clipboardAutocompleteVisible && (React.createElement(ClipboardImageAutocomplete, { images: clipboardImages, selectedIndex: clipboardSelectedIndex, isLoading: clipboardLoading }))));
1518
+ slashAutocompleteVisible && slashMaxVisibleItems > 0 && (React.createElement(SlashCommandAutocomplete, { commands: slashAutocompleteCommands, selectedIndex: slashAutocompleteSelectedIndex, maxVisibleItems: slashMaxVisibleItems, scrollOffset: slashAutocompleteScrollOffset })),
1519
+ fileTagAutocompleteVisible && (React.createElement(FileTagAutocomplete, { files: fileTagSuggestions, selectedIndex: fileTagSelectedIndex }))));
1521
1520
  });
1522
1521
  //# sourceMappingURL=InputBox.js.map