@oxecli/oxe 1.0.88 → 1.0.89
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 +42 -6
- package/dist/config.js +1 -1
- package/dist/engine.js +4 -4
- package/dist/tools.js +37 -1
- package/dist/ui.js +111 -70
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ export class CLI {
|
|
|
18
18
|
story = [];
|
|
19
19
|
sessionId = null;
|
|
20
20
|
promptHistory = [];
|
|
21
|
+
lastPrompt = "";
|
|
21
22
|
interruptPending = false;
|
|
22
23
|
constructor() {
|
|
23
24
|
clearScreen();
|
|
@@ -395,10 +396,50 @@ export class CLI {
|
|
|
395
396
|
const used = estimateTokens(this.inputItems);
|
|
396
397
|
const budget = max_context_tokens - context_overhead_margin;
|
|
397
398
|
const pct = Math.max(0, Math.min(100, Math.round(((budget - used) / budget) * 100)));
|
|
399
|
+
// Queries run through onSubmit so the prompt box stays focused and
|
|
400
|
+
// editable while the AI streams its reply and tool calls above it.
|
|
401
|
+
// Commands (slash inputs) settle back to this loop for handling.
|
|
402
|
+
const onSubmit = async (input, spans) => {
|
|
403
|
+
this.interruptPending = false;
|
|
404
|
+
this.promptHistory.push(input);
|
|
405
|
+
this.lastPrompt = input;
|
|
406
|
+
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
407
|
+
queryStarted = true;
|
|
408
|
+
try {
|
|
409
|
+
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
410
|
+
}
|
|
411
|
+
catch (qerr) {
|
|
412
|
+
if (qerr?.message === "interrupt") {
|
|
413
|
+
if (!engine.queryHasOutput && !engine.queryCalledTool) {
|
|
414
|
+
dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
|
|
415
|
+
}
|
|
416
|
+
engine.inQuery = false;
|
|
417
|
+
stripOrphanCalls(this.inputItems);
|
|
418
|
+
if (this.inputItems.length &&
|
|
419
|
+
this.inputItems[this.inputItems.length - 1]["role"] === "user") {
|
|
420
|
+
this.inputItems.pop();
|
|
421
|
+
}
|
|
422
|
+
if (this.story.length &&
|
|
423
|
+
this.story[this.story.length - 1]["type"] === "user") {
|
|
424
|
+
this.story.pop();
|
|
425
|
+
}
|
|
426
|
+
this.interruptPending = true;
|
|
427
|
+
dockAppendContent("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
|
|
428
|
+
}
|
|
429
|
+
else if (qerr?.message !== "eof") {
|
|
430
|
+
throw qerr;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
queryStarted = false;
|
|
435
|
+
}
|
|
436
|
+
this.saveSession(engine);
|
|
437
|
+
};
|
|
438
|
+
const onInterrupt = () => engine.interrupt();
|
|
398
439
|
const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory, {
|
|
399
440
|
left: "\x1b[90mPress \x1b[1;36mCtrl+C\x1b[0m\x1b[90m to interrupt\x1b[0m",
|
|
400
441
|
right: `\x1b[1;36m${pct}%\x1b[0m \x1b[90muntil auto-compact\x1b[0m`,
|
|
401
|
-
});
|
|
442
|
+
}, this.lastPrompt, onSubmit, onInterrupt);
|
|
402
443
|
if (!input.trim())
|
|
403
444
|
continue;
|
|
404
445
|
this.interruptPending = false;
|
|
@@ -450,11 +491,6 @@ export class CLI {
|
|
|
450
491
|
}
|
|
451
492
|
continue;
|
|
452
493
|
}
|
|
453
|
-
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
454
|
-
queryStarted = true;
|
|
455
|
-
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
456
|
-
queryStarted = false;
|
|
457
|
-
this.saveSession(engine);
|
|
458
494
|
}
|
|
459
495
|
catch (err) {
|
|
460
496
|
if (err?.message === "eof") {
|
package/dist/config.js
CHANGED
|
@@ -191,7 +191,7 @@ export async function loadOrPrompt() {
|
|
|
191
191
|
break;
|
|
192
192
|
}
|
|
193
193
|
else {
|
|
194
|
-
process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n
|
|
194
|
+
process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n`);
|
|
195
195
|
api_key = "";
|
|
196
196
|
}
|
|
197
197
|
}
|
package/dist/engine.js
CHANGED
|
@@ -542,7 +542,7 @@ export class InferenceEngine {
|
|
|
542
542
|
// Print the tool label the instant the command starts, then show a
|
|
543
543
|
// "╰─ Working..." dots line that swaps to the real result in place.
|
|
544
544
|
dockAppendContent(`${started}\n`);
|
|
545
|
-
dockTransientStart();
|
|
545
|
+
dockTransientStart("flush");
|
|
546
546
|
const toolSpinner = new Spinner();
|
|
547
547
|
toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
|
|
548
548
|
const rawResult = await this.runTool(c.name, c.arguments);
|
|
@@ -564,10 +564,10 @@ export class InferenceEngine {
|
|
|
564
564
|
// produced a diff it renders directly below the action line with no
|
|
565
565
|
// gap; otherwise the dock gap separates the action from what follows.
|
|
566
566
|
toolSpinner.stop();
|
|
567
|
-
dockReplaceTransient(action
|
|
567
|
+
dockReplaceTransient(action);
|
|
568
568
|
if (diff) {
|
|
569
|
-
dockTransientStart();
|
|
570
|
-
dockReplaceTransient(diff
|
|
569
|
+
dockTransientStart("flush");
|
|
570
|
+
dockReplaceTransient(diff);
|
|
571
571
|
}
|
|
572
572
|
const truncatedResult = c.name === "read_file"
|
|
573
573
|
? truncateToolOutput(rawResult, max_read_file_stored_chars)
|
package/dist/tools.js
CHANGED
|
@@ -31,6 +31,36 @@ function truncateDiffLine(line) {
|
|
|
31
31
|
}
|
|
32
32
|
return line;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Recolor bracket syntax characters (`( ) [ ] { }`) inside an already-ANSI
|
|
36
|
+
* rendered string to `fg`, restoring the row's white base after each bracket.
|
|
37
|
+
* ANSI escape sequences are skipped so the injected color codes are never
|
|
38
|
+
* corrupted. Used so a deleted (red) diff row's bracket syntax matches the
|
|
39
|
+
* red of its line number instead of inheriting the syntax highlighter's color.
|
|
40
|
+
*/
|
|
41
|
+
function recolorBrackets(text, fg, base) {
|
|
42
|
+
let out = "";
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < text.length) {
|
|
45
|
+
if (text[i] === "\x1b") {
|
|
46
|
+
const m = text.slice(i).match(/^\x1b\[[0-9;]*[a-zA-Z]/);
|
|
47
|
+
if (m) {
|
|
48
|
+
out += m[0];
|
|
49
|
+
i += m[0].length;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const ch = text[i];
|
|
54
|
+
if (ch === "(" || ch === ")" || ch === "[" || ch === "]" || ch === "{" || ch === "}") {
|
|
55
|
+
out += fg + ch + base;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
out += ch;
|
|
59
|
+
}
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
34
64
|
// Diff output is deferred (not written straight to stdout) so it doesn't
|
|
35
65
|
// collide with the tool spinner that is still animating while the tool runs.
|
|
36
66
|
const pendingDiffOutput = [];
|
|
@@ -148,8 +178,14 @@ function displayDiff(pathName, oldContent, newContent) {
|
|
|
148
178
|
// the whole body.
|
|
149
179
|
const raw = highlightCodeLine(v.body);
|
|
150
180
|
const body = truncateStyled(raw.replace(/\x1b\[0m/g, `\x1b[0m${bg}\x1b[97m`), bodyMax);
|
|
181
|
+
// Deleted rows: recolor bracket syntax to the same red as the line
|
|
182
|
+
// number (fg) so brackets match the row's deleted color. Added rows keep
|
|
183
|
+
// their green highlighter as-is.
|
|
184
|
+
const shown = isAdd
|
|
185
|
+
? body
|
|
186
|
+
: recolorBrackets(body, fg, "\x1b[97m");
|
|
151
187
|
const fill = Math.max(0, termW - ind.length - numW - 4 - plainLen(body));
|
|
152
|
-
pendingDiffOutput.push(`${ind}${bg}${fg}${numStr} ${sign} \x1b[0m${bg}\x1b[97m${
|
|
188
|
+
pendingDiffOutput.push(`${ind}${bg}${fg}${numStr} ${sign} \x1b[0m${bg}\x1b[97m${shown}${bg}\x1b[97m${" ".repeat(fill)}\x1b[0m`);
|
|
153
189
|
}
|
|
154
190
|
else {
|
|
155
191
|
const raw = v.kind === "\\" ? v.body : highlightCodeLine(v.body);
|
package/dist/ui.js
CHANGED
|
@@ -293,6 +293,12 @@ export function markdownToAnsi(text) {
|
|
|
293
293
|
out.push(`\x1b[90m${"─".repeat(Math.min(width, 60))}\x1b[0m`);
|
|
294
294
|
continue;
|
|
295
295
|
}
|
|
296
|
+
// Collapse consecutive blank lines to a single empty row (never a leading one)
|
|
297
|
+
if (!raw.trim()) {
|
|
298
|
+
if (out.length && out[out.length - 1] !== "")
|
|
299
|
+
out.push("");
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
296
302
|
let line = raw;
|
|
297
303
|
// Headers (render as styled headings with the '#' markers stripped)
|
|
298
304
|
const h1 = line.match(/^#\s+(.+)$/);
|
|
@@ -551,17 +557,20 @@ export function dockTearDown() {
|
|
|
551
557
|
dockFrameLines = [];
|
|
552
558
|
}
|
|
553
559
|
}
|
|
554
|
-
/**
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
|
|
560
|
+
/** Park a transient spinner two rows above the box (dockRel=2), keeping boxTop-1
|
|
561
|
+
* as the single blank gap. "flush" parks the spinner directly under the
|
|
562
|
+
* preceding content (label, spinner, blank, box); "blank" gives it its own
|
|
563
|
+
* blank row above (echo, blank, spinner, blank, box). Idempotent while parked. */
|
|
564
|
+
export function dockTransientStart(mode = "blank") {
|
|
558
565
|
if (!docked || dockBoxRows < 1)
|
|
559
566
|
return;
|
|
560
567
|
if (dockTransient)
|
|
561
568
|
return;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
569
|
+
const insert = mode === "flush" ? 1 : 2;
|
|
570
|
+
process.stdout.write(`\x1b[${insert}L`);
|
|
571
|
+
if (mode === "flush")
|
|
572
|
+
process.stdout.write("\x1b[1A");
|
|
573
|
+
process.stdout.write("\r");
|
|
565
574
|
dockRel = 2;
|
|
566
575
|
dockTransient = true;
|
|
567
576
|
}
|
|
@@ -587,35 +596,9 @@ function dockEraseBox() {
|
|
|
587
596
|
dockGap = true;
|
|
588
597
|
dockTransient = false;
|
|
589
598
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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. */
|
|
599
|
+
/** Commit content at the box top: insert `rows+1` lines to push the box down
|
|
600
|
+
* (no erase/redraw), write the body, leave exactly one blank row between the
|
|
601
|
+
* content and the box, and park the cursor at the new boxTop. */
|
|
619
602
|
export function dockAppendContent(text) {
|
|
620
603
|
if (!text)
|
|
621
604
|
return;
|
|
@@ -623,15 +606,18 @@ export function dockAppendContent(text) {
|
|
|
623
606
|
process.stdout.write(text);
|
|
624
607
|
return;
|
|
625
608
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
609
|
+
const body = text.replace(/\n+$/, "");
|
|
610
|
+
if (!body)
|
|
611
|
+
return;
|
|
612
|
+
const rows = displayRows(body);
|
|
613
|
+
process.stdout.write(`\x1b[${rows + 1}L`);
|
|
614
|
+
process.stdout.write(body + "\n\r");
|
|
615
|
+
process.stdout.write("\x1b[1B\r");
|
|
632
616
|
}
|
|
633
|
-
/** Replace a transient spinner row (two rows above the box) with
|
|
634
|
-
*
|
|
617
|
+
/** Replace a transient spinner row (dockRel=2, two rows above the box) with
|
|
618
|
+
* real (possibly multi-row) content: erase the spinner line, push the box
|
|
619
|
+
* down by `rows-1` if the content is taller than the transient row, write the
|
|
620
|
+
* content, and park the cursor at the boxTop with one blank row above it. */
|
|
635
621
|
export function dockReplaceTransient(text) {
|
|
636
622
|
if (!text)
|
|
637
623
|
return;
|
|
@@ -639,12 +625,22 @@ export function dockReplaceTransient(text) {
|
|
|
639
625
|
process.stdout.write(text);
|
|
640
626
|
return;
|
|
641
627
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
628
|
+
if (!dockTransient) {
|
|
629
|
+
dockAppendContent(text);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const body = text.replace(/\n+$/, "");
|
|
633
|
+
if (!body)
|
|
634
|
+
return;
|
|
635
|
+
const rows = displayRows(body);
|
|
636
|
+
const push = rows + 1 - dockRel;
|
|
637
|
+
process.stdout.write("\r\x1b[K");
|
|
638
|
+
if (push > 0)
|
|
639
|
+
process.stdout.write(`\x1b[${push}L`);
|
|
640
|
+
process.stdout.write(body);
|
|
641
|
+
process.stdout.write("\x1b[2B\r");
|
|
642
|
+
dockRel = 0;
|
|
643
|
+
dockTransient = false;
|
|
648
644
|
}
|
|
649
645
|
// ---------------------------------------------------------------------------
|
|
650
646
|
// Cursor & Spinner
|
|
@@ -925,9 +921,23 @@ export function cursorLineCol(buffer, cursor) {
|
|
|
925
921
|
function endsWithWs(s) {
|
|
926
922
|
return s.length > 0 && /[ \t\n]/.test(s[s.length - 1]);
|
|
927
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Whether a spacer should be inserted between the previous segment and the
|
|
926
|
+
* current one. A space is added when the previous segment doesn't already end
|
|
927
|
+
* in whitespace — EXCEPT for non-block (inline) pastes, which keep their
|
|
928
|
+
* leading space but never force a trailing space after them. Collapsed pasted
|
|
929
|
+
* blocks always get spaced on both sides.
|
|
930
|
+
*/
|
|
931
|
+
function joinNeedsSpace(prev) {
|
|
932
|
+
if (endsWithWs(prev[2]))
|
|
933
|
+
return false;
|
|
934
|
+
if (prev[1] === "text" && prev[3])
|
|
935
|
+
return false; // non-block inline paste
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
928
938
|
export function splitBlocks(buffer, pasteSpans) {
|
|
929
939
|
if (!buffer)
|
|
930
|
-
return [["", "text", ""]];
|
|
940
|
+
return [["", "text", "", false]];
|
|
931
941
|
const pts = new Set([0, buffer.length]);
|
|
932
942
|
for (const [s, e] of pasteSpans) {
|
|
933
943
|
pts.add(s);
|
|
@@ -944,10 +954,10 @@ export function splitBlocks(buffer, pasteSpans) {
|
|
|
944
954
|
const text = buffer.slice(a, b);
|
|
945
955
|
if (isPaste(a, b) && shouldCollapsePaste(text)) {
|
|
946
956
|
const n = text.split("\n").length;
|
|
947
|
-
segs.push(["", "collapsed", `[Pasted text, ${n} lines]
|
|
957
|
+
segs.push(["", "collapsed", `[Pasted text, ${n} lines]`, true]);
|
|
948
958
|
}
|
|
949
959
|
else {
|
|
950
|
-
segs.push(["", "text", text]);
|
|
960
|
+
segs.push(["", "text", text, isPaste(a, b)]);
|
|
951
961
|
}
|
|
952
962
|
}
|
|
953
963
|
return segs;
|
|
@@ -1078,8 +1088,8 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
1078
1088
|
}
|
|
1079
1089
|
}
|
|
1080
1090
|
for (let i = 0; i < segs.length; i++) {
|
|
1081
|
-
const [, kind, disp] = segs[i];
|
|
1082
|
-
if (i &&
|
|
1091
|
+
const [, kind, disp, pasted] = segs[i];
|
|
1092
|
+
if (i && joinNeedsSpace(segs[i - 1]))
|
|
1083
1093
|
runs.push({ text: " ", style: "" });
|
|
1084
1094
|
if (i === target) {
|
|
1085
1095
|
if (kind === "collapsed") {
|
|
@@ -1153,7 +1163,7 @@ function isEnterKey(str, key) {
|
|
|
1153
1163
|
seq === "\x1bOM" // application keypad
|
|
1154
1164
|
);
|
|
1155
1165
|
}
|
|
1156
|
-
export function askBottomPrompt(label = "You", prefix = "❯", history = [], hints) {
|
|
1166
|
+
export function askBottomPrompt(label = "You", prefix = "❯", history = [], hints, initialBuffer = "", onSubmit, onInterrupt) {
|
|
1157
1167
|
return new Promise((resolve, reject) => {
|
|
1158
1168
|
const isTTY = process.stdin.isTTY;
|
|
1159
1169
|
if (!isTTY) {
|
|
@@ -1169,8 +1179,8 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1169
1179
|
});
|
|
1170
1180
|
return;
|
|
1171
1181
|
}
|
|
1172
|
-
let buffer =
|
|
1173
|
-
let cursor =
|
|
1182
|
+
let buffer = initialBuffer;
|
|
1183
|
+
let cursor = buffer.length;
|
|
1174
1184
|
let pasteSpans = [];
|
|
1175
1185
|
const hist = history.slice();
|
|
1176
1186
|
let histIdx = hist.length;
|
|
@@ -1191,6 +1201,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1191
1201
|
let lastCursorRow = 0;
|
|
1192
1202
|
let lastTotalRows = 0;
|
|
1193
1203
|
let prevFrameLines = null;
|
|
1204
|
+
// True while an onSubmit query is running. While it runs the box stays
|
|
1205
|
+
// focused and editable; Enter won't re-submit and Ctrl+C interrupts it.
|
|
1206
|
+
let submitting = false;
|
|
1194
1207
|
const clearBox = () => {
|
|
1195
1208
|
process.stdout.write(`\x1b[${lastCursorRow}A`);
|
|
1196
1209
|
for (let i = 0; i < lastTotalRows; i++) {
|
|
@@ -1243,12 +1256,14 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1243
1256
|
process.stdout.write("\n");
|
|
1244
1257
|
}
|
|
1245
1258
|
prevFrameLines = newLines;
|
|
1246
|
-
lastCursorRow =
|
|
1259
|
+
lastCursorRow = 0;
|
|
1247
1260
|
lastTotalRows = totalRows;
|
|
1248
1261
|
dockSetFrame(frame, totalRows);
|
|
1249
1262
|
dockEnterDocked();
|
|
1250
|
-
|
|
1251
|
-
|
|
1263
|
+
// Park the cursor at the box top so concurrent dock content (AI
|
|
1264
|
+
// streaming, tool results) inserts above the box rather than into it.
|
|
1265
|
+
const up = totalRows - 1;
|
|
1266
|
+
process.stdout.write(`\x1b[${up}A\r`);
|
|
1252
1267
|
}
|
|
1253
1268
|
else {
|
|
1254
1269
|
process.stdout.write(`\x1b[${lastCursorRow}A\r`);
|
|
@@ -1262,13 +1277,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1262
1277
|
process.stdout.write("\n");
|
|
1263
1278
|
}
|
|
1264
1279
|
const atLine = maxLen - 1;
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
process.stdout.write(`\x1b[${cursorRow - atLine}B`);
|
|
1269
|
-
process.stdout.write(`\r\x1b[${cursorCol}C`);
|
|
1280
|
+
// Return the cursor to the box top so concurrent dock content keeps
|
|
1281
|
+
// inserting above the box.
|
|
1282
|
+
process.stdout.write(`\x1b[${atLine}A\r`);
|
|
1270
1283
|
prevFrameLines = newLines;
|
|
1271
|
-
lastCursorRow =
|
|
1284
|
+
lastCursorRow = 0;
|
|
1272
1285
|
lastTotalRows = totalRows;
|
|
1273
1286
|
dockSetFrame(frame, totalRows);
|
|
1274
1287
|
}
|
|
@@ -1553,6 +1566,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1553
1566
|
pasteBurstTimer = null;
|
|
1554
1567
|
}
|
|
1555
1568
|
if (key && key.ctrl && key.name === "c") {
|
|
1569
|
+
// While a query is running, Ctrl+C interrupts the query instead of
|
|
1570
|
+
// leaving the prompt (the box stays focused for editing).
|
|
1571
|
+
if (submitting && onInterrupt) {
|
|
1572
|
+
onInterrupt();
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1556
1575
|
settle(new Error("interrupt"), true);
|
|
1557
1576
|
return;
|
|
1558
1577
|
}
|
|
@@ -1593,9 +1612,31 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1593
1612
|
return;
|
|
1594
1613
|
}
|
|
1595
1614
|
if (isEnterKey(str, key)) {
|
|
1596
|
-
|
|
1597
|
-
|
|
1615
|
+
const trimmed = buffer.trim();
|
|
1616
|
+
if (!trimmed)
|
|
1617
|
+
return;
|
|
1618
|
+
// While a query is running the box stays focused and editable; Enter is
|
|
1619
|
+
// ignored (even for commands) so the query finishes cleanly first.
|
|
1620
|
+
if (submitting)
|
|
1621
|
+
return;
|
|
1622
|
+
// With an onSubmit handler, ordinary prompts are submitted in place so
|
|
1623
|
+
// the box stays focused and editable while the query streams above it.
|
|
1624
|
+
// Slash commands (and any prompt when no handler is wired) settle so the
|
|
1625
|
+
// caller's existing command loop handles them.
|
|
1626
|
+
if (onSubmit && !trimmed.startsWith("/")) {
|
|
1627
|
+
submitting = true;
|
|
1628
|
+
const submitted = buffer;
|
|
1629
|
+
const submittedSpans = pasteSpans.slice();
|
|
1630
|
+
Promise.resolve()
|
|
1631
|
+
.then(() => onSubmit(submitted, submittedSpans))
|
|
1632
|
+
.then(() => {
|
|
1633
|
+
submitting = false;
|
|
1634
|
+
}, () => {
|
|
1635
|
+
submitting = false;
|
|
1636
|
+
});
|
|
1637
|
+
return;
|
|
1598
1638
|
}
|
|
1639
|
+
settle([buffer, pasteSpans], false);
|
|
1599
1640
|
return;
|
|
1600
1641
|
}
|
|
1601
1642
|
if (key && key.name === "backspace") {
|
|
@@ -1675,7 +1716,7 @@ export function userDisplayText(payload, pasteSpans) {
|
|
|
1675
1716
|
let out = "";
|
|
1676
1717
|
for (let i = 0; i < segs.length; i++) {
|
|
1677
1718
|
const [, kind, disp] = segs[i];
|
|
1678
|
-
if (i &&
|
|
1719
|
+
if (i && joinNeedsSpace(segs[i - 1]))
|
|
1679
1720
|
out += " ";
|
|
1680
1721
|
out += kind === "collapsed" ? `\x1b[1;36m${disp}\x1b[0m` : disp;
|
|
1681
1722
|
}
|