@oxecli/oxe 1.0.106 → 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.
Files changed (3) hide show
  1. package/dist/cli.js +2 -2
  2. package/dist/ui.js +72 -14
  3. package/package.json +1 -1
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/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.106",
3
+ "version": "1.0.107",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },