@compilr-dev/cli 0.4.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.
- package/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- package/package.json +66 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Prompt v2 (Event-Driven)
|
|
3
|
+
*
|
|
4
|
+
* Refactored input handling with event-driven architecture.
|
|
5
|
+
* - Emits events instead of blocking with getInput()
|
|
6
|
+
* - Supports queue mode for capturing input during agent execution
|
|
7
|
+
* - Always captures keystrokes (no blocking)
|
|
8
|
+
*
|
|
9
|
+
* Events:
|
|
10
|
+
* - 'submit' - User pressed Enter with input
|
|
11
|
+
* - 'command' - User submitted a slash command
|
|
12
|
+
* - 'cancel' - User pressed Ctrl+C
|
|
13
|
+
* - 'escape' - User pressed Esc (for aborting agent)
|
|
14
|
+
* - 'change' - Input text changed
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { getStyles } from '../themes/index.js';
|
|
19
|
+
import * as terminal from './terminal.js';
|
|
20
|
+
import { getAutocompleteCommands } from '../commands.js';
|
|
21
|
+
import { getFileMatches, extractAtMention, replaceAtMention, } from './file-autocomplete.js';
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Constants
|
|
24
|
+
// =============================================================================
|
|
25
|
+
const MAX_VISIBLE_COMMANDS = 10;
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Default Commands (from central registry)
|
|
28
|
+
// =============================================================================
|
|
29
|
+
export const DEFAULT_COMMANDS = getAutocompleteCommands();
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Helper Functions
|
|
32
|
+
// =============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Strip ANSI codes from string
|
|
35
|
+
*/
|
|
36
|
+
export function stripAnsi(str) {
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculate physical lines for wrapped text
|
|
42
|
+
*/
|
|
43
|
+
function calcPhysicalLines(text, startCol, termWidth) {
|
|
44
|
+
if (text.length === 0)
|
|
45
|
+
return 1;
|
|
46
|
+
const totalLen = startCol + text.length;
|
|
47
|
+
return Math.ceil(totalLen / termWidth) || 1;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculate fuzzy match score for a query against a target string.
|
|
51
|
+
* Higher score = better match.
|
|
52
|
+
* Returns -1 if no match.
|
|
53
|
+
*/
|
|
54
|
+
function fuzzyMatchScore(query, target) {
|
|
55
|
+
const queryLower = query.toLowerCase();
|
|
56
|
+
const targetLower = target.toLowerCase();
|
|
57
|
+
// Exact prefix match - highest priority (score 1000+)
|
|
58
|
+
if (targetLower.startsWith(queryLower)) {
|
|
59
|
+
return 1000 + (100 - target.length); // Shorter commands rank higher
|
|
60
|
+
}
|
|
61
|
+
// Contiguous substring match - high priority (score 500+)
|
|
62
|
+
if (targetLower.includes(queryLower)) {
|
|
63
|
+
const index = targetLower.indexOf(queryLower);
|
|
64
|
+
return 500 + (100 - index); // Earlier matches rank higher
|
|
65
|
+
}
|
|
66
|
+
// Fuzzy match - characters appear in order (score 100+)
|
|
67
|
+
let queryIdx = 0;
|
|
68
|
+
let consecutiveBonus = 0;
|
|
69
|
+
let lastMatchIdx = -1;
|
|
70
|
+
for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
|
|
71
|
+
if (targetLower[i] === queryLower[queryIdx]) {
|
|
72
|
+
// Bonus for consecutive matches
|
|
73
|
+
if (lastMatchIdx === i - 1) {
|
|
74
|
+
consecutiveBonus += 10;
|
|
75
|
+
}
|
|
76
|
+
lastMatchIdx = i;
|
|
77
|
+
queryIdx++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// All query characters found in order
|
|
81
|
+
if (queryIdx === queryLower.length) {
|
|
82
|
+
return 100 + consecutiveBonus + (100 - target.length);
|
|
83
|
+
}
|
|
84
|
+
// No match
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Filter and rank commands matching input using fuzzy matching
|
|
89
|
+
*/
|
|
90
|
+
function filterCommands(input, commands) {
|
|
91
|
+
// Score all commands
|
|
92
|
+
const scored = commands
|
|
93
|
+
.map((cmd) => ({
|
|
94
|
+
cmd,
|
|
95
|
+
score: fuzzyMatchScore(input, cmd.command),
|
|
96
|
+
}))
|
|
97
|
+
.filter((item) => item.score >= 0);
|
|
98
|
+
// Sort by score (highest first)
|
|
99
|
+
scored.sort((a, b) => b.score - a.score);
|
|
100
|
+
return scored.map((item) => item.cmd);
|
|
101
|
+
}
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Input Prompt Class
|
|
104
|
+
// =============================================================================
|
|
105
|
+
export class InputPrompt extends EventEmitter {
|
|
106
|
+
// Configuration
|
|
107
|
+
prompt;
|
|
108
|
+
promptLen;
|
|
109
|
+
showSeparators;
|
|
110
|
+
commands;
|
|
111
|
+
// Input state
|
|
112
|
+
state = {
|
|
113
|
+
lines: [''],
|
|
114
|
+
currentLine: 0,
|
|
115
|
+
cursorPos: 0,
|
|
116
|
+
};
|
|
117
|
+
// Command autocomplete (for /commands)
|
|
118
|
+
autocomplete = {
|
|
119
|
+
active: false,
|
|
120
|
+
matches: [],
|
|
121
|
+
selectedIndex: 0,
|
|
122
|
+
scrollOffset: 0,
|
|
123
|
+
};
|
|
124
|
+
// File autocomplete (for @paths)
|
|
125
|
+
fileAutocomplete = {
|
|
126
|
+
active: false,
|
|
127
|
+
matches: [],
|
|
128
|
+
selectedIndex: 0,
|
|
129
|
+
scrollOffset: 0,
|
|
130
|
+
partial: '',
|
|
131
|
+
};
|
|
132
|
+
// History
|
|
133
|
+
history = [];
|
|
134
|
+
historyIndex = -1;
|
|
135
|
+
savedInput = '';
|
|
136
|
+
// Queue mode
|
|
137
|
+
queueMode = false;
|
|
138
|
+
queuedInputs = [];
|
|
139
|
+
// Rendering tracking
|
|
140
|
+
renderedLines = 0;
|
|
141
|
+
dropdownLines = 0;
|
|
142
|
+
linesAboveCursor = 0;
|
|
143
|
+
hasSeparators = false;
|
|
144
|
+
// Control
|
|
145
|
+
isRunning = false;
|
|
146
|
+
// Double Esc detection
|
|
147
|
+
lastEscTime = 0;
|
|
148
|
+
DOUBLE_ESC_THRESHOLD_MS = 500; // 500ms window for double Esc
|
|
149
|
+
// Suggestion (ghost text for next action)
|
|
150
|
+
suggestion = null;
|
|
151
|
+
constructor(options = {}) {
|
|
152
|
+
super();
|
|
153
|
+
this.prompt = options.prompt ?? getStyles().primary('❯ ');
|
|
154
|
+
this.promptLen = stripAnsi(this.prompt).length;
|
|
155
|
+
this.showSeparators = options.showSeparators ?? true;
|
|
156
|
+
this.commands = options.commands ?? DEFAULT_COMMANDS;
|
|
157
|
+
}
|
|
158
|
+
// ===========================================================================
|
|
159
|
+
// Public API
|
|
160
|
+
// ===========================================================================
|
|
161
|
+
/**
|
|
162
|
+
* Update the prompt string (for dynamic theme changes)
|
|
163
|
+
*/
|
|
164
|
+
setPrompt(prompt) {
|
|
165
|
+
this.prompt = prompt;
|
|
166
|
+
this.promptLen = stripAnsi(prompt).length;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Set a suggestion for the next action (ghost text)
|
|
170
|
+
*/
|
|
171
|
+
setSuggestion(action) {
|
|
172
|
+
this.suggestion = action;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the current suggestion
|
|
176
|
+
*/
|
|
177
|
+
getSuggestion() {
|
|
178
|
+
return this.suggestion;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Clear the current suggestion
|
|
182
|
+
*/
|
|
183
|
+
clearSuggestion() {
|
|
184
|
+
this.suggestion = null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Start input capture (non-blocking, event-driven)
|
|
188
|
+
*/
|
|
189
|
+
start() {
|
|
190
|
+
if (this.isRunning)
|
|
191
|
+
return;
|
|
192
|
+
this.isRunning = true;
|
|
193
|
+
terminal.enableRawMode();
|
|
194
|
+
this.resetState();
|
|
195
|
+
process.stdin.on('data', this.handleData);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Stop input capture
|
|
199
|
+
*/
|
|
200
|
+
stop() {
|
|
201
|
+
if (!this.isRunning)
|
|
202
|
+
return;
|
|
203
|
+
this.isRunning = false;
|
|
204
|
+
terminal.disableRawMode();
|
|
205
|
+
process.stdin.removeListener('data', this.handleData);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if running
|
|
209
|
+
*/
|
|
210
|
+
isActive() {
|
|
211
|
+
return this.isRunning;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Enable/disable queue mode
|
|
215
|
+
* In queue mode, Enter adds to queue instead of emitting 'submit'
|
|
216
|
+
*/
|
|
217
|
+
setQueueMode(enabled) {
|
|
218
|
+
this.queueMode = enabled;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check if in queue mode
|
|
222
|
+
*/
|
|
223
|
+
isQueueMode() {
|
|
224
|
+
return this.queueMode;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get all queued inputs (FIFO order)
|
|
228
|
+
*/
|
|
229
|
+
getQueuedInputs() {
|
|
230
|
+
return [...this.queuedInputs];
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pop the first queued input
|
|
234
|
+
*/
|
|
235
|
+
popQueuedInput() {
|
|
236
|
+
return this.queuedInputs.shift() ?? null;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Check if there are queued inputs
|
|
240
|
+
*/
|
|
241
|
+
hasQueuedInput() {
|
|
242
|
+
return this.queuedInputs.length > 0;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get number of queued inputs
|
|
246
|
+
*/
|
|
247
|
+
getQueueLength() {
|
|
248
|
+
return this.queuedInputs.length;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Clear the queue
|
|
252
|
+
*/
|
|
253
|
+
clearQueue() {
|
|
254
|
+
this.queuedInputs = [];
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get current buffer value
|
|
258
|
+
*/
|
|
259
|
+
getValue() {
|
|
260
|
+
return this.state.lines.join('\n');
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Set buffer value
|
|
264
|
+
*/
|
|
265
|
+
setValue(value) {
|
|
266
|
+
this.state.lines = value.split('\n');
|
|
267
|
+
this.state.currentLine = this.state.lines.length - 1;
|
|
268
|
+
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
269
|
+
this.updateAutocomplete();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Clear input buffer
|
|
273
|
+
*/
|
|
274
|
+
clearInput() {
|
|
275
|
+
this.state = {
|
|
276
|
+
lines: [''],
|
|
277
|
+
currentLine: 0,
|
|
278
|
+
cursorPos: 0,
|
|
279
|
+
};
|
|
280
|
+
this.autocomplete = {
|
|
281
|
+
active: false,
|
|
282
|
+
matches: [],
|
|
283
|
+
selectedIndex: 0,
|
|
284
|
+
scrollOffset: 0,
|
|
285
|
+
};
|
|
286
|
+
this.fileAutocomplete = {
|
|
287
|
+
active: false,
|
|
288
|
+
matches: [],
|
|
289
|
+
selectedIndex: 0,
|
|
290
|
+
scrollOffset: 0,
|
|
291
|
+
partial: '',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Add command to history
|
|
296
|
+
*/
|
|
297
|
+
addToHistory(input) {
|
|
298
|
+
if (input.trim() && (this.history.length === 0 || this.history[this.history.length - 1] !== input)) {
|
|
299
|
+
this.history.push(input);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Render the input prompt - returns array of lines
|
|
304
|
+
* Does NOT write to terminal (Footer handles that)
|
|
305
|
+
*/
|
|
306
|
+
render() {
|
|
307
|
+
const lines = [];
|
|
308
|
+
// Top separator
|
|
309
|
+
if (this.showSeparators) {
|
|
310
|
+
lines.push(this.getSeparatorLine());
|
|
311
|
+
}
|
|
312
|
+
// Input lines
|
|
313
|
+
const s = getStyles();
|
|
314
|
+
for (let i = 0; i < this.state.lines.length; i++) {
|
|
315
|
+
const linePrompt = i === 0 ? this.prompt : s.muted(' \\ ');
|
|
316
|
+
const lineContent = this.state.lines[i];
|
|
317
|
+
// Show suggestion as ghost text on first line when input is empty
|
|
318
|
+
if (i === 0 && lineContent === '' && this.suggestion && !this.autocomplete.active && !this.fileAutocomplete.active) {
|
|
319
|
+
const hint = s.muted(' (tab to accept)');
|
|
320
|
+
lines.push(linePrompt + s.muted(this.suggestion) + hint);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
lines.push(linePrompt + lineContent);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Bottom separator
|
|
327
|
+
if (this.showSeparators) {
|
|
328
|
+
lines.push(this.getSeparatorLine());
|
|
329
|
+
}
|
|
330
|
+
return lines;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Get the cursor position info for rendering
|
|
334
|
+
*/
|
|
335
|
+
getCursorInfo() {
|
|
336
|
+
const termWidth = terminal.getTerminalWidth();
|
|
337
|
+
const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
338
|
+
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
339
|
+
// Calculate which row within input (accounting for separators)
|
|
340
|
+
let row = this.showSeparators ? 1 : 0; // Start after top separator
|
|
341
|
+
for (let i = 0; i < this.state.currentLine; i++) {
|
|
342
|
+
const lp = i === 0 ? this.promptLen : 5;
|
|
343
|
+
row += calcPhysicalLines(this.state.lines[i], lp, termWidth);
|
|
344
|
+
}
|
|
345
|
+
row += Math.floor(cursorAbsPos / termWidth);
|
|
346
|
+
const col = cursorAbsPos % termWidth;
|
|
347
|
+
return { row, col };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get autocomplete dropdown lines (if active)
|
|
351
|
+
*/
|
|
352
|
+
getAutocompleteLines() {
|
|
353
|
+
const s = getStyles();
|
|
354
|
+
// File autocomplete takes priority
|
|
355
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
356
|
+
const lines = [];
|
|
357
|
+
const offset = this.fileAutocomplete.scrollOffset;
|
|
358
|
+
const visible = this.fileAutocomplete.matches.slice(offset, offset + MAX_VISIBLE_COMMANDS);
|
|
359
|
+
const total = this.fileAutocomplete.matches.length;
|
|
360
|
+
// Show scroll indicator if there are more items above
|
|
361
|
+
if (offset > 0) {
|
|
362
|
+
lines.push(s.muted(` ↑ ${String(offset)} more above`));
|
|
363
|
+
}
|
|
364
|
+
for (let i = 0; i < visible.length; i++) {
|
|
365
|
+
const file = visible[i];
|
|
366
|
+
const actualIndex = offset + i;
|
|
367
|
+
const isSelected = actualIndex === this.fileAutocomplete.selectedIndex;
|
|
368
|
+
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
369
|
+
const icon = file.isDirectory ? s.warning('📁 ') : s.info('📄 ');
|
|
370
|
+
const name = isSelected ? s.primary(chalk.bold(file.path)) : file.path;
|
|
371
|
+
lines.push(`${prefix}${icon}${name}`);
|
|
372
|
+
}
|
|
373
|
+
// Show scroll indicator if there are more items below
|
|
374
|
+
const remaining = total - offset - visible.length;
|
|
375
|
+
if (remaining > 0) {
|
|
376
|
+
lines.push(s.muted(` ↓ ${String(remaining)} more below`));
|
|
377
|
+
}
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
// Command autocomplete
|
|
381
|
+
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
const lines = [];
|
|
385
|
+
const offset = this.autocomplete.scrollOffset;
|
|
386
|
+
const visible = this.autocomplete.matches.slice(offset, offset + MAX_VISIBLE_COMMANDS);
|
|
387
|
+
const total = this.autocomplete.matches.length;
|
|
388
|
+
// Show scroll indicator if there are more items above
|
|
389
|
+
if (offset > 0) {
|
|
390
|
+
lines.push(s.muted(` ↑ ${String(offset)} more above`));
|
|
391
|
+
}
|
|
392
|
+
for (let i = 0; i < visible.length; i++) {
|
|
393
|
+
const cmd = visible[i];
|
|
394
|
+
const actualIndex = offset + i;
|
|
395
|
+
const isSelected = actualIndex === this.autocomplete.selectedIndex;
|
|
396
|
+
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
397
|
+
const name = isSelected ? s.primary(chalk.bold(cmd.command)) : cmd.command;
|
|
398
|
+
const desc = s.muted(` - ${cmd.description}`);
|
|
399
|
+
lines.push(`${prefix}${name}${desc}`);
|
|
400
|
+
}
|
|
401
|
+
// Show scroll indicator if there are more items below
|
|
402
|
+
const remaining = total - offset - visible.length;
|
|
403
|
+
if (remaining > 0) {
|
|
404
|
+
lines.push(s.muted(` ↓ ${String(remaining)} more below`));
|
|
405
|
+
}
|
|
406
|
+
return lines;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if autocomplete is showing
|
|
410
|
+
*/
|
|
411
|
+
isAutocompleteActive() {
|
|
412
|
+
return ((this.autocomplete.active && this.autocomplete.matches.length > 0) ||
|
|
413
|
+
(this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0));
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get height of rendered content
|
|
417
|
+
*/
|
|
418
|
+
getHeight() {
|
|
419
|
+
let height = this.state.lines.length;
|
|
420
|
+
if (this.showSeparators)
|
|
421
|
+
height += 2;
|
|
422
|
+
return height;
|
|
423
|
+
}
|
|
424
|
+
// ===========================================================================
|
|
425
|
+
// Legacy API (for backwards compatibility during migration)
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
/**
|
|
428
|
+
* @deprecated Use event-driven start() instead
|
|
429
|
+
* Kept for backwards compatibility during migration
|
|
430
|
+
*/
|
|
431
|
+
async getInput() {
|
|
432
|
+
return new Promise((resolve) => {
|
|
433
|
+
const onSubmit = (input) => {
|
|
434
|
+
cleanup();
|
|
435
|
+
resolve({ action: 'submit', value: input });
|
|
436
|
+
};
|
|
437
|
+
const onCommand = (command, args) => {
|
|
438
|
+
cleanup();
|
|
439
|
+
resolve({ action: 'command', command, args });
|
|
440
|
+
};
|
|
441
|
+
const onCancel = () => {
|
|
442
|
+
cleanup();
|
|
443
|
+
resolve({ action: 'cancel' });
|
|
444
|
+
};
|
|
445
|
+
const cleanup = () => {
|
|
446
|
+
this.removeListener('submit', onSubmit);
|
|
447
|
+
this.removeListener('command', onCommand);
|
|
448
|
+
this.removeListener('cancel', onCancel);
|
|
449
|
+
};
|
|
450
|
+
this.on('submit', onSubmit);
|
|
451
|
+
this.on('command', onCommand);
|
|
452
|
+
this.on('cancel', onCancel);
|
|
453
|
+
if (!this.isRunning) {
|
|
454
|
+
this.start();
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
// ===========================================================================
|
|
459
|
+
// Private: State Management
|
|
460
|
+
// ===========================================================================
|
|
461
|
+
resetState() {
|
|
462
|
+
this.state = {
|
|
463
|
+
lines: [''],
|
|
464
|
+
currentLine: 0,
|
|
465
|
+
cursorPos: 0,
|
|
466
|
+
};
|
|
467
|
+
this.autocomplete = {
|
|
468
|
+
active: false,
|
|
469
|
+
matches: [],
|
|
470
|
+
selectedIndex: 0,
|
|
471
|
+
scrollOffset: 0,
|
|
472
|
+
};
|
|
473
|
+
this.fileAutocomplete = {
|
|
474
|
+
active: false,
|
|
475
|
+
matches: [],
|
|
476
|
+
selectedIndex: 0,
|
|
477
|
+
scrollOffset: 0,
|
|
478
|
+
partial: '',
|
|
479
|
+
};
|
|
480
|
+
this.historyIndex = -1;
|
|
481
|
+
this.savedInput = '';
|
|
482
|
+
this.renderedLines = 0;
|
|
483
|
+
this.dropdownLines = 0;
|
|
484
|
+
this.linesAboveCursor = 0;
|
|
485
|
+
this.hasSeparators = false;
|
|
486
|
+
}
|
|
487
|
+
updateAutocomplete() {
|
|
488
|
+
const fullInput = this.getValue();
|
|
489
|
+
const currentLine = this.state.lines[this.state.currentLine];
|
|
490
|
+
const cursorPos = this.state.cursorPos;
|
|
491
|
+
// Check for @ file path autocomplete first
|
|
492
|
+
const atMention = extractAtMention(currentLine, cursorPos);
|
|
493
|
+
if (atMention !== null) {
|
|
494
|
+
// File autocomplete mode
|
|
495
|
+
this.autocomplete.active = false;
|
|
496
|
+
this.autocomplete.matches = [];
|
|
497
|
+
this.autocomplete.selectedIndex = 0;
|
|
498
|
+
this.autocomplete.scrollOffset = 0;
|
|
499
|
+
this.fileAutocomplete.active = true;
|
|
500
|
+
this.fileAutocomplete.partial = atMention;
|
|
501
|
+
this.fileAutocomplete.matches = getFileMatches(atMention);
|
|
502
|
+
if (this.fileAutocomplete.selectedIndex >= this.fileAutocomplete.matches.length) {
|
|
503
|
+
this.fileAutocomplete.selectedIndex = Math.max(0, this.fileAutocomplete.matches.length - 1);
|
|
504
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// Reset file autocomplete
|
|
509
|
+
this.fileAutocomplete.active = false;
|
|
510
|
+
this.fileAutocomplete.matches = [];
|
|
511
|
+
this.fileAutocomplete.selectedIndex = 0;
|
|
512
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
513
|
+
this.fileAutocomplete.partial = '';
|
|
514
|
+
// Check for / command autocomplete
|
|
515
|
+
if (fullInput.startsWith('/') && this.state.lines.length === 1) {
|
|
516
|
+
this.autocomplete.active = true;
|
|
517
|
+
// Refresh commands dynamically to include newly created custom commands
|
|
518
|
+
const freshCommands = getAutocompleteCommands();
|
|
519
|
+
this.autocomplete.matches = filterCommands(fullInput, freshCommands);
|
|
520
|
+
if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
|
|
521
|
+
this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
|
|
522
|
+
this.autocomplete.scrollOffset = 0;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
this.autocomplete.active = false;
|
|
527
|
+
this.autocomplete.matches = [];
|
|
528
|
+
this.autocomplete.selectedIndex = 0;
|
|
529
|
+
this.autocomplete.scrollOffset = 0;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
getSeparatorLine() {
|
|
533
|
+
const s = getStyles();
|
|
534
|
+
return s.muted('─'.repeat(terminal.getTerminalWidth()));
|
|
535
|
+
}
|
|
536
|
+
// ===========================================================================
|
|
537
|
+
// Private: Input Handling
|
|
538
|
+
// ===========================================================================
|
|
539
|
+
handleData = (data) => {
|
|
540
|
+
if (!this.isRunning)
|
|
541
|
+
return;
|
|
542
|
+
const key = data.toString();
|
|
543
|
+
this.handleKey(key, data);
|
|
544
|
+
};
|
|
545
|
+
handleKey(key, data) {
|
|
546
|
+
// Detect special keys
|
|
547
|
+
const isEscape = data.length === 1 && data[0] === 0x1b;
|
|
548
|
+
const isEnter = key === '\r' || key === '\n';
|
|
549
|
+
const isBackspace = key === '\x7f' || key === '\b';
|
|
550
|
+
const isTab = key === '\t';
|
|
551
|
+
const isShiftTab = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x5a;
|
|
552
|
+
const isCtrlC = key === '\x03';
|
|
553
|
+
const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
|
|
554
|
+
const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
|
|
555
|
+
const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
|
|
556
|
+
const isRightArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x43;
|
|
557
|
+
// Mac Option + Arrow (word navigation)
|
|
558
|
+
const isOptionLeft = (data.length === 6 &&
|
|
559
|
+
data[0] === 0x1b &&
|
|
560
|
+
data[1] === 0x5b &&
|
|
561
|
+
data[2] === 0x31 &&
|
|
562
|
+
data[3] === 0x3b &&
|
|
563
|
+
data[4] === 0x33 &&
|
|
564
|
+
data[5] === 0x44) ||
|
|
565
|
+
(data.length === 2 && data[0] === 0x1b && data[1] === 0x62);
|
|
566
|
+
const isOptionRight = (data.length === 6 &&
|
|
567
|
+
data[0] === 0x1b &&
|
|
568
|
+
data[1] === 0x5b &&
|
|
569
|
+
data[2] === 0x31 &&
|
|
570
|
+
data[3] === 0x3b &&
|
|
571
|
+
data[4] === 0x33 &&
|
|
572
|
+
data[5] === 0x43) ||
|
|
573
|
+
(data.length === 2 && data[0] === 0x1b && data[1] === 0x66);
|
|
574
|
+
// Home/End (Cmd+Left/Right on Mac, or Home/End keys)
|
|
575
|
+
const isHome = key === '\x01' || (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x48);
|
|
576
|
+
const isEnd = key === '\x05' || (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
|
|
577
|
+
// Ctrl+C - cancel
|
|
578
|
+
if (isCtrlC) {
|
|
579
|
+
this.emit('cancel');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// Escape - close autocomplete, double Esc clears input, or emit escape for agent abort
|
|
583
|
+
if (isEscape) {
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
const isDoubleEsc = now - this.lastEscTime < this.DOUBLE_ESC_THRESHOLD_MS;
|
|
586
|
+
this.lastEscTime = now;
|
|
587
|
+
if (this.fileAutocomplete.active) {
|
|
588
|
+
// First priority: close file autocomplete
|
|
589
|
+
this.fileAutocomplete.active = false;
|
|
590
|
+
this.fileAutocomplete.matches = [];
|
|
591
|
+
this.emit('change', this.getValue());
|
|
592
|
+
}
|
|
593
|
+
else if (this.autocomplete.active) {
|
|
594
|
+
// Second priority: close command autocomplete
|
|
595
|
+
this.autocomplete.active = false;
|
|
596
|
+
this.autocomplete.matches = [];
|
|
597
|
+
this.emit('change', this.getValue());
|
|
598
|
+
}
|
|
599
|
+
else if (isDoubleEsc && this.getValue().length > 0) {
|
|
600
|
+
// Third priority: double Esc clears input (if there's content)
|
|
601
|
+
this.clearInput();
|
|
602
|
+
this.emit('change', this.getValue());
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
// Fourth priority: emit escape for agent abort
|
|
606
|
+
this.emit('escape');
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// Enter
|
|
611
|
+
if (isEnter) {
|
|
612
|
+
this.handleEnter();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// Shift+Tab - cycle modes
|
|
616
|
+
if (isShiftTab) {
|
|
617
|
+
this.emit('modeChange');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// Tab - accept autocomplete or insert spaces
|
|
621
|
+
if (isTab) {
|
|
622
|
+
this.handleTab();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// Arrow keys
|
|
626
|
+
if (isUpArrow) {
|
|
627
|
+
this.handleArrowUp();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (isDownArrow) {
|
|
631
|
+
this.handleArrowDown();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (isLeftArrow) {
|
|
635
|
+
this.handleArrowLeft();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (isRightArrow) {
|
|
639
|
+
this.handleArrowRight();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// Word navigation
|
|
643
|
+
if (isOptionLeft) {
|
|
644
|
+
this.handleWordLeft();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (isOptionRight) {
|
|
648
|
+
this.handleWordRight();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
// Home/End
|
|
652
|
+
if (isHome) {
|
|
653
|
+
this.state.cursorPos = 0;
|
|
654
|
+
this.emit('change', this.getValue());
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (isEnd) {
|
|
658
|
+
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
659
|
+
this.emit('change', this.getValue());
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
// Backspace
|
|
663
|
+
if (isBackspace) {
|
|
664
|
+
this.handleBackspace();
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
// Regular character(s) - handles typing and paste
|
|
668
|
+
if (key.length >= 1) {
|
|
669
|
+
const printable = key
|
|
670
|
+
.split('')
|
|
671
|
+
.filter((c) => c.charCodeAt(0) >= 32)
|
|
672
|
+
.join('');
|
|
673
|
+
if (printable.length > 0) {
|
|
674
|
+
// Clear suggestion when user starts typing
|
|
675
|
+
if (this.suggestion) {
|
|
676
|
+
this.suggestion = null;
|
|
677
|
+
}
|
|
678
|
+
const line = this.state.lines[this.state.currentLine];
|
|
679
|
+
this.state.lines[this.state.currentLine] =
|
|
680
|
+
line.slice(0, this.state.cursorPos) + printable + line.slice(this.state.cursorPos);
|
|
681
|
+
this.state.cursorPos += printable.length;
|
|
682
|
+
this.historyIndex = -1;
|
|
683
|
+
this.updateAutocomplete();
|
|
684
|
+
this.emit('change', this.getValue());
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
handleEnter() {
|
|
689
|
+
const currentLine = this.state.lines[this.state.currentLine];
|
|
690
|
+
// Continuation with backslash
|
|
691
|
+
if (currentLine.endsWith('\\')) {
|
|
692
|
+
this.state.lines[this.state.currentLine] = currentLine.slice(0, -1);
|
|
693
|
+
this.state.lines.push('');
|
|
694
|
+
this.state.currentLine++;
|
|
695
|
+
this.state.cursorPos = 0;
|
|
696
|
+
this.autocomplete.active = false;
|
|
697
|
+
this.emit('change', this.getValue());
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
// File autocomplete selection - accept the path and continue
|
|
701
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
702
|
+
const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
|
|
703
|
+
const result = replaceAtMention(currentLine, this.state.cursorPos, selectedFile.path);
|
|
704
|
+
this.state.lines[this.state.currentLine] = result.input;
|
|
705
|
+
this.state.cursorPos = result.cursorPos;
|
|
706
|
+
this.fileAutocomplete.active = false;
|
|
707
|
+
this.fileAutocomplete.matches = [];
|
|
708
|
+
// Don't return - fall through to submit
|
|
709
|
+
}
|
|
710
|
+
// Command autocomplete selection - accept the command and continue to execute it
|
|
711
|
+
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
712
|
+
this.state.lines[0] = this.autocomplete.matches[this.autocomplete.selectedIndex].command;
|
|
713
|
+
this.state.cursorPos = this.state.lines[0].length;
|
|
714
|
+
this.autocomplete.active = false;
|
|
715
|
+
// Don't return - fall through to execute the command
|
|
716
|
+
}
|
|
717
|
+
const input = this.getValue();
|
|
718
|
+
// Empty input - ignore
|
|
719
|
+
if (!input.trim()) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// Add to history
|
|
723
|
+
this.addToHistory(input);
|
|
724
|
+
// Queue mode - add to queue instead of emitting
|
|
725
|
+
if (this.queueMode) {
|
|
726
|
+
this.queuedInputs.push(input);
|
|
727
|
+
this.clearInput();
|
|
728
|
+
this.emit('change', this.getValue());
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// Check if it's a command
|
|
732
|
+
if (input.startsWith('/')) {
|
|
733
|
+
const parts = input.slice(1).split(/\s+/);
|
|
734
|
+
const command = parts[0];
|
|
735
|
+
const args = parts.slice(1).join(' ');
|
|
736
|
+
this.clearInput();
|
|
737
|
+
this.emit('command', command, args);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
this.clearInput();
|
|
741
|
+
this.emit('submit', input);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
handleTab() {
|
|
745
|
+
// Accept suggestion if present and input is empty
|
|
746
|
+
if (this.suggestion && this.getValue() === '') {
|
|
747
|
+
this.state.lines[0] = this.suggestion;
|
|
748
|
+
this.state.cursorPos = this.suggestion.length;
|
|
749
|
+
this.suggestion = null;
|
|
750
|
+
this.emit('change', this.getValue());
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
// File autocomplete completion
|
|
754
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
755
|
+
const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
|
|
756
|
+
const currentLine = this.state.lines[this.state.currentLine];
|
|
757
|
+
const result = replaceAtMention(currentLine, this.state.cursorPos, selectedFile.path);
|
|
758
|
+
this.state.lines[this.state.currentLine] = result.input;
|
|
759
|
+
this.state.cursorPos = result.cursorPos;
|
|
760
|
+
this.fileAutocomplete.active = false;
|
|
761
|
+
this.fileAutocomplete.matches = [];
|
|
762
|
+
this.updateAutocomplete(); // Check if still in @ context (e.g., directory selected)
|
|
763
|
+
this.emit('change', this.getValue());
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Command autocomplete completion
|
|
767
|
+
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
768
|
+
this.state.lines[this.state.currentLine] =
|
|
769
|
+
this.autocomplete.matches[this.autocomplete.selectedIndex].command;
|
|
770
|
+
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
771
|
+
this.autocomplete.active = false;
|
|
772
|
+
this.autocomplete.matches = [];
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
// Insert 2 spaces
|
|
776
|
+
const line = this.state.lines[this.state.currentLine];
|
|
777
|
+
this.state.lines[this.state.currentLine] =
|
|
778
|
+
line.slice(0, this.state.cursorPos) + ' ' + line.slice(this.state.cursorPos);
|
|
779
|
+
this.state.cursorPos += 2;
|
|
780
|
+
this.historyIndex = -1;
|
|
781
|
+
this.updateAutocomplete();
|
|
782
|
+
}
|
|
783
|
+
this.emit('change', this.getValue());
|
|
784
|
+
}
|
|
785
|
+
handleArrowUp() {
|
|
786
|
+
// File autocomplete navigation
|
|
787
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.selectedIndex > 0) {
|
|
788
|
+
this.fileAutocomplete.selectedIndex--;
|
|
789
|
+
// Adjust scroll offset if selection goes above visible area
|
|
790
|
+
if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.scrollOffset) {
|
|
791
|
+
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex;
|
|
792
|
+
}
|
|
793
|
+
this.emit('change', this.getValue());
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
// Command autocomplete navigation
|
|
797
|
+
if (this.autocomplete.active && this.autocomplete.selectedIndex > 0) {
|
|
798
|
+
this.autocomplete.selectedIndex--;
|
|
799
|
+
// Adjust scroll offset if selection goes above visible area
|
|
800
|
+
if (this.autocomplete.selectedIndex < this.autocomplete.scrollOffset) {
|
|
801
|
+
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex;
|
|
802
|
+
}
|
|
803
|
+
this.emit('change', this.getValue());
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const termWidth = terminal.getTerminalWidth();
|
|
807
|
+
const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
808
|
+
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
809
|
+
const currentPhysicalRow = Math.floor(cursorAbsPos / termWidth);
|
|
810
|
+
const cursorColInRow = cursorAbsPos % termWidth;
|
|
811
|
+
// Navigate within wrapped line
|
|
812
|
+
if (currentPhysicalRow > 0) {
|
|
813
|
+
const newAbsPos = (currentPhysicalRow - 1) * termWidth + cursorColInRow;
|
|
814
|
+
this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
|
|
815
|
+
this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
|
|
816
|
+
this.emit('change', this.getValue());
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
// Navigate to previous logical line
|
|
820
|
+
if (this.state.currentLine > 0) {
|
|
821
|
+
this.state.currentLine--;
|
|
822
|
+
const prevLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
823
|
+
const prevLineLen = this.state.lines[this.state.currentLine].length;
|
|
824
|
+
const prevLinePhysical = calcPhysicalLines(this.state.lines[this.state.currentLine], prevLinePromptLen, termWidth);
|
|
825
|
+
const lastRowStart = (prevLinePhysical - 1) * termWidth;
|
|
826
|
+
const targetAbsPos = lastRowStart + cursorColInRow;
|
|
827
|
+
this.state.cursorPos = Math.max(0, targetAbsPos - prevLinePromptLen);
|
|
828
|
+
this.state.cursorPos = Math.min(this.state.cursorPos, prevLineLen);
|
|
829
|
+
this.emit('change', this.getValue());
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
// History navigation (at top of input)
|
|
833
|
+
if (!this.autocomplete.active && this.history.length > 0) {
|
|
834
|
+
if (this.historyIndex === -1) {
|
|
835
|
+
this.savedInput = this.getValue();
|
|
836
|
+
}
|
|
837
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
838
|
+
this.historyIndex++;
|
|
839
|
+
const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
|
|
840
|
+
this.state.lines = [historyEntry];
|
|
841
|
+
this.state.currentLine = 0;
|
|
842
|
+
this.state.cursorPos = historyEntry.length;
|
|
843
|
+
this.emit('change', this.getValue());
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
handleArrowDown() {
|
|
848
|
+
// File autocomplete navigation
|
|
849
|
+
if (this.fileAutocomplete.active &&
|
|
850
|
+
this.fileAutocomplete.selectedIndex < this.fileAutocomplete.matches.length - 1) {
|
|
851
|
+
this.fileAutocomplete.selectedIndex++;
|
|
852
|
+
// Adjust scroll offset if selection goes below visible area
|
|
853
|
+
const maxVisibleIndex = this.fileAutocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
854
|
+
if (this.fileAutocomplete.selectedIndex > maxVisibleIndex) {
|
|
855
|
+
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
856
|
+
}
|
|
857
|
+
this.emit('change', this.getValue());
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// Command autocomplete navigation
|
|
861
|
+
if (this.autocomplete.active &&
|
|
862
|
+
this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1) {
|
|
863
|
+
this.autocomplete.selectedIndex++;
|
|
864
|
+
// Adjust scroll offset if selection goes below visible area
|
|
865
|
+
const maxVisibleIndex = this.autocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
866
|
+
if (this.autocomplete.selectedIndex > maxVisibleIndex) {
|
|
867
|
+
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
868
|
+
}
|
|
869
|
+
this.emit('change', this.getValue());
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const termWidth = terminal.getTerminalWidth();
|
|
873
|
+
const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
874
|
+
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
875
|
+
const currentPhysicalRow = Math.floor(cursorAbsPos / termWidth);
|
|
876
|
+
const cursorColInRow = cursorAbsPos % termWidth;
|
|
877
|
+
const currentLinePhysical = calcPhysicalLines(this.state.lines[this.state.currentLine], currentLinePromptLen, termWidth);
|
|
878
|
+
// Navigate within wrapped line
|
|
879
|
+
if (currentPhysicalRow < currentLinePhysical - 1) {
|
|
880
|
+
const newAbsPos = (currentPhysicalRow + 1) * termWidth + cursorColInRow;
|
|
881
|
+
this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
|
|
882
|
+
this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
|
|
883
|
+
this.emit('change', this.getValue());
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
// Navigate to next logical line
|
|
887
|
+
if (this.state.currentLine < this.state.lines.length - 1) {
|
|
888
|
+
this.state.currentLine++;
|
|
889
|
+
const nextLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
890
|
+
const nextLineLen = this.state.lines[this.state.currentLine].length;
|
|
891
|
+
this.state.cursorPos = Math.max(0, cursorColInRow - nextLinePromptLen);
|
|
892
|
+
this.state.cursorPos = Math.min(this.state.cursorPos, nextLineLen);
|
|
893
|
+
this.emit('change', this.getValue());
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
// History forward
|
|
897
|
+
if (this.historyIndex >= 0) {
|
|
898
|
+
this.historyIndex--;
|
|
899
|
+
if (this.historyIndex === -1) {
|
|
900
|
+
this.state.lines = this.savedInput.split('\n');
|
|
901
|
+
this.state.currentLine = this.state.lines.length - 1;
|
|
902
|
+
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
|
|
906
|
+
this.state.lines = [historyEntry];
|
|
907
|
+
this.state.currentLine = 0;
|
|
908
|
+
this.state.cursorPos = historyEntry.length;
|
|
909
|
+
}
|
|
910
|
+
this.updateAutocomplete();
|
|
911
|
+
this.emit('change', this.getValue());
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
handleArrowLeft() {
|
|
915
|
+
if (this.state.cursorPos > 0) {
|
|
916
|
+
this.state.cursorPos--;
|
|
917
|
+
this.emit('change', this.getValue());
|
|
918
|
+
}
|
|
919
|
+
else if (this.state.currentLine > 0) {
|
|
920
|
+
this.state.currentLine--;
|
|
921
|
+
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
922
|
+
this.emit('change', this.getValue());
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
handleArrowRight() {
|
|
926
|
+
if (this.state.cursorPos < this.state.lines[this.state.currentLine].length) {
|
|
927
|
+
this.state.cursorPos++;
|
|
928
|
+
this.emit('change', this.getValue());
|
|
929
|
+
}
|
|
930
|
+
else if (this.state.currentLine < this.state.lines.length - 1) {
|
|
931
|
+
this.state.currentLine++;
|
|
932
|
+
this.state.cursorPos = 0;
|
|
933
|
+
this.emit('change', this.getValue());
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
handleWordLeft() {
|
|
937
|
+
const line = this.state.lines[this.state.currentLine];
|
|
938
|
+
if (this.state.cursorPos > 0) {
|
|
939
|
+
let pos = this.state.cursorPos;
|
|
940
|
+
while (pos > 0 && line[pos - 1] === ' ')
|
|
941
|
+
pos--;
|
|
942
|
+
while (pos > 0 && line[pos - 1] !== ' ')
|
|
943
|
+
pos--;
|
|
944
|
+
this.state.cursorPos = pos;
|
|
945
|
+
this.emit('change', this.getValue());
|
|
946
|
+
}
|
|
947
|
+
else if (this.state.currentLine > 0) {
|
|
948
|
+
this.state.currentLine--;
|
|
949
|
+
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
950
|
+
this.emit('change', this.getValue());
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
handleWordRight() {
|
|
954
|
+
const line = this.state.lines[this.state.currentLine];
|
|
955
|
+
if (this.state.cursorPos < line.length) {
|
|
956
|
+
let pos = this.state.cursorPos;
|
|
957
|
+
while (pos < line.length && line[pos] !== ' ')
|
|
958
|
+
pos++;
|
|
959
|
+
while (pos < line.length && line[pos] === ' ')
|
|
960
|
+
pos++;
|
|
961
|
+
this.state.cursorPos = pos;
|
|
962
|
+
this.emit('change', this.getValue());
|
|
963
|
+
}
|
|
964
|
+
else if (this.state.currentLine < this.state.lines.length - 1) {
|
|
965
|
+
this.state.currentLine++;
|
|
966
|
+
this.state.cursorPos = 0;
|
|
967
|
+
this.emit('change', this.getValue());
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
handleBackspace() {
|
|
971
|
+
this.historyIndex = -1;
|
|
972
|
+
if (this.state.cursorPos > 0) {
|
|
973
|
+
const line = this.state.lines[this.state.currentLine];
|
|
974
|
+
this.state.lines[this.state.currentLine] =
|
|
975
|
+
line.slice(0, this.state.cursorPos - 1) + line.slice(this.state.cursorPos);
|
|
976
|
+
this.state.cursorPos--;
|
|
977
|
+
this.updateAutocomplete();
|
|
978
|
+
this.emit('change', this.getValue());
|
|
979
|
+
}
|
|
980
|
+
else if (this.state.currentLine > 0) {
|
|
981
|
+
const currentLine = this.state.lines[this.state.currentLine];
|
|
982
|
+
const prevLine = this.state.lines[this.state.currentLine - 1];
|
|
983
|
+
this.state.lines[this.state.currentLine - 1] = prevLine + currentLine;
|
|
984
|
+
this.state.lines.splice(this.state.currentLine, 1);
|
|
985
|
+
this.state.currentLine--;
|
|
986
|
+
this.state.cursorPos = prevLine.length;
|
|
987
|
+
this.updateAutocomplete();
|
|
988
|
+
this.emit('change', this.getValue());
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|