@bubblebrain-ai/bubble 0.0.20 → 0.0.21
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/agent.d.ts +1 -0
- package/dist/agent.js +5 -1
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/feishu/agent-host/run-driver.js +1 -0
- package/dist/main.js +54 -13
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +80 -0
- package/dist/slash-commands/types.d.ts +4 -0
- package/dist/tools/bash.js +4 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +2 -1
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- package/dist/tui/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -0
- package/dist/tui/run.js +309 -69
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +42 -1
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +301 -247
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +50 -2
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
|
|
4
4
|
import stringWidth from "string-width";
|
|
@@ -6,7 +6,7 @@ import { appendFileSync } from "node:fs";
|
|
|
6
6
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
7
7
|
import { useTheme } from "./theme.js";
|
|
8
8
|
import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
9
|
-
import { ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
9
|
+
import { bareImageFilenameFromPaste, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
10
10
|
import { appendHistoryEntry, loadHistorySync, pushHistoryEntry, stepHistory, } from "./input-history.js";
|
|
11
11
|
import { stripTerminalMouseSequences } from "./terminal-mouse.js";
|
|
12
12
|
export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
|
|
@@ -37,6 +37,29 @@ export function resolveCursorRowCompensation(input) {
|
|
|
37
37
|
export function isCtrlCInput(input, key) {
|
|
38
38
|
return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Split a composer line around the cursor so the cell under it can render as
|
|
42
|
+
* an inverse-video software cursor. The visible cursor must not depend on the
|
|
43
|
+
* real terminal cursor: Ink only re-arms its one-shot cursor escape when the
|
|
44
|
+
* component owning useCursor re-commits, so frames produced by other
|
|
45
|
+
* components' local state (the waiting spinner, viewport scrolling) hide the
|
|
46
|
+
* hardware cursor for most of an agent run. Drawing the cell ourselves keeps
|
|
47
|
+
* the cursor visible on every frame; the real (mostly hidden) cursor is still
|
|
48
|
+
* positioned for IME anchoring.
|
|
49
|
+
*/
|
|
50
|
+
export function splitLineAtCursor(lineText, charOffset) {
|
|
51
|
+
const offset = Math.max(0, Math.min(charOffset, lineText.length));
|
|
52
|
+
if (offset >= lineText.length) {
|
|
53
|
+
return { before: lineText, at: " ", after: "" };
|
|
54
|
+
}
|
|
55
|
+
const codePoint = lineText.codePointAt(offset);
|
|
56
|
+
const length = codePoint > 0xffff ? 2 : 1;
|
|
57
|
+
return {
|
|
58
|
+
before: lineText.slice(0, offset),
|
|
59
|
+
at: lineText.slice(offset, offset + length),
|
|
60
|
+
after: lineText.slice(offset + length),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
40
63
|
// Break a logical line into segments that each fit within `maxWidth` display
|
|
41
64
|
// columns. Uses string-width so CJK and emoji wrap correctly; empty lines
|
|
42
65
|
// still produce one empty segment so cursors on blank lines render.
|
|
@@ -148,7 +171,7 @@ export function insertNewlineAtCursor(text, cursor) {
|
|
|
148
171
|
cursor: clampedCursor + 1,
|
|
149
172
|
};
|
|
150
173
|
}
|
|
151
|
-
export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, terminalColumns, cwd, }) {
|
|
174
|
+
export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disabled, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, terminalColumns, cwd, }) {
|
|
152
175
|
const theme = useTheme();
|
|
153
176
|
const width = terminalColumns;
|
|
154
177
|
const [text, setText] = useState("");
|
|
@@ -285,6 +308,20 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch =
|
|
|
285
308
|
}
|
|
286
309
|
return;
|
|
287
310
|
}
|
|
311
|
+
// Copying an image file in Finder pastes only the file's NAME while the
|
|
312
|
+
// real bits stay on the system clipboard — attach from there. If the
|
|
313
|
+
// clipboard turns out to hold no image, it was just text: insert it
|
|
314
|
+
// quietly.
|
|
315
|
+
const bareName = bareImageFilenameFromPaste(clean);
|
|
316
|
+
if (bareName && process.platform === "darwin") {
|
|
317
|
+
void tryClipboardImage()
|
|
318
|
+
.then((attached) => {
|
|
319
|
+
if (!attached)
|
|
320
|
+
insertTextAtCursor(clean);
|
|
321
|
+
})
|
|
322
|
+
.finally(clearPending);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
288
325
|
// Look for image paths inside the paste (drag-and-drop from Finder/
|
|
289
326
|
// Nautilus/Explorer). Multi-selection can arrive newline- or
|
|
290
327
|
// space-separated.
|
|
@@ -352,11 +389,12 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch =
|
|
|
352
389
|
setCursor(before.length + insert.length);
|
|
353
390
|
setSelectedIndex(0);
|
|
354
391
|
};
|
|
355
|
-
const submitInput = (submittedText) => {
|
|
392
|
+
const submitInput = (submittedText, target = "submit") => {
|
|
356
393
|
const expandedText = expandPastedContentMarkers(submittedText, pastedContentRefs);
|
|
357
394
|
if (expandedText.trim().length === 0 && attachments.length === 0)
|
|
358
395
|
return;
|
|
359
|
-
onSubmit
|
|
396
|
+
const deliver = target === "queue" && onQueue ? onQueue : onSubmit;
|
|
397
|
+
deliver({
|
|
360
398
|
text: expandedText,
|
|
361
399
|
displayText: expandedText === submittedText ? undefined : submittedText,
|
|
362
400
|
images: attachments,
|
|
@@ -481,6 +519,14 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch =
|
|
|
481
519
|
}
|
|
482
520
|
}
|
|
483
521
|
}
|
|
522
|
+
// While the agent runs, Tab queues the composer content for the next
|
|
523
|
+
// turn (Enter steers — handled by the app-level submit routing).
|
|
524
|
+
if (key.tab && !key.shift && onQueue && !showSuggestions) {
|
|
525
|
+
if (pastePendingRef.current)
|
|
526
|
+
return;
|
|
527
|
+
submitInput(text, "queue");
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
484
530
|
if (enterIntent === "submit") {
|
|
485
531
|
// A paste is still mid-flight — dropping this Enter avoids submitting
|
|
486
532
|
// an input state that doesn't yet include the paste.
|
|
@@ -514,45 +560,23 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch =
|
|
|
514
560
|
setSelectedIndex(0);
|
|
515
561
|
return;
|
|
516
562
|
}
|
|
517
|
-
if (key.upArrow) {
|
|
518
|
-
|
|
519
|
-
setCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, "up", text);
|
|
523
|
-
if (result.changed) {
|
|
524
|
-
setText(result.text);
|
|
525
|
-
setCursor(result.text.length);
|
|
526
|
-
setHistoryIndex(result.index);
|
|
527
|
-
historyDraftRef.current = result.draft;
|
|
528
|
-
setSelectedIndex(0);
|
|
529
|
-
setPastedContentRefs([]);
|
|
530
|
-
nextPastedContentIndexRef.current = 1;
|
|
531
|
-
}
|
|
563
|
+
if (key.upArrow || key.downArrow) {
|
|
564
|
+
classifyVerticalArrow(key.upArrow ? "up" : "down", key.eventType);
|
|
532
565
|
return;
|
|
533
566
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, "down", text);
|
|
540
|
-
if (result.changed) {
|
|
541
|
-
setText(result.text);
|
|
542
|
-
setCursor(result.text.length);
|
|
543
|
-
setHistoryIndex(result.index);
|
|
544
|
-
historyDraftRef.current = result.draft;
|
|
545
|
-
setSelectedIndex(0);
|
|
546
|
-
setPastedContentRefs([]);
|
|
547
|
-
nextPastedContentIndexRef.current = 1;
|
|
548
|
-
}
|
|
567
|
+
// Ctrl/meta chords are app-level shortcuts (Ctrl+S selection mode,
|
|
568
|
+
// Ctrl+O trace, Ctrl+R thinking, …) — never type their letter. Raw C0
|
|
569
|
+
// control bytes (kitty protocol off) are equally not text.
|
|
570
|
+
if (key.ctrl || key.meta)
|
|
549
571
|
return;
|
|
550
|
-
}
|
|
551
572
|
if (input) {
|
|
573
|
+
const printable = input.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
574
|
+
if (!printable)
|
|
575
|
+
return;
|
|
552
576
|
const before = text.slice(0, cursor);
|
|
553
577
|
const after = text.slice(cursor);
|
|
554
|
-
setText(before +
|
|
555
|
-
setCursor(cursor +
|
|
578
|
+
setText(before + printable + after);
|
|
579
|
+
setCursor(cursor + printable.length);
|
|
556
580
|
setSelectedIndex(0);
|
|
557
581
|
}
|
|
558
582
|
});
|
|
@@ -620,6 +644,103 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch =
|
|
|
620
644
|
const lineWidth = Math.max(1, contentWidth - PROMPT.length);
|
|
621
645
|
const visualLines = useMemo(() => computeVisualLines(text, lineWidth), [text, lineWidth]);
|
|
622
646
|
const { row: cursorVisualRow, col: cursorVisualCol } = cursorToVisual(visualLines, cursor);
|
|
647
|
+
// ---- Wheel-vs-keyboard classification for Up/Down arrows ----
|
|
648
|
+
//
|
|
649
|
+
// With mouse reporting off (so native drag-select/copy works), terminals
|
|
650
|
+
// translate the wheel into Up/Down arrow keys while in the alternate
|
|
651
|
+
// screen. Those synthetic arrows must scroll the transcript, not move the
|
|
652
|
+
// composer cursor or browse history. Three signals, in priority order:
|
|
653
|
+
// 1. kitty keyboard protocol: real key presses carry `eventType`;
|
|
654
|
+
// synthetic wheel arrows are bare legacy sequences. Once one enhanced
|
|
655
|
+
// arrow is seen, bare arrows are classified as wheel with no delay.
|
|
656
|
+
// 2. burst heuristic (non-kitty terminals): a wheel notch delivers
|
|
657
|
+
// several arrows within a few ms; a keypress delivers one. The first
|
|
658
|
+
// arrow is briefly deferred to see whether siblings follow.
|
|
659
|
+
// 3. wheel session: shortly after a wheel burst, single arrows continue
|
|
660
|
+
// to scroll, so slow trackpad scrolling doesn't fall back to history.
|
|
661
|
+
const performVerticalArrowRef = useRef(() => { });
|
|
662
|
+
performVerticalArrowRef.current = (direction) => {
|
|
663
|
+
if (direction === "up") {
|
|
664
|
+
if (cursorVisualRow > 0) {
|
|
665
|
+
setCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
if (cursorVisualRow < visualLines.length - 1) {
|
|
671
|
+
setCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, text);
|
|
676
|
+
if (result.changed) {
|
|
677
|
+
setText(result.text);
|
|
678
|
+
setCursor(result.text.length);
|
|
679
|
+
setHistoryIndex(result.index);
|
|
680
|
+
historyDraftRef.current = result.draft;
|
|
681
|
+
setSelectedIndex(0);
|
|
682
|
+
setPastedContentRefs([]);
|
|
683
|
+
nextPastedContentIndexRef.current = 1;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
const kittyArrowsSeenRef = useRef(false);
|
|
687
|
+
const arrowBurstRef = useRef(null);
|
|
688
|
+
const lastWheelFlushRef = useRef(0);
|
|
689
|
+
const ARROW_BURST_WINDOW_MS = 20;
|
|
690
|
+
const WHEEL_SESSION_MS = 300;
|
|
691
|
+
const flushArrowBurst = (burst) => {
|
|
692
|
+
if (burst.count > 1 && onWheelScroll) {
|
|
693
|
+
lastWheelFlushRef.current = Date.now();
|
|
694
|
+
onWheelScroll(burst.direction, burst.count);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
performVerticalArrowRef.current(burst.direction);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
const classifyVerticalArrow = (direction, eventType) => {
|
|
701
|
+
if (eventType) {
|
|
702
|
+
kittyArrowsSeenRef.current = true;
|
|
703
|
+
performVerticalArrowRef.current(direction);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (!onWheelScroll) {
|
|
707
|
+
performVerticalArrowRef.current(direction);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (kittyArrowsSeenRef.current) {
|
|
711
|
+
lastWheelFlushRef.current = Date.now();
|
|
712
|
+
onWheelScroll(direction, 1);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const pending = arrowBurstRef.current;
|
|
716
|
+
if (pending) {
|
|
717
|
+
if (pending.direction === direction) {
|
|
718
|
+
pending.count += 1;
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
clearTimeout(pending.timer);
|
|
722
|
+
arrowBurstRef.current = null;
|
|
723
|
+
flushArrowBurst(pending);
|
|
724
|
+
}
|
|
725
|
+
if (Date.now() - lastWheelFlushRef.current < WHEEL_SESSION_MS) {
|
|
726
|
+
lastWheelFlushRef.current = Date.now();
|
|
727
|
+
onWheelScroll(direction, 1);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const burst = {
|
|
731
|
+
direction,
|
|
732
|
+
count: 1,
|
|
733
|
+
timer: setTimeout(() => {
|
|
734
|
+
arrowBurstRef.current = null;
|
|
735
|
+
flushArrowBurst(burst);
|
|
736
|
+
}, ARROW_BURST_WINDOW_MS),
|
|
737
|
+
};
|
|
738
|
+
arrowBurstRef.current = burst;
|
|
739
|
+
};
|
|
740
|
+
useEffect(() => () => {
|
|
741
|
+
if (arrowBurstRef.current)
|
|
742
|
+
clearTimeout(arrowBurstRef.current.timer);
|
|
743
|
+
}, []);
|
|
623
744
|
const totalLines = Math.max(visualLines.length, 1);
|
|
624
745
|
const visibleLines = Math.min(Math.max(totalLines, MIN_VISIBLE_LINES), MAX_VISIBLE_LINES);
|
|
625
746
|
let scrollOffset = 0;
|
|
@@ -694,7 +815,10 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch =
|
|
|
694
815
|
node = node.parentNode;
|
|
695
816
|
}
|
|
696
817
|
const rootHeight = lastNode?.yogaNode?.getComputedHeight() ?? 0;
|
|
697
|
-
|
|
818
|
+
// `||` on purpose: some ptys (and Bun on a detached tty) report rows as 0,
|
|
819
|
+
// which `??` would happily accept — and `rootHeight >= 0` then flags every
|
|
820
|
+
// frame as fullscreen, forcing a bogus +1 row compensation.
|
|
821
|
+
const viewportRows = stdout.rows || process.stdout.rows || 24;
|
|
698
822
|
const previousOutputHeight = previousOutputHeightRef.current;
|
|
699
823
|
// After a clear/sync frame, Ink's physical terminal cursor remains on the
|
|
700
824
|
// last rendered row even though log-update records an output string with a
|
|
@@ -761,12 +885,18 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch =
|
|
|
761
885
|
const isFirst = visualIdx === 0;
|
|
762
886
|
const isCursorLine = visualIdx === cursorVisualRow;
|
|
763
887
|
const prompt = isFirst ? PROMPT : " ".repeat(PROMPT.length);
|
|
764
|
-
const
|
|
888
|
+
const cursorSegments = isCursorLine && !disabled
|
|
889
|
+
? splitLineAtCursor(lineText, cursor - (visualLines[cursorVisualRow]?.absStart ?? 0))
|
|
890
|
+
: null;
|
|
891
|
+
const renderedLine = cursorSegments
|
|
892
|
+
? cursorSegments.before + cursorSegments.at + cursorSegments.after
|
|
893
|
+
: lineText;
|
|
894
|
+
const fill = " ".repeat(Math.max(0, lineWidth - stringWidth(renderedLine)));
|
|
765
895
|
return (_jsxs(Box, { height: 1, overflow: "hidden", ref: isCursorLine
|
|
766
896
|
? (el) => {
|
|
767
897
|
cursorLineRef.current = el;
|
|
768
898
|
}
|
|
769
|
-
: undefined, children: [_jsx(Text, { backgroundColor: inputBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), _jsx(Text, { backgroundColor: inputBg, color: theme.inputText, children: lineText }), _jsx(Text, { backgroundColor: inputBg, children: fill })] }, visualIdx));
|
|
899
|
+
: undefined, children: [_jsx(Text, { backgroundColor: inputBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), cursorSegments ? (_jsxs(_Fragment, { children: [cursorSegments.before && (_jsx(Text, { backgroundColor: inputBg, color: theme.inputText, children: cursorSegments.before })), _jsx(Text, { backgroundColor: theme.inputText, color: inputBg, children: cursorSegments.at }), cursorSegments.after && (_jsx(Text, { backgroundColor: inputBg, color: theme.inputText, children: cursorSegments.after }))] })) : (_jsx(Text, { backgroundColor: inputBg, color: theme.inputText, children: lineText })), _jsx(Text, { backgroundColor: inputBg, children: fill })] }, visualIdx));
|
|
770
900
|
}), hasMoreBelow && (_jsx(Text, { backgroundColor: inputBg, color: theme.muted, dimColor: true, children: filledLine(` ↓ ${moreBelow} more`) }))] }), showSuggestions && mode === "slash" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 4, children: [slashSuggestions
|
|
771
901
|
.slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
|
|
772
902
|
.map((cmd, visibleIndex) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { type DisplayMessage, type DisplayMessagePart, type DisplayToolCall } from "./display-history.js";
|
|
3
3
|
/**
|
|
4
4
|
* Hint surfaced when the user can interrupt the currently-running pending tool
|
|
5
5
|
* via the approval dialog. The match is loose (by request type → tool name),
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Box,
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
4
|
import { useTheme } from "./theme.js";
|
|
5
5
|
import { highlightCode, inferLang } from "./code-highlight.js";
|
|
6
6
|
import { MarkdownContent, StreamingMarkdown } from "./markdown.js";
|
|
7
|
-
import {
|
|
7
|
+
import { userInputStatusBadgeLabel, } from "./display-history.js";
|
|
8
|
+
import { buildTraceGroups, executeCommandBlock, formatTracePath, shouldInlineExecuteCommand, traceGroupLabel, } from "./trace-groups.js";
|
|
8
9
|
import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
|
|
9
10
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
10
11
|
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
12
|
+
const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
|
|
11
13
|
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
|
|
12
14
|
const hasStreaming = !!(streamingContent ||
|
|
13
15
|
streamingReasoning ||
|
|
@@ -27,17 +29,22 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
27
29
|
showExpandHint: !hasStreaming && i === lastMessageIndex,
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [staticItems.map((item) => {
|
|
33
|
+
if (item.kind === "welcome") {
|
|
34
|
+
return _jsx(React.Fragment, { children: welcomeBanner }, item.key);
|
|
35
|
+
}
|
|
36
|
+
return (_jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, nowTick: item.showExpandHint ? nowTick : undefined }, item.key));
|
|
37
|
+
}), hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick }))] }));
|
|
38
|
+
}
|
|
39
|
+
// Memoized: with no <Static> region, every transcript row re-renders on each
|
|
40
|
+
// state change unless its props are referentially stable. Message objects are
|
|
41
|
+
// append-only (compaction reuses already-compacted instances), keys are
|
|
42
|
+
// stable, and nowTick is only threaded to the last row, so memo hits for all
|
|
43
|
+
// settled history rows.
|
|
44
|
+
const MessageItem = React.memo(function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, nowTick, }) {
|
|
38
45
|
const theme = useTheme();
|
|
39
46
|
if (message.role === "user") {
|
|
40
|
-
return _jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns });
|
|
47
|
+
return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus }));
|
|
41
48
|
}
|
|
42
49
|
if (message.role === "error") {
|
|
43
50
|
return (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { color: theme.error, children: ["Error: ", message.content] }) }));
|
|
@@ -45,6 +52,9 @@ function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, n
|
|
|
45
52
|
if (message.syntheticKind === "ui_compact_summary") {
|
|
46
53
|
return _jsx(CompactionSummaryBlock, { message: message });
|
|
47
54
|
}
|
|
55
|
+
if (message.syntheticKind === "ui_interrupt") {
|
|
56
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.error, children: "\u23F9 " }), _jsx(Text, { color: theme.muted, dimColor: true, children: message.content || "Interrupted by user" })] }));
|
|
57
|
+
}
|
|
48
58
|
const visibleReasoning = sanitizeInternalReminderBlocks(message.reasoning ?? "").trim();
|
|
49
59
|
const hasVisibleAssistantContent = !!message.content ||
|
|
50
60
|
(message.toolCalls?.length ?? 0) > 0 ||
|
|
@@ -53,7 +63,7 @@ function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, n
|
|
|
53
63
|
if (!hasVisibleAssistantContent)
|
|
54
64
|
return null;
|
|
55
65
|
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && verboseTrace && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), message.content && _jsx(MarkdownContent, { content: message.content })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
|
|
56
|
-
}
|
|
66
|
+
});
|
|
57
67
|
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, verboseTrace, pendingApproval, nowTick, }) {
|
|
58
68
|
const deferredContent = React.useDeferredValue(content);
|
|
59
69
|
const deferredReasoning = React.useDeferredValue(reasoning);
|
|
@@ -63,14 +73,12 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, v
|
|
|
63
73
|
? deferredParts
|
|
64
74
|
: fallbackStreamingParts(deferredContent, tools);
|
|
65
75
|
return (_jsxs(Box, { flexDirection: "column", children: [visibleReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }) })), visibleParts.length > 0 && (
|
|
66
|
-
// marginTop
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
// WaitingIndicator rendered below.
|
|
73
|
-
_jsx(Box, { marginTop: 0, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
|
|
76
|
+
// marginTop=1 matches the committed MessageItem layout exactly, so the
|
|
77
|
+
// gap under the user message is identical while streaming and after the
|
|
78
|
+
// turn commits — no spacing jump at finalize time. (The old marginTop=0
|
|
79
|
+
// was a flicker mitigation for the main-screen <Static> renderer; the
|
|
80
|
+
// alt-screen viewport repaints frames atomically, so it's obsolete.)
|
|
81
|
+
_jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
|
|
74
82
|
}
|
|
75
83
|
function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
|
|
76
84
|
const lastToolsPartIndex = findLastToolsPartIndex(parts);
|
|
@@ -157,14 +165,22 @@ function TraceGroupBlock({ group, terminalColumns, pendingApproval, compactTop,
|
|
|
157
165
|
const commandWidth = Math.max(14, terminalColumns - group.title.length - 20);
|
|
158
166
|
const detailWidth = Math.max(20, terminalColumns - 8);
|
|
159
167
|
const detailLines = group.previewLines.length > 0 ? group.previewLines : group.items;
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
168
|
+
// A model-provided bash description owns the header slot; the command then
|
|
169
|
+
// always renders as a block below. Without one, single-line commands that
|
|
170
|
+
// fit stay inline; anything longer becomes a wrapped block preserving the
|
|
171
|
+
// command's own line structure — commands are never clipped mid-line.
|
|
172
|
+
const showDescription = group.kind === "execute" && !!group.description;
|
|
173
|
+
const inlineCommand = !showDescription && group.command
|
|
174
|
+
? (group.kind === "execute"
|
|
175
|
+
? (shouldInlineExecuteCommand(group, commandWidth) ? group.command : undefined)
|
|
176
|
+
: (visualWidth(group.command) <= commandWidth ? group.command : undefined))
|
|
177
|
+
: undefined;
|
|
178
|
+
const commandBlock = group.command && !inlineCommand
|
|
179
|
+
? (group.kind === "execute"
|
|
180
|
+
? executeCommandBlock(group, EXECUTE_COMMAND_BLOCK_MAX_LINES)
|
|
181
|
+
: { lines: [group.command], omitted: 0 })
|
|
166
182
|
: null;
|
|
167
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: titleColor, children: group.title }), group.
|
|
183
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: titleColor, children: group.title }), showDescription ? (_jsxs(Text, { color: theme.traceDetail, children: [" ", truncateVisual(group.description, commandWidth)] })) : inlineCommand ? (_jsxs(Text, { color: theme.traceCommand, children: [" ", inlineCommand] })) : !group.command && group.count !== undefined && group.noun ? (_jsxs(Text, { color: theme.traceCount, children: [" ", group.count, " ", group.noun] })) : null, status && _jsxs(Text, { color: status.color, children: [" ", status.text] })] }), commandBlock && commandBlock.lines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [commandBlock.lines.flatMap((line, idx) => wrapByVisualWidth(line || " ", Math.max(10, detailWidth - 2)).map((seg, segIdx) => (_jsx(Text, { color: theme.traceCommand, children: seg }, `cmd-${idx}-${segIdx}`)))), commandBlock.omitted > 0 && (_jsxs(Text, { color: theme.traceDetail, children: ["\u2026 ", commandBlock.omitted, " more line", commandBlock.omitted === 1 ? "" : "s"] }))] })), detailLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: detailLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: detailColor, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, index))) })), group.errorLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: group.errorLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: theme.error, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, `error-${index}`))) })), group.omitted > 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.traceDetail, children: ["... ", group.omitted, " more, Ctrl+O to view"] }) }))] }));
|
|
168
184
|
}
|
|
169
185
|
function EditTraceBlock({ tool, details, terminalColumns, compactTop, status, }) {
|
|
170
186
|
const theme = useTheme();
|
|
@@ -220,8 +236,9 @@ function CompactionSummaryBlock({ message }) {
|
|
|
220
236
|
const summary = message.compactionSummary?.trim();
|
|
221
237
|
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, paddingX: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.success, bold: true, children: "\u2713 " }), _jsx(Text, { color: theme.accent, bold: true, children: "Compaction checkpoint" }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", status] })] }), summary && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, dimColor: true, children: "Preserved context summary" }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsx(MarkdownContent, { content: summary }) })] }))] }));
|
|
222
238
|
}
|
|
223
|
-
function UserMessageBlock({ content, terminalColumns }) {
|
|
239
|
+
function UserMessageBlock({ content, terminalColumns, inputStatus, }) {
|
|
224
240
|
const theme = useTheme();
|
|
241
|
+
const badge = userInputStatusBadgeLabel(inputStatus);
|
|
225
242
|
// Rail and its right gutter must share the bubble background; otherwise the
|
|
226
243
|
// terminal background shows up as a dark seam between rail and message.
|
|
227
244
|
const railWidth = 2;
|
|
@@ -230,7 +247,7 @@ function UserMessageBlock({ content, terminalColumns }) {
|
|
|
230
247
|
const wrappedLines = content
|
|
231
248
|
.split("\n")
|
|
232
249
|
.flatMap((line) => wrapByVisualWidth(line, bubbleTextWidth));
|
|
233
|
-
return (
|
|
250
|
+
return (_jsxs(Box, { flexDirection: "column", children: [badge && (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: inputStatus === "pending_steer" ? theme.warning : theme.muted, children: ` ${badge} ` }), _jsx(Text, { color: theme.dim, children: inputStatus === "pending_steer" ? "applies at the next model call" : "runs after this turn" })] })), wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userRail, children: index === 0 ? "▌ " : " " }), _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: ` ${padVisual(line || " ", bubbleTextWidth)} ` })] }, index)))] }));
|
|
234
251
|
}
|
|
235
252
|
const TOOL_DISPLAY_NAMES = {
|
|
236
253
|
read: "Read",
|
package/dist/tui-ink/run.d.ts
CHANGED
|
@@ -4,13 +4,14 @@ import type { SessionManager } from "../session.js";
|
|
|
4
4
|
import type { Provider } from "../types.js";
|
|
5
5
|
import type { ProviderRegistry } from "../provider-registry.js";
|
|
6
6
|
import type { SkillRegistry } from "../skills/registry.js";
|
|
7
|
-
import { type ApprovalHandlerRef, type PlanHandlerRef } from "./app.js";
|
|
7
|
+
import { type ApprovalHandlerRef, type ExitSummary, type PlanHandlerRef } from "./app.js";
|
|
8
8
|
import type { BashAllowlist } from "../approval/session-cache.js";
|
|
9
9
|
import type { SettingsManager } from "../permissions/settings.js";
|
|
10
10
|
import type { McpManager } from "../mcp/manager.js";
|
|
11
11
|
import type { LspService } from "../lsp/index.js";
|
|
12
12
|
import type { QuestionController } from "../question/index.js";
|
|
13
13
|
import type { MemoryScope } from "../memory/index.js";
|
|
14
|
+
import type { ExternalHookController } from "../hooks/controller.js";
|
|
14
15
|
import type { ResolvedTheme, ThemeMode } from "./theme.js";
|
|
15
16
|
export interface RunTuiOptions {
|
|
16
17
|
sessionManager?: SessionManager;
|
|
@@ -33,5 +34,9 @@ export interface RunTuiOptions {
|
|
|
33
34
|
runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
|
|
34
35
|
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
35
36
|
bypassEnabled?: boolean;
|
|
37
|
+
/** One-line "update available" notice rendered under the welcome banner version. */
|
|
38
|
+
updateNotice?: string;
|
|
39
|
+
/** External lifecycle hooks, threaded into slash-command execution. */
|
|
40
|
+
hookController?: ExternalHookController;
|
|
36
41
|
}
|
|
37
|
-
export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<
|
|
42
|
+
export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<ExitSummary | undefined>;
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -1,15 +1,55 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
|
-
import chalk from "chalk";
|
|
4
3
|
import { App } from "./app.js";
|
|
4
|
+
import { MOUSE_REPORTING_DISABLE } from "./terminal-mouse.js";
|
|
5
|
+
// DECSET 1007: terminals translate the mouse wheel into Up/Down arrow keys
|
|
6
|
+
// while the alternate screen is active. Mouse reporting stays OFF on purpose
|
|
7
|
+
// so plain drag-select and copy keep their native terminal behavior; the
|
|
8
|
+
// composer classifies wheel-synthesized arrows vs real key presses.
|
|
9
|
+
const ALTERNATE_SCROLL_ENABLE = "\x1b[?1007h";
|
|
10
|
+
const ALTERNATE_SCROLL_DISABLE = "\x1b[?1007l";
|
|
5
11
|
import { warmHighlighter } from "./code-highlight.js";
|
|
12
|
+
/**
|
|
13
|
+
* Best-effort terminal restore for abnormal exits. DECSET mouse modes are
|
|
14
|
+
* global terminal state — if the process dies without disabling them, the
|
|
15
|
+
* user's shell receives \x1b[<35;… garbage on every mouse move. The alt-screen
|
|
16
|
+
* and cursor writes are defensive duplicates of Ink's own teardown (idempotent
|
|
17
|
+
* when Ink already ran; load-bearing when it didn't).
|
|
18
|
+
*/
|
|
19
|
+
function restoreTerminal() {
|
|
20
|
+
if (!process.stdout.isTTY)
|
|
21
|
+
return;
|
|
22
|
+
try {
|
|
23
|
+
process.stdout.write(ALTERNATE_SCROLL_DISABLE + MOUSE_REPORTING_DISABLE + "\x1b[?1049l\x1b[?25h");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// stdout may already be destroyed during shutdown
|
|
27
|
+
}
|
|
28
|
+
}
|
|
6
29
|
export async function runTui(agent, args, options = {}) {
|
|
7
30
|
// Kick off shiki load before the first code block is rendered. Fire and
|
|
8
31
|
// forget — CodeBlock's lazy init falls back to raw lines if this isn't ready
|
|
9
32
|
// yet, so callers don't need to await it.
|
|
10
33
|
warmHighlighter();
|
|
11
34
|
let exitSummary;
|
|
12
|
-
const
|
|
35
|
+
const onFatalError = (err) => {
|
|
36
|
+
restoreTerminal();
|
|
37
|
+
const detail = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
38
|
+
try {
|
|
39
|
+
process.stderr.write(`${detail}\n`);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// nothing left to report to
|
|
43
|
+
}
|
|
44
|
+
process.exit(1);
|
|
45
|
+
};
|
|
46
|
+
const onSigterm = () => {
|
|
47
|
+
restoreTerminal();
|
|
48
|
+
process.exit(143);
|
|
49
|
+
};
|
|
50
|
+
process.on("uncaughtException", onFatalError);
|
|
51
|
+
process.on("SIGTERM", onSigterm);
|
|
52
|
+
const instance = render(_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, updateNotice: options.updateNotice, hookController: options.hookController, onExit: (summary) => {
|
|
13
53
|
// The app already called useApp().exit() inside requestExit, which
|
|
14
54
|
// triggers Ink's own unmount + TTY restore. waitUntilExit() below is
|
|
15
55
|
// the canonical signal that we're done — we deliberately do *not*
|
|
@@ -25,34 +65,44 @@ export async function runTui(agent, args, options = {}) {
|
|
|
25
65
|
exitOnCtrlC: false,
|
|
26
66
|
kittyKeyboard: {
|
|
27
67
|
mode: "enabled",
|
|
28
|
-
|
|
68
|
+
// reportEventTypes lets the composer tell real arrow-key presses
|
|
69
|
+
// (kitty-enhanced, carry eventType) apart from the bare arrow
|
|
70
|
+
// sequences terminals synthesize for wheel scrolling in alternate
|
|
71
|
+
// screen — see the classifier in input-box.tsx.
|
|
72
|
+
flags: ["disambiguateEscapeCodes", "reportEventTypes"],
|
|
29
73
|
},
|
|
74
|
+
// The whole point of the Ink migration: render into the 1049 alternate
|
|
75
|
+
// screen so streaming repaints never touch the user's shell scrollback.
|
|
76
|
+
// Ink degrades this to false automatically when stdout is not a TTY.
|
|
77
|
+
alternateScreen: true,
|
|
30
78
|
});
|
|
31
|
-
|
|
79
|
+
// Enable alternate-scroll after render() so it follows alt-screen entry:
|
|
80
|
+
// the wheel arrives as Up/Down arrows, while plain drag-select and copy
|
|
81
|
+
// keep their native terminal behavior (no mouse reporting).
|
|
82
|
+
if (process.stdout.isTTY) {
|
|
83
|
+
process.stdout.write(ALTERNATE_SCROLL_ENABLE);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await instance.waitUntilExit();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
// Reset scroll translation before anything is printed to the primary
|
|
90
|
+
// screen; Ink has already left the alt screen by the time
|
|
91
|
+
// waitUntilExit() resolves.
|
|
92
|
+
if (process.stdout.isTTY) {
|
|
93
|
+
process.stdout.write(ALTERNATE_SCROLL_DISABLE);
|
|
94
|
+
}
|
|
95
|
+
process.off("uncaughtException", onFatalError);
|
|
96
|
+
process.off("SIGTERM", onSigterm);
|
|
97
|
+
}
|
|
32
98
|
// zsh's PROMPT_SP prints a reverse-video `%` if the previous program left
|
|
33
99
|
// the cursor mid-line. Ink's interactive teardown (log-update.done) doesn't
|
|
34
100
|
// emit a trailing newline, so mirror Ink's non-interactive branch and align
|
|
35
101
|
// the cursor to column 0 before handing control back to the shell.
|
|
36
102
|
if (process.stdout.isTTY) {
|
|
37
103
|
process.stdout.write("\n");
|
|
38
|
-
if (exitSummary) {
|
|
39
|
-
process.stdout.write(formatExitSummary(exitSummary) + "\n");
|
|
40
|
-
}
|
|
41
104
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return chalk.dim(`${label} ${formatWallMs(summary.wallMs)}`);
|
|
46
|
-
}
|
|
47
|
-
function formatWallMs(ms) {
|
|
48
|
-
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
49
|
-
if (totalSeconds < 60)
|
|
50
|
-
return `${totalSeconds}s`;
|
|
51
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
52
|
-
const seconds = totalSeconds % 60;
|
|
53
|
-
if (minutes < 60)
|
|
54
|
-
return `${minutes}m ${seconds}s`;
|
|
55
|
-
const hours = Math.floor(minutes / 60);
|
|
56
|
-
const minutesRest = minutes % 60;
|
|
57
|
-
return `${hours}h ${minutesRest}m ${seconds}s`;
|
|
105
|
+
// The exit summary is printed by main.ts (single print site, after the alt
|
|
106
|
+
// screen has been left, so it lands in the real shell scrollback).
|
|
107
|
+
return exitSummary;
|
|
58
108
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const MOUSE_REPORTING_DISABLE = "\u001B[?1006l\u001B[?1000l";
|
|
1
2
|
export type MouseWheelDirection = "up" | "down";
|
|
2
3
|
export declare function stripTerminalMouseSequences(input: string): string;
|
|
3
4
|
export declare function hasTerminalMouseSequence(input: string): boolean;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Bubble does NOT enable mouse reporting — plain drag-select and copy keep
|
|
2
|
+
// their native terminal behavior. This disable sequence is written
|
|
3
|
+
// defensively on teardown in case a previous crash left reporting on.
|
|
4
|
+
export const MOUSE_REPORTING_DISABLE = "\x1b[?1006l\x1b[?1000l";
|
|
1
5
|
const SGR_MOUSE_SEQUENCE_RE = /\x1b?\[?<\d+;\d+;\d+[mM]/g;
|
|
2
6
|
const SGR_MOUSE_WHEEL_RE = /\x1b?\[?<(\d+);\d+;\d+([mM])/g;
|
|
3
7
|
export function stripTerminalMouseSequences(input) {
|