@oxecli/oxe 1.0.97 → 1.0.99
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 +64 -13
- package/dist/config.js +3 -0
- package/dist/engine.js +24 -8
- package/dist/ui.js +81 -10
- 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, dockAppendContent, dockSetInactive, dockTearDown, startDraftCapture, } 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, renderWarning, enableRawStdin, disableRawStdin, waitRawKey, dockAppendContent, dockSetInactive, dockTearDown, dockRenderPrompt, startDraftCapture, } 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");
|
|
@@ -20,6 +20,13 @@ export class CLI {
|
|
|
20
20
|
promptHistory = [];
|
|
21
21
|
lastPrompt = "";
|
|
22
22
|
interruptPending = false;
|
|
23
|
+
/**
|
|
24
|
+
* When a prompt is submitted during an in-flight query (Enter while the AI is
|
|
25
|
+
* responding), we interrupt the query silently and queue the submitted prompt
|
|
26
|
+
* here so the next loop iteration processes it directly without showing the
|
|
27
|
+
* "Interrupted agent" alert.
|
|
28
|
+
*/
|
|
29
|
+
autoSubmit = null;
|
|
23
30
|
constructor() {
|
|
24
31
|
clearScreen();
|
|
25
32
|
this.renderHeader();
|
|
@@ -241,7 +248,7 @@ export class CLI {
|
|
|
241
248
|
process.stdout.write(mutedMarkdown(text) + "\n");
|
|
242
249
|
}
|
|
243
250
|
else if (typ === "warning") {
|
|
244
|
-
|
|
251
|
+
renderWarning(text);
|
|
245
252
|
}
|
|
246
253
|
else if (typ === "error") {
|
|
247
254
|
process.stdout.write(`\x1b[31m${text}\x1b[0m\n`);
|
|
@@ -404,14 +411,33 @@ export class CLI {
|
|
|
404
411
|
const pct = Math.max(0, Math.min(100, Math.round(((budget - used) / budget) * 100)));
|
|
405
412
|
// Queries run the proven settle-on-Enter flow: the prompt settles and
|
|
406
413
|
// the query streams above it (robust dock streaming). While the query
|
|
407
|
-
// runs,
|
|
408
|
-
//
|
|
409
|
-
|
|
414
|
+
// runs, the prompt box stays live-editable: typed keys are rendered
|
|
415
|
+
// into the box, and Enter submits a new prompt that interrupts the
|
|
416
|
+
// running response (no "Interrupted agent" alert) and is processed
|
|
417
|
+
// immediately. A plain (non-submitted) draft is restored into the next
|
|
418
|
+
// prompt box so input is never lost.
|
|
419
|
+
const hints = {
|
|
410
420
|
left: "\x1b[90mPress \x1b[1;36mCtrl+C\x1b[0m\x1b[90m to interrupt\x1b[0m",
|
|
411
421
|
right: `\x1b[1;36m${pct}%\x1b[0m \x1b[90muntil auto-compact\x1b[0m`,
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
422
|
+
};
|
|
423
|
+
let input;
|
|
424
|
+
let spans;
|
|
425
|
+
if (this.autoSubmit) {
|
|
426
|
+
// A prompt was submitted while the previous response was running. It
|
|
427
|
+
// has already interrupted that response; process it directly, first
|
|
428
|
+
// clearing the prompt box as a normal send would.
|
|
429
|
+
input = this.autoSubmit.input;
|
|
430
|
+
spans = this.autoSubmit.spans;
|
|
431
|
+
this.autoSubmit = null;
|
|
432
|
+
dockRenderPrompt("", [], "You", "❯", hints);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const got = await askBottomPrompt("You", "❯", this.promptHistory, hints, this.lastPrompt);
|
|
436
|
+
input = got[0];
|
|
437
|
+
spans = got[1];
|
|
438
|
+
if (!input.trim())
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
415
441
|
this.interruptPending = false;
|
|
416
442
|
this.promptHistory.push(input);
|
|
417
443
|
const lower = input.trim().toLowerCase();
|
|
@@ -463,18 +489,25 @@ export class CLI {
|
|
|
463
489
|
continue;
|
|
464
490
|
}
|
|
465
491
|
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
466
|
-
// Empty the prompt field as soon as the prompt is sent.
|
|
467
|
-
//
|
|
492
|
+
// Empty the prompt field as soon as the prompt is sent. A draft typed
|
|
493
|
+
// while the query runs is rendered into the box and restored into the
|
|
494
|
+
// next one; Enter submits it as a new prompt.
|
|
468
495
|
this.lastPrompt = "";
|
|
469
496
|
queryStarted = true;
|
|
470
|
-
const stopCapture = startDraftCapture(() => engine.interrupt());
|
|
497
|
+
const stopCapture = startDraftCapture(() => engine.interrupt(), (buf, sp) => dockRenderPrompt(buf, sp, "You", "❯", hints));
|
|
471
498
|
try {
|
|
472
499
|
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
473
500
|
}
|
|
474
501
|
finally {
|
|
475
502
|
const captured = stopCapture();
|
|
476
|
-
if (captured.trim())
|
|
477
|
-
|
|
503
|
+
if (captured.submitted && captured.draft.trim()) {
|
|
504
|
+
// The user pressed Enter during the response: interrupt it and queue
|
|
505
|
+
// the new prompt so the next loop iteration sends it (silently).
|
|
506
|
+
this.autoSubmit = { input: captured.draft, spans: [] };
|
|
507
|
+
}
|
|
508
|
+
else if (captured.draft.trim()) {
|
|
509
|
+
this.lastPrompt = captured.draft;
|
|
510
|
+
}
|
|
478
511
|
queryStarted = false;
|
|
479
512
|
}
|
|
480
513
|
this.saveSession(engine);
|
|
@@ -488,6 +521,24 @@ export class CLI {
|
|
|
488
521
|
}
|
|
489
522
|
if (err?.message === "interrupt") {
|
|
490
523
|
const wasActive = queryStarted || engine.inQuery;
|
|
524
|
+
// A prompt submitted during the response was queued in `autoSubmit`
|
|
525
|
+
// (by the finally above). Interrupt silently — no "Interrupted agent"
|
|
526
|
+
// alert — and let the next iteration process the new prompt (the loop
|
|
527
|
+
// top consumes `autoSubmit`).
|
|
528
|
+
if (this.autoSubmit && this.autoSubmit.input.trim()) {
|
|
529
|
+
engine.inQuery = false;
|
|
530
|
+
stripOrphanCalls(this.inputItems);
|
|
531
|
+
if (this.inputItems.length &&
|
|
532
|
+
this.inputItems[this.inputItems.length - 1]["role"] === "user") {
|
|
533
|
+
this.inputItems.pop();
|
|
534
|
+
}
|
|
535
|
+
if (this.story.length &&
|
|
536
|
+
this.story[this.story.length - 1]["type"] === "user") {
|
|
537
|
+
this.story.pop();
|
|
538
|
+
}
|
|
539
|
+
this.interruptPending = false;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
491
542
|
if (wasActive && !engine.queryHasOutput && !engine.queryCalledTool) {
|
|
492
543
|
dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
|
|
493
544
|
}
|
package/dist/config.js
CHANGED
|
@@ -28,6 +28,9 @@ export const context_overhead_margin = 32_000;
|
|
|
28
28
|
export const compact_keep_recent_turns = 4;
|
|
29
29
|
export const max_summary_source_chars = 200_000;
|
|
30
30
|
export const max_output_tokens = 16384;
|
|
31
|
+
/** Cap on how many times a single turn may be continued past the output-token
|
|
32
|
+
* cap before we give up extending it (prevents runaway generation). */
|
|
33
|
+
export const max_output_continuations = 6;
|
|
31
34
|
export const max_empty_retries = 2;
|
|
32
35
|
export const max_diff_source_chars = 1_000_000;
|
|
33
36
|
export const max_grep_file_bytes = 20 * 1024 * 1024;
|
package/dist/engine.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
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";
|
|
2
|
+
import { max_output_tokens, max_empty_retries, max_output_continuations, 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, safeCommitPoint, printAiChunk, toolCallLabel, formatToolResult, renderPanel, Spinner, hideCursor, dockAppendContent, dockReplaceTransient, dockTransientStart, } from "./ui.js";
|
|
5
|
+
import { mutedMarkdown, aiMarkdown, tickDuration, safeCommitPoint, printAiChunk, toolCallLabel, formatToolResult, renderPanel, renderWarning, 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";
|
|
@@ -315,10 +315,13 @@ export class InferenceEngine {
|
|
|
315
315
|
const text = response?.output_text ?? content;
|
|
316
316
|
if (text.trim())
|
|
317
317
|
story.push({ type: "assistant", text });
|
|
318
|
-
|
|
318
|
+
// The incomplete reason must always be captured — not only in the empty
|
|
319
|
+
// case — so a response truncated by the output-token cap is never mistaken
|
|
320
|
+
// for a complete answer. `emptyMessage` is only relevant when nothing at
|
|
321
|
+
// all (text or tool call) was produced.
|
|
322
|
+
const incompleteReason = this.incompleteReason(response);
|
|
319
323
|
let emptyMessage = null;
|
|
320
324
|
if (!text.trim() && calls.length === 0) {
|
|
321
|
-
incompleteReason = this.incompleteReason(response);
|
|
322
325
|
emptyMessage = this.diagnoseEmptyResponse(response, sawReasoning);
|
|
323
326
|
}
|
|
324
327
|
return {
|
|
@@ -428,6 +431,7 @@ export class InferenceEngine {
|
|
|
428
431
|
let conversation = [...inputItems];
|
|
429
432
|
[conversation] = await this.compactIfNeeded(conversation, inputItems, story);
|
|
430
433
|
let emptyRetries = 0;
|
|
434
|
+
let outputContinuations = 0;
|
|
431
435
|
const retryPrompts = [];
|
|
432
436
|
const stats = {
|
|
433
437
|
thinking: 0,
|
|
@@ -524,6 +528,20 @@ export class InferenceEngine {
|
|
|
524
528
|
prevId = null;
|
|
525
529
|
pending = conversation;
|
|
526
530
|
}
|
|
531
|
+
// A response cut off by the output-token cap is NOT a final answer —
|
|
532
|
+
// treating it as one is what returned force-truncated replies. Instead,
|
|
533
|
+
// continue generating from the previous response via `previous_response_id`
|
|
534
|
+
// until the response completes naturally. Bounded to prevent runaway
|
|
535
|
+
// output. Each continuation's output items are already committed above,
|
|
536
|
+
// so the full (concatenated) response is preserved in the transcript.
|
|
537
|
+
if (!compactedNow &&
|
|
538
|
+
incompleteReason === "max_output_tokens" &&
|
|
539
|
+
outputContinuations < max_output_continuations) {
|
|
540
|
+
outputContinuations++;
|
|
541
|
+
prevId = respId;
|
|
542
|
+
pending = [];
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
527
545
|
if (emptyMessage !== null) {
|
|
528
546
|
if (incompleteReason === "max_output_tokens" &&
|
|
529
547
|
emptyRetries < max_empty_retries) {
|
|
@@ -617,12 +635,10 @@ export class InferenceEngine {
|
|
|
617
635
|
// The preceding tool action already leaves the cursor on a blank row, so
|
|
618
636
|
// no extra newline is needed above; renderPanel ends with its own "\n",
|
|
619
637
|
// and the prompt adds one leading "\n" -> exactly one blank row below.
|
|
620
|
-
const maxIterationMsg = "
|
|
638
|
+
const maxIterationMsg = "Max tool-call iterations reached for this turn. The work so far is " +
|
|
621
639
|
"saved. If you want the agent to keep going, type continue and the next " +
|
|
622
640
|
"step will resume from where it left off.";
|
|
623
|
-
|
|
624
|
-
"[yellow]The work so far is saved. If you want the agent to keep going, type " +
|
|
625
|
-
"[bold]continue[/bold] and the next step will resume from where it left off.[/yellow]", "Warning", "", false, "33");
|
|
641
|
+
renderWarning(maxIterationMsg);
|
|
626
642
|
story.push({ type: "warning", text: maxIterationMsg });
|
|
627
643
|
if (retryPrompts.length)
|
|
628
644
|
dropRetryPrompts(conversation, inputItems, retryPrompts);
|
package/dist/ui.js
CHANGED
|
@@ -39,14 +39,18 @@ export function waitRawKey() {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Capture typed input while a query runs (the prompt box is
|
|
43
|
-
* streaming). Keystrokes are accumulated into a draft
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Capture typed input while a query runs (the prompt box is docked during
|
|
43
|
+
* streaming). Keystrokes are accumulated into a draft AND rendered into the
|
|
44
|
+
* docked prompt box via `renderBox`, so the user sees their input live while
|
|
45
|
+
* the AI is busy. Enter submits the draft: it interrupts the running query
|
|
46
|
+
* (as if Ctrl+C) and flags `submitted`, so the caller can send the new prompt.
|
|
47
|
+
* Returns a stop() function returning `{ draft, submitted }`.
|
|
46
48
|
*/
|
|
47
|
-
export function startDraftCapture(onInterrupt) {
|
|
49
|
+
export function startDraftCapture(onInterrupt, renderBox) {
|
|
48
50
|
let draft = "";
|
|
51
|
+
let pasteSpans = [];
|
|
49
52
|
let active = true;
|
|
53
|
+
let submitted = false;
|
|
50
54
|
const onKey = (str, key) => {
|
|
51
55
|
if (!active)
|
|
52
56
|
return;
|
|
@@ -56,14 +60,24 @@ export function startDraftCapture(onInterrupt) {
|
|
|
56
60
|
}
|
|
57
61
|
if (key && key.ctrl && (key.name === "d" || key.name === "z"))
|
|
58
62
|
return;
|
|
63
|
+
if (isEnterKey(str, key)) {
|
|
64
|
+
if (draft.trim()) {
|
|
65
|
+
submitted = true;
|
|
66
|
+
active = false;
|
|
67
|
+
onInterrupt();
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key && (key.name === "enter" || key.name === "linefeed"))
|
|
72
|
+
return;
|
|
59
73
|
if (key && key.name === "backspace") {
|
|
60
74
|
draft = draft.slice(0, -1);
|
|
75
|
+
renderBox?.(draft, pasteSpans);
|
|
61
76
|
return;
|
|
62
77
|
}
|
|
63
|
-
if (key && (key.name === "return" || key.name === "enter"))
|
|
64
|
-
return;
|
|
65
78
|
if (str && !key?.ctrl && !key?.meta) {
|
|
66
79
|
draft += str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
80
|
+
renderBox?.(draft, pasteSpans);
|
|
67
81
|
}
|
|
68
82
|
};
|
|
69
83
|
readline.emitKeypressEvents(process.stdin);
|
|
@@ -81,7 +95,7 @@ export function startDraftCapture(onInterrupt) {
|
|
|
81
95
|
/* ignore */
|
|
82
96
|
}
|
|
83
97
|
process.stdin.pause();
|
|
84
|
-
return draft;
|
|
98
|
+
return { draft, submitted };
|
|
85
99
|
};
|
|
86
100
|
}
|
|
87
101
|
// ---------------------------------------------------------------------------
|
|
@@ -465,6 +479,25 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
465
479
|
export function renderPanel(content, title = "", subtitle = "", expand = true, borderStyle = "90", titleAlign = "center") {
|
|
466
480
|
dockAppendContent(panelString(content, title, subtitle, expand, borderStyle, titleAlign) + "\n");
|
|
467
481
|
}
|
|
482
|
+
/** Warning sign with the text-presentation variation selector, so it renders
|
|
483
|
+
* as a monochrome icon (matching the app's ✓/✗ symbols) rather than a color
|
|
484
|
+
* emoji that varies by platform. */
|
|
485
|
+
export const WARNING_ICON = "\u26A0\uFE0E";
|
|
486
|
+
/**
|
|
487
|
+
* Render a warning as a framed panel (consistent shape whether shown live or
|
|
488
|
+
* replayed from a resumed session), using the text-presentation warning icon
|
|
489
|
+
* instead of an emoji. The first sentence is emphasized; the rest is plain.
|
|
490
|
+
*/
|
|
491
|
+
export function renderWarning(message) {
|
|
492
|
+
const text = String(message ?? "").replace(/^[\s\u26A0\uFE0E]+/, "").trim();
|
|
493
|
+
const idx = text.indexOf(". ");
|
|
494
|
+
const head = idx === -1 ? text : text.slice(0, idx + 1);
|
|
495
|
+
const body = idx === -1 ? "" : text.slice(idx + 2).trim();
|
|
496
|
+
let content = `[bold yellow]${WARNING_ICON} ${head}[/bold yellow]`;
|
|
497
|
+
if (body)
|
|
498
|
+
content += `\n[yellow]${body}[/yellow]`;
|
|
499
|
+
renderPanel(content, "Warning", "", false, "33");
|
|
500
|
+
}
|
|
468
501
|
// ---------------------------------------------------------------------------
|
|
469
502
|
// Table Panel Rendering
|
|
470
503
|
// ---------------------------------------------------------------------------
|
|
@@ -568,6 +601,13 @@ let dockBoxRows = 0;
|
|
|
568
601
|
let dockGap = false;
|
|
569
602
|
let dockTransient = false;
|
|
570
603
|
let dockFrameLines = [];
|
|
604
|
+
/**
|
|
605
|
+
* How many rows the caret sits BELOW boxTop when the prompt box is being
|
|
606
|
+
* live-edited during a query (0 = cursor parked at boxTop / transient, the
|
|
607
|
+
* normal dock invariant). dockEraseBox must know this to erase the box from
|
|
608
|
+
* the caret's position instead of assuming the cursor is at boxTop.
|
|
609
|
+
*/
|
|
610
|
+
let dockCaretBelow = 0;
|
|
571
611
|
export function dockIsDocked() {
|
|
572
612
|
return docked;
|
|
573
613
|
}
|
|
@@ -585,12 +625,14 @@ export function dockSetInactive() {
|
|
|
585
625
|
dockGap = false;
|
|
586
626
|
dockTransient = false;
|
|
587
627
|
dockFrameLines = [];
|
|
628
|
+
dockCaretBelow = 0;
|
|
588
629
|
}
|
|
589
630
|
export function dockEnterDocked() {
|
|
590
631
|
docked = true;
|
|
591
632
|
dockRel = 0;
|
|
592
633
|
dockGap = false;
|
|
593
634
|
dockTransient = false;
|
|
635
|
+
dockCaretBelow = 0;
|
|
594
636
|
}
|
|
595
637
|
/** Erase the docked box and leave the cursor at boxTop with the gap above it,
|
|
596
638
|
* so a pager/replay can write there and a later prompt re-docks cleanly. */
|
|
@@ -639,12 +681,18 @@ export function dockTransientStart(mode = "blank") {
|
|
|
639
681
|
dockTransient = true;
|
|
640
682
|
}
|
|
641
683
|
/** Erase the box and any transient rows above it, leaving the cursor at boxTop
|
|
642
|
-
* with the gap row intact above.
|
|
684
|
+
* with the gap row intact above. Handles the cursor being parked at boxTop,
|
|
685
|
+
* above it (transient), or inside it at the live-edit caret. */
|
|
643
686
|
function dockEraseBox() {
|
|
644
687
|
if (!docked || dockBoxRows < 1)
|
|
645
688
|
return;
|
|
646
689
|
const rel = dockRel;
|
|
647
|
-
|
|
690
|
+
const caret = dockCaretBelow;
|
|
691
|
+
// Move from the cursor to the box's bottom row, then erase the box upward.
|
|
692
|
+
// cursor at boxTop - rel (transient): go down (dockBoxRows - 1 + rel)
|
|
693
|
+
// cursor at boxTop + caret (live caret): go down (dockBoxRows - 1 - caret)
|
|
694
|
+
const down = caret > 0 ? dockBoxRows - 1 - caret : dockBoxRows - 1 + rel;
|
|
695
|
+
process.stdout.write(`\x1b[${down}B`);
|
|
648
696
|
for (let i = 0; i < dockBoxRows; i++) {
|
|
649
697
|
process.stdout.write("\r\x1b[K");
|
|
650
698
|
if (i < dockBoxRows - 1)
|
|
@@ -657,6 +705,7 @@ function dockEraseBox() {
|
|
|
657
705
|
}
|
|
658
706
|
docked = false;
|
|
659
707
|
dockRel = 0;
|
|
708
|
+
dockCaretBelow = 0;
|
|
660
709
|
dockGap = true;
|
|
661
710
|
dockTransient = false;
|
|
662
711
|
}
|
|
@@ -674,6 +723,7 @@ function dockEraseBoxKeep() {
|
|
|
674
723
|
process.stdout.write(`\x1b[${dockRel}A`);
|
|
675
724
|
docked = false;
|
|
676
725
|
dockRel = 0;
|
|
726
|
+
dockCaretBelow = 0;
|
|
677
727
|
dockGap = true;
|
|
678
728
|
dockTransient = false;
|
|
679
729
|
}
|
|
@@ -685,10 +735,31 @@ function dockRenderBox() {
|
|
|
685
735
|
process.stdout.write("\n\n" + dockFrameLines.join("\n"));
|
|
686
736
|
process.stdout.write(`\x1b[${dockBoxRows - 1}A\r`);
|
|
687
737
|
dockRel = 0;
|
|
738
|
+
dockCaretBelow = 0;
|
|
688
739
|
docked = true;
|
|
689
740
|
dockGap = false;
|
|
690
741
|
dockTransient = false;
|
|
691
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Redraw the docked prompt box with the given buffer/caret, keeping whatever
|
|
745
|
+
* content already streams above it. Used to make the prompt field live-edit
|
|
746
|
+
* while a query runs. Erases the box (and any transient row above it),
|
|
747
|
+
* writes the new frame, and parks the cursor at the caret. Preserves exactly
|
|
748
|
+
* one blank row between the last content and the box.
|
|
749
|
+
*/
|
|
750
|
+
export function dockRenderPrompt(buffer, pasteSpans, prefix, label, hints) {
|
|
751
|
+
if (!docked || dockBoxRows < 1)
|
|
752
|
+
return;
|
|
753
|
+
const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, buffer.length, hints);
|
|
754
|
+
dockEraseBox();
|
|
755
|
+
process.stdout.write(frame);
|
|
756
|
+
// Park the cursor at the caret inside the box (cursorRow is 1-indexed, so
|
|
757
|
+
// the caret sits cursorRow rows below boxTop).
|
|
758
|
+
process.stdout.write(`\x1b[${totalRows - 1 - cursorRow}A\r\x1b[${cursorCol}C`);
|
|
759
|
+
dockSetFrame(frame, totalRows);
|
|
760
|
+
dockEnterDocked();
|
|
761
|
+
dockCaretBelow = cursorRow;
|
|
762
|
+
}
|
|
692
763
|
/** Commit content at the box top, pushing the box down, preserving exactly one
|
|
693
764
|
* blank row between the content and the box. */
|
|
694
765
|
export function dockAppendContent(text) {
|