aiexecode 1.0.66 → 1.0.68

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.

Potentially problematic release.


This version of aiexecode might be problematic. Click here for more details.

Files changed (86) hide show
  1. package/config_template/settings.json +1 -3
  2. package/index.js +46 -71
  3. package/package.json +1 -12
  4. package/payload_viewer/out/404/index.html +1 -1
  5. package/payload_viewer/out/404.html +1 -1
  6. package/payload_viewer/out/index.html +1 -1
  7. package/payload_viewer/out/index.txt +1 -1
  8. package/payload_viewer/web_server.js +0 -163
  9. package/src/ai_based/completion_judge.js +96 -5
  10. package/src/ai_based/orchestrator.js +71 -3
  11. package/src/ai_based/pip_package_installer.js +14 -12
  12. package/src/ai_based/pip_package_lookup.js +13 -10
  13. package/src/commands/apikey.js +8 -34
  14. package/src/commands/help.js +3 -4
  15. package/src/commands/model.js +17 -74
  16. package/src/commands/reasoning_effort.js +1 -1
  17. package/src/config/feature_flags.js +0 -12
  18. package/src/{ui → frontend}/App.js +23 -25
  19. package/src/frontend/README.md +81 -0
  20. package/src/{ui/components/SuggestionsDisplay.js → frontend/components/AutocompleteMenu.js} +3 -3
  21. package/src/{ui/components/HistoryItemDisplay.js → frontend/components/ConversationItem.js} +37 -89
  22. package/src/{ui → frontend}/components/CurrentModelView.js +3 -5
  23. package/src/{ui → frontend}/components/Footer.js +4 -6
  24. package/src/{ui → frontend}/components/Header.js +2 -5
  25. package/src/{ui/components/InputPrompt.js → frontend/components/Input.js} +16 -54
  26. package/src/frontend/components/ModelListView.js +106 -0
  27. package/src/{ui → frontend}/components/ModelUpdatedView.js +3 -5
  28. package/src/{ui → frontend}/components/SessionSpinner.js +3 -3
  29. package/src/{ui → frontend}/components/SetupWizard.js +8 -101
  30. package/src/{ui → frontend}/components/ToolApprovalPrompt.js +16 -14
  31. package/src/frontend/design/themeColors.js +42 -0
  32. package/src/{ui → frontend}/index.js +7 -7
  33. package/src/frontend/utils/inputBuffer.js +441 -0
  34. package/src/{ui/utils/markdownRenderer.js → frontend/utils/markdownParser.js} +3 -3
  35. package/src/{ui/utils/ConsolePatcher.js → frontend/utils/outputRedirector.js} +9 -9
  36. package/src/{ui/utils/codeColorizer.js → frontend/utils/syntaxHighlighter.js} +2 -3
  37. package/src/system/ai_request.js +145 -595
  38. package/src/system/code_executer.js +111 -16
  39. package/src/system/file_integrity.js +5 -7
  40. package/src/system/log.js +3 -3
  41. package/src/system/mcp_integration.js +15 -13
  42. package/src/system/output_helper.js +0 -20
  43. package/src/system/session.js +97 -23
  44. package/src/system/session_memory.js +2 -82
  45. package/src/system/system_info.js +1 -1
  46. package/src/system/ui_events.js +0 -43
  47. package/src/tools/code_editor.js +17 -2
  48. package/src/tools/file_reader.js +17 -2
  49. package/src/tools/glob.js +9 -1
  50. package/src/tools/response_message.js +0 -2
  51. package/src/tools/ripgrep.js +9 -1
  52. package/src/tools/web_downloader.js +9 -1
  53. package/src/util/config.js +3 -8
  54. package/src/util/debug_log.js +4 -11
  55. package/src/util/mcp_config_manager.js +3 -5
  56. package/src/util/output_formatter.js +0 -47
  57. package/src/util/prompt_loader.js +3 -4
  58. package/src/util/safe_fs.js +60 -0
  59. package/src/util/setup_wizard.js +1 -3
  60. package/src/util/text_formatter.js +0 -86
  61. package/src/config/claude_models.js +0 -195
  62. package/src/ui/README.md +0 -208
  63. package/src/ui/api.js +0 -167
  64. package/src/ui/components/AgenticProgressDisplay.js +0 -126
  65. package/src/ui/components/Composer.js +0 -55
  66. package/src/ui/components/LoadingIndicator.js +0 -54
  67. package/src/ui/components/ModelListView.js +0 -214
  68. package/src/ui/components/Notifications.js +0 -55
  69. package/src/ui/components/StreamingIndicator.js +0 -36
  70. package/src/ui/contexts/AppContext.js +0 -25
  71. package/src/ui/contexts/StreamingContext.js +0 -20
  72. package/src/ui/contexts/UIStateContext.js +0 -117
  73. package/src/ui/example-usage.js +0 -180
  74. package/src/ui/hooks/useTerminalResize.js +0 -39
  75. package/src/ui/themes/semantic-tokens.js +0 -73
  76. package/src/ui/utils/text-buffer.js +0 -975
  77. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → Z3AZSKhutj-kS4L8VpcOl}/_buildManifest.js +0 -0
  78. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → Z3AZSKhutj-kS4L8VpcOl}/_clientMiddlewareManifest.json +0 -0
  79. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → Z3AZSKhutj-kS4L8VpcOl}/_ssgManifest.js +0 -0
  80. /package/src/{ui → frontend}/components/BlankLine.js +0 -0
  81. /package/src/{ui → frontend}/components/FileDiffViewer.js +0 -0
  82. /package/src/{ui → frontend}/components/HelpView.js +0 -0
  83. /package/src/{ui → frontend}/hooks/useCompletion.js +0 -0
  84. /package/src/{ui → frontend}/hooks/useKeypress.js +0 -0
  85. /package/src/{ui → frontend}/utils/diffUtils.js +0 -0
  86. /package/src/{ui → frontend}/utils/renderInkComponent.js +0 -0
@@ -1,975 +0,0 @@
1
- /**
2
- * Advanced Text Buffer for multi-line input with cursor management
3
- */
4
-
5
- import stringWidth from 'string-width';
6
-
7
- function consolelog() { }
8
- /**
9
- * Convert string to code points array
10
- */
11
- export function toCodePoints(str) {
12
- return Array.from(str);
13
- }
14
-
15
- /**
16
- * Get length in code points
17
- */
18
- export function cpLen(str) {
19
- return toCodePoints(str).length;
20
- }
21
-
22
- /**
23
- * Slice string by code points
24
- */
25
- export function cpSlice(str, start, end) {
26
- const cps = toCodePoints(str);
27
- return cps.slice(start, end).join('');
28
- }
29
-
30
- /**
31
- * Convert logical position (row, col) to offset
32
- */
33
- export function logicalPosToOffset(lines, row, col) {
34
- let offset = 0;
35
- for (let i = 0; i < row && i < lines.length; i++) {
36
- offset += cpLen(lines[i]) + 1; // +1 for newline
37
- }
38
- offset += Math.min(col, cpLen(lines[row] || ''));
39
- return offset;
40
- }
41
-
42
- /**
43
- * Convert offset to logical position
44
- */
45
- export function offsetToLogicalPos(lines, offset) {
46
- let currentOffset = 0;
47
- for (let row = 0; row < lines.length; row++) {
48
- const lineLen = cpLen(lines[row]);
49
- if (currentOffset + lineLen >= offset) {
50
- return [row, offset - currentOffset];
51
- }
52
- currentOffset += lineLen + 1; // +1 for newline
53
- }
54
- const lastRow = Math.max(0, lines.length - 1);
55
- return [lastRow, cpLen(lines[lastRow] || '')];
56
- }
57
-
58
- /**
59
- * TextBuffer class for managing multi-line text input
60
- */
61
- export class TextBuffer {
62
- constructor(options = {}) {
63
- const {
64
- initialText = '',
65
- viewport = { width: 80, height: 10 },
66
- stdin = process.stdin,
67
- setRawMode = null,
68
- onChange = null,
69
- } = options;
70
-
71
- this.lines = initialText ? initialText.split('\n') : [''];
72
- this.cursor = [0, 0]; // [row, col] in logical coordinates
73
- this.viewport = viewport;
74
- this.stdin = stdin;
75
- this.setRawMode = setRawMode;
76
- this.visualScrollRow = 0;
77
- this.onChange = onChange;
78
-
79
- // Visual line mapping
80
- this.visualToLogicalMap = [];
81
- this.allVisualLines = [];
82
-
83
- // Cache for visual cursor to avoid creating new arrays
84
- this._cachedVisualCursor = [0, 0];
85
- this._cachedCursorKey = '0,0';
86
-
87
- // Cache for viewport visual lines
88
- this._cachedViewportLines = [];
89
- this._cachedViewportKey = '';
90
-
91
- // Track previous text for change detection
92
- this._previousText = this.text;
93
-
94
- this.updateVisualLines();
95
- }
96
-
97
- notifyChange() {
98
- if (this.onChange) {
99
- // Only notify if text actually changed, not just cursor position
100
- const currentText = this.text;
101
- if (currentText !== this._previousText) {
102
- this._previousText = currentText;
103
- this.onChange();
104
- }
105
- }
106
- }
107
-
108
- get text() {
109
- return this.lines.join('\n');
110
- }
111
-
112
- get viewportVisualLines() {
113
- const start = this.visualScrollRow;
114
- const end = start + this.viewport.height;
115
- const viewportKey = `${start},${end},${this.allVisualLines.length}`;
116
-
117
- if (viewportKey !== this._cachedViewportKey) {
118
- this._cachedViewportLines = this.allVisualLines.slice(start, end);
119
- this._cachedViewportKey = viewportKey;
120
- }
121
-
122
- return this._cachedViewportLines;
123
- }
124
-
125
- get visualCursor() {
126
- const cursorKey = `${this.cursor[0]},${this.cursor[1]}`;
127
- if (cursorKey !== this._cachedCursorKey) {
128
- this._cachedVisualCursor = this.logicalToVisual(this.cursor);
129
- this._cachedCursorKey = cursorKey;
130
- }
131
- return this._cachedVisualCursor;
132
- }
133
-
134
- /**
135
- * Update visual line wrapping
136
- */
137
- updateVisualLines() {
138
- const newVisualLines = [];
139
- const newVisualToLogicalMap = [];
140
-
141
- for (let logicalRow = 0; logicalRow < this.lines.length; logicalRow++) {
142
- const line = this.lines[logicalRow];
143
- const wrapped = this.wrapLine(line);
144
-
145
- for (let i = 0; i < wrapped.length; i++) {
146
- newVisualLines.push(wrapped[i]);
147
- newVisualToLogicalMap.push([logicalRow, i * this.viewport.width]);
148
- }
149
- }
150
-
151
- // Only update if something changed
152
- let changed = false;
153
- if (newVisualLines.length !== this.allVisualLines.length) {
154
- changed = true;
155
- } else {
156
- for (let i = 0; i < newVisualLines.length; i++) {
157
- if (newVisualLines[i] !== this.allVisualLines[i]) {
158
- changed = true;
159
- break;
160
- }
161
- }
162
- }
163
-
164
- if (changed) {
165
- this.allVisualLines = newVisualLines;
166
- this.visualToLogicalMap = newVisualToLogicalMap;
167
- // Invalidate viewport cache when visual lines change
168
- this._cachedViewportKey = '';
169
- this._cachedCursorKey = '';
170
- }
171
-
172
- // Ensure cursor is visible
173
- const [visualRow] = this.visualCursor;
174
- if (visualRow < this.visualScrollRow) {
175
- this.visualScrollRow = visualRow;
176
- } else if (visualRow >= this.visualScrollRow + this.viewport.height) {
177
- this.visualScrollRow = visualRow - this.viewport.height + 1;
178
- }
179
- }
180
-
181
-
182
- /**
183
- * Wrap a line to viewport width
184
- */
185
- wrapLine(line) {
186
- if (!line) return [''];
187
-
188
- const result = [];
189
- const cps = toCodePoints(line);
190
- let currentLine = '';
191
- let currentWidth = 0;
192
-
193
- for (const cp of cps) {
194
- const cpWidth = stringWidth(cp);
195
- if (currentWidth + cpWidth > this.viewport.width) {
196
- result.push(currentLine);
197
- currentLine = cp;
198
- currentWidth = cpWidth;
199
- } else {
200
- currentLine += cp;
201
- currentWidth += cpWidth;
202
- }
203
- }
204
-
205
- result.push(currentLine);
206
- return result.length > 0 ? result : [''];
207
- }
208
-
209
- /**
210
- * Convert logical cursor to visual cursor
211
- */
212
- logicalToVisual(logicalCursor) {
213
- const [row, col] = logicalCursor;
214
- const line = this.lines[row] || '';
215
- const wrappedLines = this.wrapLine(line);
216
-
217
- let remainingCol = col;
218
- let visualRow = 0;
219
-
220
- // Count visual rows before this logical row
221
- for (let i = 0; i < row; i++) {
222
- visualRow += this.wrapLine(this.lines[i]).length;
223
- }
224
-
225
- // Find position within wrapped lines
226
- for (let i = 0; i < wrappedLines.length; i++) {
227
- const lineLen = cpLen(wrappedLines[i]);
228
- if (remainingCol <= lineLen) {
229
- return [visualRow + i, remainingCol];
230
- }
231
- remainingCol -= lineLen;
232
- }
233
-
234
- return [visualRow + wrappedLines.length - 1, cpLen(wrappedLines[wrappedLines.length - 1])];
235
- }
236
-
237
- /**
238
- * Set text content
239
- */
240
- setText(newText) {
241
- this.lines = newText ? newText.split('\n') : [''];
242
- this.cursor = [0, 0];
243
- this.updateVisualLines();
244
- this.notifyChange();
245
- }
246
-
247
- /**
248
- * Handle keyboard input
249
- * Only handles regular character input, not special keys
250
- */
251
- handleInput(key) {
252
- // Only handle regular text input (not control characters)
253
- if (key.sequence && key.sequence.length > 0 && !key.ctrl && !key.meta) {
254
- // Handle paste with newlines (check for \n, \r\n, or \r)
255
- if (key.sequence.includes('\n') || key.sequence.includes('\r')) {
256
- // Normalize line endings to \n
257
- const normalized = key.sequence.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
258
- this.insertMultilineText(normalized);
259
- return;
260
- }
261
-
262
- // Filter out control characters
263
- const isPrintable = key.sequence.split('').every(char => {
264
- const code = char.charCodeAt(0);
265
- // Allow printable ASCII (32-126) and extended characters (>= 128)
266
- return (code >= 32 && code <= 126) || code >= 128;
267
- });
268
-
269
- if (isPrintable) {
270
- this.insertText(key.sequence);
271
- }
272
- }
273
- }
274
-
275
- /**
276
- * Insert text at cursor position
277
- */
278
- insertText(text) {
279
- const [row, col] = this.cursor;
280
- const line = this.lines[row] || '';
281
- const before = cpSlice(line, 0, col);
282
- const after = cpSlice(line, col);
283
-
284
- this.lines[row] = before + text + after;
285
- this.cursor = [row, col + cpLen(text)];
286
- this.updateVisualLines();
287
- this.notifyChange();
288
- }
289
-
290
- /**
291
- * Insert multiline text at cursor position
292
- */
293
- insertMultilineText(text) {
294
- const [row, col] = this.cursor;
295
- const currentLine = this.lines[row] || '';
296
- const before = cpSlice(currentLine, 0, col);
297
- const after = cpSlice(currentLine, col);
298
-
299
- const newLines = text.split('\n');
300
-
301
- if (newLines.length === 1) {
302
- // Single line, use regular insert
303
- this.insertText(text);
304
- return;
305
- }
306
-
307
- // First line: append to current line before cursor
308
- this.lines[row] = before + newLines[0];
309
-
310
- // Middle lines: insert as new lines
311
- for (let i = 1; i < newLines.length - 1; i++) {
312
- this.lines.splice(row + i, 0, newLines[i]);
313
- }
314
-
315
- // Last line: prepend to text after cursor
316
- const lastLine = newLines[newLines.length - 1];
317
- this.lines.splice(row + newLines.length - 1, 0, lastLine + after);
318
-
319
- // Update cursor to end of pasted text
320
- this.cursor = [row + newLines.length - 1, cpLen(lastLine)];
321
- this.updateVisualLines();
322
- this.notifyChange();
323
- }
324
-
325
- /**
326
- * Insert newline
327
- */
328
- newline() {
329
- const [row, col] = this.cursor;
330
- const line = this.lines[row] || '';
331
- const before = cpSlice(line, 0, col);
332
- const after = cpSlice(line, col);
333
-
334
- this.lines[row] = before;
335
- this.lines.splice(row + 1, 0, after);
336
- this.cursor = [row + 1, 0];
337
- this.updateVisualLines();
338
- this.notifyChange();
339
- }
340
-
341
- /**
342
- * Backspace
343
- * Safely handles edge cases and ensures proper cursor positioning
344
- */
345
- backspace() {
346
- const [row, col] = this.cursor;
347
-
348
- // Ensure we have valid lines array
349
- if (!this.lines || this.lines.length === 0) {
350
- this.lines = [''];
351
- this.cursor = [0, 0];
352
- this.updateVisualLines();
353
-
354
- return;
355
- }
356
-
357
- // Case 1: Delete character within line
358
- if (col > 0) {
359
- const line = this.lines[row] || '';
360
- const before = cpSlice(line, 0, col - 1);
361
- const after = cpSlice(line, col);
362
- this.lines[row] = before + after;
363
- this.cursor = [row, col - 1];
364
- }
365
- // Case 2: Merge with previous line
366
- else if (row > 0) {
367
- const currentLine = this.lines[row] || '';
368
- const prevLine = this.lines[row - 1] || '';
369
- const prevLineLen = cpLen(prevLine);
370
-
371
- // Merge lines
372
- this.lines[row - 1] = prevLine + currentLine;
373
- this.lines.splice(row, 1);
374
-
375
- // Move cursor to end of previous line
376
- this.cursor = [row - 1, prevLineLen];
377
- }
378
- // Case 3: Already at start of first line - do nothing
379
- else {
380
- // At position [0, 0], cannot backspace further
381
- this.updateVisualLines();
382
- return;
383
- }
384
-
385
- this.updateVisualLines();
386
- this.notifyChange();
387
- }
388
-
389
- /**
390
- * Delete character at cursor
391
- * Safely handles edge cases
392
- */
393
- delete() {
394
- const [row, col] = this.cursor;
395
-
396
- // Ensure we have valid lines array
397
- if (!this.lines || this.lines.length === 0) {
398
- this.lines = [''];
399
- this.cursor = [0, 0];
400
- this.updateVisualLines();
401
-
402
- return;
403
- }
404
-
405
- const line = this.lines[row] || '';
406
- const lineLen = cpLen(line);
407
-
408
- // Case 1: Delete character within line
409
- if (col < lineLen) {
410
- const before = cpSlice(line, 0, col);
411
- const after = cpSlice(line, col + 1);
412
- this.lines[row] = before + after;
413
- }
414
- // Case 2: Merge with next line
415
- else if (row < this.lines.length - 1) {
416
- const nextLine = this.lines[row + 1] || '';
417
- this.lines[row] = line + nextLine;
418
- this.lines.splice(row + 1, 1);
419
- }
420
- // Case 3: At end of last line - do nothing
421
- else {
422
- // Already at end, cannot delete further
423
- this.updateVisualLines();
424
- return;
425
- }
426
-
427
- this.updateVisualLines();
428
- this.notifyChange();
429
- }
430
-
431
- /**
432
- * Move cursor
433
- */
434
- move(direction) {
435
- const [row, col] = this.cursor;
436
- const line = this.lines[row] || '';
437
- const oldCursor = [...this.cursor];
438
-
439
- switch (direction) {
440
- case 'left':
441
- if (col > 0) {
442
- this.cursor = [row, col - 1];
443
- } else if (row > 0) {
444
- this.cursor = [row - 1, cpLen(this.lines[row - 1] || '')];
445
- }
446
- break;
447
-
448
- case 'right':
449
- if (col < cpLen(line)) {
450
- this.cursor = [row, col + 1];
451
- } else if (row < this.lines.length - 1) {
452
- this.cursor = [row + 1, 0];
453
- }
454
- break;
455
-
456
- case 'up':
457
- if (row > 0) {
458
- const prevLineLen = cpLen(this.lines[row - 1] || '');
459
- this.cursor = [row - 1, Math.min(col, prevLineLen)];
460
- }
461
- break;
462
-
463
- case 'down':
464
- if (row < this.lines.length - 1) {
465
- const nextLineLen = cpLen(this.lines[row + 1] || '');
466
- this.cursor = [row + 1, Math.min(col, nextLineLen)];
467
- }
468
- break;
469
-
470
- case 'home':
471
- this.cursor = [row, 0];
472
- break;
473
-
474
- case 'end':
475
- this.cursor = [row, cpLen(line)];
476
- break;
477
- }
478
-
479
- const cursorChanged = oldCursor[0] !== this.cursor[0] || oldCursor[1] !== this.cursor[1];
480
-
481
- if (cursorChanged) {
482
- this.updateVisualLines();
483
- // Always notify for cursor changes - needed for display update
484
- this.notifyChange();
485
- }
486
- }
487
-
488
- /**
489
- * Move to offset position
490
- */
491
- moveToOffset(offset) {
492
- this.cursor = offsetToLogicalPos(this.lines, offset);
493
- this.updateVisualLines();
494
-
495
- }
496
-
497
- /**
498
- * Replace range by offset
499
- */
500
- replaceRangeByOffset(startOffset, endOffset, replacement) {
501
- const fullText = this.text;
502
- const cps = toCodePoints(fullText);
503
- const before = cps.slice(0, startOffset).join('');
504
- const after = cps.slice(endOffset).join('');
505
-
506
- this.setText(before + replacement + after);
507
- this.moveToOffset(startOffset + cpLen(replacement));
508
- }
509
-
510
- /**
511
- * Kill line right (Ctrl+K)
512
- */
513
- killLineRight() {
514
- const [row, col] = this.cursor;
515
- const line = this.lines[row] || '';
516
- this.lines[row] = cpSlice(line, 0, col);
517
- this.updateVisualLines();
518
- this.notifyChange();
519
- }
520
-
521
- /**
522
- * Kill line left (Ctrl+U)
523
- */
524
- killLineLeft() {
525
- const [row, col] = this.cursor;
526
- const line = this.lines[row] || '';
527
- this.lines[row] = cpSlice(line, col);
528
- this.cursor = [row, 0];
529
- this.updateVisualLines();
530
- this.notifyChange();
531
- }
532
-
533
- /**
534
- * Delete word left (Ctrl+W)
535
- */
536
- deleteWordLeft() {
537
- const [row, col] = this.cursor;
538
- if (col === 0) return;
539
-
540
- const line = this.lines[row] || '';
541
- const before = cpSlice(line, 0, col);
542
- const after = cpSlice(line, col);
543
-
544
- // Find word boundary
545
- const trimmed = before.trimEnd();
546
- const lastSpace = trimmed.lastIndexOf(' ');
547
- const newCol = lastSpace >= 0 ? lastSpace + 1 : 0;
548
-
549
- this.lines[row] = cpSlice(line, 0, newCol) + after;
550
- this.cursor = [row, newCol];
551
- this.updateVisualLines();
552
- this.notifyChange();
553
- }
554
-
555
- /**
556
- * Open in external editor (stub for now)
557
- */
558
- openInExternalEditor() {
559
- consolelog('External editor not implemented yet');
560
- }
561
- }
562
-
563
- /**
564
- * Hook to create and manage text buffer using useReducer
565
- */
566
- import { useReducer, useMemo, useCallback, useState, useEffect, useRef } from 'react';
567
-
568
- function textBufferReducer(state, action) {
569
- switch (action.type) {
570
- case 'set_text': {
571
- const newLines = action.payload ? action.payload.split('\n') : [''];
572
- return {
573
- ...state,
574
- lines: newLines,
575
- cursor: [0, 0]
576
- };
577
- }
578
- case 'insert_text': {
579
- const { text } = action.payload;
580
- const [row, col] = state.cursor;
581
- const newLines = [...state.lines];
582
- const line = newLines[row] || '';
583
- const before = cpSlice(line, 0, col);
584
- const after = cpSlice(line, col);
585
- newLines[row] = before + text + after;
586
- return {
587
- ...state,
588
- lines: newLines,
589
- cursor: [row, col + cpLen(text)]
590
- };
591
- }
592
- case 'move_cursor': {
593
- const { direction } = action.payload;
594
- const [row, col] = state.cursor;
595
- const line = state.lines[row] || '';
596
- let newCursor = [row, col];
597
-
598
- switch (direction) {
599
- case 'left':
600
- if (col > 0) {
601
- newCursor = [row, col - 1];
602
- } else if (row > 0) {
603
- newCursor = [row - 1, cpLen(state.lines[row - 1] || '')];
604
- }
605
- break;
606
- case 'right':
607
- if (col < cpLen(line)) {
608
- newCursor = [row, col + 1];
609
- } else if (row < state.lines.length - 1) {
610
- newCursor = [row + 1, 0];
611
- }
612
- break;
613
- case 'up':
614
- if (row > 0) {
615
- const prevLineLen = cpLen(state.lines[row - 1] || '');
616
- newCursor = [row - 1, Math.min(col, prevLineLen)];
617
- }
618
- break;
619
- case 'down':
620
- if (row < state.lines.length - 1) {
621
- const nextLineLen = cpLen(state.lines[row + 1] || '');
622
- newCursor = [row + 1, Math.min(col, nextLineLen)];
623
- }
624
- break;
625
- case 'home':
626
- newCursor = [row, 0];
627
- break;
628
- case 'end':
629
- newCursor = [row, cpLen(line)];
630
- break;
631
- }
632
-
633
- return {
634
- ...state,
635
- cursor: newCursor
636
- };
637
- }
638
- case 'backspace': {
639
- const [row, col] = state.cursor;
640
- if (col === 0 && row === 0) return state;
641
-
642
- const newLines = [...state.lines];
643
- let newCursor = [row, col];
644
-
645
- if (col > 0) {
646
- const line = newLines[row] || '';
647
- newLines[row] = cpSlice(line, 0, col - 1) + cpSlice(line, col);
648
- newCursor = [row, col - 1];
649
- } else if (row > 0) {
650
- const currentLine = newLines[row] || '';
651
- const prevLine = newLines[row - 1] || '';
652
- const prevLineLen = cpLen(prevLine);
653
- newLines[row - 1] = prevLine + currentLine;
654
- newLines.splice(row, 1);
655
- newCursor = [row - 1, prevLineLen];
656
- }
657
-
658
- return {
659
- ...state,
660
- lines: newLines,
661
- cursor: newCursor
662
- };
663
- }
664
- case 'delete': {
665
- const [row, col] = state.cursor;
666
- const newLines = [...state.lines];
667
- const line = newLines[row] || '';
668
- const lineLen = cpLen(line);
669
-
670
- if (col < lineLen) {
671
- newLines[row] = cpSlice(line, 0, col) + cpSlice(line, col + 1);
672
- } else if (row < state.lines.length - 1) {
673
- const nextLine = newLines[row + 1] || '';
674
- newLines[row] = line + nextLine;
675
- newLines.splice(row + 1, 1);
676
- } else {
677
- return state;
678
- }
679
-
680
- return {
681
- ...state,
682
- lines: newLines
683
- };
684
- }
685
- case 'newline': {
686
- const [row, col] = state.cursor;
687
- const newLines = [...state.lines];
688
- const line = newLines[row] || '';
689
- const before = cpSlice(line, 0, col);
690
- const after = cpSlice(line, col);
691
- newLines[row] = before;
692
- newLines.splice(row + 1, 0, after);
693
-
694
- return {
695
- ...state,
696
- lines: newLines,
697
- cursor: [row + 1, 0]
698
- };
699
- }
700
- case 'kill_line_right': {
701
- const [row, col] = state.cursor;
702
- const newLines = [...state.lines];
703
- const line = newLines[row] || '';
704
- newLines[row] = cpSlice(line, 0, col);
705
- return {
706
- ...state,
707
- lines: newLines
708
- };
709
- }
710
- case 'kill_line_left': {
711
- const [row, col] = state.cursor;
712
- const newLines = [...state.lines];
713
- const line = newLines[row] || '';
714
- newLines[row] = cpSlice(line, col);
715
- return {
716
- ...state,
717
- lines: newLines,
718
- cursor: [row, 0]
719
- };
720
- }
721
- case 'delete_word_left': {
722
- const [row, col] = state.cursor;
723
- if (col === 0) return state;
724
-
725
- const newLines = [...state.lines];
726
- const line = newLines[row] || '';
727
- const before = cpSlice(line, 0, col);
728
- const after = cpSlice(line, col);
729
-
730
- const trimmed = before.trimEnd();
731
- const lastSpace = trimmed.lastIndexOf(' ');
732
- const newCol = lastSpace >= 0 ? lastSpace + 1 : 0;
733
-
734
- newLines[row] = cpSlice(line, 0, newCol) + after;
735
-
736
- return {
737
- ...state,
738
- lines: newLines,
739
- cursor: [row, newCol]
740
- };
741
- }
742
- case 'set_viewport': {
743
- return {
744
- ...state,
745
- viewport: action.payload
746
- };
747
- }
748
- default:
749
- return state;
750
- }
751
- }
752
-
753
- export function useTextBuffer(options) {
754
- const { initialText = '', viewport } = options;
755
-
756
- const initialState = useMemo(() => ({
757
- lines: initialText ? initialText.split('\n') : [''],
758
- cursor: [0, 0],
759
- viewport: viewport || { width: 80, height: 10 }
760
- }), [initialText, viewport]);
761
-
762
- const [state, dispatch] = useReducer(textBufferReducer, initialState);
763
-
764
- // Update viewport when it changes
765
- useEffect(() => {
766
- if (viewport && (viewport.width !== state.viewport.width || viewport.height !== state.viewport.height)) {
767
- dispatch({ type: 'set_viewport', payload: viewport });
768
- }
769
- }, [viewport, state.viewport]);
770
-
771
- // Calculate visual layout
772
- const visualLayout = useMemo(() => {
773
- const allVisualLines = [];
774
- const visualToLogicalMap = [];
775
- const viewportWidth = state.viewport.width;
776
-
777
- state.lines.forEach((line, logicalRow) => {
778
- if (!line) {
779
- allVisualLines.push('');
780
- visualToLogicalMap.push([logicalRow, 0]);
781
- } else {
782
- const wrapped = wrapLine(line, viewportWidth);
783
- wrapped.forEach((segment, i) => {
784
- allVisualLines.push(segment);
785
- visualToLogicalMap.push([logicalRow, i * viewportWidth]);
786
- });
787
- }
788
- });
789
-
790
- return { allVisualLines, visualToLogicalMap };
791
- }, [state.lines, state.viewport.width]);
792
-
793
- // Calculate visual cursor
794
- const visualCursor = useMemo(() => {
795
- const [row, col] = state.cursor;
796
- const line = state.lines[row] || '';
797
- const wrappedLines = wrapLine(line, state.viewport.width);
798
-
799
- let remainingCol = col;
800
- let visualRow = 0;
801
-
802
- for (let i = 0; i < row; i++) {
803
- visualRow += wrapLine(state.lines[i], state.viewport.width).length;
804
- }
805
-
806
- for (let i = 0; i < wrappedLines.length; i++) {
807
- const lineLen = cpLen(wrappedLines[i]);
808
- if (remainingCol <= lineLen) {
809
- return [visualRow + i, remainingCol];
810
- }
811
- remainingCol -= lineLen;
812
- }
813
-
814
- return [visualRow + wrappedLines.length - 1, cpLen(wrappedLines[wrappedLines.length - 1])];
815
- }, [state.cursor, state.lines, state.viewport.width]);
816
-
817
- const [visualScrollRow, setVisualScrollRow] = useState(0);
818
-
819
- // Update scroll position - memoize the calculation to avoid unnecessary state updates
820
- const computedScrollRow = useMemo(() => {
821
- const { height } = state.viewport;
822
- const totalVisualLines = visualLayout.allVisualLines.length;
823
- const maxScrollStart = Math.max(0, totalVisualLines - height);
824
- let newVisualScrollRow = visualScrollRow;
825
-
826
- // Only adjust scroll if cursor is out of viewport
827
- if (visualCursor[0] < visualScrollRow) {
828
- newVisualScrollRow = visualCursor[0];
829
- } else if (visualCursor[0] >= visualScrollRow + height) {
830
- newVisualScrollRow = visualCursor[0] - height + 1;
831
- }
832
-
833
- newVisualScrollRow = Math.min(Math.max(newVisualScrollRow, 0), maxScrollStart);
834
- return newVisualScrollRow;
835
- }, [visualCursor, visualScrollRow, state.viewport.height, visualLayout.allVisualLines.length]);
836
-
837
- // Only update state when scroll actually changes
838
- useEffect(() => {
839
- if (computedScrollRow !== visualScrollRow) {
840
- setVisualScrollRow(computedScrollRow);
841
- }
842
- }, [computedScrollRow, visualScrollRow]);
843
-
844
- // Create stable callbacks with useCallback
845
- const setText = useCallback((text) => {
846
- dispatch({ type: 'set_text', payload: text });
847
- }, []);
848
-
849
- const insertText = useCallback((text) => {
850
- dispatch({ type: 'insert_text', payload: { text } });
851
- }, []);
852
-
853
- const move = useCallback((direction) => {
854
- dispatch({ type: 'move_cursor', payload: { direction } });
855
- }, []);
856
-
857
- const backspace = useCallback(() => {
858
- dispatch({ type: 'backspace' });
859
- }, []);
860
-
861
- const deleteChar = useCallback(() => {
862
- dispatch({ type: 'delete' });
863
- }, []);
864
-
865
- const newline = useCallback(() => {
866
- dispatch({ type: 'newline' });
867
- }, []);
868
-
869
- const killLineRight = useCallback(() => {
870
- dispatch({ type: 'kill_line_right' });
871
- }, []);
872
-
873
- const killLineLeft = useCallback(() => {
874
- dispatch({ type: 'kill_line_left' });
875
- }, []);
876
-
877
- const deleteWordLeft = useCallback(() => {
878
- dispatch({ type: 'delete_word_left' });
879
- }, []);
880
-
881
- const handleInput = useCallback((key) => {
882
- if (key.sequence && key.sequence.length > 0 && !key.ctrl && !key.meta) {
883
- if (key.sequence.includes('\n') || key.sequence.includes('\r')) {
884
- const normalized = key.sequence.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
885
- const parts = normalized.split('\n');
886
- parts.forEach((part, i) => {
887
- if (part) dispatch({ type: 'insert_text', payload: { text: part } });
888
- if (i < parts.length - 1) dispatch({ type: 'newline' });
889
- });
890
- return;
891
- }
892
-
893
- const isPrintable = key.sequence.split('').every(char => {
894
- const code = char.charCodeAt(0);
895
- return (code >= 32 && code <= 126) || code >= 128;
896
- });
897
-
898
- if (isPrintable) {
899
- dispatch({ type: 'insert_text', payload: { text: key.sequence } });
900
- }
901
- }
902
- }, []);
903
-
904
- const text = useMemo(() => state.lines.join('\n'), [state.lines]);
905
-
906
- const viewportVisualLines = useMemo(() => {
907
- return visualLayout.allVisualLines.slice(visualScrollRow, visualScrollRow + state.viewport.height);
908
- }, [visualLayout.allVisualLines, visualScrollRow, state.viewport.height]);
909
-
910
- // Return stable buffer object using useRef to maintain reference identity
911
- const bufferRef = useRef(null);
912
-
913
- if (!bufferRef.current) {
914
- bufferRef.current = {
915
- get lines() { return state.lines; },
916
- get cursor() { return state.cursor; },
917
- get text() { return text; },
918
- get allVisualLines() { return visualLayout.allVisualLines; },
919
- get viewportVisualLines() { return viewportVisualLines; },
920
- get visualCursor() { return visualCursor; },
921
- get visualScrollRow() { return visualScrollRow; },
922
- get visualToLogicalMap() { return visualLayout.visualToLogicalMap; },
923
- get viewport() { return state.viewport; },
924
- setText,
925
- insertText,
926
- move,
927
- backspace,
928
- delete: deleteChar,
929
- newline,
930
- killLineRight,
931
- killLineLeft,
932
- deleteWordLeft,
933
- handleInput
934
- };
935
- } else {
936
- // Update getters to return current values without changing object reference
937
- Object.defineProperties(bufferRef.current, {
938
- lines: { get: () => state.lines, enumerable: true, configurable: true },
939
- cursor: { get: () => state.cursor, enumerable: true, configurable: true },
940
- text: { get: () => text, enumerable: true, configurable: true },
941
- allVisualLines: { get: () => visualLayout.allVisualLines, enumerable: true, configurable: true },
942
- viewportVisualLines: { get: () => viewportVisualLines, enumerable: true, configurable: true },
943
- visualCursor: { get: () => visualCursor, enumerable: true, configurable: true },
944
- visualScrollRow: { get: () => visualScrollRow, enumerable: true, configurable: true },
945
- visualToLogicalMap: { get: () => visualLayout.visualToLogicalMap, enumerable: true, configurable: true },
946
- viewport: { get: () => state.viewport, enumerable: true, configurable: true }
947
- });
948
- }
949
-
950
- return bufferRef.current;
951
- }
952
-
953
- function wrapLine(line, viewportWidth) {
954
- if (!line) return [''];
955
-
956
- const result = [];
957
- const cps = toCodePoints(line);
958
- let currentLine = '';
959
- let currentWidth = 0;
960
-
961
- for (const cp of cps) {
962
- const cpWidth = stringWidth(cp);
963
- if (currentWidth + cpWidth > viewportWidth) {
964
- result.push(currentLine);
965
- currentLine = cp;
966
- currentWidth = cpWidth;
967
- } else {
968
- currentLine += cp;
969
- currentWidth += cpWidth;
970
- }
971
- }
972
-
973
- result.push(currentLine);
974
- return result.length > 0 ? result : [''];
975
- }