@oxecli/oxe 1.0.113 → 1.0.115

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 (2) hide show
  1. package/dist/ui.js +143 -62
  2. package/package.json +1 -1
package/dist/ui.js CHANGED
@@ -116,8 +116,10 @@ export function clearScreen() {
116
116
  export function terminalWidth() {
117
117
  return process.stdout.columns || 80;
118
118
  }
119
- /** Minimum width for boxes/cards so they don't collapse to nothing. */
120
- const MIN_BOX_WIDTH = 52;
119
+ /** Minimum width for boxes/cards so they don't collapse to nothing. Kept
120
+ * well below the smallest terminal width so a card can keep scaling down
121
+ * (it never forces a box wider than the terminal). */
122
+ const MIN_BOX_WIDTH = 30;
121
123
  /** Horizontal margin reserved on each side of a box/card from the terminal edge. */
122
124
  const BOX_MARGIN = 4;
123
125
  /**
@@ -1336,69 +1338,126 @@ export function splitBlocks(buffer, pasteSpans) {
1336
1338
  }
1337
1339
  const PROMPT_CARET = "\u0000";
1338
1340
  function wrapRuns(runs, width) {
1341
+ const units = [];
1342
+ for (const run of runs) {
1343
+ if (run.text === PROMPT_CARET) {
1344
+ units.push({ ch: PROMPT_CARET, style: run.style, caret: true });
1345
+ }
1346
+ else {
1347
+ for (const ch of run.text) {
1348
+ units.push({ ch, style: run.style, caret: false });
1349
+ }
1350
+ }
1351
+ }
1352
+ if (units.length === 0)
1353
+ units.push({ ch: "", style: "", caret: false });
1339
1354
  const rows = [];
1340
- let cur = "";
1341
- let curLen = 0;
1355
+ let line = [];
1356
+ let lineLen = 0;
1342
1357
  let caretRow = -1;
1343
1358
  let caretCol = -1;
1344
- const flush = () => {
1345
- rows.push(cur);
1346
- cur = "";
1347
- curLen = 0;
1359
+ const flushLine = () => {
1360
+ if (line.length === 0)
1361
+ return;
1362
+ let out = "";
1363
+ for (const u of line) {
1364
+ if (u.caret)
1365
+ out += u.style + "▏" + "\x1b[0m";
1366
+ else if (u.style)
1367
+ out += u.style + u.ch + "\x1b[0m";
1368
+ else
1369
+ out += u.ch;
1370
+ }
1371
+ rows.push(out);
1372
+ line = [];
1373
+ lineLen = 0;
1348
1374
  };
1349
- for (const run of runs) {
1350
- if (run.text === PROMPT_CARET) {
1351
- if (curLen >= width)
1352
- flush();
1375
+ const isWs = (u) => !u.caret && /\s/.test(u.ch);
1376
+ const addUnit = (u) => {
1377
+ if (u.caret) {
1353
1378
  caretRow = rows.length;
1354
- caretCol = curLen;
1355
- cur += run.style + "▏" + "\x1b[0m";
1356
- curLen += 1;
1379
+ caretCol = lineLen;
1380
+ }
1381
+ line.push(u);
1382
+ lineLen += 1;
1383
+ };
1384
+ let i = 0;
1385
+ while (i < units.length) {
1386
+ const u = units[i];
1387
+ if (u.caret) {
1388
+ if (lineLen >= width)
1389
+ flushLine();
1390
+ addUnit(u);
1391
+ i++;
1357
1392
  continue;
1358
1393
  }
1359
- let text = run.text;
1360
- while (text) {
1361
- const nl = text.indexOf("\n");
1362
- const seg = nl === -1 ? text : text.slice(0, nl);
1363
- let rest = seg;
1364
- while (rest.length > 0) {
1365
- if (curLen >= width)
1366
- flush();
1367
- let take;
1368
- if (curLen === 0) {
1369
- // Start of a line: take a whole word (or one char for a lone space,
1370
- // or a full-width chunk for an overlong word).
1371
- const sp = rest.indexOf(" ");
1372
- take = sp === -1 ? rest.length : sp;
1373
- if (take === 0)
1374
- take = 1;
1375
- take = Math.min(take, width);
1376
- }
1377
- else {
1378
- // Mid-line: append the next whole word (plus its trailing space) only
1379
- // if it fits; otherwise move the entire word to the next line.
1380
- const sp = rest.indexOf(" ");
1381
- take = sp === -1 ? rest.length : sp + 1;
1382
- if (take > width - curLen) {
1383
- flush();
1384
- continue;
1385
- }
1386
- }
1387
- const chunk = rest.slice(0, take);
1388
- cur += run.style ? run.style + chunk + "\x1b[0m" : chunk;
1389
- curLen += take;
1390
- rest = rest.slice(take);
1394
+ if (u.ch === "\n") {
1395
+ // Hard line break: end the current row and start a fresh one.
1396
+ flushLine();
1397
+ i++;
1398
+ continue;
1399
+ }
1400
+ if (isWs(u)) {
1401
+ if (lineLen + 1 > width)
1402
+ flushLine();
1403
+ // Never start a line with whitespace: if the space lands at the start of
1404
+ // a fresh line, drop it (it was the fill of the previous full line).
1405
+ if (lineLen === 0) {
1406
+ i++;
1407
+ continue;
1391
1408
  }
1392
- if (nl !== -1) {
1393
- flush();
1394
- text = text.slice(nl + 1);
1409
+ addUnit(u);
1410
+ i++;
1411
+ continue;
1412
+ }
1413
+ // Start of a word (which may contain the caret). Find its full extent in
1414
+ // the flattened stream so we never wrap mid-word.
1415
+ let j = i;
1416
+ let wordLen = 0; // each unit (including a caret) occupies one column
1417
+ while (j < units.length && !isWs(units[j])) {
1418
+ wordLen += 1;
1419
+ j++;
1420
+ }
1421
+ if (wordLen === 0) {
1422
+ i++;
1423
+ continue;
1424
+ }
1425
+ if (lineLen === 0) {
1426
+ if (wordLen > width) {
1427
+ // Overlong word: split into width-sized chunks, keeping the caret in
1428
+ // the chunk that contains it.
1429
+ let from = i;
1430
+ let remaining = wordLen;
1431
+ while (remaining > 0) {
1432
+ const n = Math.min(width, remaining);
1433
+ for (let k = 0; k < n; k++)
1434
+ addUnit(units[from + k]);
1435
+ flushLine();
1436
+ from += n;
1437
+ remaining -= n;
1438
+ }
1395
1439
  }
1396
1440
  else {
1397
- break;
1441
+ for (let k = i; k < j; k++)
1442
+ addUnit(units[k]);
1398
1443
  }
1444
+ i = j;
1445
+ }
1446
+ else {
1447
+ if (lineLen + wordLen > width) {
1448
+ // Word doesn't fit on the current line; move the whole word to a new
1449
+ // line (leaving any trailing space from the previous word behind).
1450
+ flushLine();
1451
+ continue;
1452
+ }
1453
+ for (let k = i; k < j; k++)
1454
+ addUnit(units[k]);
1455
+ i = j;
1399
1456
  }
1400
1457
  }
1401
- flush();
1458
+ flushLine();
1459
+ if (rows.length === 0)
1460
+ rows.push("");
1402
1461
  return { rows, caretRow, caretCol };
1403
1462
  }
1404
1463
  export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints) {
@@ -1490,8 +1549,19 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
1490
1549
  }
1491
1550
  const { rows, caretRow, caretCol } = wrapRuns(runs, innerW);
1492
1551
  const labelLen = plainLen(label);
1493
- const topPad = Math.max(0, boxW - labelLen - 7);
1494
- const top = `\x1b[90m╭─── \x1b[1m\x1b[36m${label}\x1b[0m\x1b[90m ${"─".repeat(topPad)}╮\x1b[0m`;
1552
+ // Truncate the label so the top border never exceeds the box width — a long
1553
+ // label used to push past `boxW` and wrap onto its own row, breaking the box
1554
+ // on narrow/scaled terminals. Reserve room for "╭─── ", the two spaces and
1555
+ // "╮" (7 cols) plus at least one dash.
1556
+ let topLabel = label;
1557
+ let topLabelLen = labelLen;
1558
+ const topRoom = Math.max(0, boxW - 8);
1559
+ if (topLabelLen > topRoom) {
1560
+ topLabel = truncateStyledEllipsis(label, topRoom);
1561
+ topLabelLen = plainLen(topLabel);
1562
+ }
1563
+ const topPad = Math.max(0, boxW - topLabelLen - 7);
1564
+ const top = `\x1b[90m╭─── \x1b[1m\x1b[36m${topLabel}\x1b[0m\x1b[90m ${"─".repeat(topPad)}╮\x1b[0m`;
1495
1565
  const body = [];
1496
1566
  for (const l of rows) {
1497
1567
  const plain = stripAnsi(l);
@@ -1503,18 +1573,29 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
1503
1573
  let totalRows = body.length + 2;
1504
1574
  if (hints && (hints.left || hints.right)) {
1505
1575
  let left = hints.left ?? "";
1506
- const right = hints.right ?? "";
1576
+ let right = hints.right ?? "";
1507
1577
  // Inset the hint row inside the box (box spans columns 0..boxW-1): a
1508
1578
  // leading space gives left-edge padding, and the right hint ends one
1509
- // column before the box's right border for the same padding. Truncate the
1510
- // left hint so the row never exceeds the box width on narrow terminals
1511
- // (otherwise it overflows past the right border instead of scaling down).
1579
+ // column before the box's right border for the same padding. Truncate so
1580
+ // the row NEVER exceeds the box width on narrow/scaled terminals: the left
1581
+ // hint is cut first to keep the (priority) right hint intact, then the
1582
+ // right hint is cut as a last resort. Otherwise a long right hint would
1583
+ // overflow past the border instead of scaling down.
1512
1584
  const hintMax = boxW - 2;
1513
- if (plainLen(left) + plainLen(right) + 1 > hintMax) {
1514
- const room = Math.max(0, hintMax - plainLen(right) - 1);
1515
- left = truncateStyled(left, room);
1585
+ const maxText = Math.max(0, hintMax - 1); // reserve a >=1 gap between hints
1586
+ let leftW = plainLen(left);
1587
+ let rightW = plainLen(right);
1588
+ if (leftW + rightW > maxText) {
1589
+ const roomL = Math.max(0, maxText - rightW);
1590
+ left = truncateStyled(left, roomL);
1591
+ leftW = plainLen(left);
1592
+ }
1593
+ const roomR = Math.max(0, maxText - leftW);
1594
+ if (rightW > roomR) {
1595
+ right = truncateStyled(right, roomR);
1596
+ rightW = plainLen(right);
1516
1597
  }
1517
- const gap = Math.max(1, boxW - 2 - plainLen(left) - plainLen(right));
1598
+ const gap = Math.max(1, hintMax - leftW - rightW);
1518
1599
  lines.push(" " + left + " ".repeat(gap) + right);
1519
1600
  totalRows += 1;
1520
1601
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.113",
3
+ "version": "1.0.115",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },