@oxecli/oxe 1.0.86 → 1.0.87

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/cli.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { loadOrPrompt, default_reasoning_effort, max_action_chars, max_resume_history_items, runtimeOsSummary, max_context_tokens, context_overhead_margin, } from "./config.js";
5
5
  import { InferenceEngine, estimateTokens } from "./engine.js";
6
6
  import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
7
- import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, plainLen, truncateStyled, terminalWidth, TOOL_RESULT_MAX_LINES, MAX_COMMAND_DISPLAY_CHARS, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
7
+ import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, plainLen, truncateStyled, terminalWidth, TOOL_RESULT_MAX_LINES, MAX_COMMAND_DISPLAY_CHARS, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, dockAppendContent, dockSetInactive, dockTearDown, } from "./ui.js";
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  const require = createRequire(import.meta.url);
10
10
  const packageInfo = require("../package.json");
@@ -64,7 +64,7 @@ export class CLI {
64
64
  process.stdout.write(`${labelLine}\n${lines.join("\n")}\n`);
65
65
  if (diff) {
66
66
  const diffLines = diff.split("\n").map((ln) => plainLen(ln) > lineMax ? truncateStyled(ln, lineMax) + "…\x1b[0m" : ln);
67
- process.stdout.write(diffLines.join("\n") + "\n\n");
67
+ process.stdout.write(diffLines.join("\n") + "\n");
68
68
  }
69
69
  return;
70
70
  }
@@ -91,6 +91,7 @@ export class CLI {
91
91
  process.stdout.write("\n");
92
92
  }
93
93
  async showHelp() {
94
+ dockTearDown();
94
95
  const rows = COMMAND_HELP.map(([cmd, desc]) => {
95
96
  const styled = cmd.replace(/(<[^>]+>)/g, "\x1b[36m$1\x1b[0m");
96
97
  return {
@@ -104,7 +105,7 @@ export class CLI {
104
105
  titleAlign: "left",
105
106
  colGap: 3,
106
107
  });
107
- process.stdout.write("\n" + panel + "\n");
108
+ process.stdout.write(panel + "\n");
108
109
  if (process.stdin.isTTY) {
109
110
  enableRawStdin();
110
111
  try {
@@ -117,7 +118,7 @@ export class CLI {
117
118
  disableRawStdin();
118
119
  }
119
120
  const n = panel.split("\n").length;
120
- process.stdout.write(`\x1b[${n + 1}A\r\x1b[J`);
121
+ process.stdout.write(`\x1b[${n}A\r\x1b[J`);
121
122
  }
122
123
  }
123
124
  async awaitRawCloseKey() {
@@ -309,7 +310,6 @@ export class CLI {
309
310
  async pickConversation() {
310
311
  const recs = listSessions();
311
312
  if (!recs.length) {
312
- process.stdout.write("\n");
313
313
  renderPanel("No saved conversations yet. Type a prompt to start one.", "Conversations");
314
314
  return null;
315
315
  }
@@ -318,10 +318,11 @@ export class CLI {
318
318
  return null;
319
319
  }
320
320
  enableRawStdin();
321
+ dockTearDown();
321
322
  let selected = 0;
322
323
  let lastRows = 0;
323
324
  const draw = () => {
324
- const frame = "\n" + this.renderPickerPanel(recs, selected);
325
+ const frame = this.renderPickerPanel(recs, selected);
325
326
  const n = frame.split("\n").length;
326
327
  if (lastRows > 0)
327
328
  process.stdout.write(`\x1b[${lastRows - 1}A\r\x1b[J`);
@@ -359,13 +360,11 @@ export class CLI {
359
360
  async resumeConversation(engine, num) {
360
361
  const sid = parseInt(num, 10);
361
362
  if (Number.isNaN(sid)) {
362
- process.stdout.write("\n");
363
363
  renderPanel(`Invalid conversation number: ${num}`, "Error", "", true, "31");
364
364
  return;
365
365
  }
366
366
  const rec = loadSession(sid);
367
367
  if (!rec) {
368
- process.stdout.write("\n");
369
368
  renderPanel(`Conversation ${sid} not found.`, "Error", "", true, "31");
370
369
  return;
371
370
  }
@@ -376,10 +375,11 @@ export class CLI {
376
375
  this.sessionId = sid;
377
376
  engine.reasoningEffort = String(rec["reasoning_effort"] ?? default_reasoning_effort);
378
377
  clearScreen();
378
+ dockSetInactive();
379
379
  this.renderHeader();
380
380
  process.stdout.write("\n");
381
381
  process.stdout.write(`\x1b[90mResumed:\x1b[0m \x1b[1m${truncateLabel(rec["label"])}\x1b[0m\n`);
382
- process.stdout.write(`\x1b[90m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n\n`);
382
+ process.stdout.write(`\x1b[90m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n`);
383
383
  renderPanel("Conversation history restored", "Restored");
384
384
  process.stdout.write("\n");
385
385
  if (this.story.length)
@@ -421,6 +421,7 @@ export class CLI {
421
421
  this.story = [];
422
422
  this.sessionId = null;
423
423
  clearScreen();
424
+ dockSetInactive();
424
425
  this.renderHeader();
425
426
  continue;
426
427
  }
@@ -442,15 +443,14 @@ export class CLI {
442
443
  const effort = parts[1]?.toLowerCase() ?? "";
443
444
  if (["none", "low", "high"].includes(effort)) {
444
445
  engine.reasoningEffort = effort;
445
- process.stdout.write(`\n\x1b[32m✓\x1b[0m \x1b[90mReasoning effort set to \x1b[1;36m${effort}\x1b[0m\x1b[90m.\x1b[0m\n`);
446
+ dockAppendContent(`\x1b[32m✓\x1b[0m \x1b[90mReasoning effort set to \x1b[1;36m${effort}\x1b[0m\x1b[90m.\x1b[0m\n`);
446
447
  }
447
448
  else {
448
- process.stdout.write("\n");
449
449
  renderPanel("Usage: /effort none|low|high", "Error", "", true, "31");
450
450
  }
451
451
  continue;
452
452
  }
453
- process.stdout.write(`\n\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n\n`);
453
+ dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
454
454
  queryStarted = true;
455
455
  await engine.executeQuery(input, this.inputItems, this.story, spans);
456
456
  queryStarted = false;
@@ -466,7 +466,7 @@ export class CLI {
466
466
  if (err?.message === "interrupt") {
467
467
  const wasActive = queryStarted || engine.inQuery;
468
468
  if (wasActive && !engine.queryHasOutput && !engine.queryCalledTool) {
469
- process.stdout.write(aiMarkdown("What else can I help you with?") + "\n");
469
+ dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
470
470
  }
471
471
  engine.inQuery = false;
472
472
  stripOrphanCalls(this.inputItems);
@@ -488,8 +488,7 @@ export class CLI {
488
488
  break;
489
489
  }
490
490
  this.interruptPending = true;
491
- process.stdout.write("\n");
492
- process.stdout.write("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
491
+ dockAppendContent("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
493
492
  continue;
494
493
  }
495
494
  throw err;
package/dist/engine.js CHANGED
@@ -2,7 +2,7 @@ import OpenAI from "openai";
2
2
  import { max_output_tokens, max_empty_retries, max_agent_steps, max_context_tokens, context_overhead_margin, compact_keep_recent_turns, max_summary_source_chars, max_read_file_stored_chars, } from "./config.js";
3
3
  import { getSystemPrompt } from "./system.js";
4
4
  import { buildTools, truncateToolOutput, toolReadFile, toolWriteFile, toolEditFile, toolBash, toolGlob, toolGrep, toolLoadSkill, } from "./tools.js";
5
- import { mutedMarkdown, aiMarkdown, tickDuration, displayRows, safeCommitPoint, printAiChunk, toolCallLabel, formatToolResult, renderPanel, Spinner, hideCursor, } from "./ui.js";
5
+ import { mutedMarkdown, aiMarkdown, tickDuration, safeCommitPoint, printAiChunk, toolCallLabel, formatToolResult, renderPanel, Spinner, hideCursor, dockAppendContent, dockReplaceTransient, dockTransientStart, } from "./ui.js";
6
6
  import { reportUsage } from "./api.js";
7
7
  import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
8
8
  import { takePendingDiffOutput } from "./tools.js";
@@ -43,8 +43,6 @@ export class InferenceEngine {
43
43
  reasoningEffort;
44
44
  keyData;
45
45
  client;
46
- workActive = false;
47
- workRows = 0;
48
46
  inQuery = false;
49
47
  activeAbort = null;
50
48
  interrupted = false;
@@ -146,22 +144,6 @@ export class InferenceEngine {
146
144
  incompleteReason(response) {
147
145
  return response?.incomplete_details?.reason ?? null;
148
146
  }
149
- overwriteWorkLine(text) {
150
- for (let i = 0; i < this.workRows; i++) {
151
- process.stdout.write("\x1b[1A\x1b[2K");
152
- }
153
- process.stdout.write(mutedMarkdown(text) + "\n\n");
154
- this.workRows = displayRows(mutedMarkdown(text)) + 1;
155
- }
156
- clearWorkLine() {
157
- if (!this.workActive || this.workRows < 1)
158
- return;
159
- for (let i = 0; i < this.workRows; i++) {
160
- process.stdout.write("\x1b[1A\x1b[2K");
161
- }
162
- this.workActive = false;
163
- this.workRows = 0;
164
- }
165
147
  async streamOnce(inputItems, stats, story, previousResponseId) {
166
148
  let content = "";
167
149
  let committedLen = 0;
@@ -169,6 +151,7 @@ export class InferenceEngine {
169
151
  let sawReasoning = false;
170
152
  let status = null;
171
153
  let thinkingStart = null;
154
+ dockTransientStart();
172
155
  const workStatus = new Spinner();
173
156
  const workingStarted = Date.now();
174
157
  workStatus.startWithRender(() => `Working ${tickDuration((Date.now() - workingStarted) / 1000)}`);
@@ -187,14 +170,7 @@ export class InferenceEngine {
187
170
  const elapsed = (Date.now() - workingStarted) / 1000;
188
171
  const t = tickDuration(elapsed);
189
172
  story.push({ type: "worked", text: `Worked for ${t}` });
190
- if (this.workActive) {
191
- this.overwriteWorkLine(`Worked for ${t}`);
192
- }
193
- else {
194
- process.stdout.write(mutedMarkdown(`Worked for ${t}`) + "\n\n");
195
- this.workActive = true;
196
- this.workRows = displayRows(mutedMarkdown(`Worked for ${t}`)) + 1;
197
- }
173
+ dockReplaceTransient(mutedMarkdown(`Worked for ${t}`) + "\n");
198
174
  };
199
175
  const finishThinking = (report) => {
200
176
  if (thinkingStart === null)
@@ -208,8 +184,7 @@ export class InferenceEngine {
208
184
  if (report) {
209
185
  const t = tickDuration(elapsed);
210
186
  story.push({ type: "thought", text: `Thought for ${t}` });
211
- process.stdout.write(mutedMarkdown(`Thought for ${t}`) + "\n\n");
212
- this.workActive = false;
187
+ dockReplaceTransient(mutedMarkdown(`Thought for ${t}`) + "\n");
213
188
  }
214
189
  thinkingStart = null;
215
190
  };
@@ -248,6 +223,7 @@ export class InferenceEngine {
248
223
  etype === "response.reasoning.summary.delta") {
249
224
  sawReasoning = true;
250
225
  silenceWorking();
226
+ dockTransientStart();
251
227
  if (thinkingStart === null) {
252
228
  thinkingStart = Date.now();
253
229
  status = new Spinner();
@@ -288,9 +264,7 @@ export class InferenceEngine {
288
264
  workStatus.stop();
289
265
  const stillThinking = thinkingStart !== null;
290
266
  finishThinking(stillThinking);
291
- if (this.interrupted && !this.queryHasOutput)
292
- this.clearWorkLine();
293
- else
267
+ if (!this.interrupted || this.queryHasOutput)
294
268
  reportWorking();
295
269
  if (content.slice(committedLen))
296
270
  printAiChunk(content.slice(committedLen));
@@ -398,6 +372,7 @@ export class InferenceEngine {
398
372
  if (!this.contextOverBudget(conversation))
399
373
  return [conversation, false];
400
374
  const started = Date.now();
375
+ dockTransientStart();
401
376
  const comp = new Spinner();
402
377
  comp.start(`Compacting conversation ${tickDuration(0)}`);
403
378
  const compTimer = setInterval(() => {
@@ -416,14 +391,11 @@ export class InferenceEngine {
416
391
  persistCompactionSummary(inputItems, compacted[0]);
417
392
  const text = `Compacted conversation in ${tickDuration((Date.now() - started) / 1000)}`;
418
393
  story.push({ type: "compacted", text });
419
- this.workActive = false;
420
- process.stdout.write(mutedMarkdown(text) + "\n\n");
394
+ dockReplaceTransient(mutedMarkdown(text) + "\n");
421
395
  return [compacted, true];
422
396
  }
423
397
  async executeQuery(userPrompt, inputItems, story, pasteSpans) {
424
398
  this.inQuery = true;
425
- this.workActive = false;
426
- this.workRows = 0;
427
399
  this.interrupted = false;
428
400
  this.queryHasOutput = false;
429
401
  this.queryCalledTool = false;
@@ -553,36 +525,24 @@ export class InferenceEngine {
553
525
  continue;
554
526
  }
555
527
  dropRetryPrompts(conversation, inputItems, retryPrompts);
556
- process.stdout.write(aiMarkdown(emptyMessage) + "\n");
557
- process.stdout.write("\n" + summary() + "\n");
558
- this.workActive = false;
559
- this.workRows = 0;
528
+ dockAppendContent(aiMarkdown(emptyMessage) + "\n");
529
+ dockAppendContent(summary() + "\n");
560
530
  return;
561
531
  }
562
532
  if (!calls.length) {
563
- // Exactly one blank row between the message and the footer summary.
564
- const trailing = (text.match(/\n+$/) ?? [""])[0].length;
565
- process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)) + summary() + "\n");
566
533
  story.push({ type: "footer", text: footerText() });
567
534
  attachFooter(conversation);
568
535
  attachFooter(inputItems);
569
- this.workActive = false;
570
- this.workRows = 0;
536
+ dockAppendContent(summary() + "\n");
571
537
  return;
572
538
  }
573
- if (text.trim()) {
574
- // Ensure exactly one blank row between the assistant message and the
575
- // first tool call, no matter how many newlines the message ended with.
576
- const trailing = (text.match(/\n+$/) ?? [""])[0].length;
577
- process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)));
578
- }
579
539
  for (const c of calls) {
580
- this.workActive = false;
581
540
  this.queryCalledTool = true;
582
541
  const started = toolCallLabel(c.name, c.arguments);
583
542
  // Print the tool label the instant the command starts, then show a
584
543
  // "╰─ Working..." dots line that swaps to the real result in place.
585
- process.stdout.write(`${started}\n`);
544
+ dockAppendContent(`${started}\n`);
545
+ dockTransientStart();
586
546
  const toolSpinner = new Spinner();
587
547
  toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
588
548
  const rawResult = await this.runTool(c.name, c.arguments);
@@ -602,12 +562,13 @@ export class InferenceEngine {
602
562
  });
603
563
  // Swap the "╰─ Working..." line for the real result. If the tool
604
564
  // produced a diff it renders directly below the action line with no
605
- // gap; otherwise a blank line separates the action from what follows.
606
- toolSpinner.replaceWith(action);
607
- if (diff)
608
- process.stdout.write(diff + "\n\n");
609
- else
610
- process.stdout.write("\n");
565
+ // gap; otherwise the dock gap separates the action from what follows.
566
+ toolSpinner.stop();
567
+ dockReplaceTransient(action + "\n");
568
+ if (diff) {
569
+ dockTransientStart();
570
+ dockReplaceTransient(diff + "\n");
571
+ }
611
572
  const truncatedResult = c.name === "read_file"
612
573
  ? truncateToolOutput(rawResult, max_read_file_stored_chars)
613
574
  : truncateToolOutput(rawResult);
@@ -634,10 +595,6 @@ export class InferenceEngine {
634
595
  stripOrphanCalls(inputItems);
635
596
  }
636
597
  catch (err) {
637
- if (this.interrupted) {
638
- this.workActive = false;
639
- this.workRows = 0;
640
- }
641
598
  if (this.interrupted)
642
599
  throw new Error("interrupt");
643
600
  if (err?.message === "interrupt" || err?.message === "eof") {
package/dist/ui.js CHANGED
@@ -411,7 +411,7 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
411
411
  return out.join("\n");
412
412
  }
413
413
  export function renderPanel(content, title = "", subtitle = "", expand = true, borderStyle = "90", titleAlign = "center") {
414
- process.stdout.write(panelString(content, title, subtitle, expand, borderStyle, titleAlign) + "\n");
414
+ dockAppendContent(panelString(content, title, subtitle, expand, borderStyle, titleAlign) + "\n");
415
415
  }
416
416
  // ---------------------------------------------------------------------------
417
417
  // Table Panel Rendering
@@ -462,7 +462,7 @@ export function tickDuration(seconds) {
462
462
  export function printAiChunk(chunk) {
463
463
  if (!chunk)
464
464
  return;
465
- process.stdout.write(markdownToAnsi(chunk));
465
+ dockAppendContent(markdownToAnsi(chunk));
466
466
  }
467
467
  const FENCE_MARKER_RE = /`{3,}/g;
468
468
  export function safeCommitPoint(text) {
@@ -497,6 +497,134 @@ export function displayRows(text) {
497
497
  return rows;
498
498
  }
499
499
  // ---------------------------------------------------------------------------
500
+ // Persistent Prompt Dock
501
+ // ---------------------------------------------------------------------------
502
+ // The prompt box stays on screen at all times (docked). The cursor is parked
503
+ // at the box's TOP row; incoming content is written there and the box is
504
+ // re-rendered below it, so the box is pushed down by content while exactly
505
+ // one blank row is preserved between the latest content and the box.
506
+ //
507
+ // dockRel: how many rows the cursor sits above boxTop. 0 = parked at boxTop,
508
+ // 1 = one row up (the gap row), where transient spinners render.
509
+ // dockGap: true when a blank row already exists above the current row (after
510
+ // the box is torn down), so askBottomPrompt does not add a second one.
511
+ let docked = false;
512
+ let dockRel = 0;
513
+ let dockBoxRows = 0;
514
+ let dockGap = false;
515
+ let dockFrameLines = [];
516
+ export function dockIsDocked() {
517
+ return docked;
518
+ }
519
+ export function dockHasGap() {
520
+ return dockGap;
521
+ }
522
+ export function dockSetFrame(frame, totalRows) {
523
+ dockFrameLines = frame.split("\n");
524
+ dockBoxRows = totalRows;
525
+ }
526
+ export function dockSetInactive() {
527
+ docked = false;
528
+ dockRel = 0;
529
+ dockBoxRows = 0;
530
+ dockGap = false;
531
+ dockFrameLines = [];
532
+ }
533
+ export function dockEnterDocked() {
534
+ docked = true;
535
+ dockRel = 0;
536
+ dockGap = false;
537
+ }
538
+ /** Erase the docked box and leave the cursor at boxTop with the gap above it,
539
+ * so a pager/replay can write there and a later prompt re-docks cleanly. */
540
+ export function dockTearDown() {
541
+ const wasDocked = docked;
542
+ dockEraseBox();
543
+ if (wasDocked) {
544
+ dockGap = true;
545
+ dockBoxRows = 0;
546
+ dockFrameLines = [];
547
+ }
548
+ }
549
+ /** Park the cursor on the gap row (above the box) for a transient spinner. */
550
+ export function dockTransientStart() {
551
+ if (!docked || dockBoxRows < 1)
552
+ return;
553
+ if (dockRel === 0) {
554
+ process.stdout.write("\x1b[1A\r");
555
+ dockRel = 1;
556
+ }
557
+ }
558
+ function dockEraseBox() {
559
+ if (!docked || dockBoxRows < 1)
560
+ return;
561
+ process.stdout.write(`\x1b[${dockBoxRows - 1 + dockRel}B`);
562
+ for (let i = 0; i < dockBoxRows; i++) {
563
+ process.stdout.write("\r\x1b[K");
564
+ if (i < dockBoxRows - 1)
565
+ process.stdout.write("\x1b[1A");
566
+ }
567
+ docked = false;
568
+ dockRel = 0;
569
+ dockGap = true;
570
+ }
571
+ function dockEraseBoxKeep() {
572
+ if (!docked || dockBoxRows < 1)
573
+ return;
574
+ process.stdout.write(`\x1b[${dockBoxRows - 1 + dockRel}B`);
575
+ for (let i = 0; i < dockBoxRows; i++) {
576
+ process.stdout.write("\r\x1b[K");
577
+ if (i < dockBoxRows - 1)
578
+ process.stdout.write("\x1b[1A");
579
+ }
580
+ process.stdout.write(`\x1b[${dockRel}A`);
581
+ docked = false;
582
+ dockRel = 0;
583
+ dockGap = true;
584
+ }
585
+ /** Write exactly one blank row then the box frame below the current row, and
586
+ * park the cursor at the box top. */
587
+ function dockRenderBox() {
588
+ if (dockBoxRows < 1)
589
+ return;
590
+ process.stdout.write("\n\n" + dockFrameLines.join("\n"));
591
+ process.stdout.write(`\x1b[${dockBoxRows - 1}A\r`);
592
+ dockRel = 0;
593
+ docked = true;
594
+ dockGap = false;
595
+ }
596
+ /** Commit content at the box top, pushing the box down, preserving exactly one
597
+ * blank row between the content and the box. */
598
+ export function dockAppendContent(text) {
599
+ if (!text)
600
+ return;
601
+ if (!docked) {
602
+ process.stdout.write(text);
603
+ return;
604
+ }
605
+ dockEraseBox();
606
+ process.stdout.write(text);
607
+ const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
608
+ if (tb > 0)
609
+ process.stdout.write(`\x1b[${tb}A`);
610
+ dockRenderBox();
611
+ }
612
+ /** Replace a transient spinner row (the gap row) with content, then re-dock. */
613
+ export function dockReplaceTransient(text) {
614
+ if (!text)
615
+ return;
616
+ if (!docked) {
617
+ process.stdout.write(text);
618
+ return;
619
+ }
620
+ dockEraseBoxKeep();
621
+ process.stdout.write(text);
622
+ const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
623
+ if (tb > 0)
624
+ process.stdout.write(`\x1b[${tb}A`);
625
+ dockRenderBox();
626
+ }
627
+ // ---------------------------------------------------------------------------
500
628
  // Cursor & Spinner
501
629
  // ---------------------------------------------------------------------------
502
630
  let cursorHidden = false;
@@ -1060,7 +1188,15 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1060
1188
  process.stdin.setRawMode(false);
1061
1189
  if (pasteBurstTimer)
1062
1190
  clearTimeout(pasteBurstTimer);
1063
- clearBox();
1191
+ if (isErr) {
1192
+ clearBox();
1193
+ dockSetInactive();
1194
+ }
1195
+ else {
1196
+ process.stdout.write(`\x1b[${lastCursorRow}A\r`);
1197
+ dockSetFrame((prevFrameLines ?? []).join("\n"), lastTotalRows);
1198
+ dockEnterDocked();
1199
+ }
1064
1200
  process.stdin.removeListener("keypress", onKeypress);
1065
1201
  process.stdin.pause();
1066
1202
  showCursor();
@@ -1073,7 +1209,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1073
1209
  const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints);
1074
1210
  const newLines = frame.split("\n");
1075
1211
  if (isFirst) {
1076
- process.stdout.write("\n");
1212
+ if (dockIsDocked()) {
1213
+ dockEraseBox();
1214
+ }
1215
+ else if (!dockHasGap()) {
1216
+ process.stdout.write("\n");
1217
+ }
1077
1218
  for (let i = 0; i < newLines.length; i++) {
1078
1219
  process.stdout.write(newLines[i]);
1079
1220
  if (i < newLines.length - 1)
@@ -1082,6 +1223,8 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1082
1223
  prevFrameLines = newLines;
1083
1224
  lastCursorRow = cursorRow;
1084
1225
  lastTotalRows = totalRows;
1226
+ dockSetFrame(frame, totalRows);
1227
+ dockEnterDocked();
1085
1228
  const up = totalRows - 1 - cursorRow;
1086
1229
  process.stdout.write(`\x1b[${up}A\r\x1b[${cursorCol}C`);
1087
1230
  }
@@ -1105,6 +1248,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1105
1248
  prevFrameLines = newLines;
1106
1249
  lastCursorRow = cursorRow;
1107
1250
  lastTotalRows = totalRows;
1251
+ dockSetFrame(frame, totalRows);
1108
1252
  }
1109
1253
  };
1110
1254
  const shiftSpansAfterInsert = (pos, delta) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.86",
3
+ "version": "1.0.87",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },