@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.
@@ -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, backgroundColor: palette.background, 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) => ({
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, backgroundColor: palette.background, 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({
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$/.test(sequence))
33
+ if (/^(?:O|\[[\d;:]*)A/.test(sequence))
27
34
  return "up";
28
- if (/^(?:O|\[[\d;:]*)B$/.test(sequence))
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[selectedIndex];
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
- setSelectedIndex((i) => Math.max(0, i - 1));
833
+ moveSelection((selectedIndexRef.current - 1 + providers.length) % providers.length);
817
834
  return;
818
835
  }
819
836
  if (action === "down") {
820
- setSelectedIndex((i) => Math.min(providers.length - 1, i + 1));
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
- for (let i = selectedIndex + 1; i < providers.length; i++) {
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
- setSelectedIndex(i);
845
+ moveSelection(i);
828
846
  return;
829
847
  }
830
848
  }
831
- for (let i = 0; i <= selectedIndex; i++) {
849
+ for (let i = 0; i <= from; i++) {
832
850
  if (providers[i].name.toLowerCase().startsWith(char)) {
833
- setSelectedIndex(i);
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 (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, 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) => {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {