@oxecli/oxe 1.0.86 → 1.0.88
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 +14 -15
- package/dist/engine.js +20 -63
- package/dist/ui.js +170 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { loadOrPrompt, default_reasoning_effort, max_action_chars, max_resume_history_items, runtimeOsSummary, max_context_tokens, context_overhead_margin, } from "./config.js";
|
|
5
5
|
import { InferenceEngine, estimateTokens } from "./engine.js";
|
|
6
6
|
import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
|
|
7
|
-
import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, plainLen, truncateStyled, terminalWidth, TOOL_RESULT_MAX_LINES, MAX_COMMAND_DISPLAY_CHARS, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
|
|
7
|
+
import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, plainLen, truncateStyled, terminalWidth, TOOL_RESULT_MAX_LINES, MAX_COMMAND_DISPLAY_CHARS, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, dockAppendContent, dockSetInactive, dockTearDown, } from "./ui.js";
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const packageInfo = require("../package.json");
|
|
@@ -64,7 +64,7 @@ export class CLI {
|
|
|
64
64
|
process.stdout.write(`${labelLine}\n${lines.join("\n")}\n`);
|
|
65
65
|
if (diff) {
|
|
66
66
|
const diffLines = diff.split("\n").map((ln) => plainLen(ln) > lineMax ? truncateStyled(ln, lineMax) + "…\x1b[0m" : ln);
|
|
67
|
-
process.stdout.write(diffLines.join("\n") + "\n
|
|
67
|
+
process.stdout.write(diffLines.join("\n") + "\n");
|
|
68
68
|
}
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
@@ -91,6 +91,7 @@ export class CLI {
|
|
|
91
91
|
process.stdout.write("\n");
|
|
92
92
|
}
|
|
93
93
|
async showHelp() {
|
|
94
|
+
dockTearDown();
|
|
94
95
|
const rows = COMMAND_HELP.map(([cmd, desc]) => {
|
|
95
96
|
const styled = cmd.replace(/(<[^>]+>)/g, "\x1b[36m$1\x1b[0m");
|
|
96
97
|
return {
|
|
@@ -104,7 +105,7 @@ export class CLI {
|
|
|
104
105
|
titleAlign: "left",
|
|
105
106
|
colGap: 3,
|
|
106
107
|
});
|
|
107
|
-
process.stdout.write(
|
|
108
|
+
process.stdout.write(panel + "\n");
|
|
108
109
|
if (process.stdin.isTTY) {
|
|
109
110
|
enableRawStdin();
|
|
110
111
|
try {
|
|
@@ -117,7 +118,7 @@ export class CLI {
|
|
|
117
118
|
disableRawStdin();
|
|
118
119
|
}
|
|
119
120
|
const n = panel.split("\n").length;
|
|
120
|
-
process.stdout.write(`\x1b[${n
|
|
121
|
+
process.stdout.write(`\x1b[${n}A\r\x1b[J`);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
async awaitRawCloseKey() {
|
|
@@ -309,7 +310,6 @@ export class CLI {
|
|
|
309
310
|
async pickConversation() {
|
|
310
311
|
const recs = listSessions();
|
|
311
312
|
if (!recs.length) {
|
|
312
|
-
process.stdout.write("\n");
|
|
313
313
|
renderPanel("No saved conversations yet. Type a prompt to start one.", "Conversations");
|
|
314
314
|
return null;
|
|
315
315
|
}
|
|
@@ -318,10 +318,11 @@ export class CLI {
|
|
|
318
318
|
return null;
|
|
319
319
|
}
|
|
320
320
|
enableRawStdin();
|
|
321
|
+
dockTearDown();
|
|
321
322
|
let selected = 0;
|
|
322
323
|
let lastRows = 0;
|
|
323
324
|
const draw = () => {
|
|
324
|
-
const frame =
|
|
325
|
+
const frame = this.renderPickerPanel(recs, selected);
|
|
325
326
|
const n = frame.split("\n").length;
|
|
326
327
|
if (lastRows > 0)
|
|
327
328
|
process.stdout.write(`\x1b[${lastRows - 1}A\r\x1b[J`);
|
|
@@ -359,13 +360,11 @@ export class CLI {
|
|
|
359
360
|
async resumeConversation(engine, num) {
|
|
360
361
|
const sid = parseInt(num, 10);
|
|
361
362
|
if (Number.isNaN(sid)) {
|
|
362
|
-
process.stdout.write("\n");
|
|
363
363
|
renderPanel(`Invalid conversation number: ${num}`, "Error", "", true, "31");
|
|
364
364
|
return;
|
|
365
365
|
}
|
|
366
366
|
const rec = loadSession(sid);
|
|
367
367
|
if (!rec) {
|
|
368
|
-
process.stdout.write("\n");
|
|
369
368
|
renderPanel(`Conversation ${sid} not found.`, "Error", "", true, "31");
|
|
370
369
|
return;
|
|
371
370
|
}
|
|
@@ -376,10 +375,11 @@ export class CLI {
|
|
|
376
375
|
this.sessionId = sid;
|
|
377
376
|
engine.reasoningEffort = String(rec["reasoning_effort"] ?? default_reasoning_effort);
|
|
378
377
|
clearScreen();
|
|
378
|
+
dockSetInactive();
|
|
379
379
|
this.renderHeader();
|
|
380
380
|
process.stdout.write("\n");
|
|
381
381
|
process.stdout.write(`\x1b[90mResumed:\x1b[0m \x1b[1m${truncateLabel(rec["label"])}\x1b[0m\n`);
|
|
382
|
-
process.stdout.write(`\x1b[90m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n
|
|
382
|
+
process.stdout.write(`\x1b[90m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n`);
|
|
383
383
|
renderPanel("Conversation history restored", "Restored");
|
|
384
384
|
process.stdout.write("\n");
|
|
385
385
|
if (this.story.length)
|
|
@@ -421,6 +421,7 @@ export class CLI {
|
|
|
421
421
|
this.story = [];
|
|
422
422
|
this.sessionId = null;
|
|
423
423
|
clearScreen();
|
|
424
|
+
dockSetInactive();
|
|
424
425
|
this.renderHeader();
|
|
425
426
|
continue;
|
|
426
427
|
}
|
|
@@ -442,15 +443,14 @@ export class CLI {
|
|
|
442
443
|
const effort = parts[1]?.toLowerCase() ?? "";
|
|
443
444
|
if (["none", "low", "high"].includes(effort)) {
|
|
444
445
|
engine.reasoningEffort = effort;
|
|
445
|
-
|
|
446
|
+
dockAppendContent(`\x1b[32m✓\x1b[0m \x1b[90mReasoning effort set to \x1b[1;36m${effort}\x1b[0m\x1b[90m.\x1b[0m\n`);
|
|
446
447
|
}
|
|
447
448
|
else {
|
|
448
|
-
process.stdout.write("\n");
|
|
449
449
|
renderPanel("Usage: /effort none|low|high", "Error", "", true, "31");
|
|
450
450
|
}
|
|
451
451
|
continue;
|
|
452
452
|
}
|
|
453
|
-
|
|
453
|
+
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
454
454
|
queryStarted = true;
|
|
455
455
|
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
456
456
|
queryStarted = false;
|
|
@@ -466,7 +466,7 @@ export class CLI {
|
|
|
466
466
|
if (err?.message === "interrupt") {
|
|
467
467
|
const wasActive = queryStarted || engine.inQuery;
|
|
468
468
|
if (wasActive && !engine.queryHasOutput && !engine.queryCalledTool) {
|
|
469
|
-
|
|
469
|
+
dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
|
|
470
470
|
}
|
|
471
471
|
engine.inQuery = false;
|
|
472
472
|
stripOrphanCalls(this.inputItems);
|
|
@@ -488,8 +488,7 @@ export class CLI {
|
|
|
488
488
|
break;
|
|
489
489
|
}
|
|
490
490
|
this.interruptPending = true;
|
|
491
|
-
|
|
492
|
-
process.stdout.write("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
|
|
491
|
+
dockAppendContent("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
|
|
493
492
|
continue;
|
|
494
493
|
}
|
|
495
494
|
throw err;
|
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 { getSystemPrompt } from "./system.js";
|
|
4
4
|
import { buildTools, truncateToolOutput, toolReadFile, toolWriteFile, toolEditFile, toolBash, toolGlob, toolGrep, toolLoadSkill, } from "./tools.js";
|
|
5
|
-
import { mutedMarkdown, aiMarkdown, tickDuration,
|
|
5
|
+
import { mutedMarkdown, aiMarkdown, tickDuration, safeCommitPoint, printAiChunk, toolCallLabel, formatToolResult, renderPanel, Spinner, hideCursor, dockAppendContent, dockReplaceTransient, dockTransientStart, } from "./ui.js";
|
|
6
6
|
import { reportUsage } from "./api.js";
|
|
7
7
|
import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
|
|
8
8
|
import { takePendingDiffOutput } from "./tools.js";
|
|
@@ -43,8 +43,6 @@ export class InferenceEngine {
|
|
|
43
43
|
reasoningEffort;
|
|
44
44
|
keyData;
|
|
45
45
|
client;
|
|
46
|
-
workActive = false;
|
|
47
|
-
workRows = 0;
|
|
48
46
|
inQuery = false;
|
|
49
47
|
activeAbort = null;
|
|
50
48
|
interrupted = false;
|
|
@@ -146,22 +144,6 @@ export class InferenceEngine {
|
|
|
146
144
|
incompleteReason(response) {
|
|
147
145
|
return response?.incomplete_details?.reason ?? null;
|
|
148
146
|
}
|
|
149
|
-
overwriteWorkLine(text) {
|
|
150
|
-
for (let i = 0; i < this.workRows; i++) {
|
|
151
|
-
process.stdout.write("\x1b[1A\x1b[2K");
|
|
152
|
-
}
|
|
153
|
-
process.stdout.write(mutedMarkdown(text) + "\n\n");
|
|
154
|
-
this.workRows = displayRows(mutedMarkdown(text)) + 1;
|
|
155
|
-
}
|
|
156
|
-
clearWorkLine() {
|
|
157
|
-
if (!this.workActive || this.workRows < 1)
|
|
158
|
-
return;
|
|
159
|
-
for (let i = 0; i < this.workRows; i++) {
|
|
160
|
-
process.stdout.write("\x1b[1A\x1b[2K");
|
|
161
|
-
}
|
|
162
|
-
this.workActive = false;
|
|
163
|
-
this.workRows = 0;
|
|
164
|
-
}
|
|
165
147
|
async streamOnce(inputItems, stats, story, previousResponseId) {
|
|
166
148
|
let content = "";
|
|
167
149
|
let committedLen = 0;
|
|
@@ -169,6 +151,7 @@ export class InferenceEngine {
|
|
|
169
151
|
let sawReasoning = false;
|
|
170
152
|
let status = null;
|
|
171
153
|
let thinkingStart = null;
|
|
154
|
+
dockTransientStart();
|
|
172
155
|
const workStatus = new Spinner();
|
|
173
156
|
const workingStarted = Date.now();
|
|
174
157
|
workStatus.startWithRender(() => `Working ${tickDuration((Date.now() - workingStarted) / 1000)}`);
|
|
@@ -187,14 +170,7 @@ export class InferenceEngine {
|
|
|
187
170
|
const elapsed = (Date.now() - workingStarted) / 1000;
|
|
188
171
|
const t = tickDuration(elapsed);
|
|
189
172
|
story.push({ type: "worked", text: `Worked for ${t}` });
|
|
190
|
-
|
|
191
|
-
this.overwriteWorkLine(`Worked for ${t}`);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
process.stdout.write(mutedMarkdown(`Worked for ${t}`) + "\n\n");
|
|
195
|
-
this.workActive = true;
|
|
196
|
-
this.workRows = displayRows(mutedMarkdown(`Worked for ${t}`)) + 1;
|
|
197
|
-
}
|
|
173
|
+
dockReplaceTransient(mutedMarkdown(`Worked for ${t}`) + "\n");
|
|
198
174
|
};
|
|
199
175
|
const finishThinking = (report) => {
|
|
200
176
|
if (thinkingStart === null)
|
|
@@ -208,8 +184,7 @@ export class InferenceEngine {
|
|
|
208
184
|
if (report) {
|
|
209
185
|
const t = tickDuration(elapsed);
|
|
210
186
|
story.push({ type: "thought", text: `Thought for ${t}` });
|
|
211
|
-
|
|
212
|
-
this.workActive = false;
|
|
187
|
+
dockReplaceTransient(mutedMarkdown(`Thought for ${t}`) + "\n");
|
|
213
188
|
}
|
|
214
189
|
thinkingStart = null;
|
|
215
190
|
};
|
|
@@ -248,6 +223,7 @@ export class InferenceEngine {
|
|
|
248
223
|
etype === "response.reasoning.summary.delta") {
|
|
249
224
|
sawReasoning = true;
|
|
250
225
|
silenceWorking();
|
|
226
|
+
dockTransientStart();
|
|
251
227
|
if (thinkingStart === null) {
|
|
252
228
|
thinkingStart = Date.now();
|
|
253
229
|
status = new Spinner();
|
|
@@ -288,9 +264,7 @@ export class InferenceEngine {
|
|
|
288
264
|
workStatus.stop();
|
|
289
265
|
const stillThinking = thinkingStart !== null;
|
|
290
266
|
finishThinking(stillThinking);
|
|
291
|
-
if (this.interrupted
|
|
292
|
-
this.clearWorkLine();
|
|
293
|
-
else
|
|
267
|
+
if (!this.interrupted || this.queryHasOutput)
|
|
294
268
|
reportWorking();
|
|
295
269
|
if (content.slice(committedLen))
|
|
296
270
|
printAiChunk(content.slice(committedLen));
|
|
@@ -398,6 +372,7 @@ export class InferenceEngine {
|
|
|
398
372
|
if (!this.contextOverBudget(conversation))
|
|
399
373
|
return [conversation, false];
|
|
400
374
|
const started = Date.now();
|
|
375
|
+
dockTransientStart();
|
|
401
376
|
const comp = new Spinner();
|
|
402
377
|
comp.start(`Compacting conversation ${tickDuration(0)}`);
|
|
403
378
|
const compTimer = setInterval(() => {
|
|
@@ -416,14 +391,11 @@ export class InferenceEngine {
|
|
|
416
391
|
persistCompactionSummary(inputItems, compacted[0]);
|
|
417
392
|
const text = `Compacted conversation in ${tickDuration((Date.now() - started) / 1000)}`;
|
|
418
393
|
story.push({ type: "compacted", text });
|
|
419
|
-
|
|
420
|
-
process.stdout.write(mutedMarkdown(text) + "\n\n");
|
|
394
|
+
dockReplaceTransient(mutedMarkdown(text) + "\n");
|
|
421
395
|
return [compacted, true];
|
|
422
396
|
}
|
|
423
397
|
async executeQuery(userPrompt, inputItems, story, pasteSpans) {
|
|
424
398
|
this.inQuery = true;
|
|
425
|
-
this.workActive = false;
|
|
426
|
-
this.workRows = 0;
|
|
427
399
|
this.interrupted = false;
|
|
428
400
|
this.queryHasOutput = false;
|
|
429
401
|
this.queryCalledTool = false;
|
|
@@ -553,36 +525,24 @@ export class InferenceEngine {
|
|
|
553
525
|
continue;
|
|
554
526
|
}
|
|
555
527
|
dropRetryPrompts(conversation, inputItems, retryPrompts);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
this.workActive = false;
|
|
559
|
-
this.workRows = 0;
|
|
528
|
+
dockAppendContent(aiMarkdown(emptyMessage) + "\n");
|
|
529
|
+
dockAppendContent(summary() + "\n");
|
|
560
530
|
return;
|
|
561
531
|
}
|
|
562
532
|
if (!calls.length) {
|
|
563
|
-
// Exactly one blank row between the message and the footer summary.
|
|
564
|
-
const trailing = (text.match(/\n+$/) ?? [""])[0].length;
|
|
565
|
-
process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)) + summary() + "\n");
|
|
566
533
|
story.push({ type: "footer", text: footerText() });
|
|
567
534
|
attachFooter(conversation);
|
|
568
535
|
attachFooter(inputItems);
|
|
569
|
-
|
|
570
|
-
this.workRows = 0;
|
|
536
|
+
dockAppendContent(summary() + "\n");
|
|
571
537
|
return;
|
|
572
538
|
}
|
|
573
|
-
if (text.trim()) {
|
|
574
|
-
// Ensure exactly one blank row between the assistant message and the
|
|
575
|
-
// first tool call, no matter how many newlines the message ended with.
|
|
576
|
-
const trailing = (text.match(/\n+$/) ?? [""])[0].length;
|
|
577
|
-
process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)));
|
|
578
|
-
}
|
|
579
539
|
for (const c of calls) {
|
|
580
|
-
this.workActive = false;
|
|
581
540
|
this.queryCalledTool = true;
|
|
582
541
|
const started = toolCallLabel(c.name, c.arguments);
|
|
583
542
|
// Print the tool label the instant the command starts, then show a
|
|
584
543
|
// "╰─ Working..." dots line that swaps to the real result in place.
|
|
585
|
-
|
|
544
|
+
dockAppendContent(`${started}\n`);
|
|
545
|
+
dockTransientStart();
|
|
586
546
|
const toolSpinner = new Spinner();
|
|
587
547
|
toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
|
|
588
548
|
const rawResult = await this.runTool(c.name, c.arguments);
|
|
@@ -602,12 +562,13 @@ export class InferenceEngine {
|
|
|
602
562
|
});
|
|
603
563
|
// Swap the "╰─ Working..." line for the real result. If the tool
|
|
604
564
|
// produced a diff it renders directly below the action line with no
|
|
605
|
-
// gap; otherwise
|
|
606
|
-
toolSpinner.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
565
|
+
// gap; otherwise the dock gap separates the action from what follows.
|
|
566
|
+
toolSpinner.stop();
|
|
567
|
+
dockReplaceTransient(action + "\n");
|
|
568
|
+
if (diff) {
|
|
569
|
+
dockTransientStart();
|
|
570
|
+
dockReplaceTransient(diff + "\n");
|
|
571
|
+
}
|
|
611
572
|
const truncatedResult = c.name === "read_file"
|
|
612
573
|
? truncateToolOutput(rawResult, max_read_file_stored_chars)
|
|
613
574
|
: truncateToolOutput(rawResult);
|
|
@@ -634,10 +595,6 @@ export class InferenceEngine {
|
|
|
634
595
|
stripOrphanCalls(inputItems);
|
|
635
596
|
}
|
|
636
597
|
catch (err) {
|
|
637
|
-
if (this.interrupted) {
|
|
638
|
-
this.workActive = false;
|
|
639
|
-
this.workRows = 0;
|
|
640
|
-
}
|
|
641
598
|
if (this.interrupted)
|
|
642
599
|
throw new Error("interrupt");
|
|
643
600
|
if (err?.message === "interrupt" || err?.message === "eof") {
|
package/dist/ui.js
CHANGED
|
@@ -411,7 +411,7 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
411
411
|
return out.join("\n");
|
|
412
412
|
}
|
|
413
413
|
export function renderPanel(content, title = "", subtitle = "", expand = true, borderStyle = "90", titleAlign = "center") {
|
|
414
|
-
|
|
414
|
+
dockAppendContent(panelString(content, title, subtitle, expand, borderStyle, titleAlign) + "\n");
|
|
415
415
|
}
|
|
416
416
|
// ---------------------------------------------------------------------------
|
|
417
417
|
// Table Panel Rendering
|
|
@@ -462,7 +462,7 @@ export function tickDuration(seconds) {
|
|
|
462
462
|
export function printAiChunk(chunk) {
|
|
463
463
|
if (!chunk)
|
|
464
464
|
return;
|
|
465
|
-
|
|
465
|
+
dockAppendContent(markdownToAnsi(chunk));
|
|
466
466
|
}
|
|
467
467
|
const FENCE_MARKER_RE = /`{3,}/g;
|
|
468
468
|
export function safeCommitPoint(text) {
|
|
@@ -497,6 +497,156 @@ export function displayRows(text) {
|
|
|
497
497
|
return rows;
|
|
498
498
|
}
|
|
499
499
|
// ---------------------------------------------------------------------------
|
|
500
|
+
// Persistent Prompt Dock
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// The prompt box stays on screen at all times (docked). The cursor is parked
|
|
503
|
+
// at the box's TOP row; incoming content is written there and the box is
|
|
504
|
+
// re-rendered below it, so the box is pushed down by content while exactly
|
|
505
|
+
// one blank row is preserved between the latest content and the box.
|
|
506
|
+
//
|
|
507
|
+
// dockRel: how many rows the cursor sits above boxTop. 0 = parked at boxTop,
|
|
508
|
+
// 2 = transient content row (spinner/thinking line), with the blank gap row
|
|
509
|
+
// (boxTop-1) kept between it and the box.
|
|
510
|
+
// dockTransient: true while a transient spinner is parked on the content row.
|
|
511
|
+
// dockGap: true when a blank row already exists above the current row (after
|
|
512
|
+
// the box is torn down), so askBottomPrompt does not add a second one.
|
|
513
|
+
let docked = false;
|
|
514
|
+
let dockRel = 0;
|
|
515
|
+
let dockBoxRows = 0;
|
|
516
|
+
let dockGap = false;
|
|
517
|
+
let dockTransient = false;
|
|
518
|
+
let dockFrameLines = [];
|
|
519
|
+
export function dockIsDocked() {
|
|
520
|
+
return docked;
|
|
521
|
+
}
|
|
522
|
+
export function dockHasGap() {
|
|
523
|
+
return dockGap;
|
|
524
|
+
}
|
|
525
|
+
export function dockSetFrame(frame, totalRows) {
|
|
526
|
+
dockFrameLines = frame.split("\n");
|
|
527
|
+
dockBoxRows = totalRows;
|
|
528
|
+
}
|
|
529
|
+
export function dockSetInactive() {
|
|
530
|
+
docked = false;
|
|
531
|
+
dockRel = 0;
|
|
532
|
+
dockBoxRows = 0;
|
|
533
|
+
dockGap = false;
|
|
534
|
+
dockTransient = false;
|
|
535
|
+
dockFrameLines = [];
|
|
536
|
+
}
|
|
537
|
+
export function dockEnterDocked() {
|
|
538
|
+
docked = true;
|
|
539
|
+
dockRel = 0;
|
|
540
|
+
dockGap = false;
|
|
541
|
+
dockTransient = false;
|
|
542
|
+
}
|
|
543
|
+
/** Erase the docked box and leave the cursor at boxTop with the gap above it,
|
|
544
|
+
* so a pager/replay can write there and a later prompt re-docks cleanly. */
|
|
545
|
+
export function dockTearDown() {
|
|
546
|
+
const wasDocked = docked;
|
|
547
|
+
dockEraseBox();
|
|
548
|
+
if (wasDocked) {
|
|
549
|
+
dockGap = true;
|
|
550
|
+
dockBoxRows = 0;
|
|
551
|
+
dockFrameLines = [];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/** Make a content row two rows above the box for a transient spinner: push the
|
|
555
|
+
* box down one content row, then park the cursor on it with the blank gap
|
|
556
|
+
* row (boxTop-1) preserved between it and the box. Idempotent while parked. */
|
|
557
|
+
export function dockTransientStart() {
|
|
558
|
+
if (!docked || dockBoxRows < 1)
|
|
559
|
+
return;
|
|
560
|
+
if (dockTransient)
|
|
561
|
+
return;
|
|
562
|
+
dockEraseBox();
|
|
563
|
+
dockRenderBox();
|
|
564
|
+
process.stdout.write("\x1b[2A\r");
|
|
565
|
+
dockRel = 2;
|
|
566
|
+
dockTransient = true;
|
|
567
|
+
}
|
|
568
|
+
/** Erase the box and any transient rows above it, leaving the cursor at boxTop
|
|
569
|
+
* with the gap row intact above. */
|
|
570
|
+
function dockEraseBox() {
|
|
571
|
+
if (!docked || dockBoxRows < 1)
|
|
572
|
+
return;
|
|
573
|
+
const rel = dockRel;
|
|
574
|
+
process.stdout.write(`\x1b[${dockBoxRows - 1 + rel}B`);
|
|
575
|
+
for (let i = 0; i < dockBoxRows; i++) {
|
|
576
|
+
process.stdout.write("\r\x1b[K");
|
|
577
|
+
if (i < dockBoxRows - 1)
|
|
578
|
+
process.stdout.write("\x1b[1A");
|
|
579
|
+
}
|
|
580
|
+
if (rel > 0) {
|
|
581
|
+
for (let i = 0; i < rel; i++)
|
|
582
|
+
process.stdout.write("\x1b[1A\r\x1b[K");
|
|
583
|
+
process.stdout.write(`\x1b[${rel}B`);
|
|
584
|
+
}
|
|
585
|
+
docked = false;
|
|
586
|
+
dockRel = 0;
|
|
587
|
+
dockGap = true;
|
|
588
|
+
dockTransient = false;
|
|
589
|
+
}
|
|
590
|
+
function dockEraseBoxKeep() {
|
|
591
|
+
if (!docked || dockBoxRows < 1)
|
|
592
|
+
return;
|
|
593
|
+
process.stdout.write(`\x1b[${dockBoxRows - 1 + dockRel}B`);
|
|
594
|
+
for (let i = 0; i < dockBoxRows; i++) {
|
|
595
|
+
process.stdout.write("\r\x1b[K");
|
|
596
|
+
if (i < dockBoxRows - 1)
|
|
597
|
+
process.stdout.write("\x1b[1A");
|
|
598
|
+
}
|
|
599
|
+
process.stdout.write(`\x1b[${dockRel}A`);
|
|
600
|
+
docked = false;
|
|
601
|
+
dockRel = 0;
|
|
602
|
+
dockGap = true;
|
|
603
|
+
dockTransient = false;
|
|
604
|
+
}
|
|
605
|
+
/** Write exactly one blank row then the box frame below the current row, and
|
|
606
|
+
* park the cursor at the box top. */
|
|
607
|
+
function dockRenderBox() {
|
|
608
|
+
if (dockBoxRows < 1)
|
|
609
|
+
return;
|
|
610
|
+
process.stdout.write("\n\n" + dockFrameLines.join("\n"));
|
|
611
|
+
process.stdout.write(`\x1b[${dockBoxRows - 1}A\r`);
|
|
612
|
+
dockRel = 0;
|
|
613
|
+
docked = true;
|
|
614
|
+
dockGap = false;
|
|
615
|
+
dockTransient = false;
|
|
616
|
+
}
|
|
617
|
+
/** Commit content at the box top, pushing the box down, preserving exactly one
|
|
618
|
+
* blank row between the content and the box. */
|
|
619
|
+
export function dockAppendContent(text) {
|
|
620
|
+
if (!text)
|
|
621
|
+
return;
|
|
622
|
+
if (!docked) {
|
|
623
|
+
process.stdout.write(text);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
dockEraseBox();
|
|
627
|
+
process.stdout.write(text);
|
|
628
|
+
const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
|
|
629
|
+
if (tb > 0)
|
|
630
|
+
process.stdout.write(`\x1b[${tb}A`);
|
|
631
|
+
dockRenderBox();
|
|
632
|
+
}
|
|
633
|
+
/** Replace a transient spinner row (two rows above the box) with content,
|
|
634
|
+
* then re-dock, keeping exactly one blank row between content and box. */
|
|
635
|
+
export function dockReplaceTransient(text) {
|
|
636
|
+
if (!text)
|
|
637
|
+
return;
|
|
638
|
+
if (!docked) {
|
|
639
|
+
process.stdout.write(text);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
dockEraseBoxKeep();
|
|
643
|
+
process.stdout.write(text);
|
|
644
|
+
const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
|
|
645
|
+
if (tb > 0)
|
|
646
|
+
process.stdout.write(`\x1b[${tb}A`);
|
|
647
|
+
dockRenderBox();
|
|
648
|
+
}
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
500
650
|
// Cursor & Spinner
|
|
501
651
|
// ---------------------------------------------------------------------------
|
|
502
652
|
let cursorHidden = false;
|
|
@@ -1060,7 +1210,15 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1060
1210
|
process.stdin.setRawMode(false);
|
|
1061
1211
|
if (pasteBurstTimer)
|
|
1062
1212
|
clearTimeout(pasteBurstTimer);
|
|
1063
|
-
|
|
1213
|
+
if (isErr) {
|
|
1214
|
+
clearBox();
|
|
1215
|
+
dockSetInactive();
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
process.stdout.write(`\x1b[${lastCursorRow}A\r`);
|
|
1219
|
+
dockSetFrame((prevFrameLines ?? []).join("\n"), lastTotalRows);
|
|
1220
|
+
dockEnterDocked();
|
|
1221
|
+
}
|
|
1064
1222
|
process.stdin.removeListener("keypress", onKeypress);
|
|
1065
1223
|
process.stdin.pause();
|
|
1066
1224
|
showCursor();
|
|
@@ -1073,7 +1231,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1073
1231
|
const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints);
|
|
1074
1232
|
const newLines = frame.split("\n");
|
|
1075
1233
|
if (isFirst) {
|
|
1076
|
-
|
|
1234
|
+
if (dockIsDocked()) {
|
|
1235
|
+
dockEraseBox();
|
|
1236
|
+
}
|
|
1237
|
+
else if (!dockHasGap()) {
|
|
1238
|
+
process.stdout.write("\n");
|
|
1239
|
+
}
|
|
1077
1240
|
for (let i = 0; i < newLines.length; i++) {
|
|
1078
1241
|
process.stdout.write(newLines[i]);
|
|
1079
1242
|
if (i < newLines.length - 1)
|
|
@@ -1082,6 +1245,8 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1082
1245
|
prevFrameLines = newLines;
|
|
1083
1246
|
lastCursorRow = cursorRow;
|
|
1084
1247
|
lastTotalRows = totalRows;
|
|
1248
|
+
dockSetFrame(frame, totalRows);
|
|
1249
|
+
dockEnterDocked();
|
|
1085
1250
|
const up = totalRows - 1 - cursorRow;
|
|
1086
1251
|
process.stdout.write(`\x1b[${up}A\r\x1b[${cursorCol}C`);
|
|
1087
1252
|
}
|
|
@@ -1105,6 +1270,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1105
1270
|
prevFrameLines = newLines;
|
|
1106
1271
|
lastCursorRow = cursorRow;
|
|
1107
1272
|
lastTotalRows = totalRows;
|
|
1273
|
+
dockSetFrame(frame, totalRows);
|
|
1108
1274
|
}
|
|
1109
1275
|
};
|
|
1110
1276
|
const shiftSpansAfterInsert = (pos, delta) => {
|