@oxecli/oxe 1.0.65 → 1.0.67

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
@@ -466,7 +466,10 @@ export class CLI {
466
466
  this.story[this.story.length - 1]["type"] === "user") {
467
467
  this.story.pop();
468
468
  }
469
- if (this.interruptPending) {
469
+ // Exit cleanly on: a second Ctrl+C, or a first Ctrl+C while idle
470
+ // (no active query). Only an in-flight query's first Ctrl+C keeps
471
+ // running so the agent can be interrupted without a stray key exiting.
472
+ if (this.interruptPending || !wasActive) {
470
473
  this.saveSession(engine);
471
474
  await engine.cleanupStoredResponses();
472
475
  process.stdout.write("\nClosing terminal session. Goodbye!\n\n");
@@ -474,9 +477,7 @@ export class CLI {
474
477
  }
475
478
  this.interruptPending = true;
476
479
  process.stdout.write("\n");
477
- process.stdout.write(wasActive
478
- ? "\x1b[2mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n"
479
- : "\x1b[2mNo active query. Press Ctrl+C again to exit.\x1b[0m\n");
480
+ process.stdout.write("\x1b[2mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
480
481
  continue;
481
482
  }
482
483
  throw err;
package/dist/tools.js CHANGED
@@ -72,9 +72,12 @@ function displayDiff(pathName, oldContent, newContent) {
72
72
  const limit = kind === " " ? max_diff_context_lines : max_diff_lines;
73
73
  const hidden = run.length - limit;
74
74
  for (const r of run.slice(0, limit))
75
- shown.push(truncateDiffLine(r.line));
75
+ shown.push({ kind, line: truncateDiffLine(r.line) });
76
76
  if (hidden > 0) {
77
- shown.push(`… (${hidden} ${hidden === 1 ? "line" : "lines"} of diff hidden) …`);
77
+ shown.push({
78
+ kind: "~",
79
+ line: `… (${hidden} ${hidden === 1 ? "line" : "lines"} of diff hidden) …`,
80
+ });
78
81
  }
79
82
  run = [];
80
83
  };
@@ -87,9 +90,12 @@ function displayDiff(pathName, oldContent, newContent) {
87
90
  }
88
91
  flush();
89
92
  // Build the lines, stripping the leading + / - / space marker for display.
90
- const visible = shown.map((l) => {
91
- const kind = l[0] === "+" ? "+" : l[0] === "-" ? "-" : " ";
92
- return { kind, body: l.slice(1) };
93
+ // Summary lines ("~") are already plain text and are kept whole.
94
+ const visible = shown.map(({ kind, line }) => {
95
+ if (kind === "~")
96
+ return { kind, body: line };
97
+ const k = line[0] === "+" ? "+" : line[0] === "-" ? "-" : " ";
98
+ return { kind: k, body: line.slice(1) };
93
99
  });
94
100
  // A "… (N lines hidden) …" summary line is neutral context.
95
101
  const termW = terminalWidth();
@@ -110,7 +116,11 @@ function displayDiff(pathName, oldContent, newContent) {
110
116
  pendingDiffOutput.push(`\x1b[90m${top}\x1b[0m`);
111
117
  for (const v of visible) {
112
118
  let line;
113
- if (v.kind === "+") {
119
+ if (v.kind === "~") {
120
+ const body = truncateStyled(v.body, innerW - 2);
121
+ line = `\x1b[2m ${body}\x1b[0m`;
122
+ }
123
+ else if (v.kind === "+") {
114
124
  const body = truncateStyled(v.body, innerW - 2);
115
125
  line = `\x1b[32m+ ${body}\x1b[0m`;
116
126
  }
package/dist/ui.js CHANGED
@@ -9,7 +9,7 @@ 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[<u\x1b[>1u");
12
+ process.stdout.write("\x1b[?9001h\x1b[>1u");
13
13
  }
14
14
  process.stdin.resume();
15
15
  hideCursor();
@@ -259,7 +259,7 @@ export function markdownToAnsi(text) {
259
259
  const highlighted = highlightCodeLine(cline, codeLang);
260
260
  out.push(`\x1b[90m│\x1b[0m ${highlighted}`);
261
261
  }
262
- out.push(`\x1b[90m╰${"─".repeat(barLen + plainLen(badge) + 2)}╯\x1b[0m`);
262
+ out.push(`\x1b[90m╰${"─".repeat(barLen + plainLen(badge) + 1)}╯\x1b[0m`);
263
263
  codeBuffer = [];
264
264
  codeLang = "";
265
265
  };
@@ -911,27 +911,63 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
911
911
  * modifyOtherKeys: ESC [ 27 ; M ; 13 ~
912
912
  * application: ESC O M
913
913
  */
914
- function csiShiftEnter(seq) {
915
- if (seq === "\x1bOM")
916
- return true;
917
- // modifyOtherKeys: ESC [ 27 ; M ; 13 ~
918
- const mod = seq.match(/^\x1b\[27;(\d+);13[~_]$/);
919
- if (mod) {
920
- const m = parseInt(mod[1], 10);
921
- return Number.isInteger(m) && ((m - 1) & 1) === 1;
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
+ * bit0 = 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 & 1 ? "newline" : "submit";
922
943
  }
923
- // CSI-u / legacy: ESC [ 13 ; M u (or ~ or _)
924
- const csi = seq.match(/^\x1b\[13;(\d+)[~u_]$/);
925
- if (csi) {
926
- const m = parseInt(csi[1], 10);
927
- return Number.isInteger(m) && ((m - 1) & 1) === 1;
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;
928
951
  }
929
- return false;
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;
930
964
  }
931
965
  export function isShiftEnterKey(str, key) {
932
966
  if (!key)
933
967
  return false;
934
968
  const seq = key.sequence || str || "";
969
+ if (parseEnterSequence(seq) === "newline")
970
+ return true;
935
971
  // Shift modifier reported by readline on a return/enter/linefeed key.
936
972
  if (key.shift &&
937
973
  (key.name === "return" ||
@@ -941,8 +977,6 @@ export function isShiftEnterKey(str, key) {
941
977
  seq === "\n")) {
942
978
  return true;
943
979
  }
944
- if (csiShiftEnter(seq))
945
- return true;
946
980
  return false;
947
981
  }
948
982
  export const isNewlineKey = isShiftEnterKey;
@@ -952,6 +986,8 @@ export function isSubmitEnterKey(str, key) {
952
986
  if (isShiftEnterKey(str, key))
953
987
  return false;
954
988
  const seq = key.sequence || str || "";
989
+ if (parseEnterSequence(seq) === "submit")
990
+ return true;
955
991
  if ((key.name === "return" ||
956
992
  seq === "\r" ||
957
993
  seq === "\x1b[13u" ||
@@ -961,6 +997,115 @@ export function isSubmitEnterKey(str, key) {
961
997
  }
962
998
  return false;
963
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
+ const shift = (mod & 1) === 1;
1049
+ const ctrl = (mod & 4) === 4;
1050
+ const alt = (mod & 2) === 2;
1051
+ const cmd = (c) => ({ type: "cmd", cmd: c });
1052
+ const text = (t) => ({ type: "text", text: t });
1053
+ // Control combos
1054
+ if (ctrl && vk === 67)
1055
+ return cmd("interrupt"); // Ctrl+C
1056
+ if (ctrl && vk === 68)
1057
+ return cmd("eof"); // Ctrl+D
1058
+ if (ctrl && vk === 90)
1059
+ return cmd("eof"); // Ctrl+Z
1060
+ if (ctrl && vk === 65)
1061
+ return cmd("home"); // Ctrl+A
1062
+ if (ctrl && vk === 69)
1063
+ return cmd("end"); // Ctrl+E
1064
+ if (ctrl && vk === 85)
1065
+ return cmd("clear-line"); // Ctrl+U
1066
+ if (ctrl && vk === 75)
1067
+ return cmd("kill-line"); // Ctrl+K
1068
+ if (ctrl && vk === 87)
1069
+ return cmd("delete-word"); // Ctrl+W
1070
+ switch (vk) {
1071
+ case 13: // Enter
1072
+ return shift ? cmd("newline") : cmd("submit");
1073
+ case 8: // Backspace
1074
+ return cmd(ctrl ? "delete-word" : "backspace");
1075
+ case 46: // Delete
1076
+ return cmd("delete");
1077
+ case 37: // Left
1078
+ return ctrl ? cmd("back-word") : cmd("left");
1079
+ case 39: // Right
1080
+ return ctrl ? cmd("forward-word") : cmd("right");
1081
+ case 38: // Up
1082
+ return cmd("up");
1083
+ case 40: // Down
1084
+ return cmd("down");
1085
+ case 36: // Home
1086
+ return cmd("home");
1087
+ case 35: // End
1088
+ return cmd("end");
1089
+ case 33: // PageUp
1090
+ return cmd("up");
1091
+ case 34: // PageDown
1092
+ return cmd("down");
1093
+ case 9: // Tab
1094
+ return cmd("tab");
1095
+ case 32: // Space
1096
+ return text(" ");
1097
+ case 27: // Escape
1098
+ return cmd("escape");
1099
+ default: {
1100
+ if (alt)
1101
+ return null; // Alt combos unsupported -> ignore
1102
+ const ch = vkToChar(vk, shift);
1103
+ if (ch)
1104
+ return text(ch);
1105
+ return cmd("ignore");
1106
+ }
1107
+ }
1108
+ }
964
1109
  export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
965
1110
  return new Promise((resolve, reject) => {
966
1111
  const isTTY = process.stdin.isTTY;
@@ -995,10 +1140,10 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
995
1140
  if (process.stdin.isTTY) {
996
1141
  process.stdin.setRawMode(true);
997
1142
  process.stdout.write("\x1b[?2004h");
998
- // Enable CSI-u "disambiguate" so the terminal reports Shift+Enter as a
999
- // distinct code (`ESC [ 13 ; 2 u`) instead of collapsing it into a plain
1000
- // Enter. Pop any prior mode, then push flags=1 (disambiguate).
1001
- process.stdout.write("\x1b[<u\x1b[>1u");
1143
+ // Enable win32-input-mode (preferred by Windows Terminal) and CSI-u
1144
+ // disambiguation so Shift+Enter arrives as a distinct code instead of a
1145
+ // plain Enter. Harmless if a terminal doesn't support them.
1146
+ process.stdout.write("\x1b[?9001h\x1b[>1u");
1002
1147
  }
1003
1148
  process.stdin.resume();
1004
1149
  hideCursor();
@@ -1021,13 +1166,14 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1021
1166
  if (done)
1022
1167
  return;
1023
1168
  done = true;
1024
- process.stdout.write("\x1b[?2004l");
1169
+ process.stdout.write("\x1b[?2004l\x1b[?9001l");
1025
1170
  process.stdin.setRawMode(false);
1026
1171
  if (pasteBurstTimer)
1027
1172
  clearTimeout(pasteBurstTimer);
1028
1173
  clearBox();
1029
1174
  process.stdin.removeListener("keypress", onKeypress);
1030
1175
  process.stdin.pause();
1176
+ showCursor();
1031
1177
  if (isErr)
1032
1178
  reject(value);
1033
1179
  else
@@ -1341,26 +1487,122 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1341
1487
  pasteBurstTimer = null;
1342
1488
  }, 500);
1343
1489
  };
1344
- const onKeypress = (str, key) => {
1345
- // Reassemble a modifier-prefixed CSI sequence that readline splits up,
1346
- // e.g. Shift+Enter arriving as `\x1b[27;2;` + "1" + "3" + "~".
1347
- if (key?.sequence === "\x1b[27;" || key?.sequence === "\x1b[27;2;") {
1348
- modSeq = key.sequence;
1490
+ const handleRawSequence = (full) => {
1491
+ // Enter sequences (CSI-u, kitty, legacy, SS3, win32).
1492
+ const enter = parseEnterSequence(full);
1493
+ if (enter === "newline") {
1494
+ insert("\n");
1495
+ repaint();
1496
+ return;
1497
+ }
1498
+ if (enter === "submit") {
1499
+ if (buffer.trim())
1500
+ settle([buffer, pasteSpans], false);
1501
+ return;
1502
+ }
1503
+ // Other win32-input-mode keys (arrows, backspace, etc.).
1504
+ const dec = decodeWin32Key(full);
1505
+ if (!dec)
1506
+ return;
1507
+ if (dec.type === "text") {
1508
+ insert(dec.text);
1509
+ repaint();
1349
1510
  return;
1350
1511
  }
1512
+ switch (dec.cmd) {
1513
+ case "interrupt":
1514
+ settle(new Error("interrupt"), true);
1515
+ return;
1516
+ case "eof":
1517
+ settle(new Error("eof"), true);
1518
+ return;
1519
+ case "backspace":
1520
+ backspace();
1521
+ repaint();
1522
+ return;
1523
+ case "delete-word":
1524
+ deleteWordBefore();
1525
+ repaint();
1526
+ return;
1527
+ case "delete":
1528
+ deleteChar();
1529
+ repaint();
1530
+ return;
1531
+ case "left":
1532
+ moveLeft();
1533
+ repaint();
1534
+ return;
1535
+ case "right":
1536
+ moveRight();
1537
+ repaint();
1538
+ return;
1539
+ case "back-word": {
1540
+ const sp = buffer.slice(0, cursor).lastIndexOf(" ");
1541
+ cursor = sp === -1 ? 0 : sp + 1;
1542
+ repaint();
1543
+ return;
1544
+ }
1545
+ case "forward-word": {
1546
+ const sp = buffer.indexOf(" ", cursor);
1547
+ cursor = sp === -1 ? buffer.length : sp + 1;
1548
+ repaint();
1549
+ return;
1550
+ }
1551
+ case "up":
1552
+ moveUp();
1553
+ repaint();
1554
+ return;
1555
+ case "down":
1556
+ moveDown();
1557
+ repaint();
1558
+ return;
1559
+ case "home":
1560
+ cursor = 0;
1561
+ repaint();
1562
+ return;
1563
+ case "end":
1564
+ cursor = buffer.length;
1565
+ repaint();
1566
+ return;
1567
+ case "tab":
1568
+ insert(" ");
1569
+ repaint();
1570
+ return;
1571
+ case "clear-line":
1572
+ buffer = "";
1573
+ cursor = 0;
1574
+ pasteSpans = [];
1575
+ repaint();
1576
+ return;
1577
+ case "kill-line":
1578
+ buffer = buffer.slice(0, cursor);
1579
+ repaint();
1580
+ return;
1581
+ default:
1582
+ return;
1583
+ }
1584
+ };
1585
+ const onKeypress = (str, key) => {
1586
+ // Reassemble modifier-prefixed / win32-input-mode CSI sequences, which
1587
+ // Node's readline splits into several keypress events (e.g. Shift+Enter
1588
+ // arriving as `\x1b[27;2;` + "1" + "3" + "~", or a win32-input-mode
1589
+ // sequence `\x1b[0;1;1;13;2u` arriving piecemeal). A "partial CSI" is
1590
+ // any ESC [ sequence readline left unterminated; we keep appending until
1591
+ // it ends in a terminator, then classify it.
1592
+ const seq = key?.sequence || str || "";
1351
1593
  if (modSeq) {
1352
- modSeq += str || key?.sequence || "";
1353
- if (/[~u_]/.test(modSeq.slice(-1)) || modSeq.endsWith("~")) {
1594
+ modSeq += str || seq || "";
1595
+ if (/^\x1b\[[\d;]*[a-zA-Z~_u]$/.test(modSeq)) {
1354
1596
  const full = modSeq;
1355
1597
  modSeq = "";
1356
- // e.g. `\x1b[27;2;13~` = Shift+Enter.
1357
- if (csiShiftEnter(full)) {
1358
- insert("\n");
1359
- repaint();
1360
- }
1598
+ handleRawSequence(full);
1361
1599
  }
1362
1600
  return;
1363
1601
  }
1602
+ if (/^\x1b\[[\d;]*$/.test(seq)) {
1603
+ modSeq = seq;
1604
+ return;
1605
+ }
1364
1606
  if (consumeBracketedPaste(str, key))
1365
1607
  return;
1366
1608
  if (pasteBurst && str && str.length === 1 && !key?.ctrl && !key?.meta) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.65",
3
+ "version": "1.0.67",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },