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.
- package/dist/cli-adapter.d.ts +85 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +773 -31
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/mcp-config-manager.d.ts.map +1 -1
- package/dist/config/mcp-config-manager.js +9 -8
- package/dist/config/mcp-config-manager.js.map +1 -1
- package/dist/config/slash-commands.d.ts +2 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +31 -1
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/context/handlers/docker-handler.js.map +1 -1
- package/dist/context/handlers/ssh-handler.d.ts +16 -1
- package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
- package/dist/context/handlers/ssh-handler.js +57 -12
- package/dist/context/handlers/ssh-handler.js.map +1 -1
- package/dist/context/subshell-handler.d.ts +14 -0
- package/dist/context/subshell-handler.d.ts.map +1 -1
- package/dist/hooks/useTerminalDimensions.d.ts +41 -0
- package/dist/hooks/useTerminalDimensions.d.ts.map +1 -0
- package/dist/hooks/useTerminalDimensions.js +84 -0
- package/dist/hooks/useTerminalDimensions.js.map +1 -0
- package/dist/index.js +57 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
- package/dist/mcp/mcp-command-handler.js +3 -1
- package/dist/mcp/mcp-command-handler.js.map +1 -1
- package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
- package/dist/mcp/mcp-server-manager.js +5 -3
- package/dist/mcp/mcp-server-manager.js.map +1 -1
- package/dist/services/api-client.d.ts +24 -0
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +27 -0
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/auth-handler.js +1 -1
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/clipboard-service.d.ts +42 -0
- package/dist/services/clipboard-service.d.ts.map +1 -0
- package/dist/services/clipboard-service.js +217 -0
- package/dist/services/clipboard-service.js.map +1 -0
- package/dist/services/local-chat-storage.d.ts +154 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -0
- package/dist/services/local-chat-storage.js +258 -0
- package/dist/services/local-chat-storage.js.map +1 -0
- package/dist/tools/grep-search.d.ts +5 -0
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +68 -16
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/plan-mode.d.ts +57 -6
- package/dist/tools/plan-mode.d.ts.map +1 -1
- package/dist/tools/plan-mode.js +297 -46
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/read-binary-file.d.ts +10 -0
- package/dist/tools/read-binary-file.d.ts.map +1 -0
- package/dist/tools/read-binary-file.js +210 -0
- package/dist/tools/read-binary-file.js.map +1 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/components/App.d.ts +35 -0
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +622 -43
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/ClipboardImageAutocomplete.d.ts +14 -0
- package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +1 -0
- package/dist/ui/components/ClipboardImageAutocomplete.js +39 -0
- package/dist/ui/components/ClipboardImageAutocomplete.js.map +1 -0
- package/dist/ui/components/ConnectionStatusMessage.d.ts +1 -1
- package/dist/ui/components/ConnectionStatusMessage.d.ts.map +1 -1
- package/dist/ui/components/ConnectionStatusMessage.js +21 -0
- package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts +17 -0
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +1 -0
- package/dist/ui/components/DetailedPlanReviewScreen.js +110 -0
- package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -0
- package/dist/ui/components/InputBox.d.ts +4 -1
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +419 -30
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +20 -6
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts +6 -0
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +66 -3
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.d.ts +8 -0
- package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.js +26 -8
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.d.ts +3 -0
- package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +10 -6
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.js +4 -4
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
- package/dist/ui/components/TaskProgressIndicator.d.ts +18 -0
- package/dist/ui/components/TaskProgressIndicator.d.ts.map +1 -0
- package/dist/ui/components/TaskProgressIndicator.js +72 -0
- package/dist/ui/components/TaskProgressIndicator.js.map +1 -0
- package/dist/ui/components/ThinkingDisplay.d.ts +3 -0
- package/dist/ui/components/ThinkingDisplay.d.ts.map +1 -1
- package/dist/ui/components/ThinkingDisplay.js +6 -4
- package/dist/ui/components/ThinkingDisplay.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +85 -15
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.d.ts +1 -2
- package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.js +108 -27
- package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
- package/dist/utils/custom-commands-manager.d.ts +59 -0
- package/dist/utils/custom-commands-manager.d.ts.map +1 -0
- package/dist/utils/custom-commands-manager.js +142 -0
- package/dist/utils/custom-commands-manager.js.map +1 -0
- package/dist/utils/input-classifier.d.ts +10 -11
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +299 -75
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/terminal-output.d.ts.map +1 -1
- package/dist/utils/terminal-output.js +110 -14
- package/dist/utils/terminal-output.js.map +1 -1
- package/dist/utils/unicode-sanitizer.d.ts +44 -0
- package/dist/utils/unicode-sanitizer.d.ts.map +1 -0
- package/dist/utils/unicode-sanitizer.js +211 -0
- package/dist/utils/unicode-sanitizer.js.map +1 -0
- package/models-config.json +2 -3
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1259
|
+
if (logicalLines.length === 1 && logicalLines[0] === '') {
|
|
929
1260
|
return React.createElement(Text, { color: "gray" }, placeholder);
|
|
930
1261
|
}
|
|
931
|
-
//
|
|
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
|
-
//
|
|
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 (
|
|
951
|
-
|
|
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 =
|
|
1303
|
+
startLine = cursorVisualLine - MAX_VISIBLE_LINES + 1;
|
|
956
1304
|
}
|
|
957
1305
|
}
|
|
958
|
-
const endLine = Math.min(startLine + MAX_VISIBLE_LINES,
|
|
959
|
-
|
|
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
|
-
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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 (
|
|
1322
|
+
if (lineText.length === 0) {
|
|
972
1323
|
return React.createElement(Text, { key: idx }, " ");
|
|
973
1324
|
}
|
|
974
|
-
return React.createElement(Text, { key: idx },
|
|
1325
|
+
return React.createElement(Text, { key: idx }, lineText);
|
|
975
1326
|
}
|
|
976
1327
|
// Render with selection and cursor
|
|
977
|
-
const chars =
|
|
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 <
|
|
1346
|
+
absPos < activeFileTagEnd;
|
|
987
1347
|
// Check if this character is part of a confirmed file tag
|
|
988
|
-
|
|
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 ===
|
|
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 <
|
|
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
|