@oxecli/oxe 1.0.97 → 1.0.98
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 +63 -12
- package/dist/ui.js +62 -10
- 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, startDraftCapture, } 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, dockRenderPrompt, 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");
|
|
@@ -20,6 +20,13 @@ export class CLI {
|
|
|
20
20
|
promptHistory = [];
|
|
21
21
|
lastPrompt = "";
|
|
22
22
|
interruptPending = false;
|
|
23
|
+
/**
|
|
24
|
+
* When a prompt is submitted during an in-flight query (Enter while the AI is
|
|
25
|
+
* responding), we interrupt the query silently and queue the submitted prompt
|
|
26
|
+
* here so the next loop iteration processes it directly without showing the
|
|
27
|
+
* "Interrupted agent" alert.
|
|
28
|
+
*/
|
|
29
|
+
autoSubmit = null;
|
|
23
30
|
constructor() {
|
|
24
31
|
clearScreen();
|
|
25
32
|
this.renderHeader();
|
|
@@ -404,14 +411,33 @@ export class CLI {
|
|
|
404
411
|
const pct = Math.max(0, Math.min(100, Math.round(((budget - used) / budget) * 100)));
|
|
405
412
|
// Queries run the proven settle-on-Enter flow: the prompt settles and
|
|
406
413
|
// the query streams above it (robust dock streaming). While the query
|
|
407
|
-
// runs,
|
|
408
|
-
//
|
|
409
|
-
|
|
414
|
+
// runs, the prompt box stays live-editable: typed keys are rendered
|
|
415
|
+
// into the box, and Enter submits a new prompt that interrupts the
|
|
416
|
+
// running response (no "Interrupted agent" alert) and is processed
|
|
417
|
+
// immediately. A plain (non-submitted) draft is restored into the next
|
|
418
|
+
// prompt box so input is never lost.
|
|
419
|
+
const hints = {
|
|
410
420
|
left: "\x1b[90mPress \x1b[1;36mCtrl+C\x1b[0m\x1b[90m to interrupt\x1b[0m",
|
|
411
421
|
right: `\x1b[1;36m${pct}%\x1b[0m \x1b[90muntil auto-compact\x1b[0m`,
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
422
|
+
};
|
|
423
|
+
let input;
|
|
424
|
+
let spans;
|
|
425
|
+
if (this.autoSubmit) {
|
|
426
|
+
// A prompt was submitted while the previous response was running. It
|
|
427
|
+
// has already interrupted that response; process it directly, first
|
|
428
|
+
// clearing the prompt box as a normal send would.
|
|
429
|
+
input = this.autoSubmit.input;
|
|
430
|
+
spans = this.autoSubmit.spans;
|
|
431
|
+
this.autoSubmit = null;
|
|
432
|
+
dockRenderPrompt("", [], "You", "❯", hints);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const got = await askBottomPrompt("You", "❯", this.promptHistory, hints, this.lastPrompt);
|
|
436
|
+
input = got[0];
|
|
437
|
+
spans = got[1];
|
|
438
|
+
if (!input.trim())
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
415
441
|
this.interruptPending = false;
|
|
416
442
|
this.promptHistory.push(input);
|
|
417
443
|
const lower = input.trim().toLowerCase();
|
|
@@ -463,18 +489,25 @@ export class CLI {
|
|
|
463
489
|
continue;
|
|
464
490
|
}
|
|
465
491
|
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
466
|
-
// Empty the prompt field as soon as the prompt is sent.
|
|
467
|
-
//
|
|
492
|
+
// Empty the prompt field as soon as the prompt is sent. A draft typed
|
|
493
|
+
// while the query runs is rendered into the box and restored into the
|
|
494
|
+
// next one; Enter submits it as a new prompt.
|
|
468
495
|
this.lastPrompt = "";
|
|
469
496
|
queryStarted = true;
|
|
470
|
-
const stopCapture = startDraftCapture(() => engine.interrupt());
|
|
497
|
+
const stopCapture = startDraftCapture(() => engine.interrupt(), (buf, sp) => dockRenderPrompt(buf, sp, "You", "❯", hints));
|
|
471
498
|
try {
|
|
472
499
|
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
473
500
|
}
|
|
474
501
|
finally {
|
|
475
502
|
const captured = stopCapture();
|
|
476
|
-
if (captured.trim())
|
|
477
|
-
|
|
503
|
+
if (captured.submitted && captured.draft.trim()) {
|
|
504
|
+
// The user pressed Enter during the response: interrupt it and queue
|
|
505
|
+
// the new prompt so the next loop iteration sends it (silently).
|
|
506
|
+
this.autoSubmit = { input: captured.draft, spans: [] };
|
|
507
|
+
}
|
|
508
|
+
else if (captured.draft.trim()) {
|
|
509
|
+
this.lastPrompt = captured.draft;
|
|
510
|
+
}
|
|
478
511
|
queryStarted = false;
|
|
479
512
|
}
|
|
480
513
|
this.saveSession(engine);
|
|
@@ -488,6 +521,24 @@ export class CLI {
|
|
|
488
521
|
}
|
|
489
522
|
if (err?.message === "interrupt") {
|
|
490
523
|
const wasActive = queryStarted || engine.inQuery;
|
|
524
|
+
// A prompt submitted during the response was queued in `autoSubmit`
|
|
525
|
+
// (by the finally above). Interrupt silently — no "Interrupted agent"
|
|
526
|
+
// alert — and let the next iteration process the new prompt (the loop
|
|
527
|
+
// top consumes `autoSubmit`).
|
|
528
|
+
if (this.autoSubmit && this.autoSubmit.input.trim()) {
|
|
529
|
+
engine.inQuery = false;
|
|
530
|
+
stripOrphanCalls(this.inputItems);
|
|
531
|
+
if (this.inputItems.length &&
|
|
532
|
+
this.inputItems[this.inputItems.length - 1]["role"] === "user") {
|
|
533
|
+
this.inputItems.pop();
|
|
534
|
+
}
|
|
535
|
+
if (this.story.length &&
|
|
536
|
+
this.story[this.story.length - 1]["type"] === "user") {
|
|
537
|
+
this.story.pop();
|
|
538
|
+
}
|
|
539
|
+
this.interruptPending = false;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
491
542
|
if (wasActive && !engine.queryHasOutput && !engine.queryCalledTool) {
|
|
492
543
|
dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
|
|
493
544
|
}
|
package/dist/ui.js
CHANGED
|
@@ -39,14 +39,18 @@ export function waitRawKey() {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Capture typed input while a query runs (the prompt box is
|
|
43
|
-
* streaming). Keystrokes are accumulated into a draft
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Capture typed input while a query runs (the prompt box is docked during
|
|
43
|
+
* streaming). Keystrokes are accumulated into a draft AND rendered into the
|
|
44
|
+
* docked prompt box via `renderBox`, so the user sees their input live while
|
|
45
|
+
* the AI is busy. Enter submits the draft: it interrupts the running query
|
|
46
|
+
* (as if Ctrl+C) and flags `submitted`, so the caller can send the new prompt.
|
|
47
|
+
* Returns a stop() function returning `{ draft, submitted }`.
|
|
46
48
|
*/
|
|
47
|
-
export function startDraftCapture(onInterrupt) {
|
|
49
|
+
export function startDraftCapture(onInterrupt, renderBox) {
|
|
48
50
|
let draft = "";
|
|
51
|
+
let pasteSpans = [];
|
|
49
52
|
let active = true;
|
|
53
|
+
let submitted = false;
|
|
50
54
|
const onKey = (str, key) => {
|
|
51
55
|
if (!active)
|
|
52
56
|
return;
|
|
@@ -56,14 +60,24 @@ export function startDraftCapture(onInterrupt) {
|
|
|
56
60
|
}
|
|
57
61
|
if (key && key.ctrl && (key.name === "d" || key.name === "z"))
|
|
58
62
|
return;
|
|
63
|
+
if (isEnterKey(str, key)) {
|
|
64
|
+
if (draft.trim()) {
|
|
65
|
+
submitted = true;
|
|
66
|
+
active = false;
|
|
67
|
+
onInterrupt();
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key && (key.name === "enter" || key.name === "linefeed"))
|
|
72
|
+
return;
|
|
59
73
|
if (key && key.name === "backspace") {
|
|
60
74
|
draft = draft.slice(0, -1);
|
|
75
|
+
renderBox?.(draft, pasteSpans);
|
|
61
76
|
return;
|
|
62
77
|
}
|
|
63
|
-
if (key && (key.name === "return" || key.name === "enter"))
|
|
64
|
-
return;
|
|
65
78
|
if (str && !key?.ctrl && !key?.meta) {
|
|
66
79
|
draft += str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
80
|
+
renderBox?.(draft, pasteSpans);
|
|
67
81
|
}
|
|
68
82
|
};
|
|
69
83
|
readline.emitKeypressEvents(process.stdin);
|
|
@@ -81,7 +95,7 @@ export function startDraftCapture(onInterrupt) {
|
|
|
81
95
|
/* ignore */
|
|
82
96
|
}
|
|
83
97
|
process.stdin.pause();
|
|
84
|
-
return draft;
|
|
98
|
+
return { draft, submitted };
|
|
85
99
|
};
|
|
86
100
|
}
|
|
87
101
|
// ---------------------------------------------------------------------------
|
|
@@ -568,6 +582,13 @@ let dockBoxRows = 0;
|
|
|
568
582
|
let dockGap = false;
|
|
569
583
|
let dockTransient = false;
|
|
570
584
|
let dockFrameLines = [];
|
|
585
|
+
/**
|
|
586
|
+
* How many rows the caret sits BELOW boxTop when the prompt box is being
|
|
587
|
+
* live-edited during a query (0 = cursor parked at boxTop / transient, the
|
|
588
|
+
* normal dock invariant). dockEraseBox must know this to erase the box from
|
|
589
|
+
* the caret's position instead of assuming the cursor is at boxTop.
|
|
590
|
+
*/
|
|
591
|
+
let dockCaretBelow = 0;
|
|
571
592
|
export function dockIsDocked() {
|
|
572
593
|
return docked;
|
|
573
594
|
}
|
|
@@ -585,12 +606,14 @@ export function dockSetInactive() {
|
|
|
585
606
|
dockGap = false;
|
|
586
607
|
dockTransient = false;
|
|
587
608
|
dockFrameLines = [];
|
|
609
|
+
dockCaretBelow = 0;
|
|
588
610
|
}
|
|
589
611
|
export function dockEnterDocked() {
|
|
590
612
|
docked = true;
|
|
591
613
|
dockRel = 0;
|
|
592
614
|
dockGap = false;
|
|
593
615
|
dockTransient = false;
|
|
616
|
+
dockCaretBelow = 0;
|
|
594
617
|
}
|
|
595
618
|
/** Erase the docked box and leave the cursor at boxTop with the gap above it,
|
|
596
619
|
* so a pager/replay can write there and a later prompt re-docks cleanly. */
|
|
@@ -639,12 +662,18 @@ export function dockTransientStart(mode = "blank") {
|
|
|
639
662
|
dockTransient = true;
|
|
640
663
|
}
|
|
641
664
|
/** Erase the box and any transient rows above it, leaving the cursor at boxTop
|
|
642
|
-
* with the gap row intact above.
|
|
665
|
+
* with the gap row intact above. Handles the cursor being parked at boxTop,
|
|
666
|
+
* above it (transient), or inside it at the live-edit caret. */
|
|
643
667
|
function dockEraseBox() {
|
|
644
668
|
if (!docked || dockBoxRows < 1)
|
|
645
669
|
return;
|
|
646
670
|
const rel = dockRel;
|
|
647
|
-
|
|
671
|
+
const caret = dockCaretBelow;
|
|
672
|
+
// Move from the cursor to the box's bottom row, then erase the box upward.
|
|
673
|
+
// cursor at boxTop - rel (transient): go down (dockBoxRows - 1 + rel)
|
|
674
|
+
// cursor at boxTop + caret (live caret): go down (dockBoxRows - 1 - caret)
|
|
675
|
+
const down = caret > 0 ? dockBoxRows - 1 - caret : dockBoxRows - 1 + rel;
|
|
676
|
+
process.stdout.write(`\x1b[${down}B`);
|
|
648
677
|
for (let i = 0; i < dockBoxRows; i++) {
|
|
649
678
|
process.stdout.write("\r\x1b[K");
|
|
650
679
|
if (i < dockBoxRows - 1)
|
|
@@ -657,6 +686,7 @@ function dockEraseBox() {
|
|
|
657
686
|
}
|
|
658
687
|
docked = false;
|
|
659
688
|
dockRel = 0;
|
|
689
|
+
dockCaretBelow = 0;
|
|
660
690
|
dockGap = true;
|
|
661
691
|
dockTransient = false;
|
|
662
692
|
}
|
|
@@ -674,6 +704,7 @@ function dockEraseBoxKeep() {
|
|
|
674
704
|
process.stdout.write(`\x1b[${dockRel}A`);
|
|
675
705
|
docked = false;
|
|
676
706
|
dockRel = 0;
|
|
707
|
+
dockCaretBelow = 0;
|
|
677
708
|
dockGap = true;
|
|
678
709
|
dockTransient = false;
|
|
679
710
|
}
|
|
@@ -685,10 +716,31 @@ function dockRenderBox() {
|
|
|
685
716
|
process.stdout.write("\n\n" + dockFrameLines.join("\n"));
|
|
686
717
|
process.stdout.write(`\x1b[${dockBoxRows - 1}A\r`);
|
|
687
718
|
dockRel = 0;
|
|
719
|
+
dockCaretBelow = 0;
|
|
688
720
|
docked = true;
|
|
689
721
|
dockGap = false;
|
|
690
722
|
dockTransient = false;
|
|
691
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Redraw the docked prompt box with the given buffer/caret, keeping whatever
|
|
726
|
+
* content already streams above it. Used to make the prompt field live-edit
|
|
727
|
+
* while a query runs. Erases the box (and any transient row above it),
|
|
728
|
+
* writes the new frame, and parks the cursor at the caret. Preserves exactly
|
|
729
|
+
* one blank row between the last content and the box.
|
|
730
|
+
*/
|
|
731
|
+
export function dockRenderPrompt(buffer, pasteSpans, prefix, label, hints) {
|
|
732
|
+
if (!docked || dockBoxRows < 1)
|
|
733
|
+
return;
|
|
734
|
+
const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, buffer.length, hints);
|
|
735
|
+
dockEraseBox();
|
|
736
|
+
process.stdout.write(frame);
|
|
737
|
+
// Park the cursor at the caret inside the box (cursorRow is 1-indexed, so
|
|
738
|
+
// the caret sits cursorRow rows below boxTop).
|
|
739
|
+
process.stdout.write(`\x1b[${totalRows - 1 - cursorRow}A\r\x1b[${cursorCol}C`);
|
|
740
|
+
dockSetFrame(frame, totalRows);
|
|
741
|
+
dockEnterDocked();
|
|
742
|
+
dockCaretBelow = cursorRow;
|
|
743
|
+
}
|
|
692
744
|
/** Commit content at the box top, pushing the box down, preserving exactly one
|
|
693
745
|
* blank row between the content and the box. */
|
|
694
746
|
export function dockAppendContent(text) {
|