@rind-ai/cli 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bin/rind.js +5 -5
  2. package/lib/assistant-renderer.js +179 -265
  3. package/lib/choice-menu-state.js +46 -46
  4. package/lib/cli-input-actions.js +548 -0
  5. package/lib/cli-output-controller.js +460 -0
  6. package/lib/cli-runtime-controller.js +350 -0
  7. package/lib/cli-state-store.js +32 -0
  8. package/lib/cli-state.js +41 -0
  9. package/lib/command-controller.js +159 -126
  10. package/lib/compact-context-state.js +22 -22
  11. package/lib/components/assistant-message.js +169 -0
  12. package/lib/components/composer-area.js +25 -0
  13. package/lib/components/dynamic-block.js +20 -0
  14. package/lib/components/monitor-stack.js +35 -0
  15. package/lib/components/text-block.js +47 -0
  16. package/lib/components/tool-block.js +122 -0
  17. package/lib/composer-terminal.js +224 -203
  18. package/lib/event-controller.js +243 -242
  19. package/lib/frontend-cli-implementation.js +656 -1111
  20. package/lib/input-controller.js +75 -94
  21. package/lib/input-errors.js +3 -3
  22. package/lib/interrupt-state.js +9 -9
  23. package/lib/line-editor.js +541 -541
  24. package/lib/local-slash-commands.js +217 -0
  25. package/lib/markdown-lines.js +103 -0
  26. package/lib/model-menu-state.js +50 -50
  27. package/lib/one-shot-progress.js +145 -0
  28. package/lib/one-shot.js +228 -0
  29. package/lib/question-menu-state.js +61 -0
  30. package/lib/rendering.js +1309 -1060
  31. package/lib/runtime-client.js +241 -193
  32. package/lib/runtime-env.js +21 -21
  33. package/lib/runtime-protocol.js +122 -15
  34. package/lib/slash-command-mode.js +0 -11
  35. package/lib/slash-menu-state.js +59 -59
  36. package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
  37. package/lib/terminal-key.js +97 -97
  38. package/lib/text-width.js +335 -151
  39. package/lib/theme-menu-state.js +31 -0
  40. package/lib/theme.js +134 -0
  41. package/lib/tool-display.js +675 -0
  42. package/lib/tui/component.js +55 -0
  43. package/lib/tui/cursor.js +29 -0
  44. package/lib/tui/input-buffer.js +172 -0
  45. package/lib/tui/tui.js +591 -0
  46. package/lib/turn-controller.js +68 -78
  47. package/package.json +28 -28
  48. package/lib/assistant-stream-buffer.js +0 -25
  49. package/lib/terminal-ui.js +0 -581
@@ -1,541 +1,541 @@
1
- import { graphemes, textWidth, wrapTextCells } from "./text-width.js";
2
-
3
- const HISTORY_LIMIT = 100;
4
- const UNDO_LIMIT = 100;
5
- const INPUT_PREFIX_WIDTH = 4;
6
-
7
- export function createLineEditor(initialValue = "") {
8
- let lines = splitLines(initialValue);
9
- let cursorLine = lines.length - 1;
10
- let cursorColumn = graphemes(lines[cursorLine]).length;
11
- const history = [];
12
- let historyIndex = -1;
13
- let historyDraft = null;
14
- const undoStack = [];
15
- let killedText = "";
16
- let viewportWidth = 80;
17
- let preferredVisualColumn = null;
18
-
19
- const editor = {
20
- input() {
21
- return lines.join("\n");
22
- },
23
- cursorPosition() {
24
- return { line: cursorLine, column: cursorColumn };
25
- },
26
- setInput(value) {
27
- setText(value);
28
- },
29
- setViewportWidth(value) {
30
- const width = Math.max(1, Math.floor(Number(value) || 0));
31
- if (width !== viewportWidth) {
32
- viewportWidth = width;
33
- preferredVisualColumn = null;
34
- }
35
- },
36
- handleInput(event = {}) {
37
- if (event.kind === "paste") {
38
- insertText(cleanPaste(event.text));
39
- return "edit";
40
- }
41
- if (event.kind === "text") {
42
- insertText(event.text);
43
- return "edit";
44
- }
45
- return editor.handleKey(event.text || "", event);
46
- },
47
- handleKey(chunk, key = {}) {
48
- if (key.name !== "up" && key.name !== "down") {
49
- preferredVisualColumn = null;
50
- }
51
- if (key.name === "enter" || key.name === "return") {
52
- if (key.shift || key.ctrl) {
53
- insertNewline();
54
- return "edit";
55
- }
56
- return "submit";
57
- }
58
- if (key.name === "left") {
59
- return key.ctrl || key.alt ? moveWord(-1) : moveHorizontal(-1);
60
- }
61
- if (key.name === "right") {
62
- return key.ctrl || key.alt ? moveWord(1) : moveHorizontal(1);
63
- }
64
- if (key.ctrl && key.name === "b") {
65
- return moveHorizontal(-1);
66
- }
67
- if (key.ctrl && key.name === "f") {
68
- return moveHorizontal(1);
69
- }
70
- if (key.alt && key.name === "b") {
71
- return moveWord(-1);
72
- }
73
- if (key.alt && key.name === "f") {
74
- return moveWord(1);
75
- }
76
- if (key.name === "up") {
77
- if (key.ctrl || key.alt || key.shift) {
78
- return "";
79
- }
80
- return moveUp();
81
- }
82
- if (key.name === "down") {
83
- if (key.ctrl || key.alt || key.shift) {
84
- return "";
85
- }
86
- return moveDown();
87
- }
88
- if (key.name === "home") {
89
- cursorColumn = 0;
90
- return "move";
91
- }
92
- if (key.name === "end") {
93
- cursorColumn = lineLength();
94
- return "move";
95
- }
96
- if (key.name === "backspace") {
97
- return key.ctrl || key.alt ? deleteWordBackward() : deleteBackward();
98
- }
99
- if (key.name === "delete") {
100
- return key.ctrl || key.alt ? deleteWordForward() : deleteForward();
101
- }
102
- if (key.ctrl && key.name === "a") {
103
- cursorColumn = 0;
104
- return "move";
105
- }
106
- if (key.ctrl && key.name === "e") {
107
- cursorColumn = lineLength();
108
- return "move";
109
- }
110
- if (key.ctrl && key.name === "w") {
111
- return deleteWordBackward();
112
- }
113
- if (key.ctrl && key.name === "d") {
114
- return deleteForward();
115
- }
116
- if (key.alt && key.name === "d") {
117
- return deleteWordForward();
118
- }
119
- if (key.ctrl && key.name === "u") {
120
- return deleteToStart();
121
- }
122
- if (key.ctrl && key.name === "k") {
123
- return deleteToEnd();
124
- }
125
- if (key.ctrl && key.name === "y") {
126
- return yank();
127
- }
128
- if (key.ctrl && (key.name === "-" || key.name === "_")) {
129
- return undo();
130
- }
131
- if (key.name === "j" && key.ctrl) {
132
- insertNewline();
133
- return "edit";
134
- }
135
- if (isPrintable(chunk, key)) {
136
- insertText(chunk);
137
- return "edit";
138
- }
139
- return "";
140
- },
141
- addToHistory(value = "") {
142
- const text = String(value || "").trim();
143
- if (!text || history[0] === text) {
144
- resetHistory();
145
- return;
146
- }
147
- history.unshift(text);
148
- if (history.length > HISTORY_LIMIT) {
149
- history.length = HISTORY_LIMIT;
150
- }
151
- resetHistory();
152
- },
153
- };
154
- return editor;
155
-
156
- function setText(value) {
157
- lines = splitLines(value);
158
- cursorLine = lines.length - 1;
159
- cursorColumn = lineLength();
160
- preferredVisualColumn = null;
161
- resetHistory();
162
- undoStack.length = 0;
163
- }
164
-
165
- function splitLines(value) {
166
- const result = normalizeText(value).split("\n");
167
- return result.length ? result : [""];
168
- }
169
-
170
- function normalizeText(value) {
171
- return String(value || "").replace(/\r\n?/g, "\n").replace(/\t/g, " ");
172
- }
173
-
174
- function cleanPaste(value) {
175
- return normalizeText(value).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
176
- }
177
-
178
- function lineLength(line = cursorLine) {
179
- return graphemes(lines[line] || "").length;
180
- }
181
-
182
- function isEditorEmpty() {
183
- return lines.length === 1 && lines[0] === "";
184
- }
185
-
186
- function moveHorizontal(delta) {
187
- if (delta < 0 && cursorColumn === 0 && cursorLine > 0) {
188
- cursorLine -= 1;
189
- cursorColumn = lineLength();
190
- return "move";
191
- }
192
- if (delta > 0 && cursorColumn === lineLength() && cursorLine < lines.length - 1) {
193
- cursorLine += 1;
194
- cursorColumn = 0;
195
- return "move";
196
- }
197
- cursorColumn = Math.max(0, Math.min(lineLength(), cursorColumn + delta));
198
- return "move";
199
- }
200
-
201
- function moveUp() {
202
- const visual = visualLineState();
203
- if (visual.index > 0) {
204
- return moveVertical(-1, visual);
205
- }
206
- if (isEditorEmpty() || historyIndex > -1 || cursorColumn === 0) {
207
- return navigateHistory(-1);
208
- }
209
- return moveToLineStart();
210
- }
211
-
212
- function moveDown() {
213
- const visual = visualLineState();
214
- if (visual.index < visual.lines.length - 1) {
215
- return moveVertical(1, visual);
216
- }
217
- if (historyIndex > -1) {
218
- return navigateHistory(1);
219
- }
220
- return moveToLineEnd();
221
- }
222
-
223
- function moveVertical(delta, visual) {
224
- const current = visual.lines[visual.index];
225
- const targetIndex = visual.index + delta;
226
- const target = visual.lines[targetIndex];
227
- const currentColumn = visualColumn(current, cursorColumn - current.startColumn);
228
- const desiredColumn = preferredVisualColumn ?? currentColumn;
229
- const targetOffset = visualOffset(target, desiredColumn);
230
- const targetColumn = visualColumn(target, targetOffset);
231
- cursorLine = target.line;
232
- cursorColumn = target.startColumn + targetOffset;
233
- preferredVisualColumn = targetColumn === desiredColumn ? null : desiredColumn;
234
- return "move";
235
- }
236
-
237
- function visualLineState() {
238
- const visualLines = [];
239
- const contentWidth = Math.max(1, viewportWidth - INPUT_PREFIX_WIDTH);
240
- for (const [lineIndex, text] of lines.entries()) {
241
- const chunks = wrapTextCells(text, contentWidth, contentWidth);
242
- for (const chunk of chunks) {
243
- visualLines.push({
244
- line: lineIndex,
245
- startColumn: chunk.startColumn,
246
- length: chunk.length,
247
- allowsEnd: chunk.allowsEnd,
248
- text: chunk.text,
249
- });
250
- }
251
- const lastChunk = chunks.at(-1);
252
- if (lineIndex === lines.length - 1 && !lastChunk.allowsEnd) {
253
- visualLines.push({
254
- line: lineIndex,
255
- startColumn: lastChunk.startColumn + lastChunk.length,
256
- length: 0,
257
- allowsEnd: true,
258
- text: "",
259
- });
260
- }
261
- }
262
-
263
- let index = visualLines.length - 1;
264
- for (let candidate = 0; candidate < visualLines.length; candidate += 1) {
265
- const visualLine = visualLines[candidate];
266
- if (visualLine.line !== cursorLine) {
267
- continue;
268
- }
269
- const offset = cursorColumn - visualLine.startColumn;
270
- if (offset >= 0 && (offset < visualLine.length || (visualLine.allowsEnd && offset === visualLine.length))) {
271
- index = candidate;
272
- break;
273
- }
274
- }
275
- return { lines: visualLines, index };
276
- }
277
-
278
- function visualColumn(visualLine, offset) {
279
- return textWidth(graphemes(visualLine.text).slice(0, offset).join(""));
280
- }
281
-
282
- function visualOffset(visualLine, targetColumn) {
283
- const chars = graphemes(visualLine.text);
284
- const maxOffset = visualLine.allowsEnd ? chars.length : Math.max(0, chars.length - 1);
285
- let offset = 0;
286
- let column = 0;
287
- while (offset < maxOffset) {
288
- const nextWidth = textWidth(chars[offset]);
289
- if (column + nextWidth > targetColumn) {
290
- break;
291
- }
292
- column += nextWidth;
293
- offset += 1;
294
- }
295
- return offset;
296
- }
297
-
298
- function moveToLineStart() {
299
- preferredVisualColumn = null;
300
- cursorColumn = 0;
301
- return "move";
302
- }
303
-
304
- function moveToLineEnd() {
305
- preferredVisualColumn = null;
306
- cursorColumn = lineLength();
307
- return "move";
308
- }
309
-
310
- function moveWord(delta) {
311
- const chars = graphemes(lines[cursorLine]);
312
- if (delta < 0) {
313
- if (cursorColumn === 0) {
314
- return moveHorizontal(-1);
315
- }
316
- let next = cursorColumn;
317
- while (next > 0 && /\s/.test(chars[next - 1])) {
318
- next -= 1;
319
- }
320
- while (next > 0 && !/\s/.test(chars[next - 1])) {
321
- next -= 1;
322
- }
323
- cursorColumn = next;
324
- return "move";
325
- }
326
- if (cursorColumn >= chars.length) {
327
- return moveHorizontal(1);
328
- }
329
- let next = cursorColumn;
330
- while (next < chars.length && /\s/.test(chars[next])) {
331
- next += 1;
332
- }
333
- while (next < chars.length && !/\s/.test(chars[next])) {
334
- next += 1;
335
- }
336
- cursorColumn = next;
337
- return "move";
338
- }
339
-
340
- function insertText(value) {
341
- const text = normalizeText(value);
342
- if (!text) {
343
- return;
344
- }
345
- preferredVisualColumn = null;
346
- pushUndoSnapshot();
347
- const inserted = text.split("\n");
348
- const current = graphemes(lines[cursorLine]);
349
- const before = current.slice(0, cursorColumn).join("");
350
- const after = current.slice(cursorColumn).join("");
351
- if (inserted.length === 1) {
352
- lines[cursorLine] = before + inserted[0] + after;
353
- cursorColumn = graphemes(before + inserted[0]).length;
354
- } else {
355
- lines.splice(cursorLine, 1, `${before}${inserted[0]}`, ...inserted.slice(1, -1), `${inserted.at(-1)}${after}`);
356
- cursorLine += inserted.length - 1;
357
- cursorColumn = graphemes(inserted.at(-1)).length;
358
- }
359
- resetHistory();
360
- }
361
-
362
- function insertNewline() {
363
- insertText("\n");
364
- }
365
-
366
- function deleteBackward() {
367
- if (cursorColumn === 0 && cursorLine === 0) {
368
- return "edit";
369
- }
370
- pushUndoSnapshot();
371
- if (cursorColumn === 0) {
372
- const previousLength = lineLength(cursorLine - 1);
373
- lines[cursorLine - 1] += lines[cursorLine];
374
- lines.splice(cursorLine, 1);
375
- cursorLine -= 1;
376
- cursorColumn = previousLength;
377
- } else {
378
- const chars = graphemes(lines[cursorLine]);
379
- chars.splice(cursorColumn - 1, 1);
380
- lines[cursorLine] = chars.join("");
381
- cursorColumn -= 1;
382
- }
383
- resetHistory();
384
- return "edit";
385
- }
386
-
387
- function deleteForward() {
388
- if (cursorColumn === lineLength() && cursorLine === lines.length - 1) {
389
- return "edit";
390
- }
391
- pushUndoSnapshot();
392
- if (cursorColumn === lineLength()) {
393
- lines[cursorLine] += lines[cursorLine + 1];
394
- lines.splice(cursorLine + 1, 1);
395
- } else {
396
- const chars = graphemes(lines[cursorLine]);
397
- chars.splice(cursorColumn, 1);
398
- lines[cursorLine] = chars.join("");
399
- }
400
- resetHistory();
401
- return "edit";
402
- }
403
-
404
- function deleteWordBackward() {
405
- const chars = graphemes(lines[cursorLine]);
406
- if (cursorColumn === 0) {
407
- return deleteBackward();
408
- }
409
- let next = cursorColumn;
410
- while (next > 0 && /\s/.test(chars[next - 1])) {
411
- next -= 1;
412
- }
413
- while (next > 0 && !/\s/.test(chars[next - 1])) {
414
- next -= 1;
415
- }
416
- pushUndoSnapshot();
417
- killedText = chars.slice(next, cursorColumn).join("");
418
- lines[cursorLine] = chars.slice(0, next).concat(chars.slice(cursorColumn)).join("");
419
- cursorColumn = next;
420
- resetHistory();
421
- return "edit";
422
- }
423
-
424
- function deleteWordForward() {
425
- const chars = graphemes(lines[cursorLine]);
426
- if (cursorColumn >= chars.length) {
427
- return deleteForward();
428
- }
429
- let next = cursorColumn;
430
- while (next < chars.length && /\s/.test(chars[next])) {
431
- next += 1;
432
- }
433
- while (next < chars.length && !/\s/.test(chars[next])) {
434
- next += 1;
435
- }
436
- pushUndoSnapshot();
437
- killedText = chars.slice(cursorColumn, next).join("");
438
- lines[cursorLine] = chars.slice(0, cursorColumn).concat(chars.slice(next)).join("");
439
- resetHistory();
440
- return "edit";
441
- }
442
-
443
- function deleteToStart() {
444
- if (cursorColumn === 0) {
445
- return "edit";
446
- }
447
- pushUndoSnapshot();
448
- killedText = graphemes(lines[cursorLine]).slice(0, cursorColumn).join("");
449
- lines[cursorLine] = graphemes(lines[cursorLine]).slice(cursorColumn).join("");
450
- cursorColumn = 0;
451
- resetHistory();
452
- return "edit";
453
- }
454
-
455
- function deleteToEnd() {
456
- if (cursorColumn === lineLength()) {
457
- return "edit";
458
- }
459
- pushUndoSnapshot();
460
- killedText = graphemes(lines[cursorLine]).slice(cursorColumn).join("");
461
- lines[cursorLine] = graphemes(lines[cursorLine]).slice(0, cursorColumn).join("");
462
- resetHistory();
463
- return "edit";
464
- }
465
-
466
- function yank() {
467
- if (!killedText) {
468
- return "move";
469
- }
470
- insertText(killedText);
471
- return "edit";
472
- }
473
-
474
- function undo() {
475
- const snapshot = undoStack.pop();
476
- if (!snapshot) {
477
- return "move";
478
- }
479
- lines = snapshot.lines;
480
- cursorLine = snapshot.cursorLine;
481
- cursorColumn = snapshot.cursorColumn;
482
- resetHistory();
483
- return "edit";
484
- }
485
-
486
- function pushUndoSnapshot() {
487
- undoStack.push({
488
- lines: [...lines],
489
- cursorLine,
490
- cursorColumn,
491
- });
492
- if (undoStack.length > UNDO_LIMIT) {
493
- undoStack.shift();
494
- }
495
- }
496
-
497
- function resetHistory() {
498
- historyIndex = -1;
499
- historyDraft = null;
500
- }
501
-
502
- function navigateHistory(direction) {
503
- if (!history.length) {
504
- return "move";
505
- }
506
- const nextIndex = historyIndex - direction;
507
- if (nextIndex < -1 || nextIndex >= history.length) {
508
- return "move";
509
- }
510
- if (historyIndex === -1 && nextIndex >= 0) {
511
- pushUndoSnapshot();
512
- historyDraft = snapshot();
513
- }
514
- preferredVisualColumn = null;
515
- historyIndex = nextIndex;
516
- if (historyIndex === -1) {
517
- restore(historyDraft || { lines: [""], cursorLine: 0, cursorColumn: 0 });
518
- historyDraft = null;
519
- } else {
520
- const value = history[historyIndex];
521
- lines = splitLines(value);
522
- cursorLine = direction < 0 ? 0 : lines.length - 1;
523
- cursorColumn = direction < 0 ? 0 : lineLength();
524
- }
525
- return "edit";
526
- }
527
-
528
- function snapshot() {
529
- return { lines: [...lines], cursorLine, cursorColumn };
530
- }
531
-
532
- function restore(value) {
533
- lines = [...value.lines];
534
- cursorLine = value.cursorLine;
535
- cursorColumn = value.cursorColumn;
536
- }
537
- }
538
-
539
- function isPrintable(chunk, key) {
540
- return Boolean(chunk && !key.ctrl && !key.alt && String(chunk) >= " ");
541
- }
1
+ import { graphemes, textWidth, wrapTextCells } from "./text-width.js";
2
+
3
+ const HISTORY_LIMIT = 100;
4
+ const UNDO_LIMIT = 100;
5
+ const INPUT_PREFIX_WIDTH = 4;
6
+
7
+ export function createLineEditor(initialValue = "") {
8
+ let lines = splitLines(initialValue);
9
+ let cursorLine = lines.length - 1;
10
+ let cursorColumn = graphemes(lines[cursorLine]).length;
11
+ const history = [];
12
+ let historyIndex = -1;
13
+ let historyDraft = null;
14
+ const undoStack = [];
15
+ let killedText = "";
16
+ let viewportWidth = 80;
17
+ let preferredVisualColumn = null;
18
+
19
+ const editor = {
20
+ input() {
21
+ return lines.join("\n");
22
+ },
23
+ cursorPosition() {
24
+ return { line: cursorLine, column: cursorColumn };
25
+ },
26
+ setInput(value) {
27
+ setText(value);
28
+ },
29
+ setViewportWidth(value) {
30
+ const width = Math.max(1, Math.floor(Number(value) || 0));
31
+ if (width !== viewportWidth) {
32
+ viewportWidth = width;
33
+ preferredVisualColumn = null;
34
+ }
35
+ },
36
+ handleInput(event = {}) {
37
+ if (event.kind === "paste") {
38
+ insertText(cleanPaste(event.text));
39
+ return "edit";
40
+ }
41
+ if (event.kind === "text") {
42
+ insertText(event.text);
43
+ return "edit";
44
+ }
45
+ return editor.handleKey(event.text || "", event);
46
+ },
47
+ handleKey(chunk, key = {}) {
48
+ if (key.name !== "up" && key.name !== "down") {
49
+ preferredVisualColumn = null;
50
+ }
51
+ if (key.name === "enter" || key.name === "return") {
52
+ if (key.shift || key.ctrl) {
53
+ insertNewline();
54
+ return "edit";
55
+ }
56
+ return "submit";
57
+ }
58
+ if (key.name === "left") {
59
+ return key.ctrl || key.alt ? moveWord(-1) : moveHorizontal(-1);
60
+ }
61
+ if (key.name === "right") {
62
+ return key.ctrl || key.alt ? moveWord(1) : moveHorizontal(1);
63
+ }
64
+ if (key.ctrl && key.name === "b") {
65
+ return moveHorizontal(-1);
66
+ }
67
+ if (key.ctrl && key.name === "f") {
68
+ return moveHorizontal(1);
69
+ }
70
+ if (key.alt && key.name === "b") {
71
+ return moveWord(-1);
72
+ }
73
+ if (key.alt && key.name === "f") {
74
+ return moveWord(1);
75
+ }
76
+ if (key.name === "up") {
77
+ if (key.ctrl || key.alt || key.shift) {
78
+ return "";
79
+ }
80
+ return moveUp();
81
+ }
82
+ if (key.name === "down") {
83
+ if (key.ctrl || key.alt || key.shift) {
84
+ return "";
85
+ }
86
+ return moveDown();
87
+ }
88
+ if (key.name === "home") {
89
+ cursorColumn = 0;
90
+ return "move";
91
+ }
92
+ if (key.name === "end") {
93
+ cursorColumn = lineLength();
94
+ return "move";
95
+ }
96
+ if (key.name === "backspace") {
97
+ return key.ctrl || key.alt ? deleteWordBackward() : deleteBackward();
98
+ }
99
+ if (key.name === "delete") {
100
+ return key.ctrl || key.alt ? deleteWordForward() : deleteForward();
101
+ }
102
+ if (key.ctrl && key.name === "a") {
103
+ cursorColumn = 0;
104
+ return "move";
105
+ }
106
+ if (key.ctrl && key.name === "e") {
107
+ cursorColumn = lineLength();
108
+ return "move";
109
+ }
110
+ if (key.ctrl && key.name === "w") {
111
+ return deleteWordBackward();
112
+ }
113
+ if (key.ctrl && key.name === "d") {
114
+ return deleteForward();
115
+ }
116
+ if (key.alt && key.name === "d") {
117
+ return deleteWordForward();
118
+ }
119
+ if (key.ctrl && key.name === "u") {
120
+ return deleteToStart();
121
+ }
122
+ if (key.ctrl && key.name === "k") {
123
+ return deleteToEnd();
124
+ }
125
+ if (key.ctrl && key.name === "y") {
126
+ return yank();
127
+ }
128
+ if (key.ctrl && (key.name === "-" || key.name === "_")) {
129
+ return undo();
130
+ }
131
+ if (key.name === "j" && key.ctrl) {
132
+ insertNewline();
133
+ return "edit";
134
+ }
135
+ if (isPrintable(chunk, key)) {
136
+ insertText(chunk);
137
+ return "edit";
138
+ }
139
+ return "";
140
+ },
141
+ addToHistory(value = "") {
142
+ const text = String(value || "").trim();
143
+ if (!text || history[0] === text) {
144
+ resetHistory();
145
+ return;
146
+ }
147
+ history.unshift(text);
148
+ if (history.length > HISTORY_LIMIT) {
149
+ history.length = HISTORY_LIMIT;
150
+ }
151
+ resetHistory();
152
+ },
153
+ };
154
+ return editor;
155
+
156
+ function setText(value) {
157
+ lines = splitLines(value);
158
+ cursorLine = lines.length - 1;
159
+ cursorColumn = lineLength();
160
+ preferredVisualColumn = null;
161
+ resetHistory();
162
+ undoStack.length = 0;
163
+ }
164
+
165
+ function splitLines(value) {
166
+ const result = normalizeText(value).split("\n");
167
+ return result.length ? result : [""];
168
+ }
169
+
170
+ function normalizeText(value) {
171
+ return String(value || "").replace(/\r\n?/g, "\n").replace(/\t/g, " ");
172
+ }
173
+
174
+ function cleanPaste(value) {
175
+ return normalizeText(value).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
176
+ }
177
+
178
+ function lineLength(line = cursorLine) {
179
+ return graphemes(lines[line] || "").length;
180
+ }
181
+
182
+ function isEditorEmpty() {
183
+ return lines.length === 1 && lines[0] === "";
184
+ }
185
+
186
+ function moveHorizontal(delta) {
187
+ if (delta < 0 && cursorColumn === 0 && cursorLine > 0) {
188
+ cursorLine -= 1;
189
+ cursorColumn = lineLength();
190
+ return "move";
191
+ }
192
+ if (delta > 0 && cursorColumn === lineLength() && cursorLine < lines.length - 1) {
193
+ cursorLine += 1;
194
+ cursorColumn = 0;
195
+ return "move";
196
+ }
197
+ cursorColumn = Math.max(0, Math.min(lineLength(), cursorColumn + delta));
198
+ return "move";
199
+ }
200
+
201
+ function moveUp() {
202
+ const visual = visualLineState();
203
+ if (visual.index > 0) {
204
+ return moveVertical(-1, visual);
205
+ }
206
+ if (isEditorEmpty() || historyIndex > -1 || cursorColumn === 0) {
207
+ return navigateHistory(-1);
208
+ }
209
+ return moveToLineStart();
210
+ }
211
+
212
+ function moveDown() {
213
+ const visual = visualLineState();
214
+ if (visual.index < visual.lines.length - 1) {
215
+ return moveVertical(1, visual);
216
+ }
217
+ if (historyIndex > -1) {
218
+ return navigateHistory(1);
219
+ }
220
+ return moveToLineEnd();
221
+ }
222
+
223
+ function moveVertical(delta, visual) {
224
+ const current = visual.lines[visual.index];
225
+ const targetIndex = visual.index + delta;
226
+ const target = visual.lines[targetIndex];
227
+ const currentColumn = visualColumn(current, cursorColumn - current.startColumn);
228
+ const desiredColumn = preferredVisualColumn ?? currentColumn;
229
+ const targetOffset = visualOffset(target, desiredColumn);
230
+ const targetColumn = visualColumn(target, targetOffset);
231
+ cursorLine = target.line;
232
+ cursorColumn = target.startColumn + targetOffset;
233
+ preferredVisualColumn = targetColumn === desiredColumn ? null : desiredColumn;
234
+ return "move";
235
+ }
236
+
237
+ function visualLineState() {
238
+ const visualLines = [];
239
+ const contentWidth = Math.max(1, viewportWidth - INPUT_PREFIX_WIDTH);
240
+ for (const [lineIndex, text] of lines.entries()) {
241
+ const chunks = wrapTextCells(text, contentWidth, contentWidth);
242
+ for (const chunk of chunks) {
243
+ visualLines.push({
244
+ line: lineIndex,
245
+ startColumn: chunk.startColumn,
246
+ length: chunk.length,
247
+ allowsEnd: chunk.allowsEnd,
248
+ text: chunk.text,
249
+ });
250
+ }
251
+ const lastChunk = chunks.at(-1);
252
+ if (lineIndex === lines.length - 1 && !lastChunk.allowsEnd) {
253
+ visualLines.push({
254
+ line: lineIndex,
255
+ startColumn: lastChunk.startColumn + lastChunk.length,
256
+ length: 0,
257
+ allowsEnd: true,
258
+ text: "",
259
+ });
260
+ }
261
+ }
262
+
263
+ let index = visualLines.length - 1;
264
+ for (let candidate = 0; candidate < visualLines.length; candidate += 1) {
265
+ const visualLine = visualLines[candidate];
266
+ if (visualLine.line !== cursorLine) {
267
+ continue;
268
+ }
269
+ const offset = cursorColumn - visualLine.startColumn;
270
+ if (offset >= 0 && (offset < visualLine.length || (visualLine.allowsEnd && offset === visualLine.length))) {
271
+ index = candidate;
272
+ break;
273
+ }
274
+ }
275
+ return { lines: visualLines, index };
276
+ }
277
+
278
+ function visualColumn(visualLine, offset) {
279
+ return textWidth(graphemes(visualLine.text).slice(0, offset).join(""));
280
+ }
281
+
282
+ function visualOffset(visualLine, targetColumn) {
283
+ const chars = graphemes(visualLine.text);
284
+ const maxOffset = visualLine.allowsEnd ? chars.length : Math.max(0, chars.length - 1);
285
+ let offset = 0;
286
+ let column = 0;
287
+ while (offset < maxOffset) {
288
+ const nextWidth = textWidth(chars[offset]);
289
+ if (column + nextWidth > targetColumn) {
290
+ break;
291
+ }
292
+ column += nextWidth;
293
+ offset += 1;
294
+ }
295
+ return offset;
296
+ }
297
+
298
+ function moveToLineStart() {
299
+ preferredVisualColumn = null;
300
+ cursorColumn = 0;
301
+ return "move";
302
+ }
303
+
304
+ function moveToLineEnd() {
305
+ preferredVisualColumn = null;
306
+ cursorColumn = lineLength();
307
+ return "move";
308
+ }
309
+
310
+ function moveWord(delta) {
311
+ const chars = graphemes(lines[cursorLine]);
312
+ if (delta < 0) {
313
+ if (cursorColumn === 0) {
314
+ return moveHorizontal(-1);
315
+ }
316
+ let next = cursorColumn;
317
+ while (next > 0 && /\s/.test(chars[next - 1])) {
318
+ next -= 1;
319
+ }
320
+ while (next > 0 && !/\s/.test(chars[next - 1])) {
321
+ next -= 1;
322
+ }
323
+ cursorColumn = next;
324
+ return "move";
325
+ }
326
+ if (cursorColumn >= chars.length) {
327
+ return moveHorizontal(1);
328
+ }
329
+ let next = cursorColumn;
330
+ while (next < chars.length && /\s/.test(chars[next])) {
331
+ next += 1;
332
+ }
333
+ while (next < chars.length && !/\s/.test(chars[next])) {
334
+ next += 1;
335
+ }
336
+ cursorColumn = next;
337
+ return "move";
338
+ }
339
+
340
+ function insertText(value) {
341
+ const text = normalizeText(value);
342
+ if (!text) {
343
+ return;
344
+ }
345
+ preferredVisualColumn = null;
346
+ pushUndoSnapshot();
347
+ const inserted = text.split("\n");
348
+ const current = graphemes(lines[cursorLine]);
349
+ const before = current.slice(0, cursorColumn).join("");
350
+ const after = current.slice(cursorColumn).join("");
351
+ if (inserted.length === 1) {
352
+ lines[cursorLine] = before + inserted[0] + after;
353
+ cursorColumn = graphemes(before + inserted[0]).length;
354
+ } else {
355
+ lines.splice(cursorLine, 1, `${before}${inserted[0]}`, ...inserted.slice(1, -1), `${inserted.at(-1)}${after}`);
356
+ cursorLine += inserted.length - 1;
357
+ cursorColumn = graphemes(inserted.at(-1)).length;
358
+ }
359
+ resetHistory();
360
+ }
361
+
362
+ function insertNewline() {
363
+ insertText("\n");
364
+ }
365
+
366
+ function deleteBackward() {
367
+ if (cursorColumn === 0 && cursorLine === 0) {
368
+ return "edit";
369
+ }
370
+ pushUndoSnapshot();
371
+ if (cursorColumn === 0) {
372
+ const previousLength = lineLength(cursorLine - 1);
373
+ lines[cursorLine - 1] += lines[cursorLine];
374
+ lines.splice(cursorLine, 1);
375
+ cursorLine -= 1;
376
+ cursorColumn = previousLength;
377
+ } else {
378
+ const chars = graphemes(lines[cursorLine]);
379
+ chars.splice(cursorColumn - 1, 1);
380
+ lines[cursorLine] = chars.join("");
381
+ cursorColumn -= 1;
382
+ }
383
+ resetHistory();
384
+ return "edit";
385
+ }
386
+
387
+ function deleteForward() {
388
+ if (cursorColumn === lineLength() && cursorLine === lines.length - 1) {
389
+ return "edit";
390
+ }
391
+ pushUndoSnapshot();
392
+ if (cursorColumn === lineLength()) {
393
+ lines[cursorLine] += lines[cursorLine + 1];
394
+ lines.splice(cursorLine + 1, 1);
395
+ } else {
396
+ const chars = graphemes(lines[cursorLine]);
397
+ chars.splice(cursorColumn, 1);
398
+ lines[cursorLine] = chars.join("");
399
+ }
400
+ resetHistory();
401
+ return "edit";
402
+ }
403
+
404
+ function deleteWordBackward() {
405
+ const chars = graphemes(lines[cursorLine]);
406
+ if (cursorColumn === 0) {
407
+ return deleteBackward();
408
+ }
409
+ let next = cursorColumn;
410
+ while (next > 0 && /\s/.test(chars[next - 1])) {
411
+ next -= 1;
412
+ }
413
+ while (next > 0 && !/\s/.test(chars[next - 1])) {
414
+ next -= 1;
415
+ }
416
+ pushUndoSnapshot();
417
+ killedText = chars.slice(next, cursorColumn).join("");
418
+ lines[cursorLine] = chars.slice(0, next).concat(chars.slice(cursorColumn)).join("");
419
+ cursorColumn = next;
420
+ resetHistory();
421
+ return "edit";
422
+ }
423
+
424
+ function deleteWordForward() {
425
+ const chars = graphemes(lines[cursorLine]);
426
+ if (cursorColumn >= chars.length) {
427
+ return deleteForward();
428
+ }
429
+ let next = cursorColumn;
430
+ while (next < chars.length && /\s/.test(chars[next])) {
431
+ next += 1;
432
+ }
433
+ while (next < chars.length && !/\s/.test(chars[next])) {
434
+ next += 1;
435
+ }
436
+ pushUndoSnapshot();
437
+ killedText = chars.slice(cursorColumn, next).join("");
438
+ lines[cursorLine] = chars.slice(0, cursorColumn).concat(chars.slice(next)).join("");
439
+ resetHistory();
440
+ return "edit";
441
+ }
442
+
443
+ function deleteToStart() {
444
+ if (cursorColumn === 0) {
445
+ return "edit";
446
+ }
447
+ pushUndoSnapshot();
448
+ killedText = graphemes(lines[cursorLine]).slice(0, cursorColumn).join("");
449
+ lines[cursorLine] = graphemes(lines[cursorLine]).slice(cursorColumn).join("");
450
+ cursorColumn = 0;
451
+ resetHistory();
452
+ return "edit";
453
+ }
454
+
455
+ function deleteToEnd() {
456
+ if (cursorColumn === lineLength()) {
457
+ return "edit";
458
+ }
459
+ pushUndoSnapshot();
460
+ killedText = graphemes(lines[cursorLine]).slice(cursorColumn).join("");
461
+ lines[cursorLine] = graphemes(lines[cursorLine]).slice(0, cursorColumn).join("");
462
+ resetHistory();
463
+ return "edit";
464
+ }
465
+
466
+ function yank() {
467
+ if (!killedText) {
468
+ return "move";
469
+ }
470
+ insertText(killedText);
471
+ return "edit";
472
+ }
473
+
474
+ function undo() {
475
+ const snapshot = undoStack.pop();
476
+ if (!snapshot) {
477
+ return "move";
478
+ }
479
+ lines = snapshot.lines;
480
+ cursorLine = snapshot.cursorLine;
481
+ cursorColumn = snapshot.cursorColumn;
482
+ resetHistory();
483
+ return "edit";
484
+ }
485
+
486
+ function pushUndoSnapshot() {
487
+ undoStack.push({
488
+ lines: [...lines],
489
+ cursorLine,
490
+ cursorColumn,
491
+ });
492
+ if (undoStack.length > UNDO_LIMIT) {
493
+ undoStack.shift();
494
+ }
495
+ }
496
+
497
+ function resetHistory() {
498
+ historyIndex = -1;
499
+ historyDraft = null;
500
+ }
501
+
502
+ function navigateHistory(direction) {
503
+ if (!history.length) {
504
+ return "move";
505
+ }
506
+ const nextIndex = historyIndex - direction;
507
+ if (nextIndex < -1 || nextIndex >= history.length) {
508
+ return "move";
509
+ }
510
+ if (historyIndex === -1 && nextIndex >= 0) {
511
+ pushUndoSnapshot();
512
+ historyDraft = snapshot();
513
+ }
514
+ preferredVisualColumn = null;
515
+ historyIndex = nextIndex;
516
+ if (historyIndex === -1) {
517
+ restore(historyDraft || { lines: [""], cursorLine: 0, cursorColumn: 0 });
518
+ historyDraft = null;
519
+ } else {
520
+ const value = history[historyIndex];
521
+ lines = splitLines(value);
522
+ cursorLine = direction < 0 ? 0 : lines.length - 1;
523
+ cursorColumn = direction < 0 ? 0 : lineLength();
524
+ }
525
+ return "edit";
526
+ }
527
+
528
+ function snapshot() {
529
+ return { lines: [...lines], cursorLine, cursorColumn };
530
+ }
531
+
532
+ function restore(value) {
533
+ lines = [...value.lines];
534
+ cursorLine = value.cursorLine;
535
+ cursorColumn = value.cursorColumn;
536
+ }
537
+ }
538
+
539
+ function isPrintable(chunk, key) {
540
+ return Boolean(chunk && !key.ctrl && !key.alt && String(chunk) >= " ");
541
+ }