centaurus-cli 2.6.2 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 -46
  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 +586 -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,547 @@
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();
58
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
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;
223
+ }
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;
61
234
  }
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);
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);
254
+ }
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);
71
259
  }
72
- else if (historyIndex > 0) {
73
- // Go to older command
74
- setHistoryIndex(historyIndex - 1);
75
- setValue(commandHistory[historyIndex - 1]);
76
- setInputKey(prev => prev + 1);
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);
77
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
+ // Priority 1: Accept autocomplete suggestion
483
+ if (autocompleteSuggestion) {
484
+ setValue(autocompleteSuggestion);
485
+ setCursorOffset(autocompleteSuggestion.length);
486
+ setAutocompleteSuggestion(null);
487
+ return;
488
+ }
489
+ // Priority 2: File completion (only in command mode)
490
+ if (commandMode) {
491
+ handleTabCompletion();
492
+ return;
493
+ }
125
494
  }
126
- // Reset history navigation when user types
127
- if (historyIndex !== -1) {
495
+ // Regular Input
496
+ // Ignore control keys to prevent printing garbage (like 'v' for Ctrl+V)
497
+ if (input && !key.ctrl && !key.meta) {
498
+ pushToUndoStack();
499
+ // Handle paste with newlines
500
+ const cleanedInput = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
501
+ let newValue = value;
502
+ let newOffset = cursorOffset;
503
+ if (selection) {
504
+ const start = Math.min(selection.start, selection.end);
505
+ const end = Math.max(selection.start, selection.end);
506
+ newValue = value.slice(0, start) + cleanedInput + value.slice(end);
507
+ newOffset = start + cleanedInput.length;
508
+ setSelection(null);
509
+ }
510
+ else {
511
+ newValue = value.slice(0, cursorOffset) + cleanedInput + value.slice(cursorOffset);
512
+ newOffset = cursorOffset + cleanedInput.length;
513
+ }
514
+ setValue(newValue);
515
+ setCursorOffset(newOffset);
516
+ // Reset history/completions
128
517
  setHistoryIndex(-1);
129
- setTempValue('');
518
+ setCompletions([]);
130
519
  }
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
- };
520
+ }, { isActive });
135
521
  const handleTabCompletion = async () => {
136
522
  if (!value)
137
523
  return;
138
- // Get the last word (path) from the command
139
524
  const words = value.split(' ');
140
525
  const lastWord = words[words.length - 1];
141
- // If we have existing completions, cycle through them
142
526
  if (completions.length > 0) {
143
527
  const nextIndex = (completionIndex + 1) % completions.length;
144
528
  setCompletionIndex(nextIndex);
145
- // Replace the last word with the next completion
146
529
  words[words.length - 1] = completions[nextIndex];
147
530
  const newValue = words.join(' ');
148
- // Force remount to reset cursor position
149
531
  setValue(newValue);
150
- setInputKey(prev => prev + 1);
532
+ setCursorOffset(newValue.length);
151
533
  return;
152
534
  }
153
- // Get completions for the current path
154
535
  try {
155
536
  const cwd = currentWorkingDirectory || process.cwd();
156
537
  let searchDir = cwd;
157
538
  let searchPattern = lastWord;
158
539
  let dirPart = '';
159
- // If the path contains a directory separator, split it
160
540
  if (lastWord.includes('/') || lastWord.includes('\\')) {
161
541
  const lastSep = Math.max(lastWord.lastIndexOf('/'), lastWord.lastIndexOf('\\'));
162
542
  dirPart = lastWord.substring(0, lastSep + 1);
163
543
  searchPattern = lastWord.substring(lastSep + 1);
164
- // For subshells, use their path format
165
544
  if (subshellContext && subshellContext.type !== 'local') {
166
- // Use forward slashes for Unix-like paths
167
545
  searchDir = dirPart.startsWith('/') ? dirPart.slice(0, -1) : cwd + '/' + dirPart.slice(0, -1);
168
546
  }
169
547
  else {
@@ -171,27 +549,19 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
171
549
  }
172
550
  }
173
551
  let entries;
174
- // Check if we're in a subshell context
175
552
  if (subshellContext && subshellContext.type !== 'local' && subshellContext.handler) {
176
- // Use the subshell handler to list directory
177
553
  const dirEntries = await subshellContext.handler.listDirectory(searchDir);
178
- entries = dirEntries.map(entry => ({
179
- name: entry.name,
180
- type: entry.type
181
- }));
554
+ entries = dirEntries.map(entry => ({ name: entry.name, type: entry.type }));
182
555
  }
183
556
  else {
184
- // Local filesystem - use fs
185
- if (!fs.existsSync(searchDir)) {
557
+ if (!fs.existsSync(searchDir))
186
558
  return;
187
- }
188
559
  const fsEntries = fs.readdirSync(searchDir, { withFileTypes: true });
189
560
  entries = fsEntries.map(entry => ({
190
561
  name: entry.name,
191
562
  type: entry.isDirectory() ? 'directory' : 'file'
192
563
  }));
193
564
  }
194
- // Filter matches
195
565
  const matches = entries
196
566
  .filter(entry => entry.name.toLowerCase().startsWith(searchPattern.toLowerCase()))
197
567
  .map(entry => {
@@ -200,48 +570,132 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
200
570
  return entry.type === 'directory' ? fullPath + separator : fullPath;
201
571
  })
202
572
  .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);
573
+ if (matches.length > 0) {
574
+ if (matches.length > 1) {
575
+ setCompletions(matches);
576
+ setCompletionIndex(0);
577
+ }
216
578
  words[words.length - 1] = matches[0];
217
579
  const newValue = words.join(' ');
218
- // Force remount to reset cursor position
219
580
  setValue(newValue);
220
- setInputKey(prev => prev + 1);
581
+ setCursorOffset(newValue.length);
221
582
  }
222
583
  }
223
584
  catch (error) {
224
- // Ignore errors in tab completion
585
+ // Ignore errors
225
586
  }
226
587
  };
227
588
  const handleSubmit = () => {
228
- // Don't submit when input is inactive (during approvals)
229
- if (!isActive) {
589
+ if (!isActive)
230
590
  return;
231
- }
232
591
  const trimmedValue = value.trim();
233
592
  if (trimmedValue) {
593
+ // Save to history if it was a command
594
+ if (commandMode) {
595
+ CommandHistoryManager.getInstance().addCommand(trimmedValue, currentDir);
596
+ }
234
597
  onSubmit(trimmedValue);
235
598
  setValue('');
599
+ setCursorOffset(0);
236
600
  setCompletions([]);
237
601
  setCompletionIndex(0);
238
602
  setHistoryIndex(-1);
239
603
  setTempValue('');
240
- setInputKey(prev => prev + 1); // Force remount to clear any residual state
604
+ setUndoStack([]);
605
+ setRedoStack([]);
606
+ setSelection(null);
607
+ setAutocompleteSuggestion(null);
608
+ }
609
+ };
610
+ // Rendering Logic with Scrolling
611
+ const renderInput = () => {
612
+ const lines = value.split('\n');
613
+ // If empty, show placeholder
614
+ if (lines.length === 1 && lines[0] === '') {
615
+ return React.createElement(Text, { color: "gray" }, placeholder);
616
+ }
617
+ // Calculate cursor line and column
618
+ let currentPos = 0;
619
+ let cursorLine = 0;
620
+ let cursorCol = 0;
621
+ for (let i = 0; i < lines.length; i++) {
622
+ if (currentPos + lines[i].length >= cursorOffset) {
623
+ cursorLine = i;
624
+ cursorCol = cursorOffset - currentPos;
625
+ break;
626
+ }
627
+ currentPos += lines[i].length + 1; // +1 for newline
628
+ }
629
+ // Edge case: cursor at very end of content
630
+ if (cursorOffset === value.length && lines.length > 0) {
631
+ cursorLine = lines.length - 1;
632
+ cursorCol = lines[lines.length - 1].length;
633
+ }
634
+ // Calculate visible range
635
+ let startLine = 0;
636
+ if (lines.length > MAX_VISIBLE_LINES) {
637
+ if (cursorLine < MAX_VISIBLE_LINES) {
638
+ startLine = 0;
639
+ }
640
+ else {
641
+ startLine = cursorLine - MAX_VISIBLE_LINES + 1;
642
+ }
241
643
  }
644
+ const endLine = Math.min(startLine + MAX_VISIBLE_LINES, lines.length);
645
+ const visibleLines = lines.slice(startLine, endLine);
646
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
647
+ startLine > 0 && React.createElement(Text, { color: "gray" }, "\u2191 ..."),
648
+ visibleLines.map((line, idx) => {
649
+ const actualLineIndex = startLine + idx;
650
+ const isCursorLine = actualLineIndex === cursorLine;
651
+ const isLastLine = actualLineIndex === lines.length - 1;
652
+ // Calculate absolute position of this line start
653
+ let lineStartPos = 0;
654
+ for (let k = 0; k < actualLineIndex; k++)
655
+ lineStartPos += lines[k].length + 1;
656
+ if (!isActive) {
657
+ if (line.length === 0) {
658
+ return React.createElement(Text, { key: idx }, " ");
659
+ }
660
+ return React.createElement(Text, { key: idx }, line);
661
+ }
662
+ // Render with selection and cursor
663
+ const chars = line.split('');
664
+ const renderedChars = chars.map((char, charIdx) => {
665
+ const absPos = lineStartPos + charIdx;
666
+ const isSelected = selection &&
667
+ absPos >= Math.min(selection.start, selection.end) &&
668
+ absPos < Math.max(selection.start, selection.end);
669
+ const isCursor = isCursorLine && charIdx === cursorCol;
670
+ if (isCursor) {
671
+ return React.createElement(Text, { key: charIdx, inverse: true, color: isSelected ? "yellow" : undefined }, char);
672
+ }
673
+ if (isSelected) {
674
+ return React.createElement(Text, { key: charIdx, backgroundColor: "white", color: "black" }, char);
675
+ }
676
+ return React.createElement(Text, { key: charIdx }, char);
677
+ });
678
+ // Handle cursor at end of line
679
+ if (isCursorLine && cursorCol === line.length) {
680
+ renderedChars.push(React.createElement(Text, { key: "cursor", inverse: true }, " "));
681
+ }
682
+ // Render Autocomplete Ghost Text
683
+ // Only on the last line, if suggestion exists and matches start
684
+ if (isLastLine && autocompleteSuggestion && autocompleteSuggestion.startsWith(value)) {
685
+ const suffix = autocompleteSuggestion.slice(value.length);
686
+ if (suffix) {
687
+ renderedChars.push(React.createElement(Text, { key: "ghost", color: "gray" }, suffix));
688
+ }
689
+ }
690
+ if (renderedChars.length === 0) {
691
+ return React.createElement(Text, { key: idx }, " ");
692
+ }
693
+ return React.createElement(Text, { key: idx }, renderedChars);
694
+ }),
695
+ endLine < lines.length && React.createElement(Text, { color: "gray" }, "\u2193 ...")));
242
696
  };
243
- return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#003b59", paddingX: 1, paddingY: 0 },
244
- React.createElement(Box, { marginY: 1, justifyContent: "space-between" },
697
+ return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: commandMode ? "#00cc66" : "#257aa5ff", paddingX: 1, paddingY: 0, width: "100%" },
698
+ React.createElement(Box, { marginY: 1, justifyContent: "space-between", width: "100%" },
245
699
  React.createElement(Box, null,
246
700
  subshellContext && subshellContext.type !== 'local' && (React.createElement(Breadcrumbs, { context: subshellContext })),
247
701
  React.createElement(Text, { color: "#666666" }, "CWD: "),
@@ -252,13 +706,15 @@ export const InputBox = React.memo(({ onSubmit, placeholder = 'Ask anything...',
252
706
  React.createElement(Text, { color: "#00ccff" }, model))),
253
707
  React.createElement(Box, null,
254
708
  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,
709
+ isAutoMode ? (React.createElement(Text, null,
710
+ React.createElement(Text, { color: "#00ccff", bold: true }, "Auto ["),
711
+ detectedIntent === 'command' ? (React.createElement(Text, { color: "#00cc66", bold: true }, "Terminal")) : (React.createElement(Text, { color: "#00ccff" }, "Agent")),
712
+ 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"))))),
713
+ React.createElement(Box, { flexDirection: "row", width: "100%" },
257
714
  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')),
715
+ renderInput()),
716
+ React.createElement(Box, { marginY: 1, justifyContent: "space-between", width: "100%" },
717
+ 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
718
  React.createElement(Box, { gap: 1 },
263
719
  !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
720
  !commandMode && (React.createElement(Box, { marginLeft: 1 },