@oxecli/oxe 1.0.111 → 1.0.113

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
@@ -11,10 +11,6 @@ import { takePendingDiffOutput } from "./tools.js";
11
11
  // ---------------------------------------------------------------------------
12
12
  const tokenEstCache = new Map();
13
13
  const tokenEstCacheMax = 4096;
14
- /** Minimum time (ms) a tool's "Working"/"Running" status stays visible after
15
- * the tool returns, so instant tools (write_file/edit_file) are still seen to
16
- * stream when called before the result replaces the status line. */
17
- const MIN_TOOL_STATUS_MS = 350;
18
14
  export function estimateTokens(items) {
19
15
  let total = 0;
20
16
  for (const item of items) {
@@ -596,7 +592,6 @@ export class InferenceEngine {
596
592
  dockTransientStart("flush");
597
593
  const toolSpinner = new Spinner();
598
594
  toolSpinner.startDots(`\x1b[90m╰─\x1b[0m ${c.name === "bash" ? "Running" : "Working"}`);
599
- const t0 = Date.now();
600
595
  const rawResult = await this.runTool(c.name, c.arguments, this.activeAbort?.signal);
601
596
  // A tool call was made, which interrupts any current working phase,
602
597
  // so the next agent iteration may commit a fresh "Worked for …" block.
@@ -605,14 +600,6 @@ export class InferenceEngine {
605
600
  toolSpinner.stop();
606
601
  throw new Error("interrupt");
607
602
  }
608
- // Keep the status line streaming for at least MIN_TOOL_STATUS_MS so
609
- // even instant tools (write_file/edit_file) visibly show their
610
- // "Working" indicator when called, then swap to the result. The
611
- // spinner's own timer animates the dots during this window.
612
- const elapsed = Date.now() - t0;
613
- if (elapsed < MIN_TOOL_STATUS_MS) {
614
- await new Promise((res) => setTimeout(res, MIN_TOOL_STATUS_MS - elapsed));
615
- }
616
603
  const failed = toolOutputFailed(c.name, rawResult);
617
604
  const action = formatToolResult(rawResult, failed);
618
605
  const diff = takePendingDiffOutput();
package/dist/ui.js CHANGED
@@ -173,6 +173,19 @@ export function truncateStyled(text, maxVisible) {
173
173
  }
174
174
  return out;
175
175
  }
176
+ /**
177
+ * Truncate styled text to at most `maxVisible` visible characters and, if
178
+ * truncated, append a visible ellipsis "…" so the cut is obvious. Used by the
179
+ * panel frame so long titles/subtitles shrink to the box instead of overflowing.
180
+ */
181
+ export function truncateStyledEllipsis(text, maxVisible) {
182
+ if (maxVisible <= 0)
183
+ return "";
184
+ if (plainLen(text) <= maxVisible)
185
+ return text;
186
+ const dot = maxVisible > 1 ? "…" : "";
187
+ return truncateStyled(text, Math.max(1, maxVisible - (dot ? 1 : 0))) + dot;
188
+ }
176
189
  /**
177
190
  * Wrap a styled (ANSI) string into lines of at most `width` visible characters,
178
191
  * wrapping whole words onto new lines and splitting any single word longer than
@@ -563,13 +576,17 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
563
576
  const embed = (text, align) => {
564
577
  if (!text)
565
578
  return "─".repeat(Math.max(boxW - 2, 0));
566
- const inner = ` ${text} `;
579
+ // Truncate the title/subtitle so the frame always scales down to the box
580
+ // width (matching the prompt field): a long subtitle used to overflow the
581
+ // bottom border on narrow/scaled terminals. Ellipsis marks the cut.
582
+ const prefix = align === "left" ? "───" : "";
583
+ const avail = Math.max(boxW - 2 - plainLen(prefix), 0);
584
+ const inner = ` ${truncateStyledEllipsis(text, Math.max(avail - 2, 0))} `;
567
585
  if (align === "left") {
568
- const prefix = "───";
569
- const fill = Math.max(boxW - 2 - plainLen(prefix) - plainLen(inner), 0);
586
+ const fill = Math.max(avail - plainLen(inner), 0);
570
587
  return `${prefix}${inner}${"─".repeat(fill)}`;
571
588
  }
572
- const fill = Math.max(boxW - 2 - plainLen(inner), 0);
589
+ const fill = Math.max(avail - plainLen(inner), 0);
573
590
  const left = Math.floor(fill / 2);
574
591
  const right = fill - left;
575
592
  return `${"─".repeat(left)}${inner}${"─".repeat(right)}`;
@@ -1232,6 +1249,17 @@ export const PROMPT_PLACEHOLDER = "Describe a coding task, or type /help for com
1232
1249
  const MAX_PROMPT_DISPLAY_LINES = 12;
1233
1250
  const MAX_PROMPT_PASTE_CHARS = 400;
1234
1251
  const MAX_PROMPT_CHARS = 500;
1252
+ /**
1253
+ * Watchdog for bracketed paste. Some terminals (notably Windows Terminal /
1254
+ * conpty) occasionally drop the paste-END marker, which would leave the field
1255
+ * permanently capturing keystrokes (a "locked" prompt). If a paste-start
1256
+ * arrives but no paste-end AND no further paste data shows up within this
1257
+ * window, we treat the paste as aborted, release capture, and keep whatever
1258
+ * text already arrived so input is never lost. The window resets on every
1259
+ * received paste chunk, so a genuine (continuously-streaming) paste is never
1260
+ * cut short.
1261
+ */
1262
+ const PASTE_GUARD_MS = 3000;
1235
1263
  function shouldCollapsePaste(text) {
1236
1264
  return (text.split("\n").length > MAX_PROMPT_DISPLAY_LINES ||
1237
1265
  text.length > MAX_PROMPT_PASTE_CHARS);
@@ -1538,6 +1566,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1538
1566
  let draft = null;
1539
1567
  let bracketedPaste = false;
1540
1568
  let bracketedPasteBuffer = "";
1569
+ let pasteGuardTimer = null;
1541
1570
  let pasteBurst = false;
1542
1571
  let pasteBurstTimer = null;
1543
1572
  readline.emitKeypressEvents(process.stdin);
@@ -1576,6 +1605,10 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1576
1605
  bracketedPasteBuffer = "";
1577
1606
  if (pasteBurstTimer)
1578
1607
  clearTimeout(pasteBurstTimer);
1608
+ if (pasteGuardTimer) {
1609
+ clearTimeout(pasteGuardTimer);
1610
+ pasteGuardTimer = null;
1611
+ }
1579
1612
  if (isErr) {
1580
1613
  clearBox();
1581
1614
  dockSetInactive();
@@ -1881,17 +1914,46 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1881
1914
  const end = "\x1b[201~";
1882
1915
  const sequence = key?.sequence || "";
1883
1916
  const value = str || "";
1917
+ // A paste-end marker must close the capture. If it never arrives and no
1918
+ // more paste data follows, the guard below auto-releases the field so it
1919
+ // can't lock. (definitions hoisted below)
1920
+ const armPasteGuard = () => {
1921
+ if (pasteGuardTimer)
1922
+ clearTimeout(pasteGuardTimer);
1923
+ pasteGuardTimer = setTimeout(() => {
1924
+ pasteGuardTimer = null;
1925
+ if (!bracketedPaste)
1926
+ return;
1927
+ // The terminal dropped the paste-end (a Windows/conpty failure). Bail
1928
+ // out of capture mode and insert whatever already arrived so nothing
1929
+ // typed afterward is swallowed.
1930
+ bracketedPaste = false;
1931
+ const leftover = bracketedPasteBuffer;
1932
+ bracketedPasteBuffer = "";
1933
+ if (leftover)
1934
+ insert(leftover, true);
1935
+ repaint();
1936
+ }, PASTE_GUARD_MS);
1937
+ };
1938
+ const disarmPasteGuard = () => {
1939
+ if (pasteGuardTimer) {
1940
+ clearTimeout(pasteGuardTimer);
1941
+ pasteGuardTimer = null;
1942
+ }
1943
+ };
1884
1944
  // A new paste-start while already inside a paste means the previous paste
1885
1945
  // was aborted (its end marker never arrived). Reset and start over so we
1886
1946
  // never swallow subsequent typing.
1887
1947
  if ((key?.name === "paste-start" || sequence === start || value === start)) {
1888
1948
  bracketedPaste = true;
1889
1949
  bracketedPasteBuffer = "";
1950
+ armPasteGuard();
1890
1951
  const inline = value === start ? "" : value.startsWith(start) ? value.slice(start.length) : "";
1891
1952
  if (inline)
1892
1953
  bracketedPasteBuffer = inline;
1893
1954
  if (inline.includes(end)) {
1894
1955
  const pasted = inline.slice(0, inline.indexOf(end)).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1956
+ disarmPasteGuard();
1895
1957
  bracketedPaste = false;
1896
1958
  bracketedPasteBuffer = "";
1897
1959
  if (pasted)
@@ -1909,6 +1971,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1909
1971
  if (key?.name === "paste-end" || sequence === end || value === end || endAt >= 0) {
1910
1972
  bracketedPasteBuffer += endAt >= 0 ? value.slice(0, endAt) : "";
1911
1973
  const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1974
+ disarmPasteGuard();
1912
1975
  bracketedPaste = false;
1913
1976
  bracketedPasteBuffer = "";
1914
1977
  if (pasted)
@@ -1917,8 +1980,10 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1917
1980
  return true;
1918
1981
  }
1919
1982
  const chunk = value || (sequence && !sequence.startsWith("\x1b[") ? sequence : "");
1920
- if (chunk)
1983
+ if (chunk) {
1921
1984
  bracketedPasteBuffer += chunk;
1985
+ armPasteGuard();
1986
+ }
1922
1987
  return true;
1923
1988
  };
1924
1989
  const markPasteBurst = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.111",
3
+ "version": "1.0.113",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },