@copilot-swarm/core 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/textarea.js CHANGED
@@ -1,10 +1,206 @@
1
1
  /**
2
2
  * Interactive multi-line text editor for the terminal.
3
3
  * Uses raw stdin mode with ANSI rendering — no external dependencies.
4
- * Arrow keys to navigate, Enter for newlines, Ctrl+Enter (or Ctrl+D) to submit, Esc to cancel.
4
+ *
5
+ * Two modes:
6
+ * - `openTextarea()`: full-width single-pane editor
7
+ * - `openSplitEditor(context)`: two-column layout with scrollable read-only context panel
5
8
  */
6
- const TITLE = " Task Description ";
7
- const FOOTER_HINT = " Ctrl+Enter submit Esc cancel ";
9
+ const FOOTER_HINT_SINGLE = " Ctrl+S actions \u2502 Esc menu ";
10
+ const FOOTER_HINT_SPLIT = " Tab switch \u2502 Ctrl+S actions \u2502 Esc menu ";
11
+ // ── Shared helpers ──
12
+ function wordLeftIn(lines, curRow, curCol) {
13
+ if (curCol > 0) {
14
+ const line = lines[curRow];
15
+ let c = curCol;
16
+ while (c > 0 && line[c - 1] === " ")
17
+ c--;
18
+ while (c > 0 && line[c - 1] !== " ")
19
+ c--;
20
+ return { row: curRow, col: c };
21
+ }
22
+ if (curRow > 0)
23
+ return { row: curRow - 1, col: lines[curRow - 1].length };
24
+ return { row: curRow, col: curCol };
25
+ }
26
+ function wordRightIn(lines, curRow, curCol) {
27
+ const line = lines[curRow];
28
+ if (curCol < line.length) {
29
+ let c = curCol;
30
+ while (c < line.length && line[c] !== " ")
31
+ c++;
32
+ while (c < line.length && line[c] === " ")
33
+ c++;
34
+ return { row: curRow, col: c };
35
+ }
36
+ if (curRow < lines.length - 1)
37
+ return { row: curRow + 1, col: 0 };
38
+ return { row: curRow, col: curCol };
39
+ }
40
+ function deleteWordBackIn(lines, curRow, curCol) {
41
+ if (curCol > 0) {
42
+ const line = lines[curRow];
43
+ let c = curCol;
44
+ while (c > 0 && line[c - 1] === " ")
45
+ c--;
46
+ while (c > 0 && line[c - 1] !== " ")
47
+ c--;
48
+ lines[curRow] = line.substring(0, c) + line.substring(curCol);
49
+ return { lines, row: curRow, col: c };
50
+ }
51
+ if (curRow > 0) {
52
+ const col = lines[curRow - 1].length;
53
+ lines[curRow - 1] += lines[curRow];
54
+ lines.splice(curRow, 1);
55
+ return { lines, row: curRow - 1, col };
56
+ }
57
+ return { lines, row: curRow, col: curCol };
58
+ }
59
+ /** Wrap text to a given width, preserving words where possible. */
60
+ function wrapText(text, width) {
61
+ if (width <= 0)
62
+ return [""];
63
+ const result = [];
64
+ for (const rawLine of text.split("\n")) {
65
+ if (rawLine.length <= width) {
66
+ result.push(rawLine);
67
+ continue;
68
+ }
69
+ let remaining = rawLine;
70
+ while (remaining.length > width) {
71
+ let cut = remaining.lastIndexOf(" ", width);
72
+ if (cut <= 0)
73
+ cut = width;
74
+ result.push(remaining.substring(0, cut));
75
+ remaining = remaining.substring(cut).trimStart();
76
+ }
77
+ result.push(remaining);
78
+ }
79
+ return result;
80
+ }
81
+ function renderMenuOverlay(items, selectedIdx, rows, cols) {
82
+ process.stdout.write("\x1b[?25l");
83
+ const boxW = 30;
84
+ const startRow = Math.floor((rows - items.length - 4) / 2);
85
+ const startCol = Math.floor((cols - boxW) / 2);
86
+ const inner = boxW - 2;
87
+ const menuTitle = " Actions ";
88
+ const mTitlePad = Math.max(0, inner - menuTitle.length);
89
+ process.stdout.write(`\x1b[${startRow};${startCol}H\u250c${menuTitle}${"─".repeat(mTitlePad)}\u2510`);
90
+ process.stdout.write(`\x1b[${startRow + 1};${startCol}H\u2502${" ".repeat(inner)}\u2502`);
91
+ for (let idx = 0; idx < items.length; idx++) {
92
+ const selected = idx === selectedIdx;
93
+ const prefix = selected ? "\u203a " : " ";
94
+ const label = `${prefix}${items[idx].label}`;
95
+ const pad = Math.max(0, inner - label.length);
96
+ const row = startRow + 2 + idx;
97
+ if (selected) {
98
+ process.stdout.write(`\x1b[${row};${startCol}H\u2502\x1b[7m${label}${" ".repeat(pad)}\x1b[0m\u2502`);
99
+ }
100
+ else {
101
+ process.stdout.write(`\x1b[${row};${startCol}H\u2502${label}${" ".repeat(pad)}\u2502`);
102
+ }
103
+ }
104
+ const bottomRow = startRow + 2 + items.length;
105
+ const hint = " \u2191\u2193 select Enter confirm ";
106
+ const hPad = Math.max(0, inner - hint.length);
107
+ process.stdout.write(`\x1b[${bottomRow};${startCol}H\u2502\x1b[2m${hint}${" ".repeat(hPad)}\x1b[0m\u2502`);
108
+ process.stdout.write(`\x1b[${bottomRow + 1};${startCol}H\u2514${"─".repeat(inner)}\u2518`);
109
+ }
110
+ /** Process common editing keys. Returns true if the key was handled. */
111
+ function handleEditorKey(ch, i, data, state) {
112
+ const needsFullRender = false;
113
+ const needsCursorUpdate = false;
114
+ const newI = i;
115
+ if (ch === 1) {
116
+ state.curCol = 0;
117
+ return { handled: true, newI, needsFullRender, needsCursorUpdate: true };
118
+ }
119
+ if (ch === 5) {
120
+ state.curCol = state.lines[state.curRow].length;
121
+ return { handled: true, newI, needsFullRender, needsCursorUpdate: true };
122
+ }
123
+ if (ch === 23) {
124
+ const r = deleteWordBackIn(state.lines, state.curRow, state.curCol);
125
+ state.curRow = r.row;
126
+ state.curCol = r.col;
127
+ return { handled: true, newI, needsFullRender: true, needsCursorUpdate: false };
128
+ }
129
+ if (ch === 27 && i + 2 < data.length && data.charCodeAt(i + 1) === 91) {
130
+ const code = data.charCodeAt(i + 2);
131
+ if (code === 49 && i + 5 <= data.length && data.charCodeAt(i + 3) === 59) {
132
+ const mod = data.charCodeAt(i + 4);
133
+ const dir = data.charCodeAt(i + 5);
134
+ if (mod === 53) {
135
+ if (dir === 68) {
136
+ const r = wordLeftIn(state.lines, state.curRow, state.curCol);
137
+ state.curRow = r.row;
138
+ state.curCol = r.col;
139
+ }
140
+ else if (dir === 67) {
141
+ const r = wordRightIn(state.lines, state.curRow, state.curCol);
142
+ state.curRow = r.row;
143
+ state.curCol = r.col;
144
+ }
145
+ }
146
+ return { handled: true, newI: i + 5, needsFullRender: false, needsCursorUpdate: true };
147
+ }
148
+ if (code === 65)
149
+ state.curRow--;
150
+ else if (code === 66)
151
+ state.curRow++;
152
+ else if (code === 67)
153
+ state.curCol++;
154
+ else if (code === 68)
155
+ state.curCol--;
156
+ else if (code === 72)
157
+ state.curCol = 0;
158
+ else if (code === 70)
159
+ state.curCol = state.lines[state.curRow].length;
160
+ else if (code === 51 && i + 3 < data.length && data.charCodeAt(i + 3) === 126) {
161
+ if (state.curCol < state.lines[state.curRow].length) {
162
+ state.lines[state.curRow] =
163
+ state.lines[state.curRow].substring(0, state.curCol) + state.lines[state.curRow].substring(state.curCol + 1);
164
+ }
165
+ else if (state.curRow < state.lines.length - 1) {
166
+ state.lines[state.curRow] += state.lines[state.curRow + 1];
167
+ state.lines.splice(state.curRow + 1, 1);
168
+ }
169
+ return { handled: true, newI: i + 3, needsFullRender: true, needsCursorUpdate: false };
170
+ }
171
+ return { handled: true, newI: i + 2, needsFullRender: false, needsCursorUpdate: true };
172
+ }
173
+ if (ch === 13) {
174
+ const after = state.lines[state.curRow].substring(state.curCol);
175
+ state.lines[state.curRow] = state.lines[state.curRow].substring(0, state.curCol);
176
+ state.lines.splice(state.curRow + 1, 0, after);
177
+ state.curRow++;
178
+ state.curCol = 0;
179
+ return { handled: true, newI, needsFullRender: true, needsCursorUpdate: false };
180
+ }
181
+ if (ch === 127) {
182
+ if (state.curCol > 0) {
183
+ state.lines[state.curRow] =
184
+ state.lines[state.curRow].substring(0, state.curCol - 1) + state.lines[state.curRow].substring(state.curCol);
185
+ state.curCol--;
186
+ }
187
+ else if (state.curRow > 0) {
188
+ state.curCol = state.lines[state.curRow - 1].length;
189
+ state.lines[state.curRow - 1] += state.lines[state.curRow];
190
+ state.lines.splice(state.curRow, 1);
191
+ state.curRow--;
192
+ }
193
+ return { handled: true, newI, needsFullRender: true, needsCursorUpdate: false };
194
+ }
195
+ if (ch === 8) {
196
+ const r = deleteWordBackIn(state.lines, state.curRow, state.curCol);
197
+ state.curRow = r.row;
198
+ state.curCol = r.col;
199
+ return { handled: true, newI, needsFullRender: true, needsCursorUpdate: false };
200
+ }
201
+ return { handled: false, newI, needsFullRender, needsCursorUpdate };
202
+ }
203
+ // ── Single-pane editor ──
8
204
  export async function openTextarea() {
9
205
  if (!process.stdin.isTTY)
10
206
  return undefined;
@@ -12,143 +208,438 @@ export async function openTextarea() {
12
208
  const rows = process.stdout.rows || 24;
13
209
  const editorHeight = Math.max(5, rows - 4);
14
210
  const innerWidth = cols - 4;
15
- const lines = [""];
16
- let curRow = 0;
17
- let curCol = 0;
211
+ const title = " Task Description ";
212
+ const state = { lines: [""], curRow: 0, curCol: 0 };
18
213
  let scroll = 0;
214
+ let menuOpen = false;
215
+ let menuIdx = 0;
216
+ const menuItems = [
217
+ { label: "Submit", value: "submit" },
218
+ { label: "Cancel", value: "cancel" },
219
+ ];
19
220
  function clamp() {
20
- if (curRow < 0)
21
- curRow = 0;
22
- if (curRow >= lines.length)
23
- curRow = lines.length - 1;
24
- if (curCol < 0)
25
- curCol = 0;
26
- if (curCol > lines[curRow].length)
27
- curCol = lines[curRow].length;
28
- if (curRow < scroll)
29
- scroll = curRow;
30
- if (curRow >= scroll + editorHeight)
31
- scroll = curRow - editorHeight + 1;
32
- }
33
- function render() {
34
- const titlePad = Math.max(0, cols - 2 - TITLE.length);
35
- const top = `┌${TITLE}${"─".repeat(titlePad)}┐`;
36
- const footerPad = Math.max(0, cols - 2 - FOOTER_HINT.length);
37
- const bottom = `└${"".repeat(footerPad)}${FOOTER_HINT}┘`;
38
- process.stdout.write("\x1b[2J\x1b[1;1H");
39
- process.stdout.write(`${top}\n`);
40
- for (let i = 0; i < editorHeight; i++) {
41
- const idx = scroll + i;
42
- const text = idx < lines.length ? lines[idx] : "";
43
- const display = text.substring(0, innerWidth);
44
- const pad = Math.max(0, innerWidth - display.length);
45
- process.stdout.write(`│ ${display}${" ".repeat(pad)} │\n`);
46
- }
47
- process.stdout.write(bottom);
48
- const screenRow = 2 + (curRow - scroll);
49
- const screenCol = 3 + curCol;
221
+ if (state.curRow < 0)
222
+ state.curRow = 0;
223
+ if (state.curRow >= state.lines.length)
224
+ state.curRow = state.lines.length - 1;
225
+ if (state.curCol < 0)
226
+ state.curCol = 0;
227
+ if (state.curCol > state.lines[state.curRow].length)
228
+ state.curCol = state.lines[state.curRow].length;
229
+ if (state.curRow < scroll)
230
+ scroll = state.curRow;
231
+ if (state.curRow >= scroll + editorHeight)
232
+ scroll = state.curRow - editorHeight + 1;
233
+ }
234
+ function renderLine(screenRow, lineIdx) {
235
+ const text = lineIdx < state.lines.length ? state.lines[lineIdx] : "";
236
+ const display = text.substring(0, innerWidth);
237
+ const pad = Math.max(0, innerWidth - display.length);
238
+ process.stdout.write(`\x1b[${screenRow};1H\x1b[2K\u2502 ${display}${" ".repeat(pad)} \u2502`);
239
+ }
240
+ function renderFull() {
241
+ const titlePad = Math.max(0, cols - 2 - title.length);
242
+ const top = `\u250c${title}${"─".repeat(titlePad)}\u2510`;
243
+ const footerPad = Math.max(0, cols - 2 - FOOTER_HINT_SINGLE.length);
244
+ const bottom = `\u2514${"─".repeat(footerPad)}${FOOTER_HINT_SINGLE}\u2518`;
245
+ process.stdout.write("\x1b[?25l");
246
+ process.stdout.write(`\x1b[1;1H\x1b[2K${top}`);
247
+ for (let i = 0; i < editorHeight; i++)
248
+ renderLine(2 + i, scroll + i);
249
+ process.stdout.write(`\x1b[${2 + editorHeight};1H\x1b[2K${bottom}`);
250
+ placeCursor();
251
+ process.stdout.write("\x1b[?25h");
252
+ }
253
+ function placeCursor() {
254
+ const screenRow = 2 + (state.curRow - scroll);
255
+ const screenCol = 3 + Math.min(state.curCol, innerWidth);
50
256
  process.stdout.write(`\x1b[${screenRow};${screenCol}H`);
51
257
  }
52
258
  return new Promise((resolve) => {
53
- process.stdout.write("\x1b[?1049h"); // alternate screen
54
- process.stdout.write("\x1b[?25h"); // show cursor
259
+ process.stdout.write("\x1b[?1049h");
260
+ process.stdout.write("\x1b[?25h");
55
261
  process.stdin.setRawMode(true);
56
262
  process.stdin.resume();
57
263
  process.stdin.setEncoding("utf-8");
58
- render();
264
+ renderFull();
59
265
  function cleanup(result) {
60
266
  process.stdin.removeListener("data", onData);
61
267
  process.stdin.setRawMode(false);
62
268
  process.stdin.pause();
63
- process.stdout.write("\x1b[?1049l"); // leave alternate screen
269
+ process.stdout.write("\x1b[?1049l");
64
270
  resolve(result);
65
271
  }
66
272
  function onData(data) {
273
+ if (menuOpen) {
274
+ for (let i = 0; i < data.length; i++) {
275
+ const ch = data.charCodeAt(i);
276
+ if (ch === 27 && i + 2 < data.length && data.charCodeAt(i + 1) === 91) {
277
+ const code = data.charCodeAt(i + 2);
278
+ if (code === 65)
279
+ menuIdx = (menuIdx - 1 + menuItems.length) % menuItems.length;
280
+ else if (code === 66)
281
+ menuIdx = (menuIdx + 1) % menuItems.length;
282
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
283
+ i += 2;
284
+ continue;
285
+ }
286
+ if (ch === 13) {
287
+ menuOpen = false;
288
+ if (menuItems[menuIdx].value === "submit") {
289
+ cleanup(state.lines.join("\n").trim() || undefined);
290
+ }
291
+ else {
292
+ cleanup(undefined);
293
+ }
294
+ return;
295
+ }
296
+ if (ch === 27 || ch === 3) {
297
+ menuOpen = false;
298
+ renderFull();
299
+ return;
300
+ }
301
+ }
302
+ return;
303
+ }
304
+ let needsFullRender = false;
305
+ let needsCursorUpdate = false;
67
306
  for (let i = 0; i < data.length; i++) {
68
307
  const ch = data.charCodeAt(i);
69
- // Ctrl+C — cancel
70
308
  if (ch === 3) {
71
309
  cleanup(undefined);
72
310
  return;
73
311
  }
74
- // Ctrl+D (4) or LF (10, Ctrl+Enter in most terminals) — submit
75
- if (ch === 4 || ch === 10) {
76
- const text = lines.join("\n").trim();
77
- cleanup(text || undefined);
312
+ if (ch === 19 || ch === 10) {
313
+ menuOpen = true;
314
+ menuIdx = 0;
315
+ renderFull();
316
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
317
+ return;
318
+ }
319
+ if (ch === 4) {
320
+ cleanup(state.lines.join("\n").trim() || undefined);
78
321
  return;
79
322
  }
80
- // Escape / escape sequences
323
+ // Try shared editor key handler
324
+ const result = handleEditorKey(ch, i, data, state);
325
+ if (result.handled) {
326
+ i = result.newI;
327
+ if (result.needsFullRender)
328
+ needsFullRender = true;
329
+ if (result.needsCursorUpdate)
330
+ needsCursorUpdate = true;
331
+ clamp();
332
+ continue;
333
+ }
334
+ // Esc → menu
81
335
  if (ch === 27) {
82
- if (i + 2 < data.length && data.charCodeAt(i + 1) === 91) {
336
+ menuOpen = true;
337
+ menuIdx = 1;
338
+ renderFull();
339
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
340
+ return;
341
+ }
342
+ // Printable characters
343
+ if (ch >= 32) {
344
+ state.lines[state.curRow] =
345
+ state.lines[state.curRow].substring(0, state.curCol) +
346
+ data[i] +
347
+ state.lines[state.curRow].substring(state.curCol);
348
+ state.curCol++;
349
+ clamp();
350
+ renderLine(2 + (state.curRow - scroll), state.curRow);
351
+ placeCursor();
352
+ }
353
+ }
354
+ if (needsFullRender)
355
+ renderFull();
356
+ else if (needsCursorUpdate)
357
+ placeCursor();
358
+ }
359
+ process.stdin.on("data", onData);
360
+ });
361
+ }
362
+ /**
363
+ * Opens a two-column editor: left side is an editable textarea, right side is a
364
+ * scrollable read-only context panel showing agent questions or other reference text.
365
+ *
366
+ * Returns the user's typed text, or `undefined` if cancelled.
367
+ * Returns empty string `""` if the user chose "Skip".
368
+ *
369
+ * Keyboard:
370
+ * Tab — switch focus between left (editor) and right (context) panels
371
+ * Arrow keys — navigate cursor (left) or scroll (right)
372
+ * PgUp/PgDown — page scroll in context panel
373
+ * Ctrl+S / Esc — open command palette
374
+ * Ctrl+D — submit directly
375
+ */
376
+ export async function openSplitEditor(contextText, options) {
377
+ if (!process.stdin.isTTY)
378
+ return undefined;
379
+ const cols = process.stdout.columns || 80;
380
+ const rows = process.stdout.rows || 24;
381
+ const bodyHeight = Math.max(5, rows - 4);
382
+ // Panel widths: split roughly 50/50
383
+ const dividerCol = Math.floor(cols / 2);
384
+ const leftInner = dividerCol - 3;
385
+ const rightInner = cols - dividerCol - 4;
386
+ const editorTitle = ` ${options?.editorTitle ?? "Your Answer"} `;
387
+ const contextTitle = ` ${options?.contextTitle ?? "Agent Questions"} `;
388
+ // Editor state (left panel)
389
+ const state = { lines: [""], curRow: 0, curCol: 0 };
390
+ let edScroll = 0;
391
+ // Context state (right panel)
392
+ const ctxLines = wrapText(contextText, rightInner);
393
+ let ctxScroll = 0;
394
+ const ctxMaxScroll = Math.max(0, ctxLines.length - bodyHeight);
395
+ let focus = "left";
396
+ let menuOpen = false;
397
+ let menuIdx = 0;
398
+ const menuItems = [
399
+ { label: "Submit", value: "submit" },
400
+ { label: "Skip (use AI judgment)", value: "skip" },
401
+ { label: "Cancel", value: "cancel" },
402
+ ];
403
+ function clampEditor() {
404
+ if (state.curRow < 0)
405
+ state.curRow = 0;
406
+ if (state.curRow >= state.lines.length)
407
+ state.curRow = state.lines.length - 1;
408
+ if (state.curCol < 0)
409
+ state.curCol = 0;
410
+ if (state.curCol > state.lines[state.curRow].length)
411
+ state.curCol = state.lines[state.curRow].length;
412
+ if (state.curRow < edScroll)
413
+ edScroll = state.curRow;
414
+ if (state.curRow >= edScroll + bodyHeight)
415
+ edScroll = state.curRow - bodyHeight + 1;
416
+ }
417
+ function clampCtx() {
418
+ if (ctxScroll < 0)
419
+ ctxScroll = 0;
420
+ if (ctxScroll > ctxMaxScroll)
421
+ ctxScroll = ctxMaxScroll;
422
+ }
423
+ function renderRow(screenRow, bodyIdx) {
424
+ const edLineIdx = edScroll + bodyIdx;
425
+ const edText = edLineIdx < state.lines.length ? state.lines[edLineIdx] : "";
426
+ const edDisplay = edText.substring(0, leftInner);
427
+ const edPad = Math.max(0, leftInner - edDisplay.length);
428
+ const leftDim = focus === "right" ? "\x1b[2m" : "";
429
+ const leftReset = focus === "right" ? "\x1b[0m" : "";
430
+ const ctxLineIdx = ctxScroll + bodyIdx;
431
+ const ctxText = ctxLineIdx < ctxLines.length ? ctxLines[ctxLineIdx] : "";
432
+ const ctxDisplay = ctxText.substring(0, rightInner);
433
+ const ctxPad = Math.max(0, rightInner - ctxDisplay.length);
434
+ const rightDim = focus === "left" ? "\x1b[2m" : "";
435
+ const rightReset = focus === "left" ? "\x1b[0m" : "";
436
+ process.stdout.write(`\x1b[${screenRow};1H\x1b[2K` +
437
+ `\u2502${leftDim} ${edDisplay}${" ".repeat(edPad)} ${leftReset}\u2502` +
438
+ `${rightDim} ${ctxDisplay}${" ".repeat(ctxPad)} ${rightReset}\u2502`);
439
+ }
440
+ function renderFull() {
441
+ process.stdout.write("\x1b[?25l");
442
+ // Top border
443
+ const leftTitlePad = Math.max(0, dividerCol - 1 - editorTitle.length);
444
+ const rightTitlePad = Math.max(0, cols - dividerCol - 1 - contextTitle.length);
445
+ process.stdout.write(`\x1b[1;1H\x1b[2K\u250c${editorTitle}${"─".repeat(leftTitlePad)}\u252c${contextTitle}${"─".repeat(rightTitlePad)}\u2510`);
446
+ for (let i = 0; i < bodyHeight; i++)
447
+ renderRow(2 + i, i);
448
+ // Bottom border
449
+ const bottomLeftW = dividerCol - 1;
450
+ const bottomRightW = cols - dividerCol - 1;
451
+ const hintLen = FOOTER_HINT_SPLIT.length;
452
+ const bottomDash = Math.max(0, bottomRightW - hintLen);
453
+ process.stdout.write(`\x1b[${2 + bodyHeight};1H\x1b[2K\u2514${"─".repeat(bottomLeftW)}\u2534${"─".repeat(bottomDash)}${FOOTER_HINT_SPLIT}\u2518`);
454
+ // Scroll indicator
455
+ if (ctxLines.length > bodyHeight) {
456
+ const pct = ctxMaxScroll > 0 ? Math.round((ctxScroll / ctxMaxScroll) * 100) : 0;
457
+ const indicator = ` ${pct}% `;
458
+ process.stdout.write(`\x1b[${2 + bodyHeight};${cols - indicator.length}H\x1b[2m${indicator}\x1b[0m`);
459
+ }
460
+ placeCursorSplit();
461
+ process.stdout.write("\x1b[?25h");
462
+ }
463
+ function placeCursorSplit() {
464
+ if (focus === "left") {
465
+ const screenRow = 2 + (state.curRow - edScroll);
466
+ const screenCol = 3 + Math.min(state.curCol, leftInner);
467
+ process.stdout.write(`\x1b[?25h\x1b[${screenRow};${screenCol}H`);
468
+ }
469
+ else {
470
+ process.stdout.write("\x1b[?25l");
471
+ }
472
+ }
473
+ return new Promise((resolve) => {
474
+ process.stdout.write("\x1b[?1049h");
475
+ process.stdout.write("\x1b[?25h");
476
+ process.stdin.setRawMode(true);
477
+ process.stdin.resume();
478
+ process.stdin.setEncoding("utf-8");
479
+ renderFull();
480
+ function cleanup(result) {
481
+ process.stdin.removeListener("data", onData);
482
+ process.stdin.setRawMode(false);
483
+ process.stdin.pause();
484
+ process.stdout.write("\x1b[?25h");
485
+ process.stdout.write("\x1b[?1049l");
486
+ resolve(result);
487
+ }
488
+ function onData(data) {
489
+ // Menu mode
490
+ if (menuOpen) {
491
+ for (let i = 0; i < data.length; i++) {
492
+ const ch = data.charCodeAt(i);
493
+ if (ch === 27 && i + 2 < data.length && data.charCodeAt(i + 1) === 91) {
83
494
  const code = data.charCodeAt(i + 2);
84
495
  if (code === 65)
85
- curRow--;
496
+ menuIdx = (menuIdx - 1 + menuItems.length) % menuItems.length;
86
497
  else if (code === 66)
87
- curRow++;
88
- else if (code === 67)
89
- curCol++;
90
- else if (code === 68)
91
- curCol--;
92
- else if (code === 72)
93
- curCol = 0;
94
- else if (code === 70)
95
- curCol = lines[curRow].length;
96
- else if (code === 51 && i + 3 < data.length && data.charCodeAt(i + 3) === 126) {
97
- // Delete key
98
- if (curCol < lines[curRow].length) {
99
- lines[curRow] = lines[curRow].substring(0, curCol) + lines[curRow].substring(curCol + 1);
100
- }
101
- else if (curRow < lines.length - 1) {
102
- lines[curRow] += lines[curRow + 1];
103
- lines.splice(curRow + 1, 1);
104
- }
105
- i++;
106
- }
498
+ menuIdx = (menuIdx + 1) % menuItems.length;
499
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
107
500
  i += 2;
501
+ continue;
108
502
  }
109
- else {
110
- // Plain Escape — cancel
503
+ if (ch === 13) {
504
+ menuOpen = false;
505
+ const choice = menuItems[menuIdx].value;
506
+ if (choice === "submit") {
507
+ cleanup(state.lines.join("\n").trim() || undefined);
508
+ return;
509
+ }
510
+ if (choice === "skip") {
511
+ cleanup("");
512
+ return;
513
+ }
111
514
  cleanup(undefined);
112
515
  return;
113
516
  }
114
- clamp();
115
- render();
116
- continue;
117
- }
118
- // Enter (CR) — new line
119
- if (ch === 13) {
120
- const after = lines[curRow].substring(curCol);
121
- lines[curRow] = lines[curRow].substring(0, curCol);
122
- lines.splice(curRow + 1, 0, after);
123
- curRow++;
124
- curCol = 0;
125
- clamp();
126
- render();
127
- continue;
517
+ if (ch === 27 || ch === 3) {
518
+ menuOpen = false;
519
+ renderFull();
520
+ return;
521
+ }
128
522
  }
129
- // Backspace
130
- if (ch === 127) {
131
- if (curCol > 0) {
132
- lines[curRow] = lines[curRow].substring(0, curCol - 1) + lines[curRow].substring(curCol);
133
- curCol--;
523
+ return;
524
+ }
525
+ // Context panel focused (right)
526
+ if (focus === "right") {
527
+ for (let i = 0; i < data.length; i++) {
528
+ const ch = data.charCodeAt(i);
529
+ if (ch === 3) {
530
+ cleanup(undefined);
531
+ return;
134
532
  }
135
- else if (curRow > 0) {
136
- curCol = lines[curRow - 1].length;
137
- lines[curRow - 1] += lines[curRow];
138
- lines.splice(curRow, 1);
139
- curRow--;
533
+ if (ch === 9) {
534
+ focus = "left";
535
+ renderFull();
536
+ return;
140
537
  }
141
- clamp();
142
- render();
538
+ if (ch === 19 || ch === 10) {
539
+ menuOpen = true;
540
+ menuIdx = 0;
541
+ renderFull();
542
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
543
+ return;
544
+ }
545
+ if (ch === 4) {
546
+ cleanup(state.lines.join("\n").trim() || undefined);
547
+ return;
548
+ }
549
+ if (ch === 27) {
550
+ if (i + 2 < data.length && data.charCodeAt(i + 1) === 91) {
551
+ const code = data.charCodeAt(i + 2);
552
+ if (code === 65)
553
+ ctxScroll--;
554
+ else if (code === 66)
555
+ ctxScroll++;
556
+ else if (code === 53 && i + 3 < data.length && data.charCodeAt(i + 3) === 126) {
557
+ ctxScroll -= bodyHeight;
558
+ i++;
559
+ }
560
+ else if (code === 54 && i + 3 < data.length && data.charCodeAt(i + 3) === 126) {
561
+ ctxScroll += bodyHeight;
562
+ i++;
563
+ }
564
+ else if (code === 72) {
565
+ ctxScroll = 0;
566
+ }
567
+ else if (code === 70) {
568
+ ctxScroll = ctxMaxScroll;
569
+ }
570
+ i += 2;
571
+ clampCtx();
572
+ renderFull();
573
+ continue;
574
+ }
575
+ menuOpen = true;
576
+ menuIdx = 1;
577
+ renderFull();
578
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
579
+ return;
580
+ }
581
+ }
582
+ return;
583
+ }
584
+ // Editor panel focused (left)
585
+ let needsFullRender = false;
586
+ let needsCursorUpdate = false;
587
+ for (let i = 0; i < data.length; i++) {
588
+ const ch = data.charCodeAt(i);
589
+ if (ch === 3) {
590
+ cleanup(undefined);
591
+ return;
592
+ }
593
+ if (ch === 9) {
594
+ focus = "right";
595
+ renderFull();
596
+ return;
597
+ }
598
+ if (ch === 19 || ch === 10) {
599
+ menuOpen = true;
600
+ menuIdx = 0;
601
+ renderFull();
602
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
603
+ return;
604
+ }
605
+ if (ch === 4) {
606
+ cleanup(state.lines.join("\n").trim() || undefined);
607
+ return;
608
+ }
609
+ const result = handleEditorKey(ch, i, data, state);
610
+ if (result.handled) {
611
+ i = result.newI;
612
+ if (result.needsFullRender)
613
+ needsFullRender = true;
614
+ if (result.needsCursorUpdate)
615
+ needsCursorUpdate = true;
616
+ clampEditor();
143
617
  continue;
144
618
  }
145
- // Printable characters (ASCII + Unicode)
619
+ // Esc menu
620
+ if (ch === 27) {
621
+ menuOpen = true;
622
+ menuIdx = 1;
623
+ renderFull();
624
+ renderMenuOverlay(menuItems, menuIdx, rows, cols);
625
+ return;
626
+ }
627
+ // Printable characters
146
628
  if (ch >= 32) {
147
- lines[curRow] = lines[curRow].substring(0, curCol) + data[i] + lines[curRow].substring(curCol);
148
- curCol++;
149
- render();
629
+ state.lines[state.curRow] =
630
+ state.lines[state.curRow].substring(0, state.curCol) +
631
+ data[i] +
632
+ state.lines[state.curRow].substring(state.curCol);
633
+ state.curCol++;
634
+ clampEditor();
635
+ renderRow(2 + (state.curRow - edScroll), state.curRow - edScroll);
636
+ placeCursorSplit();
150
637
  }
151
638
  }
639
+ if (needsFullRender)
640
+ renderFull();
641
+ else if (needsCursorUpdate)
642
+ placeCursorSplit();
152
643
  }
153
644
  process.stdin.on("data", onData);
154
645
  });