@oxecli/oxe 1.0.61 → 1.0.62
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 +20 -6
- 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,29 @@ export class Spinner {
|
|
|
526
526
|
timer = null;
|
|
527
527
|
frame = 0;
|
|
528
528
|
rows = 0;
|
|
529
|
-
|
|
529
|
+
render = null;
|
|
530
530
|
enabled;
|
|
531
|
+
lastRendered = "";
|
|
531
532
|
constructor(enabled = true) {
|
|
532
533
|
this.enabled = enabled;
|
|
533
534
|
}
|
|
534
535
|
start(text) {
|
|
536
|
+
this.startWithRender(() => text);
|
|
537
|
+
}
|
|
538
|
+
/** Start the spinner with a render callback (recomputed each animation tick),
|
|
539
|
+
* avoiding the need for a second, conflicting timer. */
|
|
540
|
+
startWithRender(render) {
|
|
535
541
|
if (!this.enabled)
|
|
536
542
|
return;
|
|
537
543
|
if (this.timer) {
|
|
538
|
-
this.
|
|
544
|
+
this.render = render;
|
|
545
|
+
this.draw();
|
|
539
546
|
return;
|
|
540
547
|
}
|
|
541
|
-
this.
|
|
548
|
+
this.render = render;
|
|
542
549
|
this.frame = 0;
|
|
543
550
|
this.rows = 0;
|
|
551
|
+
this.lastRendered = "";
|
|
544
552
|
this.timer = setInterval(() => {
|
|
545
553
|
this.frame++;
|
|
546
554
|
this.draw();
|
|
@@ -548,13 +556,19 @@ export class Spinner {
|
|
|
548
556
|
this.draw();
|
|
549
557
|
}
|
|
550
558
|
update(text) {
|
|
551
|
-
this.
|
|
552
|
-
|
|
559
|
+
if (this.timer) {
|
|
560
|
+
this.render = () => text;
|
|
553
561
|
this.draw();
|
|
562
|
+
}
|
|
554
563
|
}
|
|
555
564
|
draw() {
|
|
565
|
+
const text = this.render ? this.render() : "";
|
|
556
566
|
const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
557
|
-
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[2m${
|
|
567
|
+
const rendered = `\x1b[36m${icon}\x1b[0m \x1b[2m${text}\x1b[0m`;
|
|
568
|
+
// Nothing changed on this tick -> do not rewrite the line (avoids flicker).
|
|
569
|
+
if (rendered === this.lastRendered)
|
|
570
|
+
return;
|
|
571
|
+
this.lastRendered = rendered;
|
|
558
572
|
const newRows = Math.max(1, displayRows(rendered));
|
|
559
573
|
const clearRows = Math.max(this.rows, newRows, 1);
|
|
560
574
|
for (let i = 0; i < clearRows; i++) {
|