@oxecli/oxe 1.0.89 → 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 +67 -67
- 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,11 +1246,9 @@ 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)
|
|
1251
|
+
process.stdout.write(`\x1b[${lastCursorRow}A`);
|
|
1209
1252
|
for (let i = 0; i < lastTotalRows; i++) {
|
|
1210
1253
|
process.stdout.write("\r\x1b[K");
|
|
1211
1254
|
if (i < lastTotalRows - 1)
|
|
@@ -1228,7 +1271,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1228
1271
|
dockSetInactive();
|
|
1229
1272
|
}
|
|
1230
1273
|
else {
|
|
1231
|
-
|
|
1274
|
+
if (lastCursorRow > 0)
|
|
1275
|
+
process.stdout.write(`\x1b[${lastCursorRow}A`);
|
|
1276
|
+
process.stdout.write("\r");
|
|
1232
1277
|
dockSetFrame((prevFrameLines ?? []).join("\n"), lastTotalRows);
|
|
1233
1278
|
dockEnterDocked();
|
|
1234
1279
|
}
|
|
@@ -1256,14 +1301,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1256
1301
|
process.stdout.write("\n");
|
|
1257
1302
|
}
|
|
1258
1303
|
prevFrameLines = newLines;
|
|
1259
|
-
lastCursorRow =
|
|
1304
|
+
lastCursorRow = cursorRow;
|
|
1260
1305
|
lastTotalRows = totalRows;
|
|
1261
1306
|
dockSetFrame(frame, totalRows);
|
|
1262
1307
|
dockEnterDocked();
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const up = totalRows - 1;
|
|
1266
|
-
process.stdout.write(`\x1b[${up}A\r`);
|
|
1308
|
+
const up = totalRows - 1 - cursorRow;
|
|
1309
|
+
process.stdout.write(`\x1b[${up}A\r\x1b[${cursorCol}C`);
|
|
1267
1310
|
}
|
|
1268
1311
|
else {
|
|
1269
1312
|
process.stdout.write(`\x1b[${lastCursorRow}A\r`);
|
|
@@ -1277,11 +1320,13 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1277
1320
|
process.stdout.write("\n");
|
|
1278
1321
|
}
|
|
1279
1322
|
const atLine = maxLen - 1;
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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`);
|
|
1283
1328
|
prevFrameLines = newLines;
|
|
1284
|
-
lastCursorRow =
|
|
1329
|
+
lastCursorRow = cursorRow;
|
|
1285
1330
|
lastTotalRows = totalRows;
|
|
1286
1331
|
dockSetFrame(frame, totalRows);
|
|
1287
1332
|
}
|
|
@@ -1314,8 +1359,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1314
1359
|
}
|
|
1315
1360
|
cursor += text.length;
|
|
1316
1361
|
};
|
|
1317
|
-
// A paste span is treated as a single atomic "block" only when it collapses
|
|
1318
|
-
// to a marker (large pastes). Small inline pastes behave like normal text.
|
|
1319
1362
|
const spanCollapsed = (span) => {
|
|
1320
1363
|
return shouldCollapsePaste(buffer.slice(span[0], span[1]));
|
|
1321
1364
|
};
|
|
@@ -1492,11 +1535,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1492
1535
|
if (!bracketedPaste && (sequence === start || value === start)) {
|
|
1493
1536
|
bracketedPaste = true;
|
|
1494
1537
|
bracketedPasteBuffer = "";
|
|
1495
|
-
const inline = value === start
|
|
1496
|
-
? ""
|
|
1497
|
-
: value.startsWith(start)
|
|
1498
|
-
? value.slice(start.length)
|
|
1499
|
-
: "";
|
|
1538
|
+
const inline = value === start ? "" : value.startsWith(start) ? value.slice(start.length) : "";
|
|
1500
1539
|
if (inline)
|
|
1501
1540
|
bracketedPasteBuffer = inline;
|
|
1502
1541
|
if (inline.includes(end)) {
|
|
@@ -1516,9 +1555,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1516
1555
|
if (!bracketedPaste)
|
|
1517
1556
|
return false;
|
|
1518
1557
|
if (sequence === end || value === end) {
|
|
1519
|
-
const pasted = bracketedPasteBuffer
|
|
1520
|
-
.replace(/\r\n/g, "\n")
|
|
1521
|
-
.replace(/\r/g, "\n");
|
|
1558
|
+
const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1522
1559
|
bracketedPaste = false;
|
|
1523
1560
|
bracketedPasteBuffer = "";
|
|
1524
1561
|
if (pasted)
|
|
@@ -1526,15 +1563,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1526
1563
|
repaint();
|
|
1527
1564
|
return true;
|
|
1528
1565
|
}
|
|
1529
|
-
const chunk = value ||
|
|
1530
|
-
(sequence && !sequence.startsWith("\x1b[") ? sequence : "");
|
|
1566
|
+
const chunk = value || (sequence && !sequence.startsWith("\x1b[") ? sequence : "");
|
|
1531
1567
|
if (chunk) {
|
|
1532
1568
|
const endAt = chunk.indexOf(end);
|
|
1533
1569
|
if (endAt >= 0) {
|
|
1534
1570
|
bracketedPasteBuffer += chunk.slice(0, endAt);
|
|
1535
|
-
const pasted = bracketedPasteBuffer
|
|
1536
|
-
.replace(/\r\n/g, "\n")
|
|
1537
|
-
.replace(/\r/g, "\n");
|
|
1571
|
+
const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1538
1572
|
bracketedPaste = false;
|
|
1539
1573
|
bracketedPasteBuffer = "";
|
|
1540
1574
|
if (pasted)
|
|
@@ -1566,12 +1600,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1566
1600
|
pasteBurstTimer = null;
|
|
1567
1601
|
}
|
|
1568
1602
|
if (key && key.ctrl && key.name === "c") {
|
|
1569
|
-
// While a query is running, Ctrl+C interrupts the query instead of
|
|
1570
|
-
// leaving the prompt (the box stays focused for editing).
|
|
1571
|
-
if (submitting && onInterrupt) {
|
|
1572
|
-
onInterrupt();
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
1603
|
settle(new Error("interrupt"), true);
|
|
1576
1604
|
return;
|
|
1577
1605
|
}
|
|
@@ -1612,31 +1640,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1612
1640
|
return;
|
|
1613
1641
|
}
|
|
1614
1642
|
if (isEnterKey(str, key)) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
return;
|
|
1618
|
-
// While a query is running the box stays focused and editable; Enter is
|
|
1619
|
-
// ignored (even for commands) so the query finishes cleanly first.
|
|
1620
|
-
if (submitting)
|
|
1621
|
-
return;
|
|
1622
|
-
// With an onSubmit handler, ordinary prompts are submitted in place so
|
|
1623
|
-
// the box stays focused and editable while the query streams above it.
|
|
1624
|
-
// Slash commands (and any prompt when no handler is wired) settle so the
|
|
1625
|
-
// caller's existing command loop handles them.
|
|
1626
|
-
if (onSubmit && !trimmed.startsWith("/")) {
|
|
1627
|
-
submitting = true;
|
|
1628
|
-
const submitted = buffer;
|
|
1629
|
-
const submittedSpans = pasteSpans.slice();
|
|
1630
|
-
Promise.resolve()
|
|
1631
|
-
.then(() => onSubmit(submitted, submittedSpans))
|
|
1632
|
-
.then(() => {
|
|
1633
|
-
submitting = false;
|
|
1634
|
-
}, () => {
|
|
1635
|
-
submitting = false;
|
|
1636
|
-
});
|
|
1637
|
-
return;
|
|
1643
|
+
if (buffer.trim()) {
|
|
1644
|
+
settle([buffer, pasteSpans], false);
|
|
1638
1645
|
}
|
|
1639
|
-
settle([buffer, pasteSpans], false);
|
|
1640
1646
|
return;
|
|
1641
1647
|
}
|
|
1642
1648
|
if (key && key.name === "backspace") {
|
|
@@ -1689,9 +1695,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1689
1695
|
repaint();
|
|
1690
1696
|
return;
|
|
1691
1697
|
}
|
|
1692
|
-
// Ctrl+J sends a bare LF (0x0a). Node reports it as name "enter" or
|
|
1693
|
-
// "linefeed". It is not a real Enter (a real Enter arrives as CR / name
|
|
1694
|
-
// "return"), so ignore it rather than submitting or inserting a newline.
|
|
1695
1698
|
if (key && (key.name === "enter" || key.name === "linefeed")) {
|
|
1696
1699
|
return;
|
|
1697
1700
|
}
|
|
@@ -1707,9 +1710,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1707
1710
|
repaint(true);
|
|
1708
1711
|
});
|
|
1709
1712
|
}
|
|
1710
|
-
// ---------------------------------------------------------------------------
|
|
1711
|
-
// User Display & Message Text Helpers
|
|
1712
|
-
// ---------------------------------------------------------------------------
|
|
1713
1713
|
export function userDisplayText(payload, pasteSpans) {
|
|
1714
1714
|
if (pasteSpans && pasteSpans.length) {
|
|
1715
1715
|
const segs = splitBlocks(payload, pasteSpans);
|