centaurus-cli 2.8.5 → 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.
- package/dist/cli-adapter.d.ts +85 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +769 -28
- package/dist/cli-adapter.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 +27 -0
- package/dist/index.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 +608 -16
- 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 +2 -1
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +399 -28
- 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/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 +7 -3
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
1241
|
+
if (logicalLines.length === 1 && logicalLines[0] === '') {
|
|
929
1242
|
return React.createElement(Text, { color: "gray" }, placeholder);
|
|
930
1243
|
}
|
|
931
|
-
//
|
|
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
|
-
//
|
|
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 (
|
|
951
|
-
|
|
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 =
|
|
1285
|
+
startLine = cursorVisualLine - MAX_VISIBLE_LINES + 1;
|
|
956
1286
|
}
|
|
957
1287
|
}
|
|
958
|
-
const endLine = Math.min(startLine + MAX_VISIBLE_LINES,
|
|
959
|
-
|
|
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
|
-
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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 (
|
|
1304
|
+
if (lineText.length === 0) {
|
|
972
1305
|
return React.createElement(Text, { key: idx }, " ");
|
|
973
1306
|
}
|
|
974
|
-
return React.createElement(Text, { key: idx },
|
|
1307
|
+
return React.createElement(Text, { key: idx }, lineText);
|
|
975
1308
|
}
|
|
976
1309
|
// Render with selection and cursor
|
|
977
|
-
const chars =
|
|
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 <
|
|
1328
|
+
absPos < activeFileTagEnd;
|
|
987
1329
|
// Check if this character is part of a confirmed file tag
|
|
988
|
-
|
|
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 ===
|
|
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 <
|
|
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
|