@bubblebrain-ai/bubble 0.0.28 → 0.0.30

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 (63) hide show
  1. package/README.md +23 -3
  2. package/dist/agent/categories.d.ts +2 -0
  3. package/dist/agent/categories.js +4 -0
  4. package/dist/agent/child-runner.d.ts +5 -1
  5. package/dist/agent/child-runner.js +35 -2
  6. package/dist/agent/profiles.js +3 -0
  7. package/dist/agent/structured-output.d.ts +37 -0
  8. package/dist/agent/structured-output.js +193 -0
  9. package/dist/agent/subagent-control.d.ts +3 -0
  10. package/dist/agent/subagent-scheduler.d.ts +10 -0
  11. package/dist/agent/subagent-scheduler.js +31 -0
  12. package/dist/agent/workflow/control.d.ts +37 -0
  13. package/dist/agent/workflow/control.js +20 -0
  14. package/dist/agent/workflow/errors.d.ts +16 -0
  15. package/dist/agent/workflow/errors.js +24 -0
  16. package/dist/agent/workflow/runtime.d.ts +75 -0
  17. package/dist/agent/workflow/runtime.js +237 -0
  18. package/dist/agent.d.ts +105 -0
  19. package/dist/agent.js +425 -17
  20. package/dist/context/compact-llm.d.ts +10 -1
  21. package/dist/context/compact-llm.js +13 -5
  22. package/dist/context/compact.d.ts +30 -0
  23. package/dist/context/compact.js +34 -17
  24. package/dist/goal/format.d.ts +1 -1
  25. package/dist/goal/format.js +1 -1
  26. package/dist/network/provider-transport.d.ts +9 -0
  27. package/dist/network/provider-transport.js +19 -1
  28. package/dist/provider.d.ts +14 -0
  29. package/dist/provider.js +24 -0
  30. package/dist/session.d.ts +16 -0
  31. package/dist/session.js +33 -1
  32. package/dist/slash-commands/commands.js +41 -113
  33. package/dist/slash-commands/types.d.ts +14 -9
  34. package/dist/tools/agent-lifecycle.d.ts +6 -0
  35. package/dist/tools/agent-lifecycle.js +285 -0
  36. package/dist/tools/child-tools.d.ts +10 -0
  37. package/dist/tools/child-tools.js +12 -0
  38. package/dist/tools/read.d.ts +1 -1
  39. package/dist/tools/read.js +9 -0
  40. package/dist/tui/image-display.d.ts +6 -0
  41. package/dist/tui/image-display.js +26 -1
  42. package/dist/tui-ink/app.d.ts +0 -18
  43. package/dist/tui-ink/app.js +168 -230
  44. package/dist/tui-ink/compaction-progress.d.ts +19 -0
  45. package/dist/tui-ink/compaction-progress.js +74 -0
  46. package/dist/tui-ink/input-box.d.ts +10 -1
  47. package/dist/tui-ink/input-box.js +56 -16
  48. package/dist/tui-ink/markdown.d.ts +18 -0
  49. package/dist/tui-ink/markdown.js +172 -16
  50. package/dist/tui-ink/message-list.d.ts +1 -2
  51. package/dist/tui-ink/message-list.js +50 -107
  52. package/dist/tui-ink/run.js +5 -0
  53. package/dist/tui-ink/subagent-inspector.d.ts +17 -0
  54. package/dist/tui-ink/subagent-inspector.js +189 -0
  55. package/dist/tui-ink/subagent-view.d.ts +47 -0
  56. package/dist/tui-ink/subagent-view.js +163 -0
  57. package/dist/tui-ink/terminal-env.d.ts +15 -0
  58. package/dist/tui-ink/terminal-env.js +22 -0
  59. package/dist/tui-ink/use-terminal-size.js +33 -6
  60. package/dist/tui-ink/width.d.ts +18 -0
  61. package/dist/tui-ink/width.js +130 -0
  62. package/dist/types.d.ts +35 -0
  63. package/package.json +2 -1
@@ -4,14 +4,15 @@ import { Box, Static, Text, measureElement } from "ink";
4
4
  import { useTheme } from "./theme.js";
5
5
  import { highlightCode, inferLang } from "./code-highlight.js";
6
6
  import { MarkdownContent, StreamingMarkdown } from "./markdown.js";
7
+ import { visualWidth, graphemeWidth, ambiguousIsWide } from "./width.js";
7
8
  import { userInputStatusBadgeLabel, } from "./display-history.js";
8
9
  import { buildTraceGroups, executeCommandBlock, formatTracePath, shouldInlineExecuteCommand, traceGroupLabel, } from "./trace-groups.js";
9
10
  import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
10
- import { formatSubagentRoute } from "../agent/subagent-route-format.js";
11
+ import { latestSubagentNote, sortSubagents, subagentDescriptor, subagentLabel, subagentStatusColor, subagentSummary, } from "./subagent-view.js";
11
12
  import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
12
13
  import { splitImageDisplayContent } from "../tui/image-display.js";
13
14
  const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
14
- export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, expandedToolOutput = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
15
+ export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
15
16
  const theme = useTheme();
16
17
  const hasStreaming = !!(streamingContent ||
17
18
  streamingReasoning ||
@@ -46,8 +47,8 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
46
47
  if (item.kind === "welcome") {
47
48
  return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: welcomeBanner }, item.key));
48
49
  }
49
- return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
50
- } }, `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, expandedToolOutput: expandedToolOutput, 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 }))] }))] }));
50
+ 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));
51
+ } }, `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 }))] }))] }));
51
52
  }
52
53
  /**
53
54
  * Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
@@ -98,7 +99,7 @@ function DynamicClamp({ maxRows, paddingX, children, }) {
98
99
  // append-only (compaction reuses already-compacted instances), keys are
99
100
  // stable, and nowTick is only threaded to the last row, so memo hits for all
100
101
  // settled history rows.
101
- const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking, expandedToolOutput, verboseTrace, showExpandHint, separateFromPrevious, nowTick, }) {
102
+ const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking, verboseTrace, showExpandHint, separateFromPrevious, nowTick, }) {
102
103
  const theme = useTheme();
103
104
  if (message.role === "user") {
104
105
  return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus, separateFromPrevious: separateFromPrevious }));
@@ -116,15 +117,31 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
116
117
  // Same defense as reasoning: strip any internal reminder markup the model
117
118
  // echoed back into its visible answer so it never reaches the transcript.
118
119
  const visibleContent = sanitizeInternalReminderBlocks(message.content ?? "");
119
- const hasVisibleAssistantContent = !!visibleContent.trim() ||
120
- (message.toolCalls?.length ?? 0) > 0 ||
121
- (message.parts?.length ?? 0) > 0 ||
122
- (!!visibleReasoning && (showThinking || verboseTrace));
120
+ // Decide visibility by what will ACTUALLY render below, not by raw array
121
+ // lengths. A turn whose only text part is an echoed <bubble_internal_*>
122
+ // reminder sanitizes to empty (TimelineText returns null), but the wrapper's
123
+ // marginTop/marginBottom would still emit a blank band — and long sessions
124
+ // inject more reminders, so consecutive empty turns stack into a large gap
125
+ // after the tool rows. Mirror the render: parts path → MessageParts; non-parts
126
+ // path → toolCalls/visibleContent; verbose adds a TurnDigest from toolCalls.
127
+ const hasParts = (message.parts?.length ?? 0) > 0;
128
+ const hasVisibleParts = hasParts &&
129
+ (message.parts ?? []).some((part) => part.type === "tools"
130
+ ? part.toolCalls.length > 0
131
+ : sanitizeInternalReminderBlocks(part.content).trim() !== "");
132
+ const toolCallCount = message.toolCalls?.length ?? 0;
133
+ const hasVisibleAssistantContent = (hasParts ? hasVisibleParts : (!!visibleContent.trim() || toolCallCount > 0)) ||
134
+ (!!visibleReasoning && (showThinking || verboseTrace)) ||
135
+ // A finalized turn carries taskElapsedMs and renders a TaskDurationLine even
136
+ // when its text/parts are empty — mirror that so the duration isn't dropped.
137
+ // (The verbose TurnDigest needs no term: whenever it renders, the same
138
+ // toolCalls already make the turn visible via the parts/non-parts branch.)
139
+ message.taskElapsedMs !== undefined;
123
140
  if (!hasVisibleAssistantContent)
124
141
  return null;
125
- 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, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, 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() && _jsx(MarkdownContent, { content: visibleContent })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
126
143
  });
127
- function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, }) {
144
+ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, verboseTrace, pendingApproval, nowTick, }) {
128
145
  const deferredContent = React.useDeferredValue(content);
129
146
  const deferredReasoning = React.useDeferredValue(reasoning);
130
147
  const deferredParts = React.useDeferredValue(parts);
@@ -138,16 +155,16 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, s
138
155
  // turn commits — no spacing jump at finalize time. (The old marginTop=0
139
156
  // was a flicker mitigation for the main-screen <Static> renderer; the
140
157
  // alt-screen viewport repaints frames atomically, so it's obsolete.)
141
- _jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
158
+ _jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
142
159
  }
143
- function MessageParts({ parts, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
160
+ function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
144
161
  const lastToolsPartIndex = findLastToolsPartIndex(parts);
145
162
  const lastTextPartIndex = findLastTextPartIndex(parts);
146
163
  return (_jsx(Box, { flexDirection: "column", children: parts.map((part, idx) => {
147
164
  if (part.type === "text") {
148
165
  return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns, streaming: streaming && idx === lastTextPartIndex }, `text-${idx}`));
149
166
  }
150
- return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
167
+ return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
151
168
  }) }));
152
169
  }
153
170
  function findLastTextPartIndex(parts) {
@@ -167,24 +184,27 @@ function TimelineText({ content, compactTop, terminalColumns, streaming = false,
167
184
  const visible = sanitizeInternalReminderBlocks(content);
168
185
  if (!visible.trim())
169
186
  return null;
170
- // marginLeft (2) + "● " marker (3 visual cells) = 5 cells consumed by the
171
- // timeline gutter; pass the remaining width so wide blocks like tables size
172
- // themselves against the actual content area instead of the raw terminal.
173
- const available = terminalColumns ? Math.max(20, terminalColumns - 5) : undefined;
187
+ // Timeline gutter = marginLeft (2) + "● " marker. The (U+25CF) is itself
188
+ // an ambiguous-width glyph, so the marker is 3 cells on a narrow terminal but
189
+ // 4 on an ambiguous-wide one Ink lays it out as 3 either way (it measures
190
+ // ●=1), so on a wide terminal the first line of every message gets shoved 1
191
+ // cell right and would overflow. Reserve that extra cell up front so the
192
+ // pre-wrap never packs a line the terminal then hard-wraps.
193
+ const gutter = ambiguousIsWide() ? 6 : 5;
194
+ const available = terminalColumns ? Math.max(20, terminalColumns - gutter) : undefined;
174
195
  const trimmed = visible.trim();
175
196
  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 })) })] }));
176
197
  }
177
- function ToolsPart({ toolCalls, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
198
+ function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
178
199
  if (toolCalls.length === 0)
179
200
  return null;
180
- const expandTools = verboseTrace || expandedToolOutput;
181
- if (!expandTools) {
201
+ if (!verboseTrace) {
182
202
  return (_jsx(TraceGroupList, { toolCalls: toolCalls, terminalColumns: terminalColumns, pendingApproval: pendingApproval, nowTick: nowTick, compactTop: compactTop, showActivity: showActivity }));
183
203
  }
184
204
  const lastIdx = toolCalls.length - 1;
185
205
  return (_jsx(Box, { flexDirection: "column", children: toolCalls.map((tc, idx) => {
186
206
  const isWaitingApproval = isToolPending(tc) && !!pendingApproval && approvalMatchesTool(pendingApproval, tc);
187
- return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: expandTools, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
207
+ return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: verboseTrace, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
188
208
  }) }));
189
209
  }
190
210
  function fallbackStreamingParts(content, tools) {
@@ -300,7 +320,11 @@ function CompactionSummaryBlock({ message }) {
300
320
  const theme = useTheme();
301
321
  const rawStatus = message.content.replace(/^✓\s*/, "").trim();
302
322
  const status = rawStatus.replace(/^Compaction complete\s*(?:·\s*)?/i, "").trim() || "Session compacted";
303
- const summary = message.compactionSummary?.trim();
323
+ // Same defense as every other visible-text path: strip any internal reminder
324
+ // markup before rendering, so a summary that echoed it never reaches the
325
+ // transcript. Belt-and-suspenders — the summarizer is fed sanitized history,
326
+ // but the summary is model-generated and also re-injected as context.
327
+ const summary = sanitizeInternalReminderBlocks(message.compactionSummary ?? "").trim() || undefined;
304
328
  return (_jsxs(Box, { marginTop: 1, marginBottom: 1, paddingX: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.success, bold: true, children: "\u2713 " }), _jsx(Text, { color: theme.accent, bold: true, children: "Compaction checkpoint" }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", status] })] }), summary && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, dimColor: true, children: "Preserved context summary" }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsx(MarkdownContent, { content: summary }) })] }))] }));
305
329
  }
306
330
  function UserMessageBlock({ content, terminalColumns, inputStatus, separateFromPrevious = false, }) {
@@ -427,59 +451,6 @@ function subagentsFrom(toolCall) {
427
451
  return [];
428
452
  return raw.filter((item) => typeof item === "object" && item !== null);
429
453
  }
430
- function latestSubagentNote(subagent) {
431
- const note = subagent.error
432
- || subagent.toolNotes?.filter(Boolean).at(-1)
433
- || subagent.summary
434
- || subagent.task
435
- || "";
436
- return note.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).find(Boolean) ?? "";
437
- }
438
- function subagentLabel(subagent) {
439
- return subagent.nickname ?? subagent.agentName ?? "subagent";
440
- }
441
- function subagentRole(subagent) {
442
- return [subagent.agentName, subagent.category ? `/${subagent.category}` : ""].join("") || "default";
443
- }
444
- function subagentDescriptor(subagent, includeThinking = false) {
445
- const route = formatSubagentRoute(subagent.route, { includeThinking });
446
- const role = subagentRole(subagent);
447
- return route ? `${role} @ ${route}` : role;
448
- }
449
- function subagentStatusColor(status, theme) {
450
- if (status === "completed")
451
- return theme.success;
452
- if (status === "failed" || status === "blocked" || status === "cancelled")
453
- return theme.error;
454
- if (status === "queued")
455
- return theme.muted;
456
- return theme.toolPending;
457
- }
458
- function subagentSummary(subagents) {
459
- if (subagents.length === 0)
460
- return "no subagents";
461
- const counts = new Map();
462
- for (const subagent of subagents) {
463
- const status = subagent.status ?? "running";
464
- counts.set(status, (counts.get(status) ?? 0) + 1);
465
- }
466
- const order = ["running", "queued", "completed", "blocked", "failed", "cancelled"];
467
- return order
468
- .filter((status) => counts.has(status))
469
- .map((status) => `${counts.get(status)} ${status}`)
470
- .join(" ");
471
- }
472
- function sortSubagents(subagents) {
473
- const rank = {
474
- running: 0,
475
- blocked: 1,
476
- failed: 2,
477
- queued: 3,
478
- cancelled: 4,
479
- completed: 5,
480
- };
481
- return [...subagents].sort((a, b) => (rank[a.status ?? "running"] ?? 9) - (rank[b.status ?? "running"] ?? 9));
482
- }
483
454
  const COLLAPSED_PREVIEW_LINES = 10;
484
455
  const EXPANDED_PREVIEW_LINES = 50;
485
456
  function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, showExpandHint = false, waitingApproval = false, compactTop = false, nowTick, }) {
@@ -581,7 +552,7 @@ function SubagentToolDisplay({ toolCall, verbose, terminalColumns, compactTop, }
581
552
  const descriptor = padVisual(truncateVisual(subagentDescriptor(subagent), descriptorWidth), descriptorWidth);
582
553
  const note = truncateVisual(latestSubagentNote(subagent), Math.max(12, detailWidth - 16 - descriptorWidth - 10));
583
554
  return (_jsxs(Box, { children: [_jsx(Text, { color: subagentStatusColor(status, theme), children: label }), _jsxs(Text, { color: theme.traceAction, children: [" ", descriptor] }), _jsxs(Text, { color: subagentStatusColor(status, theme), children: [" ", padVisual(status, 9)] }), note && _jsxs(Text, { color: subagent.error ? theme.error : theme.traceDetail, children: [" ", note] })] }, subagent.subAgentId ?? `${subagentLabel(subagent)}-${index}`));
584
- }), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more, Ctrl+O to view"] }))] })), subagents.length === 0 && toolCall.result && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: hasError ? theme.error : theme.muted, children: summarizeToolResult(toolCall) }) }))] }));
555
+ }), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more \u00B7 Ctrl+O to expand \u00B7 \u2193 then Enter to inspect traces"] }))] })), subagents.length === 0 && toolCall.result && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: hasError ? theme.error : theme.muted, children: summarizeToolResult(toolCall) }) }))] }));
585
556
  }
586
557
  function TruncationHint({ remaining, verbose, showExpandHint, }) {
587
558
  const theme = useTheme();
@@ -766,7 +737,7 @@ function truncateVisual(str, maxWidth) {
766
737
  let out = "";
767
738
  let width = 0;
768
739
  for (const char of str) {
769
- const w = charVisualWidth(char);
740
+ const w = graphemeWidth(char);
770
741
  if (width + w > maxWidth)
771
742
  break;
772
743
  out += char;
@@ -774,38 +745,10 @@ function truncateVisual(str, maxWidth) {
774
745
  }
775
746
  return out;
776
747
  }
777
- function visualWidth(str) {
778
- let width = 0;
779
- for (const char of str) {
780
- const code = char.codePointAt(0) || 0;
781
- if ((code >= 0x4e00 && code <= 0x9fff) ||
782
- (code >= 0x3000 && code <= 0x303f) ||
783
- (code >= 0xff00 && code <= 0xffef) ||
784
- (code >= 0x3040 && code <= 0x309f) ||
785
- (code >= 0x30a0 && code <= 0x30ff)) {
786
- width += 2;
787
- }
788
- else {
789
- width += 1;
790
- }
791
- }
792
- return width;
793
- }
794
748
  function padVisual(str, width) {
795
749
  const currentWidth = visualWidth(str);
796
750
  return str + " ".repeat(Math.max(0, width - currentWidth));
797
751
  }
798
- function charVisualWidth(char) {
799
- const code = char.codePointAt(0) || 0;
800
- if ((code >= 0x4e00 && code <= 0x9fff) ||
801
- (code >= 0x3000 && code <= 0x303f) ||
802
- (code >= 0xff00 && code <= 0xffef) ||
803
- (code >= 0x3040 && code <= 0x309f) ||
804
- (code >= 0x30a0 && code <= 0x30ff)) {
805
- return 2;
806
- }
807
- return 1;
808
- }
809
752
  function wrapByVisualWidth(line, maxWidth) {
810
753
  if (maxWidth <= 0)
811
754
  return [line];
@@ -815,7 +758,7 @@ function wrapByVisualWidth(line, maxWidth) {
815
758
  let current = "";
816
759
  let currentWidth = 0;
817
760
  for (const char of line) {
818
- const w = charVisualWidth(char);
761
+ const w = graphemeWidth(char);
819
762
  if (currentWidth + w > maxWidth) {
820
763
  result.push(current);
821
764
  current = char;
@@ -28,6 +28,11 @@ export async function runTui(agent, args, options = {}) {
28
28
  // forget — CodeBlock's lazy init falls back to raw lines if this isn't ready
29
29
  // yet, so callers don't need to await it.
30
30
  warmHighlighter();
31
+ // NOTE: the CSI 6n ambiguous-width probe is intentionally NOT run here. Doing
32
+ // raw-mode stdin I/O before Ink mounts left stdin in a state Bun's TTY compat
33
+ // didn't hand cleanly back to Ink, swallowing all composer keystrokes. The
34
+ // verdict now comes from `BUBBLE_AMBIGUOUS_WIDTH` / locale only (see width.ts);
35
+ // a safe Ink-mounted probe can be reintroduced later behind a flag.
31
36
  let exitSummary;
32
37
  const onFatalError = (err) => {
33
38
  restoreTerminal();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Full-screen subagent inspector opened from the subagent entry line.
3
+ *
4
+ * Two-level drill-in modeled on Claude Code's workflow view: a grouped list of
5
+ * subagents (each spawn_agent is one member; each agent_team/agent_batch is a
6
+ * group of members) → a per-member working-trace detail (its task, every tool
7
+ * step it ran, and its final summary/error). Data is live: app.tsx derives the
8
+ * groups from the message state each render, so the inspector reflects running
9
+ * members as their events stream in.
10
+ */
11
+ import { type SubagentGroup } from "./subagent-view.js";
12
+ export type { SubagentGroup };
13
+ export interface SubagentInspectorProps {
14
+ groups: SubagentGroup[];
15
+ onCancel: () => void;
16
+ }
17
+ export declare function SubagentInspector({ groups, onCancel }: SubagentInspectorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,189 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Full-screen subagent inspector opened from the subagent entry line.
4
+ *
5
+ * Two-level drill-in modeled on Claude Code's workflow view: a grouped list of
6
+ * subagents (each spawn_agent is one member; each agent_team/agent_batch is a
7
+ * group of members) → a per-member working-trace detail (its task, every tool
8
+ * step it ran, and its final summary/error). Data is live: app.tsx derives the
9
+ * groups from the message state each render, so the inspector reflects running
10
+ * members as their events stream in.
11
+ */
12
+ import { useMemo, useState } from "react";
13
+ import { Box, Text, useInput, useStdout } from "ink";
14
+ import { isKeyReleaseEvent } from "./key-events.js";
15
+ import { useTheme } from "./theme.js";
16
+ import { padVisual, truncateVisual } from "../text-display.js";
17
+ import { latestSubagentNote, subagentDescriptor, subagentLabel, subagentStatusColor, subagentSummary, } from "./subagent-view.js";
18
+ const STATUS_FILTERS = [null, "running", "queued", "completed", "failed"];
19
+ export function SubagentInspector({ groups, onCancel }) {
20
+ const theme = useTheme();
21
+ const { stdout } = useStdout();
22
+ const termHeight = stdout?.rows || 24;
23
+ const termWidth = stdout?.columns || 80;
24
+ const maxVisible = Math.max(6, termHeight - 12);
25
+ const [view, setView] = useState("list");
26
+ const [selectedIdx, setSelectedIdx] = useState(0);
27
+ const [detailScroll, setDetailScroll] = useState(0);
28
+ const [filterIdx, setFilterIdx] = useState(0);
29
+ const statusFilter = STATUS_FILTERS[filterIdx];
30
+ const allMembers = useMemo(() => groups.flatMap((g) => g.members), [groups]);
31
+ // Flat row list: a header per multi-member group, then its (filtered) members.
32
+ const rows = useMemo(() => {
33
+ const out = [];
34
+ for (const group of groups) {
35
+ const members = statusFilter
36
+ ? group.members.filter((m) => (m.status ?? "running") === statusFilter)
37
+ : group.members;
38
+ if (members.length === 0)
39
+ continue;
40
+ if (group.kind !== "single")
41
+ out.push({ type: "header", group });
42
+ members.forEach((member, i) => {
43
+ out.push({ type: "member", group, member, key: member.subAgentId ?? `${group.id}:${i}` });
44
+ });
45
+ }
46
+ return out;
47
+ }, [groups, statusFilter]);
48
+ const memberRowIndices = useMemo(() => rows.map((row, i) => (row.type === "member" ? i : -1)).filter((i) => i >= 0), [rows]);
49
+ const clampedIdx = memberRowIndices.length === 0 ? 0 : Math.min(selectedIdx, memberRowIndices.length - 1);
50
+ const selectedRowIndex = memberRowIndices[clampedIdx] ?? -1;
51
+ const selectedRow = rows[selectedRowIndex];
52
+ const selectedMember = selectedRow?.type === "member" ? selectedRow.member : undefined;
53
+ useInput((input, key) => {
54
+ if (isKeyReleaseEvent(key))
55
+ return;
56
+ if (view === "detail") {
57
+ if (key.escape || key.leftArrow) {
58
+ setView("list");
59
+ return;
60
+ }
61
+ if (key.upArrow || input === "k") {
62
+ setDetailScroll((s) => Math.max(0, s - 1));
63
+ return;
64
+ }
65
+ if (key.downArrow || input === "j") {
66
+ setDetailScroll((s) => s + 1);
67
+ return;
68
+ }
69
+ return;
70
+ }
71
+ // list view
72
+ if (key.escape) {
73
+ onCancel();
74
+ return;
75
+ }
76
+ if (input === "f") {
77
+ setFilterIdx((i) => (i + 1) % STATUS_FILTERS.length);
78
+ setSelectedIdx(0);
79
+ return;
80
+ }
81
+ if (key.upArrow) {
82
+ setSelectedIdx((i) => Math.max(0, i - 1));
83
+ return;
84
+ }
85
+ if (key.downArrow) {
86
+ setSelectedIdx((i) => Math.min(Math.max(0, memberRowIndices.length - 1), i + 1));
87
+ return;
88
+ }
89
+ if ((key.return || key.rightArrow) && selectedMember) {
90
+ setDetailScroll(0);
91
+ setView("detail");
92
+ return;
93
+ }
94
+ });
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." })] }));
97
+ }
98
+ if (view === "detail" && selectedMember) {
99
+ return (_jsx(SubagentDetail, { member: selectedMember, group: selectedRow?.type === "member" ? selectedRow.group : undefined, scroll: detailScroll, maxVisible: maxVisible, termWidth: termWidth }));
100
+ }
101
+ // ---- list view ----
102
+ const start = clampWindowStart(rows, selectedRowIndex, maxVisible);
103
+ const visible = rows.slice(start, start + maxVisible);
104
+ const labelWidth = 12;
105
+ 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) => {
107
+ const actualIndex = start + i;
108
+ if (row.type === "header") {
109
+ 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
+ }
111
+ const member = row.member;
112
+ const status = member.status ?? "running";
113
+ const isSelected = actualIndex === selectedRowIndex;
114
+ const note = truncateVisual(latestSubagentNote(member), Math.max(12, termWidth - labelWidth - descriptorWidth - 18));
115
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? theme.accent : undefined, children: isSelected ? "> " : " " }), _jsx(Text, { color: subagentStatusColor(status, theme), children: padVisual(truncateVisual(subagentLabel(member), labelWidth), labelWidth) }), _jsxs(Text, { color: theme.traceAction, children: [" ", padVisual(truncateVisual(subagentDescriptor(member), descriptorWidth), descriptorWidth)] }), _jsxs(Text, { color: subagentStatusColor(status, theme), children: [" ", padVisual(status, 9)] }), note && _jsxs(Text, { color: member.error ? theme.error : theme.traceDetail, children: [" ", note] })] }, row.key));
116
+ })] })] }));
117
+ }
118
+ function SubagentDetail({ member, group, scroll, maxVisible, termWidth, }) {
119
+ const theme = useTheme();
120
+ const status = member.status ?? "running";
121
+ const wrapWidth = Math.max(20, termWidth - 6);
122
+ // Build the scrollable body: task → working trace (every tool step) → summary/error.
123
+ const body = [];
124
+ if (member.task) {
125
+ body.push({ text: "Task", color: theme.muted });
126
+ for (const line of wrapText(member.task, wrapWidth))
127
+ body.push({ text: ` ${line}` });
128
+ body.push({ text: "" });
129
+ }
130
+ body.push({ text: `Working trace (${member.toolNotes?.length ?? 0} steps)`, color: theme.muted });
131
+ const notes = member.toolNotes?.filter(Boolean) ?? [];
132
+ if (notes.length === 0) {
133
+ body.push({ text: " (no tool steps recorded yet)", dim: true });
134
+ }
135
+ else {
136
+ notes.forEach((noteRaw, i) => {
137
+ const note = noteRaw.replace(/\r\n/g, "\n").split("\n").map((l) => l.trim()).filter(Boolean).join(" ");
138
+ const wrapped = wrapText(`${String(i + 1).padStart(2, " ")}. ${note}`, wrapWidth);
139
+ wrapped.forEach((line, j) => body.push({ text: ` ${j === 0 ? line : ` ${line}`}`, color: theme.traceDetail }));
140
+ });
141
+ }
142
+ if (member.error) {
143
+ body.push({ text: "" });
144
+ body.push({ text: "Error", color: theme.error });
145
+ for (const line of wrapText(member.error, wrapWidth))
146
+ body.push({ text: ` ${line}`, color: theme.error });
147
+ }
148
+ else if (member.summary) {
149
+ body.push({ text: "" });
150
+ body.push({ text: "Summary", color: theme.muted });
151
+ for (const line of wrapText(member.summary, wrapWidth))
152
+ body.push({ text: ` ${line}` });
153
+ }
154
+ const maxScroll = Math.max(0, body.length - maxVisible);
155
+ const clampedScroll = Math.min(scroll, maxScroll);
156
+ const visible = body.slice(clampedScroll, clampedScroll + maxVisible);
157
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.accent, children: subagentLabel(member) }), _jsxs(Text, { color: theme.traceAction, children: [" ", subagentDescriptor(member, true)] }), _jsxs(Text, { color: subagentStatusColor(status, theme), children: [" ", status] }), group && group.kind !== "single" && _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", group.kind, " \u201C", truncateVisual(group.label, 28), "\u201D"] })] }), _jsxs(Text, { color: theme.muted, children: ["\u2191/\u2193 or j/k scroll \u00B7 \u2190/Esc back", maxScroll > 0 ? ` · ${clampedScroll + 1}-${Math.min(clampedScroll + maxVisible, body.length)}/${body.length}` : ""] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((line, i) => (_jsx(Text, { color: line.color, dimColor: line.dim, children: line.text || " " }, i))) })] }));
158
+ }
159
+ function wrapText(text, width) {
160
+ const out = [];
161
+ for (const rawLine of text.replace(/\r\n/g, "\n").split("\n")) {
162
+ let line = rawLine;
163
+ if (line.length === 0) {
164
+ out.push("");
165
+ continue;
166
+ }
167
+ while (line.length > width) {
168
+ // Prefer breaking at the last space within the width window.
169
+ let cut = line.lastIndexOf(" ", width);
170
+ if (cut <= 0)
171
+ cut = width;
172
+ out.push(line.slice(0, cut).trimEnd());
173
+ line = line.slice(cut).trimStart();
174
+ }
175
+ out.push(line);
176
+ }
177
+ return out;
178
+ }
179
+ function clampWindowStart(rows, selectedRowIndex, maxVisible) {
180
+ if (rows.length <= maxVisible)
181
+ return 0;
182
+ if (selectedRowIndex < 0)
183
+ return 0;
184
+ const half = Math.floor(maxVisible / 2);
185
+ let start = Math.max(0, selectedRowIndex - half);
186
+ if (start + maxVisible > rows.length)
187
+ start = rows.length - maxVisible;
188
+ return Math.max(0, start);
189
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared subagent view-model + presentation helpers, used by both the inline
3
+ * Subagents block (message-list.tsx) and the full-screen inspector
4
+ * (subagent-inspector.tsx). Pure functions only — no React.
5
+ */
6
+ import { type SubagentRouteLike } from "../agent/subagent-route-format.js";
7
+ import type { Theme } from "./theme.js";
8
+ import type { DisplayMessage, DisplayToolCall } from "./display-history.js";
9
+ export interface SubagentDisplay {
10
+ subAgentId?: string;
11
+ agentName?: string;
12
+ nickname?: string;
13
+ status?: string;
14
+ category?: string;
15
+ route?: SubagentRouteLike;
16
+ profileSource?: string;
17
+ task?: string;
18
+ summary?: string;
19
+ toolNotes?: string[];
20
+ error?: string;
21
+ }
22
+ export declare function latestSubagentNote(subagent: SubagentDisplay): string;
23
+ export declare function subagentLabel(subagent: SubagentDisplay): string;
24
+ export declare function subagentRole(subagent: SubagentDisplay): string;
25
+ export declare function subagentDescriptor(subagent: SubagentDisplay, includeThinking?: boolean): string;
26
+ export declare function subagentStatusColor(status: string | undefined, theme: Theme): string;
27
+ export declare function subagentSummary(subagents: SubagentDisplay[]): string;
28
+ export declare function sortSubagents(subagents: SubagentDisplay[]): SubagentDisplay[];
29
+ /** A spawn_agent (one member), an agent_team/agent_batch, or a run_workflow (a group of members). */
30
+ export interface SubagentGroup {
31
+ id: string;
32
+ kind: "single" | "team" | "batch" | "workflow";
33
+ label: string;
34
+ members: SubagentDisplay[];
35
+ }
36
+ /**
37
+ * Collects every spawned subagent from the live transcript + streaming tools,
38
+ * grouped by their originating tool call, for the inspector. Pure.
39
+ *
40
+ * The same subagent is echoed by MULTIPLE lifecycle tool calls — its spawn_agent
41
+ * (a stale snapshot) plus every wait_agent/list_agents that observed it (later
42
+ * snapshots), all carrying metadata.subagents (agent-lifecycle formatLifecycleResult).
43
+ * So we dedupe by subAgentId, keep the freshest snapshot, group team/batch members
44
+ * by their originating tool call, and collapse a single agent's many lifecycle
45
+ * echoes into one "single" group keyed by the agent itself.
46
+ */
47
+ export declare function collectSubagentGroups(messages: DisplayMessage[], streamingTools: DisplayToolCall[]): SubagentGroup[];