centaurus-cli 2.6.2 → 2.7.1

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 (92) hide show
  1. package/dist/cli-adapter.d.ts +13 -22
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +383 -240
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/defaultConfig.d.ts +1 -0
  6. package/dist/config/defaultConfig.d.ts.map +1 -1
  7. package/dist/config/defaultConfig.js +3 -1
  8. package/dist/config/defaultConfig.js.map +1 -1
  9. package/dist/config/types.d.ts +1 -0
  10. package/dist/config/types.d.ts.map +1 -1
  11. package/dist/config/types.js +1 -0
  12. package/dist/config/types.js.map +1 -1
  13. package/dist/index.js +4 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/services/ai-service-client.d.ts +1 -1
  16. package/dist/services/ai-service-client.d.ts.map +1 -1
  17. package/dist/services/ai-service-client.js +7 -2
  18. package/dist/services/ai-service-client.js.map +1 -1
  19. package/dist/services/api-client.d.ts +6 -0
  20. package/dist/services/api-client.d.ts.map +1 -1
  21. package/dist/services/api-client.js +16 -0
  22. package/dist/services/api-client.js.map +1 -1
  23. package/dist/tools/command.d.ts.map +1 -1
  24. package/dist/tools/command.js +77 -25
  25. package/dist/tools/command.js.map +1 -1
  26. package/dist/tools/file-ops.d.ts.map +1 -1
  27. package/dist/tools/file-ops.js +28 -4
  28. package/dist/tools/file-ops.js.map +1 -1
  29. package/dist/tools/registry.d.ts +1 -0
  30. package/dist/tools/registry.d.ts.map +1 -1
  31. package/dist/tools/registry.js +25 -1
  32. package/dist/tools/registry.js.map +1 -1
  33. package/dist/tools/task-complete.d.ts +3 -0
  34. package/dist/tools/task-complete.d.ts.map +1 -0
  35. package/dist/tools/task-complete.js +48 -0
  36. package/dist/tools/task-complete.js.map +1 -0
  37. package/dist/tools/types.d.ts +1 -0
  38. package/dist/tools/types.d.ts.map +1 -1
  39. package/dist/tools/web-search.d.ts.map +1 -1
  40. package/dist/tools/web-search.js +16 -2
  41. package/dist/tools/web-search.js.map +1 -1
  42. package/dist/ui/components/AgentTimer.d.ts +7 -0
  43. package/dist/ui/components/AgentTimer.d.ts.map +1 -0
  44. package/dist/ui/components/AgentTimer.js +27 -0
  45. package/dist/ui/components/AgentTimer.js.map +1 -0
  46. package/dist/ui/components/App.d.ts +2 -0
  47. package/dist/ui/components/App.d.ts.map +1 -1
  48. package/dist/ui/components/App.js +229 -53
  49. package/dist/ui/components/App.js.map +1 -1
  50. package/dist/ui/components/InputBox.d.ts.map +1 -1
  51. package/dist/ui/components/InputBox.js +579 -130
  52. package/dist/ui/components/InputBox.js.map +1 -1
  53. package/dist/ui/components/InteractiveShell.d.ts +16 -0
  54. package/dist/ui/components/InteractiveShell.d.ts.map +1 -0
  55. package/dist/ui/components/InteractiveShell.js +152 -0
  56. package/dist/ui/components/InteractiveShell.js.map +1 -0
  57. package/dist/ui/components/LoadingIndicator.js +1 -1
  58. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  59. package/dist/ui/components/MarkdownRenderer.js +1 -1
  60. package/dist/ui/components/StreamingMessageDisplay.js +1 -1
  61. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  62. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  63. package/dist/ui/components/ToolExecutionMessage.js +43 -47
  64. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  65. package/dist/utils/ansi-encoder.d.ts +2 -0
  66. package/dist/utils/ansi-encoder.d.ts.map +1 -0
  67. package/dist/utils/ansi-encoder.js +57 -0
  68. package/dist/utils/ansi-encoder.js.map +1 -0
  69. package/dist/utils/command-history.d.ts +14 -0
  70. package/dist/utils/command-history.d.ts.map +1 -0
  71. package/dist/utils/command-history.js +140 -0
  72. package/dist/utils/command-history.js.map +1 -0
  73. package/dist/utils/input-classifier.d.ts +26 -0
  74. package/dist/utils/input-classifier.d.ts.map +1 -0
  75. package/dist/utils/input-classifier.js +154 -0
  76. package/dist/utils/input-classifier.js.map +1 -0
  77. package/dist/utils/markdown-parser.d.ts.map +1 -1
  78. package/dist/utils/markdown-parser.js +6 -5
  79. package/dist/utils/markdown-parser.js.map +1 -1
  80. package/dist/utils/shell.d.ts +7 -0
  81. package/dist/utils/shell.d.ts.map +1 -1
  82. package/dist/utils/shell.js +97 -37
  83. package/dist/utils/shell.js.map +1 -1
  84. package/models-config.json +30 -0
  85. package/package.json +2 -1
  86. package/prompts/system-prompt-autonomous.md +428 -0
  87. package/dist/tools/ToolRegistry.d.ts +0 -55
  88. package/dist/tools/ToolRegistry.d.ts.map +0 -1
  89. package/dist/tools/ToolRegistry.js +0 -600
  90. package/dist/tools/ToolRegistry.js.map +0 -1
  91. package/prompts/system-prompt-enhanced.md +0 -659
  92. package/prompts/system-prompt.md +0 -206
@@ -1,169 +1,540 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect, useRef, useMemo } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
- import TextInput from 'ink-text-input';
4
3
  import * as fs from 'fs';
5
4
  import * as path from 'path';
6
5
  import { Breadcrumbs } from './Breadcrumbs.js';
7
6
  import { ContextWindowIndicator } from './ContextWindowIndicator.js';
7
+ import { detectIntent } from '../../utils/input-classifier.js';
8
+ import { CommandHistoryManager } from '../../utils/command-history.js';
9
+ const getVisualLines = (text, width) => {
10
+ const logicalLines = text.split('\n');
11
+ const visualLines = [];
12
+ let currentOffset = 0;
13
+ logicalLines.forEach((line, i) => {
14
+ if (line.length === 0) {
15
+ visualLines.push({ start: currentOffset, end: currentOffset, isHardEnd: true });
16
+ currentOffset += 1;
17
+ return;
18
+ }
19
+ let remaining = line;
20
+ let lineStartOffset = currentOffset;
21
+ while (remaining.length > 0) {
22
+ let splitIndex = remaining.length;
23
+ let isHardEnd = false;
24
+ if (remaining.length > width) {
25
+ splitIndex = width;
26
+ const lastSpace = remaining.lastIndexOf(' ', width);
27
+ if (lastSpace > 0) {
28
+ splitIndex = lastSpace + 1;
29
+ }
30
+ }
31
+ else {
32
+ isHardEnd = true;
33
+ }
34
+ visualLines.push({
35
+ start: lineStartOffset,
36
+ end: lineStartOffset + splitIndex,
37
+ isHardEnd: isHardEnd
38
+ });
39
+ lineStartOffset += splitIndex;
40
+ remaining = remaining.slice(splitIndex);
41
+ }
42
+ currentOffset += line.length + (i < logicalLines.length - 1 ? 1 : 0);
43
+ });
44
+ return visualLines;
45
+ };
8
46
  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 }) => {
9
47
  const [value, setValue] = useState('');
48
+ const [cursorOffset, setCursorOffset] = useState(0);
10
49
  const [completions, setCompletions] = useState([]);
11
50
  const [completionIndex, setCompletionIndex] = useState(0);
12
- const [inputKey, setInputKey] = useState(0);
13
51
  const [historyIndex, setHistoryIndex] = useState(-1);
14
52
  const [tempValue, setTempValue] = useState('');
15
- const ignoreNextChangeRef = React.useRef(false);
53
+ const ignoreNextChangeRef = useRef(false);
54
+ // Auto Mode State
55
+ const [isAutoMode, setIsAutoMode] = useState(true);
56
+ const [detectedIntent, setDetectedIntent] = useState('ai');
57
+ // Autocomplete State
58
+ const [autocompleteSuggestion, setAutocompleteSuggestion] = useState(null);
59
+ // Undo/Redo State
60
+ const [undoStack, setUndoStack] = useState([]);
61
+ const [redoStack, setRedoStack] = useState([]);
62
+ // Selection State
63
+ const [selection, setSelection] = useState(null);
64
+ // Configuration for scrolling
65
+ const MAX_VISIBLE_LINES = 9;
66
+ // Load history on mount
67
+ useEffect(() => {
68
+ CommandHistoryManager.getInstance().load();
69
+ }, []);
16
70
  // Force clear value if it becomes empty or whitespace-only after external changes
17
- React.useEffect(() => {
18
- if (value && value.trim() === '') {
71
+ useEffect(() => {
72
+ if (value && value.trim() === '' && cursorOffset > 0) {
19
73
  setValue('');
20
- setInputKey(prev => prev + 1);
74
+ setCursorOffset(0);
21
75
  }
22
- }, [value]);
23
- // Determine current working directory - use prop if provided, otherwise process.cwd()
24
- const currentDir = React.useMemo(() => {
76
+ }, [value, cursorOffset]);
77
+ // Determine current working directory
78
+ const currentDir = useMemo(() => {
25
79
  const cwd = currentWorkingDirectory || process.cwd();
26
- // Truncate long paths to prevent layout shifts
27
80
  if (cwd.length > 60) {
28
81
  return '...' + cwd.slice(-57);
29
82
  }
30
83
  return cwd;
31
84
  }, [currentWorkingDirectory]);
85
+ // Autocomplete Logic
86
+ useEffect(() => {
87
+ if (!value || value.trim() === '') {
88
+ setAutocompleteSuggestion(null);
89
+ return;
90
+ }
91
+ // Only show suggestions if we are in command mode (manual or auto-detected)
92
+ // OR if we are in Auto mode and it looks like a command
93
+ const shouldSuggest = commandMode || (isAutoMode && detectedIntent === 'command');
94
+ if (shouldSuggest) {
95
+ const matches = CommandHistoryManager.getInstance().getMatches(value, currentDir);
96
+ if (matches.length > 0) {
97
+ setAutocompleteSuggestion(matches[0]);
98
+ }
99
+ else {
100
+ setAutocompleteSuggestion(null);
101
+ }
102
+ }
103
+ else {
104
+ setAutocompleteSuggestion(null);
105
+ }
106
+ }, [value, commandMode, isAutoMode, detectedIntent, currentDir]);
107
+ // Auto-classification effect (Synchronous Heuristics Only)
108
+ useEffect(() => {
109
+ // Only run classification if in Auto Mode
110
+ if (!isAutoMode) {
111
+ return;
112
+ }
113
+ // Only classify if value is non-empty and not just whitespace
114
+ const trimmedValue = value.trim();
115
+ if (!trimmedValue || !onToggleCommandMode) {
116
+ // Default to AI if empty
117
+ setDetectedIntent('ai');
118
+ if (commandMode)
119
+ onToggleCommandMode();
120
+ return;
121
+ }
122
+ // Run local heuristic detection immediately
123
+ const intent = detectIntent(trimmedValue);
124
+ setDetectedIntent(intent);
125
+ // Switch mode based on intent
126
+ if (intent === 'command') {
127
+ if (!commandMode)
128
+ onToggleCommandMode();
129
+ }
130
+ else {
131
+ // intent === 'ai'
132
+ if (commandMode)
133
+ onToggleCommandMode();
134
+ }
135
+ }, [value, commandMode, onToggleCommandMode, isAutoMode]);
136
+ const pushToUndoStack = () => {
137
+ setUndoStack(prev => [...prev, { value, cursorOffset }]);
138
+ setRedoStack([]); // Clear redo stack on new action
139
+ };
140
+ const handleUndo = () => {
141
+ if (undoStack.length === 0)
142
+ return;
143
+ const previousState = undoStack[undoStack.length - 1];
144
+ setRedoStack(prev => [...prev, { value, cursorOffset }]);
145
+ setUndoStack(prev => prev.slice(0, -1));
146
+ setValue(previousState.value);
147
+ setCursorOffset(previousState.cursorOffset);
148
+ setSelection(null);
149
+ };
32
150
  useInput((input, key) => {
33
- // Ctrl+T to toggle auto-accept - handle FIRST to prevent character from appearing
151
+ if (!isActive)
152
+ return;
153
+ // Detect OS for platform-specific key handling
154
+ const isWindows = process.platform === 'win32';
155
+ const inputCharCode = input ? input.charCodeAt(0) : null;
156
+ // DELETE WORD BACKWARDS - Check this FIRST before standard backspace/delete
157
+ // Triggers on any of these conditions:
158
+ // 1. Ctrl+W (char code 23) - Standard Unix terminal shortcut
159
+ // 2. Meta/Alt + Backspace/Delete - Mac alternative
160
+ // 3. Ctrl + Delete - Works on all platforms (Ctrl+Backspace doesn't work on Windows/Ink)
161
+ // 4. Windows Ctrl+Del sends char code 127
162
+ const isDeleteWord = inputCharCode === 23 || // Ctrl+W
163
+ (key.meta && (key.backspace || key.delete)) || // Meta/Alt + Backspace/Delete
164
+ (key.ctrl && key.delete) || // Ctrl+Delete (NOTE: Ctrl+Backspace not detectable on Windows/Ink)
165
+ (isWindows && inputCharCode === 127); // Windows: Ctrl+Del sends char 127
166
+ if (isDeleteWord) {
167
+ pushToUndoStack();
168
+ if (cursorOffset > 0) {
169
+ let newOffset = cursorOffset;
170
+ // Skip whitespace backwards
171
+ while (newOffset > 0 && /\s/.test(value[newOffset - 1])) {
172
+ newOffset--;
173
+ }
174
+ // Skip non-whitespace backwards
175
+ while (newOffset > 0 && !/\s/.test(value[newOffset - 1])) {
176
+ newOffset--;
177
+ }
178
+ const newValue = value.slice(0, newOffset) + value.slice(cursorOffset);
179
+ setValue(newValue);
180
+ setCursorOffset(newOffset);
181
+ }
182
+ setHistoryIndex(-1);
183
+ setCompletions([]);
184
+ return;
185
+ }
186
+ // Ctrl+T: Toggle auto-accept
34
187
  if (key.ctrl && input.toLowerCase() === 't') {
35
188
  ignoreNextChangeRef.current = true;
36
189
  onToggleAutoAccept();
37
- // Clear the flag after a short delay in case the change doesn't come
38
- setTimeout(() => {
39
- ignoreNextChangeRef.current = false;
40
- }, 100);
41
- return; // Return immediately to prevent any further processing
190
+ setTimeout(() => { ignoreNextChangeRef.current = false; }, 100);
191
+ return;
42
192
  }
43
- // Ctrl+D to toggle command mode - handle SECOND
193
+ // Ctrl+D: Cycle modes (Agent -> Terminal -> Auto -> Agent)
44
194
  if (key.ctrl && input.toLowerCase() === 'd') {
45
195
  if (onToggleCommandMode) {
46
196
  ignoreNextChangeRef.current = true;
47
- onToggleCommandMode();
48
- // Clear the flag after a short delay
49
- setTimeout(() => {
50
- ignoreNextChangeRef.current = false;
51
- }, 100);
52
- // Only remount if input is empty to prevent losing typed content
53
- if (!value || value.trim() === '') {
54
- setTimeout(() => {
55
- setValue('');
56
- setInputKey(prev => prev + 1);
57
- }, 10);
197
+ // Cycle Logic
198
+ if (!isAutoMode && !commandMode) {
199
+ // Agent -> Terminal
200
+ onToggleCommandMode();
201
+ }
202
+ else if (!isAutoMode && commandMode) {
203
+ // Terminal -> Auto
204
+ setIsAutoMode(true);
205
+ // Trigger initial detection for Auto mode
206
+ const intent = detectIntent(value);
207
+ setDetectedIntent(intent);
208
+ // Set command mode based on intent
209
+ if (intent === 'ai' && commandMode)
210
+ onToggleCommandMode();
211
+ // if intent is command, we are already in command mode
58
212
  }
213
+ else if (isAutoMode) {
214
+ // Auto -> Agent
215
+ setIsAutoMode(false);
216
+ // Ensure we go back to Agent mode (commandMode = false)
217
+ if (commandMode)
218
+ onToggleCommandMode();
219
+ }
220
+ setTimeout(() => { ignoreNextChangeRef.current = false; }, 100);
59
221
  }
60
- return; // Return immediately to prevent any further processing
222
+ return;
61
223
  }
62
- // Up arrow - navigate to previous command in history
63
- if (key.upArrow && commandHistory.length > 0) {
64
- if (historyIndex === -1) {
65
- // Save current input before navigating history
66
- setTempValue(value);
67
- // Go to most recent command
68
- setHistoryIndex(commandHistory.length - 1);
69
- setValue(commandHistory[commandHistory.length - 1]);
70
- setInputKey(prev => prev + 1);
224
+ // Ctrl+Z: Undo
225
+ if (key.ctrl && input.toLowerCase() === 'z') {
226
+ handleUndo();
227
+ return;
228
+ }
229
+ // Ctrl+A: Select All
230
+ if (key.ctrl && input.toLowerCase() === 'a') {
231
+ setSelection({ start: 0, end: value.length });
232
+ setCursorOffset(value.length);
233
+ return;
234
+ }
235
+ // DELETE CHAR - Only runs if Delete Word did NOT trigger
236
+ // Triggers on:
237
+ // 1. Backspace or Delete key flag is present
238
+ // 2. OR char code 8 (standard backspace)
239
+ // 3. OR char code 127 (DEL) on non-Windows (Mac/Linux treat 127 as normal backspace)
240
+ const isDeleteChar = key.backspace ||
241
+ key.delete ||
242
+ inputCharCode === 8 ||
243
+ (!isWindows && inputCharCode === 127);
244
+ if (isDeleteChar) {
245
+ pushToUndoStack();
246
+ if (selection) {
247
+ // Delete selection
248
+ const start = Math.min(selection.start, selection.end);
249
+ const end = Math.max(selection.start, selection.end);
250
+ const newValue = value.slice(0, start) + value.slice(end);
251
+ setValue(newValue);
252
+ setCursorOffset(start);
253
+ setSelection(null);
71
254
  }
72
- else if (historyIndex > 0) {
73
- // Go to older command
74
- setHistoryIndex(historyIndex - 1);
75
- setValue(commandHistory[historyIndex - 1]);
76
- setInputKey(prev => prev + 1);
255
+ else if (key.delete && cursorOffset < value.length) {
256
+ // Delete key: Delete character at cursor (forward delete)
257
+ const newValue = value.slice(0, cursorOffset) + value.slice(cursorOffset + 1);
258
+ setValue(newValue);
77
259
  }
260
+ else if (cursorOffset > 0) {
261
+ // Backspace: Delete character before cursor
262
+ const newValue = value.slice(0, cursorOffset - 1) + value.slice(cursorOffset);
263
+ setValue(newValue);
264
+ setCursorOffset(cursorOffset - 1);
265
+ }
266
+ // Reset history/completions on edit
267
+ setHistoryIndex(-1);
268
+ setCompletions([]);
78
269
  return;
79
270
  }
80
- // Down arrow - navigate to next command in history
81
- if (key.downArrow && historyIndex !== -1) {
82
- if (historyIndex < commandHistory.length - 1) {
83
- // Go to newer command
84
- setHistoryIndex(historyIndex + 1);
85
- setValue(commandHistory[historyIndex + 1]);
86
- setInputKey(prev => prev + 1);
271
+ // Handle Enter
272
+ // Check input length to distinguish single key press from paste
273
+ if (key.return && input.length <= 1) {
274
+ // Check for Shift+Enter, Ctrl+Enter, OR explicit newline char
275
+ // Note: Shift+Enter doesn't work on Windows/Ink, so Ctrl+Enter is the alternative
276
+ // Alt+Enter can't be used as it fullscreens the terminal on Windows
277
+ if (key.shift || key.ctrl || input === '\n') {
278
+ pushToUndoStack();
279
+ // Shift+Enter or Ctrl+Enter: Insert newline
280
+ // If selection exists, replace it
281
+ let newValue = value;
282
+ let newOffset = cursorOffset;
283
+ if (selection) {
284
+ const start = Math.min(selection.start, selection.end);
285
+ const end = Math.max(selection.start, selection.end);
286
+ newValue = value.slice(0, start) + '\n' + value.slice(end);
287
+ newOffset = start + 1;
288
+ setSelection(null);
289
+ }
290
+ else {
291
+ newValue = value.slice(0, cursorOffset) + '\n' + value.slice(cursorOffset);
292
+ newOffset = cursorOffset + 1;
293
+ }
294
+ setValue(newValue);
295
+ setCursorOffset(newOffset);
87
296
  }
88
297
  else {
89
- // Return to the input that was being typed
90
- setHistoryIndex(-1);
91
- setValue(tempValue);
92
- setInputKey(prev => prev + 1);
298
+ // Enter: Submit
299
+ handleSubmit();
93
300
  }
94
301
  return;
95
302
  }
96
- // Tab for autocomplete in command mode
97
- if (key.tab && !key.shift && commandMode) {
98
- handleTabCompletion();
303
+ // Handle Arrows
304
+ if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
305
+ // Clear selection on arrow keys
306
+ if (selection) {
307
+ setSelection(null);
308
+ // If left/right, maybe move cursor to start/end of selection?
309
+ // For now, just clear selection and let default logic run (or reset cursor to one end)
310
+ if (key.leftArrow)
311
+ setCursorOffset(Math.min(selection.start, selection.end));
312
+ if (key.rightArrow)
313
+ setCursorOffset(Math.max(selection.start, selection.end));
314
+ if (key.upArrow || key.downArrow) {
315
+ // Keep cursor where it is (at the end usually) or move it?
316
+ // Let's just fall through to normal arrow logic from current cursorOffset
317
+ }
318
+ if (key.leftArrow || key.rightArrow)
319
+ return; // We handled the move
320
+ }
321
+ }
322
+ if (key.upArrow) {
323
+ const width = (process.stdout.columns || 80) - 6;
324
+ const visualLines = getVisualLines(value, width);
325
+ let currentVisualLineIndex = visualLines.findIndex(line => {
326
+ if (cursorOffset >= line.start && cursorOffset < line.end)
327
+ return true;
328
+ if (cursorOffset === line.end && line.isHardEnd)
329
+ return true;
330
+ return false;
331
+ });
332
+ // If cursor is at the very end of the last line
333
+ if (currentVisualLineIndex === -1 && cursorOffset === value.length) {
334
+ currentVisualLineIndex = visualLines.length - 1;
335
+ }
336
+ if (currentVisualLineIndex <= 0) {
337
+ // Top line: History navigation
338
+ if (commandHistory.length > 0) {
339
+ if (historyIndex === -1) {
340
+ setTempValue(value);
341
+ const newIndex = commandHistory.length - 1;
342
+ setHistoryIndex(newIndex);
343
+ const newValue = commandHistory[newIndex];
344
+ setValue(newValue);
345
+ setCursorOffset(newValue.length);
346
+ }
347
+ else if (historyIndex > 0) {
348
+ const newIndex = historyIndex - 1;
349
+ setHistoryIndex(newIndex);
350
+ const newValue = commandHistory[newIndex];
351
+ setValue(newValue);
352
+ setCursorOffset(newValue.length);
353
+ }
354
+ }
355
+ }
356
+ else {
357
+ // Move cursor up one visual line
358
+ const currentLine = visualLines[currentVisualLineIndex];
359
+ const targetLine = visualLines[currentVisualLineIndex - 1];
360
+ const offsetInLine = cursorOffset - currentLine.start;
361
+ const newOffset = targetLine.start + Math.min(offsetInLine, targetLine.end - targetLine.start);
362
+ setCursorOffset(newOffset);
363
+ }
99
364
  return;
100
365
  }
101
- // Detect paste events - when input contains newlines or is very long
102
- if (input && (input.includes('\n') || input.includes('\r') || input.length > 10)) {
103
- // This is likely a paste event with multi-line content
104
- // Preserve the newlines by appending to current value
105
- const cleanedInput = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
106
- setValue(prev => prev + cleanedInput);
366
+ if (key.downArrow) {
367
+ const width = (process.stdout.columns || 80) - 6;
368
+ const visualLines = getVisualLines(value, width);
369
+ let currentVisualLineIndex = visualLines.findIndex(line => {
370
+ if (cursorOffset >= line.start && cursorOffset < line.end)
371
+ return true;
372
+ if (cursorOffset === line.end && line.isHardEnd)
373
+ return true;
374
+ return false;
375
+ });
376
+ // If cursor is at the very end of the last line
377
+ if (currentVisualLineIndex === -1 && cursorOffset === value.length) {
378
+ currentVisualLineIndex = visualLines.length - 1;
379
+ }
380
+ if (currentVisualLineIndex === visualLines.length - 1) {
381
+ // Bottom line: History navigation
382
+ if (historyIndex !== -1) {
383
+ if (historyIndex < commandHistory.length - 1) {
384
+ const newIndex = historyIndex + 1;
385
+ setHistoryIndex(newIndex);
386
+ const newValue = commandHistory[newIndex];
387
+ setValue(newValue);
388
+ setCursorOffset(newValue.length);
389
+ }
390
+ else {
391
+ setHistoryIndex(-1);
392
+ setValue(tempValue);
393
+ setCursorOffset(tempValue.length);
394
+ }
395
+ }
396
+ }
397
+ else {
398
+ // Move cursor down one visual line
399
+ const currentLine = visualLines[currentVisualLineIndex];
400
+ const targetLine = visualLines[currentVisualLineIndex + 1];
401
+ const offsetInLine = cursorOffset - currentLine.start;
402
+ const newOffset = targetLine.start + Math.min(offsetInLine, targetLine.end - targetLine.start);
403
+ setCursorOffset(newOffset);
404
+ }
107
405
  return;
108
406
  }
109
- }, { isActive });
110
- const handleChange = (newValue) => {
111
- // Don't accept changes when input is inactive (during approvals)
112
- if (!isActive) {
407
+ if (key.leftArrow) {
408
+ if (key.ctrl) {
409
+ // Ctrl+Left: Move word backwards
410
+ let newOffset = cursorOffset;
411
+ if (newOffset > 0) {
412
+ // Skip whitespace backwards
413
+ while (newOffset > 0 && /\s/.test(value[newOffset - 1])) {
414
+ newOffset--;
415
+ }
416
+ // Skip non-whitespace backwards
417
+ while (newOffset > 0 && !/\s/.test(value[newOffset - 1])) {
418
+ newOffset--;
419
+ }
420
+ setCursorOffset(newOffset);
421
+ }
422
+ }
423
+ else if (cursorOffset > 0) {
424
+ setCursorOffset(cursorOffset - 1);
425
+ }
113
426
  return;
114
427
  }
115
- // Ignore changes that come from Ctrl+T or Ctrl+D shortcuts
116
- // These add unwanted 't' or 'd' characters
117
- if (ignoreNextChangeRef.current) {
118
- ignoreNextChangeRef.current = false;
428
+ if (key.rightArrow) {
429
+ // Autocomplete Logic (Only at end of line)
430
+ if (autocompleteSuggestion && cursorOffset === value.length) {
431
+ if (key.ctrl) {
432
+ // Ctrl+Right: Accept FULL suggestion
433
+ setValue(autocompleteSuggestion);
434
+ setCursorOffset(autocompleteSuggestion.length);
435
+ setAutocompleteSuggestion(null);
436
+ return;
437
+ }
438
+ else {
439
+ // Right: Accept NEXT WORD
440
+ const remaining = autocompleteSuggestion.slice(value.length);
441
+ // Match next chunk of non-whitespace (including preceding whitespace)
442
+ const match = remaining.match(/^(\s*\S+)/);
443
+ if (match) {
444
+ const toAdd = match[0];
445
+ const newValue = value + toAdd;
446
+ setValue(newValue);
447
+ setCursorOffset(newValue.length);
448
+ return;
449
+ }
450
+ else if (remaining.length > 0) {
451
+ // Fallback: if only whitespace remains or regex fails, take it all
452
+ const newValue = value + remaining;
453
+ setValue(newValue);
454
+ setCursorOffset(newValue.length);
455
+ return;
456
+ }
457
+ }
458
+ }
459
+ // Navigation Logic (if not completing)
460
+ if (key.ctrl) {
461
+ // Ctrl+Right: Move word forwards
462
+ let newOffset = cursorOffset;
463
+ if (newOffset < value.length) {
464
+ // Skip non-whitespace forwards
465
+ while (newOffset < value.length && !/\s/.test(value[newOffset])) {
466
+ newOffset++;
467
+ }
468
+ // Skip whitespace forwards
469
+ while (newOffset < value.length && /\s/.test(value[newOffset])) {
470
+ newOffset++;
471
+ }
472
+ setCursorOffset(newOffset);
473
+ }
474
+ }
475
+ else if (cursorOffset < value.length) {
476
+ setCursorOffset(cursorOffset + 1);
477
+ }
119
478
  return;
120
479
  }
121
- // Reset completions when user types
122
- if (newValue !== value) {
123
- setCompletions([]);
124
- setCompletionIndex(0);
480
+ // Tab Completion
481
+ if (key.tab && !key.shift) {
482
+ // Only file completion (only in command mode)
483
+ if (commandMode) {
484
+ handleTabCompletion();
485
+ return;
486
+ }
125
487
  }
126
- // Reset history navigation when user types
127
- if (historyIndex !== -1) {
488
+ // Regular Input
489
+ // Ignore control keys to prevent printing garbage (like 'v' for Ctrl+V)
490
+ if (input && !key.ctrl && !key.meta) {
491
+ pushToUndoStack();
492
+ // Handle paste with newlines
493
+ const cleanedInput = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
494
+ let newValue = value;
495
+ let newOffset = cursorOffset;
496
+ if (selection) {
497
+ const start = Math.min(selection.start, selection.end);
498
+ const end = Math.max(selection.start, selection.end);
499
+ newValue = value.slice(0, start) + cleanedInput + value.slice(end);
500
+ newOffset = start + cleanedInput.length;
501
+ setSelection(null);
502
+ }
503
+ else {
504
+ newValue = value.slice(0, cursorOffset) + cleanedInput + value.slice(cursorOffset);
505
+ newOffset = cursorOffset + cleanedInput.length;
506
+ }
507
+ setValue(newValue);
508
+ setCursorOffset(newOffset);
509
+ // Reset history/completions
128
510
  setHistoryIndex(-1);
129
- setTempValue('');
511
+ setCompletions([]);
130
512
  }
131
- // Handle multi-line paste - preserve newlines
132
- // ink-text-input doesn't handle newlines well, so we keep them in the value
133
- setValue(newValue);
134
- };
513
+ }, { isActive });
135
514
  const handleTabCompletion = async () => {
136
515
  if (!value)
137
516
  return;
138
- // Get the last word (path) from the command
139
517
  const words = value.split(' ');
140
518
  const lastWord = words[words.length - 1];
141
- // If we have existing completions, cycle through them
142
519
  if (completions.length > 0) {
143
520
  const nextIndex = (completionIndex + 1) % completions.length;
144
521
  setCompletionIndex(nextIndex);
145
- // Replace the last word with the next completion
146
522
  words[words.length - 1] = completions[nextIndex];
147
523
  const newValue = words.join(' ');
148
- // Force remount to reset cursor position
149
524
  setValue(newValue);
150
- setInputKey(prev => prev + 1);
525
+ setCursorOffset(newValue.length);
151
526
  return;
152
527
  }
153
- // Get completions for the current path
154
528
  try {
155
529
  const cwd = currentWorkingDirectory || process.cwd();
156
530
  let searchDir = cwd;
157
531
  let searchPattern = lastWord;
158
532
  let dirPart = '';
159
- // If the path contains a directory separator, split it
160
533
  if (lastWord.includes('/') || lastWord.includes('\\')) {
161
534
  const lastSep = Math.max(lastWord.lastIndexOf('/'), lastWord.lastIndexOf('\\'));
162
535
  dirPart = lastWord.substring(0, lastSep + 1);
163
536
  searchPattern = lastWord.substring(lastSep + 1);
164
- // For subshells, use their path format
165
537
  if (subshellContext && subshellContext.type !== 'local') {
166
- // Use forward slashes for Unix-like paths
167
538
  searchDir = dirPart.startsWith('/') ? dirPart.slice(0, -1) : cwd + '/' + dirPart.slice(0, -1);
168
539
  }
169
540
  else {
@@ -171,27 +542,19 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
171
542
  }
172
543
  }
173
544
  let entries;
174
- // Check if we're in a subshell context
175
545
  if (subshellContext && subshellContext.type !== 'local' && subshellContext.handler) {
176
- // Use the subshell handler to list directory
177
546
  const dirEntries = await subshellContext.handler.listDirectory(searchDir);
178
- entries = dirEntries.map(entry => ({
179
- name: entry.name,
180
- type: entry.type
181
- }));
547
+ entries = dirEntries.map(entry => ({ name: entry.name, type: entry.type }));
182
548
  }
183
549
  else {
184
- // Local filesystem - use fs
185
- if (!fs.existsSync(searchDir)) {
550
+ if (!fs.existsSync(searchDir))
186
551
  return;
187
- }
188
552
  const fsEntries = fs.readdirSync(searchDir, { withFileTypes: true });
189
553
  entries = fsEntries.map(entry => ({
190
554
  name: entry.name,
191
555
  type: entry.isDirectory() ? 'directory' : 'file'
192
556
  }));
193
557
  }
194
- // Filter matches
195
558
  const matches = entries
196
559
  .filter(entry => entry.name.toLowerCase().startsWith(searchPattern.toLowerCase()))
197
560
  .map(entry => {
@@ -200,48 +563,132 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
200
563
  return entry.type === 'directory' ? fullPath + separator : fullPath;
201
564
  })
202
565
  .sort();
203
- if (matches.length === 1) {
204
- // Single match - complete it
205
- words[words.length - 1] = matches[0];
206
- const newValue = words.join(' ');
207
- // Force remount to reset cursor position
208
- setValue(newValue);
209
- setInputKey(prev => prev + 1);
210
- setCompletions([]);
211
- }
212
- else if (matches.length > 1) {
213
- // Multiple matches - set up for cycling
214
- setCompletions(matches);
215
- setCompletionIndex(0);
566
+ if (matches.length > 0) {
567
+ if (matches.length > 1) {
568
+ setCompletions(matches);
569
+ setCompletionIndex(0);
570
+ }
216
571
  words[words.length - 1] = matches[0];
217
572
  const newValue = words.join(' ');
218
- // Force remount to reset cursor position
219
573
  setValue(newValue);
220
- setInputKey(prev => prev + 1);
574
+ setCursorOffset(newValue.length);
221
575
  }
222
576
  }
223
577
  catch (error) {
224
- // Ignore errors in tab completion
578
+ // Ignore errors
225
579
  }
226
580
  };
227
581
  const handleSubmit = () => {
228
- // Don't submit when input is inactive (during approvals)
229
- if (!isActive) {
582
+ if (!isActive)
230
583
  return;
231
- }
232
584
  const trimmedValue = value.trim();
233
585
  if (trimmedValue) {
586
+ // Save to history if it was a command
587
+ if (commandMode) {
588
+ CommandHistoryManager.getInstance().addCommand(trimmedValue, currentDir);
589
+ }
234
590
  onSubmit(trimmedValue);
235
591
  setValue('');
592
+ setCursorOffset(0);
236
593
  setCompletions([]);
237
594
  setCompletionIndex(0);
238
595
  setHistoryIndex(-1);
239
596
  setTempValue('');
240
- setInputKey(prev => prev + 1); // Force remount to clear any residual state
597
+ setUndoStack([]);
598
+ setRedoStack([]);
599
+ setSelection(null);
600
+ setAutocompleteSuggestion(null);
601
+ }
602
+ };
603
+ // Rendering Logic with Scrolling
604
+ const renderInput = () => {
605
+ const lines = value.split('\n');
606
+ // If empty, show placeholder
607
+ if (lines.length === 1 && lines[0] === '') {
608
+ return React.createElement(Text, { color: "gray" }, placeholder);
609
+ }
610
+ // Calculate cursor line and column
611
+ let currentPos = 0;
612
+ let cursorLine = 0;
613
+ let cursorCol = 0;
614
+ for (let i = 0; i < lines.length; i++) {
615
+ if (currentPos + lines[i].length >= cursorOffset) {
616
+ cursorLine = i;
617
+ cursorCol = cursorOffset - currentPos;
618
+ break;
619
+ }
620
+ currentPos += lines[i].length + 1; // +1 for newline
621
+ }
622
+ // Edge case: cursor at very end of content
623
+ if (cursorOffset === value.length && lines.length > 0) {
624
+ cursorLine = lines.length - 1;
625
+ cursorCol = lines[lines.length - 1].length;
626
+ }
627
+ // Calculate visible range
628
+ let startLine = 0;
629
+ if (lines.length > MAX_VISIBLE_LINES) {
630
+ if (cursorLine < MAX_VISIBLE_LINES) {
631
+ startLine = 0;
632
+ }
633
+ else {
634
+ startLine = cursorLine - MAX_VISIBLE_LINES + 1;
635
+ }
241
636
  }
637
+ const endLine = Math.min(startLine + MAX_VISIBLE_LINES, lines.length);
638
+ const visibleLines = lines.slice(startLine, endLine);
639
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
640
+ startLine > 0 && React.createElement(Text, { color: "gray" }, "\u2191 ..."),
641
+ visibleLines.map((line, idx) => {
642
+ const actualLineIndex = startLine + idx;
643
+ const isCursorLine = actualLineIndex === cursorLine;
644
+ const isLastLine = actualLineIndex === lines.length - 1;
645
+ // Calculate absolute position of this line start
646
+ let lineStartPos = 0;
647
+ for (let k = 0; k < actualLineIndex; k++)
648
+ lineStartPos += lines[k].length + 1;
649
+ if (!isActive) {
650
+ if (line.length === 0) {
651
+ return React.createElement(Text, { key: idx }, " ");
652
+ }
653
+ return React.createElement(Text, { key: idx }, line);
654
+ }
655
+ // Render with selection and cursor
656
+ const chars = line.split('');
657
+ const renderedChars = chars.map((char, charIdx) => {
658
+ const absPos = lineStartPos + charIdx;
659
+ const isSelected = selection &&
660
+ absPos >= Math.min(selection.start, selection.end) &&
661
+ absPos < Math.max(selection.start, selection.end);
662
+ const isCursor = isCursorLine && charIdx === cursorCol;
663
+ if (isCursor) {
664
+ return React.createElement(Text, { key: charIdx, inverse: true, color: isSelected ? "yellow" : undefined }, char);
665
+ }
666
+ if (isSelected) {
667
+ return React.createElement(Text, { key: charIdx, backgroundColor: "white", color: "black" }, char);
668
+ }
669
+ return React.createElement(Text, { key: charIdx }, char);
670
+ });
671
+ // Handle cursor at end of line
672
+ if (isCursorLine && cursorCol === line.length) {
673
+ renderedChars.push(React.createElement(Text, { key: "cursor", inverse: true }, " "));
674
+ }
675
+ // Render Autocomplete Ghost Text
676
+ // Only on the last line, if suggestion exists and matches start
677
+ if (isLastLine && autocompleteSuggestion && autocompleteSuggestion.startsWith(value)) {
678
+ const suffix = autocompleteSuggestion.slice(value.length);
679
+ if (suffix) {
680
+ renderedChars.push(React.createElement(Text, { key: "ghost", color: "gray" }, suffix));
681
+ }
682
+ }
683
+ if (renderedChars.length === 0) {
684
+ return React.createElement(Text, { key: idx }, " ");
685
+ }
686
+ return React.createElement(Text, { key: idx }, renderedChars);
687
+ }),
688
+ endLine < lines.length && React.createElement(Text, { color: "gray" }, "\u2193 ...")));
242
689
  };
243
- return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#003b59", paddingX: 1, paddingY: 0 },
244
- React.createElement(Box, { marginY: 1, justifyContent: "space-between" },
690
+ return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: commandMode ? "#00cc66" : "#257aa5ff", paddingX: 1, paddingY: 0, width: "100%" },
691
+ React.createElement(Box, { marginY: 1, justifyContent: "space-between", width: "100%" },
245
692
  React.createElement(Box, null,
246
693
  subshellContext && subshellContext.type !== 'local' && (React.createElement(Breadcrumbs, { context: subshellContext })),
247
694
  React.createElement(Text, { color: "#666666" }, "CWD: "),
@@ -252,13 +699,15 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
252
699
  React.createElement(Text, { color: "#00ccff" }, model))),
253
700
  React.createElement(Box, null,
254
701
  React.createElement(Text, { color: "#666666" }, "Mode: "),
255
- commandMode ? (React.createElement(Text, { color: "#00cc66", bold: true }, "Command")) : planMode ? (React.createElement(Text, { color: "#ffaa00", bold: true }, "Plan")) : (React.createElement(Text, { color: "#00ccff" }, "Execution"))))),
256
- React.createElement(Box, null,
702
+ isAutoMode ? (React.createElement(Text, null,
703
+ React.createElement(Text, { color: "#00ccff", bold: true }, "Auto ["),
704
+ detectedIntent === 'command' ? (React.createElement(Text, { color: "#00cc66", bold: true }, "Terminal")) : (React.createElement(Text, { color: "#00ccff" }, "Agent")),
705
+ React.createElement(Text, { color: "#00ccff", bold: true }, "]"))) : commandMode ? (React.createElement(Text, { color: "#00cc66", bold: true }, "Terminal")) : planMode ? (React.createElement(Text, { color: "#ffaa00", bold: true }, "Plan")) : (React.createElement(Text, { color: "#00ccff" }, "Agent"))))),
706
+ React.createElement(Box, { flexDirection: "row", width: "100%" },
257
707
  React.createElement(Text, { color: "#666666" }, "> "),
258
- React.createElement(Text, { color: "white", bold: false },
259
- React.createElement(TextInput, { key: inputKey, value: value, onChange: handleChange, onSubmit: handleSubmit, placeholder: placeholder, showCursor: isActive }))),
260
- React.createElement(Box, { marginY: 1, justifyContent: "space-between" },
261
- React.createElement(Text, { color: "#666666", dimColor: true }, commandMode ? ('Ctrl+D to exit command mode • Tab for autocomplete') : ('Ctrl+D for command mode • Ctrl+T to toggle auto-accept')),
708
+ renderInput()),
709
+ React.createElement(Box, { marginY: 1, justifyContent: "space-between", width: "100%" },
710
+ React.createElement(Text, { color: "#666666", dimColor: true }, isAutoMode ? ('Ctrl+D to cycle modes • Auto-detecting intent') : commandMode ? ('Ctrl+D to cycle modes • Tab for autocomplete') : ('Ctrl+D to cycle modes • Shift+Enter for new line')),
262
711
  React.createElement(Box, { gap: 1 },
263
712
  !commandMode && autoAcceptMode ? (React.createElement(Text, { color: "#00cc66", bold: true }, "[AUTO-ACCEPT: ON]")) : !commandMode ? (React.createElement(Text, { color: "#666666", dimColor: true }, "[AUTO-ACCEPT: OFF]")) : null,
264
713
  !commandMode && (React.createElement(Box, { marginLeft: 1 },