@oxecli/oxe 1.0.6 → 1.0.7
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/config.js +4 -1
- package/dist/engine.js +85 -48
- package/dist/ui.js +109 -9
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
147
|
-
const workStatus = new
|
|
148
|
-
|
|
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(
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -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(
|
|
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
|
|
500
|
-
|
|
501
|
-
"[
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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,92 @@ 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
|
+
if (this.rows > 0)
|
|
310
|
+
process.stdout.write(`\x1b[${this.rows}A`);
|
|
311
|
+
for (let i = 0; i < clearRows; i++) {
|
|
312
|
+
process.stdout.write("\r\x1b[2K");
|
|
313
|
+
if (i < clearRows - 1)
|
|
314
|
+
process.stdout.write("\n");
|
|
315
|
+
}
|
|
316
|
+
if (clearRows > 0)
|
|
317
|
+
process.stdout.write(`\x1b[${clearRows - 1}A\r`);
|
|
318
|
+
process.stdout.write(rendered);
|
|
319
|
+
this.rows = newRows;
|
|
320
|
+
}
|
|
321
|
+
stop() {
|
|
322
|
+
if (this.timer) {
|
|
323
|
+
clearInterval(this.timer);
|
|
324
|
+
this.timer = null;
|
|
325
|
+
}
|
|
326
|
+
if (this.enabled && this.rows > 0) {
|
|
327
|
+
process.stdout.write(`\x1b[${this.rows}A`);
|
|
328
|
+
for (let i = 0; i < this.rows; i++) {
|
|
329
|
+
process.stdout.write("\r\x1b[2K");
|
|
330
|
+
if (i < this.rows - 1)
|
|
331
|
+
process.stdout.write("\n");
|
|
332
|
+
}
|
|
333
|
+
if (this.rows > 0)
|
|
334
|
+
process.stdout.write(`\x1b[${this.rows - 1}A`);
|
|
335
|
+
}
|
|
336
|
+
this.rows = 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
248
340
|
// Tool action formatter
|
|
249
341
|
// ---------------------------------------------------------------------------
|
|
250
342
|
export function formatToolAction(name, argumentsJson, status = "ok") {
|
|
@@ -481,6 +573,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
481
573
|
if (process.stdin.isTTY)
|
|
482
574
|
process.stdin.setRawMode(true);
|
|
483
575
|
process.stdin.resume();
|
|
576
|
+
hideCursor();
|
|
484
577
|
let done = false;
|
|
485
578
|
let lastCursorRow = 0;
|
|
486
579
|
let lastTotalRows = 0;
|
|
@@ -489,19 +582,22 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
489
582
|
return;
|
|
490
583
|
done = true;
|
|
491
584
|
process.stdin.setRawMode(false);
|
|
492
|
-
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
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.
|
|
496
591
|
process.stdout.write(`\x1b[${lastCursorRow}A`);
|
|
497
592
|
for (let i = 0; i < lastTotalRows; i++) {
|
|
498
593
|
process.stdout.write("\r\x1b[K");
|
|
499
594
|
if (i < lastTotalRows - 1)
|
|
500
595
|
process.stdout.write("\n");
|
|
501
596
|
}
|
|
502
|
-
// Cursor is now on the last box row; move back up to the box's top row.
|
|
503
597
|
if (lastTotalRows > 1)
|
|
504
598
|
process.stdout.write(`\x1b[${lastTotalRows - 1}A`);
|
|
599
|
+
// Clear the leading blank line above the box.
|
|
600
|
+
process.stdout.write("\x1b[1A\r\x1b[K");
|
|
505
601
|
resolve(resolveVal);
|
|
506
602
|
};
|
|
507
603
|
const repaint = (isFirst = false) => {
|
|
@@ -509,7 +605,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
509
605
|
lastCursorRow = cursorRow;
|
|
510
606
|
lastTotalRows = totalRows;
|
|
511
607
|
const frameLines = frame.split("\n");
|
|
512
|
-
if (
|
|
608
|
+
if (isFirst) {
|
|
609
|
+
// A blank line above the box (mirrors the original's leading Text("")).
|
|
610
|
+
process.stdout.write("\n");
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
513
613
|
// Cursor currently sits at cursorRow (inside the box). Move to the top
|
|
514
614
|
// border, then clear+rewrite each line so old content is fully removed.
|
|
515
615
|
process.stdout.write(`\x1b[${cursorRow}A`);
|