@oxecli/oxe 1.0.102 → 1.0.104
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/README.md +3 -1
- package/dist/ui.js +78 -32
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/ui.js
CHANGED
|
@@ -667,13 +667,17 @@ export function dockTransientStart(mode = "blank") {
|
|
|
667
667
|
return;
|
|
668
668
|
dockEraseBox();
|
|
669
669
|
if (mode === "flush") {
|
|
670
|
-
// Park
|
|
670
|
+
// Park directly under the preceding content (adjacent, no blank above), but
|
|
671
|
+
// still keep one blank row between the transient and the box — otherwise the
|
|
672
|
+
// working line (and the tool result that replaces it) sits flush against the
|
|
673
|
+
// prompt box. dockRel=2 mirrors the blank mode's spacing below the row.
|
|
671
674
|
process.stdout.write("\x1b[1A\r");
|
|
672
675
|
process.stdout.write("\n");
|
|
676
|
+
process.stdout.write("\n");
|
|
673
677
|
process.stdout.write(dockFrameLines.join("\n"));
|
|
674
678
|
process.stdout.write(`\x1b[${dockBoxRows - 1}A`);
|
|
675
|
-
process.stdout.write("\x1b[
|
|
676
|
-
dockRel =
|
|
679
|
+
process.stdout.write("\x1b[2A\r");
|
|
680
|
+
dockRel = 2;
|
|
677
681
|
}
|
|
678
682
|
else {
|
|
679
683
|
// Park on a fresh spinner row that keeps a blank above and below it.
|
|
@@ -750,49 +754,64 @@ function dockRenderBox() {
|
|
|
750
754
|
/**
|
|
751
755
|
* Redraw the docked prompt box with the given buffer/caret, keeping whatever
|
|
752
756
|
* content already streams above it. Used to make the prompt field live-edit
|
|
753
|
-
* while a query runs.
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* transient row, so streaming content is never pulled into the prompt
|
|
757
|
+
* while a query runs. Preserves exactly one blank row between the last content
|
|
758
|
+
* and the box. Rows that did not change (top border, hint row) are left
|
|
759
|
+
* untouched, so static parts never flicker on each keystroke. When a transient
|
|
760
|
+
* (thinking/working spinner) is parked above the box, the physical cursor stays
|
|
761
|
+
* on that transient row, so streaming content is never pulled into the prompt
|
|
762
|
+
* and the box does not drift.
|
|
758
763
|
*/
|
|
759
764
|
export function dockRenderPrompt(buffer, pasteSpans, prefix, label, hints) {
|
|
760
765
|
if (!docked || dockBoxRows < 1)
|
|
761
766
|
return;
|
|
762
|
-
// If a thinking/working spinner is currently parked above the box, typing
|
|
763
|
-
// must NOT disturb it. dockEraseBox() wipes that transient row and resets
|
|
764
|
-
// its state; the still-running spinner then keeps drawing at the physical
|
|
765
|
-
// caret (now inside the box), pulling streaming content into the prompt.
|
|
766
|
-
// Capture the transient before erasing so we can rebuild it above the frame
|
|
767
|
-
// and park the cursor back on the transient row, keeping the box intact.
|
|
768
767
|
const wasTransient = dockTransient;
|
|
769
768
|
const wasRel = dockRel;
|
|
769
|
+
const wasCaret = dockCaretBelow;
|
|
770
770
|
const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, buffer.length, hints);
|
|
771
|
-
|
|
771
|
+
const newLines = frame.split("\n");
|
|
772
|
+
const oldLines = dockFrameLines.length ? dockFrameLines : newLines;
|
|
773
|
+
// Move the caret to the box's top row (R). The physical cursor is either on
|
|
774
|
+
// the transient row (R - wasRel) or at the live caret (R + wasCaret).
|
|
775
|
+
if (wasTransient)
|
|
776
|
+
process.stdout.write(`\x1b[${wasRel}B\r`);
|
|
777
|
+
else if (wasCaret > 0)
|
|
778
|
+
process.stdout.write(`\x1b[${wasCaret}A\r`);
|
|
779
|
+
// In-place line diff: rewrite only lines that actually changed (the body),
|
|
780
|
+
// leaving unchanged rows (top border, hint row) untouched so they never
|
|
781
|
+
// flicker. Clear each rewritten row so a shorter line leaves no residue.
|
|
782
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
783
|
+
for (let i = 0; i < maxLen; i++) {
|
|
784
|
+
if (newLines[i] !== oldLines[i]) {
|
|
785
|
+
process.stdout.write("\r\x1b[K" + (newLines[i] ?? ""));
|
|
786
|
+
}
|
|
787
|
+
if (i < maxLen - 1)
|
|
788
|
+
process.stdout.write("\n");
|
|
789
|
+
}
|
|
790
|
+
// Reposition the caret.
|
|
791
|
+
if (wasTransient) {
|
|
792
|
+
// Park back on the transient row so the running spinner keeps drawing there.
|
|
793
|
+
process.stdout.write(`\x1b[${maxLen - 1 + wasRel}A\r`);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
const atLine = maxLen - 1;
|
|
797
|
+
if (atLine > cursorRow)
|
|
798
|
+
process.stdout.write(`\x1b[${atLine - cursorRow}A\r`);
|
|
799
|
+
else if (atLine < cursorRow)
|
|
800
|
+
process.stdout.write(`\x1b[${cursorRow - atLine}B\r`);
|
|
801
|
+
process.stdout.write(`\x1b[${cursorCol}C`);
|
|
802
|
+
}
|
|
803
|
+
dockSetFrame(frame, totalRows);
|
|
772
804
|
if (wasTransient) {
|
|
773
|
-
// dockEraseBox has left the transient row and gap still present above the
|
|
774
|
-
// box-top row (cleared but physically occupying rows R-2 and R-1). Write
|
|
775
|
-
// the frame at R and park the cursor back on the transient row (R-2) so the
|
|
776
|
-
// running spinner keeps drawing THERE instead of on/inside the box. Do NOT
|
|
777
|
-
// write any leading newlines: they would push the box down and add a blank
|
|
778
|
-
// row on every keystroke.
|
|
779
|
-
process.stdout.write(frame);
|
|
780
|
-
process.stdout.write(`\x1b[${totalRows - 1 + wasRel}A\r`);
|
|
781
|
-
dockSetFrame(frame, totalRows);
|
|
782
805
|
docked = true;
|
|
783
806
|
dockGap = false;
|
|
784
807
|
dockTransient = true;
|
|
785
808
|
dockRel = wasRel;
|
|
786
809
|
dockCaretBelow = 0;
|
|
787
|
-
return;
|
|
788
810
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
dockSetFrame(frame, totalRows);
|
|
794
|
-
dockEnterDocked();
|
|
795
|
-
dockCaretBelow = cursorRow;
|
|
811
|
+
else {
|
|
812
|
+
dockEnterDocked();
|
|
813
|
+
dockCaretBelow = cursorRow;
|
|
814
|
+
}
|
|
796
815
|
}
|
|
797
816
|
/** Commit content at the box top, pushing the box down, preserving exactly one
|
|
798
817
|
* blank row between the content and the box. */
|
|
@@ -1375,6 +1394,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1375
1394
|
let draft = null;
|
|
1376
1395
|
let bracketedPaste = false;
|
|
1377
1396
|
let bracketedPasteBuffer = "";
|
|
1397
|
+
let bracketedPasteSince = 0;
|
|
1378
1398
|
let pasteBurst = false;
|
|
1379
1399
|
let pasteBurstTimer = null;
|
|
1380
1400
|
readline.emitKeypressEvents(process.stdin);
|
|
@@ -1409,6 +1429,8 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1409
1429
|
: value;
|
|
1410
1430
|
process.stdout.write("\x1b[?2004l");
|
|
1411
1431
|
process.stdin.setRawMode(false);
|
|
1432
|
+
bracketedPaste = false;
|
|
1433
|
+
bracketedPasteBuffer = "";
|
|
1412
1434
|
if (pasteBurstTimer)
|
|
1413
1435
|
clearTimeout(pasteBurstTimer);
|
|
1414
1436
|
if (isErr) {
|
|
@@ -1699,6 +1721,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1699
1721
|
const value = str || "";
|
|
1700
1722
|
if (!bracketedPaste && (sequence === start || value === start)) {
|
|
1701
1723
|
bracketedPaste = true;
|
|
1724
|
+
bracketedPasteSince = Date.now();
|
|
1702
1725
|
bracketedPasteBuffer = "";
|
|
1703
1726
|
const inline = value === start ? "" : value.startsWith(start) ? value.slice(start.length) : "";
|
|
1704
1727
|
if (inline)
|
|
@@ -1719,6 +1742,29 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1719
1742
|
}
|
|
1720
1743
|
if (!bracketedPaste)
|
|
1721
1744
|
return false;
|
|
1745
|
+
// Safety net: if the paste-end marker (\x1b[201~) was lost — interrupted
|
|
1746
|
+
// paste, split escape sequence, or a terminal quirk — bracketedPaste stays
|
|
1747
|
+
// true and every later keystroke is swallowed here, which looks like the
|
|
1748
|
+
// input field locked up (even Ctrl+C is captured as a paste chunk). If the
|
|
1749
|
+
// paste has been open far longer than any real paste takes and a fresh
|
|
1750
|
+
// single-printable-char keystroke arrives, flush the buffered text as
|
|
1751
|
+
// literal input and reset so typing works again.
|
|
1752
|
+
if (Date.now() - bracketedPasteSince > 3000 &&
|
|
1753
|
+
str &&
|
|
1754
|
+
str.length === 1 &&
|
|
1755
|
+
!key?.ctrl &&
|
|
1756
|
+
!key?.meta) {
|
|
1757
|
+
const pasted = bracketedPasteBuffer
|
|
1758
|
+
.replace(/\r\n/g, "\n")
|
|
1759
|
+
.replace(/\r/g, "\n");
|
|
1760
|
+
bracketedPaste = false;
|
|
1761
|
+
bracketedPasteBuffer = "";
|
|
1762
|
+
if (pasted) {
|
|
1763
|
+
insert(pasted, true);
|
|
1764
|
+
repaint();
|
|
1765
|
+
}
|
|
1766
|
+
return false; // let this keystroke be handled normally below
|
|
1767
|
+
}
|
|
1722
1768
|
if (sequence === end || value === end) {
|
|
1723
1769
|
const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1724
1770
|
bracketedPaste = false;
|