@oxecli/oxe 1.0.51 → 1.0.53
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/cli.js +69 -65
- package/dist/config.js +35 -35
- package/dist/engine.js +56 -43
- package/dist/skills.js +10 -4
- package/dist/system.js +2 -2
- package/dist/tools.js +134 -75
- package/dist/ui.js +396 -297
- package/package.json +1 -1
package/dist/ui.js
CHANGED
|
@@ -9,8 +9,6 @@ export function enableRawStdin() {
|
|
|
9
9
|
readline.emitKeypressEvents(process.stdin);
|
|
10
10
|
if (process.stdin.isTTY) {
|
|
11
11
|
process.stdin.setRawMode(true);
|
|
12
|
-
// Enable the kitty keyboard protocol (CSI u) so modified Enter (Shift /
|
|
13
|
-
// Ctrl+Enter) arrives as \x1b[13;<mod>u rather than a plain \r.
|
|
14
12
|
process.stdout.write("\x1b[>1u");
|
|
15
13
|
}
|
|
16
14
|
process.stdin.resume();
|
|
@@ -28,10 +26,6 @@ export function disableRawStdin() {
|
|
|
28
26
|
catch {
|
|
29
27
|
/* ignore */
|
|
30
28
|
}
|
|
31
|
-
// Do NOT pause stdin here: askBottomPrompt re-acquires raw mode and
|
|
32
|
-
// resumes the stream itself. Pausing can race with that resume and drop
|
|
33
|
-
// keypress events on some terminals (prompt appearing "locked" after an
|
|
34
|
-
// interactive help/picker).
|
|
35
29
|
showCursor();
|
|
36
30
|
}
|
|
37
31
|
}
|
|
@@ -40,7 +34,7 @@ export function waitRawKey() {
|
|
|
40
34
|
return new Promise((resolve) => {
|
|
41
35
|
const onKeypress = (str, key) => {
|
|
42
36
|
process.stdin.removeListener("keypress", onKeypress);
|
|
43
|
-
resolve({ name: key?.name ?? "", str: str ?? "", ctrl: !!
|
|
37
|
+
resolve({ name: key?.name ?? "", str: str ?? "", ctrl: !!key?.ctrl });
|
|
44
38
|
};
|
|
45
39
|
process.stdin.on("keypress", onKeypress);
|
|
46
40
|
});
|
|
@@ -52,22 +46,25 @@ export function isWindows() {
|
|
|
52
46
|
return process.platform === "win32";
|
|
53
47
|
}
|
|
54
48
|
export function enableAnsi() {
|
|
55
|
-
//
|
|
56
|
-
// No native SetConsoleMode available without a native module; rely on the host.
|
|
49
|
+
// Modern Windows Terminal and host consoles support ANSI automatically.
|
|
57
50
|
}
|
|
58
51
|
export function clearScreen() {
|
|
59
|
-
|
|
60
|
-
// crude but reliable clear
|
|
61
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
65
|
-
}
|
|
52
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
66
53
|
}
|
|
67
54
|
export function terminalWidth() {
|
|
68
55
|
return process.stdout.columns || 80;
|
|
69
56
|
}
|
|
70
57
|
// ---------------------------------------------------------------------------
|
|
58
|
+
// ANSI and plain text length helpers
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
export const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[()][AB012]|\x1b\[\?[0-9]+[a-zA-Z]|\x1b\[>[0-9]+[a-zA-Z]/g;
|
|
61
|
+
export function stripAnsi(s) {
|
|
62
|
+
return s.replace(ANSI_RE, "");
|
|
63
|
+
}
|
|
64
|
+
export function plainLen(s) {
|
|
65
|
+
return stripAnsi(s).length;
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
71
68
|
// Markup tag -> ANSI
|
|
72
69
|
// ---------------------------------------------------------------------------
|
|
73
70
|
const TAG_MAP = {
|
|
@@ -82,9 +79,17 @@ const TAG_MAP = {
|
|
|
82
79
|
magenta: "\x1b[35m",
|
|
83
80
|
cyan: "\x1b[36m",
|
|
84
81
|
white: "\x1b[37m",
|
|
82
|
+
bright_black: "\x1b[90m",
|
|
83
|
+
bright_red: "\x1b[91m",
|
|
84
|
+
bright_green: "\x1b[92m",
|
|
85
|
+
bright_yellow: "\x1b[93m",
|
|
86
|
+
bright_blue: "\x1b[94m",
|
|
87
|
+
bright_magenta: "\x1b[95m",
|
|
88
|
+
bright_cyan: "\x1b[96m",
|
|
89
|
+
bright_white: "\x1b[97m",
|
|
85
90
|
};
|
|
86
91
|
const RESET = "\x1b[0m";
|
|
87
|
-
const MARKUP_RE = /\[(\/?)([a-z0-
|
|
92
|
+
const MARKUP_RE = /\[(\/?)([a-z0-9_ ]+)\]/gi;
|
|
88
93
|
export function escapeMarkup(text) {
|
|
89
94
|
return text.replace(/\[/g, "\\[").replace(/\]/g, "\\]");
|
|
90
95
|
}
|
|
@@ -115,90 +120,214 @@ export function markupToAnsi(text) {
|
|
|
115
120
|
});
|
|
116
121
|
}
|
|
117
122
|
// ---------------------------------------------------------------------------
|
|
118
|
-
//
|
|
123
|
+
// Syntax Highlighter for Code Blocks
|
|
119
124
|
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
125
|
+
const KEYWORDS = new Set([
|
|
126
|
+
"const", "let", "var", "function", "class", "import", "export", "from",
|
|
127
|
+
"return", "if", "else", "for", "while", "do", "switch", "case", "break",
|
|
128
|
+
"continue", "async", "await", "try", "catch", "finally", "throw", "new",
|
|
129
|
+
"typeof", "instanceof", "interface", "type", "enum", "extends", "implements",
|
|
130
|
+
"public", "private", "protected", "readonly", "static", "def", "self",
|
|
131
|
+
"lambda", "with", "as", "yield", "pass", "None", "True", "False", "null",
|
|
132
|
+
"undefined", "true", "false", "fn", "struct", "pub", "impl", "mut", "use",
|
|
133
|
+
]);
|
|
134
|
+
function highlightCodeLine(line, _lang = "") {
|
|
135
|
+
if (!line)
|
|
136
|
+
return "";
|
|
137
|
+
if (/^\s*(\/\/|#|\/\*)/.test(line)) {
|
|
138
|
+
return `\x1b[2;37m${line}\x1b[0m`;
|
|
139
|
+
}
|
|
140
|
+
return line.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+\.?\d*\b)|(\b[a-zA-Z_$][\w$]*\b)|(\/\/[^\n]*|#[^\n]*)/g, (m, str, num, ident, comment) => {
|
|
141
|
+
if (str)
|
|
142
|
+
return `\x1b[32m${str}\x1b[0m`;
|
|
143
|
+
if (num)
|
|
144
|
+
return `\x1b[33m${num}\x1b[0m`;
|
|
145
|
+
if (comment)
|
|
146
|
+
return `\x1b[2;37m${comment}\x1b[0m`;
|
|
147
|
+
if (ident && KEYWORDS.has(ident))
|
|
148
|
+
return `\x1b[1;36m${ident}\x1b[0m`;
|
|
149
|
+
return m;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function formatInlineMarkdown(text) {
|
|
153
|
+
let line = text;
|
|
154
|
+
line = line.replace(/\*\*\*([^*]+)\*\*\*/g, "\x1b[1;3m$1\x1b[0m");
|
|
155
|
+
line = line.replace(/\*\*([^*]+)\*\*/g, "\x1b[1m$1\x1b[0m");
|
|
156
|
+
line = line.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
|
|
157
|
+
line = line.replace(/`([^`]+)`/g, "\x1b[1;36m`$1`\x1b[0m");
|
|
158
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[2m($2)\x1b[0m");
|
|
159
|
+
return line;
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Markdown Table Formatter
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
function formatMarkdownTable(tableLines) {
|
|
165
|
+
if (tableLines.length < 2)
|
|
166
|
+
return tableLines;
|
|
167
|
+
const parseRow = (row) => row
|
|
168
|
+
.replace(/^\||\|$/g, "")
|
|
169
|
+
.split("|")
|
|
170
|
+
.map((c) => c.trim());
|
|
171
|
+
const header = parseRow(tableLines[0]);
|
|
172
|
+
const rows = tableLines.slice(2).map(parseRow);
|
|
173
|
+
const allRows = [header, ...rows];
|
|
174
|
+
const colWidths = [];
|
|
175
|
+
for (let c = 0; c < header.length; c++) {
|
|
176
|
+
let mw = 0;
|
|
177
|
+
for (const r of allRows) {
|
|
178
|
+
if (r[c])
|
|
179
|
+
mw = Math.max(mw, plainLen(formatInlineMarkdown(r[c])));
|
|
151
180
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
181
|
+
colWidths.push(Math.max(mw, 3));
|
|
182
|
+
}
|
|
183
|
+
const out = [];
|
|
184
|
+
const topBorder = `\x1b[90m┌${colWidths.map((w) => "─".repeat(w + 2)).join("┬")}┐\x1b[0m`;
|
|
185
|
+
const midBorder = `\x1b[90m├${colWidths.map((w) => "─".repeat(w + 2)).join("┼")}┤\x1b[0m`;
|
|
186
|
+
const botBorder = `\x1b[90m└${colWidths.map((w) => "─".repeat(w + 2)).join("┴")}┘\x1b[0m`;
|
|
187
|
+
out.push(topBorder);
|
|
188
|
+
const headCells = header.map((h, i) => {
|
|
189
|
+
const formatted = `\x1b[1;37m${formatInlineMarkdown(h)}\x1b[0m`;
|
|
190
|
+
const pad = colWidths[i] - plainLen(formatted);
|
|
191
|
+
return ` ${formatted}${" ".repeat(Math.max(pad, 0))} `;
|
|
155
192
|
});
|
|
193
|
+
out.push(`\x1b[90m│\x1b[0m${headCells.join("\x1b[90m│\x1b[0m")}\x1b[90m│\x1b[0m`);
|
|
194
|
+
out.push(midBorder);
|
|
195
|
+
for (const r of rows) {
|
|
196
|
+
const rowCells = header.map((_, i) => {
|
|
197
|
+
const cell = r[i] ? formatInlineMarkdown(r[i]) : "";
|
|
198
|
+
const pad = colWidths[i] - plainLen(cell);
|
|
199
|
+
return ` ${cell}${" ".repeat(Math.max(pad, 0))} `;
|
|
200
|
+
});
|
|
201
|
+
out.push(`\x1b[90m│\x1b[0m${rowCells.join("\x1b[90m│\x1b[0m")}\x1b[90m│\x1b[0m`);
|
|
202
|
+
}
|
|
203
|
+
out.push(botBorder);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Rich Markdown -> ANSI Terminal Renderer
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
export function markdownToAnsi(text) {
|
|
210
|
+
const width = Math.max(terminalWidth() - 2, 40);
|
|
211
|
+
const rawLines = text.split("\n");
|
|
156
212
|
const out = [];
|
|
157
213
|
let inCode = false;
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
214
|
+
let codeLang = "";
|
|
215
|
+
let codeBuffer = [];
|
|
216
|
+
let tableBuffer = [];
|
|
217
|
+
const flushCodeBlock = () => {
|
|
218
|
+
if (!codeBuffer.length && !codeLang)
|
|
219
|
+
return;
|
|
220
|
+
const badge = codeLang ? ` \x1b[1;36m${codeLang}\x1b[0m ` : " ";
|
|
221
|
+
const barLen = Math.max(10, Math.min(width - plainLen(badge) - 4, 60));
|
|
222
|
+
out.push(`\x1b[90m┌─${badge}${"─".repeat(barLen)}┐\x1b[0m`);
|
|
223
|
+
for (const cline of codeBuffer) {
|
|
224
|
+
const highlighted = highlightCodeLine(cline, codeLang);
|
|
225
|
+
out.push(`\x1b[90m│\x1b[0m ${highlighted}`);
|
|
163
226
|
}
|
|
227
|
+
out.push(`\x1b[90m└${"─".repeat(barLen + plainLen(badge) + 2)}┘\x1b[0m`);
|
|
228
|
+
codeBuffer = [];
|
|
229
|
+
codeLang = "";
|
|
164
230
|
};
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
231
|
+
const flushTable = () => {
|
|
232
|
+
if (!tableBuffer.length)
|
|
233
|
+
return;
|
|
234
|
+
const formatted = formatMarkdownTable(tableBuffer);
|
|
235
|
+
out.push(...formatted);
|
|
236
|
+
tableBuffer = [];
|
|
237
|
+
};
|
|
238
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
239
|
+
const raw = rawLines[i];
|
|
240
|
+
const fenceMatch = raw.match(/^(`{3,})(.*)$/);
|
|
241
|
+
if (fenceMatch) {
|
|
242
|
+
flushTable();
|
|
169
243
|
if (!inCode) {
|
|
170
244
|
inCode = true;
|
|
245
|
+
codeLang = fenceMatch[2].trim().toLowerCase();
|
|
246
|
+
codeBuffer = [];
|
|
171
247
|
continue;
|
|
172
248
|
}
|
|
173
249
|
else {
|
|
174
250
|
inCode = false;
|
|
175
|
-
|
|
251
|
+
flushCodeBlock();
|
|
176
252
|
continue;
|
|
177
253
|
}
|
|
178
254
|
}
|
|
179
255
|
if (inCode) {
|
|
180
|
-
|
|
256
|
+
codeBuffer.push(raw);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
// Markdown Table rows
|
|
260
|
+
if (/^\s*\|.*\|\s*$/.test(raw)) {
|
|
261
|
+
tableBuffer.push(raw.trim());
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
else if (tableBuffer.length) {
|
|
265
|
+
flushTable();
|
|
266
|
+
}
|
|
267
|
+
// Horizontal rules
|
|
268
|
+
if (/^(\*{3,}|-{3,}|_{3,})$/.test(raw.trim())) {
|
|
269
|
+
out.push(`\x1b[90m${"─".repeat(Math.min(width, 60))}\x1b[0m`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
let line = raw;
|
|
273
|
+
// Headers
|
|
274
|
+
const h1 = line.match(/^#\s+(.+)$/);
|
|
275
|
+
if (h1) {
|
|
276
|
+
out.push(`\x1b[1;36m# ${formatInlineMarkdown(h1[1])}\x1b[0m`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const h2 = line.match(/^##\s+(.+)$/);
|
|
280
|
+
if (h2) {
|
|
281
|
+
out.push(`\x1b[1;34m## ${formatInlineMarkdown(h2[1])}\x1b[0m`);
|
|
181
282
|
continue;
|
|
182
283
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
284
|
+
const h3 = line.match(/^###\s+(.+)$/);
|
|
285
|
+
if (h3) {
|
|
286
|
+
out.push(`\x1b[1;37m### ${formatInlineMarkdown(h3[1])}\x1b[0m`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Blockquote
|
|
290
|
+
const bq = line.match(/^>\s*(.+)$/);
|
|
291
|
+
if (bq) {
|
|
292
|
+
out.push(`\x1b[90m│\x1b[0m \x1b[3m${formatInlineMarkdown(bq[1])}\x1b[0m`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
// Unordered List item
|
|
296
|
+
line = line.replace(/^(\s*)[-*+]\s+(.+)$/, (_, indent, item) => {
|
|
297
|
+
return `${indent}\x1b[36m•\x1b[0m ${formatInlineMarkdown(item)}`;
|
|
298
|
+
});
|
|
299
|
+
// Ordered List item
|
|
300
|
+
line = line.replace(/^(\s*)(\d+)\.\s+(.+)$/, (_, indent, num, item) => {
|
|
301
|
+
return `${indent}\x1b[36m${num}.\x1b[0m ${formatInlineMarkdown(item)}`;
|
|
302
|
+
});
|
|
303
|
+
// Inline formatting if not a list
|
|
304
|
+
if (!line.includes("\x1b[36m•\x1b[0m") && !line.includes(".\x1b[0m ")) {
|
|
305
|
+
line = formatInlineMarkdown(line);
|
|
306
|
+
}
|
|
307
|
+
// Word wrap paragraph line if too long
|
|
308
|
+
if (plainLen(line) > width && !line.startsWith(" ")) {
|
|
309
|
+
const words = line.split(" ");
|
|
310
|
+
let cur = "";
|
|
311
|
+
for (const w of words) {
|
|
312
|
+
if (plainLen(cur) + plainLen(w) + 1 > width) {
|
|
313
|
+
if (cur)
|
|
314
|
+
out.push(cur);
|
|
315
|
+
cur = w;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
cur = cur ? `${cur} ${w}` : w;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (cur)
|
|
322
|
+
out.push(cur);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
out.push(line);
|
|
194
326
|
}
|
|
195
|
-
// links
|
|
196
|
-
s = s.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
197
|
-
// leading list markers
|
|
198
|
-
s = s.replace(/^(\s*)([-*+]\s)/, "$1· ");
|
|
199
|
-
out.push(s);
|
|
200
327
|
}
|
|
201
|
-
|
|
328
|
+
flushTable();
|
|
329
|
+
if (inCode)
|
|
330
|
+
flushCodeBlock();
|
|
202
331
|
return out.join("\n");
|
|
203
332
|
}
|
|
204
333
|
export function aiMarkdown(text) {
|
|
@@ -212,39 +341,29 @@ export function mutedMarkdown(text) {
|
|
|
212
341
|
return markdownToAnsi(dimmed);
|
|
213
342
|
}
|
|
214
343
|
// ---------------------------------------------------------------------------
|
|
215
|
-
// Panel
|
|
344
|
+
// Panel Rendering
|
|
216
345
|
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
* Render a rich-style rounded panel.
|
|
219
|
-
*
|
|
220
|
-
* Matches rich's Panel behavior:
|
|
221
|
-
* - `expand=false` shrinks the frame to fit the content (banner/auth).
|
|
222
|
-
* - title/subtitle are embedded in the top/bottom borders, centered by
|
|
223
|
-
* default (rich default) or left-aligned.
|
|
224
|
-
*/
|
|
225
|
-
function panelString(content, title = "", subtitle = "", expand = true, borderStyle = "90", // 90 = bright black (grey50)
|
|
226
|
-
titleAlign = "center") {
|
|
346
|
+
function panelString(content, title = "", subtitle = "", expand = true, borderStyle = "90", titleAlign = "center") {
|
|
227
347
|
const styled = markupToAnsi(content);
|
|
228
348
|
const styledLines = styled.split("\n");
|
|
229
|
-
const plainLines =
|
|
230
|
-
|
|
231
|
-
let contentW = 0;
|
|
349
|
+
const plainLines = styledLines.map((l) => stripAnsi(l));
|
|
350
|
+
let maxLineW = 0;
|
|
232
351
|
for (const p of plainLines)
|
|
233
|
-
|
|
234
|
-
contentW = Math.max(contentW, 1);
|
|
235
|
-
let innerW = contentW + 2;
|
|
352
|
+
maxLineW = Math.max(maxLineW, p.length);
|
|
236
353
|
if (title)
|
|
237
|
-
|
|
354
|
+
maxLineW = Math.max(maxLineW, plainLen(title) + 4);
|
|
238
355
|
if (subtitle)
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
356
|
+
maxLineW = Math.max(maxLineW, plainLen(subtitle) + 4);
|
|
357
|
+
const termW = terminalWidth();
|
|
358
|
+
const boxW = expand
|
|
359
|
+
? Math.max(termW - 4, 20)
|
|
360
|
+
: Math.min(Math.max(maxLineW + 4, 20), termW - 4);
|
|
361
|
+
const innerW = Math.max(boxW - 4, 1);
|
|
243
362
|
const embed = (text, align) => {
|
|
244
363
|
if (!text)
|
|
245
|
-
return "─".repeat(
|
|
364
|
+
return "─".repeat(Math.max(boxW - 2, 0));
|
|
246
365
|
const inner = ` ${text} `;
|
|
247
|
-
const fill = Math.max(
|
|
366
|
+
const fill = Math.max(boxW - 2 - plainLen(inner), 0);
|
|
248
367
|
if (align === "left") {
|
|
249
368
|
return `${inner}${"─".repeat(fill)}`;
|
|
250
369
|
}
|
|
@@ -258,39 +377,39 @@ titleAlign = "center") {
|
|
|
258
377
|
for (let i = 0; i < styledLines.length; i++) {
|
|
259
378
|
const line = styledLines[i];
|
|
260
379
|
const plain = plainLines[i] ?? "";
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
380
|
+
if (plain.length > innerW) {
|
|
381
|
+
// Truncate cleanly if single line overflows max box width
|
|
382
|
+
const truncated = line.slice(0, innerW);
|
|
383
|
+
out.push(`\x1b[${borderStyle}m│\x1b[0m ${truncated} \x1b[${borderStyle}m│\x1b[0m`);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const pad = Math.max(innerW - plain.length, 0);
|
|
387
|
+
out.push(`\x1b[${borderStyle}m│\x1b[0m ${line}${" ".repeat(pad)} \x1b[${borderStyle}m│\x1b[0m`);
|
|
388
|
+
}
|
|
264
389
|
}
|
|
265
390
|
const bottom = `╰${embed(subtitle, "center")}╯`;
|
|
266
391
|
out.push(`\x1b[${borderStyle}m${bottom}\x1b[0m`);
|
|
267
392
|
return out.join("\n");
|
|
268
393
|
}
|
|
269
|
-
export function renderPanel(content, title = "", subtitle = "", expand = true, borderStyle = "90",
|
|
270
|
-
titleAlign = "center") {
|
|
394
|
+
export function renderPanel(content, title = "", subtitle = "", expand = true, borderStyle = "90", titleAlign = "center") {
|
|
271
395
|
process.stdout.write(panelString(content, title, subtitle, expand, borderStyle, titleAlign) + "\n");
|
|
272
396
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Render a rich-style bordered table panel (mirrors rich's Table + Panel).
|
|
278
|
-
*
|
|
279
|
-
* `rows` is a list of {cells, style?} where `style` (ANSI prefix) is applied to
|
|
280
|
-
* the whole row. Columns are left-aligned and padded; `colWidths` can fix a
|
|
281
|
-
* column's width (used for the selection gutter).
|
|
282
|
-
*/
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Table Panel Rendering
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
283
400
|
export function renderTableString(rows, opts = {}) {
|
|
284
401
|
const ncols = Math.max(0, ...rows.map((r) => r.cells.length));
|
|
285
|
-
if (ncols === 0)
|
|
402
|
+
if (ncols === 0) {
|
|
286
403
|
return panelString("", opts.title ?? "", opts.subtitle ?? "", opts.expand, opts.borderStyle, opts.titleAlign);
|
|
404
|
+
}
|
|
287
405
|
const colGap = opts.colGap ?? 2;
|
|
288
406
|
const widths = [];
|
|
289
407
|
for (let c = 0; c < ncols; c++) {
|
|
290
408
|
let mw = 0;
|
|
291
|
-
for (const r of rows)
|
|
409
|
+
for (const r of rows) {
|
|
292
410
|
if (r.cells[c])
|
|
293
411
|
mw = Math.max(mw, plainLen(r.cells[c]));
|
|
412
|
+
}
|
|
294
413
|
widths.push(Math.max(mw, opts.colWidths?.[c] ?? 0));
|
|
295
414
|
}
|
|
296
415
|
const contentLines = [];
|
|
@@ -307,40 +426,25 @@ export function renderTableString(rows, opts = {}) {
|
|
|
307
426
|
return panelString(contentLines.join("\n"), opts.title ?? "", opts.subtitle ?? "", opts.expand, opts.borderStyle, opts.titleAlign);
|
|
308
427
|
}
|
|
309
428
|
// ---------------------------------------------------------------------------
|
|
310
|
-
// Durations
|
|
429
|
+
// Durations & Text Formatting
|
|
311
430
|
// ---------------------------------------------------------------------------
|
|
312
431
|
export function formatDuration(seconds) {
|
|
313
|
-
const total = Math.max(0, Math.round(seconds));
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const secs =
|
|
318
|
-
|
|
319
|
-
if (hours)
|
|
320
|
-
parts.push(`${hours}h`);
|
|
321
|
-
if (minutes)
|
|
322
|
-
parts.push(`${minutes}min`);
|
|
323
|
-
if (secs || !parts.length)
|
|
324
|
-
parts.push(`${secs}s`);
|
|
325
|
-
return parts.join(" ");
|
|
432
|
+
const total = Math.max(0, Math.round(seconds * 10) / 10);
|
|
433
|
+
if (total < 60)
|
|
434
|
+
return `${total}s`;
|
|
435
|
+
const mins = Math.floor(total / 60);
|
|
436
|
+
const secs = Math.round(total % 60);
|
|
437
|
+
return `${mins}m ${secs}s`;
|
|
326
438
|
}
|
|
327
439
|
export function tickDuration(seconds) {
|
|
328
|
-
// Durations are status values, not code. Keep the surrounding text muted
|
|
329
|
-
// while making values such as `0s` readable in normal white intensity.
|
|
330
|
-
// Restore dim afterward so the following labels keep their muted styling.
|
|
331
440
|
return `\x1b[22;37m${formatDuration(seconds)}\x1b[2m`;
|
|
332
441
|
}
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// Text helpers
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
const FENCE_MARKER_RE = /`{3,}/g;
|
|
337
442
|
export function printAiChunk(chunk) {
|
|
338
|
-
// Streaming commits are slices of the model's exact text. Do not trim or
|
|
339
|
-
// append newlines here, otherwise paragraph breaks from the AI disappear.
|
|
340
443
|
if (!chunk)
|
|
341
444
|
return;
|
|
342
445
|
process.stdout.write(markdownToAnsi(chunk));
|
|
343
446
|
}
|
|
447
|
+
const FENCE_MARKER_RE = /`{3,}/g;
|
|
344
448
|
export function safeCommitPoint(text) {
|
|
345
449
|
let idx = text.lastIndexOf("\n\n");
|
|
346
450
|
while (idx !== -1) {
|
|
@@ -353,14 +457,15 @@ export function safeCommitPoint(text) {
|
|
|
353
457
|
}
|
|
354
458
|
export function truncateEllipsis(text, maxChars, label = "text") {
|
|
355
459
|
if (text.length > maxChars) {
|
|
356
|
-
return text.slice(0, maxChars) +
|
|
460
|
+
return (text.slice(0, maxChars) +
|
|
461
|
+
` …(${label} truncated: ${text.length.toLocaleString()} chars total)`);
|
|
357
462
|
}
|
|
358
463
|
return text;
|
|
359
464
|
}
|
|
360
465
|
export function displayRows(text) {
|
|
361
466
|
if (!text)
|
|
362
467
|
return 1;
|
|
363
|
-
const plain = text
|
|
468
|
+
const plain = stripAnsi(text);
|
|
364
469
|
const width = Math.max(terminalWidth() || 80, 1);
|
|
365
470
|
let rows = 0;
|
|
366
471
|
const lines = plain.split("\n");
|
|
@@ -372,7 +477,7 @@ export function displayRows(text) {
|
|
|
372
477
|
return rows;
|
|
373
478
|
}
|
|
374
479
|
// ---------------------------------------------------------------------------
|
|
375
|
-
// Cursor
|
|
480
|
+
// Cursor & Spinner
|
|
376
481
|
// ---------------------------------------------------------------------------
|
|
377
482
|
let cursorHidden = false;
|
|
378
483
|
export function hideCursor() {
|
|
@@ -388,11 +493,6 @@ export function showCursor() {
|
|
|
388
493
|
}
|
|
389
494
|
}
|
|
390
495
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
391
|
-
/**
|
|
392
|
-
* A transient status line with an animated dot spinner, updated in place
|
|
393
|
-
* (no new lines appended on every tick). On stop() the line is erased.
|
|
394
|
-
* Mirrors rich's console.status(..., spinner="dots").
|
|
395
|
-
*/
|
|
396
496
|
export class Spinner {
|
|
397
497
|
timer = null;
|
|
398
498
|
frame = 0;
|
|
@@ -415,7 +515,7 @@ export class Spinner {
|
|
|
415
515
|
this.timer = setInterval(() => {
|
|
416
516
|
this.frame++;
|
|
417
517
|
this.draw();
|
|
418
|
-
},
|
|
518
|
+
}, 80);
|
|
419
519
|
this.draw();
|
|
420
520
|
}
|
|
421
521
|
update(text) {
|
|
@@ -424,24 +524,20 @@ export class Spinner {
|
|
|
424
524
|
this.draw();
|
|
425
525
|
}
|
|
426
526
|
draw() {
|
|
427
|
-
const
|
|
527
|
+
const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
528
|
+
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[2m${this.text}\x1b[0m`;
|
|
428
529
|
const newRows = Math.max(1, displayRows(rendered));
|
|
429
530
|
const clearRows = Math.max(this.rows, newRows, 1);
|
|
430
|
-
// Cursor is always at the home position (col 0) after each draw. Clear
|
|
431
|
-
// `clearRows` lines downward from home, rewrite the content, then return
|
|
432
|
-
// the cursor to home (col 0) so the next frame overwrites in place.
|
|
433
531
|
for (let i = 0; i < clearRows; i++) {
|
|
434
532
|
process.stdout.write("\r\x1b[2K");
|
|
435
533
|
if (i < clearRows - 1)
|
|
436
534
|
process.stdout.write("\n");
|
|
437
535
|
}
|
|
438
|
-
// Cursor is now `clearRows-1` lines below home; move back to home.
|
|
439
536
|
if (clearRows > 1)
|
|
440
537
|
process.stdout.write(`\x1b[${clearRows - 1}A\r`);
|
|
441
538
|
else
|
|
442
539
|
process.stdout.write("\r");
|
|
443
540
|
process.stdout.write(rendered);
|
|
444
|
-
// Return cursor to home (col 0) for the next draw.
|
|
445
541
|
if (newRows > 1)
|
|
446
542
|
process.stdout.write(`\x1b[${newRows - 1}A\r`);
|
|
447
543
|
else
|
|
@@ -454,7 +550,6 @@ export class Spinner {
|
|
|
454
550
|
this.timer = null;
|
|
455
551
|
}
|
|
456
552
|
if (this.enabled && this.rows > 0) {
|
|
457
|
-
// Cursor is at home (col 0); clear the rendered rows downward.
|
|
458
553
|
for (let i = 0; i < this.rows; i++) {
|
|
459
554
|
process.stdout.write("\r\x1b[2K");
|
|
460
555
|
if (i < this.rows - 1)
|
|
@@ -469,7 +564,7 @@ export class Spinner {
|
|
|
469
564
|
}
|
|
470
565
|
}
|
|
471
566
|
// ---------------------------------------------------------------------------
|
|
472
|
-
// Tool
|
|
567
|
+
// Tool Action Formatter
|
|
473
568
|
// ---------------------------------------------------------------------------
|
|
474
569
|
export function formatToolAction(name, argumentsJson, status = "ok") {
|
|
475
570
|
let args = {};
|
|
@@ -492,19 +587,19 @@ export function formatToolAction(name, argumentsJson, status = "ok") {
|
|
|
492
587
|
const start = args["start_line"];
|
|
493
588
|
const end = args["end_line"];
|
|
494
589
|
const span = start != null || end != null
|
|
495
|
-
?
|
|
590
|
+
? ` (lines ${start ?? 1} to ${end ?? "end"})`
|
|
496
591
|
: "";
|
|
497
592
|
return phrase("Read", "Reading", "Failed to read", `${p}${span}`);
|
|
498
593
|
}
|
|
499
594
|
case "write_file":
|
|
500
|
-
return phrase("Wrote", "Writing", "Failed to write", `${p} (${String(args["content"] ?? "").length} chars)`);
|
|
595
|
+
return phrase("Wrote", "Writing", "Failed to write", `${p} (${String(args["content"] ?? "").length.toLocaleString()} chars)`);
|
|
501
596
|
case "edit_file": {
|
|
502
597
|
const suffix = args["replace_all"] ? " (replace all)" : "";
|
|
503
598
|
return phrase("Edited", "Editing", "Failed to edit", `${p}${suffix}`);
|
|
504
599
|
}
|
|
505
600
|
case "bash": {
|
|
506
601
|
const cwd = args["cwd"];
|
|
507
|
-
const location = cwd ? ` in ${cwd}` : "";
|
|
602
|
+
const location = cwd ? ` [in ${cwd}]` : "";
|
|
508
603
|
const command = truncateEllipsis(String(args["command"] ?? ""), 400, "command");
|
|
509
604
|
if (status === "started")
|
|
510
605
|
return `Running command${location}: ${command}`;
|
|
@@ -515,27 +610,27 @@ export function formatToolAction(name, argumentsJson, status = "ok") {
|
|
|
515
610
|
case "glob": {
|
|
516
611
|
const detail = String(args["pattern"] ?? "");
|
|
517
612
|
if (status === "failed")
|
|
518
|
-
return `Search failed for ${detail}`;
|
|
519
|
-
return phrase("Searched for", "Searching for", "", detail);
|
|
613
|
+
return `Search failed for: ${detail}`;
|
|
614
|
+
return phrase("Searched files for", "Searching files for", "Search failed for", detail);
|
|
520
615
|
}
|
|
521
616
|
case "grep": {
|
|
522
617
|
const detail = String(args["pattern"] ?? "");
|
|
523
618
|
if (status === "failed")
|
|
524
|
-
return `Grep failed for ${detail}`;
|
|
525
|
-
return phrase("Grepped for", "Grepping for", "", detail);
|
|
619
|
+
return `Grep failed for: ${detail}`;
|
|
620
|
+
return phrase("Grepped for", "Grepping for", "Grep failed for", detail);
|
|
526
621
|
}
|
|
527
622
|
case "load_skill": {
|
|
528
623
|
const skill = String(args["skill_name"] ?? "");
|
|
529
624
|
return phrase("Loaded skill", "Loading skill", "Failed to load skill", skill);
|
|
530
625
|
}
|
|
531
626
|
default:
|
|
532
|
-
return phrase("
|
|
627
|
+
return phrase("Executed", "Executing", "Failed to execute", name);
|
|
533
628
|
}
|
|
534
629
|
}
|
|
535
630
|
// ---------------------------------------------------------------------------
|
|
536
|
-
// Prompt
|
|
631
|
+
// Prompt Editing & Interactive Input
|
|
537
632
|
// ---------------------------------------------------------------------------
|
|
538
|
-
export const PROMPT_PLACEHOLDER = "Describe a coding task, or type /help
|
|
633
|
+
export const PROMPT_PLACEHOLDER = "Describe a coding task, or type /help for commands";
|
|
539
634
|
const MAX_PROMPT_DISPLAY_LINES = 12;
|
|
540
635
|
const MAX_PROMPT_PASTE_CHARS = 400;
|
|
541
636
|
function shouldCollapsePaste(text) {
|
|
@@ -577,13 +672,7 @@ export function splitBlocks(buffer, pasteSpans) {
|
|
|
577
672
|
}
|
|
578
673
|
return segs;
|
|
579
674
|
}
|
|
580
|
-
const PROMPT_CARET = "\u0000";
|
|
581
|
-
/**
|
|
582
|
-
* Wrap styled runs into physical rows of `width` visible columns, wrapping long
|
|
583
|
-
* lines so the panel box stays rectangular (mirrors rich's Panel auto-wrap).
|
|
584
|
-
* Returns the styled rows (no borders) and the caret's (row, col) in content
|
|
585
|
-
* coordinates, or (-1,-1) if no caret sentinel is present.
|
|
586
|
-
*/
|
|
675
|
+
const PROMPT_CARET = "\u0000";
|
|
587
676
|
function wrapRuns(runs, width) {
|
|
588
677
|
const rows = [];
|
|
589
678
|
let cur = "";
|
|
@@ -597,8 +686,6 @@ function wrapRuns(runs, width) {
|
|
|
597
686
|
};
|
|
598
687
|
for (const run of runs) {
|
|
599
688
|
if (run.text === PROMPT_CARET) {
|
|
600
|
-
// Caret occupies one visible column; if the current row is full, move it
|
|
601
|
-
// to the start of the next row so it stays inside the box.
|
|
602
689
|
if (curLen >= width)
|
|
603
690
|
flush();
|
|
604
691
|
caretRow = rows.length;
|
|
@@ -609,7 +696,6 @@ function wrapRuns(runs, width) {
|
|
|
609
696
|
}
|
|
610
697
|
let text = run.text;
|
|
611
698
|
while (text) {
|
|
612
|
-
// Split off the leading run up to the next newline (if any).
|
|
613
699
|
const nl = text.indexOf("\n");
|
|
614
700
|
const seg = nl === -1 ? text : text.slice(0, nl);
|
|
615
701
|
let rest = seg;
|
|
@@ -617,9 +703,6 @@ function wrapRuns(runs, width) {
|
|
|
617
703
|
if (curLen >= width)
|
|
618
704
|
flush();
|
|
619
705
|
let take = Math.min(rest.length, width - curLen);
|
|
620
|
-
// Keep normal text readable by wrapping at a word boundary. A single
|
|
621
|
-
// oversized token still falls back to a hard wrap so the frame stays
|
|
622
|
-
// within its width.
|
|
623
706
|
if (take < rest.length && curLen > 0) {
|
|
624
707
|
const lastSpace = rest.slice(0, take).lastIndexOf(" ");
|
|
625
708
|
if (lastSpace > 0)
|
|
@@ -643,32 +726,26 @@ function wrapRuns(runs, width) {
|
|
|
643
726
|
return { rows, caretRow, caretCol };
|
|
644
727
|
}
|
|
645
728
|
export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor) {
|
|
646
|
-
const
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
const innerW = borderW - 2;
|
|
650
|
-
// Build styled runs: prefix, then the text with the `▏` block caret always
|
|
651
|
-
// drawn at the cursor position (mirrors the original rich frame).
|
|
729
|
+
const termW = terminalWidth();
|
|
730
|
+
const boxW = Math.max(termW - 4, 16);
|
|
731
|
+
const innerW = boxW - 4;
|
|
652
732
|
const runs = [];
|
|
653
|
-
runs.push({ text: prefix, style: "\x1b[
|
|
733
|
+
runs.push({ text: prefix, style: "\x1b[1;36m" });
|
|
654
734
|
runs.push({ text: " ", style: "" });
|
|
655
735
|
if (!buffer) {
|
|
656
|
-
runs.push({ text: PROMPT_CARET, style: "\x1b[
|
|
657
|
-
runs.push({ text: PROMPT_PLACEHOLDER, style: "\x1b[
|
|
736
|
+
runs.push({ text: PROMPT_CARET, style: "\x1b[1;36m" });
|
|
737
|
+
runs.push({ text: PROMPT_PLACEHOLDER, style: "\x1b[2;37m" });
|
|
658
738
|
}
|
|
659
739
|
else {
|
|
660
740
|
const segs = splitBlocks(buffer, pasteSpans);
|
|
661
|
-
// Recompute each segment's buffer range [a, b) so we can locate the cursor.
|
|
662
741
|
const ranges = [];
|
|
663
742
|
{
|
|
664
|
-
let pos = 0;
|
|
665
743
|
const pts = new Set([0, buffer.length]);
|
|
666
744
|
for (const [s, e] of pasteSpans) {
|
|
667
745
|
pts.add(s);
|
|
668
746
|
pts.add(e);
|
|
669
747
|
}
|
|
670
748
|
const sorted = [...pts].sort((a, b) => a - b);
|
|
671
|
-
const isPaste = (a, b) => pasteSpans.some(([s, e]) => s <= a && b <= e);
|
|
672
749
|
const used = new Set();
|
|
673
750
|
for (const seg of segs) {
|
|
674
751
|
for (let i = 0; i < sorted.length - 1; i++) {
|
|
@@ -682,12 +759,13 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
682
759
|
}
|
|
683
760
|
}
|
|
684
761
|
}
|
|
685
|
-
// Determine the target segment + inside offset (mirrors Python).
|
|
686
762
|
let target = -1;
|
|
687
763
|
let inside = 0;
|
|
688
764
|
if (cursor >= buffer.length) {
|
|
689
765
|
target = segs.length - 1;
|
|
690
|
-
inside = ranges.length
|
|
766
|
+
inside = ranges.length
|
|
767
|
+
? ranges[ranges.length - 1][1] - ranges[ranges.length - 1][0]
|
|
768
|
+
: 0;
|
|
691
769
|
}
|
|
692
770
|
else {
|
|
693
771
|
for (let i = 0; i < ranges.length; i++) {
|
|
@@ -700,7 +778,9 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
700
778
|
}
|
|
701
779
|
if (target === -1) {
|
|
702
780
|
target = segs.length - 1;
|
|
703
|
-
inside = ranges.length
|
|
781
|
+
inside = ranges.length
|
|
782
|
+
? ranges[ranges.length - 1][1] - ranges[ranges.length - 1][0]
|
|
783
|
+
: 0;
|
|
704
784
|
}
|
|
705
785
|
}
|
|
706
786
|
for (let i = 0; i < segs.length; i++) {
|
|
@@ -709,41 +789,41 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
709
789
|
runs.push({ text: " ", style: "" });
|
|
710
790
|
if (i === target) {
|
|
711
791
|
if (kind === "collapsed") {
|
|
712
|
-
runs.push({ text: disp, style: "\x1b[
|
|
792
|
+
runs.push({ text: disp, style: "\x1b[1;36m" });
|
|
713
793
|
if (!endsWithWs(disp))
|
|
714
794
|
runs.push({ text: " ", style: "" });
|
|
715
|
-
runs.push({ text: PROMPT_CARET, style: "\x1b[
|
|
795
|
+
runs.push({ text: PROMPT_CARET, style: "\x1b[1;36m" });
|
|
716
796
|
}
|
|
717
797
|
else {
|
|
718
798
|
const before = disp.slice(0, inside);
|
|
719
799
|
const after = disp.slice(inside);
|
|
720
800
|
if (before)
|
|
721
801
|
runs.push({ text: before, style: "" });
|
|
722
|
-
runs.push({ text: PROMPT_CARET, style: "\x1b[
|
|
802
|
+
runs.push({ text: PROMPT_CARET, style: "\x1b[1;36m" });
|
|
723
803
|
if (after)
|
|
724
804
|
runs.push({ text: after, style: "" });
|
|
725
805
|
}
|
|
726
806
|
}
|
|
727
807
|
else {
|
|
728
|
-
runs.push({
|
|
808
|
+
runs.push({
|
|
809
|
+
text: disp,
|
|
810
|
+
style: kind === "collapsed" ? "\x1b[1;36m" : "",
|
|
811
|
+
});
|
|
729
812
|
}
|
|
730
813
|
}
|
|
731
814
|
}
|
|
732
815
|
const { rows, caretRow, caretCol } = wrapRuns(runs, innerW);
|
|
733
|
-
|
|
734
|
-
const topPad = Math.max(
|
|
735
|
-
const top = `\x1b[90m╭─ ${label} ${"─".repeat(topPad)}╮\x1b[0m`;
|
|
736
|
-
// Body rows.
|
|
816
|
+
const labelLen = plainLen(label);
|
|
817
|
+
const topPad = Math.max(0, boxW - labelLen - 5);
|
|
818
|
+
const top = `\x1b[90m╭─ \x1b[1m\x1b[36m${label}\x1b[0m\x1b[90m ${"─".repeat(topPad)}╮\x1b[0m`;
|
|
737
819
|
const body = [];
|
|
738
820
|
for (const l of rows) {
|
|
739
|
-
const plain = l
|
|
740
|
-
const pad = Math.max(
|
|
821
|
+
const plain = stripAnsi(l);
|
|
822
|
+
const pad = Math.max(0, innerW - plain.length);
|
|
741
823
|
body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
|
|
742
824
|
}
|
|
743
|
-
const bottom = `\x1b[90m╰${"─".repeat(
|
|
825
|
+
const bottom = `\x1b[90m╰${"─".repeat(Math.max(0, boxW - 2))}╯\x1b[0m`;
|
|
744
826
|
const frame = [top, ...body, bottom].join("\n");
|
|
745
|
-
// Cursor placement: one row below the top border. The body prefix (`│ `)
|
|
746
|
-
// shifts the content right by 2 columns, so add 2 to the wrapped caret col.
|
|
747
827
|
const cursorRow = (caretRow === -1 ? 0 : caretRow) + 1;
|
|
748
828
|
const cursorCol = (caretCol === -1 ? 0 : caretCol) + 2;
|
|
749
829
|
const totalRows = body.length + 2;
|
|
@@ -753,14 +833,14 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
753
833
|
return new Promise((resolve, reject) => {
|
|
754
834
|
const isTTY = process.stdin.isTTY;
|
|
755
835
|
if (!isTTY) {
|
|
756
|
-
// Non-interactive: read a line from stdin
|
|
757
836
|
const chunks = [];
|
|
758
837
|
process.stdin.setEncoding("utf-8");
|
|
759
838
|
process.stdin.on("data", (d) => chunks.push(Buffer.from(String(d))));
|
|
760
839
|
process.stdin.on("end", () => {
|
|
761
|
-
const text = Buffer.concat(chunks)
|
|
762
|
-
|
|
763
|
-
|
|
840
|
+
const text = Buffer.concat(chunks)
|
|
841
|
+
.toString()
|
|
842
|
+
.replace(/\r\n/g, "\n")
|
|
843
|
+
.replace(/\r/g, "\n");
|
|
764
844
|
resolve([text.endsWith("\n") ? text.slice(0, -1) : text, []]);
|
|
765
845
|
});
|
|
766
846
|
return;
|
|
@@ -778,12 +858,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
778
858
|
readline.emitKeypressEvents(process.stdin);
|
|
779
859
|
if (process.stdin.isTTY) {
|
|
780
860
|
process.stdin.setRawMode(true);
|
|
781
|
-
// Ask the terminal to wrap clipboard input in explicit paste markers.
|
|
782
|
-
// This makes embedded newlines unambiguous instead of relying on timing.
|
|
783
861
|
process.stdout.write("\x1b[?2004h");
|
|
784
|
-
// Enable the kitty keyboard protocol (CSI u) so Windows Terminal / modern
|
|
785
|
-
// terminals send distinct sequences for Shift+Enter / Ctrl+Enter
|
|
786
|
-
// (\x1b[13;<mod>u) instead of an indistinguishable plain \r.
|
|
787
862
|
process.stdout.write("\x1b[>1u");
|
|
788
863
|
}
|
|
789
864
|
process.stdin.resume();
|
|
@@ -792,11 +867,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
792
867
|
let lastCursorRow = 0;
|
|
793
868
|
let lastTotalRows = 0;
|
|
794
869
|
let prevFrameLines = null;
|
|
795
|
-
// Erase the prompt region: a leading blank line + the box. Cursor sits at
|
|
796
|
-
// lastCursorRow inside the box; move up to the box's top border, erase the
|
|
797
|
-
// box rows, then erase the leading blank line, leaving the cursor on that
|
|
798
|
-
// blank row. The caller's single leading newline then yields exactly ONE
|
|
799
|
-
// blank row before the echoed user prompt / message.
|
|
800
870
|
const clearBox = () => {
|
|
801
871
|
process.stdout.write(`\x1b[${lastCursorRow}A`);
|
|
802
872
|
for (let i = 0; i < lastTotalRows; i++) {
|
|
@@ -828,11 +898,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
828
898
|
const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor);
|
|
829
899
|
const newLines = frame.split("\n");
|
|
830
900
|
if (isFirst) {
|
|
831
|
-
// The caller's content always ends with "\n", so the cursor sits at the
|
|
832
|
-
// start of a fresh (blank) line — that line is the leading blank
|
|
833
|
-
// separator (mirrors the original's Group(Text(""), panel)). Move down
|
|
834
|
-
// one line so the box starts below that blank; do NOT emit a second
|
|
835
|
-
// blank here (it caused a double gap above the box / on cancel).
|
|
836
901
|
process.stdout.write("\n");
|
|
837
902
|
for (let i = 0; i < newLines.length; i++) {
|
|
838
903
|
process.stdout.write(newLines[i]);
|
|
@@ -842,16 +907,10 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
842
907
|
prevFrameLines = newLines;
|
|
843
908
|
lastCursorRow = cursorRow;
|
|
844
909
|
lastTotalRows = totalRows;
|
|
845
|
-
// Cursor sits on the bottom border line; move it up to the input row.
|
|
846
910
|
const up = totalRows - 1 - cursorRow;
|
|
847
911
|
process.stdout.write(`\x1b[${up}A\r\x1b[${cursorCol}C`);
|
|
848
|
-
return;
|
|
849
912
|
}
|
|
850
913
|
else {
|
|
851
|
-
// Cursor currently sits at the previous input position (lastCursorRow
|
|
852
|
-
// inside the box). Move up to the top border, then rewrite ONLY the
|
|
853
|
-
// lines whose content actually changed, leaving the static borders
|
|
854
|
-
// untouched — this avoids the whole-box flicker on every keystroke.
|
|
855
914
|
process.stdout.write(`\x1b[${lastCursorRow}A\r`);
|
|
856
915
|
const oldLines = prevFrameLines ?? [];
|
|
857
916
|
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
@@ -862,8 +921,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
862
921
|
if (i < maxLen - 1)
|
|
863
922
|
process.stdout.write("\n");
|
|
864
923
|
}
|
|
865
|
-
// Cursor now sits at line (maxLen-1), column 0. Move it to the target
|
|
866
|
-
// input row (cursorRow), handling line-count changes in either direction.
|
|
867
924
|
const atLine = maxLen - 1;
|
|
868
925
|
if (atLine > cursorRow)
|
|
869
926
|
process.stdout.write(`\x1b[${atLine - cursorRow}A`);
|
|
@@ -873,7 +930,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
873
930
|
prevFrameLines = newLines;
|
|
874
931
|
lastCursorRow = cursorRow;
|
|
875
932
|
lastTotalRows = totalRows;
|
|
876
|
-
return;
|
|
877
933
|
}
|
|
878
934
|
};
|
|
879
935
|
const shiftSpansAfterInsert = (pos, delta) => {
|
|
@@ -897,12 +953,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
897
953
|
}
|
|
898
954
|
const pasteStart = cursor;
|
|
899
955
|
buffer = buffer.slice(0, cursor) + text + buffer.slice(cursor);
|
|
900
|
-
// Keep existing paste spans valid across the insertion point.
|
|
901
956
|
pasteSpans = shiftSpansAfterInsert(cursor, text.length);
|
|
902
|
-
// A real paste records a span so the prompt box can collapse it (mirrors
|
|
903
|
-
// Python's handle_paste). splitBlocks only collapses segments inside a
|
|
904
|
-
// span that meet the threshold, so small pastes stay expanded but remain
|
|
905
|
-
// tagged for cursor/backspace handling.
|
|
906
957
|
if (isPaste) {
|
|
907
958
|
pasteSpans.push([pasteStart, pasteStart + text.length]);
|
|
908
959
|
pasteSpans.sort((a, b) => a[0] - b[0]);
|
|
@@ -916,9 +967,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
916
967
|
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
917
968
|
histIdx = hist.length;
|
|
918
969
|
}
|
|
919
|
-
// A collapsed paste is one editable unit. Backspace removes the entire
|
|
920
|
-
// pasted block, matching the Python prompt rather than deleting only its
|
|
921
|
-
// final character.
|
|
922
970
|
const containing = pasteSpans.find(([s, e]) => s < cursor && cursor <= e);
|
|
923
971
|
if (containing) {
|
|
924
972
|
const [start, end] = containing;
|
|
@@ -930,14 +978,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
930
978
|
cursor = start;
|
|
931
979
|
return;
|
|
932
980
|
}
|
|
933
|
-
// remove one grapheme before cursor
|
|
934
981
|
const before = Array.from(buffer.slice(0, cursor));
|
|
935
982
|
before.pop();
|
|
936
983
|
buffer = before.join("") + buffer.slice(cursor);
|
|
937
984
|
const deletedAt = cursor - 1;
|
|
938
985
|
cursor -= 1;
|
|
939
|
-
// Adjust spans: shrink any span covering the deleted char, shift spans
|
|
940
|
-
// that start after it left by one.
|
|
941
986
|
const adjusted = [];
|
|
942
987
|
for (const [s, e] of pasteSpans) {
|
|
943
988
|
if (deletedAt >= s && deletedAt < e) {
|
|
@@ -954,11 +999,39 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
954
999
|
}
|
|
955
1000
|
pasteSpans = adjusted.filter(([s, e]) => s < e);
|
|
956
1001
|
};
|
|
1002
|
+
const deleteChar = () => {
|
|
1003
|
+
if (cursor >= buffer.length)
|
|
1004
|
+
return;
|
|
1005
|
+
if (histIdx !== hist.length) {
|
|
1006
|
+
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
1007
|
+
histIdx = hist.length;
|
|
1008
|
+
}
|
|
1009
|
+
const containing = pasteSpans.find(([s, e]) => s <= cursor && cursor < e);
|
|
1010
|
+
if (containing) {
|
|
1011
|
+
const [start, end] = containing;
|
|
1012
|
+
buffer = buffer.slice(0, start) + buffer.slice(end);
|
|
1013
|
+
const delta = start - end;
|
|
1014
|
+
pasteSpans = pasteSpans
|
|
1015
|
+
.filter(([s, e]) => s !== start || e !== end)
|
|
1016
|
+
.map(([s, e]) => (s >= end ? [s + delta, e + delta] : [s, e]));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1);
|
|
1020
|
+
};
|
|
1021
|
+
const deleteWordBefore = () => {
|
|
1022
|
+
if (cursor <= 0)
|
|
1023
|
+
return;
|
|
1024
|
+
const before = buffer.slice(0, cursor);
|
|
1025
|
+
const trimmed = before.replace(/\s+$/, "");
|
|
1026
|
+
const lastSpace = Math.max(trimmed.lastIndexOf(" "), trimmed.lastIndexOf("\t"), trimmed.lastIndexOf("\n"));
|
|
1027
|
+
const targetPos = lastSpace === -1 ? 0 : lastSpace + 1;
|
|
1028
|
+
buffer = buffer.slice(0, targetPos) + buffer.slice(cursor);
|
|
1029
|
+
cursor = targetPos;
|
|
1030
|
+
pasteSpans = [];
|
|
1031
|
+
};
|
|
957
1032
|
const moveLeft = () => {
|
|
958
1033
|
if (cursor <= 0)
|
|
959
1034
|
return;
|
|
960
|
-
// Jump the caret to the start of a paste span it sits just past (mirrors
|
|
961
|
-
// Python), so a collapsed block is treated as one unit.
|
|
962
1035
|
for (const [s, e] of pasteSpans) {
|
|
963
1036
|
if (s < cursor && cursor <= e) {
|
|
964
1037
|
cursor = s;
|
|
@@ -980,7 +1053,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
980
1053
|
};
|
|
981
1054
|
const moveUp = () => {
|
|
982
1055
|
if (!buffer.includes("\n")) {
|
|
983
|
-
// Single row: navigate command history.
|
|
984
1056
|
if (!hist.length)
|
|
985
1057
|
return;
|
|
986
1058
|
if (histIdx === hist.length) {
|
|
@@ -994,7 +1066,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
994
1066
|
}
|
|
995
1067
|
return;
|
|
996
1068
|
}
|
|
997
|
-
// Multi row: move the caret up to the previous line, preserving column.
|
|
998
1069
|
if (cursor > 0) {
|
|
999
1070
|
let lineStart = buffer.lastIndexOf("\n", cursor - 1);
|
|
1000
1071
|
lineStart = lineStart !== -1 ? lineStart : 0;
|
|
@@ -1006,7 +1077,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1006
1077
|
};
|
|
1007
1078
|
const moveDown = () => {
|
|
1008
1079
|
if (!buffer.includes("\n")) {
|
|
1009
|
-
// Single row: navigate command history.
|
|
1010
1080
|
if (histIdx === hist.length || !draft)
|
|
1011
1081
|
return;
|
|
1012
1082
|
histIdx += 1;
|
|
@@ -1022,7 +1092,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1022
1092
|
}
|
|
1023
1093
|
return;
|
|
1024
1094
|
}
|
|
1025
|
-
// Multi row: move the caret down to the next line, preserving column.
|
|
1026
1095
|
if (cursor < buffer.length) {
|
|
1027
1096
|
const [, col] = cursorLineCol(buffer, cursor);
|
|
1028
1097
|
let nextStart = buffer.indexOf("\n", cursor);
|
|
@@ -1040,23 +1109,17 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1040
1109
|
const isNewlineKey = (key) => {
|
|
1041
1110
|
if (!key)
|
|
1042
1111
|
return false;
|
|
1043
|
-
// Bare \n / Ctrl+J = newline.
|
|
1044
1112
|
if (key.ctrl && key.name === "j")
|
|
1045
1113
|
return true;
|
|
1046
1114
|
if (key.name === "enter")
|
|
1047
1115
|
return !!(key.shift || key.ctrl || key.meta);
|
|
1048
|
-
// Enter with a modifier (Shift/Ctrl/Alt/Meta) = newline.
|
|
1049
1116
|
if (key.name === "return")
|
|
1050
1117
|
return !!(key.shift || key.ctrl || key.meta);
|
|
1051
|
-
// Kitty-keyboard-protocol / CSI-u modified Enter: \x1b[13;<m>u where
|
|
1052
|
-
// <m> is a modifier bitmask (2=Shift, 3=Alt, 5=Ctrl, 6=Shift+Ctrl, ...).
|
|
1053
|
-
// Also the legacy \x1b[13;<m>~ form. ANY modifier > 1 on Enter = newline.
|
|
1054
1118
|
const seq = key.sequence || "";
|
|
1055
1119
|
if (/^\x1b\[13;([2-9]|\d{2,})u$/.test(seq))
|
|
1056
1120
|
return true;
|
|
1057
1121
|
if (/^\x1b\[13;([2-9]|\d{2,})~$/.test(seq))
|
|
1058
1122
|
return true;
|
|
1059
|
-
// Legacy Shift+Enter decoded by Node as F3 with shift.
|
|
1060
1123
|
if (key.name === "f3" && key.shift)
|
|
1061
1124
|
return true;
|
|
1062
1125
|
return false;
|
|
@@ -1064,23 +1127,20 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1064
1127
|
const isModifiedEnterInput = (str, key) => {
|
|
1065
1128
|
const value = str || "";
|
|
1066
1129
|
const sequence = key?.sequence || "";
|
|
1067
|
-
// Some Windows terminals do not preserve modifier flags when readline
|
|
1068
|
-
// decodes Ctrl+J or Ctrl/Shift+Enter. Inspect the original bytes too.
|
|
1069
1130
|
if (value === "\n" || value === "\x0a")
|
|
1070
1131
|
return true;
|
|
1071
1132
|
if (key?.name === "linefeed" || (key?.ctrl && key?.name === "j"))
|
|
1072
1133
|
return true;
|
|
1073
|
-
return /^\x1b\[13;(?:[2-9]|\d{2,})(?:u|~)$/.test(value) ||
|
|
1074
|
-
/^\x1b\[13;(?:[2-9]|\d{2,})(?:u|~)$/.test(sequence);
|
|
1134
|
+
return (/^\x1b\[13;(?:[2-9]|\d{2,})(?:u|~)$/.test(value) ||
|
|
1135
|
+
/^\x1b\[13;(?:[2-9]|\d{2,})(?:u|~)$/.test(sequence));
|
|
1075
1136
|
};
|
|
1076
|
-
// True for a plain (unmodified) Enter: Node's "return" without modifiers,
|
|
1077
|
-
// or the kitty-protocol encodings \x1b[13u / \x1b[13;1u (modifier flags = 1
|
|
1078
|
-
// meaning none).
|
|
1079
1137
|
const isSubmitEnterKey = (key) => {
|
|
1080
1138
|
if (!key)
|
|
1081
1139
|
return false;
|
|
1082
|
-
if ((key.name === "return" || key.name === "enter") &&
|
|
1140
|
+
if ((key.name === "return" || key.name === "enter") &&
|
|
1141
|
+
!(key.shift || key.ctrl || key.meta)) {
|
|
1083
1142
|
return true;
|
|
1143
|
+
}
|
|
1084
1144
|
const seq = key.sequence || "";
|
|
1085
1145
|
if (seq === "\x1b[13u" || /^\x1b\[13;1u$/.test(seq))
|
|
1086
1146
|
return true;
|
|
@@ -1094,12 +1154,19 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1094
1154
|
if (!bracketedPaste && (sequence === start || value === start)) {
|
|
1095
1155
|
bracketedPaste = true;
|
|
1096
1156
|
bracketedPasteBuffer = "";
|
|
1097
|
-
const inline = value === start
|
|
1157
|
+
const inline = value === start
|
|
1158
|
+
? ""
|
|
1159
|
+
: value.startsWith(start)
|
|
1160
|
+
? value.slice(start.length)
|
|
1161
|
+
: "";
|
|
1098
1162
|
if (inline)
|
|
1099
1163
|
bracketedPasteBuffer = inline;
|
|
1100
1164
|
if (inline.includes(end)) {
|
|
1101
1165
|
const endAt = inline.indexOf(end);
|
|
1102
|
-
const pasted = inline
|
|
1166
|
+
const pasted = inline
|
|
1167
|
+
.slice(0, endAt)
|
|
1168
|
+
.replace(/\r\n/g, "\n")
|
|
1169
|
+
.replace(/\r/g, "\n");
|
|
1103
1170
|
bracketedPaste = false;
|
|
1104
1171
|
bracketedPasteBuffer = "";
|
|
1105
1172
|
if (pasted)
|
|
@@ -1111,7 +1178,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1111
1178
|
if (!bracketedPaste)
|
|
1112
1179
|
return false;
|
|
1113
1180
|
if (sequence === end || value === end) {
|
|
1114
|
-
const pasted = bracketedPasteBuffer
|
|
1181
|
+
const pasted = bracketedPasteBuffer
|
|
1182
|
+
.replace(/\r\n/g, "\n")
|
|
1183
|
+
.replace(/\r/g, "\n");
|
|
1115
1184
|
bracketedPaste = false;
|
|
1116
1185
|
bracketedPasteBuffer = "";
|
|
1117
1186
|
if (pasted)
|
|
@@ -1119,12 +1188,15 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1119
1188
|
repaint();
|
|
1120
1189
|
return true;
|
|
1121
1190
|
}
|
|
1122
|
-
const chunk = value ||
|
|
1191
|
+
const chunk = value ||
|
|
1192
|
+
(sequence && !sequence.startsWith("\x1b[") ? sequence : "");
|
|
1123
1193
|
if (chunk) {
|
|
1124
1194
|
const endAt = chunk.indexOf(end);
|
|
1125
1195
|
if (endAt >= 0) {
|
|
1126
1196
|
bracketedPasteBuffer += chunk.slice(0, endAt);
|
|
1127
|
-
const pasted = bracketedPasteBuffer
|
|
1197
|
+
const pasted = bracketedPasteBuffer
|
|
1198
|
+
.replace(/\r\n/g, "\n")
|
|
1199
|
+
.replace(/\r/g, "\n");
|
|
1128
1200
|
bracketedPaste = false;
|
|
1129
1201
|
bracketedPasteBuffer = "";
|
|
1130
1202
|
if (pasted)
|
|
@@ -1149,9 +1221,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1149
1221
|
const onKeypress = (str, key) => {
|
|
1150
1222
|
if (consumeBracketedPaste(str, key))
|
|
1151
1223
|
return;
|
|
1152
|
-
// A normal typed character after an unbracketed paste ends the fallback
|
|
1153
|
-
// window, so the next Enter can submit immediately. Multi-character
|
|
1154
|
-
// clipboard chunks keep the window alive until their rows arrive.
|
|
1155
1224
|
if (pasteBurst && str && str.length === 1 && !key?.ctrl && !key?.meta) {
|
|
1156
1225
|
pasteBurst = false;
|
|
1157
1226
|
if (pasteBurstTimer)
|
|
@@ -1166,18 +1235,40 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1166
1235
|
settle(new Error("eof"), true);
|
|
1167
1236
|
return;
|
|
1168
1237
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1238
|
+
if (key && key.ctrl && key.name === "a") {
|
|
1239
|
+
cursor = 0;
|
|
1240
|
+
repaint();
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (key && key.ctrl && key.name === "e") {
|
|
1244
|
+
cursor = buffer.length;
|
|
1245
|
+
repaint();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (key && key.ctrl && key.name === "u") {
|
|
1249
|
+
buffer = "";
|
|
1250
|
+
cursor = 0;
|
|
1251
|
+
pasteSpans = [];
|
|
1252
|
+
repaint();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (key && key.ctrl && key.name === "k") {
|
|
1256
|
+
buffer = buffer.slice(0, cursor);
|
|
1257
|
+
repaint();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (key && key.ctrl && key.name === "w") {
|
|
1261
|
+
deleteWordBefore();
|
|
1262
|
+
repaint();
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (isNewlineKey(key) ||
|
|
1266
|
+
isModifiedEnterInput(str, key) ||
|
|
1267
|
+
(pasteBurst && isSubmitEnterKey(key))) {
|
|
1174
1268
|
insert("\n");
|
|
1175
1269
|
repaint();
|
|
1176
1270
|
return;
|
|
1177
1271
|
}
|
|
1178
|
-
// Plain Enter submits; any modified Enter (Shift/Ctrl+Enter) inserts a
|
|
1179
|
-
// newline, mirroring the Python original's _read_input_event (which maps
|
|
1180
|
-
// \x1b[13;2u / \x1b[13;5u / Shift+\r to "\n").
|
|
1181
1272
|
if (isSubmitEnterKey(key)) {
|
|
1182
1273
|
if (buffer.trim()) {
|
|
1183
1274
|
settle([buffer, pasteSpans], false);
|
|
@@ -1189,6 +1280,21 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1189
1280
|
repaint();
|
|
1190
1281
|
return;
|
|
1191
1282
|
}
|
|
1283
|
+
if (key && (key.name === "delete" || key.sequence === "\x1b[3~")) {
|
|
1284
|
+
deleteChar();
|
|
1285
|
+
repaint();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (key && (key.name === "home" || key.sequence === "\x1b[H" || key.sequence === "\x1b[1~")) {
|
|
1289
|
+
cursor = 0;
|
|
1290
|
+
repaint();
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (key && (key.name === "end" || key.sequence === "\x1b[F" || key.sequence === "\x1b[4~")) {
|
|
1294
|
+
cursor = buffer.length;
|
|
1295
|
+
repaint();
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1192
1298
|
if (key && key.name === "left") {
|
|
1193
1299
|
moveLeft();
|
|
1194
1300
|
repaint();
|
|
@@ -1210,7 +1316,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1210
1316
|
return;
|
|
1211
1317
|
}
|
|
1212
1318
|
if (key && key.name === "tab") {
|
|
1213
|
-
insert("
|
|
1319
|
+
insert(" ");
|
|
1214
1320
|
repaint();
|
|
1215
1321
|
return;
|
|
1216
1322
|
}
|
|
@@ -1220,8 +1326,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1220
1326
|
return;
|
|
1221
1327
|
}
|
|
1222
1328
|
if (str) {
|
|
1223
|
-
// Multi-char sequence = a paste (single-char typed keys arrive one per
|
|
1224
|
-
// event). Tag it so the prompt box collapses it when it meets the rules.
|
|
1225
1329
|
const isPaste = str.length > 1;
|
|
1226
1330
|
insert(str.replace(/\r\n/g, "\n").replace(/\r/g, "\n"), isPaste);
|
|
1227
1331
|
if (isPaste)
|
|
@@ -1234,14 +1338,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1234
1338
|
});
|
|
1235
1339
|
}
|
|
1236
1340
|
// ---------------------------------------------------------------------------
|
|
1237
|
-
// User
|
|
1341
|
+
// User Display & Message Text Helpers
|
|
1238
1342
|
// ---------------------------------------------------------------------------
|
|
1239
1343
|
export function userDisplayText(payload, pasteSpans) {
|
|
1240
|
-
// Mirror the prompt field exactly: collapse ONLY segments that fall inside a
|
|
1241
|
-
// paste span and exceed the collapse threshold (via splitBlocks). Content that
|
|
1242
|
-
// the user typed (no paste span) is never collapsed in the prompt, so it must
|
|
1243
|
-
// not be collapsed here either — keeping the echoed message consistent with
|
|
1244
|
-
// what the prompt box displayed.
|
|
1245
1344
|
if (pasteSpans && pasteSpans.length) {
|
|
1246
1345
|
const segs = splitBlocks(payload, pasteSpans);
|
|
1247
1346
|
let out = "";
|
|
@@ -1249,7 +1348,7 @@ export function userDisplayText(payload, pasteSpans) {
|
|
|
1249
1348
|
const [, kind, disp] = segs[i];
|
|
1250
1349
|
if (i && !endsWithWs(segs[i - 1][2]))
|
|
1251
1350
|
out += " ";
|
|
1252
|
-
out += kind === "collapsed" ? `\x1b[
|
|
1351
|
+
out += kind === "collapsed" ? `\x1b[1;36m${disp}\x1b[0m` : disp;
|
|
1253
1352
|
}
|
|
1254
1353
|
return out;
|
|
1255
1354
|
}
|