@oxecli/oxe 1.0.3 → 1.0.5

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
@@ -17,7 +17,7 @@ export class CLI {
17
17
  promptHistory = [];
18
18
  constructor() {
19
19
  clearScreen();
20
- renderPanel("AI CODING AGENT", "", "Engine: Ready");
20
+ renderPanel("AI CODING AGENT", "", "Engine: Ready", false);
21
21
  process.stdout.write("\n");
22
22
  this.config = null;
23
23
  }
@@ -189,9 +189,12 @@ export class CLI {
189
189
  this.sessionId = sid;
190
190
  engine.reasoningEffort = String(rec["reasoning_effort"] ?? default_reasoning_effort);
191
191
  clearScreen();
192
- renderPanel(`AI CODING AGENT (resumed ${sid})`, "", "Engine: Ready");
192
+ renderPanel("AI CODING AGENT", "", `Engine: Ready ${sid}`, false);
193
+ process.stdout.write("\n");
193
194
  process.stdout.write(`\x1b[2mResumed:\x1b[0m ${truncateLabel(rec["label"])}\n`);
194
- process.stdout.write(`\x1b[2m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens · Effort: \x1b[1;36m${engine.reasoningEffort}\x1b[0m\x1b[2m)\x1b[0m\n\n`);
195
+ process.stdout.write("\n");
196
+ process.stdout.write(`\x1b[2m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens · Effort: \x1b[1;36m${engine.reasoningEffort}\x1b[0m\x1b[2m)\x1b[0m\n`);
197
+ process.stdout.write("\n");
195
198
  renderPanel("Conversation history", "Restored");
196
199
  process.stdout.write("\n");
197
200
  if (this.story.length)
@@ -226,7 +229,7 @@ export class CLI {
226
229
  this.story = [];
227
230
  this.sessionId = null;
228
231
  clearScreen();
229
- renderPanel("AI CODING AGENT", "", "Engine: Ready");
232
+ renderPanel("AI CODING AGENT", "", "Engine: Ready", false);
230
233
  process.stdout.write("\n");
231
234
  continue;
232
235
  }
package/dist/config.js CHANGED
@@ -153,7 +153,7 @@ export async function loadOrPrompt() {
153
153
  renderPanel("[bold white]Oxe Desktop Authentication[/bold white]\n\n" +
154
154
  `[dim]Only valid Oxe API keys (starting with [bold]${OXE_KEY_PREFIX}[/bold]) generated on your\n` +
155
155
  "Oxe web dashboard are authorized to run this agent.\n\n" +
156
- "Get your key at: [bold underline]http://localhost:5173/dashboard/api-keys[/bold underline][/dim]", "Oxe Cloud Access");
156
+ "Get your key at: [bold underline]http://localhost:5173/dashboard/api-keys[/bold underline][/dim]", "Oxe Cloud Access", "", false);
157
157
  process.stdout.write("\n");
158
158
  api_key = await promptApiKey("Enter Oxe API Key: ");
159
159
  if (!api_key) {
@@ -209,7 +209,7 @@ export async function loadOrPrompt() {
209
209
  };
210
210
  }
211
211
  /**
212
- * Prompt for a secret (API key) without echoing it to the terminal.
212
+ * Prompt for an API key, echoing what the user types.
213
213
  * Falls back to a plain readline read when stdin isn't a TTY (e.g. piped input).
214
214
  */
215
215
  export async function promptApiKey(prompt) {
@@ -262,7 +262,7 @@ export async function promptApiKey(prompt) {
262
262
  if (b < 32)
263
263
  continue;
264
264
  input += String.fromCharCode(b);
265
- process.stdout.write("*");
265
+ process.stdout.write(String.fromCharCode(b));
266
266
  }
267
267
  };
268
268
  const cleanup = () => {
package/dist/engine.js CHANGED
@@ -175,6 +175,7 @@ export class InferenceEngine {
175
175
  parallel_tool_calls: true,
176
176
  max_output_tokens,
177
177
  reasoning: { effort: this.reasoningEffort },
178
+ stream: true,
178
179
  stream_options: { include_usage: true },
179
180
  store: true,
180
181
  };
package/dist/ui.js CHANGED
@@ -135,46 +135,54 @@ export function mutedMarkdown(text) {
135
135
  // Panel
136
136
  // ---------------------------------------------------------------------------
137
137
  /**
138
- * Render a rich-style rounded panel: title in the top border, optional subtitle
139
- * in the bottom border, content lines inside.
138
+ * Render a rich-style rounded panel.
139
+ *
140
+ * Matches rich's Panel behavior:
141
+ * - `expand=false` shrinks the frame to fit the content (banner/auth).
142
+ * - title/subtitle are embedded in the top/bottom borders, centered by
143
+ * default (rich default) or left-aligned.
140
144
  */
141
- export function renderPanel(content, title = "", subtitle = "", borderStyle = "90" // 90 = bright black (grey50)
142
- ) {
143
- const w = terminalWidth();
144
- const innerW = Math.max(w - 4, 10);
145
+ export function renderPanel(content, title = "", subtitle = "", expand = true, borderStyle = "90", // 90 = bright black (grey50)
146
+ titleAlign = "center") {
145
147
  const styled = markupToAnsi(content);
146
148
  const styledLines = styled.split("\n");
147
149
  const plainLines = styled.replace(/\x1b\[[0-9;]*m/g, "").split("\n");
148
- // Effective width used by the border (accounting for a title/subtitle side)
149
- const borderW = innerW;
150
- const topSegs = [];
151
- topSegs.push("╭");
152
- if (title) {
153
- const t = ` ${title} `;
154
- topSegs.push(`─${t}${"─".repeat(Math.max(borderW - t.length - 2, 0))}`);
155
- }
156
- else {
157
- topSegs.push("─".repeat(borderW));
158
- }
159
- topSegs.push("╮");
160
- process.stdout.write(`\x1b[${borderStyle}m${topSegs.join("")}\x1b[0m\n`);
150
+ // Inner width = widest content line + 1 space padding each side.
151
+ let contentW = 0;
152
+ for (const p of plainLines)
153
+ contentW = Math.max(contentW, p.length);
154
+ contentW = Math.max(contentW, 1);
155
+ let innerW = contentW + 2;
156
+ if (title)
157
+ innerW = Math.max(innerW, title.length + 2);
158
+ if (subtitle)
159
+ innerW = Math.max(innerW, subtitle.length + 2);
160
+ const w = terminalWidth();
161
+ innerW = expand ? Math.max(w - 4, 10) : Math.min(innerW, w - 4);
162
+ // Build a border run of `innerW` chars with `text` embedded.
163
+ const embed = (text, align) => {
164
+ if (!text)
165
+ return "─".repeat(innerW);
166
+ const inner = ` ${text} `;
167
+ const fill = Math.max(innerW - inner.length, 0);
168
+ if (align === "left") {
169
+ return `${inner}${"─".repeat(fill)}`;
170
+ }
171
+ const left = Math.floor(fill / 2);
172
+ const right = fill - left;
173
+ return `${"─".repeat(left)}${inner}${"─".repeat(right)}`;
174
+ };
175
+ const top = `╭${embed(title, titleAlign)}╮`;
176
+ process.stdout.write(`\x1b[${borderStyle}m${top}\x1b[0m\n`);
161
177
  for (let i = 0; i < styledLines.length; i++) {
162
178
  const line = styledLines[i];
163
179
  const plain = plainLines[i] ?? "";
164
- const pad = Math.max(borderW - plain.length - 1, 0);
180
+ // Keep exactly one space padding each side; only pad the right to fill.
181
+ const pad = Math.max(innerW - plain.length - 2, 0);
165
182
  process.stdout.write(`\x1b[${borderStyle}m│\x1b[0m ${line}${" ".repeat(pad)} \x1b[${borderStyle}m│\x1b[0m\n`);
166
183
  }
167
- const bottomSegs = [];
168
- bottomSegs.push("╰");
169
- if (subtitle) {
170
- const s = ` ${subtitle} `;
171
- bottomSegs.push(`─${s}${"─".repeat(Math.max(borderW - s.length - 2, 0))}`);
172
- }
173
- else {
174
- bottomSegs.push("─".repeat(borderW));
175
- }
176
- bottomSegs.push("╯");
177
- process.stdout.write(`\x1b[${borderStyle}m${bottomSegs.join("")}\x1b[0m\n`);
184
+ const bottom = `╰${embed(subtitle, "center")}╯`;
185
+ process.stdout.write(`\x1b[${borderStyle}m${bottom}\x1b[0m\n`);
178
186
  }
179
187
  // ---------------------------------------------------------------------------
180
188
  // Durations
@@ -345,52 +353,46 @@ export function splitBlocks(buffer, pasteSpans) {
345
353
  }
346
354
  return segs;
347
355
  }
348
- function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor) {
356
+ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor) {
349
357
  const w = terminalWidth();
350
- const innerW = Math.max(w - 4, 10);
351
- const border = "─".repeat(innerW);
358
+ const borderW = Math.max(w - 4, 10);
352
359
  const [row, col] = cursorLineCol(buffer, cursor);
353
- const segs = splitBlocks(buffer, pasteSpans);
354
- let display = "";
355
- for (let i = 0; i < segs.length; i++) {
356
- const [, kind, disp] = segs[i];
357
- if (i && !endsWithWs(segs[i - 1][2]))
358
- display += " ";
359
- display += kind === "collapsed" ? `\x1b[1m\x1b[36m${disp}\x1b[0m` : disp;
360
+ // Build the inner content line(s) — the whole prefix+text (or placeholder)
361
+ // lives on a single row when the buffer is empty, matching the original.
362
+ let content;
363
+ if (!buffer) {
364
+ content = `\x1b[1m${prefix}\x1b[0m \x1b[1m▏\x1b[0m\x1b[2m${PROMPT_PLACEHOLDER}\x1b[0m`;
360
365
  }
361
- const prefixStr = `\x1b[1m${prefix}\x1b[0m `;
362
- const innerLines = (prefixStr + display).split("\n");
363
- // Build the panel lines, each padded to innerW.
364
- const lines = [];
365
- for (const l of innerLines) {
366
- const plain = l.replace(/\x1b\[[0-9;]*m/g, "");
367
- const pad = Math.max(innerW - 1 - plain.length, 0);
368
- lines.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
366
+ else {
367
+ const segs = splitBlocks(buffer, pasteSpans);
368
+ let display = "";
369
+ for (let i = 0; i < segs.length; i++) {
370
+ const [, kind, disp] = segs[i];
371
+ if (i && !endsWithWs(segs[i - 1][2]))
372
+ display += " ";
373
+ display += kind === "collapsed" ? `\x1b[1m\x1b[36m${disp}\x1b[0m` : disp;
374
+ }
375
+ content = `\x1b[1m${prefix}\x1b[0m ${display}`;
369
376
  }
370
- // Placeholder row when empty
371
- if (!buffer) {
372
- const ph = `\x1b[2m${PROMPT_PLACEHOLDER}\x1b[0m`;
373
- const pad = Math.max(innerW - 1 - PROMPT_PLACEHOLDER.length, 0);
374
- lines.push(`\x1b[90m│\x1b[0m ${ph}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
377
+ const lines = content.split("\n");
378
+ // Top border with the label embedded on the left.
379
+ const topPad = Math.max(borderW - label.length - 2, 0);
380
+ const top = `\x1b[90m╭─ ${label} ${"─".repeat(topPad)}╮\x1b[0m`;
381
+ // Body rows.
382
+ const body = [];
383
+ for (const l of lines) {
384
+ const plain = l.replace(/\x1b\[[0-9;]*m/g, "");
385
+ const pad = Math.max(borderW - 2 - plain.length, 0);
386
+ body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
375
387
  }
376
- const frame = [
377
- `\x1b[90m┌─${label}─${border.slice(label.length + 2)}┐\x1b[0m`,
378
- ...lines,
379
- `\x1b[90m└${border}┘\x1b[0m`,
380
- ].join("\n");
381
- // Compute where the cursor should be placed.
382
- let cursorRow = row;
383
- let cursorCol = col + (row === 0 ? prefix.length + 1 : 0);
384
- // +1 row for the top border.
385
- cursorRow += 1;
386
- const totalRows = frame.split("\n").length;
387
- let cmd = "";
388
- if (totalRows - 1 > cursorRow)
389
- cmd += `\x1b[${totalRows - 1 - cursorRow}A`;
390
- else if (cursorRow > totalRows - 1)
391
- cmd += `\x1b[${cursorRow - (totalRows - 1)}B`;
392
- cmd += `\r\x1b[${cursorCol + 2}C`;
393
- return frame + cmd;
388
+ const bottom = `\x1b[90m╰${"─".repeat(borderW)}╯\x1b[0m`;
389
+ const frame = [top, ...body, bottom].join("\n");
390
+ // Cursor placement: one row below the top border. Column includes the
391
+ // "│ " left border (2) plus "prefix " (prefix.length + 1).
392
+ const cursorRow = row + 1;
393
+ const cursorCol = col + prefix.length + 3;
394
+ const totalRows = body.length + 2;
395
+ return { frame, cursorRow, cursorCol, totalRows };
394
396
  }
395
397
  export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
396
398
  return new Promise((resolve, reject) => {
@@ -417,19 +419,46 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
417
419
  process.stdin.setRawMode(true);
418
420
  process.stdin.resume();
419
421
  let done = false;
422
+ let lastCursorRow = 0;
423
+ let lastTotalRows = 0;
420
424
  const finish = (resolveVal) => {
421
425
  if (done)
422
426
  return;
423
427
  done = true;
424
428
  process.stdin.setRawMode(false);
425
- process.stdout.write("\r\x1b[K");
426
- process.stdout.write("\n");
429
+ // Erase the whole prompt box (which occupies `lastTotalRows` lines) so no
430
+ // frame borders are left behind, then leave the cursor on the box's top
431
+ // row (a blank line). That way the caller's single leading newline yields
432
+ // exactly ONE blank row before the echoed user prompt — never two.
433
+ process.stdout.write(`\x1b[${lastCursorRow}A`);
434
+ for (let i = 0; i < lastTotalRows; i++) {
435
+ process.stdout.write("\r\x1b[K");
436
+ if (i < lastTotalRows - 1)
437
+ process.stdout.write("\n");
438
+ }
439
+ // Cursor is now on the last box row; move back up to the box's top row.
440
+ if (lastTotalRows > 1)
441
+ process.stdout.write(`\x1b[${lastTotalRows - 1}A`);
427
442
  resolve(resolveVal);
428
443
  };
429
- const repaint = () => {
430
- const frame = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor);
431
- process.stdout.write("\r\x1b[K");
432
- process.stdout.write(frame);
444
+ const repaint = (isFirst = false) => {
445
+ const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor);
446
+ lastCursorRow = cursorRow;
447
+ lastTotalRows = totalRows;
448
+ const frameLines = frame.split("\n");
449
+ if (!isFirst) {
450
+ // Cursor currently sits at cursorRow (inside the box). Move to the top
451
+ // border, then clear+rewrite each line so old content is fully removed.
452
+ process.stdout.write(`\x1b[${cursorRow}A`);
453
+ }
454
+ for (let i = 0; i < frameLines.length; i++) {
455
+ process.stdout.write("\r\x1b[K" + frameLines[i]);
456
+ if (i < frameLines.length - 1)
457
+ process.stdout.write("\n");
458
+ }
459
+ // Reposition the cursor inside the box.
460
+ const fromBottom = totalRows - 1 - cursorRow;
461
+ process.stdout.write(`\x1b[${fromBottom}A\r\x1b[${cursorCol}C`);
433
462
  };
434
463
  const insert = (text) => {
435
464
  if (histIdx !== hist.length) {
@@ -551,7 +580,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
551
580
  }
552
581
  };
553
582
  process.stdin.on("keypress", onKeypress);
554
- repaint();
583
+ repaint(true);
555
584
  });
556
585
  }
557
586
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },