@bubblebrain-ai/bubble 0.0.36 → 0.0.38
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/internal-reminder-sanitizer.js +2 -0
- package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
- package/dist/agent/task-lifecycle-reminder.js +59 -0
- package/dist/agent.d.ts +26 -0
- package/dist/agent.js +39 -0
- package/dist/approval/controller.d.ts +3 -1
- package/dist/approval/controller.js +5 -2
- package/dist/approval/tool-helper.js +10 -1
- package/dist/approval/types.d.ts +15 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -0
- package/dist/loop/engine.d.ts +29 -0
- package/dist/loop/engine.js +63 -0
- package/dist/main.js +29 -0
- package/dist/orchestrator/default-hooks.js +16 -0
- package/dist/session-types.d.ts +1 -1
- package/dist/tasks/manager.d.ts +136 -0
- package/dist/tasks/manager.js +530 -0
- package/dist/tasks/process-utils.d.ts +10 -0
- package/dist/tasks/process-utils.js +49 -0
- package/dist/tasks/promotion.d.ts +17 -0
- package/dist/tasks/promotion.js +30 -0
- package/dist/tasks/wake.d.ts +53 -0
- package/dist/tasks/wake.js +125 -0
- package/dist/tools/bash.d.ts +15 -1
- package/dist/tools/bash.js +103 -3
- package/dist/tools/child-tools.js +3 -1
- package/dist/tools/edit.js +4 -12
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +8 -1
- package/dist/tools/server-manager.d.ts +6 -36
- package/dist/tools/server-manager.js +6 -234
- package/dist/tools/task-tools.d.ts +10 -0
- package/dist/tools/task-tools.js +129 -0
- package/dist/tools/write.js +6 -12
- package/dist/tui-ink/app.d.ts +20 -2
- package/dist/tui-ink/app.js +309 -21
- package/dist/tui-ink/approval/approval-dialog.js +4 -4
- package/dist/tui-ink/display-history.d.ts +1 -1
- package/dist/tui-ink/footer.d.ts +3 -3
- package/dist/tui-ink/footer.js +3 -3
- package/dist/tui-ink/input-box.d.ts +6 -0
- package/dist/tui-ink/markdown.d.ts +10 -2
- package/dist/tui-ink/markdown.js +13 -12
- package/dist/tui-ink/message-list.js +18 -7
- package/dist/tui-ink/model-picker.js +33 -10
- package/dist/tui-ink/run.d.ts +6 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-inspector.d.ts +5 -1
- package/dist/tui-ink/subagent-inspector.js +23 -7
- package/dist/tui-ink/welcome.js +4 -0
- package/package.json +1 -1
package/dist/tui-ink/markdown.js
CHANGED
|
@@ -344,7 +344,7 @@ function InlineText({ text, maxWidth }) {
|
|
|
344
344
|
const wrapped = wrapInlineSegments(parseMarkdownInlineSegments(text), effectiveWidth);
|
|
345
345
|
return (_jsx(_Fragment, { children: wrapped.map((segments, li) => (_jsx(Text, { children: segments.length ? renderSegmentNodes(segments, `inline-${li}`, theme.inlineCode) : " " }, li))) }));
|
|
346
346
|
}
|
|
347
|
-
function CodeBlock({ lang, lines }) {
|
|
347
|
+
function CodeBlock({ lang, lines, flushTop = false }) {
|
|
348
348
|
const theme = useTheme();
|
|
349
349
|
// Lazy init: try sync highlight when shiki is already warm so the very first
|
|
350
350
|
// paint carries highlighted output. Fall back to raw lines if shiki hasn't
|
|
@@ -376,9 +376,9 @@ function CodeBlock({ lang, lines }) {
|
|
|
376
376
|
cancelled = true;
|
|
377
377
|
};
|
|
378
378
|
}, [lang, lines]);
|
|
379
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
379
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: flushTop ? 0 : 1, marginBottom: 1, children: [lang && _jsx(Text, { color: theme.muted, children: lang }), _jsx(Box, { flexDirection: "column", children: highlighted.map((line, i) => (_jsx(Text, { children: line || " " }, i))) })] }));
|
|
380
380
|
}
|
|
381
|
-
function TableBlock({ headers, rows, maxWidth, }) {
|
|
381
|
+
function TableBlock({ headers, rows, maxWidth, flushTop = false, }) {
|
|
382
382
|
const theme = useTheme();
|
|
383
383
|
const { columns: termWidth } = useTerminalSize();
|
|
384
384
|
const colCount = headers.length;
|
|
@@ -429,7 +429,7 @@ function TableBlock({ headers, rows, maxWidth, }) {
|
|
|
429
429
|
const height = Math.max(1, ...wrapped.map((lines) => lines.length));
|
|
430
430
|
return (_jsx(Box, { flexDirection: "column", children: Array.from({ length: height }, (_, line) => (_jsxs(Text, { children: [`${g.v} `, wrapped.map((cellLines, i) => (_jsxs(React.Fragment, { children: [renderCellLine(cellLines[line] ?? [], widths[i] ?? 4, `${keyPrefix}-cell-${i}-${line}`, theme.inlineCode), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, `${keyPrefix}-line-${line}`))) }, keyPrefix));
|
|
431
431
|
};
|
|
432
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
432
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: flushTop ? 0 : 1, marginBottom: 1, children: [_jsx(Text, { children: top }), renderRow(headers, "header", true), _jsx(Text, { children: mid }), rows.map((row, ri) => renderRow(row, `row-${ri}`)), _jsx(Text, { children: bot })] }));
|
|
433
433
|
}
|
|
434
434
|
/** Cap a pathological cell (huge blob of text) at this many wrapped lines. */
|
|
435
435
|
const MAX_TABLE_CELL_LINES = 8;
|
|
@@ -663,7 +663,7 @@ export function wrapInlineSegments(segments, maxWidth) {
|
|
|
663
663
|
function inlineSegmentsWidth(segments) {
|
|
664
664
|
return segments.reduce((sum, segment) => sum + visualWidth(segment.text), 0);
|
|
665
665
|
}
|
|
666
|
-
function HeadingBlock({ level, text }) {
|
|
666
|
+
function HeadingBlock({ level, text, flushTop = false }) {
|
|
667
667
|
const theme = useTheme();
|
|
668
668
|
const props = { bold: true };
|
|
669
669
|
if (level === 1) {
|
|
@@ -676,7 +676,7 @@ function HeadingBlock({ level, text }) {
|
|
|
676
676
|
else if (level === 3) {
|
|
677
677
|
props.color = theme.warning;
|
|
678
678
|
}
|
|
679
|
-
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { ...props, children: text }) }));
|
|
679
|
+
return (_jsx(Box, { marginTop: flushTop ? 0 : 1, marginBottom: 1, children: _jsx(Text, { ...props, children: text }) }));
|
|
680
680
|
}
|
|
681
681
|
/**
|
|
682
682
|
* Streaming-aware wrapper around `MarkdownContent`.
|
|
@@ -693,7 +693,7 @@ function HeadingBlock({ level, text }) {
|
|
|
693
693
|
* reset handles the rare case where `content` is replaced wholesale (e.g.,
|
|
694
694
|
* the user re-enters a turn).
|
|
695
695
|
*/
|
|
696
|
-
export function StreamingMarkdown({ content, maxWidth, }) {
|
|
696
|
+
export function StreamingMarkdown({ content, maxWidth, flushTop = false, }) {
|
|
697
697
|
const stablePrefixRef = React.useRef("");
|
|
698
698
|
if (!content.startsWith(stablePrefixRef.current)) {
|
|
699
699
|
stablePrefixRef.current = "";
|
|
@@ -706,7 +706,7 @@ export function StreamingMarkdown({ content, maxWidth, }) {
|
|
|
706
706
|
}
|
|
707
707
|
const stablePrefix = stablePrefixRef.current;
|
|
708
708
|
const unstableSuffix = content.substring(stablePrefix.length);
|
|
709
|
-
return (_jsxs(Box, { flexDirection: "column", children: [stablePrefix && _jsx(MarkdownContent, { content: stablePrefix, maxWidth: maxWidth }), unstableSuffix && _jsx(MarkdownContent, { content: unstableSuffix, maxWidth: maxWidth })] }));
|
|
709
|
+
return (_jsxs(Box, { flexDirection: "column", children: [stablePrefix && _jsx(MarkdownContent, { content: stablePrefix, maxWidth: maxWidth, flushTop: flushTop }), unstableSuffix && _jsx(MarkdownContent, { content: unstableSuffix, maxWidth: maxWidth, flushTop: flushTop && !stablePrefix })] }));
|
|
710
710
|
}
|
|
711
711
|
// A markdown list item: optional indent, a bullet (-, *, +) or ordered marker
|
|
712
712
|
// (1. / 2)), then whitespace, then the item text. The captured prefix is
|
|
@@ -720,17 +720,18 @@ export function splitListItem(line) {
|
|
|
720
720
|
const prefix = m[1] + m[2] + m[3];
|
|
721
721
|
return { prefix, content: m[4], indent: visualWidth(prefix) };
|
|
722
722
|
}
|
|
723
|
-
export function MarkdownContent({ content, maxWidth, }) {
|
|
723
|
+
export function MarkdownContent({ content, maxWidth, flushTop = false, }) {
|
|
724
724
|
const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
|
|
725
725
|
return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => {
|
|
726
|
+
const blockFlushTop = flushTop && i === 0;
|
|
726
727
|
if (block.type === "code") {
|
|
727
|
-
return _jsx(CodeBlock, { lang: block.lang, lines: block.lines }, i);
|
|
728
|
+
return _jsx(CodeBlock, { lang: block.lang, lines: block.lines, flushTop: blockFlushTop }, i);
|
|
728
729
|
}
|
|
729
730
|
if (block.type === "table") {
|
|
730
|
-
return (_jsx(TableBlock, { headers: block.headers, rows: block.rows, maxWidth: maxWidth }, i));
|
|
731
|
+
return (_jsx(TableBlock, { headers: block.headers, rows: block.rows, maxWidth: maxWidth, flushTop: blockFlushTop }, i));
|
|
731
732
|
}
|
|
732
733
|
if (block.type === "heading") {
|
|
733
|
-
return _jsx(HeadingBlock, { level: block.level, text: block.text }, i);
|
|
734
|
+
return _jsx(HeadingBlock, { level: block.level, text: block.text, flushTop: blockFlushTop }, i);
|
|
734
735
|
}
|
|
735
736
|
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: block.lines.map((line, li) => {
|
|
736
737
|
const item = splitListItem(line);
|
|
@@ -31,8 +31,14 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
31
31
|
const regularMessages = messages.filter((message) => !message.inputStatus);
|
|
32
32
|
const pendingSteerMessages = messages.filter((message) => message.inputStatus === "pending_steer");
|
|
33
33
|
const queuedInputMessages = messages.filter((message) => message.inputStatus === "queued");
|
|
34
|
+
// While the transcript has no settled rows, the banner stays OUT of
|
|
35
|
+
// <Static> and renders in the live region instead, so it repaints on every
|
|
36
|
+
// commit — a /model switch on the fresh launch screen updates the banner in
|
|
37
|
+
// place. It is committed to scrollback together with the first settled row
|
|
38
|
+
// (same render commit, so the hand-off is seamless) and frozen from then on.
|
|
39
|
+
const bannerCommitted = regularMessages.length > 0;
|
|
34
40
|
const staticItems = [];
|
|
35
|
-
if (welcomeBanner) {
|
|
41
|
+
if (welcomeBanner && bannerCommitted) {
|
|
36
42
|
staticItems.push({ kind: "welcome", key: "welcome" });
|
|
37
43
|
}
|
|
38
44
|
const lastMessageIndex = regularMessages.length - 1;
|
|
@@ -55,10 +61,10 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
55
61
|
const clampDynamic = typeof maxStreamRows === "number" && maxStreamRows > 0;
|
|
56
62
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Static, { items: staticItems, children: (item) => {
|
|
57
63
|
if (item.kind === "welcome") {
|
|
58
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: welcomeBanner }, item.key));
|
|
64
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: welcomeBanner }, item.key));
|
|
59
65
|
}
|
|
60
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
|
|
61
|
-
} }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
|
|
66
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
|
|
67
|
+
} }, `transcript-${staticGeneration}`), welcomeBanner && !bannerCommitted && (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: welcomeBanner })), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, backgroundColor: theme.background, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
|
|
@@ -70,7 +76,7 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
70
76
|
* (no reserved-space gap). The full turn still lands in <Static> scrollback the
|
|
71
77
|
* moment it commits, so nothing is lost — only the live preview is windowed.
|
|
72
78
|
*/
|
|
73
|
-
function DynamicClamp({ maxRows, paddingX, children, }) {
|
|
79
|
+
function DynamicClamp({ maxRows, paddingX, backgroundColor, children, }) {
|
|
74
80
|
const innerRef = React.useRef(null);
|
|
75
81
|
const [offset, setOffset] = React.useState(0);
|
|
76
82
|
const [clipHeight, setClipHeight] = React.useState(undefined);
|
|
@@ -102,7 +108,7 @@ function DynamicClamp({ maxRows, paddingX, children, }) {
|
|
|
102
108
|
setClipHeight(undefined);
|
|
103
109
|
}
|
|
104
110
|
});
|
|
105
|
-
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: paddingX, ...(clipHeight !== undefined ? { height: clipHeight, overflowY: "hidden" } : {}), children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: offset, children: children }) }));
|
|
111
|
+
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: paddingX, backgroundColor: backgroundColor, ...(clipHeight !== undefined ? { height: clipHeight, overflowY: "hidden" } : {}), children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: offset, children: children }) }));
|
|
106
112
|
}
|
|
107
113
|
// Memoized: with no <Static> region, every transcript row re-renders on each
|
|
108
114
|
// state change unless its props are referentially stable. Message objects are
|
|
@@ -123,6 +129,11 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
|
|
|
123
129
|
if (message.syntheticKind === "ui_interrupt") {
|
|
124
130
|
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" })] }));
|
|
125
131
|
}
|
|
132
|
+
// One-line status notices (model switch etc.): accent-colored so they read
|
|
133
|
+
// as UI feedback, not assistant prose.
|
|
134
|
+
if (message.syntheticKind === "ui_notice") {
|
|
135
|
+
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsxs(Text, { color: theme.accent, children: ["\u2726 ", message.content] }) }));
|
|
136
|
+
}
|
|
126
137
|
const visibleReasoning = sanitizeInternalReminderBlocks(message.reasoning ?? "").trim();
|
|
127
138
|
// Same defense as reasoning: strip any internal reminder markup the model
|
|
128
139
|
// echoed back into its visible answer so it never reaches the transcript.
|
|
@@ -209,7 +220,7 @@ function TimelineText({ content, compactTop, terminalColumns, streaming = false,
|
|
|
209
220
|
? Math.max(20, terminalColumns - gutter - TRANSCRIPT_ROW_PADDING)
|
|
210
221
|
: undefined;
|
|
211
222
|
const trimmed = visible.trim();
|
|
212
|
-
return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
|
|
223
|
+
return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available, flushTop: true })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available, flushTop: true })) })] }));
|
|
213
224
|
}
|
|
214
225
|
function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
|
|
215
226
|
if (toolCalls.length === 0)
|
|
@@ -22,10 +22,17 @@ export function resolvePickerKeyAction(input, key) {
|
|
|
22
22
|
return "backspace";
|
|
23
23
|
if (key.delete)
|
|
24
24
|
return "delete";
|
|
25
|
+
// Batched chunks: slow ptys (web-terminal bridges, SSH, paste) can deliver
|
|
26
|
+
// several keys in ONE input event, in which case ink sets no key flags at
|
|
27
|
+
// all ("\r/quit\r" arrives with return=false). Act on the chunk's leading
|
|
28
|
+
// key instead of swallowing it — exact-match anchors would drop the whole
|
|
29
|
+
// chunk.
|
|
30
|
+
if (input.startsWith("\r") || input.startsWith("\n"))
|
|
31
|
+
return "enter";
|
|
25
32
|
const sequence = normalizeEscapeSequence(input);
|
|
26
|
-
if (/^(?:O|\[[\d;:]*)A
|
|
33
|
+
if (/^(?:O|\[[\d;:]*)A/.test(sequence))
|
|
27
34
|
return "up";
|
|
28
|
-
if (/^(?:O|\[[\d;:]*)B
|
|
35
|
+
if (/^(?:O|\[[\d;:]*)B/.test(sequence))
|
|
29
36
|
return "down";
|
|
30
37
|
return undefined;
|
|
31
38
|
}
|
|
@@ -793,6 +800,16 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title,
|
|
|
793
800
|
const idx = providers.findIndex((p) => p.id === current);
|
|
794
801
|
return idx >= 0 ? idx : 0;
|
|
795
802
|
});
|
|
803
|
+
// Authoritative selection lives in a ref, mutated synchronously by the
|
|
804
|
+
// input handler; state only mirrors it for display. Slow ptys (web-terminal
|
|
805
|
+
// bridges) can deliver several key events before React commits a re-render,
|
|
806
|
+
// and a state-only selection would leave the handler closure stale — Enter
|
|
807
|
+
// would then select the pre-batch highlight.
|
|
808
|
+
const selectedIndexRef = useRef(selectedIndex);
|
|
809
|
+
const moveSelection = (next) => {
|
|
810
|
+
selectedIndexRef.current = next;
|
|
811
|
+
setSelectedIndex(next);
|
|
812
|
+
};
|
|
796
813
|
useEffect(() => {
|
|
797
814
|
const p = providers[selectedIndex];
|
|
798
815
|
if (p)
|
|
@@ -807,30 +824,31 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title,
|
|
|
807
824
|
return;
|
|
808
825
|
}
|
|
809
826
|
if (action === "enter") {
|
|
810
|
-
const p = providers[
|
|
827
|
+
const p = providers[selectedIndexRef.current];
|
|
811
828
|
if (p)
|
|
812
829
|
onSelect(p.id);
|
|
813
830
|
return;
|
|
814
831
|
}
|
|
815
832
|
if (action === "up") {
|
|
816
|
-
|
|
833
|
+
moveSelection((selectedIndexRef.current - 1 + providers.length) % providers.length);
|
|
817
834
|
return;
|
|
818
835
|
}
|
|
819
836
|
if (action === "down") {
|
|
820
|
-
|
|
837
|
+
moveSelection((selectedIndexRef.current + 1) % providers.length);
|
|
821
838
|
return;
|
|
822
839
|
}
|
|
823
840
|
if (isPrintablePickerInput(input) && input.length === 1 && /[a-z]/i.test(input)) {
|
|
824
841
|
const char = input.toLowerCase();
|
|
825
|
-
|
|
842
|
+
const from = selectedIndexRef.current;
|
|
843
|
+
for (let i = from + 1; i < providers.length; i++) {
|
|
826
844
|
if (providers[i].name.toLowerCase().startsWith(char)) {
|
|
827
|
-
|
|
845
|
+
moveSelection(i);
|
|
828
846
|
return;
|
|
829
847
|
}
|
|
830
848
|
}
|
|
831
|
-
for (let i = 0; i <=
|
|
849
|
+
for (let i = 0; i <= from; i++) {
|
|
832
850
|
if (providers[i].name.toLowerCase().startsWith(char)) {
|
|
833
|
-
|
|
851
|
+
moveSelection(i);
|
|
834
852
|
return;
|
|
835
853
|
}
|
|
836
854
|
}
|
|
@@ -838,7 +856,12 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title,
|
|
|
838
856
|
});
|
|
839
857
|
const start = Math.max(0, Math.min(selectedIndex, providers.length - maxVisible));
|
|
840
858
|
const visible = providers.slice(start, start + maxVisible);
|
|
841
|
-
return (
|
|
859
|
+
return (
|
|
860
|
+
// backgroundColor lives on the picker's OWN root box, below its state:
|
|
861
|
+
// a canvas toggle (theme preview across a forced-theme mismatch) then
|
|
862
|
+
// reshapes only this stateless output subtree. On an ancestor wrapper it
|
|
863
|
+
// would remount the whole picker and reset selectedIndex mid-navigation.
|
|
864
|
+
_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, backgroundColor: theme.background, children: [_jsx(Text, { bold: true, color: theme.accent, children: title || "Select Provider" }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 type letter to jump" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((p, i) => {
|
|
842
865
|
const actualIndex = start + i;
|
|
843
866
|
const isSelected = actualIndex === selectedIndex;
|
|
844
867
|
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", p.name, p.id === current ? " (current)" : "", !p.enabled ? " [disabled]" : ""] }) }, p.id));
|
package/dist/tui-ink/run.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface RunTuiOptions {
|
|
|
34
34
|
mcpManager?: McpManager;
|
|
35
35
|
/** Shared with the model-facing goal tools and the Ink auto-continuation loop. */
|
|
36
36
|
goalStore?: import("../goal/store.js").GoalStore;
|
|
37
|
+
/** Unified process manager; drives task notices, auto-resume, and the status row. */
|
|
38
|
+
processManager?: import("../tasks/manager.js").ProcessManager;
|
|
39
|
+
/** tasks.auto_resume from user config (design §2.3b). Default ON. */
|
|
40
|
+
tasksAutoResume?: boolean;
|
|
41
|
+
/** Ctrl+G send-to-background channel shared with the bash tool (design §2.5). */
|
|
42
|
+
promotionChannel?: import("../tasks/promotion.js").PromotionChannel;
|
|
37
43
|
themeMode?: ThemeMode;
|
|
38
44
|
themeOverrides?: Record<string, string>;
|
|
39
45
|
detectedTheme?: ResolvedTheme;
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import { render } from "ink";
|
|
|
3
3
|
import { App } from "./app.js";
|
|
4
4
|
import { warmHighlighter } from "./code-highlight.js";
|
|
5
5
|
export function createInkAppElement(agent, args, options, onExit) {
|
|
6
|
-
return (_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, switchSession: options.switchSession, 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, goalStore: options.goalStore, 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, updateNoticeRefresh: options.updateNoticeRefresh, hookController: options.hookController, externalRuntime: options.externalRuntime, onExit: onExit }));
|
|
6
|
+
return (_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, switchSession: options.switchSession, 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, goalStore: options.goalStore, processManager: options.processManager, tasksAutoResume: options.tasksAutoResume, promotionChannel: options.promotionChannel, 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, updateNoticeRefresh: options.updateNoticeRefresh, hookController: options.hookController, externalRuntime: options.externalRuntime, onExit: onExit }));
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Best-effort terminal restore for abnormal exits. Bubble renders into the
|
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* members as their events stream in.
|
|
10
10
|
*/
|
|
11
11
|
import { type SubagentGroup } from "./subagent-view.js";
|
|
12
|
+
import type { BackgroundTaskInfo } from "../tasks/manager.js";
|
|
12
13
|
export type { SubagentGroup };
|
|
13
14
|
export interface SubagentInspectorProps {
|
|
14
15
|
groups: SubagentGroup[];
|
|
15
16
|
onCancel: () => void;
|
|
17
|
+
/** Background tasks of the bound session (design §2.5). */
|
|
18
|
+
tasks?: BackgroundTaskInfo[];
|
|
19
|
+
onKillTask?: (taskId: string) => void;
|
|
16
20
|
}
|
|
17
|
-
export declare function SubagentInspector({ groups, onCancel }: SubagentInspectorProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function SubagentInspector({ groups, onCancel, tasks, onKillTask }: SubagentInspectorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -16,7 +16,7 @@ import { useTheme } from "./theme.js";
|
|
|
16
16
|
import { padVisual, truncateVisual } from "../text-display.js";
|
|
17
17
|
import { latestSubagentNote, subagentDescriptor, subagentLabel, subagentStatusColor, subagentSummary, } from "./subagent-view.js";
|
|
18
18
|
const STATUS_FILTERS = [null, "running", "queued", "completed", "failed"];
|
|
19
|
-
export function SubagentInspector({ groups, onCancel }) {
|
|
19
|
+
export function SubagentInspector({ groups, onCancel, tasks = [], onKillTask }) {
|
|
20
20
|
const theme = useTheme();
|
|
21
21
|
const { stdout } = useStdout();
|
|
22
22
|
const termHeight = stdout?.rows || 24;
|
|
@@ -28,9 +28,13 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
28
28
|
const [filterIdx, setFilterIdx] = useState(0);
|
|
29
29
|
const statusFilter = STATUS_FILTERS[filterIdx];
|
|
30
30
|
const allMembers = useMemo(() => groups.flatMap((g) => g.members), [groups]);
|
|
31
|
-
// Flat row list:
|
|
31
|
+
// Flat row list: background tasks first (design §2.5), then a header per
|
|
32
|
+
// multi-member group and its (filtered) members.
|
|
32
33
|
const rows = useMemo(() => {
|
|
33
34
|
const out = [];
|
|
35
|
+
for (const task of tasks) {
|
|
36
|
+
out.push({ type: "task", task, key: task.id });
|
|
37
|
+
}
|
|
34
38
|
for (const group of groups) {
|
|
35
39
|
const members = statusFilter
|
|
36
40
|
? group.members.filter((m) => (m.status ?? "running") === statusFilter)
|
|
@@ -44,12 +48,13 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
return out;
|
|
47
|
-
}, [groups, statusFilter]);
|
|
48
|
-
const memberRowIndices = useMemo(() => rows.map((row, i) => (row.type === "member" ? i : -1)).filter((i) => i >= 0), [rows]);
|
|
51
|
+
}, [groups, statusFilter, tasks]);
|
|
52
|
+
const memberRowIndices = useMemo(() => rows.map((row, i) => (row.type === "member" || row.type === "task" ? i : -1)).filter((i) => i >= 0), [rows]);
|
|
49
53
|
const clampedIdx = memberRowIndices.length === 0 ? 0 : Math.min(selectedIdx, memberRowIndices.length - 1);
|
|
50
54
|
const selectedRowIndex = memberRowIndices[clampedIdx] ?? -1;
|
|
51
55
|
const selectedRow = rows[selectedRowIndex];
|
|
52
56
|
const selectedMember = selectedRow?.type === "member" ? selectedRow.member : undefined;
|
|
57
|
+
const selectedTask = selectedRow?.type === "task" ? selectedRow.task : undefined;
|
|
53
58
|
useInput((input, key) => {
|
|
54
59
|
if (isKeyReleaseEvent(key))
|
|
55
60
|
return;
|
|
@@ -91,9 +96,13 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
91
96
|
setView("detail");
|
|
92
97
|
return;
|
|
93
98
|
}
|
|
99
|
+
if (input === "x" && selectedTask && selectedTask.status === "running") {
|
|
100
|
+
onKillTask?.(selectedTask.id);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
94
103
|
});
|
|
95
|
-
if (groups.length === 0) {
|
|
96
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Subagents" }), _jsx(Text, { color: theme.muted, children: "No subagents
|
|
104
|
+
if (groups.length === 0 && tasks.length === 0) {
|
|
105
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Subagents & tasks" }), _jsx(Text, { color: theme.muted, children: "No subagents or background tasks yet. Esc to close." })] }));
|
|
97
106
|
}
|
|
98
107
|
if (view === "detail" && selectedMember) {
|
|
99
108
|
return (_jsx(SubagentDetail, { member: selectedMember, group: selectedRow?.type === "member" ? selectedRow.group : undefined, scroll: detailScroll, maxVisible: maxVisible, termWidth: termWidth }));
|
|
@@ -103,8 +112,15 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
103
112
|
const visible = rows.slice(start, start + maxVisible);
|
|
104
113
|
const labelWidth = 12;
|
|
105
114
|
const descriptorWidth = Math.max(20, Math.min(46, termWidth - 48));
|
|
106
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Subagents
|
|
115
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: tasks.length > 0 ? "Subagents & tasks · working traces" : "Subagents · working traces" }), _jsxs(Text, { color: theme.muted, children: [allMembers.length, " member", allMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(allMembers), tasks.length > 0 ? ` · ${tasks.length} task${tasks.length === 1 ? "" : "s"}` : "", statusFilter ? ` · filter: ${statusFilter}` : ""] }), _jsxs(Text, { color: theme.muted, children: ["\u2191/\u2193 select \u00B7 Enter/\u2192 open trace", tasks.length > 0 ? " · x kill task" : "", " \u00B7 f filter status \u00B7 Esc close"] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [rows.length === 0 && (_jsx(Text, { color: theme.muted, children: "No members match the current filter." })), visible.map((row, i) => {
|
|
107
116
|
const actualIndex = start + i;
|
|
117
|
+
if (row.type === "task") {
|
|
118
|
+
const task = row.task;
|
|
119
|
+
const isSelected = actualIndex === selectedRowIndex;
|
|
120
|
+
const elapsed = Math.max(0, Math.round(((task.endedAt ?? Date.now()) - task.startedAt) / 1000));
|
|
121
|
+
const statusColor = task.status === "running" ? theme.warning : task.status === "completed" ? theme.success : theme.error;
|
|
122
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? theme.accent : undefined, children: isSelected ? "> " : " " }), _jsx(Text, { color: statusColor, children: padVisual(task.id, labelWidth) }), _jsxs(Text, { color: theme.traceAction, children: [" ", padVisual(truncateVisual(task.description || task.command, descriptorWidth), descriptorWidth)] }), _jsxs(Text, { color: statusColor, children: [" ", padVisual(task.status, 9)] }), _jsxs(Text, { color: theme.traceDetail, children: [" ", elapsed, "s", task.exitCode != null ? ` · exit ${task.exitCode}` : ""] })] }, row.key));
|
|
123
|
+
}
|
|
108
124
|
if (row.type === "header") {
|
|
109
125
|
return (_jsx(Box, { marginTop: i === 0 ? 0 : 1, children: _jsxs(Text, { bold: true, color: theme.muted, children: ["\u25A6 ", row.group.kind, " \u00B7 ", truncateVisual(row.group.label, termWidth - 18), " (", row.group.members.length, ")"] }) }, `h-${actualIndex}`));
|
|
110
126
|
}
|
package/dist/tui-ink/welcome.js
CHANGED
|
@@ -32,6 +32,10 @@ function GradientText({ text, from, to }) {
|
|
|
32
32
|
export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelId, modelLabel, thinkingLabel, }) {
|
|
33
33
|
const theme = useTheme();
|
|
34
34
|
const effectiveWidth = Math.max(24, Math.min(terminalColumns - 2, 96));
|
|
35
|
+
// The model row here refreshes live only while the banner is still in the
|
|
36
|
+
// dynamic region (empty transcript). Once the first settled row commits it
|
|
37
|
+
// into <Static> scrollback it freezes; from then on the accent-colored
|
|
38
|
+
// "Model switched" transcript notices carry model changes.
|
|
35
39
|
const modelLine = formatModelLine({
|
|
36
40
|
providerId,
|
|
37
41
|
modelId,
|