@oxecli/oxe 1.0.107 → 1.0.109

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/engine.js CHANGED
@@ -586,11 +586,11 @@ export class InferenceEngine {
586
586
  this.queryCalledTool = true;
587
587
  const started = toolCallLabel(c.name, c.arguments);
588
588
  // Print the tool label the instant the command starts, then show a
589
- // "╰─ Working..." dots line that swaps to the real result in place.
589
+ // "╰─ Running..." dots line that swaps to the real result in place.
590
590
  dockAppendContent(`${started}\n`);
591
591
  dockTransientStart("flush");
592
592
  const toolSpinner = new Spinner();
593
- toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
593
+ toolSpinner.startDots("\x1b[90m╰─\x1b[0m Running");
594
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.
@@ -609,7 +609,7 @@ export class InferenceEngine {
609
609
  status: failed ? "failed" : "ok",
610
610
  diff,
611
611
  });
612
- // Swap the "╰─ Working..." line for the real result. If the tool
612
+ // Swap the "╰─ Running..." line for the real result. If the tool
613
613
  // produced a diff it renders directly below the action line with no
614
614
  // gap; otherwise the dock gap separates the action from what follows.
615
615
  toolSpinner.stop();
package/dist/oxe.js CHANGED
@@ -1,10 +1,18 @@
1
1
  import { main } from "./cli.js";
2
+ import { dockTearDown, showCursor } from "./ui.js";
2
3
  export { main };
3
4
  // The TUI keeps the terminal cursor hidden except inside a prompt field. Always
4
- // restore it on exit so the shell isn't left with an invisible cursor.
5
+ // restore it on exit and tear down any docked prompt box so the shell isn't
6
+ // left with a stale prompt box or an invisible cursor.
5
7
  process.on("exit", () => {
6
8
  try {
7
- process.stdout.write("\x1b[?25h");
9
+ dockTearDown();
10
+ }
11
+ catch {
12
+ /* ignore */
13
+ }
14
+ try {
15
+ showCursor();
8
16
  }
9
17
  catch {
10
18
  /* ignore */
package/dist/ui.js CHANGED
@@ -117,7 +117,7 @@ export function terminalWidth() {
117
117
  return process.stdout.columns || 80;
118
118
  }
119
119
  /** Minimum width for boxes/cards so they don't collapse to nothing. */
120
- const MIN_BOX_WIDTH = 20;
120
+ const MIN_BOX_WIDTH = 40;
121
121
  /** Horizontal margin reserved on each side of a box/card from the terminal edge. */
122
122
  const BOX_MARGIN = 4;
123
123
  /**
@@ -173,6 +173,88 @@ export function truncateStyled(text, maxVisible) {
173
173
  }
174
174
  return out;
175
175
  }
176
+ /**
177
+ * Wrap a styled (ANSI) string into lines of at most `width` visible characters,
178
+ * wrapping whole words onto new lines and splitting any single word longer than
179
+ * `width`. ANSI escape sequences are preserved and re-attached to their glyphs,
180
+ * so no style is lost and no escape sequence is ever cut mid-way.
181
+ */
182
+ function wrapStyledToWidth(line, width) {
183
+ if (width <= 0)
184
+ return [line];
185
+ if (plainLen(line) <= width)
186
+ return [line];
187
+ // Tokenize into glyph units, each carrying the ANSI prefix that precedes it.
188
+ const units = [];
189
+ const re = /(\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07)|([\s\S])/g;
190
+ let pending = "";
191
+ let m;
192
+ re.lastIndex = 0;
193
+ while ((m = re.exec(line)) !== null) {
194
+ if (m[1] !== undefined)
195
+ pending += m[1];
196
+ else {
197
+ units.push({ ch: m[2], pre: pending });
198
+ pending = "";
199
+ }
200
+ }
201
+ const render = (atoms) => atoms.map((a) => a.pre + a.ch).join("");
202
+ const rows = [];
203
+ let cur = [];
204
+ let curLen = 0;
205
+ // Index (in `cur`) of the whitespace atom that is a valid break point, or -1.
206
+ let breakAt = -1;
207
+ // The last SGR open code currently in effect, so a word split mid-way can
208
+ // re-apply the style on its continuation.
209
+ let activeStyle = "";
210
+ for (const a of units) {
211
+ // Track the active style from this glyph's ANSI prefix.
212
+ if (a.pre) {
213
+ const sgr = a.pre.match(/\x1b\[[0-9;]*m/g);
214
+ if (sgr && sgr.length) {
215
+ const last = sgr[sgr.length - 1];
216
+ activeStyle = last === "\x1b[0m" ? "" : last;
217
+ }
218
+ }
219
+ const isSpace = /\s/.test(a.ch);
220
+ if (isSpace) {
221
+ cur.push(a);
222
+ curLen++;
223
+ breakAt = cur.length - 1;
224
+ continue;
225
+ }
226
+ if (curLen + 1 > width) {
227
+ if (breakAt >= 0) {
228
+ // Break after the whitespace at breakAt: keep the whitespace's ANSI
229
+ // prefix (often a reset) so style never leaks onto the continuation.
230
+ const space = cur[breakAt];
231
+ const lead = space.pre; // e.g. \x1b[0m — applied at start of next row
232
+ const kept = cur.slice(breakAt + 1);
233
+ rows.push(cur.slice(0, breakAt));
234
+ cur = [{ ch: "", pre: lead }, ...kept];
235
+ curLen = kept.length;
236
+ breakAt = -1;
237
+ }
238
+ else if (cur.length) {
239
+ // No whitespace break point: split the word mid-way, re-applying the
240
+ // active style on the continuation so a styled word keeps its style.
241
+ rows.push(cur);
242
+ const lead = activeStyle ? activeStyle : "";
243
+ cur = lead ? [{ ch: "", pre: lead }] : [];
244
+ curLen = 0;
245
+ }
246
+ }
247
+ cur.push(a);
248
+ curLen++;
249
+ }
250
+ if (cur.length)
251
+ rows.push(cur);
252
+ // Emit each row, ending with a reset so no style bleeds across rows.
253
+ return rows.map((r) => {
254
+ const rendered = render(r);
255
+ return rendered + (plainLen(rendered) ? "\x1b[0m" : "");
256
+ });
257
+ }
176
258
  // ---------------------------------------------------------------------------
177
259
  // Markup tag -> ANSI
178
260
  // ---------------------------------------------------------------------------
@@ -412,22 +494,11 @@ export function markdownToAnsi(text) {
412
494
  if (!line.includes("\x1b[36m•\x1b[0m") && !line.includes(".\x1b[0m ")) {
413
495
  line = formatInlineMarkdown(line);
414
496
  }
415
- // Word wrap paragraph line if too long
497
+ // Word wrap paragraph line if too long. Whole words move to a new row; any
498
+ // single word longer than the width is split so nothing is ever cut off at
499
+ // the right edge.
416
500
  if (plainLen(line) > width && !line.startsWith(" ")) {
417
- const words = line.split(" ");
418
- let cur = "";
419
- for (const w of words) {
420
- if (plainLen(cur) + plainLen(w) + 1 > width) {
421
- if (cur)
422
- out.push(cur);
423
- cur = w;
424
- }
425
- else {
426
- cur = cur ? `${cur} ${w}` : w;
427
- }
428
- }
429
- if (cur)
430
- out.push(cur);
501
+ out.push(...wrapStyledToWidth(line, width));
431
502
  }
432
503
  else {
433
504
  out.push(line);
@@ -884,7 +955,7 @@ export function showCursor() {
884
955
  }
885
956
  }
886
957
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
887
- const DOT_FRAMES = [". ", ".. ", "..."];
958
+ const DOT_FRAMES = [".", "..", "..."];
888
959
  export class Spinner {
889
960
  timer = null;
890
961
  frame = 0;
@@ -944,12 +1015,15 @@ export class Spinner {
944
1015
  }
945
1016
  writeDots(initial) {
946
1017
  const dots = DOT_FRAMES[this.dotsFrame];
1018
+ // Always emit a fixed 3-cell field so packed dots (., .., ...) animate
1019
+ // without leaving residue when the frame shrinks (e.g. ... -> .).
1020
+ const padded = dots.padEnd(3, " ");
947
1021
  if (initial) {
948
- process.stdout.write("\r" + this.dotsText + dots + "\r");
1022
+ process.stdout.write("\r" + this.dotsText + padded + "\r");
949
1023
  }
950
1024
  else {
951
1025
  // Jump to the dots column (1-based) and rewrite only the 3-cell field.
952
- process.stdout.write(`\x1b[${plainLen(this.dotsText) + 1}G` + dots + "\r");
1026
+ process.stdout.write(`\x1b[${plainLen(this.dotsText) + 1}G` + padded + "\r");
953
1027
  }
954
1028
  }
955
1029
  /** Overwrite the dots line in place with `text` and advance to the next
@@ -1377,11 +1451,18 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
1377
1451
  const lines = [top, ...body, bottom];
1378
1452
  let totalRows = body.length + 2;
1379
1453
  if (hints && (hints.left || hints.right)) {
1380
- const left = hints.left ?? "";
1454
+ let left = hints.left ?? "";
1381
1455
  const right = hints.right ?? "";
1382
1456
  // Inset the hint row inside the box (box spans columns 0..boxW-1): a
1383
1457
  // leading space gives left-edge padding, and the right hint ends one
1384
- // column before the box's right border for the same padding.
1458
+ // column before the box's right border for the same padding. Truncate the
1459
+ // left hint so the row never exceeds the box width on narrow terminals
1460
+ // (otherwise it overflows past the right border instead of scaling down).
1461
+ const hintMax = boxW - 2;
1462
+ if (plainLen(left) + plainLen(right) + 1 > hintMax) {
1463
+ const room = Math.max(0, hintMax - plainLen(right) - 1);
1464
+ left = truncateStyled(left, room);
1465
+ }
1385
1466
  const gap = Math.max(1, boxW - 2 - plainLen(left) - plainLen(right));
1386
1467
  lines.push(" " + left + " ".repeat(gap) + right);
1387
1468
  totalRows += 1;
@@ -1504,6 +1585,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1504
1585
  dockEnterDocked();
1505
1586
  }
1506
1587
  process.stdin.removeListener("keypress", onKeypress);
1588
+ process.stdout.removeListener("resize", onResize);
1507
1589
  process.stdin.pause();
1508
1590
  showCursor();
1509
1591
  if (isErr)
@@ -1945,6 +2027,17 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1945
2027
  }
1946
2028
  };
1947
2029
  process.stdin.on("keypress", onKeypress);
2030
+ // Reflow the idle prompt box when the terminal resizes so it scales down
2031
+ // instead of wrapping/overflowing its borders on a narrow/scaled terminal.
2032
+ const onResize = () => {
2033
+ try {
2034
+ repaint(false);
2035
+ }
2036
+ catch {
2037
+ /* ignore */
2038
+ }
2039
+ };
2040
+ process.stdout.on("resize", onResize);
1948
2041
  repaint(true);
1949
2042
  });
1950
2043
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.107",
3
+ "version": "1.0.109",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },