@bubblebrain-ai/bubble 0.0.27 → 0.0.29
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 +21 -0
- 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/network/provider-transport.d.ts +9 -0
- package/dist/network/provider-transport.js +19 -1
- package/dist/provider-anthropic.js +13 -0
- 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 +47 -1
- package/dist/slash-commands/types.d.ts +16 -1
- 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.js +84 -6
- 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 +7 -1
- package/dist/tui-ink/input-box.js +48 -15
- package/dist/tui-ink/markdown.d.ts +18 -0
- package/dist/tui-ink/markdown.js +172 -16
- package/dist/tui-ink/message-list.js +38 -94
- 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,10 +4,11 @@ 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;
|
|
@@ -116,10 +117,26 @@ 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
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, 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 }))] }));
|
|
@@ -167,10 +184,14 @@ 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
|
}
|
|
@@ -300,7 +321,11 @@ function CompactionSummaryBlock({ message }) {
|
|
|
300
321
|
const theme = useTheme();
|
|
301
322
|
const rawStatus = message.content.replace(/^✓\s*/, "").trim();
|
|
302
323
|
const status = rawStatus.replace(/^Compaction complete\s*(?:·\s*)?/i, "").trim() || "Session compacted";
|
|
303
|
-
|
|
324
|
+
// Same defense as every other visible-text path: strip any internal reminder
|
|
325
|
+
// markup before rendering, so a summary that echoed it never reaches the
|
|
326
|
+
// transcript. Belt-and-suspenders — the summarizer is fed sanitized history,
|
|
327
|
+
// but the summary is model-generated and also re-injected as context.
|
|
328
|
+
const summary = sanitizeInternalReminderBlocks(message.compactionSummary ?? "").trim() || undefined;
|
|
304
329
|
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
330
|
}
|
|
306
331
|
function UserMessageBlock({ content, terminalColumns, inputStatus, separateFromPrevious = false, }) {
|
|
@@ -427,59 +452,6 @@ function subagentsFrom(toolCall) {
|
|
|
427
452
|
return [];
|
|
428
453
|
return raw.filter((item) => typeof item === "object" && item !== null);
|
|
429
454
|
}
|
|
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
455
|
const COLLAPSED_PREVIEW_LINES = 10;
|
|
484
456
|
const EXPANDED_PREVIEW_LINES = 50;
|
|
485
457
|
function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, showExpandHint = false, waitingApproval = false, compactTop = false, nowTick, }) {
|
|
@@ -581,7 +553,7 @@ function SubagentToolDisplay({ toolCall, verbose, terminalColumns, compactTop, }
|
|
|
581
553
|
const descriptor = padVisual(truncateVisual(subagentDescriptor(subagent), descriptorWidth), descriptorWidth);
|
|
582
554
|
const note = truncateVisual(latestSubagentNote(subagent), Math.max(12, detailWidth - 16 - descriptorWidth - 10));
|
|
583
555
|
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
|
|
556
|
+
}), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more \u00B7 Ctrl+O to expand \u00B7 Ctrl+G 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
557
|
}
|
|
586
558
|
function TruncationHint({ remaining, verbose, showExpandHint, }) {
|
|
587
559
|
const theme = useTheme();
|
|
@@ -766,7 +738,7 @@ function truncateVisual(str, maxWidth) {
|
|
|
766
738
|
let out = "";
|
|
767
739
|
let width = 0;
|
|
768
740
|
for (const char of str) {
|
|
769
|
-
const w =
|
|
741
|
+
const w = graphemeWidth(char);
|
|
770
742
|
if (width + w > maxWidth)
|
|
771
743
|
break;
|
|
772
744
|
out += char;
|
|
@@ -774,38 +746,10 @@ function truncateVisual(str, maxWidth) {
|
|
|
774
746
|
}
|
|
775
747
|
return out;
|
|
776
748
|
}
|
|
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
749
|
function padVisual(str, width) {
|
|
795
750
|
const currentWidth = visualWidth(str);
|
|
796
751
|
return str + " ".repeat(Math.max(0, width - currentWidth));
|
|
797
752
|
}
|
|
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
753
|
function wrapByVisualWidth(line, maxWidth) {
|
|
810
754
|
if (maxWidth <= 0)
|
|
811
755
|
return [line];
|
|
@@ -815,7 +759,7 @@ function wrapByVisualWidth(line, maxWidth) {
|
|
|
815
759
|
let current = "";
|
|
816
760
|
let currentWidth = 0;
|
|
817
761
|
for (const char of line) {
|
|
818
|
-
const w =
|
|
762
|
+
const w = graphemeWidth(char);
|
|
819
763
|
if (currentWidth + w > maxWidth) {
|
|
820
764
|
result.push(current);
|
|
821
765
|
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 (Ctrl+G / /agents).
|
|
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 (Ctrl+G / /agents).
|
|
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[];
|
|
@@ -0,0 +1,163 @@
|
|
|
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 { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
7
|
+
export function latestSubagentNote(subagent) {
|
|
8
|
+
const note = subagent.error
|
|
9
|
+
|| subagent.toolNotes?.filter(Boolean).at(-1)
|
|
10
|
+
|| subagent.summary
|
|
11
|
+
|| subagent.task
|
|
12
|
+
|| "";
|
|
13
|
+
return note.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).find(Boolean) ?? "";
|
|
14
|
+
}
|
|
15
|
+
export function subagentLabel(subagent) {
|
|
16
|
+
return subagent.nickname ?? subagent.agentName ?? "subagent";
|
|
17
|
+
}
|
|
18
|
+
export function subagentRole(subagent) {
|
|
19
|
+
return [subagent.agentName, subagent.category ? `/${subagent.category}` : ""].join("") || "default";
|
|
20
|
+
}
|
|
21
|
+
export function subagentDescriptor(subagent, includeThinking = false) {
|
|
22
|
+
const route = formatSubagentRoute(subagent.route, { includeThinking });
|
|
23
|
+
const role = subagentRole(subagent);
|
|
24
|
+
return route ? `${role} @ ${route}` : role;
|
|
25
|
+
}
|
|
26
|
+
export function subagentStatusColor(status, theme) {
|
|
27
|
+
if (status === "completed")
|
|
28
|
+
return theme.success;
|
|
29
|
+
if (status === "failed" || status === "blocked" || status === "cancelled")
|
|
30
|
+
return theme.error;
|
|
31
|
+
if (status === "queued")
|
|
32
|
+
return theme.muted;
|
|
33
|
+
return theme.toolPending;
|
|
34
|
+
}
|
|
35
|
+
export function subagentSummary(subagents) {
|
|
36
|
+
if (subagents.length === 0)
|
|
37
|
+
return "no subagents";
|
|
38
|
+
const counts = new Map();
|
|
39
|
+
for (const subagent of subagents) {
|
|
40
|
+
const status = subagent.status ?? "running";
|
|
41
|
+
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
42
|
+
}
|
|
43
|
+
const order = ["running", "queued", "completed", "blocked", "failed", "cancelled"];
|
|
44
|
+
return order
|
|
45
|
+
.filter((status) => counts.has(status))
|
|
46
|
+
.map((status) => `${counts.get(status)} ${status}`)
|
|
47
|
+
.join(" ");
|
|
48
|
+
}
|
|
49
|
+
export function sortSubagents(subagents) {
|
|
50
|
+
const rank = {
|
|
51
|
+
running: 0,
|
|
52
|
+
blocked: 1,
|
|
53
|
+
failed: 2,
|
|
54
|
+
queued: 3,
|
|
55
|
+
cancelled: 4,
|
|
56
|
+
completed: 5,
|
|
57
|
+
};
|
|
58
|
+
return [...subagents].sort((a, b) => (rank[a.status ?? "running"] ?? 9) - (rank[b.status ?? "running"] ?? 9));
|
|
59
|
+
}
|
|
60
|
+
function subagentStatusRank(status) {
|
|
61
|
+
if (status === "completed" || status === "failed" || status === "blocked" || status === "cancelled" || status === "closed")
|
|
62
|
+
return 3;
|
|
63
|
+
if (status === "running")
|
|
64
|
+
return 2;
|
|
65
|
+
if (status === "queued")
|
|
66
|
+
return 1;
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
/** Higher = more "complete"/recent snapshot of the same subagent. */
|
|
70
|
+
function subagentFreshness(member) {
|
|
71
|
+
return subagentStatusRank(member.status) * 100_000
|
|
72
|
+
+ (member.toolNotes?.length ?? 0) * 10
|
|
73
|
+
+ (member.summary ? 1 : 0);
|
|
74
|
+
}
|
|
75
|
+
function memberKey(member) {
|
|
76
|
+
return member.subAgentId || `${member.nickname ?? ""}|${member.task ?? ""}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Collects every spawned subagent from the live transcript + streaming tools,
|
|
80
|
+
* grouped by their originating tool call, for the inspector. Pure.
|
|
81
|
+
*
|
|
82
|
+
* The same subagent is echoed by MULTIPLE lifecycle tool calls — its spawn_agent
|
|
83
|
+
* (a stale snapshot) plus every wait_agent/list_agents that observed it (later
|
|
84
|
+
* snapshots), all carrying metadata.subagents (agent-lifecycle formatLifecycleResult).
|
|
85
|
+
* So we dedupe by subAgentId, keep the freshest snapshot, group team/batch members
|
|
86
|
+
* by their originating tool call, and collapse a single agent's many lifecycle
|
|
87
|
+
* echoes into one "single" group keyed by the agent itself.
|
|
88
|
+
*/
|
|
89
|
+
export function collectSubagentGroups(messages, streamingTools) {
|
|
90
|
+
const toolCalls = [];
|
|
91
|
+
const ingest = (tcs) => {
|
|
92
|
+
if (!tcs)
|
|
93
|
+
return;
|
|
94
|
+
for (const tc of tcs)
|
|
95
|
+
if (tc.metadata?.kind === "subagent")
|
|
96
|
+
toolCalls.push(tc);
|
|
97
|
+
};
|
|
98
|
+
for (const message of messages) {
|
|
99
|
+
ingest(message.toolCalls);
|
|
100
|
+
if (message.parts) {
|
|
101
|
+
for (const part of message.parts) {
|
|
102
|
+
if (part.type === "tools")
|
|
103
|
+
ingest(part.toolCalls);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
ingest(streamingTools);
|
|
108
|
+
const freshest = new Map();
|
|
109
|
+
const memberToGroup = new Map();
|
|
110
|
+
const groups = new Map();
|
|
111
|
+
let order = 0;
|
|
112
|
+
for (const tc of toolCalls) {
|
|
113
|
+
const rawMembers = Array.isArray(tc.metadata?.subagents) ? tc.metadata.subagents : [];
|
|
114
|
+
const members = rawMembers.filter((m) => typeof m === "object" && m !== null);
|
|
115
|
+
if (members.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
// Track the freshest snapshot seen for each subagent.
|
|
118
|
+
for (const m of members) {
|
|
119
|
+
const key = memberKey(m);
|
|
120
|
+
const prev = freshest.get(key);
|
|
121
|
+
if (!prev || subagentFreshness(m) >= subagentFreshness(prev))
|
|
122
|
+
freshest.set(key, m);
|
|
123
|
+
}
|
|
124
|
+
const mode = tc.metadata.mode;
|
|
125
|
+
if (mode === "team" || mode === "batch" || mode === "workflow") {
|
|
126
|
+
// A team/batch/workflow tool call is the canonical group for its members.
|
|
127
|
+
const groupKey = tc.id;
|
|
128
|
+
if (!groups.has(groupKey)) {
|
|
129
|
+
const description = typeof tc.args?.description === "string" ? tc.args.description.trim()
|
|
130
|
+
: typeof tc.args?.title === "string" ? tc.args.title.trim() : "";
|
|
131
|
+
groups.set(groupKey, { kind: mode, label: description || mode, memberKeys: [], order: order++ });
|
|
132
|
+
}
|
|
133
|
+
const group = groups.get(groupKey);
|
|
134
|
+
for (const m of members) {
|
|
135
|
+
const key = memberKey(m);
|
|
136
|
+
if (!memberToGroup.has(key)) {
|
|
137
|
+
memberToGroup.set(key, groupKey);
|
|
138
|
+
group.memberKeys.push(key);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Lifecycle echo (spawn/wait/list/...): one "single" group per agent,
|
|
144
|
+
// collapsing all its echoes; skip any already claimed by a team/batch.
|
|
145
|
+
for (const m of members) {
|
|
146
|
+
const key = memberKey(m);
|
|
147
|
+
if (memberToGroup.has(key))
|
|
148
|
+
continue;
|
|
149
|
+
const groupKey = `single:${key}`;
|
|
150
|
+
memberToGroup.set(key, groupKey);
|
|
151
|
+
groups.set(groupKey, { kind: "single", label: m.nickname ?? m.task ?? "subagent", memberKeys: [key], order: order++ });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [...groups.entries()]
|
|
156
|
+
.sort((a, b) => a[1].order - b[1].order)
|
|
157
|
+
.map(([id, g]) => ({
|
|
158
|
+
id,
|
|
159
|
+
kind: g.kind,
|
|
160
|
+
label: g.label,
|
|
161
|
+
members: g.memberKeys.map((k) => freshest.get(k)).filter((m) => !!m),
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether we're running inside a terminal multiplexer (tmux or GNU screen).
|
|
3
|
+
*
|
|
4
|
+
* Ink commits settled transcript rows to native scrollback via <Static> and
|
|
5
|
+
* repaints only the short live region in place. When that live region SHRINKS
|
|
6
|
+
* (a turn settles, a steer commits, a run is interrupted), Ink erases the prior
|
|
7
|
+
* frame with a cursor-up + clear. Under a multiplexer that erase cannot reach
|
|
8
|
+
* rows that have already scrolled out of the pane, leaving a blank gap — so
|
|
9
|
+
* those transitions fall back to a full screen+scrollback reprint to stay clean.
|
|
10
|
+
*
|
|
11
|
+
* On a normal terminal that reprint is unnecessary (Ink's in-place erase works)
|
|
12
|
+
* and visible as a one-frame full-screen flash, so we skip it. This predicate is
|
|
13
|
+
* the gate. Pure + injectable for tests.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isMultiplexedTerminal(env?: NodeJS.ProcessEnv): boolean;
|