@copilot-swarm/core 0.0.11 → 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/checkpoint.d.ts +7 -0
- package/dist/checkpoint.d.ts.map +1 -1
- package/dist/checkpoint.js.map +1 -1
- package/dist/planning-engine.d.ts +1 -6
- package/dist/planning-engine.d.ts.map +1 -1
- package/dist/planning-engine.js +80 -49
- package/dist/planning-engine.js.map +1 -1
- package/dist/textarea.d.ts +25 -1
- package/dist/textarea.d.ts.map +1 -1
- package/dist/textarea.js +591 -100
- package/dist/textarea.js.map +1 -1
- package/dist/tui-renderer.d.ts +2 -0
- package/dist/tui-renderer.d.ts.map +1 -1
- package/dist/tui-renderer.js +36 -3
- package/dist/tui-renderer.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
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
|
|
7
|
-
const
|
|
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
|
|
16
|
-
|
|
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
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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");
|
|
54
|
-
process.stdout.write("\x1b[?25h");
|
|
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
|
-
|
|
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");
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
496
|
+
menuIdx = (menuIdx - 1 + menuItems.length) % menuItems.length;
|
|
86
497
|
else if (code === 66)
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
curRow--;
|
|
533
|
+
if (ch === 9) {
|
|
534
|
+
focus = "left";
|
|
535
|
+
renderFull();
|
|
536
|
+
return;
|
|
140
537
|
}
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
//
|
|
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] =
|
|
148
|
-
|
|
149
|
-
|
|
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
|
});
|