@oxecli/oxe 1.0.60 → 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 +26 -9
- 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
|
@@ -183,11 +183,14 @@ function highlightCodeLine(line, _lang = "") {
|
|
|
183
183
|
}
|
|
184
184
|
function formatInlineMarkdown(text) {
|
|
185
185
|
let line = text;
|
|
186
|
+
// Links MUST be converted first: the other rules inject ANSI escape codes,
|
|
187
|
+
// and the link pattern `\[[^\]]+\]\([^)]+\)` would otherwise match the `[`
|
|
188
|
+
// inside an already-inserted escape (e.g. `\x1b[1m`) and corrupt the output.
|
|
189
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[2m($2)\x1b[0m");
|
|
186
190
|
line = line.replace(/\*\*\*([^*]+)\*\*\*/g, "\x1b[1;3m$1\x1b[0m");
|
|
187
191
|
line = line.replace(/\*\*([^*]+)\*\*/g, "\x1b[1m$1\x1b[0m");
|
|
188
192
|
line = line.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
|
|
189
193
|
line = line.replace(/`([^`]+)`/g, "\x1b[1;36m`$1`\x1b[0m");
|
|
190
|
-
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[2m($2)\x1b[0m");
|
|
191
194
|
return line;
|
|
192
195
|
}
|
|
193
196
|
// ---------------------------------------------------------------------------
|
|
@@ -251,12 +254,12 @@ export function markdownToAnsi(text) {
|
|
|
251
254
|
return;
|
|
252
255
|
const badge = codeLang ? ` \x1b[1;36m${codeLang}\x1b[0m ` : " ";
|
|
253
256
|
const barLen = Math.max(10, Math.min(width - plainLen(badge) - 4, 60));
|
|
254
|
-
out.push(`\x1b[90m
|
|
257
|
+
out.push(`\x1b[90m╭─${badge}${"─".repeat(barLen)}╮\x1b[0m`);
|
|
255
258
|
for (const cline of codeBuffer) {
|
|
256
259
|
const highlighted = highlightCodeLine(cline, codeLang);
|
|
257
260
|
out.push(`\x1b[90m│\x1b[0m ${highlighted}`);
|
|
258
261
|
}
|
|
259
|
-
out.push(`\x1b[90m
|
|
262
|
+
out.push(`\x1b[90m╰${"─".repeat(barLen + plainLen(badge) + 2)}╯\x1b[0m`);
|
|
260
263
|
codeBuffer = [];
|
|
261
264
|
codeLang = "";
|
|
262
265
|
};
|
|
@@ -523,21 +526,29 @@ export class Spinner {
|
|
|
523
526
|
timer = null;
|
|
524
527
|
frame = 0;
|
|
525
528
|
rows = 0;
|
|
526
|
-
|
|
529
|
+
render = null;
|
|
527
530
|
enabled;
|
|
531
|
+
lastRendered = "";
|
|
528
532
|
constructor(enabled = true) {
|
|
529
533
|
this.enabled = enabled;
|
|
530
534
|
}
|
|
531
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) {
|
|
532
541
|
if (!this.enabled)
|
|
533
542
|
return;
|
|
534
543
|
if (this.timer) {
|
|
535
|
-
this.
|
|
544
|
+
this.render = render;
|
|
545
|
+
this.draw();
|
|
536
546
|
return;
|
|
537
547
|
}
|
|
538
|
-
this.
|
|
548
|
+
this.render = render;
|
|
539
549
|
this.frame = 0;
|
|
540
550
|
this.rows = 0;
|
|
551
|
+
this.lastRendered = "";
|
|
541
552
|
this.timer = setInterval(() => {
|
|
542
553
|
this.frame++;
|
|
543
554
|
this.draw();
|
|
@@ -545,13 +556,19 @@ export class Spinner {
|
|
|
545
556
|
this.draw();
|
|
546
557
|
}
|
|
547
558
|
update(text) {
|
|
548
|
-
this.
|
|
549
|
-
|
|
559
|
+
if (this.timer) {
|
|
560
|
+
this.render = () => text;
|
|
550
561
|
this.draw();
|
|
562
|
+
}
|
|
551
563
|
}
|
|
552
564
|
draw() {
|
|
565
|
+
const text = this.render ? this.render() : "";
|
|
553
566
|
const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
554
|
-
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;
|
|
555
572
|
const newRows = Math.max(1, displayRows(rendered));
|
|
556
573
|
const clearRows = Math.max(this.rows, newRows, 1);
|
|
557
574
|
for (let i = 0; i < clearRows; i++) {
|