@oxecli/oxe 1.0.6 → 1.0.8

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
@@ -15,6 +15,7 @@ export class CLI {
15
15
  story = [];
16
16
  sessionId = null;
17
17
  promptHistory = [];
18
+ interruptPending = false;
18
19
  constructor() {
19
20
  clearScreen();
20
21
  renderPanel("AI CODING AGENT", "", "Engine: Ready", false);
@@ -107,7 +108,8 @@ export class CLI {
107
108
  if (footer) {
108
109
  if (!footer.startsWith("("))
109
110
  footer = `(${footer})`;
110
- process.stdout.write("\n" + mutedMarkdown(footer) + "\n");
111
+ // Footer: labels white, only the backtick values grey (not the whole line).
112
+ process.stdout.write("\n" + aiMarkdown(footer) + "\n");
111
113
  }
112
114
  }
113
115
  else {
@@ -138,6 +140,10 @@ export class CLI {
138
140
  else if (typ === "tool") {
139
141
  this.printToolEntry(e["started"], text, e["status"] ?? "");
140
142
  }
143
+ else if (typ === "footer") {
144
+ // Footer: labels white, only the backtick values grey (not the whole line).
145
+ process.stdout.write(aiMarkdown(text) + "\n");
146
+ }
141
147
  else {
142
148
  process.stdout.write(mutedMarkdown(text) + "\n");
143
149
  }
@@ -210,6 +216,7 @@ export class CLI {
210
216
  const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory);
211
217
  if (!input.trim())
212
218
  continue;
219
+ this.interruptPending = false;
213
220
  this.promptHistory.push(input);
214
221
  const lower = input.trim().toLowerCase();
215
222
  if (lower === "/exit" || lower === "/quit") {
@@ -260,12 +267,35 @@ export class CLI {
260
267
  this.saveSession(engine);
261
268
  }
262
269
  catch (err) {
263
- if (err?.message === "interrupt" || err?.message === "eof") {
270
+ if (err?.message === "eof") {
264
271
  this.saveSession(engine);
265
272
  await engine.cleanupStoredResponses();
266
- process.stdout.write("\nClosing terminal session. Goodbye!\n");
273
+ process.stdout.write("\nSession closing via exit interrupt hook.\n");
267
274
  break;
268
275
  }
276
+ if (err?.message === "interrupt") {
277
+ const wasActive = engine.inQuery;
278
+ engine.inQuery = false;
279
+ // Drop the pending user message/story entry that was never run.
280
+ if (this.inputItems.length && this.inputItems[this.inputItems.length - 1]["role"] === "user") {
281
+ this.inputItems.pop();
282
+ }
283
+ if (this.story.length && this.story[this.story.length - 1]["type"] === "user") {
284
+ this.story.pop();
285
+ }
286
+ if (this.interruptPending) {
287
+ this.saveSession(engine);
288
+ await engine.cleanupStoredResponses();
289
+ process.stdout.write("\nClosing terminal session. Goodbye!\n");
290
+ break;
291
+ }
292
+ this.interruptPending = true;
293
+ process.stdout.write("\n");
294
+ process.stdout.write(wasActive
295
+ ? "\x1b[2mInterrupted. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n"
296
+ : "\x1b[2mNo active response. Press Ctrl+C again to exit.\x1b[0m\n");
297
+ continue;
298
+ }
269
299
  throw err;
270
300
  }
271
301
  }
package/dist/config.js CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { Spinner } from "./ui.js";
5
6
  // ---------------------------------------------------------------------------
6
7
  // Paths
7
8
  // ---------------------------------------------------------------------------
@@ -162,8 +163,10 @@ export async function loadOrPrompt() {
162
163
  }
163
164
  }
164
165
  process.stdout.write("\n");
165
- process.stdout.write(markupToAnsi("[dim]Authenticating key with Oxe Cloud…[/dim]") + "\n");
166
+ const authSpinner = new Spinner();
167
+ authSpinner.start("Authenticating key with Oxe Cloud…");
166
168
  const validation = await validateOxeApiKey(api_key);
169
+ authSpinner.stop();
167
170
  if (validation.valid) {
168
171
  key_data = validation.key_data || {};
169
172
  process.stdout.write("\n");
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 { SYSTEM_PROMPT } from "./system.js";
4
4
  import { buildTools, truncateToolOutput, TOOL_IMPLEMENTATIONS } from "./tools.js";
5
- import { mutedMarkdown, aiMarkdown, tickDuration, safeCommitPoint, printAiChunk, formatToolAction, } from "./ui.js";
5
+ import { mutedMarkdown, aiMarkdown, tickDuration, displayRows, safeCommitPoint, printAiChunk, formatToolAction, renderPanel, Spinner, hideCursor, showCursor, } from "./ui.js";
6
6
  import { reportUsage } from "./api.js";
7
7
  import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
8
8
  // ---------------------------------------------------------------------------
@@ -35,30 +35,6 @@ const objectId = (() => {
35
35
  };
36
36
  })();
37
37
  // ---------------------------------------------------------------------------
38
- // Simple status line helper
39
- // ---------------------------------------------------------------------------
40
- class Status {
41
- text;
42
- timer = null;
43
- started;
44
- constructor(initial) {
45
- this.text = initial;
46
- this.started = Date.now();
47
- }
48
- start() {
49
- process.stdout.write("\r\x1b[2K" + mutedMarkdown(this.text) + "\n");
50
- }
51
- update(text) {
52
- this.text = text;
53
- process.stdout.write("\r\x1b[2K" + mutedMarkdown(text) + "\x1b[1A");
54
- }
55
- stop() {
56
- if (this.timer)
57
- clearInterval(this.timer);
58
- this.timer = null;
59
- }
60
- }
61
- // ---------------------------------------------------------------------------
62
38
  // InferenceEngine
63
39
  // ---------------------------------------------------------------------------
64
40
  export class InferenceEngine {
@@ -66,6 +42,10 @@ export class InferenceEngine {
66
42
  reasoningEffort;
67
43
  keyData;
68
44
  client;
45
+ // "Worked for Xs" line is overwritten in place across tool iterations
46
+ // (mirrors Python's _work_active / _overwrite_work_line).
47
+ workActive = false;
48
+ workRows = 2;
69
49
  inQuery = false;
70
50
  temperature;
71
51
  storedResponseIds = [];
@@ -137,28 +117,70 @@ export class InferenceEngine {
137
117
  incompleteReason(response) {
138
118
  return response?.incomplete_details?.reason ?? null;
139
119
  }
120
+ /** Replace the previously-printed "Worked for Xs" line in place. */
121
+ overwriteWorkLine(text) {
122
+ for (let i = 0; i < this.workRows; i++) {
123
+ process.stdout.write("\x1b[1A\x1b[2K");
124
+ }
125
+ process.stdout.write(mutedMarkdown(text) + "\n\n");
126
+ this.workRows = displayRows(text) + 1;
127
+ }
140
128
  async streamOnce(inputItems, stats, story, previousResponseId) {
141
129
  let content = "";
142
130
  let committedLen = 0;
143
131
  let pending = "";
144
132
  let sawReasoning = false;
145
133
  let status = null;
146
- const thinkingStart = Date.now();
147
- const workStatus = new Status("Working for `0s`");
148
- workStatus.start();
134
+ let thinkingStart = null;
135
+ const workStatus = new Spinner();
136
+ const workingStarted = Date.now();
137
+ workStatus.start("Working for `0s`");
138
+ const workTimer = setInterval(() => {
139
+ workStatus.update(`Working for ${tickDuration((Date.now() - workingStarted) / 1000)}`);
140
+ }, 500);
141
+ let workingReported = false;
149
142
  let response = null;
150
143
  let etype = null;
144
+ const silenceWorking = () => {
145
+ clearInterval(workTimer);
146
+ workStatus.stop();
147
+ // Reasoning took over; suppress any later "Worked for Xs" report for this
148
+ // stream (mirrors Python's working_started = None in silence_working()).
149
+ workingReported = true;
150
+ };
151
+ const reportWorking = () => {
152
+ clearInterval(workTimer);
153
+ workStatus.stop();
154
+ if (workingReported)
155
+ return;
156
+ workingReported = true;
157
+ const elapsed = (Date.now() - workingStarted) / 1000;
158
+ const t = tickDuration(elapsed);
159
+ story.push({ type: "worked", text: `Worked for ${t}` });
160
+ if (this.workActive) {
161
+ this.overwriteWorkLine(`Worked for ${t}`);
162
+ }
163
+ else {
164
+ process.stdout.write(mutedMarkdown(`Worked for ${t}`) + "\n\n");
165
+ this.workActive = true;
166
+ this.workRows = displayRows(`Worked for ${t}`) + 1;
167
+ }
168
+ };
151
169
  const finishThinking = (report) => {
170
+ if (thinkingStart === null)
171
+ return;
172
+ const elapsed = (Date.now() - thinkingStart) / 1000;
152
173
  if (status) {
153
174
  status.stop();
154
175
  status = null;
155
176
  }
156
177
  if (report) {
157
- const elapsed = (Date.now() - thinkingStart) / 1000;
158
178
  const t = tickDuration(elapsed);
159
179
  story.push({ type: "thought", text: `Thought for ${t}` });
160
- process.stdout.write("\n" + mutedMarkdown(`Thought for ${t}`) + "\n");
180
+ process.stdout.write(mutedMarkdown(`Thought for ${t}`) + "\n\n");
181
+ this.workActive = false;
161
182
  }
183
+ thinkingStart = null;
162
184
  };
163
185
  try {
164
186
  const kwargs = {
@@ -190,13 +212,14 @@ export class InferenceEngine {
190
212
  if (etype === "response.reasoning_text.delta" ||
191
213
  etype === "response.reasoning.summary.delta") {
192
214
  sawReasoning = true;
193
- if (!status) {
194
- status = new Status("Thinking for `0s`");
195
- status.start();
215
+ silenceWorking();
216
+ if (thinkingStart === null) {
217
+ thinkingStart = Date.now();
218
+ status = new Spinner();
219
+ status.start("Thinking for `0s`");
196
220
  }
197
221
  else {
198
- const elapsed = (Date.now() - thinkingStart) / 1000;
199
- status.update(`Thinking for ${tickDuration(elapsed)}`);
222
+ status?.update(`Thinking for ${tickDuration((Date.now() - thinkingStart) / 1000)}`);
200
223
  }
201
224
  }
202
225
  else if (etype === "response.reasoning_text.done" ||
@@ -204,6 +227,7 @@ export class InferenceEngine {
204
227
  finishThinking(true);
205
228
  }
206
229
  else if (etype === "response.output_text.delta") {
230
+ reportWorking();
207
231
  finishThinking(true);
208
232
  content += event.delta;
209
233
  pending += event.delta;
@@ -224,8 +248,13 @@ export class InferenceEngine {
224
248
  }
225
249
  }
226
250
  finally {
251
+ clearInterval(workTimer);
227
252
  workStatus.stop();
228
- finishThinking(true);
253
+ // If the stream ended while still reasoning (no *.done event), still report
254
+ // the elapsed "Thought for Xs" (mirrors Python's `still_thinking` handling).
255
+ const stillThinking = thinkingStart !== null;
256
+ finishThinking(stillThinking);
257
+ reportWorking();
229
258
  if (content.slice(committedLen))
230
259
  printAiChunk(content.slice(committedLen));
231
260
  }
@@ -332,13 +361,17 @@ export class InferenceEngine {
332
361
  if (!this.contextOverBudget(conversation))
333
362
  return [conversation, false];
334
363
  const started = Date.now();
335
- const comp = new Status("Compacting conversation for `0s`");
336
- comp.start();
364
+ const comp = new Spinner();
365
+ comp.start("Compacting conversation for `0s`");
366
+ const compTimer = setInterval(() => {
367
+ comp.update(`Compacting conversation for ${tickDuration((Date.now() - started) / 1000)}`);
368
+ }, 500);
337
369
  let compacted;
338
370
  try {
339
371
  compacted = await this.compactHistory(conversation);
340
372
  }
341
373
  finally {
374
+ clearInterval(compTimer);
342
375
  comp.stop();
343
376
  }
344
377
  if (!compacted)
@@ -346,11 +379,14 @@ export class InferenceEngine {
346
379
  persistCompactionSummary(inputItems, compacted[0]);
347
380
  const text = `Compacted conversation in ${tickDuration((Date.now() - started) / 1000)}`;
348
381
  story.push({ type: "compacted", text });
349
- process.stdout.write("\n" + mutedMarkdown(text) + "\n");
382
+ this.workActive = false;
383
+ process.stdout.write(mutedMarkdown(text) + "\n");
384
+ process.stdout.write("\n");
350
385
  return [compacted, true];
351
386
  }
352
387
  async executeQuery(userPrompt, inputItems, story, pasteSpans) {
353
388
  this.inQuery = true;
389
+ hideCursor();
354
390
  inputItems.push({
355
391
  role: "user",
356
392
  content: userPrompt,
@@ -364,7 +400,7 @@ export class InferenceEngine {
364
400
  const stats = { thinking: 0, tokens: 0, reasoning: 0, input: 0 };
365
401
  const queryStart = Date.now();
366
402
  const footerText = () => `(Thought for ${tickDuration(stats["thinking"])} · Worked for ${tickDuration((Date.now() - queryStart) / 1000)} · Used \`${stats["tokens"].toLocaleString()}\` tokens)`;
367
- const summary = () => mutedMarkdown(footerText());
403
+ const summary = () => aiMarkdown(footerText());
368
404
  const attachFooter = (items) => {
369
405
  const footer = footerText();
370
406
  for (let i = items.length - 1; i >= 0; i--) {
@@ -384,7 +420,6 @@ export class InferenceEngine {
384
420
  };
385
421
  let prevId = null;
386
422
  let pending = conversation;
387
- let resetInQuery = true;
388
423
  try {
389
424
  for (let step = 0; step < max_agent_steps; step++) {
390
425
  let text = "";
@@ -418,13 +453,11 @@ export class InferenceEngine {
418
453
  respId = res.responseId;
419
454
  }
420
455
  catch (err2) {
421
- process.stdout.write("\n");
422
456
  renderErrorPanel(`API Error: ${err2}`);
423
457
  return;
424
458
  }
425
459
  }
426
460
  else {
427
- process.stdout.write("\n");
428
461
  renderErrorPanel(`API Error: ${err}`);
429
462
  return;
430
463
  }
@@ -458,7 +491,7 @@ export class InferenceEngine {
458
491
  continue;
459
492
  }
460
493
  dropRetryPrompts(conversation, inputItems, retryPrompts);
461
- process.stdout.write("\n" + aiMarkdown(emptyMessage) + "\n");
494
+ process.stdout.write(aiMarkdown(emptyMessage) + "\n");
462
495
  process.stdout.write("\n" + summary() + "\n");
463
496
  return;
464
497
  }
@@ -472,6 +505,7 @@ export class InferenceEngine {
472
505
  if (text.trim())
473
506
  process.stdout.write("\n");
474
507
  for (const c of calls) {
508
+ this.workActive = false;
475
509
  const started = formatToolAction(c.name, c.arguments, "started");
476
510
  process.stdout.write(`\x1b[2m${started}\x1b[0m\n`);
477
511
  if (c.name === "edit_file" || c.name === "write_file")
@@ -496,9 +530,11 @@ export class InferenceEngine {
496
530
  pending.push(outputItem);
497
531
  }
498
532
  }
499
- process.stdout.write("\n\x1b[1;33m⚠ Max tool-call iterations reached for this turn.\x1b[0m\n" +
500
- "\x1b[33mThe work so far is saved. If you want the agent to keep going, type " +
501
- "[bold]continue[/bold] and the next step will resume from where it left off.\x1b[0m\n");
533
+ process.stdout.write("\n");
534
+ renderPanel("[bold yellow]⚠ Max tool-call iterations reached for this turn.[/bold yellow]\n" +
535
+ "[yellow]The work so far is saved. If you want the agent to keep going, type " +
536
+ "[bold]continue[/bold] and the next step will resume from where it left off.[/yellow]", "Warning", "", false, "33");
537
+ process.stdout.write("\n");
502
538
  if (retryPrompts.length)
503
539
  dropRetryPrompts(conversation, inputItems, retryPrompts);
504
540
  stripOrphanCalls(conversation);
@@ -508,11 +544,11 @@ export class InferenceEngine {
508
544
  if (err?.message === "interrupt" || err?.message === "eof") {
509
545
  throw err;
510
546
  }
511
- process.stdout.write("\n");
512
547
  renderErrorPanel(`Runtime Exception: ${err}`);
513
548
  }
514
549
  finally {
515
550
  this.inQuery = false;
551
+ showCursor();
516
552
  if (stats["tokens"] > 0 && this.keyData) {
517
553
  const userId = this.keyData["user_id"];
518
554
  if (userId) {
@@ -531,6 +567,7 @@ export class InferenceEngine {
531
567
  }
532
568
  function renderErrorPanel(msg) {
533
569
  const text = msg.replace(/\[bold yellow\]|\[yellow\]|\[\/.*?\]/g, "");
534
- process.stdout.write(`\x1b[31m${text}\x1b[0m\n`);
570
+ renderPanel(text, "Error", "", true, "31");
571
+ process.stdout.write("\n");
535
572
  }
536
573
  export { renderErrorPanel };
package/dist/ui.js CHANGED
@@ -210,8 +210,14 @@ export function tickDuration(seconds) {
210
210
  // ---------------------------------------------------------------------------
211
211
  const FENCE_MARKER_RE = /`{3,}/g;
212
212
  export function printAiChunk(chunk) {
213
- process.stdout.write(markdownToAnsi(chunk) + "\n");
214
- if (/^```/m.test(chunk))
213
+ // rich's Markdown renderer ignores leading blank lines; a commit boundary in
214
+ // "…\n\n…" text can leave the second chunk starting with "\n", so strip it to
215
+ // avoid double-printing a blank row (mirrors the original's output).
216
+ const trimmed = chunk.replace(/^\n+/, "");
217
+ if (!trimmed)
218
+ return;
219
+ process.stdout.write(markdownToAnsi(trimmed) + "\n");
220
+ if (/^```/m.test(trimmed))
215
221
  process.stdout.write("\n");
216
222
  }
217
223
  export function safeCommitPoint(text) {
@@ -233,7 +239,7 @@ export function truncateEllipsis(text, maxChars, label = "text") {
233
239
  export function displayRows(text) {
234
240
  if (!text)
235
241
  return 1;
236
- const plain = text.replace(/`/g, "");
242
+ const plain = text.replace(/\x1b\[[0-9;]*m/g, "").replace(/`/g, "");
237
243
  const width = Math.max(terminalWidth() || 80, 1);
238
244
  let rows = 0;
239
245
  const lines = plain.split("\n");
@@ -245,6 +251,103 @@ export function displayRows(text) {
245
251
  return rows;
246
252
  }
247
253
  // ---------------------------------------------------------------------------
254
+ // Cursor + spinner helpers (mirror rich's console.status spinner="dots")
255
+ // ---------------------------------------------------------------------------
256
+ let cursorHidden = false;
257
+ export function hideCursor() {
258
+ if (!cursorHidden) {
259
+ process.stdout.write("\x1b[?25l");
260
+ cursorHidden = true;
261
+ }
262
+ }
263
+ export function showCursor() {
264
+ if (cursorHidden) {
265
+ process.stdout.write("\x1b[?25h");
266
+ cursorHidden = false;
267
+ }
268
+ }
269
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
270
+ /**
271
+ * A transient status line with an animated dot spinner, updated in place
272
+ * (no new lines appended on every tick). On stop() the line is erased.
273
+ * Mirrors rich's console.status(..., spinner="dots").
274
+ */
275
+ export class Spinner {
276
+ timer = null;
277
+ frame = 0;
278
+ rows = 0;
279
+ text = "";
280
+ enabled;
281
+ constructor(enabled = true) {
282
+ this.enabled = enabled;
283
+ }
284
+ start(text) {
285
+ if (!this.enabled)
286
+ return;
287
+ if (this.timer) {
288
+ this.update(text);
289
+ return;
290
+ }
291
+ this.text = text;
292
+ this.frame = 0;
293
+ this.rows = 0;
294
+ this.timer = setInterval(() => {
295
+ this.frame++;
296
+ this.draw();
297
+ }, 120);
298
+ this.draw();
299
+ }
300
+ update(text) {
301
+ this.text = text;
302
+ if (this.timer)
303
+ this.draw();
304
+ }
305
+ draw() {
306
+ const rendered = mutedMarkdown(`${SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length]} ${this.text}`);
307
+ const newRows = Math.max(1, displayRows(rendered));
308
+ const clearRows = Math.max(this.rows, newRows, 1);
309
+ // Cursor is always at the home position (col 0) after each draw. Clear
310
+ // `clearRows` lines downward from home, rewrite the content, then return
311
+ // the cursor to home (col 0) so the next frame overwrites in place.
312
+ for (let i = 0; i < clearRows; i++) {
313
+ process.stdout.write("\r\x1b[2K");
314
+ if (i < clearRows - 1)
315
+ process.stdout.write("\n");
316
+ }
317
+ // Cursor is now `clearRows-1` lines below home; move back to home.
318
+ if (clearRows > 1)
319
+ process.stdout.write(`\x1b[${clearRows - 1}A\r`);
320
+ else
321
+ process.stdout.write("\r");
322
+ process.stdout.write(rendered);
323
+ // Return cursor to home (col 0) for the next draw.
324
+ if (newRows > 1)
325
+ process.stdout.write(`\x1b[${newRows - 1}A\r`);
326
+ else
327
+ process.stdout.write("\r");
328
+ this.rows = newRows;
329
+ }
330
+ stop() {
331
+ if (this.timer) {
332
+ clearInterval(this.timer);
333
+ this.timer = null;
334
+ }
335
+ if (this.enabled && this.rows > 0) {
336
+ // Cursor is at home (col 0); clear the rendered rows downward.
337
+ for (let i = 0; i < this.rows; i++) {
338
+ process.stdout.write("\r\x1b[2K");
339
+ if (i < this.rows - 1)
340
+ process.stdout.write("\n");
341
+ }
342
+ if (this.rows > 1)
343
+ process.stdout.write(`\x1b[${this.rows - 1}A\r`);
344
+ else
345
+ process.stdout.write("\r");
346
+ }
347
+ this.rows = 0;
348
+ }
349
+ }
350
+ // ---------------------------------------------------------------------------
248
351
  // Tool action formatter
249
352
  // ---------------------------------------------------------------------------
250
353
  export function formatToolAction(name, argumentsJson, status = "ok") {
@@ -481,35 +584,51 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
481
584
  if (process.stdin.isTTY)
482
585
  process.stdin.setRawMode(true);
483
586
  process.stdin.resume();
587
+ hideCursor();
484
588
  let done = false;
485
589
  let lastCursorRow = 0;
486
590
  let lastTotalRows = 0;
487
- const finish = (resolveVal) => {
488
- if (done)
489
- return;
490
- done = true;
491
- process.stdin.setRawMode(false);
492
- // Erase the whole prompt box (which occupies `lastTotalRows` lines) so no
493
- // frame borders are left behind, then leave the cursor on the box's top
494
- // row (a blank line). That way the caller's single leading newline yields
495
- // exactly ONE blank row before the echoed user prompt — never two.
591
+ // Erase the prompt region: a leading blank line + the box. Cursor sits at
592
+ // lastCursorRow inside the box; move up to the box's top border, erase the
593
+ // box rows, then erase the leading blank line, leaving the cursor on that
594
+ // blank row. The caller's single leading newline then yields exactly ONE
595
+ // blank row before the echoed user prompt / message.
596
+ const clearBox = () => {
496
597
  process.stdout.write(`\x1b[${lastCursorRow}A`);
497
598
  for (let i = 0; i < lastTotalRows; i++) {
498
599
  process.stdout.write("\r\x1b[K");
499
600
  if (i < lastTotalRows - 1)
500
601
  process.stdout.write("\n");
501
602
  }
502
- // Cursor is now on the last box row; move back up to the box's top row.
503
603
  if (lastTotalRows > 1)
504
604
  process.stdout.write(`\x1b[${lastTotalRows - 1}A`);
505
- resolve(resolveVal);
605
+ process.stdout.write("\x1b[1A\r\x1b[K");
606
+ };
607
+ const settle = (value, isErr) => {
608
+ if (done)
609
+ return;
610
+ done = true;
611
+ process.stdin.setRawMode(false);
612
+ showCursor();
613
+ clearBox();
614
+ if (isErr)
615
+ reject(value);
616
+ else
617
+ resolve(value);
506
618
  };
507
619
  const repaint = (isFirst = false) => {
508
620
  const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor);
509
621
  lastCursorRow = cursorRow;
510
622
  lastTotalRows = totalRows;
511
623
  const frameLines = frame.split("\n");
512
- if (!isFirst) {
624
+ if (isFirst) {
625
+ // Draw a leading blank line above the box (mirrors the original's
626
+ // Group(Text(""), panel)): move down one line, clear it as a blank, then
627
+ // move down again so the box starts below that blank.
628
+ process.stdout.write("\n");
629
+ process.stdout.write("\r\x1b[K\n");
630
+ }
631
+ else {
513
632
  // Cursor currently sits at cursorRow (inside the box). Move to the top
514
633
  // border, then clear+rewrite each line so old content is fully removed.
515
634
  process.stdout.write(`\x1b[${cursorRow}A`);
@@ -586,18 +705,16 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
586
705
  };
587
706
  const onKeypress = (str, key) => {
588
707
  if (key && key.ctrl && key.name === "c") {
589
- finish(["", []]);
590
- reject(new Error("interrupt"));
708
+ settle(new Error("interrupt"), true);
591
709
  return;
592
710
  }
593
711
  if (key && key.ctrl && (key.name === "d" || key.name === "z")) {
594
- finish([buffer, []]);
595
- reject(new Error("eof"));
712
+ settle(new Error("eof"), true);
596
713
  return;
597
714
  }
598
715
  if (key && key.name === "return") {
599
716
  if (buffer.trim()) {
600
- finish([buffer, pasteSpans]);
717
+ settle([buffer, pasteSpans], false);
601
718
  }
602
719
  return;
603
720
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },