@bubblebrain-ai/bubble 0.0.31 → 0.0.33

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.
@@ -72,6 +72,12 @@ function friendlyCwd(cwd) {
72
72
  return "~" + cwd.slice(home.length);
73
73
  return cwd;
74
74
  }
75
+ function sessionBasename(sessionFile) {
76
+ if (!sessionFile)
77
+ return undefined;
78
+ const base = sessionFile.split("/").pop() ?? sessionFile;
79
+ return base.replace(/\.jsonl$/, "");
80
+ }
75
81
  function truncate(value, max) {
76
82
  if (value.length <= max)
77
83
  return value;
@@ -1686,7 +1692,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1686
1692
  const showThinkingLabel = Boolean(thinkingLevel)
1687
1693
  && thinkingLevel !== "off"
1688
1694
  && (isMiniMaxProvider || getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2);
1689
- const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry), updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), providerId: agent.providerId || safeRegistry.getDefault()?.id, modelLabel: agent.model ? displayModel(agent.model) : undefined, thinkingLabel: showThinkingLabel ? thinkingLevel : undefined })) : null;
1695
+ const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry), updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), sessionLabel: sessionBasename(currentSessionFile()), providerId: agent.providerId || safeRegistry.getDefault()?.id, modelLabel: agent.model ? displayModel(agent.model) : undefined, thinkingLabel: showThinkingLabel ? thinkingLevel : undefined })) : null;
1690
1696
  const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry), [safeSkillRegistry]);
1691
1697
  const mcpReconnectItems = useMemo(() => buildMcpReconnectItems(mcpManager), [mcpManager]);
1692
1698
  // No fixed-height frame: settled rows flow into the terminal's native
@@ -44,6 +44,13 @@ export declare function parseMarkdownBlocks(text: string): MarkdownBlock[];
44
44
  */
45
45
  export declare function findLastBlockStart(text: string): number;
46
46
  export declare function parseMarkdownInlineSegments(text: string, style?: InlineStyle): MarkdownInlineSegment[];
47
+ /**
48
+ * Distribute `available` inner cells across columns. Columns whose natural
49
+ * width already fits their fair share keep it untouched (numbers and dates
50
+ * never wrap); only the wider columns split the remainder proportionally and
51
+ * wrap their content.
52
+ */
53
+ export declare function allocateColumnWidths(natural: number[], available: number): number[];
47
54
  /**
48
55
  * CJK-aware line wrap over styled inline segments.
49
56
  *
@@ -406,32 +406,92 @@ function TableBlock({ headers, rows, maxWidth, }) {
406
406
  let widths = [...maxWidths];
407
407
  if (totalWidth > budget) {
408
408
  const available = Math.max(budget - separatorsWidth, colCount * 4);
409
- const ratio = totalInnerWidth > 0 ? available / totalInnerWidth : 1;
410
- widths = maxWidths.map((w) => Math.max(4, Math.floor(w * ratio)));
411
- // The 4-cell floor can push the sum back above `available`; shave the
412
- // overshoot off the widest columns so the row never exceeds the budget
413
- // and gets hard-wrapped by the terminal.
414
- let excess = widths.reduce((a, b) => a + b, 0) - available;
415
- while (excess > 0) {
416
- let widest = -1;
417
- for (let i = 0; i < widths.length; i++) {
418
- if (widths[i] > 4 && (widest === -1 || widths[i] > widths[widest]))
419
- widest = i;
420
- }
421
- if (widest === -1)
422
- break;
423
- widths[widest] -= 1;
424
- excess -= 1;
425
- }
409
+ widths = allocateColumnWidths(maxWidths, available);
426
410
  }
427
411
  const top = g.tl + widths.map((w) => g.h.repeat(w + 2)).join(g.tm) + g.tr;
428
412
  const mid = g.ml + widths.map((w) => g.h.repeat(w + 2)).join(g.mm) + g.mr;
429
413
  const bot = g.bl + widths.map((w) => g.h.repeat(w + 2)).join(g.bm) + g.br;
430
- const renderRow = (cells, keyPrefix, isHeader = false) => (_jsxs(Text, { children: [`${g.v} `, cells.map((c, i) => (_jsxs(React.Fragment, { children: [renderTableCell(c, widths[i] ?? 4, isHeader, `${keyPrefix}-cell-${i}`), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, keyPrefix));
414
+ // A cell wider than its column wraps onto continuation lines (CJK-aware, so
415
+ // 、-joined user lists break cleanly); the row grows to its tallest cell.
416
+ const renderRow = (cells, keyPrefix, isHeader = false) => {
417
+ const wrapped = cells.map((c, i) => {
418
+ const width = widths[i] ?? 4;
419
+ let lines = wrapInlineSegments(parseMarkdownInlineSegments(c, { bold: isHeader }), width);
420
+ if (lines.length > MAX_TABLE_CELL_LINES) {
421
+ lines = lines.slice(0, MAX_TABLE_CELL_LINES);
422
+ const last = lines[MAX_TABLE_CELL_LINES - 1];
423
+ lines[MAX_TABLE_CELL_LINES - 1] = truncateInlineSegments([...last, { text: " …" }], width);
424
+ }
425
+ return lines;
426
+ });
427
+ const height = Math.max(1, ...wrapped.map((lines) => lines.length));
428
+ 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}`), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, `${keyPrefix}-line-${line}`))) }, keyPrefix));
429
+ };
431
430
  return (_jsxs(Box, { flexDirection: "column", marginY: 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 })] }));
432
431
  }
433
- function renderTableCell(cell, width, isHeader, keyPrefix) {
434
- const segments = truncateInlineSegments(parseMarkdownInlineSegments(cell, { bold: isHeader }), width);
432
+ /** Cap a pathological cell (huge blob of text) at this many wrapped lines. */
433
+ const MAX_TABLE_CELL_LINES = 8;
434
+ /**
435
+ * Distribute `available` inner cells across columns. Columns whose natural
436
+ * width already fits their fair share keep it untouched (numbers and dates
437
+ * never wrap); only the wider columns split the remainder proportionally and
438
+ * wrap their content.
439
+ */
440
+ export function allocateColumnWidths(natural, available) {
441
+ const count = natural.length;
442
+ const widths = new Array(count).fill(0);
443
+ const fixed = new Array(count).fill(false);
444
+ let remaining = available;
445
+ let unfixed = count;
446
+ // Each fixed column frees slack that can fit further columns — iterate to a
447
+ // fixpoint. Terminates: natural sum exceeds `available` (only caller path),
448
+ // so at least one column always stays unfixed.
449
+ let changed = true;
450
+ while (changed && unfixed > 0) {
451
+ changed = false;
452
+ const fair = remaining / unfixed;
453
+ for (let i = 0; i < count; i++) {
454
+ if (fixed[i] || natural[i] > fair)
455
+ continue;
456
+ fixed[i] = true;
457
+ widths[i] = natural[i];
458
+ remaining -= natural[i];
459
+ unfixed -= 1;
460
+ changed = true;
461
+ }
462
+ }
463
+ if (unfixed > 0) {
464
+ const wideTotal = natural.reduce((sum, w, i) => sum + (fixed[i] ? 0 : w), 0);
465
+ let assigned = 0;
466
+ let lastWide = -1;
467
+ for (let i = 0; i < count; i++) {
468
+ if (fixed[i])
469
+ continue;
470
+ widths[i] = Math.max(4, Math.floor((remaining * natural[i]) / wideTotal));
471
+ assigned += widths[i];
472
+ lastWide = i;
473
+ }
474
+ // Flooring leftovers go to the last wide column instead of being wasted.
475
+ if (lastWide >= 0 && assigned < remaining)
476
+ widths[lastWide] += remaining - assigned;
477
+ }
478
+ // On a very narrow budget the 4-cell floor can overshoot; shave the widest
479
+ // columns so the row never exceeds `available` and hard-wraps.
480
+ let excess = widths.reduce((a, b) => a + b, 0) - available;
481
+ while (excess > 0) {
482
+ let widest = -1;
483
+ for (let i = 0; i < count; i++) {
484
+ if (widths[i] > 4 && (widest === -1 || widths[i] > widths[widest]))
485
+ widest = i;
486
+ }
487
+ if (widest === -1)
488
+ break;
489
+ widths[widest] -= 1;
490
+ excess -= 1;
491
+ }
492
+ return widths;
493
+ }
494
+ function renderCellLine(segments, width, keyPrefix) {
435
495
  const padding = " ".repeat(Math.max(0, width - inlineSegmentsWidth(segments)));
436
496
  return [
437
497
  ...segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`))),
@@ -139,7 +139,11 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
139
139
  message.taskElapsedMs !== undefined;
140
140
  if (!hasVisibleAssistantContent)
141
141
  return null;
142
- return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || 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 })), visibleContent.trim() && _jsx(MarkdownContent, { content: visibleContent })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
142
+ return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || 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 })), visibleContent.trim() && (
143
+ // Thread the real terminal width down (minus the row's paddingX
144
+ // inset); without it, width-aware blocks like tables fall back to
145
+ // a stale/default useTerminalSize and mis-budget their columns.
146
+ _jsx(MarkdownContent, { content: visibleContent, maxWidth: Math.max(20, terminalColumns - 4) }))] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
143
147
  });
144
148
  function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, verboseTrace, pendingApproval, nowTick, }) {
145
149
  const deferredContent = React.useDeferredValue(content);
@@ -17,6 +17,11 @@ export interface Theme {
17
17
  success: string;
18
18
  background: string;
19
19
  accent: string;
20
+ /** Welcome banner border. */
21
+ bannerBorder: string;
22
+ /** Welcome banner logo/title gradient endpoints (top→bottom, left→right). */
23
+ bannerGradientFrom: string;
24
+ bannerGradientTo: string;
20
25
  border: string;
21
26
  borderActive: string;
22
27
  backgroundPanel: string;
@@ -16,6 +16,9 @@ export const darkTheme = {
16
16
  success: "green",
17
17
  background: "#0A0A0A",
18
18
  accent: "cyan",
19
+ bannerBorder: "#38bdf8",
20
+ bannerGradientFrom: "#67e8f9",
21
+ bannerGradientTo: "#a78bfa",
19
22
  border: "gray",
20
23
  borderActive: "cyan",
21
24
  backgroundPanel: "#141414",
@@ -62,6 +65,9 @@ export const lightTheme = {
62
65
  success: "#2F7D4A",
63
66
  background: "#FCFCFA",
64
67
  accent: "#8B4A00",
68
+ bannerBorder: "#356FD2",
69
+ bannerGradientFrom: "#0E7490",
70
+ bannerGradientTo: "#6D28D9",
65
71
  border: "#B9BDB8",
66
72
  borderActive: "#356FD2",
67
73
  backgroundPanel: "#F6F6F3",
@@ -6,6 +6,8 @@ interface WelcomeBannerProps {
6
6
  updateNotice?: string;
7
7
  /** Friendly working directory (~ collapsed). */
8
8
  cwd?: string;
9
+ /** Session identifier (session file basename). */
10
+ sessionLabel?: string;
9
11
  providerId?: string;
10
12
  modelLabel?: string;
11
13
  /** Active thinking level, rendered as part of the model unit (e.g. "xhigh"). */
@@ -16,6 +18,7 @@ interface WelcomeVisibilityInput {
16
18
  startedWithVisibleHistory: boolean;
17
19
  }
18
20
  export declare function shouldShowWelcomeBanner({ startedWithVisibleHistory, }: WelcomeVisibilityInput): boolean;
19
- export declare function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, providerId, modelLabel, thinkingLabel, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
21
+ export declare function lerpColor(from: string, to: string, t: number): string;
22
+ export declare function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelLabel, thinkingLabel, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
20
23
  export declare function formatModelLine({ providerId, modelLabel, thinkingLabel, tips, }: Pick<WelcomeBannerProps, "providerId" | "modelLabel" | "thinkingLabel" | "tips">): string;
21
24
  export {};
@@ -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 { Box, Text } from "ink";
3
3
  import { createRequire } from "node:module";
4
4
  import { useTheme } from "./theme.js";
@@ -18,7 +18,17 @@ export function shouldShowWelcomeBanner({ startedWithVisibleHistory, }) {
18
18
  return false;
19
19
  return true;
20
20
  }
21
- export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, providerId, modelLabel, thinkingLabel, }) {
21
+ export function lerpColor(from, to, t) {
22
+ const pa = [1, 3, 5].map((i) => parseInt(from.slice(i, i + 2), 16));
23
+ const pb = [1, 3, 5].map((i) => parseInt(to.slice(i, i + 2), 16));
24
+ const out = pa.map((v, i) => Math.round(v + ((pb[i] ?? v) - v) * t));
25
+ return `#${out.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
26
+ }
27
+ function GradientText({ text, from, to }) {
28
+ const chars = [...text];
29
+ return (_jsx(_Fragment, { children: chars.map((ch, i) => (_jsx(Text, { bold: true, color: lerpColor(from, to, chars.length <= 1 ? 0 : i / (chars.length - 1)), children: ch }, `ch-${i}`))) }));
30
+ }
31
+ export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelLabel, thinkingLabel, }) {
22
32
  const theme = useTheme();
23
33
  const effectiveWidth = Math.max(24, Math.min(terminalColumns - 2, 96));
24
34
  const modelLine = formatModelLine({
@@ -27,7 +37,16 @@ export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, provid
27
37
  thinkingLabel,
28
38
  tips,
29
39
  });
30
- return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", marginRight: 2, flexShrink: 0, children: COMPACT_LOGO.map((line, rowIndex) => (_jsx(Text, { color: theme.warning, bold: true, children: line }, `logo-row-${rowIndex}`))) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.inputText, children: "Bubble" }), _jsxs(Text, { color: theme.muted, children: [" ", PACKAGE_VERSION] })] }), modelLine && (_jsx(Text, { color: theme.muted, children: modelLine })), cwd && (_jsx(Text, { color: theme.muted, children: cwd }))] })] }), updateNotice && (_jsx(Box, { children: _jsx(Text, { color: theme.accent, children: updateNotice }) }))] }));
40
+ const infoRows = [];
41
+ if (cwd)
42
+ infoRows.push({ label: "Directory:", value: cwd, color: theme.inputText });
43
+ if (sessionLabel)
44
+ infoRows.push({ label: "Session:", value: sessionLabel, color: theme.muted });
45
+ if (modelLine)
46
+ infoRows.push({ label: "Model:", value: modelLine, color: theme.traceCommand });
47
+ infoRows.push({ label: "Version:", value: PACKAGE_VERSION, color: theme.muted });
48
+ const labelWidth = Math.max(...infoRows.map((row) => row.label.length)) + 1;
49
+ return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: theme.bannerBorder, paddingX: 2, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", marginRight: 2, flexShrink: 0, children: COMPACT_LOGO.map((line, rowIndex) => (_jsx(Text, { bold: true, color: lerpColor(theme.bannerGradientFrom, theme.bannerGradientTo, COMPACT_LOGO.length <= 1 ? 0 : rowIndex / (COMPACT_LOGO.length - 1)), children: line }, `logo-row-${rowIndex}`))) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, flexShrink: 1, children: [_jsx(Box, { children: _jsx(GradientText, { text: "Welcome to Bubble!", from: theme.bannerGradientFrom, to: theme.bannerGradientTo }) }), _jsx(Text, { color: theme.muted, wrap: "wrap", children: "I am a cat and you can send /help for help information." })] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: infoRows.map((row) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: theme.dim, children: row.label.padEnd(labelWidth) }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { color: row.color, wrap: "wrap", children: row.value }) })] }, row.label))) }), updateNotice && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.accent, children: updateNotice }) }))] }));
31
50
  }
32
51
  export function formatModelLine({ providerId, modelLabel, thinkingLabel, tips, }) {
33
52
  const parts = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {