@oxecli/oxe 1.0.15 → 1.0.16

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/config.js CHANGED
@@ -145,7 +145,7 @@ export const SYSTEM_PROMPT_BODY = "You are a terminal-based coding agent named O
145
145
  "consequence in your response.\n";
146
146
  export const API_KEY_MAX = 64;
147
147
  import { validateOxeApiKey } from "./api.js";
148
- import { renderPanel, markupToAnsi } from "./ui.js";
148
+ import { renderPanel, markupToAnsi, hideCursor, showCursor } from "./ui.js";
149
149
  export async function loadOrPrompt() {
150
150
  let api_key = "";
151
151
  let key_data = {};
@@ -164,9 +164,13 @@ export async function loadOrPrompt() {
164
164
  }
165
165
  process.stdout.write("\n");
166
166
  const authSpinner = new Spinner();
167
+ // promptApiKey echoes input with the cursor visible; hide it while the
168
+ // validating-key label runs so no stray block cursor sits beside it.
169
+ hideCursor();
167
170
  authSpinner.start("Authenticating key with Oxe Cloud…");
168
171
  const validation = await validateOxeApiKey(api_key);
169
172
  authSpinner.stop();
173
+ showCursor();
170
174
  if (validation.valid) {
171
175
  key_data = validation.key_data || {};
172
176
  process.stdout.write(markupToAnsi(`[green]✓ Oxe API Key verified[/green] [dim](${String(key_data["name"] ?? "Desktop")})[/dim]`) + "\n");
package/dist/ui.js CHANGED
@@ -456,15 +456,76 @@ export function splitBlocks(buffer, pasteSpans) {
456
456
  }
457
457
  return segs;
458
458
  }
459
+ const PROMPT_CARET = "\u0000"; // sentinel marking the caret position
460
+ /**
461
+ * Wrap styled runs into physical rows of `width` visible columns, wrapping long
462
+ * lines so the panel box stays rectangular (mirrors rich's Panel auto-wrap).
463
+ * Returns the styled rows (no borders) and the caret's (row, col) in content
464
+ * coordinates, or (-1,-1) if no caret sentinel is present.
465
+ */
466
+ function wrapRuns(runs, width) {
467
+ const rows = [];
468
+ let cur = "";
469
+ let curLen = 0;
470
+ let caretRow = -1;
471
+ let caretCol = -1;
472
+ const flush = () => {
473
+ rows.push(cur);
474
+ cur = "";
475
+ curLen = 0;
476
+ };
477
+ for (const run of runs) {
478
+ if (run.text === PROMPT_CARET) {
479
+ // Caret occupies one visible column; if the current row is full, move it
480
+ // to the start of the next row so it stays inside the box.
481
+ if (curLen >= width)
482
+ flush();
483
+ caretRow = rows.length;
484
+ caretCol = curLen;
485
+ cur += run.style + "▏" + "\x1b[0m";
486
+ curLen += 1;
487
+ continue;
488
+ }
489
+ let text = run.text;
490
+ while (text) {
491
+ // Split off the leading run up to the next newline (if any).
492
+ const nl = text.indexOf("\n");
493
+ const seg = nl === -1 ? text : text.slice(0, nl);
494
+ let rest = seg;
495
+ while (rest.length > 0) {
496
+ if (curLen >= width)
497
+ flush();
498
+ const take = Math.min(rest.length, width - curLen);
499
+ const chunk = rest.slice(0, take);
500
+ cur += run.style ? run.style + chunk + "\x1b[0m" : chunk;
501
+ curLen += take;
502
+ rest = rest.slice(take);
503
+ }
504
+ if (nl !== -1) {
505
+ flush();
506
+ text = text.slice(nl + 1);
507
+ }
508
+ else {
509
+ break;
510
+ }
511
+ }
512
+ }
513
+ flush();
514
+ return { rows, caretRow, caretCol };
515
+ }
459
516
  export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor) {
460
517
  const w = terminalWidth();
461
518
  const borderW = Math.max(w - 4, 10);
462
- const [row, col] = cursorLineCol(buffer, cursor);
463
- // Build the inner content: prefix, then the text with the `▏` block cursor
464
- // always drawn at the cursor position (mirrors the original rich frame).
465
- let display;
519
+ // Content width inside the box (between `│ ` and ` │`).
520
+ const innerW = borderW - 2;
521
+ // Build styled runs: prefix, then the text with the `▏` block caret always
522
+ // drawn at the cursor position (mirrors the original rich frame).
523
+ const runs = [];
524
+ runs.push({ text: prefix, style: "\x1b[1m" });
525
+ runs.push({ text: " ", style: "" });
466
526
  if (!buffer) {
467
- display = `\x1b[1m▏\x1b[0m\x1b[2m${PROMPT_PLACEHOLDER}\x1b[0m`;
527
+ runs.push({ text: PROMPT_CARET, style: "\x1b[1m" });
528
+ runs.push({ text: PROMPT_PLACEHOLDER, style: "\x1b[2m" });
468
529
  }
469
530
  else {
470
531
  const segs = splitBlocks(buffer, pasteSpans);
@@ -513,50 +574,49 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
513
574
  inside = ranges.length ? ranges[ranges.length - 1][1] - ranges[ranges.length - 1][0] : 0;
514
575
  }
515
576
  }
516
- display = "";
517
577
  for (let i = 0; i < segs.length; i++) {
518
578
  const [, kind, disp] = segs[i];
519
579
  if (i && !endsWithWs(segs[i - 1][2]))
520
- display += " ";
580
+ runs.push({ text: " ", style: "" });
521
581
  if (i === target) {
522
582
  if (kind === "collapsed") {
523
- display += `\x1b[1m\x1b[36m${disp}\x1b[0m`;
583
+ runs.push({ text: disp, style: "\x1b[1m\x1b[36m" });
524
584
  if (!endsWithWs(disp))
525
- display += " ";
526
- display += `\x1b[1m▏\x1b[0m`;
585
+ runs.push({ text: " ", style: "" });
586
+ runs.push({ text: PROMPT_CARET, style: "\x1b[1m" });
527
587
  }
528
588
  else {
529
- const bold = i === target;
530
- display += disp.slice(0, inside);
531
- display += `\x1b[1m▏\x1b[0m`;
532
- display += disp.slice(inside);
589
+ const before = disp.slice(0, inside);
590
+ const after = disp.slice(inside);
591
+ if (before)
592
+ runs.push({ text: before, style: "" });
593
+ runs.push({ text: PROMPT_CARET, style: "\x1b[1m" });
594
+ if (after)
595
+ runs.push({ text: after, style: "" });
533
596
  }
534
597
  }
535
598
  else {
536
- display += kind === "collapsed" ? `\x1b[1m\x1b[36m${disp}\x1b[0m` : disp;
599
+ runs.push({ text: disp, style: kind === "collapsed" ? "\x1b[1m\x1b[36m" : "" });
537
600
  }
538
601
  }
539
602
  }
540
- const content = `\x1b[1m${prefix}\x1b[0m ${display}`;
541
- const lines = content.split("\n");
603
+ const { rows, caretRow, caretCol } = wrapRuns(runs, innerW);
542
604
  // Top border with the label embedded on the left.
543
605
  const topPad = Math.max(borderW - label.length - 2, 0);
544
606
  const top = `\x1b[90m╭─ ${label} ${"─".repeat(topPad)}╮\x1b[0m`;
545
607
  // Body rows.
546
608
  const body = [];
547
- for (const l of lines) {
609
+ for (const l of rows) {
548
610
  const plain = l.replace(/\x1b\[[0-9;]*m/g, "");
549
611
  const pad = Math.max(borderW - 2 - plain.length, 0);
550
612
  body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
551
613
  }
552
614
  const bottom = `\x1b[90m╰${"─".repeat(borderW)}╯\x1b[0m`;
553
615
  const frame = [top, ...body, bottom].join("\n");
554
- // Cursor placement: one row below the top border. Preceding columns are
555
- // `│ ` (2) + prefix (prefix.length) + ` ` (1) = prefix.length+3, so the first
556
- // display char (the `▏` block cursor) sits at column prefix.length+4 relative
557
- // to line column `col`.
558
- const cursorRow = row + 1;
559
- const cursorCol = col + prefix.length + 4;
616
+ // Cursor placement: one row below the top border. The body prefix (`│ `)
617
+ // shifts the content right by 2 columns, so add 2 to the wrapped caret col.
618
+ const cursorRow = (caretRow === -1 ? 0 : caretRow) + 1;
619
+ const cursorCol = (caretCol === -1 ? 0 : caretCol) + 2;
560
620
  const totalRows = body.length + 2;
561
621
  return { frame, cursorRow, cursorCol, totalRows };
562
622
  }
@@ -735,15 +795,32 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
735
795
  pasteSpans = adjusted.filter(([s, e]) => s < e);
736
796
  };
737
797
  const moveLeft = () => {
738
- if (cursor > 0)
739
- cursor -= 1;
798
+ if (cursor <= 0)
799
+ return;
800
+ // Jump the caret to the start of a paste span it sits just past (mirrors
801
+ // Python), so a collapsed block is treated as one unit.
802
+ for (const [s, e] of pasteSpans) {
803
+ if (s < cursor && cursor <= e) {
804
+ cursor = s;
805
+ return;
806
+ }
807
+ }
808
+ cursor -= 1;
740
809
  };
741
810
  const moveRight = () => {
742
- if (cursor < buffer.length)
743
- cursor += 1;
811
+ if (cursor >= buffer.length)
812
+ return;
813
+ for (const [s, e] of pasteSpans) {
814
+ if (s <= cursor && cursor < e) {
815
+ cursor = e;
816
+ return;
817
+ }
818
+ }
819
+ cursor += 1;
744
820
  };
745
821
  const moveUp = () => {
746
822
  if (!buffer.includes("\n")) {
823
+ // Single row: navigate command history.
747
824
  if (!hist.length)
748
825
  return;
749
826
  if (histIdx === hist.length) {
@@ -755,10 +832,21 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
755
832
  cursor = buffer.length;
756
833
  pasteSpans = [];
757
834
  }
835
+ return;
836
+ }
837
+ // Multi row: move the caret up to the previous line, preserving column.
838
+ if (cursor > 0) {
839
+ let lineStart = buffer.lastIndexOf("\n", cursor - 1);
840
+ lineStart = lineStart !== -1 ? lineStart : 0;
841
+ let prevStart = buffer.lastIndexOf("\n", lineStart - 1);
842
+ prevStart = prevStart !== -1 ? prevStart : 0;
843
+ const [, col] = cursorLineCol(buffer, cursor);
844
+ cursor = prevStart + Math.min(col, lineStart - prevStart);
758
845
  }
759
846
  };
760
847
  const moveDown = () => {
761
848
  if (!buffer.includes("\n")) {
849
+ // Single row: navigate command history.
762
850
  if (histIdx === hist.length || !draft)
763
851
  return;
764
852
  histIdx += 1;
@@ -772,6 +860,21 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
772
860
  cursor = buffer.length;
773
861
  pasteSpans = [];
774
862
  }
863
+ return;
864
+ }
865
+ // Multi row: move the caret down to the next line, preserving column.
866
+ if (cursor < buffer.length) {
867
+ const [, col] = cursorLineCol(buffer, cursor);
868
+ let nextStart = buffer.indexOf("\n", cursor);
869
+ if (nextStart === -1) {
870
+ cursor = buffer.length;
871
+ }
872
+ else {
873
+ nextStart += 1;
874
+ let nxtEnd = buffer.indexOf("\n", nextStart);
875
+ nxtEnd = nxtEnd !== -1 ? nxtEnd : buffer.length;
876
+ cursor = Math.min(nextStart + col, nxtEnd);
877
+ }
775
878
  }
776
879
  };
777
880
  const onKeypress = (str, key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },