@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.
- package/README.md +23 -3
- package/dist/agent/categories.d.ts +2 -0
- package/dist/agent/categories.js +4 -0
- package/dist/agent/child-runner.d.ts +5 -1
- package/dist/agent/child-runner.js +35 -2
- package/dist/agent/profiles.js +3 -0
- package/dist/agent/structured-output.d.ts +37 -0
- package/dist/agent/structured-output.js +193 -0
- package/dist/agent/subagent-control.d.ts +3 -0
- package/dist/agent/subagent-scheduler.d.ts +10 -0
- package/dist/agent/subagent-scheduler.js +31 -0
- package/dist/agent/workflow/control.d.ts +37 -0
- package/dist/agent/workflow/control.js +20 -0
- package/dist/agent/workflow/errors.d.ts +16 -0
- package/dist/agent/workflow/errors.js +24 -0
- package/dist/agent/workflow/runtime.d.ts +75 -0
- package/dist/agent/workflow/runtime.js +237 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +425 -17
- package/dist/context/compact-llm.d.ts +10 -1
- package/dist/context/compact-llm.js +13 -5
- package/dist/context/compact.d.ts +30 -0
- package/dist/context/compact.js +34 -17
- package/dist/goal/format.d.ts +1 -1
- package/dist/goal/format.js +1 -1
- package/dist/network/provider-transport.d.ts +9 -0
- package/dist/network/provider-transport.js +19 -1
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +24 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +33 -1
- package/dist/slash-commands/commands.js +41 -113
- package/dist/slash-commands/types.d.ts +14 -9
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +285 -0
- package/dist/tools/child-tools.d.ts +10 -0
- package/dist/tools/child-tools.js +12 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +9 -0
- package/dist/tui/image-display.d.ts +6 -0
- package/dist/tui/image-display.js +26 -1
- package/dist/tui-ink/app.d.ts +0 -18
- package/dist/tui-ink/app.js +168 -230
- package/dist/tui-ink/compaction-progress.d.ts +19 -0
- package/dist/tui-ink/compaction-progress.js +74 -0
- package/dist/tui-ink/input-box.d.ts +10 -1
- package/dist/tui-ink/input-box.js +56 -16
- package/dist/tui-ink/markdown.d.ts +18 -0
- package/dist/tui-ink/markdown.js +172 -16
- package/dist/tui-ink/message-list.d.ts +1 -2
- package/dist/tui-ink/message-list.js +50 -107
- package/dist/tui-ink/run.js +5 -0
- package/dist/tui-ink/subagent-inspector.d.ts +17 -0
- package/dist/tui-ink/subagent-inspector.js +189 -0
- package/dist/tui-ink/subagent-view.d.ts +47 -0
- package/dist/tui-ink/subagent-view.js +163 -0
- package/dist/tui-ink/terminal-env.d.ts +15 -0
- package/dist/tui-ink/terminal-env.js +22 -0
- package/dist/tui-ink/use-terminal-size.js +33 -6
- package/dist/tui-ink/width.d.ts +18 -0
- package/dist/tui-ink/width.js +130 -0
- package/dist/types.d.ts +35 -0
- 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 {
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
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,
|
|
198
|
+
function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
|
|
178
199
|
if (toolCalls.length === 0)
|
|
179
200
|
return null;
|
|
180
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
761
|
+
const w = graphemeWidth(char);
|
|
819
762
|
if (currentWidth + w > maxWidth) {
|
|
820
763
|
result.push(current);
|
|
821
764
|
current = char;
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -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[];
|