@oxecli/oxe 1.0.105 → 1.0.107

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/README.md CHANGED
@@ -36,5 +36,9 @@ Install Oxe globally with npm:
36
36
 
37
37
 
38
38
 
39
+ **Info**
40
+
41
+
42
+
39
43
  Oxe can inspect your project, make the required changes, run commands, and show you what it is doing along the way.
40
44
 
package/dist/cli.js CHANGED
@@ -60,7 +60,7 @@ export class CLI {
60
60
  // The result body was formatted against the terminal width at tool time,
61
61
  // but the window may be narrower on resume — re-truncate defensively so
62
62
  // long persisted lines never run off the screen.
63
- const lineMax = Math.max(terminalWidth() - 4, 20);
63
+ const lineMax = Math.max(terminalWidth() - 4, 1);
64
64
  const allLines = body.split("\n");
65
65
  const lines = allLines
66
66
  .slice(0, TOOL_RESULT_MAX_LINES)
@@ -81,7 +81,7 @@ export class CLI {
81
81
  const truncated = truncateEllipsis(body, Math.min(max_action_chars, MAX_COMMAND_DISPLAY_CHARS), "text");
82
82
  // truncateEllipsis appends a "…(text truncated: N chars total)" suffix, so
83
83
  // re-truncate to the current width to keep the legacy action on one row.
84
- const displayMax = Math.max(terminalWidth() - 6, 20);
84
+ const displayMax = Math.max(terminalWidth() - 6, 1);
85
85
  process.stdout.write(`${icon} ${style}${truncateStyled(truncated, displayMax)}\x1b[0m\n`);
86
86
  }
87
87
  printAssistantBlock(text) {
package/dist/engine.js CHANGED
@@ -82,7 +82,7 @@ export class InferenceEngine {
82
82
  }
83
83
  }
84
84
  }
85
- async runTool(name, argumentsJson) {
85
+ async runTool(name, argumentsJson, signal) {
86
86
  let args = {};
87
87
  try {
88
88
  args = argumentsJson ? JSON.parse(argumentsJson) : {};
@@ -103,7 +103,7 @@ export class InferenceEngine {
103
103
  result = toolEditFile(args.path, args.old_string, args.new_string, args.replace_all);
104
104
  break;
105
105
  case "bash":
106
- result = await toolBash(args.command, args.timeout, args.cwd);
106
+ result = await toolBash(args.command, args.timeout, args.cwd, signal);
107
107
  break;
108
108
  case "glob":
109
109
  result = toolGlob(args.pattern, args.path, args.limit, args.depth);
@@ -591,7 +591,7 @@ export class InferenceEngine {
591
591
  dockTransientStart("flush");
592
592
  const toolSpinner = new Spinner();
593
593
  toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
594
- const rawResult = await this.runTool(c.name, c.arguments);
594
+ const rawResult = await this.runTool(c.name, c.arguments, this.activeAbort?.signal);
595
595
  // A tool call was made, which interrupts any current working phase,
596
596
  // so the next agent iteration may commit a fresh "Worked for …" block.
597
597
  this.workedCommitted = false;
package/dist/tools.js CHANGED
@@ -586,7 +586,7 @@ function terminateProcessTree(proc) {
586
586
  }
587
587
  }
588
588
  }
589
- export function toolBash(command, timeout = 60, cwd) {
589
+ export function toolBash(command, timeout = 60, cwd, signal) {
590
590
  try {
591
591
  let t = typeof timeout === "number" ? timeout : parseInt(String(timeout), 10);
592
592
  if (Number.isNaN(t))
@@ -597,6 +597,10 @@ export function toolBash(command, timeout = 60, cwd) {
597
597
  return Promise.resolve(`Error: working directory not found: ${cwd}`);
598
598
  }
599
599
  return new Promise((resolve) => {
600
+ if (signal && signal.aborted) {
601
+ resolve("Error: command aborted");
602
+ return;
603
+ }
600
604
  let child;
601
605
  try {
602
606
  if (process.platform === "win32") {
@@ -621,6 +625,18 @@ export function toolBash(command, timeout = 60, cwd) {
621
625
  let stdout = "";
622
626
  let stderr = "";
623
627
  let finished = false;
628
+ const onAbort = () => {
629
+ if (finished)
630
+ return;
631
+ terminateProcessTree({
632
+ pid: child.pid,
633
+ kill: (sig) => child.kill(sig),
634
+ });
635
+ finished = true;
636
+ clearTimeout(timer);
637
+ resolve("Error: command aborted");
638
+ };
639
+ signal?.addEventListener("abort", onAbort, { once: true });
624
640
  const timer = setTimeout(() => {
625
641
  if (finished)
626
642
  return;
@@ -633,6 +649,8 @@ export function toolBash(command, timeout = 60, cwd) {
633
649
  if (output)
634
650
  msg += `\n${output}`;
635
651
  finished = true;
652
+ clearTimeout(timer);
653
+ signal?.removeEventListener("abort", onAbort);
636
654
  resolve(msg);
637
655
  }, t * 1000);
638
656
  child.stdout?.on("data", (d) => (stdout += d.toString("utf-8")));
@@ -642,6 +660,7 @@ export function toolBash(command, timeout = 60, cwd) {
642
660
  return;
643
661
  finished = true;
644
662
  clearTimeout(timer);
663
+ signal?.removeEventListener("abort", onAbort);
645
664
  resolve(`Error: ${err}`);
646
665
  });
647
666
  child.on("close", (code) => {
@@ -649,6 +668,7 @@ export function toolBash(command, timeout = 60, cwd) {
649
668
  return;
650
669
  finished = true;
651
670
  clearTimeout(timer);
671
+ signal?.removeEventListener("abort", onAbort);
652
672
  const output = (stdout + stderr).trim() || "(no output)";
653
673
  resolve(`exit code: ${code}\n${output}`);
654
674
  });
package/dist/ui.js CHANGED
@@ -76,8 +76,11 @@ export function startDraftCapture(onInterrupt, renderBox) {
76
76
  return;
77
77
  }
78
78
  if (str && !key?.ctrl && !key?.meta) {
79
- draft += str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
80
- renderBox?.(draft, pasteSpans);
79
+ const added = str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
80
+ if (draft.length + added.length <= MAX_PROMPT_CHARS) {
81
+ draft += added;
82
+ renderBox?.(draft, pasteSpans);
83
+ }
81
84
  }
82
85
  };
83
86
  readline.emitKeypressEvents(process.stdin);
@@ -113,6 +116,21 @@ export function clearScreen() {
113
116
  export function terminalWidth() {
114
117
  return process.stdout.columns || 80;
115
118
  }
119
+ /** Minimum width for boxes/cards so they don't collapse to nothing. */
120
+ const MIN_BOX_WIDTH = 20;
121
+ /** Horizontal margin reserved on each side of a box/card from the terminal edge. */
122
+ const BOX_MARGIN = 4;
123
+ /**
124
+ * Clamp a preferred box width so it fits inside the terminal (borders never wrap
125
+ * or duplicate on narrow/scaled terminals, e.g. a larger command-prompt scale)
126
+ * while keeping a preferred minimum. Fitting wins over the minimum when the
127
+ * terminal is narrower than min+margin, so cards size down smoothly instead of
128
+ * overflowing.
129
+ */
130
+ function fitBoxWidth(pref, termW) {
131
+ const maxW = Math.max(termW - BOX_MARGIN, 1);
132
+ return Math.min(Math.max(Math.min(pref, maxW), MIN_BOX_WIDTH), maxW);
133
+ }
116
134
  // ---------------------------------------------------------------------------
117
135
  // ANSI and plain text length helpers
118
136
  // ---------------------------------------------------------------------------
@@ -290,7 +308,7 @@ function formatMarkdownTable(tableLines) {
290
308
  // Rich Markdown -> ANSI Terminal Renderer
291
309
  // ---------------------------------------------------------------------------
292
310
  export function markdownToAnsi(text) {
293
- const width = Math.max(terminalWidth() - 2, 40);
311
+ const width = Math.max(terminalWidth() - 2, 1);
294
312
  const rawLines = text.split("\n");
295
313
  const out = [];
296
314
  let inCode = false;
@@ -445,8 +463,8 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
445
463
  maxLineW = Math.max(maxLineW, plainLen(subtitle) + 4);
446
464
  const termW = terminalWidth();
447
465
  const boxW = expand
448
- ? Math.max(termW - 4, 20)
449
- : Math.min(Math.max(maxLineW + 4, 20), termW - 4);
466
+ ? fitBoxWidth(termW - 4, termW)
467
+ : fitBoxWidth(maxLineW + 4, termW);
450
468
  const innerW = Math.max(boxW - 4, 1);
451
469
  const embed = (text, align) => {
452
470
  if (!text)
@@ -1072,7 +1090,7 @@ export function toolCallLabel(name, argumentsJson) {
1072
1090
  arg = arg.trim().replace(/\s*\r?\n\s*/g, " ");
1073
1091
  if (!arg)
1074
1092
  return `\x1b[1;36m${display}\x1b[0m`;
1075
- const cap = Math.max(Math.min(terminalWidth() - plainLen(display) - 4, MAX_COMMAND_DISPLAY_CHARS), 20);
1093
+ const cap = Math.max(Math.min(terminalWidth() - plainLen(display) - 4, MAX_COMMAND_DISPLAY_CHARS), Math.min(terminalWidth() - plainLen(display) - 2, 8));
1076
1094
  if (plainLen(arg) > cap)
1077
1095
  arg = truncateStyled(arg, cap) + "…";
1078
1096
  return `\x1b[1;36m${display}\x1b[0m(${arg})`;
@@ -1097,7 +1115,7 @@ export function formatToolResult(rawResult, failed) {
1097
1115
  // Truncate to the terminal width (rows OR per-line length, whichever trips
1098
1116
  // first) so long tool output — e.g. Glob paths — never runs off the screen.
1099
1117
  const termW = terminalWidth();
1100
- const lineMax = Math.max(termW - 4, 20);
1118
+ const lineMax = Math.max(termW - 4, 1);
1101
1119
  const allLines = clean.split("\n");
1102
1120
  const lines = allLines
1103
1121
  .slice(0, TOOL_RESULT_MAX_LINES)
@@ -1116,10 +1134,32 @@ export function formatToolResult(rawResult, failed) {
1116
1134
  export const PROMPT_PLACEHOLDER = "Describe a coding task, or type /help for commands";
1117
1135
  const MAX_PROMPT_DISPLAY_LINES = 12;
1118
1136
  const MAX_PROMPT_PASTE_CHARS = 400;
1137
+ const MAX_PROMPT_CHARS = 500;
1119
1138
  function shouldCollapsePaste(text) {
1120
1139
  return (text.split("\n").length > MAX_PROMPT_DISPLAY_LINES ||
1121
1140
  text.length > MAX_PROMPT_PASTE_CHARS);
1122
1141
  }
1142
+ /**
1143
+ * Effective character count of a prompt for its length limit. Each non-collapsed
1144
+ * char counts as one; a collapsed pasted block counts as a single char.
1145
+ */
1146
+ export function promptCharCount(buffer, pasteSpans) {
1147
+ if (!buffer)
1148
+ return 0;
1149
+ const spans = [...pasteSpans].sort((a, b) => a[0] - b[0]);
1150
+ let count = 0;
1151
+ let last = 0;
1152
+ for (const [s, e] of spans) {
1153
+ if (s > last)
1154
+ count += s - last;
1155
+ const seg = buffer.slice(s, e);
1156
+ count += shouldCollapsePaste(seg) ? 1 : e - s;
1157
+ last = e;
1158
+ }
1159
+ if (last < buffer.length)
1160
+ count += buffer.length - last;
1161
+ return count;
1162
+ }
1123
1163
  export function cursorLineCol(buffer, cursor) {
1124
1164
  const before = buffer.slice(0, cursor);
1125
1165
  const lines = before.split("\n");
@@ -1238,7 +1278,7 @@ function wrapRuns(runs, width) {
1238
1278
  }
1239
1279
  export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints) {
1240
1280
  const termW = terminalWidth();
1241
- const boxW = Math.max(termW - 4, 16);
1281
+ const boxW = fitBoxWidth(termW - 4, termW);
1242
1282
  const innerW = boxW - 4;
1243
1283
  const runs = [];
1244
1284
  runs.push({ text: prefix, style: "\x1b[90m" });
@@ -1536,14 +1576,32 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1536
1576
  draft = { buffer, cursor, spans: pasteSpans.slice() };
1537
1577
  histIdx = hist.length;
1538
1578
  }
1539
- const pasteStart = cursor;
1540
- buffer = buffer.slice(0, cursor) + text + buffer.slice(cursor);
1541
- pasteSpans = shiftSpansAfterInsert(cursor, text.length);
1579
+ // Non-collapsing pastes get a leading space when inserted after non-empty,
1580
+ // non-whitespace text (never when a space is already present). The space is
1581
+ // inserted as plain text (outside the paste span) so display spacing isn't
1582
+ // doubled.
1583
+ let prefix = "";
1584
+ if (isPaste &&
1585
+ !shouldCollapsePaste(text) &&
1586
+ cursor > 0 &&
1587
+ !endsWithWs(buffer.slice(0, cursor))) {
1588
+ prefix = " ";
1589
+ }
1590
+ const pasteStart = cursor + prefix.length;
1591
+ const insertLen = prefix.length + text.length;
1592
+ const newBuffer = buffer.slice(0, cursor) + prefix + text + buffer.slice(cursor);
1593
+ const newSpans = shiftSpansAfterInsert(cursor, insertLen);
1542
1594
  if (isPaste) {
1543
- pasteSpans.push([pasteStart, pasteStart + text.length]);
1544
- pasteSpans.sort((a, b) => a[0] - b[0]);
1595
+ newSpans.push([pasteStart, pasteStart + text.length]);
1596
+ newSpans.sort((a, b) => a[0] - b[0]);
1597
+ }
1598
+ // Enforce the 500-char effective limit; a collapsed block counts as one.
1599
+ if (promptCharCount(newBuffer, newSpans) > MAX_PROMPT_CHARS) {
1600
+ return;
1545
1601
  }
1546
- cursor += text.length;
1602
+ buffer = newBuffer;
1603
+ pasteSpans = newSpans;
1604
+ cursor += insertLen;
1547
1605
  };
1548
1606
  const spanCollapsed = (span) => {
1549
1607
  return shouldCollapsePaste(buffer.slice(span[0], span[1]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.105",
3
+ "version": "1.0.107",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },