@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 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("Version") +
44
+ labelCell("") +
45
45
  labelCell("Model") +
46
46
  labelCell("Status") +
47
47
  "\n" +
48
- valueCell(version, "cyan") +
49
- valueCell(model, "blue") +
48
+ valueCell("", "cyan") +
49
+ valueCell(model, "cyan") +
50
50
  valueCell("Ready", "green") +
51
51
  "\n\n" +
52
- `[dim]Environment: ${runtimeOsSummary()}[/dim]`, "Oxe", "Type /help for commands · Ctrl+C to interrupt", false, "90", "left");
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[2m${desc}\x1b[0m`],
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[2m… ${skipped} earlier message(s) omitted …\x1b[0m\n\n`);
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[2m… ${skipped} earlier message(s) omitted …\x1b[0m\n\n`);
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[2m ${updated}\x1b[0m ${label}\x1b[2m ${items.length} items · ${toks} tok\x1b[0m`;
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[2m"} ${updated}\x1b[0m` +
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[2m"} ${items.length} items · ${toks} tok\x1b[0m`;
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[2m▲ ${lo} earlier · ▼ ${below} more\x1b[0m`;
286
+ footer = `\x1b[90m▲ ${lo} earlier · ▼ ${below} more\x1b[0m`;
287
287
  else if (lo > 0)
288
- footer = `\x1b[2m▲ ${lo} earlier\x1b[0m`;
288
+ footer = `\x1b[90m▲ ${lo} earlier\x1b[0m`;
289
289
  else if (below > 0)
290
- footer = `\x1b[2m▼ ${below} more\x1b[0m`;
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[2mResumed:\x1b[0m \x1b[1m${truncateLabel(rec["label"])}\x1b[0m\n`);
376
- process.stdout.write(`\x1b[2m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n\n`);
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[2mReasoning effort set to \x1b[1;36m${effort}\x1b[0m\x1b[2m.\x1b[0m\n`);
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[2mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
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[2m tokens`);
454
+ parts.push(`Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[90m tokens`);
455
455
  }
456
- return `\x1b[2m(${parts.join(" · ")})\x1b[0m`;
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[2m${pathName}: ${oldContent.length.toLocaleString()} chars -> ${newContent.length.toLocaleString()} chars ` +
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[2m ${body}\x1b[0m`;
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[2m ${body}\x1b[0m`;
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[2m",
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[2;37m${line}\x1b[0m`;
160
+ return `\x1b[90m${line}\x1b[0m`;
171
161
  }
172
- return line.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+\.?\d*\b)|(\b[a-zA-Z_$][\w$]*\b)|(\/\/[^\n]*|#[^\n]*)/g, (m, str, num, ident, comment) => {
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[2;37m${comment}\x1b[0m`;
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[2m($2)\x1b[0m");
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;34m${formatInlineMarkdown(h2[1])}\x1b[0m`);
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[2m${l}\x1b[0m`)
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
- return `${inner}${"".repeat(fill)}`;
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[2m`;
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[2m${text}\x1b[0m`;
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[2;37m" });
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
- * Generic CSI Shift+Enter detection. The modifier encoding used by CSI-u,
907
- * legacy and kitty/modifyOtherKeys is a bitmask + 1, where Shift is bit0
908
- * (so modifier 2 = shift, 4 = shift+alt, 6 = shift+ctrl, ...). Enter has
909
- * key code 13. Handles:
910
- * CSI-u / legacy: ESC [ 13 ; M u and ESC [ 13 ; M ~
911
- * modifyOtherKeys: ESC [ 27 ; M ; 13 ~
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 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) {
903
+ function isEnterKey(str, key) {
966
904
  if (!key)
967
905
  return false;
968
906
  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")) {
907
+ if (key.name === "return" || seq === "\r")
978
908
  return true;
979
- }
980
- return false;
981
- }
982
- export const isNewlineKey = isShiftEnterKey;
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 (isNewlineKey(str, key) || (pasteBurst && isSubmitEnterKey(str, key))) {
1332
+ if (pasteBurst && isEnterKey(str, key)) {
1676
1333
  insert("\n");
1677
1334
  repaint();
1678
1335
  return;
1679
1336
  }
1680
- if (isSubmitEnterKey(str, key)) {
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" with no shift modifier. Only Shift+Enter should create a
1738
- // new row, so ignore a lone LF (it is not a real Enter a real Enter
1739
- // arrives as CR / name "return").
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.69",
3
+ "version": "1.0.71",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },