@oxecli/oxe 1.0.106 → 1.0.108
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/cli.js +2 -2
- package/dist/oxe.js +10 -2
- package/dist/ui.js +172 -31
- 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,
|
|
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,
|
|
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/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
|
|
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
|
-
|
|
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
|
@@ -76,8 +76,11 @@ export function startDraftCapture(onInterrupt, renderBox) {
|
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
if (str && !key?.ctrl && !key?.meta) {
|
|
79
|
-
|
|
80
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +173,81 @@ export function truncateStyled(text, maxVisible) {
|
|
|
155
173
|
}
|
|
156
174
|
return out;
|
|
157
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 out = [];
|
|
202
|
+
let cur = "";
|
|
203
|
+
let curLen = 0;
|
|
204
|
+
let word = [];
|
|
205
|
+
const emitChunk = (chunk, prependSpace) => {
|
|
206
|
+
const add = (prependSpace && curLen > 0 ? 1 : 0) + chunk.length;
|
|
207
|
+
if (curLen + add > width)
|
|
208
|
+
return false;
|
|
209
|
+
if (prependSpace && curLen > 0) {
|
|
210
|
+
cur += " ";
|
|
211
|
+
curLen++;
|
|
212
|
+
}
|
|
213
|
+
for (const u of chunk)
|
|
214
|
+
cur += u.pre + u.ch;
|
|
215
|
+
curLen += chunk.length;
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
218
|
+
const flushWord = () => {
|
|
219
|
+
if (!word.length)
|
|
220
|
+
return;
|
|
221
|
+
const wlen = word.length;
|
|
222
|
+
// Split overlong words into <= width chunks so nothing is ever cut off.
|
|
223
|
+
const chunks = [];
|
|
224
|
+
if (wlen <= width)
|
|
225
|
+
chunks.push(word);
|
|
226
|
+
else
|
|
227
|
+
for (let i = 0; i < wlen; i += width)
|
|
228
|
+
chunks.push(word.slice(i, i + width));
|
|
229
|
+
for (let ci = 0; ci < chunks.length; ci++) {
|
|
230
|
+
const chunk = chunks[ci];
|
|
231
|
+
if (!emitChunk(chunk, ci === 0)) {
|
|
232
|
+
out.push(cur);
|
|
233
|
+
cur = "";
|
|
234
|
+
curLen = 0;
|
|
235
|
+
emitChunk(chunk, false);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
word = [];
|
|
239
|
+
};
|
|
240
|
+
for (const u of units) {
|
|
241
|
+
if (/\s/.test(u.ch))
|
|
242
|
+
flushWord();
|
|
243
|
+
else
|
|
244
|
+
word.push(u);
|
|
245
|
+
}
|
|
246
|
+
flushWord();
|
|
247
|
+
if (cur)
|
|
248
|
+
out.push(cur);
|
|
249
|
+
return out.length ? out : [line];
|
|
250
|
+
}
|
|
158
251
|
// ---------------------------------------------------------------------------
|
|
159
252
|
// Markup tag -> ANSI
|
|
160
253
|
// ---------------------------------------------------------------------------
|
|
@@ -290,7 +383,7 @@ function formatMarkdownTable(tableLines) {
|
|
|
290
383
|
// Rich Markdown -> ANSI Terminal Renderer
|
|
291
384
|
// ---------------------------------------------------------------------------
|
|
292
385
|
export function markdownToAnsi(text) {
|
|
293
|
-
const width = Math.max(terminalWidth() - 2,
|
|
386
|
+
const width = Math.max(terminalWidth() - 2, 1);
|
|
294
387
|
const rawLines = text.split("\n");
|
|
295
388
|
const out = [];
|
|
296
389
|
let inCode = false;
|
|
@@ -394,22 +487,11 @@ export function markdownToAnsi(text) {
|
|
|
394
487
|
if (!line.includes("\x1b[36m•\x1b[0m") && !line.includes(".\x1b[0m ")) {
|
|
395
488
|
line = formatInlineMarkdown(line);
|
|
396
489
|
}
|
|
397
|
-
// Word wrap paragraph line if too long
|
|
490
|
+
// Word wrap paragraph line if too long. Whole words move to a new row; any
|
|
491
|
+
// single word longer than the width is split so nothing is ever cut off at
|
|
492
|
+
// the right edge.
|
|
398
493
|
if (plainLen(line) > width && !line.startsWith(" ")) {
|
|
399
|
-
|
|
400
|
-
let cur = "";
|
|
401
|
-
for (const w of words) {
|
|
402
|
-
if (plainLen(cur) + plainLen(w) + 1 > width) {
|
|
403
|
-
if (cur)
|
|
404
|
-
out.push(cur);
|
|
405
|
-
cur = w;
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
cur = cur ? `${cur} ${w}` : w;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (cur)
|
|
412
|
-
out.push(cur);
|
|
494
|
+
out.push(...wrapStyledToWidth(line, width));
|
|
413
495
|
}
|
|
414
496
|
else {
|
|
415
497
|
out.push(line);
|
|
@@ -445,8 +527,8 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
445
527
|
maxLineW = Math.max(maxLineW, plainLen(subtitle) + 4);
|
|
446
528
|
const termW = terminalWidth();
|
|
447
529
|
const boxW = expand
|
|
448
|
-
?
|
|
449
|
-
:
|
|
530
|
+
? fitBoxWidth(termW - 4, termW)
|
|
531
|
+
: fitBoxWidth(maxLineW + 4, termW);
|
|
450
532
|
const innerW = Math.max(boxW - 4, 1);
|
|
451
533
|
const embed = (text, align) => {
|
|
452
534
|
if (!text)
|
|
@@ -1072,7 +1154,7 @@ export function toolCallLabel(name, argumentsJson) {
|
|
|
1072
1154
|
arg = arg.trim().replace(/\s*\r?\n\s*/g, " ");
|
|
1073
1155
|
if (!arg)
|
|
1074
1156
|
return `\x1b[1;36m${display}\x1b[0m`;
|
|
1075
|
-
const cap = Math.max(Math.min(terminalWidth() - plainLen(display) - 4, MAX_COMMAND_DISPLAY_CHARS),
|
|
1157
|
+
const cap = Math.max(Math.min(terminalWidth() - plainLen(display) - 4, MAX_COMMAND_DISPLAY_CHARS), Math.min(terminalWidth() - plainLen(display) - 2, 8));
|
|
1076
1158
|
if (plainLen(arg) > cap)
|
|
1077
1159
|
arg = truncateStyled(arg, cap) + "…";
|
|
1078
1160
|
return `\x1b[1;36m${display}\x1b[0m(${arg})`;
|
|
@@ -1097,7 +1179,7 @@ export function formatToolResult(rawResult, failed) {
|
|
|
1097
1179
|
// Truncate to the terminal width (rows OR per-line length, whichever trips
|
|
1098
1180
|
// first) so long tool output — e.g. Glob paths — never runs off the screen.
|
|
1099
1181
|
const termW = terminalWidth();
|
|
1100
|
-
const lineMax = Math.max(termW - 4,
|
|
1182
|
+
const lineMax = Math.max(termW - 4, 1);
|
|
1101
1183
|
const allLines = clean.split("\n");
|
|
1102
1184
|
const lines = allLines
|
|
1103
1185
|
.slice(0, TOOL_RESULT_MAX_LINES)
|
|
@@ -1116,10 +1198,32 @@ export function formatToolResult(rawResult, failed) {
|
|
|
1116
1198
|
export const PROMPT_PLACEHOLDER = "Describe a coding task, or type /help for commands";
|
|
1117
1199
|
const MAX_PROMPT_DISPLAY_LINES = 12;
|
|
1118
1200
|
const MAX_PROMPT_PASTE_CHARS = 400;
|
|
1201
|
+
const MAX_PROMPT_CHARS = 500;
|
|
1119
1202
|
function shouldCollapsePaste(text) {
|
|
1120
1203
|
return (text.split("\n").length > MAX_PROMPT_DISPLAY_LINES ||
|
|
1121
1204
|
text.length > MAX_PROMPT_PASTE_CHARS);
|
|
1122
1205
|
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Effective character count of a prompt for its length limit. Each non-collapsed
|
|
1208
|
+
* char counts as one; a collapsed pasted block counts as a single char.
|
|
1209
|
+
*/
|
|
1210
|
+
export function promptCharCount(buffer, pasteSpans) {
|
|
1211
|
+
if (!buffer)
|
|
1212
|
+
return 0;
|
|
1213
|
+
const spans = [...pasteSpans].sort((a, b) => a[0] - b[0]);
|
|
1214
|
+
let count = 0;
|
|
1215
|
+
let last = 0;
|
|
1216
|
+
for (const [s, e] of spans) {
|
|
1217
|
+
if (s > last)
|
|
1218
|
+
count += s - last;
|
|
1219
|
+
const seg = buffer.slice(s, e);
|
|
1220
|
+
count += shouldCollapsePaste(seg) ? 1 : e - s;
|
|
1221
|
+
last = e;
|
|
1222
|
+
}
|
|
1223
|
+
if (last < buffer.length)
|
|
1224
|
+
count += buffer.length - last;
|
|
1225
|
+
return count;
|
|
1226
|
+
}
|
|
1123
1227
|
export function cursorLineCol(buffer, cursor) {
|
|
1124
1228
|
const before = buffer.slice(0, cursor);
|
|
1125
1229
|
const lines = before.split("\n");
|
|
@@ -1238,7 +1342,7 @@ function wrapRuns(runs, width) {
|
|
|
1238
1342
|
}
|
|
1239
1343
|
export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints) {
|
|
1240
1344
|
const termW = terminalWidth();
|
|
1241
|
-
const boxW =
|
|
1345
|
+
const boxW = fitBoxWidth(termW - 4, termW);
|
|
1242
1346
|
const innerW = boxW - 4;
|
|
1243
1347
|
const runs = [];
|
|
1244
1348
|
runs.push({ text: prefix, style: "\x1b[90m" });
|
|
@@ -1337,11 +1441,18 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
1337
1441
|
const lines = [top, ...body, bottom];
|
|
1338
1442
|
let totalRows = body.length + 2;
|
|
1339
1443
|
if (hints && (hints.left || hints.right)) {
|
|
1340
|
-
|
|
1444
|
+
let left = hints.left ?? "";
|
|
1341
1445
|
const right = hints.right ?? "";
|
|
1342
1446
|
// Inset the hint row inside the box (box spans columns 0..boxW-1): a
|
|
1343
1447
|
// leading space gives left-edge padding, and the right hint ends one
|
|
1344
|
-
// column before the box's right border for the same padding.
|
|
1448
|
+
// column before the box's right border for the same padding. Truncate the
|
|
1449
|
+
// left hint so the row never exceeds the box width on narrow terminals
|
|
1450
|
+
// (otherwise it overflows past the right border instead of scaling down).
|
|
1451
|
+
const hintMax = boxW - 2;
|
|
1452
|
+
if (plainLen(left) + plainLen(right) + 1 > hintMax) {
|
|
1453
|
+
const room = Math.max(0, hintMax - plainLen(right) - 1);
|
|
1454
|
+
left = truncateStyled(left, room);
|
|
1455
|
+
}
|
|
1345
1456
|
const gap = Math.max(1, boxW - 2 - plainLen(left) - plainLen(right));
|
|
1346
1457
|
lines.push(" " + left + " ".repeat(gap) + right);
|
|
1347
1458
|
totalRows += 1;
|
|
@@ -1464,6 +1575,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1464
1575
|
dockEnterDocked();
|
|
1465
1576
|
}
|
|
1466
1577
|
process.stdin.removeListener("keypress", onKeypress);
|
|
1578
|
+
process.stdout.removeListener("resize", onResize);
|
|
1467
1579
|
process.stdin.pause();
|
|
1468
1580
|
showCursor();
|
|
1469
1581
|
if (isErr)
|
|
@@ -1536,14 +1648,32 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1536
1648
|
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
1537
1649
|
histIdx = hist.length;
|
|
1538
1650
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1651
|
+
// Non-collapsing pastes get a leading space when inserted after non-empty,
|
|
1652
|
+
// non-whitespace text (never when a space is already present). The space is
|
|
1653
|
+
// inserted as plain text (outside the paste span) so display spacing isn't
|
|
1654
|
+
// doubled.
|
|
1655
|
+
let prefix = "";
|
|
1656
|
+
if (isPaste &&
|
|
1657
|
+
!shouldCollapsePaste(text) &&
|
|
1658
|
+
cursor > 0 &&
|
|
1659
|
+
!endsWithWs(buffer.slice(0, cursor))) {
|
|
1660
|
+
prefix = " ";
|
|
1661
|
+
}
|
|
1662
|
+
const pasteStart = cursor + prefix.length;
|
|
1663
|
+
const insertLen = prefix.length + text.length;
|
|
1664
|
+
const newBuffer = buffer.slice(0, cursor) + prefix + text + buffer.slice(cursor);
|
|
1665
|
+
const newSpans = shiftSpansAfterInsert(cursor, insertLen);
|
|
1542
1666
|
if (isPaste) {
|
|
1543
|
-
|
|
1544
|
-
|
|
1667
|
+
newSpans.push([pasteStart, pasteStart + text.length]);
|
|
1668
|
+
newSpans.sort((a, b) => a[0] - b[0]);
|
|
1545
1669
|
}
|
|
1546
|
-
|
|
1670
|
+
// Enforce the 500-char effective limit; a collapsed block counts as one.
|
|
1671
|
+
if (promptCharCount(newBuffer, newSpans) > MAX_PROMPT_CHARS) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
buffer = newBuffer;
|
|
1675
|
+
pasteSpans = newSpans;
|
|
1676
|
+
cursor += insertLen;
|
|
1547
1677
|
};
|
|
1548
1678
|
const spanCollapsed = (span) => {
|
|
1549
1679
|
return shouldCollapsePaste(buffer.slice(span[0], span[1]));
|
|
@@ -1887,6 +2017,17 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1887
2017
|
}
|
|
1888
2018
|
};
|
|
1889
2019
|
process.stdin.on("keypress", onKeypress);
|
|
2020
|
+
// Reflow the idle prompt box when the terminal resizes so it scales down
|
|
2021
|
+
// instead of wrapping/overflowing its borders on a narrow/scaled terminal.
|
|
2022
|
+
const onResize = () => {
|
|
2023
|
+
try {
|
|
2024
|
+
repaint(false);
|
|
2025
|
+
}
|
|
2026
|
+
catch {
|
|
2027
|
+
/* ignore */
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
process.stdout.on("resize", onResize);
|
|
1890
2031
|
repaint(true);
|
|
1891
2032
|
});
|
|
1892
2033
|
}
|