@oxecli/oxe 1.0.90 → 1.0.91
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 +21 -42
- package/dist/ui.js +64 -70
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { loadOrPrompt, default_reasoning_effort, max_action_chars, max_resume_history_items, runtimeOsSummary, max_context_tokens, context_overhead_margin, } from "./config.js";
|
|
5
5
|
import { InferenceEngine, estimateTokens } from "./engine.js";
|
|
6
6
|
import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
|
|
7
|
-
import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, plainLen, truncateStyled, terminalWidth, TOOL_RESULT_MAX_LINES, MAX_COMMAND_DISPLAY_CHARS, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, dockAppendContent, dockSetInactive, dockTearDown, } from "./ui.js";
|
|
7
|
+
import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, plainLen, truncateStyled, terminalWidth, TOOL_RESULT_MAX_LINES, MAX_COMMAND_DISPLAY_CHARS, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, dockAppendContent, dockSetInactive, dockTearDown, startDraftCapture, } from "./ui.js";
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const packageInfo = require("../package.json");
|
|
@@ -396,50 +396,14 @@ export class CLI {
|
|
|
396
396
|
const used = estimateTokens(this.inputItems);
|
|
397
397
|
const budget = max_context_tokens - context_overhead_margin;
|
|
398
398
|
const pct = Math.max(0, Math.min(100, Math.round(((budget - used) / budget) * 100)));
|
|
399
|
-
// Queries run
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
this.interruptPending = false;
|
|
404
|
-
this.promptHistory.push(input);
|
|
405
|
-
this.lastPrompt = input;
|
|
406
|
-
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
407
|
-
queryStarted = true;
|
|
408
|
-
try {
|
|
409
|
-
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
410
|
-
}
|
|
411
|
-
catch (qerr) {
|
|
412
|
-
if (qerr?.message === "interrupt") {
|
|
413
|
-
if (!engine.queryHasOutput && !engine.queryCalledTool) {
|
|
414
|
-
dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
|
|
415
|
-
}
|
|
416
|
-
engine.inQuery = false;
|
|
417
|
-
stripOrphanCalls(this.inputItems);
|
|
418
|
-
if (this.inputItems.length &&
|
|
419
|
-
this.inputItems[this.inputItems.length - 1]["role"] === "user") {
|
|
420
|
-
this.inputItems.pop();
|
|
421
|
-
}
|
|
422
|
-
if (this.story.length &&
|
|
423
|
-
this.story[this.story.length - 1]["type"] === "user") {
|
|
424
|
-
this.story.pop();
|
|
425
|
-
}
|
|
426
|
-
this.interruptPending = true;
|
|
427
|
-
dockAppendContent("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
|
|
428
|
-
}
|
|
429
|
-
else if (qerr?.message !== "eof") {
|
|
430
|
-
throw qerr;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
finally {
|
|
434
|
-
queryStarted = false;
|
|
435
|
-
}
|
|
436
|
-
this.saveSession(engine);
|
|
437
|
-
};
|
|
438
|
-
const onInterrupt = () => engine.interrupt();
|
|
399
|
+
// Queries run the proven settle-on-Enter flow: the prompt settles and
|
|
400
|
+
// the query streams above it (robust dock streaming). While the query
|
|
401
|
+
// runs, typed keys are captured into a draft; the draft (or the sent
|
|
402
|
+
// prompt) is restored into the next prompt box so input is never lost.
|
|
439
403
|
const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory, {
|
|
440
404
|
left: "\x1b[90mPress \x1b[1;36mCtrl+C\x1b[0m\x1b[90m to interrupt\x1b[0m",
|
|
441
405
|
right: `\x1b[1;36m${pct}%\x1b[0m \x1b[90muntil auto-compact\x1b[0m`,
|
|
442
|
-
}, this.lastPrompt
|
|
406
|
+
}, this.lastPrompt);
|
|
443
407
|
if (!input.trim())
|
|
444
408
|
continue;
|
|
445
409
|
this.interruptPending = false;
|
|
@@ -461,6 +425,7 @@ export class CLI {
|
|
|
461
425
|
this.inputItems = [];
|
|
462
426
|
this.story = [];
|
|
463
427
|
this.sessionId = null;
|
|
428
|
+
this.lastPrompt = "";
|
|
464
429
|
clearScreen();
|
|
465
430
|
dockSetInactive();
|
|
466
431
|
this.renderHeader();
|
|
@@ -491,6 +456,20 @@ export class CLI {
|
|
|
491
456
|
}
|
|
492
457
|
continue;
|
|
493
458
|
}
|
|
459
|
+
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
460
|
+
this.lastPrompt = input;
|
|
461
|
+
queryStarted = true;
|
|
462
|
+
const stopCapture = startDraftCapture(() => engine.interrupt());
|
|
463
|
+
try {
|
|
464
|
+
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
const captured = stopCapture();
|
|
468
|
+
if (captured.trim())
|
|
469
|
+
this.lastPrompt = captured;
|
|
470
|
+
queryStarted = false;
|
|
471
|
+
}
|
|
472
|
+
this.saveSession(engine);
|
|
494
473
|
}
|
|
495
474
|
catch (err) {
|
|
496
475
|
if (err?.message === "eof") {
|
package/dist/ui.js
CHANGED
|
@@ -38,6 +38,52 @@ export function waitRawKey() {
|
|
|
38
38
|
process.stdin.on("keypress", onKeypress);
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Capture typed input while a query runs (the prompt box is torn down during
|
|
43
|
+
* streaming). Keystrokes are accumulated into a draft that the next prompt box
|
|
44
|
+
* is pre-filled with, so nothing the user types while the AI is busy is lost.
|
|
45
|
+
* Returns a stop() function returning the captured draft (may be empty).
|
|
46
|
+
*/
|
|
47
|
+
export function startDraftCapture(onInterrupt) {
|
|
48
|
+
let draft = "";
|
|
49
|
+
let active = true;
|
|
50
|
+
const onKey = (str, key) => {
|
|
51
|
+
if (!active)
|
|
52
|
+
return;
|
|
53
|
+
if (key && key.ctrl && key.name === "c") {
|
|
54
|
+
onInterrupt();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key && key.ctrl && (key.name === "d" || key.name === "z"))
|
|
58
|
+
return;
|
|
59
|
+
if (key && key.name === "backspace") {
|
|
60
|
+
draft = draft.slice(0, -1);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key && (key.name === "return" || key.name === "enter"))
|
|
64
|
+
return;
|
|
65
|
+
if (str && !key?.ctrl && !key?.meta) {
|
|
66
|
+
draft += str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
readline.emitKeypressEvents(process.stdin);
|
|
70
|
+
if (process.stdin.isTTY)
|
|
71
|
+
process.stdin.setRawMode(true);
|
|
72
|
+
process.stdin.resume();
|
|
73
|
+
process.stdin.on("keypress", onKey);
|
|
74
|
+
return () => {
|
|
75
|
+
active = false;
|
|
76
|
+
process.stdin.removeListener("keypress", onKey);
|
|
77
|
+
try {
|
|
78
|
+
process.stdin.setRawMode(false);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* ignore */
|
|
82
|
+
}
|
|
83
|
+
process.stdin.pause();
|
|
84
|
+
return draft;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
41
87
|
// ---------------------------------------------------------------------------
|
|
42
88
|
// Terminal helpers
|
|
43
89
|
// ---------------------------------------------------------------------------
|
|
@@ -1163,7 +1209,7 @@ function isEnterKey(str, key) {
|
|
|
1163
1209
|
seq === "\x1bOM" // application keypad
|
|
1164
1210
|
);
|
|
1165
1211
|
}
|
|
1166
|
-
export function askBottomPrompt(label = "You", prefix = "❯", history = [], hints, initialBuffer = ""
|
|
1212
|
+
export function askBottomPrompt(label = "You", prefix = "❯", history = [], hints, initialBuffer = "") {
|
|
1167
1213
|
return new Promise((resolve, reject) => {
|
|
1168
1214
|
const isTTY = process.stdin.isTTY;
|
|
1169
1215
|
if (!isTTY) {
|
|
@@ -1192,7 +1238,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1192
1238
|
readline.emitKeypressEvents(process.stdin);
|
|
1193
1239
|
if (process.stdin.isTTY) {
|
|
1194
1240
|
process.stdin.setRawMode(true);
|
|
1195
|
-
// Bracketed paste so multi-line pastes arrive inside \x1b[200~…\x1b[201~.
|
|
1196
1241
|
process.stdout.write("\x1b[?2004h");
|
|
1197
1242
|
}
|
|
1198
1243
|
process.stdin.resume();
|
|
@@ -1201,9 +1246,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1201
1246
|
let lastCursorRow = 0;
|
|
1202
1247
|
let lastTotalRows = 0;
|
|
1203
1248
|
let prevFrameLines = null;
|
|
1204
|
-
// True while an onSubmit query is running. While it runs the box stays
|
|
1205
|
-
// focused and editable; Enter won't re-submit and Ctrl+C interrupts it.
|
|
1206
|
-
let submitting = false;
|
|
1207
1249
|
const clearBox = () => {
|
|
1208
1250
|
if (lastCursorRow > 0)
|
|
1209
1251
|
process.stdout.write(`\x1b[${lastCursorRow}A`);
|
|
@@ -1259,21 +1301,15 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1259
1301
|
process.stdout.write("\n");
|
|
1260
1302
|
}
|
|
1261
1303
|
prevFrameLines = newLines;
|
|
1262
|
-
lastCursorRow =
|
|
1304
|
+
lastCursorRow = cursorRow;
|
|
1263
1305
|
lastTotalRows = totalRows;
|
|
1264
1306
|
dockSetFrame(frame, totalRows);
|
|
1265
1307
|
dockEnterDocked();
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
const up = totalRows - 1;
|
|
1269
|
-
process.stdout.write(`\x1b[${up}A\r`);
|
|
1308
|
+
const up = totalRows - 1 - cursorRow;
|
|
1309
|
+
process.stdout.write(`\x1b[${up}A\r\x1b[${cursorCol}C`);
|
|
1270
1310
|
}
|
|
1271
1311
|
else {
|
|
1272
|
-
|
|
1273
|
-
// terminals treat as "cursor up 1"): start from column 0, redraw only
|
|
1274
|
-
// the changed lines, then return the cursor to the box top so dock
|
|
1275
|
-
// content (AI streaming, tool results) keeps inserting above the box.
|
|
1276
|
-
process.stdout.write("\r");
|
|
1312
|
+
process.stdout.write(`\x1b[${lastCursorRow}A\r`);
|
|
1277
1313
|
const oldLines = prevFrameLines ?? [];
|
|
1278
1314
|
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
1279
1315
|
for (let i = 0; i < maxLen; i++) {
|
|
@@ -1283,11 +1319,14 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1283
1319
|
if (i < maxLen - 1)
|
|
1284
1320
|
process.stdout.write("\n");
|
|
1285
1321
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1322
|
+
const atLine = maxLen - 1;
|
|
1323
|
+
if (atLine > cursorRow)
|
|
1324
|
+
process.stdout.write(`\x1b[${atLine - cursorRow}A`);
|
|
1325
|
+
else if (atLine < cursorRow)
|
|
1326
|
+
process.stdout.write(`\x1b[${cursorRow - atLine}B`);
|
|
1327
|
+
process.stdout.write(`\r\x1b[${cursorCol}C`);
|
|
1289
1328
|
prevFrameLines = newLines;
|
|
1290
|
-
lastCursorRow =
|
|
1329
|
+
lastCursorRow = cursorRow;
|
|
1291
1330
|
lastTotalRows = totalRows;
|
|
1292
1331
|
dockSetFrame(frame, totalRows);
|
|
1293
1332
|
}
|
|
@@ -1320,8 +1359,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1320
1359
|
}
|
|
1321
1360
|
cursor += text.length;
|
|
1322
1361
|
};
|
|
1323
|
-
// A paste span is treated as a single atomic "block" only when it collapses
|
|
1324
|
-
// to a marker (large pastes). Small inline pastes behave like normal text.
|
|
1325
1362
|
const spanCollapsed = (span) => {
|
|
1326
1363
|
return shouldCollapsePaste(buffer.slice(span[0], span[1]));
|
|
1327
1364
|
};
|
|
@@ -1498,11 +1535,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1498
1535
|
if (!bracketedPaste && (sequence === start || value === start)) {
|
|
1499
1536
|
bracketedPaste = true;
|
|
1500
1537
|
bracketedPasteBuffer = "";
|
|
1501
|
-
const inline = value === start
|
|
1502
|
-
? ""
|
|
1503
|
-
: value.startsWith(start)
|
|
1504
|
-
? value.slice(start.length)
|
|
1505
|
-
: "";
|
|
1538
|
+
const inline = value === start ? "" : value.startsWith(start) ? value.slice(start.length) : "";
|
|
1506
1539
|
if (inline)
|
|
1507
1540
|
bracketedPasteBuffer = inline;
|
|
1508
1541
|
if (inline.includes(end)) {
|
|
@@ -1522,9 +1555,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1522
1555
|
if (!bracketedPaste)
|
|
1523
1556
|
return false;
|
|
1524
1557
|
if (sequence === end || value === end) {
|
|
1525
|
-
const pasted = bracketedPasteBuffer
|
|
1526
|
-
.replace(/\r\n/g, "\n")
|
|
1527
|
-
.replace(/\r/g, "\n");
|
|
1558
|
+
const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1528
1559
|
bracketedPaste = false;
|
|
1529
1560
|
bracketedPasteBuffer = "";
|
|
1530
1561
|
if (pasted)
|
|
@@ -1532,15 +1563,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1532
1563
|
repaint();
|
|
1533
1564
|
return true;
|
|
1534
1565
|
}
|
|
1535
|
-
const chunk = value ||
|
|
1536
|
-
(sequence && !sequence.startsWith("\x1b[") ? sequence : "");
|
|
1566
|
+
const chunk = value || (sequence && !sequence.startsWith("\x1b[") ? sequence : "");
|
|
1537
1567
|
if (chunk) {
|
|
1538
1568
|
const endAt = chunk.indexOf(end);
|
|
1539
1569
|
if (endAt >= 0) {
|
|
1540
1570
|
bracketedPasteBuffer += chunk.slice(0, endAt);
|
|
1541
|
-
const pasted = bracketedPasteBuffer
|
|
1542
|
-
.replace(/\r\n/g, "\n")
|
|
1543
|
-
.replace(/\r/g, "\n");
|
|
1571
|
+
const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1544
1572
|
bracketedPaste = false;
|
|
1545
1573
|
bracketedPasteBuffer = "";
|
|
1546
1574
|
if (pasted)
|
|
@@ -1572,12 +1600,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1572
1600
|
pasteBurstTimer = null;
|
|
1573
1601
|
}
|
|
1574
1602
|
if (key && key.ctrl && key.name === "c") {
|
|
1575
|
-
// While a query is running, Ctrl+C interrupts the query instead of
|
|
1576
|
-
// leaving the prompt (the box stays focused for editing).
|
|
1577
|
-
if (submitting && onInterrupt) {
|
|
1578
|
-
onInterrupt();
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
1603
|
settle(new Error("interrupt"), true);
|
|
1582
1604
|
return;
|
|
1583
1605
|
}
|
|
@@ -1618,31 +1640,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1618
1640
|
return;
|
|
1619
1641
|
}
|
|
1620
1642
|
if (isEnterKey(str, key)) {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
return;
|
|
1624
|
-
// While a query is running the box stays focused and editable; Enter is
|
|
1625
|
-
// ignored (even for commands) so the query finishes cleanly first.
|
|
1626
|
-
if (submitting)
|
|
1627
|
-
return;
|
|
1628
|
-
// With an onSubmit handler, ordinary prompts are submitted in place so
|
|
1629
|
-
// the box stays focused and editable while the query streams above it.
|
|
1630
|
-
// Slash commands (and any prompt when no handler is wired) settle so the
|
|
1631
|
-
// caller's existing command loop handles them.
|
|
1632
|
-
if (onSubmit && !trimmed.startsWith("/")) {
|
|
1633
|
-
submitting = true;
|
|
1634
|
-
const submitted = buffer;
|
|
1635
|
-
const submittedSpans = pasteSpans.slice();
|
|
1636
|
-
Promise.resolve()
|
|
1637
|
-
.then(() => onSubmit(submitted, submittedSpans))
|
|
1638
|
-
.then(() => {
|
|
1639
|
-
submitting = false;
|
|
1640
|
-
}, () => {
|
|
1641
|
-
submitting = false;
|
|
1642
|
-
});
|
|
1643
|
-
return;
|
|
1643
|
+
if (buffer.trim()) {
|
|
1644
|
+
settle([buffer, pasteSpans], false);
|
|
1644
1645
|
}
|
|
1645
|
-
settle([buffer, pasteSpans], false);
|
|
1646
1646
|
return;
|
|
1647
1647
|
}
|
|
1648
1648
|
if (key && key.name === "backspace") {
|
|
@@ -1695,9 +1695,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1695
1695
|
repaint();
|
|
1696
1696
|
return;
|
|
1697
1697
|
}
|
|
1698
|
-
// Ctrl+J sends a bare LF (0x0a). Node reports it as name "enter" or
|
|
1699
|
-
// "linefeed". It is not a real Enter (a real Enter arrives as CR / name
|
|
1700
|
-
// "return"), so ignore it rather than submitting or inserting a newline.
|
|
1701
1698
|
if (key && (key.name === "enter" || key.name === "linefeed")) {
|
|
1702
1699
|
return;
|
|
1703
1700
|
}
|
|
@@ -1713,9 +1710,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1713
1710
|
repaint(true);
|
|
1714
1711
|
});
|
|
1715
1712
|
}
|
|
1716
|
-
// ---------------------------------------------------------------------------
|
|
1717
|
-
// User Display & Message Text Helpers
|
|
1718
|
-
// ---------------------------------------------------------------------------
|
|
1719
1713
|
export function userDisplayText(payload, pasteSpans) {
|
|
1720
1714
|
if (pasteSpans && pasteSpans.length) {
|
|
1721
1715
|
const segs = splitBlocks(payload, pasteSpans);
|