@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 +7 -4
- package/dist/config.js +3 -3
- package/dist/engine.js +1 -0
- package/dist/ui.js +108 -79
- package/package.json +1 -1
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(
|
|
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(
|
|
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
|
|
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
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
|
|
139
|
-
*
|
|
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
|
-
//
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
168
|
-
|
|
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
|
|
351
|
-
const border = "─".repeat(innerW);
|
|
358
|
+
const borderW = Math.max(w - 4, 10);
|
|
352
359
|
const [row, col] = cursorLineCol(buffer, cursor);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|