@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 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.start(`Working ${tickDuration(0)}`);
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
- const gap = text.endsWith("\n\n")
567
- ? ""
568
- : text.endsWith("\n")
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
- process.stdout.write("\n");
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
- process.stdout.write("\n");
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
- process.stdout.write(`\x1b[2m${pathName}: ${oldContent.length.toLocaleString()} chars -> ${newContent.length.toLocaleString()} chars ` +
37
- `(diff hidden: exceeds ${max_diff_source_chars.toLocaleString()} char limit)\x1b[0m\n`);
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
- process.stdout.write(`\x1b[90m${top}\x1b[0m\n`);
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
- process.stdout.write(`\x1b[90m│\x1b[0m ${line}${" ".repeat(fill)} \x1b[90m│\x1b[0m\n`);
126
+ pendingDiffOutput.push(`\x1b[90m│\x1b[0m ${line}${" ".repeat(fill)} \x1b[90m│\x1b[0m`);
118
127
  }
119
- process.stdout.write(`\x1b[90m╰${"─".repeat(boxW - 2)}╯\x1b[0m\n`);
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
- text = "";
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.update(text);
544
+ this.render = render;
545
+ this.draw();
539
546
  return;
540
547
  }
541
- this.text = text;
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.text = text;
552
- if (this.timer)
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${this.text}\x1b[0m`;
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++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.61",
3
+ "version": "1.0.62",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },