@bubblebrain-ai/bubble 0.0.36 → 0.0.37
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/tui-ink/app.js
CHANGED
|
@@ -342,7 +342,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
342
342
|
const [staticGeneration, setStaticGeneration] = useState(0);
|
|
343
343
|
const reprintTranscript = useCallback(() => {
|
|
344
344
|
if (process.stdout.isTTY) {
|
|
345
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
345
|
+
process.stdout.write("\x1b[0m\x1b[2J\x1b[3J\x1b[H");
|
|
346
346
|
}
|
|
347
347
|
setStaticGeneration((generation) => generation + 1);
|
|
348
348
|
}, []);
|
|
@@ -2112,7 +2112,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2112
2112
|
// to its content keeps the composer pinned just below the latest output the
|
|
2113
2113
|
// way ordinary shell programs do.
|
|
2114
2114
|
const mainWidth = Math.max(40, terminalColumns);
|
|
2115
|
-
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", width: mainWidth,
|
|
2115
|
+
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", width: mainWidth, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode, staticGeneration: staticGeneration, paddingX: 1, maxStreamRows: Math.max(6, terminalRows - 10) }), pickerMode === "grok-model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: grokModels.length === 0 ? (_jsx(Text, { children: "Loading Grok subscription models\u2026" })) : (_jsx(ProviderPicker, { title: "Select Grok Model and Reasoning", providers: grokModels.flatMap((model) => model.reasoningLevels.map((effort) => ({
|
|
2116
2116
|
id: JSON.stringify({ modelId: model.id, reasoningEffort: effort }),
|
|
2117
2117
|
name: `${model.name}${effort === "off" ? "" : ` · ${effort}`}`,
|
|
2118
2118
|
enabled: true,
|
|
@@ -2213,7 +2213,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2213
2213
|
: 0, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
|
|
2214
2214
|
if (subagentMembers.length > 0 && !pickerMode)
|
|
2215
2215
|
setSubagentEntryFocused(true);
|
|
2216
|
-
}, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: externalSessionBound ? undefined : safeSkillRegistry, localSlashCommands: externalSessionBound ? [] : [...INK_LOCAL_SLASH_COMMANDS], allowedSlashCommands: unsupportedExternalSessionBound ? GROK_LOCAL_SLASH_COMMANDS : undefined, allowWorkspaceMentions: !unsupportedExternalSessionBound, allowImageAttachments: !externalSessionBound, terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0,
|
|
2216
|
+
}, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: externalSessionBound ? undefined : safeSkillRegistry, localSlashCommands: externalSessionBound ? [] : [...INK_LOCAL_SLASH_COMMANDS], allowedSlashCommands: unsupportedExternalSessionBound ? GROK_LOCAL_SLASH_COMMANDS : undefined, allowWorkspaceMentions: !unsupportedExternalSessionBound, allowImageAttachments: !externalSessionBound, terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
|
|
2217
2217
|
mode: permissionMode,
|
|
2218
2218
|
goalLine: externalSessionBound ? undefined : goalLine,
|
|
2219
2219
|
runtimeLabel: externalSessionBound
|
|
@@ -55,10 +55,10 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
55
55
|
const clampDynamic = typeof maxStreamRows === "number" && maxStreamRows > 0;
|
|
56
56
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Static, { items: staticItems, children: (item) => {
|
|
57
57
|
if (item.kind === "welcome") {
|
|
58
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: welcomeBanner }, item.key));
|
|
58
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: welcomeBanner }, item.key));
|
|
59
59
|
}
|
|
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 }))] }))] }));
|
|
60
|
+
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));
|
|
61
|
+
} }, `transcript-${staticGeneration}`), 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
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
|
|
@@ -70,7 +70,7 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
70
70
|
* (no reserved-space gap). The full turn still lands in <Static> scrollback the
|
|
71
71
|
* moment it commits, so nothing is lost — only the live preview is windowed.
|
|
72
72
|
*/
|
|
73
|
-
function DynamicClamp({ maxRows, paddingX, children, }) {
|
|
73
|
+
function DynamicClamp({ maxRows, paddingX, backgroundColor, children, }) {
|
|
74
74
|
const innerRef = React.useRef(null);
|
|
75
75
|
const [offset, setOffset] = React.useState(0);
|
|
76
76
|
const [clipHeight, setClipHeight] = React.useState(undefined);
|
|
@@ -102,7 +102,7 @@ function DynamicClamp({ maxRows, paddingX, children, }) {
|
|
|
102
102
|
setClipHeight(undefined);
|
|
103
103
|
}
|
|
104
104
|
});
|
|
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 }) }));
|
|
105
|
+
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
106
|
}
|
|
107
107
|
// Memoized: with no <Static> region, every transcript row re-renders on each
|
|
108
108
|
// state change unless its props are referentially stable. Message objects are
|
|
@@ -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));
|