@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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/ui.js +78 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,9 @@ Install Oxe globally with npm:
20
20
 
21
21
 
22
22
 
23
- Features
23
+ **Features**
24
+
25
+
24
26
 
25
27
  * Terminal-native — Work directly from your CLI.
26
28
  * Codebase-aware — Inspect and understand your project before making changes.
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 on the gap row directly under the preceding content (adjacent).
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[1A\r");
676
- dockRel = 1;
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. Erases the box, writes the new frame, and parks the
754
- * cursor at the caret. Preserves exactly one blank row between the last content
755
- * and the box. When a transient (thinking/working spinner) is parked above the
756
- * box, it is rebuilt above the new frame and the physical cursor stays on that
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
- dockEraseBox();
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
- process.stdout.write(frame);
790
- // Park the cursor at the caret inside the box (cursorRow is 1-indexed, so
791
- // the caret sits cursorRow rows below boxTop).
792
- process.stdout.write(`\x1b[${totalRows - 1 - cursorRow}A\r\x1b[${cursorCol}C`);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },