@oxecli/oxe 1.0.69 → 1.0.70
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 +13 -13
- package/dist/engine.js +2 -2
- package/dist/tools.js +3 -3
- package/dist/ui.js +28 -374
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -466,7 +454,7 @@ export function formatDuration(seconds) {
|
|
|
466
454
|
return `${mins}m ${remSecs}s`;
|
|
467
455
|
}
|
|
468
456
|
export function tickDuration(seconds) {
|
|
469
|
-
return `\x1b[22;37m${formatDuration(seconds)}\x1b[
|
|
457
|
+
return `\x1b[22;37m${formatDuration(seconds)}\x1b[90m`;
|
|
470
458
|
}
|
|
471
459
|
export function printAiChunk(chunk) {
|
|
472
460
|
if (!chunk)
|
|
@@ -566,7 +554,7 @@ export class Spinner {
|
|
|
566
554
|
draw() {
|
|
567
555
|
const text = this.render ? this.render() : "";
|
|
568
556
|
const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
569
|
-
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[
|
|
557
|
+
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[90m${text}\x1b[0m`;
|
|
570
558
|
// Nothing changed on this tick -> do not rewrite the line (avoids flicker).
|
|
571
559
|
if (rendered === this.lastRendered)
|
|
572
560
|
return;
|
|
@@ -807,7 +795,7 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
807
795
|
runs.push({ text: " ", style: "" });
|
|
808
796
|
if (!buffer) {
|
|
809
797
|
runs.push({ text: PROMPT_CARET, style: "\x1b[1;36m" });
|
|
810
|
-
runs.push({ text: PROMPT_PLACEHOLDER, style: "\x1b[
|
|
798
|
+
runs.push({ text: PROMPT_PLACEHOLDER, style: "\x1b[90m" });
|
|
811
799
|
}
|
|
812
800
|
else {
|
|
813
801
|
const segs = splitBlocks(buffer, pasteSpans);
|
|
@@ -903,209 +891,23 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
903
891
|
return { frame, cursorRow, cursorCol, totalRows };
|
|
904
892
|
}
|
|
905
893
|
/**
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
* application: ESC O M
|
|
894
|
+
* Returns true when a keypress encodes Enter in any form: a plain CR
|
|
895
|
+
* (readline name "return"), the application-keypad form (ESC O M), or a
|
|
896
|
+
* modifier-prefixed CSI sequence (CSI-u ESC [ 13 ; N u, kitty ESC [ 27 ; N ;
|
|
897
|
+
* 13 ~). The prompt is single-line, so every Enter submits — there is no
|
|
898
|
+
* Shift+Enter newline mode, and a bare LF (Ctrl+J, name "enter"/"linefeed")
|
|
899
|
+
* is not a real Enter.
|
|
913
900
|
*/
|
|
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)
|
|
931
|
-
*/
|
|
932
|
-
function parseEnterSequence(seq) {
|
|
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) {
|
|
966
|
-
if (!key)
|
|
967
|
-
return false;
|
|
968
|
-
const seq = key.sequence || str || "";
|
|
969
|
-
if (parseEnterSequence(seq) === "newline")
|
|
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")) {
|
|
978
|
-
return true;
|
|
979
|
-
}
|
|
980
|
-
return false;
|
|
981
|
-
}
|
|
982
|
-
export const isNewlineKey = isShiftEnterKey;
|
|
983
|
-
export function isSubmitEnterKey(str, key) {
|
|
901
|
+
function isEnterKey(str, key) {
|
|
984
902
|
if (!key)
|
|
985
903
|
return false;
|
|
986
|
-
if (isShiftEnterKey(str, key))
|
|
987
|
-
return false;
|
|
988
904
|
const seq = key.sequence || str || "";
|
|
989
|
-
if (
|
|
990
|
-
return true;
|
|
991
|
-
if ((key.name === "return" ||
|
|
992
|
-
seq === "\r" ||
|
|
993
|
-
seq === "\x1b[13u" ||
|
|
994
|
-
seq === "\x1b[13;1u") &&
|
|
995
|
-
!key.shift) {
|
|
905
|
+
if (key.name === "return" || seq === "\r")
|
|
996
906
|
return true;
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
}
|
|
907
|
+
return (/^\x1b\[13(;[\d;]*)?[u~_]$/.test(seq) || // CSI-u / legacy, any modifier
|
|
908
|
+
/^\x1b\[27;\d+;13[~_]$/.test(seq) || // modifyOtherKeys / kitty
|
|
909
|
+
seq === "\x1bOM" // application keypad
|
|
910
|
+
);
|
|
1109
911
|
}
|
|
1110
912
|
export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
1111
913
|
return new Promise((resolve, reject) => {
|
|
@@ -1133,23 +935,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1133
935
|
let bracketedPasteBuffer = "";
|
|
1134
936
|
let pasteBurst = false;
|
|
1135
937
|
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
938
|
readline.emitKeypressEvents(process.stdin);
|
|
1142
939
|
if (process.stdin.isTTY) {
|
|
1143
940
|
process.stdin.setRawMode(true);
|
|
941
|
+
// Bracketed paste so multi-line pastes arrive inside \x1b[200~…\x1b[201~.
|
|
1144
942
|
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
943
|
}
|
|
1154
944
|
process.stdin.resume();
|
|
1155
945
|
hideCursor();
|
|
@@ -1176,8 +966,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1176
966
|
process.stdin.setRawMode(false);
|
|
1177
967
|
if (pasteBurstTimer)
|
|
1178
968
|
clearTimeout(pasteBurstTimer);
|
|
1179
|
-
if (modSeqTimeout)
|
|
1180
|
-
clearTimeout(modSeqTimeout);
|
|
1181
969
|
clearBox();
|
|
1182
970
|
process.stdin.removeListener("keypress", onKeypress);
|
|
1183
971
|
process.stdin.pause();
|
|
@@ -1495,140 +1283,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1495
1283
|
pasteBurstTimer = null;
|
|
1496
1284
|
}, 500);
|
|
1497
1285
|
};
|
|
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
1286
|
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
1287
|
if (consumeBracketedPaste(str, key))
|
|
1633
1288
|
return;
|
|
1634
1289
|
if (pasteBurst && str && str.length === 1 && !key?.ctrl && !key?.meta) {
|
|
@@ -1672,12 +1327,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1672
1327
|
repaint();
|
|
1673
1328
|
return;
|
|
1674
1329
|
}
|
|
1675
|
-
if (
|
|
1330
|
+
if (pasteBurst && isEnterKey(str, key)) {
|
|
1676
1331
|
insert("\n");
|
|
1677
1332
|
repaint();
|
|
1678
1333
|
return;
|
|
1679
1334
|
}
|
|
1680
|
-
if (
|
|
1335
|
+
if (isEnterKey(str, key)) {
|
|
1681
1336
|
if (buffer.trim()) {
|
|
1682
1337
|
settle([buffer, pasteSpans], false);
|
|
1683
1338
|
}
|
|
@@ -1734,10 +1389,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1734
1389
|
return;
|
|
1735
1390
|
}
|
|
1736
1391
|
// 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")) {
|
|
1392
|
+
// "linefeed". It is not a real Enter (a real Enter arrives as CR / name
|
|
1393
|
+
// "return"), so ignore it rather than submitting or inserting a newline.
|
|
1394
|
+
if (key && (key.name === "enter" || key.name === "linefeed")) {
|
|
1741
1395
|
return;
|
|
1742
1396
|
}
|
|
1743
1397
|
if (str) {
|