@oxecli/oxe 1.0.69 → 1.0.71
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 +17 -17
- package/dist/engine.js +2 -2
- package/dist/tools.js +3 -3
- package/dist/ui.js +32 -376
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -41,15 +41,15 @@ export class CLI {
|
|
|
41
41
|
const description = "A focused terminal workspace for building and shipping software.";
|
|
42
42
|
renderPanel("[bold white]Terminal Coding Agent[/bold white]\n" +
|
|
43
43
|
`[dim]${description}[/dim]\n\n` +
|
|
44
|
-
labelCell("
|
|
44
|
+
labelCell("") +
|
|
45
45
|
labelCell("Model") +
|
|
46
46
|
labelCell("Status") +
|
|
47
47
|
"\n" +
|
|
48
|
-
valueCell(
|
|
49
|
-
valueCell(model, "
|
|
48
|
+
valueCell("", "cyan") +
|
|
49
|
+
valueCell(model, "cyan") +
|
|
50
50
|
valueCell("Ready", "green") +
|
|
51
51
|
"\n\n" +
|
|
52
|
-
`[dim]Environment: ${runtimeOsSummary()}[/dim]`,
|
|
52
|
+
`[dim]Environment: ${runtimeOsSummary()}[/dim]`, `\x1b[1;37mOxe\x1b[90m \x1b[1;36mv${version}\x1b[90m`, "Type /help for commands · Ctrl+C to interrupt", false, "90", "left");
|
|
53
53
|
}
|
|
54
54
|
saveSession(engine) {
|
|
55
55
|
if (!this.inputItems.length)
|
|
@@ -88,7 +88,7 @@ export class CLI {
|
|
|
88
88
|
const rows = COMMAND_HELP.map(([cmd, desc]) => {
|
|
89
89
|
const styled = cmd.replace(/(<[^>]+>)/g, "\x1b[36m$1\x1b[0m");
|
|
90
90
|
return {
|
|
91
|
-
cells: [`\x1b[1;36m${styled}\x1b[0m`, `\x1b[
|
|
91
|
+
cells: [`\x1b[1;36m${styled}\x1b[0m`, `\x1b[90m${desc}\x1b[0m`],
|
|
92
92
|
};
|
|
93
93
|
});
|
|
94
94
|
const panel = renderTableString(rows, {
|
|
@@ -181,7 +181,7 @@ export class CLI {
|
|
|
181
181
|
}
|
|
182
182
|
if (renders.length > maxItems) {
|
|
183
183
|
const skipped = renders.length - maxItems;
|
|
184
|
-
process.stdout.write(`\x1b[
|
|
184
|
+
process.stdout.write(`\x1b[90m… ${skipped} earlier message(s) omitted …\x1b[0m\n\n`);
|
|
185
185
|
renders.splice(0, renders.length - maxItems);
|
|
186
186
|
}
|
|
187
187
|
const last = renders.length - 1;
|
|
@@ -212,7 +212,7 @@ export class CLI {
|
|
|
212
212
|
let entries = story.filter((e) => String(e["text"] ?? "").trim());
|
|
213
213
|
if (entries.length > maxItems) {
|
|
214
214
|
const skipped = entries.length - maxItems;
|
|
215
|
-
process.stdout.write(`\x1b[
|
|
215
|
+
process.stdout.write(`\x1b[90m… ${skipped} earlier message(s) omitted …\x1b[0m\n\n`);
|
|
216
216
|
entries = entries.slice(-maxItems);
|
|
217
217
|
}
|
|
218
218
|
const last = entries.length - 1;
|
|
@@ -247,7 +247,7 @@ export class CLI {
|
|
|
247
247
|
const label = truncateLabel(rec["label"]);
|
|
248
248
|
const items = rec["input_items"] ?? [];
|
|
249
249
|
const toks = estimateTokens(items).toLocaleString();
|
|
250
|
-
return `${sid.padStart(4)}\x1b[
|
|
250
|
+
return `${sid.padStart(4)}\x1b[90m ${updated}\x1b[0m ${label}\x1b[90m ${items.length} items · ${toks} tok\x1b[0m`;
|
|
251
251
|
}
|
|
252
252
|
showConversationsPlain(recs) {
|
|
253
253
|
process.stdout.write("\n");
|
|
@@ -273,9 +273,9 @@ export class CLI {
|
|
|
273
273
|
const toks = estimateTokens(items).toLocaleString();
|
|
274
274
|
const selectedRow = idx === selected;
|
|
275
275
|
const content = `${selectedRow ? "\x1b[1;37m" : "\x1b[1m"}${sid.padStart(4)}\x1b[0m` +
|
|
276
|
-
`${selectedRow ? "\x1b[2;36m" : "\x1b[
|
|
276
|
+
`${selectedRow ? "\x1b[2;36m" : "\x1b[90m"} ${updated}\x1b[0m` +
|
|
277
277
|
`${selectedRow ? " \x1b[1;37m" : " "}${label}\x1b[0m` +
|
|
278
|
-
`${selectedRow ? "\x1b[2;36m" : "\x1b[
|
|
278
|
+
`${selectedRow ? "\x1b[2;36m" : "\x1b[90m"} ${items.length} items · ${toks} tok\x1b[0m`;
|
|
279
279
|
rows.push({
|
|
280
280
|
cells: [selectedRow ? "\x1b[1;36m❯\x1b[0m" : "", content],
|
|
281
281
|
});
|
|
@@ -283,11 +283,11 @@ export class CLI {
|
|
|
283
283
|
const below = total - hi;
|
|
284
284
|
let footer = "";
|
|
285
285
|
if (lo > 0 && below > 0)
|
|
286
|
-
footer = `\x1b[
|
|
286
|
+
footer = `\x1b[90m▲ ${lo} earlier · ▼ ${below} more\x1b[0m`;
|
|
287
287
|
else if (lo > 0)
|
|
288
|
-
footer = `\x1b[
|
|
288
|
+
footer = `\x1b[90m▲ ${lo} earlier\x1b[0m`;
|
|
289
289
|
else if (below > 0)
|
|
290
|
-
footer = `\x1b[
|
|
290
|
+
footer = `\x1b[90m▼ ${below} more\x1b[0m`;
|
|
291
291
|
if (footer)
|
|
292
292
|
rows.push({ cells: ["", footer] });
|
|
293
293
|
const title = `Conversations \x1b[1;36m${lo + 1}-${hi}/${total}\x1b[0m (↑/↓ · Quit: q · Enter to select)`;
|
|
@@ -372,8 +372,8 @@ export class CLI {
|
|
|
372
372
|
clearScreen();
|
|
373
373
|
this.renderHeader(engine.modelName);
|
|
374
374
|
process.stdout.write("\n");
|
|
375
|
-
process.stdout.write(`\x1b[
|
|
376
|
-
process.stdout.write(`\x1b[
|
|
375
|
+
process.stdout.write(`\x1b[90mResumed:\x1b[0m \x1b[1m${truncateLabel(rec["label"])}\x1b[0m\n`);
|
|
376
|
+
process.stdout.write(`\x1b[90m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n\n`);
|
|
377
377
|
renderPanel("Conversation history restored", "Restored");
|
|
378
378
|
process.stdout.write("\n");
|
|
379
379
|
if (this.story.length)
|
|
@@ -430,7 +430,7 @@ export class CLI {
|
|
|
430
430
|
const effort = parts[1]?.toLowerCase() ?? "";
|
|
431
431
|
if (["none", "low", "high"].includes(effort)) {
|
|
432
432
|
engine.reasoningEffort = effort;
|
|
433
|
-
process.stdout.write(`\n\x1b[32m✓\x1b[0m \x1b[
|
|
433
|
+
process.stdout.write(`\n\x1b[32m✓\x1b[0m \x1b[90mReasoning effort set to \x1b[1;36m${effort}\x1b[0m\x1b[90m.\x1b[0m\n`);
|
|
434
434
|
}
|
|
435
435
|
else {
|
|
436
436
|
process.stdout.write("\n");
|
|
@@ -477,7 +477,7 @@ export class CLI {
|
|
|
477
477
|
}
|
|
478
478
|
this.interruptPending = true;
|
|
479
479
|
process.stdout.write("\n");
|
|
480
|
-
process.stdout.write("\x1b[
|
|
480
|
+
process.stdout.write("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
|
|
481
481
|
continue;
|
|
482
482
|
}
|
|
483
483
|
throw err;
|
package/dist/engine.js
CHANGED
|
@@ -451,9 +451,9 @@ export class InferenceEngine {
|
|
|
451
451
|
}
|
|
452
452
|
parts.push(`Worked for ${tickDuration((Date.now() - queryStart) / 1000)}`);
|
|
453
453
|
if (stats["tokens"] > 0) {
|
|
454
|
-
parts.push(`Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[
|
|
454
|
+
parts.push(`Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[90m tokens`);
|
|
455
455
|
}
|
|
456
|
-
return `\x1b[
|
|
456
|
+
return `\x1b[90m(${parts.join(" · ")})\x1b[0m`;
|
|
457
457
|
};
|
|
458
458
|
const summary = () => mutedMarkdown(footerText());
|
|
459
459
|
const attachFooter = (items) => {
|
package/dist/tools.js
CHANGED
|
@@ -42,7 +42,7 @@ export function flushPendingDiffOutput() {
|
|
|
42
42
|
}
|
|
43
43
|
function displayDiff(pathName, oldContent, newContent) {
|
|
44
44
|
if (oldContent.length + newContent.length > max_diff_source_chars) {
|
|
45
|
-
pendingDiffOutput.push(`\x1b[
|
|
45
|
+
pendingDiffOutput.push(`\x1b[90m${pathName}: ${oldContent.length.toLocaleString()} chars -> ${newContent.length.toLocaleString()} chars ` +
|
|
46
46
|
`(diff hidden: exceeds ${max_diff_source_chars.toLocaleString()} char limit)\x1b[0m`);
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
@@ -118,7 +118,7 @@ function displayDiff(pathName, oldContent, newContent) {
|
|
|
118
118
|
let line;
|
|
119
119
|
if (v.kind === "~") {
|
|
120
120
|
const body = truncateStyled(v.body, innerW - 2);
|
|
121
|
-
line = `\x1b[
|
|
121
|
+
line = `\x1b[90m ${body}\x1b[0m`;
|
|
122
122
|
}
|
|
123
123
|
else if (v.kind === "+") {
|
|
124
124
|
const body = truncateStyled(v.body, innerW - 2);
|
|
@@ -130,7 +130,7 @@ function displayDiff(pathName, oldContent, newContent) {
|
|
|
130
130
|
}
|
|
131
131
|
else {
|
|
132
132
|
const body = truncateStyled(v.body, innerW - 2);
|
|
133
|
-
line = `\x1b[
|
|
133
|
+
line = `\x1b[90m ${body}\x1b[0m`;
|
|
134
134
|
}
|
|
135
135
|
const fill = Math.max(0, innerW - plainLen(line));
|
|
136
136
|
pendingDiffOutput.push(`\x1b[90m│\x1b[0m ${line}${" ".repeat(fill)} \x1b[90m│\x1b[0m`);
|
package/dist/ui.js
CHANGED
|
@@ -9,7 +9,6 @@ export function enableRawStdin() {
|
|
|
9
9
|
readline.emitKeypressEvents(process.stdin);
|
|
10
10
|
if (process.stdin.isTTY) {
|
|
11
11
|
process.stdin.setRawMode(true);
|
|
12
|
-
process.stdout.write("\x1b[>1u");
|
|
13
12
|
}
|
|
14
13
|
process.stdin.resume();
|
|
15
14
|
hideCursor();
|
|
@@ -101,24 +100,15 @@ export function truncateStyled(text, maxVisible) {
|
|
|
101
100
|
// ---------------------------------------------------------------------------
|
|
102
101
|
const TAG_MAP = {
|
|
103
102
|
bold: "\x1b[1m",
|
|
104
|
-
dim: "\x1b[
|
|
103
|
+
dim: "\x1b[90m",
|
|
105
104
|
italic: "\x1b[3m",
|
|
106
105
|
underline: "\x1b[4m",
|
|
107
106
|
red: "\x1b[31m",
|
|
108
107
|
green: "\x1b[32m",
|
|
109
108
|
yellow: "\x1b[33m",
|
|
110
|
-
blue: "\x1b[34m",
|
|
111
|
-
magenta: "\x1b[35m",
|
|
112
109
|
cyan: "\x1b[36m",
|
|
113
110
|
white: "\x1b[37m",
|
|
114
111
|
bright_black: "\x1b[90m",
|
|
115
|
-
bright_red: "\x1b[91m",
|
|
116
|
-
bright_green: "\x1b[92m",
|
|
117
|
-
bright_yellow: "\x1b[93m",
|
|
118
|
-
bright_blue: "\x1b[94m",
|
|
119
|
-
bright_magenta: "\x1b[95m",
|
|
120
|
-
bright_cyan: "\x1b[96m",
|
|
121
|
-
bright_white: "\x1b[97m",
|
|
122
112
|
};
|
|
123
113
|
const RESET = "\x1b[0m";
|
|
124
114
|
const MARKUP_RE = /\[(\/?)([a-z0-9_ ]+)\]/gi;
|
|
@@ -167,15 +157,13 @@ function highlightCodeLine(line, _lang = "") {
|
|
|
167
157
|
if (!line)
|
|
168
158
|
return "";
|
|
169
159
|
if (/^\s*(\/\/|#|\/\*)/.test(line)) {
|
|
170
|
-
return `\x1b[
|
|
160
|
+
return `\x1b[90m${line}\x1b[0m`;
|
|
171
161
|
}
|
|
172
|
-
return line.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b
|
|
162
|
+
return line.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b[a-zA-Z_$][\w$]*\b)|(\/\/[^\n]*|#[^\n]*)/g, (m, str, ident, comment) => {
|
|
173
163
|
if (str)
|
|
174
164
|
return `\x1b[32m${str}\x1b[0m`;
|
|
175
|
-
if (num)
|
|
176
|
-
return `\x1b[33m${num}\x1b[0m`;
|
|
177
165
|
if (comment)
|
|
178
|
-
return `\x1b[
|
|
166
|
+
return `\x1b[90m${comment}\x1b[0m`;
|
|
179
167
|
if (ident && KEYWORDS.has(ident))
|
|
180
168
|
return `\x1b[1;36m${ident}\x1b[0m`;
|
|
181
169
|
return m;
|
|
@@ -186,7 +174,7 @@ function formatInlineMarkdown(text) {
|
|
|
186
174
|
// Links MUST be converted first: the other rules inject ANSI escape codes,
|
|
187
175
|
// and the link pattern `\[[^\]]+\]\([^)]+\)` would otherwise match the `[`
|
|
188
176
|
// inside an already-inserted escape (e.g. `\x1b[1m`) and corrupt the output.
|
|
189
|
-
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[
|
|
177
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[90m($2)\x1b[0m");
|
|
190
178
|
line = line.replace(/\*\*\*([^*]+)\*\*\*/g, "\x1b[1;3m$1\x1b[0m");
|
|
191
179
|
line = line.replace(/\*\*([^*]+)\*\*/g, "\x1b[1m$1\x1b[0m");
|
|
192
180
|
line = line.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
|
|
@@ -313,7 +301,7 @@ export function markdownToAnsi(text) {
|
|
|
313
301
|
}
|
|
314
302
|
const h2 = line.match(/^##\s+(.+)$/);
|
|
315
303
|
if (h2) {
|
|
316
|
-
out.push(`\x1b[1;
|
|
304
|
+
out.push(`\x1b[1;36m${formatInlineMarkdown(h2[1])}\x1b[0m`);
|
|
317
305
|
continue;
|
|
318
306
|
}
|
|
319
307
|
const h3 = line.match(/^###\s+(.+)$/);
|
|
@@ -371,7 +359,7 @@ export function aiMarkdown(text) {
|
|
|
371
359
|
export function mutedMarkdown(text) {
|
|
372
360
|
return markdownToAnsi(text)
|
|
373
361
|
.split("\n")
|
|
374
|
-
.map((l) => `\x1b[
|
|
362
|
+
.map((l) => `\x1b[90m${l}\x1b[0m`)
|
|
375
363
|
.join("\n");
|
|
376
364
|
}
|
|
377
365
|
// ---------------------------------------------------------------------------
|
|
@@ -397,10 +385,12 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
397
385
|
if (!text)
|
|
398
386
|
return "─".repeat(Math.max(boxW - 2, 0));
|
|
399
387
|
const inner = ` ${text} `;
|
|
400
|
-
const fill = Math.max(boxW - 2 - plainLen(inner), 0);
|
|
401
388
|
if (align === "left") {
|
|
402
|
-
|
|
389
|
+
const prefix = "───";
|
|
390
|
+
const fill = Math.max(boxW - 2 - plainLen(prefix) - plainLen(inner), 0);
|
|
391
|
+
return `${prefix}${inner}${"─".repeat(fill)}`;
|
|
403
392
|
}
|
|
393
|
+
const fill = Math.max(boxW - 2 - plainLen(inner), 0);
|
|
404
394
|
const left = Math.floor(fill / 2);
|
|
405
395
|
const right = fill - left;
|
|
406
396
|
return `${"─".repeat(left)}${inner}${"─".repeat(right)}`;
|
|
@@ -466,7 +456,7 @@ export function formatDuration(seconds) {
|
|
|
466
456
|
return `${mins}m ${remSecs}s`;
|
|
467
457
|
}
|
|
468
458
|
export function tickDuration(seconds) {
|
|
469
|
-
return `\x1b[22;37m${formatDuration(seconds)}\x1b[
|
|
459
|
+
return `\x1b[22;37m${formatDuration(seconds)}\x1b[90m`;
|
|
470
460
|
}
|
|
471
461
|
export function printAiChunk(chunk) {
|
|
472
462
|
if (!chunk)
|
|
@@ -566,7 +556,7 @@ export class Spinner {
|
|
|
566
556
|
draw() {
|
|
567
557
|
const text = this.render ? this.render() : "";
|
|
568
558
|
const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
569
|
-
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[
|
|
559
|
+
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[90m${text}\x1b[0m`;
|
|
570
560
|
// Nothing changed on this tick -> do not rewrite the line (avoids flicker).
|
|
571
561
|
if (rendered === this.lastRendered)
|
|
572
562
|
return;
|
|
@@ -807,7 +797,7 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
807
797
|
runs.push({ text: " ", style: "" });
|
|
808
798
|
if (!buffer) {
|
|
809
799
|
runs.push({ text: PROMPT_CARET, style: "\x1b[1;36m" });
|
|
810
|
-
runs.push({ text: PROMPT_PLACEHOLDER, style: "\x1b[
|
|
800
|
+
runs.push({ text: PROMPT_PLACEHOLDER, style: "\x1b[90m" });
|
|
811
801
|
}
|
|
812
802
|
else {
|
|
813
803
|
const segs = splitBlocks(buffer, pasteSpans);
|
|
@@ -903,209 +893,23 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
903
893
|
return { frame, cursorRow, cursorCol, totalRows };
|
|
904
894
|
}
|
|
905
895
|
/**
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
* application: ESC O M
|
|
913
|
-
*/
|
|
914
|
-
/**
|
|
915
|
-
* Parses a complete (reassembled) escape-sequence encoding of Enter and
|
|
916
|
-
* classifies it as a newline (Shift+Enter) or a submit (plain Enter).
|
|
917
|
-
*
|
|
918
|
-
* Return values:
|
|
919
|
-
* "newline" — Shift+Enter (insert a new row)
|
|
920
|
-
* "submit" — plain Enter (submit the prompt)
|
|
921
|
-
* null — not an Enter sequence
|
|
922
|
-
*
|
|
923
|
-
* Handles every encoding a terminal can produce:
|
|
924
|
-
* CSI-u: ESC [ 13 ; M u (M = xterm bitmask + 1)
|
|
925
|
-
* legacy / kitty: ESC [ 13 ; M ~ / ESC [ 13 ; M _
|
|
926
|
-
* modifyOtherKeys: ESC [ 27 ; M ; 13 ~
|
|
927
|
-
* application (SS3): ESC O M
|
|
928
|
-
* win32-input-mode: ESC [ G ; R ; C ; VK ; MOD u
|
|
929
|
-
* (VK = 13 for Enter, MOD = raw bitmask where
|
|
930
|
-
* bit2 = Shift)
|
|
896
|
+
* Returns true when a keypress encodes Enter in any form: a plain CR
|
|
897
|
+
* (readline name "return"), the application-keypad form (ESC O M), or a
|
|
898
|
+
* modifier-prefixed CSI sequence (CSI-u ESC [ 13 ; N u, kitty ESC [ 27 ; N ;
|
|
899
|
+
* 13 ~). The prompt is single-line, so every Enter submits — there is no
|
|
900
|
+
* Shift+Enter newline mode, and a bare LF (Ctrl+J, name "enter"/"linefeed")
|
|
901
|
+
* is not a real Enter.
|
|
931
902
|
*/
|
|
932
|
-
function
|
|
933
|
-
if (!seq)
|
|
934
|
-
return null;
|
|
935
|
-
// win32-input-mode: ESC [ guard ; row ; col ; vk ; mod u
|
|
936
|
-
let m = seq.match(/^\x1b\[(\d+);(\d+);(\d+);(\d+);(\d+)u$/);
|
|
937
|
-
if (m) {
|
|
938
|
-
const vk = parseInt(m[4], 10);
|
|
939
|
-
if (vk !== 13)
|
|
940
|
-
return null;
|
|
941
|
-
const mod = parseInt(m[5], 10);
|
|
942
|
-
return mod & 4 ? "newline" : "submit";
|
|
943
|
-
}
|
|
944
|
-
// modifyOtherKeys / kitty: ESC [ 27 ; M ; 13 ~ (or _)
|
|
945
|
-
m = seq.match(/^\x1b\[27;(\d+);13[~_]$/);
|
|
946
|
-
if (m) {
|
|
947
|
-
const mod = parseInt(m[1], 10);
|
|
948
|
-
if (mod > 0)
|
|
949
|
-
return (mod - 1) & 1 ? "newline" : "submit";
|
|
950
|
-
return null;
|
|
951
|
-
}
|
|
952
|
-
// CSI-u / legacy: ESC [ 13 ; M u (or ~ or _)
|
|
953
|
-
m = seq.match(/^\x1b\[13;(\d+)[~u_]$/);
|
|
954
|
-
if (m) {
|
|
955
|
-
const mod = parseInt(m[1], 10);
|
|
956
|
-
if (mod > 0)
|
|
957
|
-
return (mod - 1) & 1 ? "newline" : "submit";
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
// Application keypad Enter (SS3)
|
|
961
|
-
if (seq === "\x1bOM")
|
|
962
|
-
return "newline";
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
965
|
-
export function isShiftEnterKey(str, key) {
|
|
903
|
+
function isEnterKey(str, key) {
|
|
966
904
|
if (!key)
|
|
967
905
|
return false;
|
|
968
906
|
const seq = key.sequence || str || "";
|
|
969
|
-
if (
|
|
970
|
-
return true;
|
|
971
|
-
// Shift modifier reported by readline on a return/enter/linefeed key.
|
|
972
|
-
if (key.shift &&
|
|
973
|
-
(key.name === "return" ||
|
|
974
|
-
key.name === "enter" ||
|
|
975
|
-
key.name === "linefeed" ||
|
|
976
|
-
seq === "\r" ||
|
|
977
|
-
seq === "\n")) {
|
|
907
|
+
if (key.name === "return" || seq === "\r")
|
|
978
908
|
return true;
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
export function isSubmitEnterKey(str, key) {
|
|
984
|
-
if (!key)
|
|
985
|
-
return false;
|
|
986
|
-
if (isShiftEnterKey(str, key))
|
|
987
|
-
return false;
|
|
988
|
-
const seq = key.sequence || str || "";
|
|
989
|
-
if (parseEnterSequence(seq) === "submit")
|
|
990
|
-
return true;
|
|
991
|
-
if ((key.name === "return" ||
|
|
992
|
-
seq === "\r" ||
|
|
993
|
-
seq === "\x1b[13u" ||
|
|
994
|
-
seq === "\x1b[13;1u") &&
|
|
995
|
-
!key.shift) {
|
|
996
|
-
return true;
|
|
997
|
-
}
|
|
998
|
-
return false;
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Maps a Windows virtual-key code + shift state to a printable character, or
|
|
1002
|
-
* null if the code isn't a printable character. US-layout approximation (the
|
|
1003
|
-
* common case); enough for a text prompt.
|
|
1004
|
-
*/
|
|
1005
|
-
function vkToChar(vk, shift) {
|
|
1006
|
-
if (vk >= 48 && vk <= 57) {
|
|
1007
|
-
const symbols = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
|
|
1008
|
-
return shift ? symbols[vk - 48] : String(vk - 48);
|
|
1009
|
-
}
|
|
1010
|
-
if (vk >= 65 && vk <= 90) {
|
|
1011
|
-
return shift ? String.fromCharCode(vk) : String.fromCharCode(vk).toLowerCase();
|
|
1012
|
-
}
|
|
1013
|
-
if (vk === 186)
|
|
1014
|
-
return shift ? ":" : ";";
|
|
1015
|
-
if (vk === 187)
|
|
1016
|
-
return shift ? "+" : "=";
|
|
1017
|
-
if (vk === 188)
|
|
1018
|
-
return shift ? "<" : ",";
|
|
1019
|
-
if (vk === 189)
|
|
1020
|
-
return shift ? "_" : "-";
|
|
1021
|
-
if (vk === 190)
|
|
1022
|
-
return shift ? ">" : ".";
|
|
1023
|
-
if (vk === 191)
|
|
1024
|
-
return shift ? "?" : "/";
|
|
1025
|
-
if (vk === 192)
|
|
1026
|
-
return shift ? "~" : "`";
|
|
1027
|
-
if (vk === 219)
|
|
1028
|
-
return shift ? "{" : "[";
|
|
1029
|
-
if (vk === 220)
|
|
1030
|
-
return shift ? "|" : "\\";
|
|
1031
|
-
if (vk === 221)
|
|
1032
|
-
return shift ? "}" : "]";
|
|
1033
|
-
if (vk === 222)
|
|
1034
|
-
return shift ? '"' : "'";
|
|
1035
|
-
return null;
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Decodes a complete win32-input-mode sequence (ESC [ G ; R ; C ; VK ; MOD u)
|
|
1039
|
-
* into a key action. Returns a descriptor for the prompt to execute, or null
|
|
1040
|
-
* if the sequence isn't a win32-input-mode sequence.
|
|
1041
|
-
*/
|
|
1042
|
-
function decodeWin32Key(seq) {
|
|
1043
|
-
const m = seq.match(/^\x1b\[(\d+);(\d+);(\d+);(\d+);(\d+)u$/);
|
|
1044
|
-
if (!m)
|
|
1045
|
-
return null;
|
|
1046
|
-
const vk = parseInt(m[4], 10);
|
|
1047
|
-
const mod = parseInt(m[5], 10);
|
|
1048
|
-
// win32-input-mode MOD bitmask: bit2 = Shift, bit1 = Ctrl, bit0 = Alt
|
|
1049
|
-
const shift = (mod & 4) === 4;
|
|
1050
|
-
const ctrl = (mod & 2) === 2;
|
|
1051
|
-
const alt = (mod & 1) === 1;
|
|
1052
|
-
const cmd = (c) => ({ type: "cmd", cmd: c });
|
|
1053
|
-
const text = (t) => ({ type: "text", text: t });
|
|
1054
|
-
// Control combos
|
|
1055
|
-
if (ctrl && vk === 67)
|
|
1056
|
-
return cmd("interrupt"); // Ctrl+C
|
|
1057
|
-
if (ctrl && vk === 68)
|
|
1058
|
-
return cmd("eof"); // Ctrl+D
|
|
1059
|
-
if (ctrl && vk === 90)
|
|
1060
|
-
return cmd("eof"); // Ctrl+Z
|
|
1061
|
-
if (ctrl && vk === 65)
|
|
1062
|
-
return cmd("home"); // Ctrl+A
|
|
1063
|
-
if (ctrl && vk === 69)
|
|
1064
|
-
return cmd("end"); // Ctrl+E
|
|
1065
|
-
if (ctrl && vk === 85)
|
|
1066
|
-
return cmd("clear-line"); // Ctrl+U
|
|
1067
|
-
if (ctrl && vk === 75)
|
|
1068
|
-
return cmd("kill-line"); // Ctrl+K
|
|
1069
|
-
if (ctrl && vk === 87)
|
|
1070
|
-
return cmd("delete-word"); // Ctrl+W
|
|
1071
|
-
switch (vk) {
|
|
1072
|
-
case 13: // Enter
|
|
1073
|
-
return shift ? cmd("newline") : cmd("submit");
|
|
1074
|
-
case 8: // Backspace
|
|
1075
|
-
return cmd(ctrl ? "delete-word" : "backspace");
|
|
1076
|
-
case 46: // Delete
|
|
1077
|
-
return cmd("delete");
|
|
1078
|
-
case 37: // Left
|
|
1079
|
-
return ctrl ? cmd("back-word") : cmd("left");
|
|
1080
|
-
case 39: // Right
|
|
1081
|
-
return ctrl ? cmd("forward-word") : cmd("right");
|
|
1082
|
-
case 38: // Up
|
|
1083
|
-
return cmd("up");
|
|
1084
|
-
case 40: // Down
|
|
1085
|
-
return cmd("down");
|
|
1086
|
-
case 36: // Home
|
|
1087
|
-
return cmd("home");
|
|
1088
|
-
case 35: // End
|
|
1089
|
-
return cmd("end");
|
|
1090
|
-
case 33: // PageUp
|
|
1091
|
-
return cmd("up");
|
|
1092
|
-
case 34: // PageDown
|
|
1093
|
-
return cmd("down");
|
|
1094
|
-
case 9: // Tab
|
|
1095
|
-
return cmd("tab");
|
|
1096
|
-
case 32: // Space
|
|
1097
|
-
return text(" ");
|
|
1098
|
-
case 27: // Escape
|
|
1099
|
-
return cmd("escape");
|
|
1100
|
-
default: {
|
|
1101
|
-
if (alt)
|
|
1102
|
-
return null; // Alt combos unsupported -> ignore
|
|
1103
|
-
const ch = vkToChar(vk, shift);
|
|
1104
|
-
if (ch)
|
|
1105
|
-
return text(ch);
|
|
1106
|
-
return cmd("ignore");
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
909
|
+
return (/^\x1b\[13(;[\d;]*)?[u~_]$/.test(seq) || // CSI-u / legacy, any modifier
|
|
910
|
+
/^\x1b\[27;\d+;13[~_]$/.test(seq) || // modifyOtherKeys / kitty
|
|
911
|
+
seq === "\x1bOM" // application keypad
|
|
912
|
+
);
|
|
1109
913
|
}
|
|
1110
914
|
export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
1111
915
|
return new Promise((resolve, reject) => {
|
|
@@ -1133,23 +937,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1133
937
|
let bracketedPasteBuffer = "";
|
|
1134
938
|
let pasteBurst = false;
|
|
1135
939
|
let pasteBurstTimer = null;
|
|
1136
|
-
// Accumulator for modifier-prefixed CSI sequences (e.g. Shift+Enter sent as
|
|
1137
|
-
// `\x1b[27;2;13~`), which Node's readline splits into several keypress
|
|
1138
|
-
// events instead of one, so they must be reassembled here.
|
|
1139
|
-
let modSeq = "";
|
|
1140
|
-
let modSeqTimeout = null;
|
|
1141
940
|
readline.emitKeypressEvents(process.stdin);
|
|
1142
941
|
if (process.stdin.isTTY) {
|
|
1143
942
|
process.stdin.setRawMode(true);
|
|
943
|
+
// Bracketed paste so multi-line pastes arrive inside \x1b[200~…\x1b[201~.
|
|
1144
944
|
process.stdout.write("\x1b[?2004h");
|
|
1145
|
-
// Enable CSI-u disambiguation so Shift+Enter arrives as a distinct code
|
|
1146
|
-
// (`\x1b[13;2u`) instead of a plain Enter. Plain keys still arrive as
|
|
1147
|
-
// single keypress events. Deliberately NOT enabling win32-input-mode
|
|
1148
|
-
// (`\x1b[?9001h`): it rewrites EVERY keystroke into a long CSI sequence
|
|
1149
|
-
// that readline splits across many events, and an interrupted split
|
|
1150
|
-
// leaves modSeq stuck so the next keystroke is silently dropped —
|
|
1151
|
-
// that regression made typing in the prompt impossible.
|
|
1152
|
-
process.stdout.write("\x1b[>1u");
|
|
1153
945
|
}
|
|
1154
946
|
process.stdin.resume();
|
|
1155
947
|
hideCursor();
|
|
@@ -1176,8 +968,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1176
968
|
process.stdin.setRawMode(false);
|
|
1177
969
|
if (pasteBurstTimer)
|
|
1178
970
|
clearTimeout(pasteBurstTimer);
|
|
1179
|
-
if (modSeqTimeout)
|
|
1180
|
-
clearTimeout(modSeqTimeout);
|
|
1181
971
|
clearBox();
|
|
1182
972
|
process.stdin.removeListener("keypress", onKeypress);
|
|
1183
973
|
process.stdin.pause();
|
|
@@ -1495,140 +1285,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1495
1285
|
pasteBurstTimer = null;
|
|
1496
1286
|
}, 500);
|
|
1497
1287
|
};
|
|
1498
|
-
const handleRawSequence = (full) => {
|
|
1499
|
-
// Enter sequences (CSI-u, kitty, legacy, SS3, win32).
|
|
1500
|
-
const enter = parseEnterSequence(full);
|
|
1501
|
-
if (enter === "newline") {
|
|
1502
|
-
insert("\n");
|
|
1503
|
-
repaint();
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
if (enter === "submit") {
|
|
1507
|
-
if (buffer.trim())
|
|
1508
|
-
settle([buffer, pasteSpans], false);
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
// Other win32-input-mode keys (arrows, backspace, etc.).
|
|
1512
|
-
const dec = decodeWin32Key(full);
|
|
1513
|
-
if (!dec)
|
|
1514
|
-
return;
|
|
1515
|
-
if (dec.type === "text") {
|
|
1516
|
-
insert(dec.text);
|
|
1517
|
-
repaint();
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
switch (dec.cmd) {
|
|
1521
|
-
case "interrupt":
|
|
1522
|
-
settle(new Error("interrupt"), true);
|
|
1523
|
-
return;
|
|
1524
|
-
case "eof":
|
|
1525
|
-
settle(new Error("eof"), true);
|
|
1526
|
-
return;
|
|
1527
|
-
case "backspace":
|
|
1528
|
-
backspace();
|
|
1529
|
-
repaint();
|
|
1530
|
-
return;
|
|
1531
|
-
case "delete-word":
|
|
1532
|
-
deleteWordBefore();
|
|
1533
|
-
repaint();
|
|
1534
|
-
return;
|
|
1535
|
-
case "delete":
|
|
1536
|
-
deleteChar();
|
|
1537
|
-
repaint();
|
|
1538
|
-
return;
|
|
1539
|
-
case "left":
|
|
1540
|
-
moveLeft();
|
|
1541
|
-
repaint();
|
|
1542
|
-
return;
|
|
1543
|
-
case "right":
|
|
1544
|
-
moveRight();
|
|
1545
|
-
repaint();
|
|
1546
|
-
return;
|
|
1547
|
-
case "back-word": {
|
|
1548
|
-
const sp = buffer.slice(0, cursor).lastIndexOf(" ");
|
|
1549
|
-
cursor = sp === -1 ? 0 : sp + 1;
|
|
1550
|
-
repaint();
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
case "forward-word": {
|
|
1554
|
-
const sp = buffer.indexOf(" ", cursor);
|
|
1555
|
-
cursor = sp === -1 ? buffer.length : sp + 1;
|
|
1556
|
-
repaint();
|
|
1557
|
-
return;
|
|
1558
|
-
}
|
|
1559
|
-
case "up":
|
|
1560
|
-
moveUp();
|
|
1561
|
-
repaint();
|
|
1562
|
-
return;
|
|
1563
|
-
case "down":
|
|
1564
|
-
moveDown();
|
|
1565
|
-
repaint();
|
|
1566
|
-
return;
|
|
1567
|
-
case "home":
|
|
1568
|
-
cursor = 0;
|
|
1569
|
-
repaint();
|
|
1570
|
-
return;
|
|
1571
|
-
case "end":
|
|
1572
|
-
cursor = buffer.length;
|
|
1573
|
-
repaint();
|
|
1574
|
-
return;
|
|
1575
|
-
case "tab":
|
|
1576
|
-
insert(" ");
|
|
1577
|
-
repaint();
|
|
1578
|
-
return;
|
|
1579
|
-
case "clear-line":
|
|
1580
|
-
buffer = "";
|
|
1581
|
-
cursor = 0;
|
|
1582
|
-
pasteSpans = [];
|
|
1583
|
-
repaint();
|
|
1584
|
-
return;
|
|
1585
|
-
case "kill-line":
|
|
1586
|
-
buffer = buffer.slice(0, cursor);
|
|
1587
|
-
repaint();
|
|
1588
|
-
return;
|
|
1589
|
-
default:
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1592
|
-
};
|
|
1593
1288
|
const onKeypress = (str, key) => {
|
|
1594
|
-
// Reassemble modifier-prefixed CSI sequences that Node's readline may
|
|
1595
|
-
// split across keypress events (e.g. `\x1b[13;2u` arriving as `\x1b[13;2`
|
|
1596
|
-
// + "u"). Only start/continue accumulation on bytes that can belong to a
|
|
1597
|
-
// CSI sequence: digits / `;` while incomplete, or a terminator byte
|
|
1598
|
-
// [A-Z~_u] that completes it. Any other keystroke (a typed letter, for
|
|
1599
|
-
// instance) ABANDONS the partial instead of being swallowed, so a stuck
|
|
1600
|
-
// partial can never eat input.
|
|
1601
|
-
const seq = key?.sequence || str || "";
|
|
1602
|
-
if (seq.startsWith("\x1b[") && /^\x1b\[[\d;]*$/.test(seq)) {
|
|
1603
|
-
modSeq = seq;
|
|
1604
|
-
if (modSeqTimeout)
|
|
1605
|
-
clearTimeout(modSeqTimeout);
|
|
1606
|
-
modSeqTimeout = setTimeout(() => {
|
|
1607
|
-
modSeq = "";
|
|
1608
|
-
modSeqTimeout = null;
|
|
1609
|
-
}, 200);
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
if (modSeq) {
|
|
1613
|
-
const s = str ?? seq ?? "";
|
|
1614
|
-
const completes = /^\x1b\[[\d;]*[A-Z~_u]$/.test(modSeq + s);
|
|
1615
|
-
if (/^[0-9;]$/.test(s) || completes) {
|
|
1616
|
-
modSeq += s;
|
|
1617
|
-
if (completes) {
|
|
1618
|
-
const full = modSeq;
|
|
1619
|
-
modSeq = "";
|
|
1620
|
-
if (modSeqTimeout)
|
|
1621
|
-
clearTimeout(modSeqTimeout);
|
|
1622
|
-
modSeqTimeout = null;
|
|
1623
|
-
handleRawSequence(full);
|
|
1624
|
-
}
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
modSeq = "";
|
|
1628
|
-
if (modSeqTimeout)
|
|
1629
|
-
clearTimeout(modSeqTimeout);
|
|
1630
|
-
modSeqTimeout = null;
|
|
1631
|
-
}
|
|
1632
1289
|
if (consumeBracketedPaste(str, key))
|
|
1633
1290
|
return;
|
|
1634
1291
|
if (pasteBurst && str && str.length === 1 && !key?.ctrl && !key?.meta) {
|
|
@@ -1672,12 +1329,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1672
1329
|
repaint();
|
|
1673
1330
|
return;
|
|
1674
1331
|
}
|
|
1675
|
-
if (
|
|
1332
|
+
if (pasteBurst && isEnterKey(str, key)) {
|
|
1676
1333
|
insert("\n");
|
|
1677
1334
|
repaint();
|
|
1678
1335
|
return;
|
|
1679
1336
|
}
|
|
1680
|
-
if (
|
|
1337
|
+
if (isEnterKey(str, key)) {
|
|
1681
1338
|
if (buffer.trim()) {
|
|
1682
1339
|
settle([buffer, pasteSpans], false);
|
|
1683
1340
|
}
|
|
@@ -1734,10 +1391,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1734
1391
|
return;
|
|
1735
1392
|
}
|
|
1736
1393
|
// Ctrl+J sends a bare LF (0x0a). Node reports it as name "enter" or
|
|
1737
|
-
// "linefeed"
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
if (key && !key.shift && (key.name === "enter" || key.name === "linefeed")) {
|
|
1394
|
+
// "linefeed". It is not a real Enter (a real Enter arrives as CR / name
|
|
1395
|
+
// "return"), so ignore it rather than submitting or inserting a newline.
|
|
1396
|
+
if (key && (key.name === "enter" || key.name === "linefeed")) {
|
|
1741
1397
|
return;
|
|
1742
1398
|
}
|
|
1743
1399
|
if (str) {
|