@oxecli/oxe 1.0.115 → 1.0.117
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/ui.js +136 -40
- package/package.json +1 -1
package/dist/ui.js
CHANGED
|
@@ -140,11 +140,85 @@ export const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[()][AB012]|\x1b
|
|
|
140
140
|
export function stripAnsi(s) {
|
|
141
141
|
return s.replace(ANSI_RE, "");
|
|
142
142
|
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Display width (wcwidth-style) helpers
|
|
145
|
+
//
|
|
146
|
+
// plainLen() historically counted JS characters, but wide characters (CJK,
|
|
147
|
+
// fullwidth forms, emoji) occupy two terminal columns while counting as one
|
|
148
|
+
// string character. That made table columns, panel borders, wrapping and
|
|
149
|
+
// truncation overflow by the number of wide glyphs. These helpers measure
|
|
150
|
+
// real terminal display width so every frame stays aligned.
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/** Wide (double-width) code-point ranges. */
|
|
153
|
+
const WIDE_RANGES = [
|
|
154
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
155
|
+
[0x2e80, 0xa4cf], // CJK radicals, Kangxi, CJK symbols, Hiragana/Katakana, Yi
|
|
156
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
157
|
+
[0xf900, 0xfaff], // CJK compatibility
|
|
158
|
+
[0xfe30, 0xfe4f], // CJK compatibility forms
|
|
159
|
+
[0xff00, 0xff60], // fullwidth forms
|
|
160
|
+
[0xffe0, 0xffe6], // fullwidth signs
|
|
161
|
+
[0x1f300, 0x1f64f], // emoji / misc pictographs
|
|
162
|
+
[0x1f680, 0x1f6ff], // transport & map symbols
|
|
163
|
+
[0x1f900, 0x1f9ff], // supplemental symbols & pictographs
|
|
164
|
+
[0x1fa70, 0x1faff], // symbols & pictographs extended-A
|
|
165
|
+
[0x20000, 0x3fffd], // CJK extension B+ (astral)
|
|
166
|
+
];
|
|
167
|
+
const WIDE_POINTS = new Set([
|
|
168
|
+
0x231a, 0x231b, 0x23e9, 0x23ea, 0x23eb, 0x23ec, 0x23f0, 0x23f3, 0x25fd,
|
|
169
|
+
0x25fe, 0x2614, 0x2615, 0x2648, 0x2649, 0x264a, 0x264b, 0x264c, 0x264d,
|
|
170
|
+
0x264e, 0x264f, 0x2650, 0x2651, 0x2652, 0x2653, 0x267f, 0x2693, 0x26a1,
|
|
171
|
+
0x26aa, 0x26ab, 0x26bd, 0x26be, 0x26c4, 0x26c5, 0x26ce, 0x26d4, 0x26ea,
|
|
172
|
+
0x26f2, 0x26f3, 0x26f5, 0x26fa, 0x26fd, 0x2705, 0x270a, 0x270b, 0x2728,
|
|
173
|
+
0x274c, 0x274e, 0x2753, 0x2754, 0x2755, 0x2757, 0x2795, 0x2796, 0x2797,
|
|
174
|
+
0x27b0, 0x27bf, 0x2b1b, 0x2b1c, 0x2b50, 0x2b55, 0x2329, 0x232a, 0x303f,
|
|
175
|
+
]);
|
|
176
|
+
/** Terminal columns occupied by a single character (0 combining, 1 normal,
|
|
177
|
+
* 2 wide). */
|
|
178
|
+
export function charWidth(ch) {
|
|
179
|
+
const c = ch.codePointAt(0);
|
|
180
|
+
if (c == null)
|
|
181
|
+
return 0;
|
|
182
|
+
// Combining marks occupy no columns of their own.
|
|
183
|
+
if ((c >= 0x0300 && c <= 0x036f) ||
|
|
184
|
+
(c >= 0x1ab0 && c <= 0x1aff) ||
|
|
185
|
+
(c >= 0x1dc0 && c <= 0x1dff) ||
|
|
186
|
+
(c >= 0x20d0 && c <= 0x20ff) ||
|
|
187
|
+
(c >= 0xfe20 && c <= 0xfe2f))
|
|
188
|
+
return 0;
|
|
189
|
+
for (const [lo, hi] of WIDE_RANGES)
|
|
190
|
+
if (c >= lo && c <= hi)
|
|
191
|
+
return 2;
|
|
192
|
+
if (WIDE_POINTS.has(c))
|
|
193
|
+
return 2;
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
/** Terminal display width of a plain (ANSI-stripped) string. */
|
|
197
|
+
export function dispLen(s) {
|
|
198
|
+
let w = 0;
|
|
199
|
+
for (const ch of s)
|
|
200
|
+
w += charWidth(ch);
|
|
201
|
+
return w;
|
|
202
|
+
}
|
|
143
203
|
export function plainLen(s) {
|
|
144
|
-
return stripAnsi(s)
|
|
204
|
+
return dispLen(stripAnsi(s));
|
|
205
|
+
}
|
|
206
|
+
/** Take as many characters as fit within maxW terminal columns, never cutting a
|
|
207
|
+
* wide glyph in half. */
|
|
208
|
+
function takeVisible(s, maxW) {
|
|
209
|
+
let w = 0;
|
|
210
|
+
let out = "";
|
|
211
|
+
for (const ch of s) {
|
|
212
|
+
const cw = charWidth(ch);
|
|
213
|
+
if (w + cw > maxW)
|
|
214
|
+
break;
|
|
215
|
+
out += ch;
|
|
216
|
+
w += cw;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
145
219
|
}
|
|
146
|
-
// Truncate styled text to a max number of visible
|
|
147
|
-
// ANSI escape sequences intact (never cut mid-sequence).
|
|
220
|
+
// Truncate styled text to a max number of visible terminal columns while
|
|
221
|
+
// keeping ANSI escape sequences intact (never cut mid-sequence).
|
|
148
222
|
export function truncateStyled(text, maxVisible) {
|
|
149
223
|
if (maxVisible <= 0)
|
|
150
224
|
return "";
|
|
@@ -159,11 +233,9 @@ export function truncateStyled(text, maxVisible) {
|
|
|
159
233
|
while ((m = re.exec(text)) !== null) {
|
|
160
234
|
if (m.index > last) {
|
|
161
235
|
const seg = text.slice(last, m.index);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
out += seg.slice(0, room);
|
|
166
|
-
visible += Math.min(seg.length, room);
|
|
236
|
+
const taken = takeVisible(seg, maxVisible - visible);
|
|
237
|
+
out += taken;
|
|
238
|
+
visible += dispLen(taken);
|
|
167
239
|
if (visible >= maxVisible)
|
|
168
240
|
return out;
|
|
169
241
|
}
|
|
@@ -171,7 +243,7 @@ export function truncateStyled(text, maxVisible) {
|
|
|
171
243
|
last = m.index + m[0].length;
|
|
172
244
|
}
|
|
173
245
|
if (visible < maxVisible) {
|
|
174
|
-
out += text.slice(last,
|
|
246
|
+
out += takeVisible(text.slice(last), maxVisible - visible);
|
|
175
247
|
}
|
|
176
248
|
return out;
|
|
177
249
|
}
|
|
@@ -234,11 +306,11 @@ function wrapStyledToWidth(line, width) {
|
|
|
234
306
|
const isSpace = /\s/.test(a.ch);
|
|
235
307
|
if (isSpace) {
|
|
236
308
|
cur.push(a);
|
|
237
|
-
curLen
|
|
309
|
+
curLen += charWidth(a.ch);
|
|
238
310
|
breakAt = cur.length - 1;
|
|
239
311
|
continue;
|
|
240
312
|
}
|
|
241
|
-
if (curLen +
|
|
313
|
+
if (curLen + charWidth(a.ch) > width) {
|
|
242
314
|
if (breakAt >= 0) {
|
|
243
315
|
// Break after the whitespace at breakAt: keep the whitespace's ANSI
|
|
244
316
|
// prefix (often a reset) so style never leaks onto the continuation.
|
|
@@ -260,7 +332,7 @@ function wrapStyledToWidth(line, width) {
|
|
|
260
332
|
}
|
|
261
333
|
}
|
|
262
334
|
cur.push(a);
|
|
263
|
-
curLen
|
|
335
|
+
curLen += charWidth(a.ch);
|
|
264
336
|
}
|
|
265
337
|
if (cur.length)
|
|
266
338
|
rows.push(cur);
|
|
@@ -441,10 +513,20 @@ export function markdownToAnsi(text) {
|
|
|
441
513
|
const badge = codeLang ? ` \x1b[1;36m${codeLang}\x1b[0m ` : "";
|
|
442
514
|
const barLen = Math.max(10, Math.min(width - plainLen(badge) - (codeLang ? 4 : 2), 60));
|
|
443
515
|
const topPrefix = codeLang ? "─" : "";
|
|
516
|
+
// Full width of the framed box = the top border's column span, used to
|
|
517
|
+
// truncate body lines so they never run off the right edge past the border.
|
|
518
|
+
const borderW = 1 + (codeLang ? 1 : 0) + plainLen(badge) + barLen + 1;
|
|
519
|
+
// Body prefix is "│ " (3 columns). Content (incl. a trailing ellipsis)
|
|
520
|
+
// must fit in borderW - 3 so a truncated line ends flush with the border.
|
|
521
|
+
const contentMax = Math.max(borderW - 3, 1);
|
|
444
522
|
out.push(`\x1b[90m╭${topPrefix}${badge}${"─".repeat(barLen)}╮\x1b[0m`);
|
|
445
523
|
for (const cline of codeBuffer) {
|
|
446
524
|
const highlighted = highlightCodeLine(cline, codeLang);
|
|
447
|
-
|
|
525
|
+
// Truncate so a long code line never overflows the box's right edge
|
|
526
|
+
// (borders are only drawn at top/bottom, so a longer line would stick
|
|
527
|
+
// out past the closing ╯).
|
|
528
|
+
const body = plainLen(highlighted) > contentMax ? truncateStyled(highlighted, Math.max(contentMax - 1, 1)) + "…" : highlighted;
|
|
529
|
+
out.push(`\x1b[90m│\x1b[0m ${body}`);
|
|
448
530
|
}
|
|
449
531
|
out.push(`\x1b[90m╰${"─".repeat(barLen + plainLen(badge) + (codeLang ? 1 : 0))}╯\x1b[0m`);
|
|
450
532
|
codeBuffer = [];
|
|
@@ -498,26 +580,29 @@ export function markdownToAnsi(text) {
|
|
|
498
580
|
continue;
|
|
499
581
|
}
|
|
500
582
|
let line = raw;
|
|
501
|
-
// Headers (render as styled headings with the '#' markers stripped)
|
|
583
|
+
// Headers (render as styled headings with the '#' markers stripped). Wrapped
|
|
584
|
+
// so a long heading never runs off the right edge.
|
|
502
585
|
const h1 = line.match(/^#\s+(.+)$/);
|
|
503
586
|
if (h1) {
|
|
504
|
-
out.push(`\x1b[1;36m${formatInlineMarkdown(h1[1])}\x1b[0m
|
|
587
|
+
out.push(...wrapStyledToWidth(`\x1b[1;36m${formatInlineMarkdown(h1[1])}\x1b[0m`, width));
|
|
505
588
|
continue;
|
|
506
589
|
}
|
|
507
590
|
const h2 = line.match(/^##\s+(.+)$/);
|
|
508
591
|
if (h2) {
|
|
509
|
-
out.push(`\x1b[1;36m${formatInlineMarkdown(h2[1])}\x1b[0m
|
|
592
|
+
out.push(...wrapStyledToWidth(`\x1b[1;36m${formatInlineMarkdown(h2[1])}\x1b[0m`, width));
|
|
510
593
|
continue;
|
|
511
594
|
}
|
|
512
595
|
const h3 = line.match(/^###\s+(.+)$/);
|
|
513
596
|
if (h3) {
|
|
514
|
-
out.push(`\x1b[1;37m${formatInlineMarkdown(h3[1])}\x1b[0m
|
|
597
|
+
out.push(...wrapStyledToWidth(`\x1b[1;37m${formatInlineMarkdown(h3[1])}\x1b[0m`, width));
|
|
515
598
|
continue;
|
|
516
599
|
}
|
|
517
|
-
// Blockquote
|
|
600
|
+
// Blockquote. Wrapped inside the "│ " gutter so a long quote never runs off
|
|
601
|
+
// the right edge.
|
|
518
602
|
const bq = line.match(/^>\s*(.+)$/);
|
|
519
603
|
if (bq) {
|
|
520
|
-
|
|
604
|
+
const wrapped = wrapStyledToWidth(`\x1b[3m${formatInlineMarkdown(bq[1])}\x1b[0m`, Math.max(width - 2, 1));
|
|
605
|
+
out.push(...wrapped.map((l) => `\x1b[90m│\x1b[0m ${l}`));
|
|
521
606
|
continue;
|
|
522
607
|
}
|
|
523
608
|
// Unordered List item
|
|
@@ -565,7 +650,7 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
565
650
|
const plainLines = styledLines.map((l) => stripAnsi(l));
|
|
566
651
|
let maxLineW = 0;
|
|
567
652
|
for (const p of plainLines)
|
|
568
|
-
maxLineW = Math.max(maxLineW, p
|
|
653
|
+
maxLineW = Math.max(maxLineW, plainLen(p));
|
|
569
654
|
if (title)
|
|
570
655
|
maxLineW = Math.max(maxLineW, plainLen(title) + 4);
|
|
571
656
|
if (subtitle)
|
|
@@ -599,8 +684,8 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
599
684
|
for (let i = 0; i < styledLines.length; i++) {
|
|
600
685
|
const line = styledLines[i];
|
|
601
686
|
const plain = plainLines[i] ?? "";
|
|
602
|
-
const pad = Math.max(innerW - plain
|
|
603
|
-
const display = plain
|
|
687
|
+
const pad = Math.max(innerW - plainLen(plain), 0);
|
|
688
|
+
const display = plainLen(plain) > innerW ? truncateStyled(line, innerW) : line;
|
|
604
689
|
out.push(`\x1b[${borderStyle}m│\x1b[0m ${display}${" ".repeat(pad)} \x1b[${borderStyle}m│\x1b[0m`);
|
|
605
690
|
}
|
|
606
691
|
const bottom = `╰${embed(subtitle, "center")}╯`;
|
|
@@ -715,7 +800,7 @@ export function displayRows(text) {
|
|
|
715
800
|
if (!lines.length)
|
|
716
801
|
return 1;
|
|
717
802
|
for (const line of lines) {
|
|
718
|
-
rows += Math.max(1, Math.ceil(line
|
|
803
|
+
rows += Math.max(1, Math.ceil(dispLen(line) / width));
|
|
719
804
|
}
|
|
720
805
|
return rows;
|
|
721
806
|
}
|
|
@@ -1084,12 +1169,12 @@ export class Spinner {
|
|
|
1084
1169
|
const first = parts[0];
|
|
1085
1170
|
const rest = parts.slice(1);
|
|
1086
1171
|
const plain = stripAnsi(first);
|
|
1087
|
-
const pad = Math.max(0, this.lastLen - plain
|
|
1172
|
+
const pad = Math.max(0, this.lastLen - plainLen(plain));
|
|
1088
1173
|
process.stdout.write("\r" + first + " ".repeat(pad) + "\r\n");
|
|
1089
1174
|
if (rest.length)
|
|
1090
1175
|
process.stdout.write(rest.join("\n") + "\n");
|
|
1091
1176
|
this.rows = 0;
|
|
1092
|
-
this.lastLen = plain
|
|
1177
|
+
this.lastLen = plainLen(plain);
|
|
1093
1178
|
}
|
|
1094
1179
|
update(text) {
|
|
1095
1180
|
if (this.timer) {
|
|
@@ -1113,9 +1198,9 @@ export class Spinner {
|
|
|
1113
1198
|
// cells (spinner frame, trailing timer) actually change on screen. Pad
|
|
1114
1199
|
// with spaces so a shorter line clears any previously longer text.
|
|
1115
1200
|
const plain = stripAnsi(rendered);
|
|
1116
|
-
const pad = Math.max(0, this.lastLen - plain
|
|
1201
|
+
const pad = Math.max(0, this.lastLen - plainLen(plain));
|
|
1117
1202
|
process.stdout.write("\r" + rendered + " ".repeat(pad) + "\r");
|
|
1118
|
-
this.lastLen = plain
|
|
1203
|
+
this.lastLen = plainLen(plain);
|
|
1119
1204
|
}
|
|
1120
1205
|
else {
|
|
1121
1206
|
// Multi-row wrapped text: fall back to an erase-based redraw.
|
|
@@ -1235,7 +1320,7 @@ export function formatToolResult(rawResult, failed) {
|
|
|
1235
1320
|
const allLines = clean.split("\n");
|
|
1236
1321
|
const lines = allLines
|
|
1237
1322
|
.slice(0, TOOL_RESULT_MAX_LINES)
|
|
1238
|
-
.map((ln) => (ln
|
|
1323
|
+
.map((ln) => (plainLen(ln) > lineMax ? truncateStyled(ln, lineMax) + "…" : ln));
|
|
1239
1324
|
if (allLines.length > TOOL_RESULT_MAX_LINES)
|
|
1240
1325
|
lines.push("…");
|
|
1241
1326
|
const styleOpen = failed ? "\x1b[31m" : "\x1b[90m";
|
|
@@ -1379,7 +1464,7 @@ function wrapRuns(runs, width) {
|
|
|
1379
1464
|
caretCol = lineLen;
|
|
1380
1465
|
}
|
|
1381
1466
|
line.push(u);
|
|
1382
|
-
lineLen +=
|
|
1467
|
+
lineLen += charWidth(u.ch);
|
|
1383
1468
|
};
|
|
1384
1469
|
let i = 0;
|
|
1385
1470
|
while (i < units.length) {
|
|
@@ -1398,7 +1483,8 @@ function wrapRuns(runs, width) {
|
|
|
1398
1483
|
continue;
|
|
1399
1484
|
}
|
|
1400
1485
|
if (isWs(u)) {
|
|
1401
|
-
|
|
1486
|
+
const w = charWidth(u.ch);
|
|
1487
|
+
if (lineLen + w > width)
|
|
1402
1488
|
flushLine();
|
|
1403
1489
|
// Never start a line with whitespace: if the space lands at the start of
|
|
1404
1490
|
// a fresh line, drop it (it was the fill of the previous full line).
|
|
@@ -1413,28 +1499,38 @@ function wrapRuns(runs, width) {
|
|
|
1413
1499
|
// Start of a word (which may contain the caret). Find its full extent in
|
|
1414
1500
|
// the flattened stream so we never wrap mid-word.
|
|
1415
1501
|
let j = i;
|
|
1416
|
-
let
|
|
1502
|
+
let wordW = 0; // display width of the whole word
|
|
1417
1503
|
while (j < units.length && !isWs(units[j])) {
|
|
1418
|
-
|
|
1504
|
+
wordW += charWidth(units[j].ch);
|
|
1419
1505
|
j++;
|
|
1420
1506
|
}
|
|
1421
|
-
if (
|
|
1507
|
+
if (wordW === 0) {
|
|
1422
1508
|
i++;
|
|
1423
1509
|
continue;
|
|
1424
1510
|
}
|
|
1425
1511
|
if (lineLen === 0) {
|
|
1426
|
-
if (
|
|
1512
|
+
if (wordW > width) {
|
|
1427
1513
|
// Overlong word: split into width-sized chunks, keeping the caret in
|
|
1428
1514
|
// the chunk that contains it.
|
|
1429
1515
|
let from = i;
|
|
1430
|
-
let
|
|
1431
|
-
while (
|
|
1432
|
-
|
|
1516
|
+
let used = 0;
|
|
1517
|
+
while (used < wordW) {
|
|
1518
|
+
let n = 0;
|
|
1519
|
+
let chunkW = 0;
|
|
1520
|
+
while (from + n < j && chunkW + charWidth(units[from + n].ch) <= width) {
|
|
1521
|
+
chunkW += charWidth(units[from + n].ch);
|
|
1522
|
+
n++;
|
|
1523
|
+
}
|
|
1524
|
+
if (n === 0) {
|
|
1525
|
+
// A single glyph is wider than the line; emit it alone.
|
|
1526
|
+
n = 1;
|
|
1527
|
+
chunkW = charWidth(units[from].ch);
|
|
1528
|
+
}
|
|
1433
1529
|
for (let k = 0; k < n; k++)
|
|
1434
1530
|
addUnit(units[from + k]);
|
|
1435
1531
|
flushLine();
|
|
1436
1532
|
from += n;
|
|
1437
|
-
|
|
1533
|
+
used += chunkW;
|
|
1438
1534
|
}
|
|
1439
1535
|
}
|
|
1440
1536
|
else {
|
|
@@ -1444,7 +1540,7 @@ function wrapRuns(runs, width) {
|
|
|
1444
1540
|
i = j;
|
|
1445
1541
|
}
|
|
1446
1542
|
else {
|
|
1447
|
-
if (lineLen +
|
|
1543
|
+
if (lineLen + wordW > width) {
|
|
1448
1544
|
// Word doesn't fit on the current line; move the whole word to a new
|
|
1449
1545
|
// line (leaving any trailing space from the previous word behind).
|
|
1450
1546
|
flushLine();
|
|
@@ -1565,7 +1661,7 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
1565
1661
|
const body = [];
|
|
1566
1662
|
for (const l of rows) {
|
|
1567
1663
|
const plain = stripAnsi(l);
|
|
1568
|
-
const pad = Math.max(0, innerW - plain
|
|
1664
|
+
const pad = Math.max(0, innerW - plainLen(plain));
|
|
1569
1665
|
body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
|
|
1570
1666
|
}
|
|
1571
1667
|
const bottom = `\x1b[90m╰${"─".repeat(Math.max(0, boxW - 2))}╯\x1b[0m`;
|