@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 +3 -3
- package/dist/oxe.js +10 -2
- package/dist/ui.js +114 -21
- package/package.json +1 -1
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
|
-
// "╰─
|
|
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
|
|
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 "╰─
|
|
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
|
|
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
|
@@ -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 =
|
|
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
|
-
|
|
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 +
|
|
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` +
|
|
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
|
-
|
|
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
|
}
|