@oxecli/oxe 1.0.96 → 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/engine.js +11 -9
- 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/engine.js
CHANGED
|
@@ -155,11 +155,12 @@ export class InferenceEngine {
|
|
|
155
155
|
let sawReasoning = false;
|
|
156
156
|
let status = null;
|
|
157
157
|
let thinkingStart = null;
|
|
158
|
-
// A "Worked for …" block is committed once per working phase.
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
158
|
+
// A "Worked for …" block is committed once per working phase. The flag is
|
|
159
|
+
// reset whenever a tool call runs (in the tool loop below), which is the
|
|
160
|
+
// only way a later phase can begin — the loop only continues after a tool
|
|
161
|
+
// call. So unlimited working blocks can appear, but never two in a row
|
|
162
|
+
// without a tool call in between. An answer ends the query. The total
|
|
163
|
+
// worked time also lands in the final footer.
|
|
163
164
|
const showWorkSpinner = !this.workedCommitted;
|
|
164
165
|
if (showWorkSpinner)
|
|
165
166
|
dockTransientStart();
|
|
@@ -184,10 +185,7 @@ export class InferenceEngine {
|
|
|
184
185
|
const t = tickDuration(elapsed);
|
|
185
186
|
if (!showWorkSpinner)
|
|
186
187
|
return;
|
|
187
|
-
|
|
188
|
-
// call) may commit a fresh block; the per-streamOnce guard above keeps
|
|
189
|
-
// the current phase from duplicating.
|
|
190
|
-
this.workedCommitted = false;
|
|
188
|
+
this.workedCommitted = true;
|
|
191
189
|
story.push({ type: "worked", text: `Worked for ${t}` });
|
|
192
190
|
dockReplaceTransient(mutedMarkdown(`Worked for ${t}`) + "\n");
|
|
193
191
|
};
|
|
@@ -555,6 +553,10 @@ export class InferenceEngine {
|
|
|
555
553
|
dockAppendContent(summary() + "\n");
|
|
556
554
|
return;
|
|
557
555
|
}
|
|
556
|
+
// A successful turn (text or tool call) after a retry must not leave the
|
|
557
|
+
// cut-off retry prompts in the transcript — they'd pollute the context of
|
|
558
|
+
// later turns. Drop them now that the retry produced real output.
|
|
559
|
+
dropRetryPrompts(conversation, inputItems, retryPrompts);
|
|
558
560
|
if (!calls.length) {
|
|
559
561
|
story.push({ type: "footer", text: footerText() });
|
|
560
562
|
attachFooter(conversation);
|
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) {
|