@bubblebrain-ai/bubble 0.0.37 → 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.
Files changed (51) hide show
  1. package/dist/agent/internal-reminder-sanitizer.js +2 -0
  2. package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
  3. package/dist/agent/task-lifecycle-reminder.js +59 -0
  4. package/dist/agent.d.ts +26 -0
  5. package/dist/agent.js +39 -0
  6. package/dist/approval/controller.d.ts +3 -1
  7. package/dist/approval/controller.js +5 -2
  8. package/dist/approval/tool-helper.js +10 -1
  9. package/dist/approval/types.d.ts +15 -0
  10. package/dist/config.d.ts +6 -0
  11. package/dist/config.js +4 -0
  12. package/dist/loop/engine.d.ts +29 -0
  13. package/dist/loop/engine.js +63 -0
  14. package/dist/main.js +29 -0
  15. package/dist/orchestrator/default-hooks.js +16 -0
  16. package/dist/session-types.d.ts +1 -1
  17. package/dist/tasks/manager.d.ts +136 -0
  18. package/dist/tasks/manager.js +530 -0
  19. package/dist/tasks/process-utils.d.ts +10 -0
  20. package/dist/tasks/process-utils.js +49 -0
  21. package/dist/tasks/promotion.d.ts +17 -0
  22. package/dist/tasks/promotion.js +30 -0
  23. package/dist/tasks/wake.d.ts +53 -0
  24. package/dist/tasks/wake.js +125 -0
  25. package/dist/tools/bash.d.ts +15 -1
  26. package/dist/tools/bash.js +103 -3
  27. package/dist/tools/child-tools.js +3 -1
  28. package/dist/tools/edit.js +4 -12
  29. package/dist/tools/index.d.ts +13 -0
  30. package/dist/tools/index.js +8 -1
  31. package/dist/tools/server-manager.d.ts +6 -36
  32. package/dist/tools/server-manager.js +6 -234
  33. package/dist/tools/task-tools.d.ts +10 -0
  34. package/dist/tools/task-tools.js +129 -0
  35. package/dist/tools/write.js +6 -12
  36. package/dist/tui-ink/app.d.ts +20 -2
  37. package/dist/tui-ink/app.js +307 -19
  38. package/dist/tui-ink/approval/approval-dialog.js +4 -4
  39. package/dist/tui-ink/display-history.d.ts +1 -1
  40. package/dist/tui-ink/footer.d.ts +3 -3
  41. package/dist/tui-ink/footer.js +3 -3
  42. package/dist/tui-ink/input-box.d.ts +6 -0
  43. package/dist/tui-ink/markdown.d.ts +10 -2
  44. package/dist/tui-ink/markdown.js +13 -12
  45. package/dist/tui-ink/message-list.js +14 -3
  46. package/dist/tui-ink/run.d.ts +6 -0
  47. package/dist/tui-ink/run.js +1 -1
  48. package/dist/tui-ink/subagent-inspector.d.ts +5 -1
  49. package/dist/tui-ink/subagent-inspector.js +23 -7
  50. package/dist/tui-ink/welcome.js +4 -0
  51. package/package.json +1 -1
@@ -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", marginY: 1, children: [lang && _jsx(Text, { color: theme.muted, children: lang }), _jsx(Box, { flexDirection: "column", children: highlighted.map((line, i) => (_jsx(Text, { children: line || " " }, i))) })] }));
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", 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
+ 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;
@@ -58,7 +64,7 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
58
64
  return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: welcomeBanner }, item.key));
59
65
  }
60
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));
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 }))] }))] }));
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
@@ -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)
@@ -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;
@@ -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: a header per multi-member group, then its (filtered) members.
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 have been spawned yet. Esc to close." })] }));
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 \u00B7 working traces" }), _jsxs(Text, { color: theme.muted, children: [allMembers.length, " member", allMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(allMembers), statusFilter ? ` · filter: ${statusFilter}` : ""] }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 select \u00B7 Enter/\u2192 open trace \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) => {
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
  }
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {