@oxecli/oxe 1.0.78 → 1.0.80

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { createRequire } from "node:module";
3
3
  import { fileURLToPath } from "node:url";
4
- import { loadOrPrompt, default_reasoning_effort, max_action_chars, max_resume_history_items, runtimeOsSummary, } from "./config.js";
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
7
  import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
@@ -372,7 +372,13 @@ export class CLI {
372
372
  engine.inQuery = false;
373
373
  let queryStarted = false;
374
374
  try {
375
- const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory);
375
+ const used = estimateTokens(this.inputItems);
376
+ const budget = max_context_tokens - context_overhead_margin;
377
+ const pct = Math.max(0, Math.min(100, Math.round(((budget - used) / budget) * 100)));
378
+ const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory, {
379
+ left: "\x1b[90mesc to interrupt\x1b[0m",
380
+ right: `\x1b[1;36m${pct}%\x1b[0m \x1b[90muntil auto-compact\x1b[0m`,
381
+ });
376
382
  if (!input.trim())
377
383
  continue;
378
384
  this.interruptPending = false;
package/dist/engine.js CHANGED
@@ -577,8 +577,11 @@ export class InferenceEngine {
577
577
  for (const c of calls) {
578
578
  this.workActive = false;
579
579
  const started = toolCallLabel(c.name, c.arguments);
580
+ // Print the tool label the instant the command starts, then show a
581
+ // "⎿ Working..." dots line that swaps to the real result in place.
582
+ process.stdout.write(`${started}\n`);
580
583
  const toolSpinner = new Spinner();
581
- toolSpinner.startDots("Running");
584
+ toolSpinner.startDots("\x1b[90m⎿\x1b[0m Working");
582
585
  const rawResult = await this.runTool(c.name, c.arguments);
583
586
  if (this.interrupted) {
584
587
  toolSpinner.stop();
@@ -592,10 +595,9 @@ export class InferenceEngine {
592
595
  text: action,
593
596
  status: failed ? "failed" : "ok",
594
597
  });
595
- // Replace the "Running..." line in place with the tool label so the
596
- // transition is seamless, then print the result beneath it.
597
- toolSpinner.replaceWith(started);
598
- process.stdout.write(`${action}\n\n`);
598
+ // Swap the "⎿ Working..." line for the real result, then a blank line.
599
+ toolSpinner.replaceWith(action);
600
+ process.stdout.write(`\n`);
599
601
  // Any diff the tool produced is flushed only now, after the spinner
600
602
  // has stopped, so the box renders cleanly below the action line.
601
603
  flushPendingDiffOutput();
package/dist/tools.js CHANGED
@@ -133,11 +133,16 @@ function displayDiff(pathName, oldContent, newContent) {
133
133
  const num = v.kind === "+" ? v.newNum : v.oldNum;
134
134
  const numStr = num ? String(num).padStart(numW, " ") : gap;
135
135
  if (v.kind === "+" || v.kind === "-") {
136
- const bg = v.kind === "+" ? "\x1b[48;2;2;40;0m" : "\x1b[48;2;61;1;0m";
137
- const sign = v.kind === "+" ? "+" : "-";
138
- const body = truncateStyled(v.body, bodyMax);
136
+ const isAdd = v.kind === "+";
137
+ const bg = isAdd ? "\x1b[48;2;2;40;0m" : "\x1b[48;2;61;1;0m";
138
+ const fg = isAdd ? "\x1b[38;2;80;200;80m" : "\x1b[38;2;220;90;90m";
139
+ const sign = isAdd ? "+" : "-";
140
+ // Changed content renders normal white with syntax highlighting;
141
+ // re-apply the white base after each token reset.
142
+ const raw = highlightCodeLine(v.body);
143
+ const body = truncateStyled(raw.replace(/\x1b\[0m/g, "\x1b[0m\x1b[97m"), bodyMax);
139
144
  const fill = Math.max(0, termW - numW - 4 - plainLen(body));
140
- pendingDiffOutput.push(`${bg}\x1b[97m${numStr} ${sign} ${body}${" ".repeat(fill)}\x1b[0m`);
145
+ pendingDiffOutput.push(`${bg}${fg}${numStr} ${sign} \x1b[0m\x1b[97m${body}${" ".repeat(fill)}\x1b[0m`);
141
146
  }
142
147
  else {
143
148
  const raw = v.kind === "\\" ? v.body : highlightCodeLine(v.body);
package/dist/ui.js CHANGED
@@ -550,9 +550,10 @@ export class Spinner {
550
550
  }, 80);
551
551
  this.draw();
552
552
  }
553
- /** Dots-mode label: renders `Running` + a fixed 3-cell animated dots field.
554
- * Each tick rewrites ONLY the dots cells via a cursor jump, so the rest of
555
- * the line never flickers. Swap in the final label with replaceWith(). */
553
+ /** Dots-mode label: renders `text` (ANSI pre-colored by the caller) + a
554
+ * fixed 3-cell animated dots field. Each tick rewrites ONLY the dots cells
555
+ * via a cursor jump, so the rest of the line never flickers. Swap in the
556
+ * final label with replaceWith(). */
556
557
  startDots(text) {
557
558
  if (!this.enabled)
558
559
  return;
@@ -562,7 +563,7 @@ export class Spinner {
562
563
  this.dotsFrame = 0;
563
564
  this.rows = 1;
564
565
  this.lastRendered = "";
565
- this.lastLen = text.length + 3;
566
+ this.lastLen = plainLen(text) + 3;
566
567
  this.writeDots(true);
567
568
  this.timer = setInterval(() => {
568
569
  this.dotsFrame = (this.dotsFrame + 1) % DOT_FRAMES.length;
@@ -570,18 +571,18 @@ export class Spinner {
570
571
  }, 300);
571
572
  }
572
573
  writeDots(initial) {
573
- const label = `\x1b[1;36m${this.dotsText}\x1b[0m`;
574
574
  const dots = `\x1b[1;36m${DOT_FRAMES[this.dotsFrame]}\x1b[0m`;
575
575
  if (initial) {
576
- process.stdout.write("\r" + label + dots + "\r");
576
+ process.stdout.write("\r" + this.dotsText + dots + "\r");
577
577
  }
578
578
  else {
579
579
  // Jump to the dots column (1-based) and rewrite only the 3-cell field.
580
- process.stdout.write(`\x1b[${this.dotsText.length + 1}G` + dots + "\r");
580
+ process.stdout.write(`\x1b[${plainLen(this.dotsText) + 1}G` + dots + "\r");
581
581
  }
582
582
  }
583
583
  /** Overwrite the dots line in place with `text` and advance to the next
584
- * line, without erasing (so there is no flash). */
584
+ * line, without erasing (so there is no flash). Multi-line text replaces
585
+ * the first line in place and prints the remaining lines below it. */
585
586
  replaceWith(text) {
586
587
  if (this.timer) {
587
588
  clearInterval(this.timer);
@@ -591,9 +592,14 @@ export class Spinner {
591
592
  process.stdout.write(`${text}\n`);
592
593
  return;
593
594
  }
594
- const plain = stripAnsi(text);
595
+ const parts = text.split("\n");
596
+ const first = parts[0];
597
+ const rest = parts.slice(1);
598
+ const plain = stripAnsi(first);
595
599
  const pad = Math.max(0, this.lastLen - plain.length);
596
- process.stdout.write("\r" + text + " ".repeat(pad) + "\r\n");
600
+ process.stdout.write("\r" + first + " ".repeat(pad) + "\r\n");
601
+ if (rest.length)
602
+ process.stdout.write(rest.join("\n") + "\n");
597
603
  this.rows = 0;
598
604
  this.lastLen = plain.length;
599
605
  }
@@ -860,7 +866,7 @@ function wrapRuns(runs, width) {
860
866
  flush();
861
867
  return { rows, caretRow, caretCol };
862
868
  }
863
- export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor) {
869
+ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints) {
864
870
  const termW = terminalWidth();
865
871
  const boxW = Math.max(termW - 4, 16);
866
872
  const innerW = boxW - 4;
@@ -958,10 +964,18 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
958
964
  body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
959
965
  }
960
966
  const bottom = `\x1b[90m╰${"─".repeat(Math.max(0, boxW - 2))}╯\x1b[0m`;
961
- const frame = [top, ...body, bottom].join("\n");
967
+ const lines = [top, ...body, bottom];
968
+ let totalRows = body.length + 2;
969
+ if (hints && (hints.left || hints.right)) {
970
+ const left = hints.left ?? "";
971
+ const right = hints.right ?? "";
972
+ const gap = Math.max(1, termW - plainLen(left) - plainLen(right));
973
+ lines.push(left + " ".repeat(gap) + right);
974
+ totalRows += 1;
975
+ }
976
+ const frame = lines.join("\n");
962
977
  const cursorRow = (caretRow === -1 ? 0 : caretRow) + 1;
963
978
  const cursorCol = (caretCol === -1 ? 0 : caretCol) + 2;
964
- const totalRows = body.length + 2;
965
979
  return { frame, cursorRow, cursorCol, totalRows };
966
980
  }
967
981
  /**
@@ -983,7 +997,7 @@ function isEnterKey(str, key) {
983
997
  seq === "\x1bOM" // application keypad
984
998
  );
985
999
  }
986
- export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1000
+ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hints) {
987
1001
  return new Promise((resolve, reject) => {
988
1002
  const isTTY = process.stdin.isTTY;
989
1003
  if (!isTTY) {
@@ -1050,7 +1064,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1050
1064
  resolve(value);
1051
1065
  };
1052
1066
  const repaint = (isFirst = false) => {
1053
- const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor);
1067
+ const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints);
1054
1068
  const newLines = frame.split("\n");
1055
1069
  if (isFirst) {
1056
1070
  process.stdout.write("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },