@bubblebrain-ai/bubble 0.0.20 → 0.0.22
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/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +64 -5
- package/dist/agent.js +365 -288
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +2 -0
- package/dist/main.js +88 -13
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider.js +23 -3
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +109 -2
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/bash.js +4 -0
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -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 +3 -3
- 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.d.ts +11 -1
- package/dist/tui/run.js +399 -71
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- 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/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +301 -247
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- 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/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
|
@@ -10,6 +10,16 @@ export interface SubmitPayload {
|
|
|
10
10
|
}
|
|
11
11
|
interface InputBoxProps {
|
|
12
12
|
onSubmit: (payload: SubmitPayload) => void;
|
|
13
|
+
/**
|
|
14
|
+
* When set (agent running), Tab queues the composer content for the next
|
|
15
|
+
* turn instead of its idle-time behavior.
|
|
16
|
+
*/
|
|
17
|
+
onQueue?: (payload: SubmitPayload) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Receives scroll intent when Up/Down arrows are classified as synthetic
|
|
20
|
+
* wheel events (terminal alternate-scroll) rather than keyboard presses.
|
|
21
|
+
*/
|
|
22
|
+
onWheelScroll?: (direction: "up" | "down", lines: number) => void;
|
|
13
23
|
onPasteNotice?: (notice: string) => void;
|
|
14
24
|
disabled?: boolean;
|
|
15
25
|
cursorResetEpoch?: number;
|
|
@@ -31,6 +41,21 @@ export declare function resolveCursorRowCompensation(input: {
|
|
|
31
41
|
export declare function isCtrlCInput(input: string, key: {
|
|
32
42
|
ctrl?: boolean;
|
|
33
43
|
}): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Split a composer line around the cursor so the cell under it can render as
|
|
46
|
+
* an inverse-video software cursor. The visible cursor must not depend on the
|
|
47
|
+
* real terminal cursor: Ink only re-arms its one-shot cursor escape when the
|
|
48
|
+
* component owning useCursor re-commits, so frames produced by other
|
|
49
|
+
* components' local state (the waiting spinner, viewport scrolling) hide the
|
|
50
|
+
* hardware cursor for most of an agent run. Drawing the cell ourselves keeps
|
|
51
|
+
* the cursor visible on every frame; the real (mostly hidden) cursor is still
|
|
52
|
+
* positioned for IME anchoring.
|
|
53
|
+
*/
|
|
54
|
+
export declare function splitLineAtCursor(lineText: string, charOffset: number): {
|
|
55
|
+
before: string;
|
|
56
|
+
at: string;
|
|
57
|
+
after: string;
|
|
58
|
+
};
|
|
34
59
|
export declare function shouldSubmitExactSlashSuggestion(input: string, suggestionName?: string): boolean;
|
|
35
60
|
export declare function resolveSlashEnterAction(input: string, suggestions: Array<{
|
|
36
61
|
name: string;
|
|
@@ -55,4 +80,4 @@ export declare function insertNewlineAtCursor(text: string, cursor: number): {
|
|
|
55
80
|
text: string;
|
|
56
81
|
cursor: number;
|
|
57
82
|
};
|
|
58
|
-
export declare function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch, draftText, draftEpoch, onDraftApplied, skillRegistry, terminalColumns, cwd, }: InputBoxProps): import("react/jsx-runtime").JSX.Element;
|
|
83
|
+
export declare function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disabled, cursorResetEpoch, draftText, draftEpoch, onDraftApplied, skillRegistry, terminalColumns, cwd, }: InputBoxProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -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>;
|