@oxecli/oxe 1.0.76 → 1.0.78

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
@@ -103,7 +103,7 @@ export class InferenceEngine {
103
103
  result = await toolBash(args.command, args.timeout, args.cwd);
104
104
  break;
105
105
  case "glob":
106
- result = toolGlob(args.pattern, args.path, args.limit);
106
+ result = toolGlob(args.pattern, args.path, args.limit, args.depth);
107
107
  break;
108
108
  case "grep":
109
109
  result = toolGrep(args.pattern, args.path, args.glob, args.limit);
@@ -578,11 +578,12 @@ export class InferenceEngine {
578
578
  this.workActive = false;
579
579
  const started = toolCallLabel(c.name, c.arguments);
580
580
  const toolSpinner = new Spinner();
581
- toolSpinner.start(started);
581
+ toolSpinner.startDots("Running");
582
582
  const rawResult = await this.runTool(c.name, c.arguments);
583
- toolSpinner.stop();
584
- if (this.interrupted)
583
+ if (this.interrupted) {
584
+ toolSpinner.stop();
585
585
  throw new Error("interrupt");
586
+ }
586
587
  const failed = toolOutputFailed(c.name, rawResult);
587
588
  const action = formatToolResult(rawResult, failed);
588
589
  story.push({
@@ -591,7 +592,10 @@ export class InferenceEngine {
591
592
  text: action,
592
593
  status: failed ? "failed" : "ok",
593
594
  });
594
- process.stdout.write(`${started}\n${action}\n\n`);
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`);
595
599
  // Any diff the tool produced is flushed only now, after the spinner
596
600
  // has stopped, so the box renders cleanly below the action line.
597
601
  flushPendingDiffOutput();
package/dist/tools.js CHANGED
@@ -122,26 +122,29 @@ function displayDiff(pathName, oldContent, newContent) {
122
122
  maxNum = Math.max(maxNum, v.newNum);
123
123
  }
124
124
  const numW = String(maxNum).length;
125
- const indent = " ";
126
- const bodyMax = Math.max(termW - plainLen(indent) - numW - 4, 20);
125
+ const bodyMax = Math.max(termW - numW - 4, 20);
127
126
  const gap = " ".repeat(numW);
128
127
  for (const v of visible) {
129
128
  if (v.kind === "~") {
130
129
  const body = truncateStyled(v.body, bodyMax);
131
- pendingDiffOutput.push(`\x1b[90m${indent}${gap} ${body}\x1b[0m`);
130
+ pendingDiffOutput.push(`\x1b[90m${gap} ${body}\x1b[0m`);
132
131
  continue;
133
132
  }
134
133
  const num = v.kind === "+" ? v.newNum : v.oldNum;
135
134
  const numStr = num ? String(num).padStart(numW, " ") : gap;
136
135
  if (v.kind === "+" || v.kind === "-") {
137
- const color = v.kind === "+" ? "\x1b[32m" : "\x1b[31m";
136
+ const bg = v.kind === "+" ? "\x1b[48;2;2;40;0m" : "\x1b[48;2;61;1;0m";
138
137
  const sign = v.kind === "+" ? "+" : "-";
139
138
  const body = truncateStyled(v.body, bodyMax);
140
- pendingDiffOutput.push(`${indent}${color}${numStr} ${sign} ${body}\x1b[0m`);
139
+ const fill = Math.max(0, termW - numW - 4 - plainLen(body));
140
+ pendingDiffOutput.push(`${bg}\x1b[97m${numStr} ${sign} ${body}${" ".repeat(fill)}\x1b[0m`);
141
141
  }
142
142
  else {
143
- const body = truncateStyled(v.kind === "\\" ? v.body : highlightCodeLine(v.body), bodyMax);
144
- pendingDiffOutput.push(`\x1b[90m${indent}${numStr} ${body}\x1b[0m`);
143
+ const raw = v.kind === "\\" ? v.body : highlightCodeLine(v.body);
144
+ // highlightCodeLine resets to default after each token; re-apply the dim
145
+ // gray base so plain text stays dim and only tokens show color.
146
+ const body = truncateStyled(raw.replace(/\x1b\[0m/g, "\x1b[0m\x1b[90m"), bodyMax);
147
+ pendingDiffOutput.push(`\x1b[90m${numStr} ${body}\x1b[0m`);
145
148
  }
146
149
  }
147
150
  return { added, removed };
@@ -629,7 +632,7 @@ function globMatch(name, pattern) {
629
632
  .replace(/\?/g, ".");
630
633
  return new RegExp(`^${regex}$`, "i").test(name);
631
634
  }
632
- function walkFiles(root, current, rel, parts, out) {
635
+ function walkFiles(root, current, rel, parts, out, depthLeft) {
633
636
  let entries;
634
637
  try {
635
638
  entries = fs.readdirSync(current, { withFileTypes: true });
@@ -643,8 +646,10 @@ function walkFiles(root, current, rel, parts, out) {
643
646
  if (entry.isDirectory()) {
644
647
  if (ignoredDirs.has(name))
645
648
  continue;
649
+ if (depthLeft <= 0)
650
+ continue;
646
651
  rel.push(name);
647
- walkFiles(root, path.join(current, name), rel, parts, out);
652
+ walkFiles(root, path.join(current, name), rel, parts, out, depthLeft - 1);
648
653
  rel.pop();
649
654
  }
650
655
  else if (entry.isFile()) {
@@ -661,13 +666,13 @@ function walkFiles(root, current, rel, parts, out) {
661
666
  }
662
667
  }
663
668
  }
664
- function rglobPruned(base, pattern) {
669
+ function rglobPruned(base, pattern, depth = 12) {
665
670
  const norm = pattern.replace(/\\/g, "/");
666
671
  const parts = norm.split("/").filter((p) => p !== "" && p !== ".");
667
672
  const hasDotDot = parts.includes("..");
668
673
  if (hasDotDot) {
669
674
  const out = [];
670
- const walk = (dir) => {
675
+ const walk = (dir, depthLeft) => {
671
676
  let entries;
672
677
  try {
673
678
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -678,8 +683,8 @@ function rglobPruned(base, pattern) {
678
683
  for (const e of entries) {
679
684
  const full = path.join(dir, e.name);
680
685
  if (e.isDirectory()) {
681
- if (!ignoredDirs.has(e.name))
682
- walk(full);
686
+ if (!ignoredDirs.has(e.name) && depthLeft > 0)
687
+ walk(full, depthLeft - 1);
683
688
  }
684
689
  else if (e.isFile()) {
685
690
  out.push([full, fs.statSync(full).size]);
@@ -690,7 +695,7 @@ function rglobPruned(base, pattern) {
690
695
  out.push([base, fs.statSync(base).size]);
691
696
  }
692
697
  else {
693
- walk(base);
698
+ walk(base, depth);
694
699
  }
695
700
  return out;
696
701
  }
@@ -702,17 +707,18 @@ function rglobPruned(base, pattern) {
702
707
  if (!parts.length)
703
708
  return [];
704
709
  const out = [];
705
- walkFiles(base, base, [], ["**", ...parts], out);
710
+ walkFiles(base, base, [], ["**", ...parts], out, depth);
706
711
  return out;
707
712
  }
708
- export function toolGlob(pattern, pathName = ".", limit = 200) {
713
+ export function toolGlob(pattern, pathName = ".", limit = 200, depth = 12) {
709
714
  const base = sanitizePath(pathName) || ".";
710
715
  if (!fs.existsSync(base))
711
716
  return `Error: path not found: ${pathName}`;
712
717
  const lim = typeof limit === "number" ? limit : parseInt(String(limit), 10) || 200;
718
+ const dep = Math.max(0, typeof depth === "number" ? depth : parseInt(String(depth), 10) || 12);
713
719
  let matches;
714
720
  try {
715
- matches = rglobPruned(base, pattern);
721
+ matches = rglobPruned(base, pattern, dep);
716
722
  }
717
723
  catch {
718
724
  return `Error: path not found: ${pathName}`;
@@ -891,6 +897,10 @@ export const RESPONSES_TOOLS = [
891
897
  type: "integer",
892
898
  description: "Max number of matches to return (default 200)",
893
899
  },
900
+ depth: {
901
+ type: "integer",
902
+ description: "Max directory depth to descend (default 12)",
903
+ },
894
904
  },
895
905
  required: ["pattern"],
896
906
  },
package/dist/ui.js CHANGED
@@ -512,6 +512,7 @@ export function showCursor() {
512
512
  }
513
513
  }
514
514
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
515
+ const DOT_FRAMES = [". ", ".. ", "..."];
515
516
  export class Spinner {
516
517
  timer = null;
517
518
  frame = 0;
@@ -520,6 +521,8 @@ export class Spinner {
520
521
  enabled;
521
522
  lastRendered = "";
522
523
  lastLen = 0;
524
+ dotsText = "";
525
+ dotsFrame = 0;
523
526
  constructor(enabled = true) {
524
527
  this.enabled = enabled;
525
528
  }
@@ -547,6 +550,53 @@ export class Spinner {
547
550
  }, 80);
548
551
  this.draw();
549
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(). */
556
+ startDots(text) {
557
+ if (!this.enabled)
558
+ return;
559
+ if (this.timer)
560
+ clearInterval(this.timer);
561
+ this.dotsText = text;
562
+ this.dotsFrame = 0;
563
+ this.rows = 1;
564
+ this.lastRendered = "";
565
+ this.lastLen = text.length + 3;
566
+ this.writeDots(true);
567
+ this.timer = setInterval(() => {
568
+ this.dotsFrame = (this.dotsFrame + 1) % DOT_FRAMES.length;
569
+ this.writeDots(false);
570
+ }, 300);
571
+ }
572
+ writeDots(initial) {
573
+ const label = `\x1b[1;36m${this.dotsText}\x1b[0m`;
574
+ const dots = `\x1b[1;36m${DOT_FRAMES[this.dotsFrame]}\x1b[0m`;
575
+ if (initial) {
576
+ process.stdout.write("\r" + label + dots + "\r");
577
+ }
578
+ else {
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");
581
+ }
582
+ }
583
+ /** Overwrite the dots line in place with `text` and advance to the next
584
+ * line, without erasing (so there is no flash). */
585
+ replaceWith(text) {
586
+ if (this.timer) {
587
+ clearInterval(this.timer);
588
+ this.timer = null;
589
+ }
590
+ if (!this.enabled) {
591
+ process.stdout.write(`${text}\n`);
592
+ return;
593
+ }
594
+ const plain = stripAnsi(text);
595
+ const pad = Math.max(0, this.lastLen - plain.length);
596
+ process.stdout.write("\r" + text + " ".repeat(pad) + "\r\n");
597
+ this.rows = 0;
598
+ this.lastLen = plain.length;
599
+ }
550
600
  update(text) {
551
601
  if (this.timer) {
552
602
  this.render = () => text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.76",
3
+ "version": "1.0.78",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },