@oxecli/oxe 1.0.14 → 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 +5 -1
- package/dist/ui.js +178 -31
- package/package.json +1 -1
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
|
-
|
|
463
|
-
|
|
464
|
-
//
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
580
|
+
runs.push({ text: " ", style: "" });
|
|
521
581
|
if (i === target) {
|
|
522
582
|
if (kind === "collapsed") {
|
|
523
|
-
|
|
583
|
+
runs.push({ text: disp, style: "\x1b[1m\x1b[36m" });
|
|
524
584
|
if (!endsWithWs(disp))
|
|
525
|
-
|
|
526
|
-
|
|
585
|
+
runs.push({ text: " ", style: "" });
|
|
586
|
+
runs.push({ text: PROMPT_CARET, style: "\x1b[1m" });
|
|
527
587
|
}
|
|
528
588
|
else {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
599
|
+
runs.push({ text: disp, style: kind === "collapsed" ? "\x1b[1m\x1b[36m" : "" });
|
|
537
600
|
}
|
|
538
601
|
}
|
|
539
602
|
}
|
|
540
|
-
const
|
|
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
|
|
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.
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
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
|
}
|
|
@@ -670,12 +730,37 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
670
730
|
return;
|
|
671
731
|
}
|
|
672
732
|
};
|
|
673
|
-
const
|
|
733
|
+
const shiftSpansAfterInsert = (pos, delta) => {
|
|
734
|
+
const newSpans = [];
|
|
735
|
+
for (const [s, e] of pasteSpans) {
|
|
736
|
+
if (pos <= s)
|
|
737
|
+
newSpans.push([s + delta, e + delta]);
|
|
738
|
+
else if (pos >= e)
|
|
739
|
+
newSpans.push([s, e]);
|
|
740
|
+
else {
|
|
741
|
+
newSpans.push([s, pos]);
|
|
742
|
+
newSpans.push([pos + delta, e + delta]);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return newSpans;
|
|
746
|
+
};
|
|
747
|
+
const insert = (text, isPaste = false) => {
|
|
674
748
|
if (histIdx !== hist.length) {
|
|
675
749
|
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
676
750
|
histIdx = hist.length;
|
|
677
751
|
}
|
|
752
|
+
const pasteStart = cursor;
|
|
678
753
|
buffer = buffer.slice(0, cursor) + text + buffer.slice(cursor);
|
|
754
|
+
// Keep existing paste spans valid across the insertion point.
|
|
755
|
+
pasteSpans = shiftSpansAfterInsert(cursor, text.length);
|
|
756
|
+
// A real paste records a span so the prompt box can collapse it (mirrors
|
|
757
|
+
// Python's handle_paste). splitBlocks only collapses segments inside a
|
|
758
|
+
// span that meet the threshold, so small pastes stay expanded but remain
|
|
759
|
+
// tagged for cursor/backspace handling.
|
|
760
|
+
if (isPaste) {
|
|
761
|
+
pasteSpans.push([pasteStart, pasteStart + text.length]);
|
|
762
|
+
pasteSpans.sort((a, b) => a[0] - b[0]);
|
|
763
|
+
}
|
|
679
764
|
cursor += text.length;
|
|
680
765
|
};
|
|
681
766
|
const backspace = () => {
|
|
@@ -689,18 +774,53 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
689
774
|
const before = Array.from(buffer.slice(0, cursor));
|
|
690
775
|
before.pop();
|
|
691
776
|
buffer = before.join("") + buffer.slice(cursor);
|
|
777
|
+
const deletedAt = cursor - 1;
|
|
692
778
|
cursor -= 1;
|
|
779
|
+
// Adjust spans: shrink any span covering the deleted char, shift spans
|
|
780
|
+
// that start after it left by one.
|
|
781
|
+
const adjusted = [];
|
|
782
|
+
for (const [s, e] of pasteSpans) {
|
|
783
|
+
if (deletedAt >= s && deletedAt < e) {
|
|
784
|
+
const ne = e - 1;
|
|
785
|
+
if (s < ne)
|
|
786
|
+
adjusted.push([s, ne]);
|
|
787
|
+
}
|
|
788
|
+
else if (deletedAt < s) {
|
|
789
|
+
adjusted.push([s - 1, e - 1]);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
adjusted.push([s, e]);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
pasteSpans = adjusted.filter(([s, e]) => s < e);
|
|
693
796
|
};
|
|
694
797
|
const moveLeft = () => {
|
|
695
|
-
if (cursor
|
|
696
|
-
|
|
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;
|
|
697
809
|
};
|
|
698
810
|
const moveRight = () => {
|
|
699
|
-
if (cursor
|
|
700
|
-
|
|
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;
|
|
701
820
|
};
|
|
702
821
|
const moveUp = () => {
|
|
703
822
|
if (!buffer.includes("\n")) {
|
|
823
|
+
// Single row: navigate command history.
|
|
704
824
|
if (!hist.length)
|
|
705
825
|
return;
|
|
706
826
|
if (histIdx === hist.length) {
|
|
@@ -712,10 +832,21 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
712
832
|
cursor = buffer.length;
|
|
713
833
|
pasteSpans = [];
|
|
714
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);
|
|
715
845
|
}
|
|
716
846
|
};
|
|
717
847
|
const moveDown = () => {
|
|
718
848
|
if (!buffer.includes("\n")) {
|
|
849
|
+
// Single row: navigate command history.
|
|
719
850
|
if (histIdx === hist.length || !draft)
|
|
720
851
|
return;
|
|
721
852
|
histIdx += 1;
|
|
@@ -729,6 +860,21 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
729
860
|
cursor = buffer.length;
|
|
730
861
|
pasteSpans = [];
|
|
731
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
|
+
}
|
|
732
878
|
}
|
|
733
879
|
};
|
|
734
880
|
const onKeypress = (str, key) => {
|
|
@@ -782,8 +928,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
782
928
|
return;
|
|
783
929
|
}
|
|
784
930
|
if (str) {
|
|
785
|
-
// paste
|
|
786
|
-
|
|
931
|
+
// Multi-char sequence = a paste (single-char typed keys arrive one per
|
|
932
|
+
// event). Tag it so the prompt box collapses it when it meets the rules.
|
|
933
|
+
insert(str, str.length > 1);
|
|
787
934
|
repaint();
|
|
788
935
|
}
|
|
789
936
|
};
|