@oxecli/oxe 1.0.61 → 1.0.63
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 +17 -17
- package/dist/tools.js +14 -5
- package/dist/ui.js +50 -20
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -5,6 +5,7 @@ import { buildTools, truncateToolOutput, toolReadFile, toolWriteFile, toolEditFi
|
|
|
5
5
|
import { mutedMarkdown, aiMarkdown, tickDuration, displayRows, safeCommitPoint, printAiChunk, formatToolAction, renderPanel, Spinner, hideCursor, } from "./ui.js";
|
|
6
6
|
import { reportUsage } from "./api.js";
|
|
7
7
|
import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
|
|
8
|
+
import { flushPendingDiffOutput } from "./tools.js";
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// Token estimation
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -169,20 +170,15 @@ export class InferenceEngine {
|
|
|
169
170
|
let thinkingStart = null;
|
|
170
171
|
const workStatus = new Spinner();
|
|
171
172
|
const workingStarted = Date.now();
|
|
172
|
-
workStatus.
|
|
173
|
-
const workTimer = setInterval(() => {
|
|
174
|
-
workStatus.update(`Working ${tickDuration((Date.now() - workingStarted) / 1000)}`);
|
|
175
|
-
}, 100);
|
|
173
|
+
workStatus.startWithRender(() => `Working ${tickDuration((Date.now() - workingStarted) / 1000)}`);
|
|
176
174
|
let workingReported = false;
|
|
177
175
|
let response = null;
|
|
178
176
|
let etype = null;
|
|
179
177
|
const silenceWorking = () => {
|
|
180
|
-
clearInterval(workTimer);
|
|
181
178
|
workStatus.stop();
|
|
182
179
|
workingReported = true;
|
|
183
180
|
};
|
|
184
181
|
const reportWorking = () => {
|
|
185
|
-
clearInterval(workTimer);
|
|
186
182
|
workStatus.stop();
|
|
187
183
|
if (workingReported)
|
|
188
184
|
return;
|
|
@@ -288,7 +284,6 @@ export class InferenceEngine {
|
|
|
288
284
|
}
|
|
289
285
|
}
|
|
290
286
|
finally {
|
|
291
|
-
clearInterval(workTimer);
|
|
292
287
|
workStatus.stop();
|
|
293
288
|
const stillThinking = thinkingStart !== null;
|
|
294
289
|
finishThinking(stillThinking);
|
|
@@ -563,12 +558,9 @@ export class InferenceEngine {
|
|
|
563
558
|
return;
|
|
564
559
|
}
|
|
565
560
|
if (!calls.length) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
? "\n"
|
|
570
|
-
: "\n\n";
|
|
571
|
-
process.stdout.write(gap + summary() + "\n");
|
|
561
|
+
// Exactly one blank row between the message and the footer summary.
|
|
562
|
+
const trailing = (text.match(/\n+$/) ?? [""])[0].length;
|
|
563
|
+
process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)) + summary() + "\n");
|
|
572
564
|
story.push({ type: "footer", text: footerText() });
|
|
573
565
|
attachFooter(conversation);
|
|
574
566
|
attachFooter(inputItems);
|
|
@@ -576,8 +568,12 @@ export class InferenceEngine {
|
|
|
576
568
|
this.workRows = 0;
|
|
577
569
|
return;
|
|
578
570
|
}
|
|
579
|
-
if (text.trim())
|
|
580
|
-
|
|
571
|
+
if (text.trim()) {
|
|
572
|
+
// Ensure exactly one blank row between the assistant message and the
|
|
573
|
+
// first tool call, no matter how many newlines the message ended with.
|
|
574
|
+
const trailing = (text.match(/\n+$/) ?? [""])[0].length;
|
|
575
|
+
process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)));
|
|
576
|
+
}
|
|
581
577
|
for (const c of calls) {
|
|
582
578
|
this.workActive = false;
|
|
583
579
|
const started = formatToolAction(c.name, c.arguments, "started");
|
|
@@ -598,6 +594,9 @@ export class InferenceEngine {
|
|
|
598
594
|
const icon = failed ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
|
|
599
595
|
const actionStyle = failed ? "\x1b[31m" : "\x1b[32m";
|
|
600
596
|
process.stdout.write(`${icon} ${actionStyle}${action}\x1b[0m\n\n`);
|
|
597
|
+
// Any diff the tool produced is flushed only now, after the spinner
|
|
598
|
+
// has stopped, so the box renders cleanly below the action line.
|
|
599
|
+
flushPendingDiffOutput();
|
|
601
600
|
const truncatedResult = c.name === "read_file"
|
|
602
601
|
? truncateToolOutput(rawResult, max_read_file_stored_chars)
|
|
603
602
|
: truncateToolOutput(rawResult);
|
|
@@ -612,11 +611,12 @@ export class InferenceEngine {
|
|
|
612
611
|
pending.push(outputItem);
|
|
613
612
|
}
|
|
614
613
|
}
|
|
615
|
-
|
|
614
|
+
// The preceding tool action already leaves the cursor on a blank row, so
|
|
615
|
+
// no extra newline is needed above; renderPanel ends with its own "\n",
|
|
616
|
+
// and the prompt adds one leading "\n" -> exactly one blank row below.
|
|
616
617
|
renderPanel("[bold yellow]⚠ Max tool-call iterations reached for this turn.[/bold yellow]\n" +
|
|
617
618
|
"[yellow]The work so far is saved. If you want the agent to keep going, type " +
|
|
618
619
|
"[bold]continue[/bold] and the next step will resume from where it left off.[/yellow]", "Warning", "", false, "33");
|
|
619
|
-
process.stdout.write("\n");
|
|
620
620
|
if (retryPrompts.length)
|
|
621
621
|
dropRetryPrompts(conversation, inputItems, retryPrompts);
|
|
622
622
|
stripOrphanCalls(conversation);
|
package/dist/tools.js
CHANGED
|
@@ -31,10 +31,19 @@ function truncateDiffLine(line) {
|
|
|
31
31
|
}
|
|
32
32
|
return line;
|
|
33
33
|
}
|
|
34
|
+
// Diff output is deferred (not written straight to stdout) so it doesn't
|
|
35
|
+
// collide with the tool spinner that is still animating while the tool runs.
|
|
36
|
+
const pendingDiffOutput = [];
|
|
37
|
+
export function flushPendingDiffOutput() {
|
|
38
|
+
if (!pendingDiffOutput.length)
|
|
39
|
+
return;
|
|
40
|
+
process.stdout.write(pendingDiffOutput.join("\n") + "\n\n");
|
|
41
|
+
pendingDiffOutput.length = 0;
|
|
42
|
+
}
|
|
34
43
|
function displayDiff(pathName, oldContent, newContent) {
|
|
35
44
|
if (oldContent.length + newContent.length > max_diff_source_chars) {
|
|
36
|
-
|
|
37
|
-
`(diff hidden: exceeds ${max_diff_source_chars.toLocaleString()} char limit)\x1b[0m
|
|
45
|
+
pendingDiffOutput.push(`\x1b[2m${pathName}: ${oldContent.length.toLocaleString()} chars -> ${newContent.length.toLocaleString()} chars ` +
|
|
46
|
+
`(diff hidden: exceeds ${max_diff_source_chars.toLocaleString()} char limit)\x1b[0m`);
|
|
38
47
|
return;
|
|
39
48
|
}
|
|
40
49
|
const patch = structuredPatch(pathName, pathName, oldContent, newContent, "", "", { context: max_diff_context_lines });
|
|
@@ -98,7 +107,7 @@ function displayDiff(pathName, oldContent, newContent) {
|
|
|
98
107
|
const header = `\x1b[1m${pathDisp}\x1b[0m \x1b[32m+${added}\x1b[0m \x1b[31m-${removed}\x1b[0m`;
|
|
99
108
|
const headerStr = `─ ${header} `;
|
|
100
109
|
const top = `╭${headerStr}${"─".repeat(Math.max(0, boxW - 2 - plainLen(headerStr)))}╮`;
|
|
101
|
-
|
|
110
|
+
pendingDiffOutput.push(`\x1b[90m${top}\x1b[0m`);
|
|
102
111
|
for (const v of visible) {
|
|
103
112
|
let line;
|
|
104
113
|
if (v.kind === "+") {
|
|
@@ -114,9 +123,9 @@ function displayDiff(pathName, oldContent, newContent) {
|
|
|
114
123
|
line = `\x1b[2m ${body}\x1b[0m`;
|
|
115
124
|
}
|
|
116
125
|
const fill = Math.max(0, innerW - plainLen(line));
|
|
117
|
-
|
|
126
|
+
pendingDiffOutput.push(`\x1b[90m│\x1b[0m ${line}${" ".repeat(fill)} \x1b[90m│\x1b[0m`);
|
|
118
127
|
}
|
|
119
|
-
|
|
128
|
+
pendingDiffOutput.push(`\x1b[90m╰${"─".repeat(boxW - 2)}╯\x1b[0m`);
|
|
120
129
|
}
|
|
121
130
|
export function truncateToolOutput(output, maxChars = max_output_chars) {
|
|
122
131
|
if (output.length > maxChars) {
|
package/dist/ui.js
CHANGED
|
@@ -526,21 +526,31 @@ export class Spinner {
|
|
|
526
526
|
timer = null;
|
|
527
527
|
frame = 0;
|
|
528
528
|
rows = 0;
|
|
529
|
-
|
|
529
|
+
render = null;
|
|
530
530
|
enabled;
|
|
531
|
+
lastRendered = "";
|
|
532
|
+
lastLen = 0;
|
|
531
533
|
constructor(enabled = true) {
|
|
532
534
|
this.enabled = enabled;
|
|
533
535
|
}
|
|
534
536
|
start(text) {
|
|
537
|
+
this.startWithRender(() => text);
|
|
538
|
+
}
|
|
539
|
+
/** Start the spinner with a render callback (recomputed each animation tick),
|
|
540
|
+
* avoiding the need for a second, conflicting timer. */
|
|
541
|
+
startWithRender(render) {
|
|
535
542
|
if (!this.enabled)
|
|
536
543
|
return;
|
|
537
544
|
if (this.timer) {
|
|
538
|
-
this.
|
|
545
|
+
this.render = render;
|
|
546
|
+
this.draw();
|
|
539
547
|
return;
|
|
540
548
|
}
|
|
541
|
-
this.
|
|
549
|
+
this.render = render;
|
|
542
550
|
this.frame = 0;
|
|
543
551
|
this.rows = 0;
|
|
552
|
+
this.lastRendered = "";
|
|
553
|
+
this.lastLen = 0;
|
|
544
554
|
this.timer = setInterval(() => {
|
|
545
555
|
this.frame++;
|
|
546
556
|
this.draw();
|
|
@@ -548,29 +558,49 @@ export class Spinner {
|
|
|
548
558
|
this.draw();
|
|
549
559
|
}
|
|
550
560
|
update(text) {
|
|
551
|
-
this.
|
|
552
|
-
|
|
561
|
+
if (this.timer) {
|
|
562
|
+
this.render = () => text;
|
|
553
563
|
this.draw();
|
|
564
|
+
}
|
|
554
565
|
}
|
|
555
566
|
draw() {
|
|
567
|
+
const text = this.render ? this.render() : "";
|
|
556
568
|
const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
557
|
-
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[2m${
|
|
569
|
+
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[2m${text}\x1b[0m`;
|
|
570
|
+
// Nothing changed on this tick -> do not rewrite the line (avoids flicker).
|
|
571
|
+
if (rendered === this.lastRendered)
|
|
572
|
+
return;
|
|
573
|
+
this.lastRendered = rendered;
|
|
558
574
|
const newRows = Math.max(1, displayRows(rendered));
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
575
|
+
if (newRows === 1) {
|
|
576
|
+
// Overwrite in place: NEVER erase the whole line (the \x1b[2K erase-to-blank
|
|
577
|
+
// followed by a full redraw is what caused the flicker/flash). We just
|
|
578
|
+
// re-emit from the start; identical glyphs stay put, only the changed
|
|
579
|
+
// cells (spinner frame, trailing timer) actually change on screen. Pad
|
|
580
|
+
// with spaces so a shorter line clears any previously longer text.
|
|
581
|
+
const plain = stripAnsi(rendered);
|
|
582
|
+
const pad = Math.max(0, this.lastLen - plain.length);
|
|
583
|
+
process.stdout.write("\r" + rendered + " ".repeat(pad) + "\r");
|
|
584
|
+
this.lastLen = plain.length;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
// Multi-row wrapped text: fall back to an erase-based redraw.
|
|
588
|
+
const clearRows = Math.max(this.rows, newRows, 1);
|
|
589
|
+
for (let i = 0; i < clearRows; i++) {
|
|
590
|
+
process.stdout.write("\r\x1b[2K");
|
|
591
|
+
if (i < clearRows - 1)
|
|
592
|
+
process.stdout.write("\n");
|
|
593
|
+
}
|
|
594
|
+
if (clearRows > 1)
|
|
595
|
+
process.stdout.write(`\x1b[${clearRows - 1}A\r`);
|
|
596
|
+
else
|
|
597
|
+
process.stdout.write("\r");
|
|
598
|
+
process.stdout.write(rendered);
|
|
599
|
+
if (newRows > 1)
|
|
600
|
+
process.stdout.write(`\x1b[${newRows - 1}A\r`);
|
|
601
|
+
else
|
|
602
|
+
process.stdout.write("\r");
|
|
564
603
|
}
|
|
565
|
-
if (clearRows > 1)
|
|
566
|
-
process.stdout.write(`\x1b[${clearRows - 1}A\r`);
|
|
567
|
-
else
|
|
568
|
-
process.stdout.write("\r");
|
|
569
|
-
process.stdout.write(rendered);
|
|
570
|
-
if (newRows > 1)
|
|
571
|
-
process.stdout.write(`\x1b[${newRows - 1}A\r`);
|
|
572
|
-
else
|
|
573
|
-
process.stdout.write("\r");
|
|
574
604
|
this.rows = newRows;
|
|
575
605
|
}
|
|
576
606
|
stop() {
|