@oxecli/oxe 1.0.114 → 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 +124 -54
  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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.114",
3
+ "version": "1.0.115",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },