maistro 1.0.390

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 (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +107 -0
  3. package/dist/app.d.ts +247 -0
  4. package/dist/app.d.ts.map +1 -0
  5. package/dist/app.js +4971 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/buildInfo.d.ts +5 -0
  8. package/dist/buildInfo.d.ts.map +1 -0
  9. package/dist/buildInfo.js +2 -0
  10. package/dist/buildInfo.js.map +1 -0
  11. package/dist/caffeinate.d.ts +72 -0
  12. package/dist/caffeinate.d.ts.map +1 -0
  13. package/dist/caffeinate.js +258 -0
  14. package/dist/caffeinate.js.map +1 -0
  15. package/dist/claudePath.d.ts +10 -0
  16. package/dist/claudePath.d.ts.map +1 -0
  17. package/dist/claudePath.js +34 -0
  18. package/dist/claudePath.js.map +1 -0
  19. package/dist/clipboard.d.ts +44 -0
  20. package/dist/clipboard.d.ts.map +1 -0
  21. package/dist/clipboard.js +442 -0
  22. package/dist/clipboard.js.map +1 -0
  23. package/dist/config.d.ts +211 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +933 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/constants.d.ts +50 -0
  28. package/dist/constants.d.ts.map +1 -0
  29. package/dist/constants.js +81 -0
  30. package/dist/constants.js.map +1 -0
  31. package/dist/contextBuilder.d.ts +38 -0
  32. package/dist/contextBuilder.d.ts.map +1 -0
  33. package/dist/contextBuilder.js +113 -0
  34. package/dist/contextBuilder.js.map +1 -0
  35. package/dist/dependencyDetector.d.ts +57 -0
  36. package/dist/dependencyDetector.d.ts.map +1 -0
  37. package/dist/dependencyDetector.js +505 -0
  38. package/dist/dependencyDetector.js.map +1 -0
  39. package/dist/executor.d.ts +83 -0
  40. package/dist/executor.d.ts.map +1 -0
  41. package/dist/executor.js +583 -0
  42. package/dist/executor.js.map +1 -0
  43. package/dist/git.d.ts +85 -0
  44. package/dist/git.d.ts.map +1 -0
  45. package/dist/git.js +283 -0
  46. package/dist/git.js.map +1 -0
  47. package/dist/imageManager.d.ts +161 -0
  48. package/dist/imageManager.d.ts.map +1 -0
  49. package/dist/imageManager.js +674 -0
  50. package/dist/imageManager.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +437 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/input-visual-test.d.ts +9 -0
  56. package/dist/input-visual-test.d.ts.map +1 -0
  57. package/dist/input-visual-test.js +108 -0
  58. package/dist/input-visual-test.js.map +1 -0
  59. package/dist/inputBox.d.ts +228 -0
  60. package/dist/inputBox.d.ts.map +1 -0
  61. package/dist/inputBox.js +966 -0
  62. package/dist/inputBox.js.map +1 -0
  63. package/dist/logger.d.ts +136 -0
  64. package/dist/logger.d.ts.map +1 -0
  65. package/dist/logger.js +347 -0
  66. package/dist/logger.js.map +1 -0
  67. package/dist/orchestrator.d.ts +149 -0
  68. package/dist/orchestrator.d.ts.map +1 -0
  69. package/dist/orchestrator.js +821 -0
  70. package/dist/orchestrator.js.map +1 -0
  71. package/dist/planner.d.ts +86 -0
  72. package/dist/planner.d.ts.map +1 -0
  73. package/dist/planner.js +830 -0
  74. package/dist/planner.js.map +1 -0
  75. package/dist/pty-test-runner.d.ts +87 -0
  76. package/dist/pty-test-runner.d.ts.map +1 -0
  77. package/dist/pty-test-runner.js +721 -0
  78. package/dist/pty-test-runner.js.map +1 -0
  79. package/dist/screen.d.ts +44 -0
  80. package/dist/screen.d.ts.map +1 -0
  81. package/dist/screen.js +152 -0
  82. package/dist/screen.js.map +1 -0
  83. package/dist/taskQueue.d.ts +70 -0
  84. package/dist/taskQueue.d.ts.map +1 -0
  85. package/dist/taskQueue.js +282 -0
  86. package/dist/taskQueue.js.map +1 -0
  87. package/dist/tui-test-harness.d.ts +216 -0
  88. package/dist/tui-test-harness.d.ts.map +1 -0
  89. package/dist/tui-test-harness.js +527 -0
  90. package/dist/tui-test-harness.js.map +1 -0
  91. package/dist/types.d.ts +257 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +46 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/ui-visual-test.d.ts +15 -0
  96. package/dist/ui-visual-test.d.ts.map +1 -0
  97. package/dist/ui-visual-test.js +141 -0
  98. package/dist/ui-visual-test.js.map +1 -0
  99. package/dist/ui.d.ts +272 -0
  100. package/dist/ui.d.ts.map +1 -0
  101. package/dist/ui.js +1531 -0
  102. package/dist/ui.js.map +1 -0
  103. package/dist/validator.d.ts +53 -0
  104. package/dist/validator.d.ts.map +1 -0
  105. package/dist/validator.js +491 -0
  106. package/dist/validator.js.map +1 -0
  107. package/dist/versionCheck.d.ts +63 -0
  108. package/dist/versionCheck.d.ts.map +1 -0
  109. package/dist/versionCheck.js +261 -0
  110. package/dist/versionCheck.js.map +1 -0
  111. package/package.json +62 -0
@@ -0,0 +1,966 @@
1
+ import { mightBePastedImage, detectPastedImage } from './imageManager.js';
2
+ import { getNewlineHint } from './config.js';
3
+ /**
4
+ * Get the display width of a string (accounts for wide characters like CJK, emoji).
5
+ * Returns the number of terminal columns the string occupies.
6
+ */
7
+ export function getDisplayWidth(str) {
8
+ let width = 0;
9
+ for (const char of str) {
10
+ width += getCharWidth(char);
11
+ }
12
+ return width;
13
+ }
14
+ /**
15
+ * Get the display width of a single character.
16
+ * - East Asian Wide characters (CJK, fullwidth forms): 2
17
+ * - Most emoji: 2
18
+ * - Regular ASCII and other characters: 1
19
+ * - Control characters: 0
20
+ */
21
+ function getCharWidth(char) {
22
+ const code = char.codePointAt(0);
23
+ if (code === undefined)
24
+ return 0;
25
+ // Control characters
26
+ if (code < 0x20 || (code >= 0x7F && code < 0xA0)) {
27
+ return 0;
28
+ }
29
+ // Check for wide characters
30
+ if (isWideCharacter(code)) {
31
+ return 2;
32
+ }
33
+ return 1;
34
+ }
35
+ /**
36
+ * Check if a Unicode code point is a wide (2-column) character.
37
+ */
38
+ function isWideCharacter(code) {
39
+ // Hangul Jamo
40
+ if (code >= 0x1100 && code <= 0x115F)
41
+ return true;
42
+ // Hangul Compatibility Jamo
43
+ if (code >= 0x3130 && code <= 0x318F)
44
+ return true;
45
+ // CJK Radicals, Kangxi Radicals, CJK Symbols
46
+ if (code >= 0x2E80 && code <= 0x303F)
47
+ return true;
48
+ // Hiragana, Katakana
49
+ if (code >= 0x3040 && code <= 0x30FF)
50
+ return true;
51
+ // Bopomofo, Hangul Compatibility Jamo, Kanbun
52
+ if (code >= 0x3100 && code <= 0x319F)
53
+ return true;
54
+ // Enclosed CJK Letters
55
+ if (code >= 0x3200 && code <= 0x32FF)
56
+ return true;
57
+ // CJK Compatibility
58
+ if (code >= 0x3300 && code <= 0x33FF)
59
+ return true;
60
+ // CJK Unified Ideographs Extension A
61
+ if (code >= 0x3400 && code <= 0x4DBF)
62
+ return true;
63
+ // CJK Unified Ideographs
64
+ if (code >= 0x4E00 && code <= 0x9FFF)
65
+ return true;
66
+ // Yi Syllables and Radicals
67
+ if (code >= 0xA000 && code <= 0xA4CF)
68
+ return true;
69
+ // Hangul Syllables
70
+ if (code >= 0xAC00 && code <= 0xD7A3)
71
+ return true;
72
+ // CJK Compatibility Ideographs
73
+ if (code >= 0xF900 && code <= 0xFAFF)
74
+ return true;
75
+ // Vertical Forms
76
+ if (code >= 0xFE10 && code <= 0xFE1F)
77
+ return true;
78
+ // CJK Compatibility Forms
79
+ if (code >= 0xFE30 && code <= 0xFE4F)
80
+ return true;
81
+ // Halfwidth and Fullwidth Forms (fullwidth range)
82
+ if (code >= 0xFF00 && code <= 0xFF60)
83
+ return true;
84
+ if (code >= 0xFFE0 && code <= 0xFFE6)
85
+ return true;
86
+ // Emoji (various ranges)
87
+ if (code >= 0x1F300 && code <= 0x1F9FF)
88
+ return true;
89
+ if (code >= 0x1FA00 && code <= 0x1FAFF)
90
+ return true;
91
+ // Supplementary Ideographic Plane
92
+ if (code >= 0x20000 && code <= 0x2FFFF)
93
+ return true;
94
+ // Tertiary Ideographic Plane
95
+ if (code >= 0x30000 && code <= 0x3FFFF)
96
+ return true;
97
+ return false;
98
+ }
99
+ /**
100
+ * Get the character index that corresponds to a given display column.
101
+ * Returns the index of the character at or before the column.
102
+ */
103
+ export function charIndexAtDisplayColumn(str, column) {
104
+ let width = 0;
105
+ let index = 0;
106
+ for (const char of str) {
107
+ if (width >= column)
108
+ break;
109
+ width += getCharWidth(char);
110
+ index++;
111
+ }
112
+ return Math.min(index, [...str].length);
113
+ }
114
+ export class InputBox {
115
+ inputBuffer;
116
+ cursorIndex;
117
+ isFirstRender;
118
+ escPendingClear;
119
+ escTimeout;
120
+ escapeBuffer;
121
+ escapeTimeout;
122
+ resolved;
123
+ options;
124
+ stdout;
125
+ stdin;
126
+ dataHandler;
127
+ rawModeWasEnabled;
128
+ // Track preferred column for vertical navigation (like VS Code)
129
+ preferredVisualCol;
130
+ // History navigation state
131
+ historyIndex;
132
+ savedInput;
133
+ // Track rendered line count for self-clearing (fixes duplication when onRedraw is empty)
134
+ lastRenderedLineCount;
135
+ constructor(options) {
136
+ this.options = {
137
+ placeholder: options.placeholder || '',
138
+ hint: options.hint || 'Enter send',
139
+ mode: options.mode || 'multi-line',
140
+ ...options,
141
+ };
142
+ this.stdout = options.stdout || process.stdout;
143
+ this.stdin = options.stdin || process.stdin;
144
+ this.inputBuffer = options.initialValue || '';
145
+ this.cursorIndex = this.inputBuffer.length;
146
+ this.isFirstRender = true;
147
+ this.escPendingClear = false;
148
+ this.escTimeout = null;
149
+ this.escapeBuffer = '';
150
+ this.escapeTimeout = null;
151
+ this.resolved = false;
152
+ this.dataHandler = null;
153
+ this.rawModeWasEnabled = false;
154
+ this.preferredVisualCol = null;
155
+ // History state: -1 means not browsing history (editing current input)
156
+ this.historyIndex = -1;
157
+ this.savedInput = '';
158
+ // Track rendered lines for self-clearing
159
+ this.lastRenderedLineCount = 0;
160
+ }
161
+ /** Get current input value */
162
+ getValue() {
163
+ return this.inputBuffer;
164
+ }
165
+ /** Set input value programmatically */
166
+ setValue(value) {
167
+ this.inputBuffer = value;
168
+ this.cursorIndex = value.length;
169
+ this.render();
170
+ }
171
+ /** Clear input */
172
+ clear() {
173
+ this.inputBuffer = '';
174
+ this.cursorIndex = 0;
175
+ this.escPendingClear = false;
176
+ this.resetHistoryState();
177
+ this.render();
178
+ }
179
+ /** Update history array */
180
+ setHistory(history) {
181
+ this.options.history = history;
182
+ }
183
+ /** Mark as first render (call after external screen redraw) */
184
+ markFirstRender() {
185
+ this.isFirstRender = true;
186
+ }
187
+ /** Get current history index (-1 means not browsing history) */
188
+ getHistoryIndex() {
189
+ return this.historyIndex;
190
+ }
191
+ /** Reset history navigation state (call when user modifies input) */
192
+ resetHistoryState() {
193
+ this.historyIndex = -1;
194
+ this.savedInput = '';
195
+ }
196
+ /**
197
+ * Navigate history in the given direction.
198
+ * @returns true if navigation occurred, false if at boundary
199
+ */
200
+ navigateHistory(direction) {
201
+ const history = this.options.history;
202
+ if (!history || history.length === 0)
203
+ return false;
204
+ // If custom handler provided, use it
205
+ if (this.options.onHistoryNavigate) {
206
+ const newValue = this.options.onHistoryNavigate(direction, this.inputBuffer, this.historyIndex);
207
+ if (newValue !== null) {
208
+ // Save current input if just starting history navigation
209
+ if (this.historyIndex === -1 && direction === 'up') {
210
+ this.savedInput = this.inputBuffer;
211
+ }
212
+ this.inputBuffer = newValue;
213
+ this.cursorIndex = newValue.length;
214
+ // Update history index (managed by callback, but we track for state)
215
+ if (direction === 'up') {
216
+ this.historyIndex = this.historyIndex === -1 ? history.length - 1 : Math.max(0, this.historyIndex - 1);
217
+ }
218
+ else {
219
+ this.historyIndex = Math.min(history.length, this.historyIndex + 1);
220
+ if (this.historyIndex >= history.length) {
221
+ this.historyIndex = -1;
222
+ }
223
+ }
224
+ this.render();
225
+ return true;
226
+ }
227
+ return false;
228
+ }
229
+ // Default history navigation logic
230
+ if (direction === 'up') {
231
+ if (this.historyIndex === -1) {
232
+ // Save current input and go to most recent history
233
+ this.savedInput = this.inputBuffer;
234
+ this.historyIndex = history.length - 1;
235
+ }
236
+ else if (this.historyIndex > 0) {
237
+ // Go to older history
238
+ this.historyIndex--;
239
+ }
240
+ else {
241
+ // Already at oldest, do nothing
242
+ return false;
243
+ }
244
+ this.inputBuffer = history[this.historyIndex];
245
+ this.cursorIndex = this.inputBuffer.length;
246
+ this.render();
247
+ return true;
248
+ }
249
+ else {
250
+ // direction === 'down'
251
+ if (this.historyIndex === -1) {
252
+ // Not in history, do nothing
253
+ return false;
254
+ }
255
+ else if (this.historyIndex < history.length - 1) {
256
+ // Go to newer history
257
+ this.historyIndex++;
258
+ this.inputBuffer = history[this.historyIndex];
259
+ }
260
+ else {
261
+ // At most recent history, go back to saved input
262
+ this.historyIndex = -1;
263
+ this.inputBuffer = this.savedInput;
264
+ }
265
+ this.cursorIndex = this.inputBuffer.length;
266
+ this.render();
267
+ return true;
268
+ }
269
+ }
270
+ /**
271
+ * Check if cursor is at the first visual line of input
272
+ */
273
+ isAtFirstLine() {
274
+ const { lineNum, visualRow } = this.getVisualLineInfo();
275
+ return lineNum === 0 && visualRow === 0;
276
+ }
277
+ /**
278
+ * Check if cursor is at the last visual line of input
279
+ */
280
+ isAtLastLine() {
281
+ const { lineNum, lines, visualRow, totalVisualRows } = this.getVisualLineInfo();
282
+ return lineNum === lines.length - 1 && visualRow === totalVisualRows - 1;
283
+ }
284
+ safeWrite(data) {
285
+ try {
286
+ if (this.stdout.writable) {
287
+ this.stdout.write(data);
288
+ }
289
+ }
290
+ catch {
291
+ // Ignore write errors
292
+ }
293
+ }
294
+ getTermWidth() {
295
+ return this.stdout.columns || 80;
296
+ }
297
+ getTermHeight() {
298
+ return this.stdout.rows || 24;
299
+ }
300
+ /**
301
+ * Get cursor position info within multi-line input.
302
+ * Returns the logical line number and column within that line.
303
+ */
304
+ getLineInfo() {
305
+ const lines = this.inputBuffer.split('\n');
306
+ let pos = 0;
307
+ for (let i = 0; i < lines.length; i++) {
308
+ const lineLen = lines[i].length;
309
+ if (this.cursorIndex <= pos + lineLen) {
310
+ return {
311
+ lineNum: i,
312
+ col: this.cursorIndex - pos,
313
+ lines,
314
+ };
315
+ }
316
+ pos += lineLen + 1; // +1 for newline
317
+ }
318
+ // Cursor at end
319
+ return {
320
+ lineNum: lines.length - 1,
321
+ col: lines[lines.length - 1].length,
322
+ lines,
323
+ };
324
+ }
325
+ /**
326
+ * Get the content width available for text (terminal width minus prefix).
327
+ * Prefix is "> " for first line and " " for continuation lines = 2 chars.
328
+ */
329
+ getContentWidth() {
330
+ return Math.max(10, this.getTermWidth() - 2);
331
+ }
332
+ /**
333
+ * Get visual line info for navigation.
334
+ * Visual lines are what you see on screen after wrapping.
335
+ * Returns info about which visual row the cursor is on within the current logical line.
336
+ * Uses display width to account for wide characters (CJK, emoji, etc.).
337
+ */
338
+ getVisualLineInfo() {
339
+ const { lineNum, col, lines } = this.getLineInfo();
340
+ const contentWidth = this.getContentWidth();
341
+ const lineContent = lines[lineNum];
342
+ // Calculate total display width of the line (plus 1 for cursor at end)
343
+ const lineDisplayWidth = getDisplayWidth(lineContent) + 1;
344
+ // Calculate how many visual rows this logical line spans
345
+ const totalVisualRows = Math.max(1, Math.ceil(lineDisplayWidth / contentWidth));
346
+ // Calculate display column of cursor (width of characters before cursor)
347
+ const textBeforeCursor = [...lineContent].slice(0, col).join('');
348
+ const cursorDisplayCol = getDisplayWidth(textBeforeCursor);
349
+ // Which visual row is the cursor on?
350
+ const visualRow = Math.floor(cursorDisplayCol / contentWidth);
351
+ // Display column within this visual row
352
+ const visualCol = cursorDisplayCol % contentWidth;
353
+ return {
354
+ lineNum,
355
+ col,
356
+ lines,
357
+ visualRow,
358
+ visualCol,
359
+ totalVisualRows,
360
+ contentWidth,
361
+ };
362
+ }
363
+ /**
364
+ * Calculate cursor index from logical line number and column.
365
+ */
366
+ cursorIndexFromLineCol(lineNum, col, lines) {
367
+ let pos = 0;
368
+ for (let i = 0; i < lineNum; i++) {
369
+ pos += lines[i].length + 1; // +1 for newline
370
+ }
371
+ return pos + Math.min(col, lines[lineNum].length);
372
+ }
373
+ /**
374
+ * Render the input box.
375
+ * Uses full-screen redraw approach for reliability.
376
+ * Supports multi-line input with proper cursor positioning.
377
+ *
378
+ * Self-clears previous output to prevent duplication when onRedraw is empty.
379
+ */
380
+ render() {
381
+ const termWidth = this.getTermWidth();
382
+ const border = '─'.repeat(termWidth);
383
+ const { placeholder, hint, onRedraw } = this.options;
384
+ // Calculate input line count for onRedraw callback
385
+ const inputLineCount = this.inputBuffer ? this.inputBuffer.split('\n').length : 1;
386
+ // Self-clear previous content before calling onRedraw
387
+ // This prevents duplicate input boxes when navigating history or re-rendering
388
+ // Only clear if we have previous content (lastRenderedLineCount > 0)
389
+ if (this.lastRenderedLineCount > 0) {
390
+ this.safeWrite(`\x1b[${this.lastRenderedLineCount}A`); // Move cursor up
391
+ this.safeWrite('\x1b[J'); // Clear from cursor to end of screen
392
+ }
393
+ // Always call onRedraw to ensure content is drawn
394
+ // On first render with hideWhen, we especially need this since the InputBox won't draw anything visible
395
+ onRedraw(inputLineCount);
396
+ this.isFirstRender = false;
397
+ // Check if we should hide the input box visually
398
+ if (this.options.hideWhen?.()) {
399
+ // Clear any residual content from previous render
400
+ this.safeWrite('\x1b[J');
401
+ this.lastRenderedLineCount = 0;
402
+ return;
403
+ }
404
+ // Track VISUAL line count for this render (terminal rows, not logical lines)
405
+ // This accounts for line wrapping when content exceeds terminal width
406
+ let visualLineCount = 0;
407
+ const contentWidth = this.getContentWidth(); // Width available for content after prefix
408
+ // Top border (always 1 visual line - it exactly fits terminal width)
409
+ this.safeWrite(`\x1b[90m${border}\x1b[0m\n`);
410
+ visualLineCount++;
411
+ // Input line(s) with cursor - supports multi-line
412
+ if (this.inputBuffer) {
413
+ // Split input into lines for multi-line display
414
+ const { lineNum: cursorLine, col: cursorCol, lines } = this.getLineInfo();
415
+ // Calculate available rows for input (terminal height minus borders, hint, some buffer for content above)
416
+ // Reserve: 1 top border + 1 bottom border + 1 hint + 2 safety buffer = 5 lines
417
+ const maxInputLines = Math.max(3, this.getTermHeight() - 5);
418
+ const totalLines = lines.length;
419
+ const needsScrolling = totalLines > maxInputLines;
420
+ // Determine which lines to show (keep cursor line visible)
421
+ let startLine = 0;
422
+ let endLine = totalLines;
423
+ if (needsScrolling) {
424
+ // Center the cursor line in the visible area, with preference to show context
425
+ const halfWindow = Math.floor(maxInputLines / 2);
426
+ startLine = Math.max(0, cursorLine - halfWindow);
427
+ endLine = Math.min(totalLines, startLine + maxInputLines);
428
+ // Adjust start if we're near the end
429
+ if (endLine === totalLines) {
430
+ startLine = Math.max(0, totalLines - maxInputLines);
431
+ }
432
+ }
433
+ // Show scroll indicator at top if there are hidden lines above
434
+ if (startLine > 0) {
435
+ this.safeWrite(`\x1b[90m ↑ ${startLine} more line${startLine > 1 ? 's' : ''} above\x1b[0m\n`);
436
+ visualLineCount++;
437
+ }
438
+ // Render visible lines
439
+ for (let idx = startLine; idx < endLine; idx++) {
440
+ const line = lines[idx];
441
+ // First line gets "> ", continuation lines get " " (same width for alignment)
442
+ const prefix = idx === 0 ? '\x1b[90m>\x1b[0m ' : ' ';
443
+ // Show line break indicator (↵) at end of line if not the last line
444
+ const lineBreakIndicator = idx < totalLines - 1 ? '\x1b[90m↵\x1b[0m' : '';
445
+ if (idx === cursorLine) {
446
+ // This line has the cursor
447
+ const beforeCursor = line.slice(0, cursorCol);
448
+ const atCursor = line[cursorCol] || ' ';
449
+ const afterCursor = line.slice(cursorCol + 1);
450
+ this.safeWrite(`${prefix}${beforeCursor}\x1b[7m${atCursor}\x1b[0m${afterCursor}${lineBreakIndicator}\n`);
451
+ }
452
+ else {
453
+ this.safeWrite(`${prefix}${line}${lineBreakIndicator}\n`);
454
+ }
455
+ // Calculate visual lines for this content line (accounting for terminal wrapping)
456
+ // Line content width = display width of line + prefix (2) + line break indicator (~1 if present)
457
+ const lineDisplayWidth = getDisplayWidth(line) + 2 + (idx < totalLines - 1 ? 1 : 0);
458
+ const termWidth = this.getTermWidth();
459
+ const visualLinesForThisLine = Math.max(1, Math.ceil(lineDisplayWidth / termWidth));
460
+ visualLineCount += visualLinesForThisLine;
461
+ }
462
+ // Show scroll indicator at bottom if there are hidden lines below
463
+ if (endLine < totalLines) {
464
+ const hiddenBelow = totalLines - endLine;
465
+ this.safeWrite(`\x1b[90m ↓ ${hiddenBelow} more line${hiddenBelow > 1 ? 's' : ''} below\x1b[0m\n`);
466
+ visualLineCount++;
467
+ }
468
+ }
469
+ else if (placeholder) {
470
+ this.safeWrite(`\x1b[90m>\x1b[0m \x1b[7m \x1b[0m\x1b[90m${placeholder}\x1b[0m\n`);
471
+ visualLineCount++;
472
+ }
473
+ else {
474
+ this.safeWrite(`\x1b[90m>\x1b[0m \x1b[7m \x1b[0m\n`);
475
+ visualLineCount++;
476
+ }
477
+ // Bottom border (always 1 visual line)
478
+ this.safeWrite(`\x1b[90m${border}\x1b[0m\n`);
479
+ visualLineCount++;
480
+ // Custom footer or default hint
481
+ if (this.options.customFooter) {
482
+ // Let caller render custom footer (e.g., image list + hint)
483
+ // Note: customFooter is responsible for its own line tracking
484
+ this.options.customFooter(this.escPendingClear, termWidth);
485
+ visualLineCount++; // Assume at least 1 line for custom footer
486
+ }
487
+ else {
488
+ // Hint OUTSIDE the input box, right-aligned
489
+ // Show escape confirmation hint if pending, otherwise normal hint
490
+ const displayHint = this.escPendingClear ? 'Escape again to clear' : (hint || `Enter send · ${getNewlineHint()}`);
491
+ const hintColor = this.escPendingClear ? '\x1b[33m' : '\x1b[90m'; // Yellow for warning
492
+ // Use ANSI cursor positioning to right-align hint reliably (move cursor to column)
493
+ // Use display width for proper alignment with non-ASCII characters
494
+ const hintCol = Math.max(1, termWidth - getDisplayWidth(displayHint) + 1);
495
+ this.safeWrite(`\x1b[${hintCol}G${hintColor}${displayHint}\x1b[0m\n`);
496
+ visualLineCount++;
497
+ }
498
+ // Clear any residual content below (from previous longer input/wrapping)
499
+ this.safeWrite('\x1b[J');
500
+ // Save visual line count for next render's self-clear
501
+ this.lastRenderedLineCount = visualLineCount;
502
+ }
503
+ /**
504
+ * Handle a key press
505
+ */
506
+ handleKey(key) {
507
+ if (this.resolved) {
508
+ return;
509
+ }
510
+ // Let custom handler try first
511
+ if (this.options.onKey && this.options.onKey(key, this)) {
512
+ return;
513
+ }
514
+ // Handle escape sequence buffering
515
+ if (this.escapeBuffer) {
516
+ key = this.escapeBuffer + key;
517
+ this.escapeBuffer = '';
518
+ if (this.escapeTimeout) {
519
+ clearTimeout(this.escapeTimeout);
520
+ this.escapeTimeout = null;
521
+ }
522
+ }
523
+ // Bare escape - wait briefly for more input (could be start of sequence)
524
+ if (key === '\x1b') {
525
+ this.escapeBuffer = key;
526
+ this.escapeTimeout = setTimeout(() => {
527
+ this.escapeBuffer = '';
528
+ this.handleBareEscape();
529
+ }, 50);
530
+ return;
531
+ }
532
+ // Clear escape pending state on any other input
533
+ if (this.escPendingClear && key !== '\x1b') {
534
+ if (this.escTimeout) {
535
+ clearTimeout(this.escTimeout);
536
+ this.escTimeout = null;
537
+ }
538
+ this.escPendingClear = false;
539
+ // Will re-render below with normal hint
540
+ }
541
+ // Ctrl+C - cancel
542
+ if (key === '\x03') {
543
+ this.cleanup();
544
+ this.options.onCancel?.();
545
+ return;
546
+ }
547
+ // Clipboard paste - check system clipboard for images
548
+ // Supported sequences:
549
+ // - \x16 (Ctrl+V) - standard control character
550
+ // - \x1b[118;9u (Kitty protocol Cmd+V) - 118='v', 9=Super modifier
551
+ // - \x1bOV (configurable in iTerm2 for Cmd+V)
552
+ const isPasteKey = key === '\x16' || key === '\x1b[118;9u' || key === '\x1bOV';
553
+ if (isPasteKey && this.options.onClipboardPaste) {
554
+ this.options.onClipboardPaste().then(() => {
555
+ // Re-render after paste completes (callback may have redrawn screen)
556
+ this.isFirstRender = true;
557
+ this.render();
558
+ }).catch(() => {
559
+ // Re-render even on error
560
+ this.isFirstRender = true;
561
+ this.render();
562
+ });
563
+ return;
564
+ }
565
+ // Shift+Up/Down - history navigation (if shiftArrowHistory enabled or no multi-line)
566
+ // Kitty protocol: \x1b[1;2A (Shift+Up), \x1b[1;2B (Shift+Down)
567
+ if (key === '\x1b[1;2A') { // Shift+Up
568
+ if (this.options.shiftArrowHistory || this.options.mode === 'single-line') {
569
+ if (this.navigateHistory('up'))
570
+ return;
571
+ }
572
+ // Fall through to normal up handling if history nav didn't happen
573
+ }
574
+ if (key === '\x1b[1;2B') { // Shift+Down
575
+ if (this.options.shiftArrowHistory || this.options.mode === 'single-line') {
576
+ if (this.navigateHistory('down'))
577
+ return;
578
+ }
579
+ // Fall through to normal down handling if history nav didn't happen
580
+ }
581
+ // Shift+Enter - insert newline (multi-line input only)
582
+ // Common sequences: \x1b[13;2u (kitty/modern), \x1bOM (some terminals), or \x0a (Ctrl+J / some Shift+Enter)
583
+ if (key === '\x1b[13;2u' || key === '\x1bOM' || key === '\x0a') {
584
+ // Only allow newlines in multi-line mode
585
+ if (this.options.mode === 'single-line') {
586
+ // In single-line mode, treat as submit (like Enter)
587
+ this.cleanup();
588
+ this.options.onSubmit?.(this.inputBuffer.trim());
589
+ return;
590
+ }
591
+ this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + '\n' + this.inputBuffer.slice(this.cursorIndex);
592
+ this.cursorIndex++;
593
+ this.resetHistoryState(); // User modified input, reset history
594
+ this.render();
595
+ this.options.onChange?.(this.inputBuffer);
596
+ return;
597
+ }
598
+ // Enter - submit (only plain Enter \r, not \n which may be Shift+Enter)
599
+ // Also handle batched input like "run\r" or "run\r\n" when typing quickly or pasting
600
+ const endsWithEnter = key === '\r' || key.endsWith('\r') || key.endsWith('\r\n');
601
+ if (endsWithEnter) {
602
+ // If batched input, add the content before Enter to the buffer first
603
+ const enterSuffix = key.endsWith('\r\n') ? '\r\n' : (key.endsWith('\r') ? '\r' : '');
604
+ if (enterSuffix && key.length > enterSuffix.length) {
605
+ const contentBeforeEnter = key.slice(0, -enterSuffix.length);
606
+ // Only insert printable characters
607
+ let charsToInsert = '';
608
+ for (const c of contentBeforeEnter) {
609
+ const code = c.charCodeAt(0);
610
+ if ((code >= 32 && code < 127) || code >= 128) {
611
+ charsToInsert += c;
612
+ }
613
+ }
614
+ if (charsToInsert.length > 0) {
615
+ this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + charsToInsert + this.inputBuffer.slice(this.cursorIndex);
616
+ this.cursorIndex += charsToInsert.length;
617
+ }
618
+ }
619
+ this.cleanup();
620
+ this.options.onSubmit?.(this.inputBuffer.trim());
621
+ return;
622
+ }
623
+ // Arrow keys
624
+ if (key === '\x1b[D') { // Left
625
+ if (this.cursorIndex > 0) {
626
+ this.cursorIndex--;
627
+ this.preferredVisualCol = null; // Reset preferred column on horizontal movement
628
+ this.render();
629
+ }
630
+ return;
631
+ }
632
+ if (key === '\x1b[C') { // Right
633
+ if (this.cursorIndex < this.inputBuffer.length) {
634
+ this.cursorIndex++;
635
+ this.preferredVisualCol = null; // Reset preferred column on horizontal movement
636
+ this.render();
637
+ }
638
+ return;
639
+ }
640
+ if (key === '\x1b[A') { // Up - move to previous visual line
641
+ const { lineNum, lines, visualRow, visualCol, totalVisualRows, contentWidth } = this.getVisualLineInfo();
642
+ const atFirstLine = lineNum === 0 && visualRow === 0;
643
+ // At first line: check for boundary navigation or history
644
+ if (atFirstLine) {
645
+ // Try boundary navigation callback first
646
+ if (this.options.onBoundaryNavigation?.('up')) {
647
+ return;
648
+ }
649
+ // Try history navigation (if not using shiftArrowHistory mode)
650
+ if (!this.options.shiftArrowHistory && this.options.history) {
651
+ if (this.navigateHistory('up'))
652
+ return;
653
+ }
654
+ // Nothing to do, already at top
655
+ return;
656
+ }
657
+ // Set preferred column if not already set (for maintaining horizontal position)
658
+ // preferredVisualCol is a display column, not character index
659
+ if (this.preferredVisualCol === null) {
660
+ this.preferredVisualCol = visualCol;
661
+ }
662
+ if (visualRow > 0) {
663
+ // Move up within the same logical line (to previous visual row)
664
+ // Calculate target display column on the previous visual row
665
+ const targetDisplayCol = (visualRow - 1) * contentWidth + Math.min(this.preferredVisualCol, contentWidth - 1);
666
+ // Convert display column to character index
667
+ const newCol = charIndexAtDisplayColumn(lines[lineNum], targetDisplayCol);
668
+ this.cursorIndex = this.cursorIndexFromLineCol(lineNum, newCol, lines);
669
+ }
670
+ else if (lineNum > 0) {
671
+ // Move to the previous logical line's last visual row
672
+ const prevLine = lines[lineNum - 1];
673
+ const prevLineDisplayWidth = getDisplayWidth(prevLine) + 1;
674
+ const prevTotalVisualRows = Math.max(1, Math.ceil(prevLineDisplayWidth / contentWidth));
675
+ // Target the last visual row of the previous line, at preferred column
676
+ const targetDisplayCol = (prevTotalVisualRows - 1) * contentWidth + Math.min(this.preferredVisualCol, contentWidth - 1);
677
+ const newCol = charIndexAtDisplayColumn(prevLine, targetDisplayCol);
678
+ this.cursorIndex = this.cursorIndexFromLineCol(lineNum - 1, newCol, lines);
679
+ }
680
+ this.render();
681
+ return;
682
+ }
683
+ if (key === '\x1b[B') { // Down - move to next visual line
684
+ const { lineNum, lines, visualRow, visualCol, totalVisualRows, contentWidth } = this.getVisualLineInfo();
685
+ const atLastLine = lineNum === lines.length - 1 && visualRow === totalVisualRows - 1;
686
+ // At last line: check for boundary navigation or history
687
+ if (atLastLine) {
688
+ // Try boundary navigation callback first
689
+ if (this.options.onBoundaryNavigation?.('down')) {
690
+ return;
691
+ }
692
+ // Try history navigation (if not using shiftArrowHistory mode)
693
+ if (!this.options.shiftArrowHistory && this.options.history) {
694
+ if (this.navigateHistory('down'))
695
+ return;
696
+ }
697
+ // Nothing to do, already at bottom
698
+ return;
699
+ }
700
+ // Set preferred column if not already set
701
+ // preferredVisualCol is a display column, not character index
702
+ if (this.preferredVisualCol === null) {
703
+ this.preferredVisualCol = visualCol;
704
+ }
705
+ if (visualRow < totalVisualRows - 1) {
706
+ // Move down within the same logical line (to next visual row)
707
+ // Calculate target display column on the next visual row
708
+ const targetDisplayCol = (visualRow + 1) * contentWidth + Math.min(this.preferredVisualCol, contentWidth - 1);
709
+ // Convert display column to character index
710
+ const newCol = charIndexAtDisplayColumn(lines[lineNum], targetDisplayCol);
711
+ this.cursorIndex = this.cursorIndexFromLineCol(lineNum, newCol, lines);
712
+ }
713
+ else if (lineNum < lines.length - 1) {
714
+ // Move to the next logical line's first visual row
715
+ const nextLine = lines[lineNum + 1];
716
+ // Target the first visual row of the next line, at preferred column
717
+ const newCol = charIndexAtDisplayColumn(nextLine, this.preferredVisualCol);
718
+ this.cursorIndex = this.cursorIndexFromLineCol(lineNum + 1, newCol, lines);
719
+ }
720
+ this.render();
721
+ return;
722
+ }
723
+ // Home key or Ctrl+A - handle various terminal sequences
724
+ // CSI H, CSI 1~, SS3 H, CSI 7~ (rxvt), Ctrl+A
725
+ if (key === '\x1b[H' || key === '\x1b[1~' || key === '\x1bOH' || key === '\x1b[7~' || key === '\x01') {
726
+ this.cursorIndex = 0;
727
+ this.preferredVisualCol = null; // Reset on horizontal movement
728
+ this.render();
729
+ return;
730
+ }
731
+ // End key or Ctrl+E - handle various terminal sequences
732
+ // CSI F, CSI 4~, SS3 F, CSI 8~ (rxvt), Ctrl+E
733
+ if (key === '\x1b[F' || key === '\x1b[4~' || key === '\x1bOF' || key === '\x1b[8~' || key === '\x05') {
734
+ this.cursorIndex = this.inputBuffer.length;
735
+ this.preferredVisualCol = null; // Reset on horizontal movement
736
+ this.render();
737
+ return;
738
+ }
739
+ // Backspace
740
+ if (key === '\x7f' || key === '\b') {
741
+ if (this.cursorIndex > 0) {
742
+ this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex - 1) + this.inputBuffer.slice(this.cursorIndex);
743
+ this.cursorIndex--;
744
+ this.preferredVisualCol = null; // Reset on text modification
745
+ this.resetHistoryState(); // User modified input, reset history
746
+ this.render();
747
+ this.options.onChange?.(this.inputBuffer);
748
+ }
749
+ return;
750
+ }
751
+ // Delete
752
+ if (key === '\x1b[3~') {
753
+ if (this.cursorIndex < this.inputBuffer.length) {
754
+ this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + this.inputBuffer.slice(this.cursorIndex + 1);
755
+ this.preferredVisualCol = null; // Reset on text modification
756
+ this.resetHistoryState(); // User modified input, reset history
757
+ this.render();
758
+ this.options.onChange?.(this.inputBuffer);
759
+ }
760
+ return;
761
+ }
762
+ // Ignore other escape sequences
763
+ if (key.startsWith('\x1b[') || key.startsWith('\x1bO')) {
764
+ return;
765
+ }
766
+ // Regular printable characters (and newlines for pasted multi-line text)
767
+ // Handle different line ending styles: \n (Unix), \r (old Mac), \r\n (Windows)
768
+ let charsToInsert = '';
769
+ for (let i = 0; i < key.length; i++) {
770
+ const c = key[i];
771
+ if (c >= ' ' && c !== '\x7f') {
772
+ // Regular printable character
773
+ charsToInsert += c;
774
+ }
775
+ else if (c === '\n') {
776
+ // Unix newline
777
+ charsToInsert += '\n';
778
+ }
779
+ else if (c === '\r') {
780
+ // Carriage return - convert to newline
781
+ // Skip following \n if this is \r\n (Windows line ending)
782
+ if (key[i + 1] === '\n') {
783
+ i++; // Skip the \n
784
+ }
785
+ charsToInsert += '\n';
786
+ }
787
+ }
788
+ if (charsToInsert.length > 0) {
789
+ // Check for pasted image data before inserting as text
790
+ if (this.options.onImagePaste && mightBePastedImage(charsToInsert)) {
791
+ const detection = detectPastedImage(charsToInsert);
792
+ if (detection.detected) {
793
+ // Call the handler - it may be async
794
+ const result = this.options.onImagePaste(charsToInsert, detection);
795
+ if (result instanceof Promise) {
796
+ // Handle async callback
797
+ result.then((handled) => {
798
+ if (!handled) {
799
+ // If not handled, insert the text anyway
800
+ this.insertText(charsToInsert);
801
+ }
802
+ }).catch(() => {
803
+ // On error, insert the text
804
+ this.insertText(charsToInsert);
805
+ });
806
+ }
807
+ else if (result) {
808
+ // Synchronously handled - don't insert text
809
+ return;
810
+ }
811
+ else {
812
+ // Not handled - insert the text
813
+ this.insertText(charsToInsert);
814
+ }
815
+ return;
816
+ }
817
+ }
818
+ this.insertText(charsToInsert);
819
+ }
820
+ }
821
+ /**
822
+ * Insert text at current cursor position
823
+ */
824
+ insertText(text) {
825
+ // In single-line mode, strip newlines from inserted text
826
+ let textToInsert = text;
827
+ if (this.options.mode === 'single-line') {
828
+ textToInsert = text.replace(/\n/g, ' ');
829
+ }
830
+ this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + textToInsert + this.inputBuffer.slice(this.cursorIndex);
831
+ this.cursorIndex += textToInsert.length;
832
+ this.preferredVisualCol = null; // Reset on text insertion
833
+ this.resetHistoryState(); // User modified input, reset history
834
+ this.render();
835
+ this.options.onChange?.(this.inputBuffer);
836
+ }
837
+ /**
838
+ * Handle bare escape key (no sequence following)
839
+ */
840
+ handleBareEscape() {
841
+ if (this.resolved)
842
+ return;
843
+ // If input is empty, just cancel
844
+ if (!this.inputBuffer) {
845
+ this.cleanup();
846
+ this.options.onCancel?.();
847
+ return;
848
+ }
849
+ // If already pending clear, this is the second escape - clear the input
850
+ if (this.escPendingClear) {
851
+ if (this.escTimeout) {
852
+ clearTimeout(this.escTimeout);
853
+ this.escTimeout = null;
854
+ }
855
+ this.escPendingClear = false;
856
+ this.clear();
857
+ this.options.onChange?.('');
858
+ return;
859
+ }
860
+ // First escape with text - show confirmation hint
861
+ this.escPendingClear = true;
862
+ this.render();
863
+ // Auto-reset after 1 second
864
+ this.escTimeout = setTimeout(() => {
865
+ this.escPendingClear = false;
866
+ this.escTimeout = null;
867
+ if (!this.resolved) {
868
+ this.render();
869
+ }
870
+ }, 1000);
871
+ }
872
+ /**
873
+ * Clean up and stop listening
874
+ */
875
+ cleanup() {
876
+ if (this.resolved)
877
+ return;
878
+ this.resolved = true;
879
+ if (this.escTimeout) {
880
+ clearTimeout(this.escTimeout);
881
+ this.escTimeout = null;
882
+ }
883
+ if (this.escapeTimeout) {
884
+ clearTimeout(this.escapeTimeout);
885
+ this.escapeTimeout = null;
886
+ }
887
+ if (this.dataHandler) {
888
+ this.stdin.removeListener('data', this.dataHandler);
889
+ this.dataHandler = null;
890
+ }
891
+ // Restore raw mode state
892
+ if (this.stdin.setRawMode && !this.rawModeWasEnabled) {
893
+ this.stdin.setRawMode(false);
894
+ }
895
+ this.safeWrite('\n');
896
+ }
897
+ /**
898
+ * Start listening for input
899
+ */
900
+ start() {
901
+ this.resolved = false;
902
+ this.isFirstRender = true;
903
+ // Enable raw mode
904
+ this.rawModeWasEnabled = !!this.stdin.isRaw;
905
+ if (this.stdin.setRawMode) {
906
+ this.stdin.setRawMode(true);
907
+ }
908
+ this.stdin.resume?.();
909
+ // Set up key handler
910
+ this.dataHandler = (key) => {
911
+ this.handleKey(key.toString());
912
+ };
913
+ this.stdin.on('data', this.dataHandler);
914
+ // Initial render
915
+ this.render();
916
+ }
917
+ /**
918
+ * Run as a promise (convenience method)
919
+ */
920
+ prompt() {
921
+ return new Promise((resolve) => {
922
+ const originalOnSubmit = this.options.onSubmit;
923
+ const originalOnCancel = this.options.onCancel;
924
+ this.options.onSubmit = (value) => {
925
+ originalOnSubmit?.(value);
926
+ resolve(value);
927
+ };
928
+ this.options.onCancel = () => {
929
+ originalOnCancel?.();
930
+ resolve(null);
931
+ };
932
+ this.start();
933
+ });
934
+ }
935
+ }
936
+ /**
937
+ * Create and run an InputBox as a promise
938
+ */
939
+ export async function inputBox(options) {
940
+ const box = new InputBox(options);
941
+ return box.prompt();
942
+ }
943
+ // === Factory methods for common use cases ===
944
+ /**
945
+ * Create a single-line InputBox (no multi-line support)
946
+ */
947
+ export function singleLineInput(options) {
948
+ return new InputBox({ ...options, mode: 'single-line' });
949
+ }
950
+ /**
951
+ * Create an InputBox with history navigation support
952
+ */
953
+ export function inputWithHistory(options) {
954
+ return new InputBox({ ...options, mode: 'multi-line' });
955
+ }
956
+ /**
957
+ * Create a single-line InputBox with history navigation
958
+ */
959
+ export function singleLineWithHistory(options) {
960
+ return new InputBox({
961
+ ...options,
962
+ mode: 'single-line',
963
+ shiftArrowHistory: true, // Use Shift+Up/Down for history in single-line mode
964
+ });
965
+ }
966
+ //# sourceMappingURL=inputBox.js.map