@oxecli/oxe 1.0.7 → 1.0.9

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
@@ -169,13 +169,11 @@ export async function loadOrPrompt() {
169
169
  authSpinner.stop();
170
170
  if (validation.valid) {
171
171
  key_data = validation.key_data || {};
172
- process.stdout.write("\n");
173
172
  process.stdout.write(markupToAnsi(`[green]✓ Oxe API Key verified[/green] [dim](${String(key_data["name"] ?? "Desktop")})[/dim]`) + "\n");
174
173
  process.env.OXE_API_KEY = api_key;
175
174
  break;
176
175
  }
177
176
  else {
178
- process.stdout.write("\n");
179
177
  process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n`);
180
178
  process.stdout.write("\n");
181
179
  api_key = "";
package/dist/engine.js CHANGED
@@ -400,7 +400,7 @@ export class InferenceEngine {
400
400
  const stats = { thinking: 0, tokens: 0, reasoning: 0, input: 0 };
401
401
  const queryStart = Date.now();
402
402
  const footerText = () => `(Thought for ${tickDuration(stats["thinking"])} · Worked for ${tickDuration((Date.now() - queryStart) / 1000)} · Used \`${stats["tokens"].toLocaleString()}\` tokens)`;
403
- const summary = () => mutedMarkdown(footerText());
403
+ const summary = () => aiMarkdown(footerText());
404
404
  const attachFooter = (items) => {
405
405
  const footer = footerText();
406
406
  for (let i = items.length - 1; i >= 0; i--) {
package/dist/ui.js CHANGED
@@ -306,16 +306,25 @@ export class Spinner {
306
306
  const rendered = mutedMarkdown(`${SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length]} ${this.text}`);
307
307
  const newRows = Math.max(1, displayRows(rendered));
308
308
  const clearRows = Math.max(this.rows, newRows, 1);
309
- if (this.rows > 0)
310
- process.stdout.write(`\x1b[${this.rows}A`);
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.
311
312
  for (let i = 0; i < clearRows; i++) {
312
313
  process.stdout.write("\r\x1b[2K");
313
314
  if (i < clearRows - 1)
314
315
  process.stdout.write("\n");
315
316
  }
316
- if (clearRows > 0)
317
+ // Cursor is now `clearRows-1` lines below home; move back to home.
318
+ if (clearRows > 1)
317
319
  process.stdout.write(`\x1b[${clearRows - 1}A\r`);
320
+ else
321
+ process.stdout.write("\r");
318
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");
319
328
  this.rows = newRows;
320
329
  }
321
330
  stop() {
@@ -324,14 +333,16 @@ export class Spinner {
324
333
  this.timer = null;
325
334
  }
326
335
  if (this.enabled && this.rows > 0) {
327
- process.stdout.write(`\x1b[${this.rows}A`);
336
+ // Cursor is at home (col 0); clear the rendered rows downward.
328
337
  for (let i = 0; i < this.rows; i++) {
329
338
  process.stdout.write("\r\x1b[2K");
330
339
  if (i < this.rows - 1)
331
340
  process.stdout.write("\n");
332
341
  }
333
- if (this.rows > 0)
334
- process.stdout.write(`\x1b[${this.rows - 1}A`);
342
+ if (this.rows > 1)
343
+ process.stdout.write(`\x1b[${this.rows - 1}A\r`);
344
+ else
345
+ process.stdout.write("\r");
335
346
  }
336
347
  this.rows = 0;
337
348
  }
@@ -577,17 +588,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
577
588
  let done = false;
578
589
  let lastCursorRow = 0;
579
590
  let lastTotalRows = 0;
580
- const finish = (resolveVal) => {
581
- if (done)
582
- return;
583
- done = true;
584
- process.stdin.setRawMode(false);
585
- showCursor();
586
- // Erase the prompt region: a leading blank line + the box. Cursor sits at
587
- // lastCursorRow inside the box; move up to the box's top border, erase the
588
- // box rows, then erase the leading blank line, leaving the cursor on that
589
- // blank row. The caller's single leading newline then yields exactly ONE
590
- // blank row before the echoed user prompt.
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 = () => {
591
597
  process.stdout.write(`\x1b[${lastCursorRow}A`);
592
598
  for (let i = 0; i < lastTotalRows; i++) {
593
599
  process.stdout.write("\r\x1b[K");
@@ -596,9 +602,19 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
596
602
  }
597
603
  if (lastTotalRows > 1)
598
604
  process.stdout.write(`\x1b[${lastTotalRows - 1}A`);
599
- // Clear the leading blank line above the box.
600
605
  process.stdout.write("\x1b[1A\r\x1b[K");
601
- resolve(resolveVal);
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);
602
618
  };
603
619
  const repaint = (isFirst = false) => {
604
620
  const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor);
@@ -606,7 +622,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
606
622
  lastTotalRows = totalRows;
607
623
  const frameLines = frame.split("\n");
608
624
  if (isFirst) {
609
- // A blank line above the box (mirrors the original's leading Text("")).
625
+ // The caller's content always ends with "\n", so the cursor sits at the
626
+ // start of a fresh (blank) line — that line is the leading blank
627
+ // separator (mirrors the original's Group(Text(""), panel)). Move down
628
+ // one line so the box starts below that blank; do NOT emit a second
629
+ // blank here (it caused a double gap above the box / on cancel).
610
630
  process.stdout.write("\n");
611
631
  }
612
632
  else {
@@ -686,18 +706,16 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
686
706
  };
687
707
  const onKeypress = (str, key) => {
688
708
  if (key && key.ctrl && key.name === "c") {
689
- finish(["", []]);
690
- reject(new Error("interrupt"));
709
+ settle(new Error("interrupt"), true);
691
710
  return;
692
711
  }
693
712
  if (key && key.ctrl && (key.name === "d" || key.name === "z")) {
694
- finish([buffer, []]);
695
- reject(new Error("eof"));
713
+ settle(new Error("eof"), true);
696
714
  return;
697
715
  }
698
716
  if (key && key.name === "return") {
699
717
  if (buffer.trim()) {
700
- finish([buffer, pasteSpans]);
718
+ settle([buffer, pasteSpans], false);
701
719
  }
702
720
  return;
703
721
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },