@bubblebrain-ai/bubble 0.0.37 → 0.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/internal-reminder-sanitizer.js +2 -0
- package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
- package/dist/agent/task-lifecycle-reminder.js +59 -0
- package/dist/agent.d.ts +26 -0
- package/dist/agent.js +39 -0
- package/dist/approval/controller.d.ts +3 -1
- package/dist/approval/controller.js +5 -2
- package/dist/approval/tool-helper.js +10 -1
- package/dist/approval/types.d.ts +15 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -0
- package/dist/loop/engine.d.ts +29 -0
- package/dist/loop/engine.js +63 -0
- package/dist/main.js +29 -0
- package/dist/orchestrator/default-hooks.js +16 -0
- package/dist/session-types.d.ts +1 -1
- package/dist/tasks/manager.d.ts +136 -0
- package/dist/tasks/manager.js +530 -0
- package/dist/tasks/process-utils.d.ts +10 -0
- package/dist/tasks/process-utils.js +49 -0
- package/dist/tasks/promotion.d.ts +17 -0
- package/dist/tasks/promotion.js +30 -0
- package/dist/tasks/wake.d.ts +53 -0
- package/dist/tasks/wake.js +125 -0
- package/dist/tools/bash.d.ts +15 -1
- package/dist/tools/bash.js +103 -3
- package/dist/tools/child-tools.js +3 -1
- package/dist/tools/edit.js +4 -12
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +8 -1
- package/dist/tools/server-manager.d.ts +6 -36
- package/dist/tools/server-manager.js +6 -234
- package/dist/tools/task-tools.d.ts +10 -0
- package/dist/tools/task-tools.js +129 -0
- package/dist/tools/write.js +6 -12
- package/dist/tui-ink/app.d.ts +20 -2
- package/dist/tui-ink/app.js +307 -19
- package/dist/tui-ink/approval/approval-dialog.js +4 -4
- package/dist/tui-ink/display-history.d.ts +1 -1
- package/dist/tui-ink/footer.d.ts +3 -3
- package/dist/tui-ink/footer.js +3 -3
- package/dist/tui-ink/input-box.d.ts +6 -0
- package/dist/tui-ink/markdown.d.ts +10 -2
- package/dist/tui-ink/markdown.js +13 -12
- package/dist/tui-ink/message-list.js +14 -3
- package/dist/tui-ink/run.d.ts +6 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-inspector.d.ts +5 -1
- package/dist/tui-ink/subagent-inspector.js +23 -7
- package/dist/tui-ink/welcome.js +4 -0
- package/package.json +1 -1
package/dist/tui-ink/markdown.js
CHANGED
|
@@ -344,7 +344,7 @@ function InlineText({ text, maxWidth }) {
|
|
|
344
344
|
const wrapped = wrapInlineSegments(parseMarkdownInlineSegments(text), effectiveWidth);
|
|
345
345
|
return (_jsx(_Fragment, { children: wrapped.map((segments, li) => (_jsx(Text, { children: segments.length ? renderSegmentNodes(segments, `inline-${li}`, theme.inlineCode) : " " }, li))) }));
|
|
346
346
|
}
|
|
347
|
-
function CodeBlock({ lang, lines }) {
|
|
347
|
+
function CodeBlock({ lang, lines, flushTop = false }) {
|
|
348
348
|
const theme = useTheme();
|
|
349
349
|
// Lazy init: try sync highlight when shiki is already warm so the very first
|
|
350
350
|
// paint carries highlighted output. Fall back to raw lines if shiki hasn't
|
|
@@ -376,9 +376,9 @@ function CodeBlock({ lang, lines }) {
|
|
|
376
376
|
cancelled = true;
|
|
377
377
|
};
|
|
378
378
|
}, [lang, lines]);
|
|
379
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
379
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: flushTop ? 0 : 1, marginBottom: 1, children: [lang && _jsx(Text, { color: theme.muted, children: lang }), _jsx(Box, { flexDirection: "column", children: highlighted.map((line, i) => (_jsx(Text, { children: line || " " }, i))) })] }));
|
|
380
380
|
}
|
|
381
|
-
function TableBlock({ headers, rows, maxWidth, }) {
|
|
381
|
+
function TableBlock({ headers, rows, maxWidth, flushTop = false, }) {
|
|
382
382
|
const theme = useTheme();
|
|
383
383
|
const { columns: termWidth } = useTerminalSize();
|
|
384
384
|
const colCount = headers.length;
|
|
@@ -429,7 +429,7 @@ function TableBlock({ headers, rows, maxWidth, }) {
|
|
|
429
429
|
const height = Math.max(1, ...wrapped.map((lines) => lines.length));
|
|
430
430
|
return (_jsx(Box, { flexDirection: "column", children: Array.from({ length: height }, (_, line) => (_jsxs(Text, { children: [`${g.v} `, wrapped.map((cellLines, i) => (_jsxs(React.Fragment, { children: [renderCellLine(cellLines[line] ?? [], widths[i] ?? 4, `${keyPrefix}-cell-${i}-${line}`, theme.inlineCode), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, `${keyPrefix}-line-${line}`))) }, keyPrefix));
|
|
431
431
|
};
|
|
432
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
432
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: flushTop ? 0 : 1, marginBottom: 1, children: [_jsx(Text, { children: top }), renderRow(headers, "header", true), _jsx(Text, { children: mid }), rows.map((row, ri) => renderRow(row, `row-${ri}`)), _jsx(Text, { children: bot })] }));
|
|
433
433
|
}
|
|
434
434
|
/** Cap a pathological cell (huge blob of text) at this many wrapped lines. */
|
|
435
435
|
const MAX_TABLE_CELL_LINES = 8;
|
|
@@ -663,7 +663,7 @@ export function wrapInlineSegments(segments, maxWidth) {
|
|
|
663
663
|
function inlineSegmentsWidth(segments) {
|
|
664
664
|
return segments.reduce((sum, segment) => sum + visualWidth(segment.text), 0);
|
|
665
665
|
}
|
|
666
|
-
function HeadingBlock({ level, text }) {
|
|
666
|
+
function HeadingBlock({ level, text, flushTop = false }) {
|
|
667
667
|
const theme = useTheme();
|
|
668
668
|
const props = { bold: true };
|
|
669
669
|
if (level === 1) {
|
|
@@ -676,7 +676,7 @@ function HeadingBlock({ level, text }) {
|
|
|
676
676
|
else if (level === 3) {
|
|
677
677
|
props.color = theme.warning;
|
|
678
678
|
}
|
|
679
|
-
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { ...props, children: text }) }));
|
|
679
|
+
return (_jsx(Box, { marginTop: flushTop ? 0 : 1, marginBottom: 1, children: _jsx(Text, { ...props, children: text }) }));
|
|
680
680
|
}
|
|
681
681
|
/**
|
|
682
682
|
* Streaming-aware wrapper around `MarkdownContent`.
|
|
@@ -693,7 +693,7 @@ function HeadingBlock({ level, text }) {
|
|
|
693
693
|
* reset handles the rare case where `content` is replaced wholesale (e.g.,
|
|
694
694
|
* the user re-enters a turn).
|
|
695
695
|
*/
|
|
696
|
-
export function StreamingMarkdown({ content, maxWidth, }) {
|
|
696
|
+
export function StreamingMarkdown({ content, maxWidth, flushTop = false, }) {
|
|
697
697
|
const stablePrefixRef = React.useRef("");
|
|
698
698
|
if (!content.startsWith(stablePrefixRef.current)) {
|
|
699
699
|
stablePrefixRef.current = "";
|
|
@@ -706,7 +706,7 @@ export function StreamingMarkdown({ content, maxWidth, }) {
|
|
|
706
706
|
}
|
|
707
707
|
const stablePrefix = stablePrefixRef.current;
|
|
708
708
|
const unstableSuffix = content.substring(stablePrefix.length);
|
|
709
|
-
return (_jsxs(Box, { flexDirection: "column", children: [stablePrefix && _jsx(MarkdownContent, { content: stablePrefix, maxWidth: maxWidth }), unstableSuffix && _jsx(MarkdownContent, { content: unstableSuffix, maxWidth: maxWidth })] }));
|
|
709
|
+
return (_jsxs(Box, { flexDirection: "column", children: [stablePrefix && _jsx(MarkdownContent, { content: stablePrefix, maxWidth: maxWidth, flushTop: flushTop }), unstableSuffix && _jsx(MarkdownContent, { content: unstableSuffix, maxWidth: maxWidth, flushTop: flushTop && !stablePrefix })] }));
|
|
710
710
|
}
|
|
711
711
|
// A markdown list item: optional indent, a bullet (-, *, +) or ordered marker
|
|
712
712
|
// (1. / 2)), then whitespace, then the item text. The captured prefix is
|
|
@@ -720,17 +720,18 @@ export function splitListItem(line) {
|
|
|
720
720
|
const prefix = m[1] + m[2] + m[3];
|
|
721
721
|
return { prefix, content: m[4], indent: visualWidth(prefix) };
|
|
722
722
|
}
|
|
723
|
-
export function MarkdownContent({ content, maxWidth, }) {
|
|
723
|
+
export function MarkdownContent({ content, maxWidth, flushTop = false, }) {
|
|
724
724
|
const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
|
|
725
725
|
return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => {
|
|
726
|
+
const blockFlushTop = flushTop && i === 0;
|
|
726
727
|
if (block.type === "code") {
|
|
727
|
-
return _jsx(CodeBlock, { lang: block.lang, lines: block.lines }, i);
|
|
728
|
+
return _jsx(CodeBlock, { lang: block.lang, lines: block.lines, flushTop: blockFlushTop }, i);
|
|
728
729
|
}
|
|
729
730
|
if (block.type === "table") {
|
|
730
|
-
return (_jsx(TableBlock, { headers: block.headers, rows: block.rows, maxWidth: maxWidth }, i));
|
|
731
|
+
return (_jsx(TableBlock, { headers: block.headers, rows: block.rows, maxWidth: maxWidth, flushTop: blockFlushTop }, i));
|
|
731
732
|
}
|
|
732
733
|
if (block.type === "heading") {
|
|
733
|
-
return _jsx(HeadingBlock, { level: block.level, text: block.text }, i);
|
|
734
|
+
return _jsx(HeadingBlock, { level: block.level, text: block.text, flushTop: blockFlushTop }, i);
|
|
734
735
|
}
|
|
735
736
|
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: block.lines.map((line, li) => {
|
|
736
737
|
const item = splitListItem(line);
|
|
@@ -31,8 +31,14 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
31
31
|
const regularMessages = messages.filter((message) => !message.inputStatus);
|
|
32
32
|
const pendingSteerMessages = messages.filter((message) => message.inputStatus === "pending_steer");
|
|
33
33
|
const queuedInputMessages = messages.filter((message) => message.inputStatus === "queued");
|
|
34
|
+
// While the transcript has no settled rows, the banner stays OUT of
|
|
35
|
+
// <Static> and renders in the live region instead, so it repaints on every
|
|
36
|
+
// commit — a /model switch on the fresh launch screen updates the banner in
|
|
37
|
+
// place. It is committed to scrollback together with the first settled row
|
|
38
|
+
// (same render commit, so the hand-off is seamless) and frozen from then on.
|
|
39
|
+
const bannerCommitted = regularMessages.length > 0;
|
|
34
40
|
const staticItems = [];
|
|
35
|
-
if (welcomeBanner) {
|
|
41
|
+
if (welcomeBanner && bannerCommitted) {
|
|
36
42
|
staticItems.push({ kind: "welcome", key: "welcome" });
|
|
37
43
|
}
|
|
38
44
|
const lastMessageIndex = regularMessages.length - 1;
|
|
@@ -58,7 +64,7 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
58
64
|
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: welcomeBanner }, item.key));
|
|
59
65
|
}
|
|
60
66
|
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
|
|
61
|
-
} }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, backgroundColor: theme.background, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
|
|
67
|
+
} }, `transcript-${staticGeneration}`), welcomeBanner && !bannerCommitted && (_jsx(Box, { flexDirection: "column", paddingX: paddingX, backgroundColor: theme.background, children: welcomeBanner })), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, backgroundColor: theme.background, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
|
|
@@ -123,6 +129,11 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
|
|
|
123
129
|
if (message.syntheticKind === "ui_interrupt") {
|
|
124
130
|
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.error, children: "\u23F9 " }), _jsx(Text, { color: theme.muted, dimColor: true, children: message.content || "Interrupted by user" })] }));
|
|
125
131
|
}
|
|
132
|
+
// One-line status notices (model switch etc.): accent-colored so they read
|
|
133
|
+
// as UI feedback, not assistant prose.
|
|
134
|
+
if (message.syntheticKind === "ui_notice") {
|
|
135
|
+
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsxs(Text, { color: theme.accent, children: ["\u2726 ", message.content] }) }));
|
|
136
|
+
}
|
|
126
137
|
const visibleReasoning = sanitizeInternalReminderBlocks(message.reasoning ?? "").trim();
|
|
127
138
|
// Same defense as reasoning: strip any internal reminder markup the model
|
|
128
139
|
// echoed back into its visible answer so it never reaches the transcript.
|
|
@@ -209,7 +220,7 @@ function TimelineText({ content, compactTop, terminalColumns, streaming = false,
|
|
|
209
220
|
? Math.max(20, terminalColumns - gutter - TRANSCRIPT_ROW_PADDING)
|
|
210
221
|
: undefined;
|
|
211
222
|
const trimmed = visible.trim();
|
|
212
|
-
return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
|
|
223
|
+
return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available, flushTop: true })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available, flushTop: true })) })] }));
|
|
213
224
|
}
|
|
214
225
|
function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
|
|
215
226
|
if (toolCalls.length === 0)
|
package/dist/tui-ink/run.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface RunTuiOptions {
|
|
|
34
34
|
mcpManager?: McpManager;
|
|
35
35
|
/** Shared with the model-facing goal tools and the Ink auto-continuation loop. */
|
|
36
36
|
goalStore?: import("../goal/store.js").GoalStore;
|
|
37
|
+
/** Unified process manager; drives task notices, auto-resume, and the status row. */
|
|
38
|
+
processManager?: import("../tasks/manager.js").ProcessManager;
|
|
39
|
+
/** tasks.auto_resume from user config (design §2.3b). Default ON. */
|
|
40
|
+
tasksAutoResume?: boolean;
|
|
41
|
+
/** Ctrl+G send-to-background channel shared with the bash tool (design §2.5). */
|
|
42
|
+
promotionChannel?: import("../tasks/promotion.js").PromotionChannel;
|
|
37
43
|
themeMode?: ThemeMode;
|
|
38
44
|
themeOverrides?: Record<string, string>;
|
|
39
45
|
detectedTheme?: ResolvedTheme;
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import { render } from "ink";
|
|
|
3
3
|
import { App } from "./app.js";
|
|
4
4
|
import { warmHighlighter } from "./code-highlight.js";
|
|
5
5
|
export function createInkAppElement(agent, args, options, onExit) {
|
|
6
|
-
return (_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, switchSession: options.switchSession, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, goalStore: options.goalStore, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, updateNotice: options.updateNotice, updateNoticeRefresh: options.updateNoticeRefresh, hookController: options.hookController, externalRuntime: options.externalRuntime, onExit: onExit }));
|
|
6
|
+
return (_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, switchSession: options.switchSession, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, goalStore: options.goalStore, processManager: options.processManager, tasksAutoResume: options.tasksAutoResume, promotionChannel: options.promotionChannel, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, updateNotice: options.updateNotice, updateNoticeRefresh: options.updateNoticeRefresh, hookController: options.hookController, externalRuntime: options.externalRuntime, onExit: onExit }));
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Best-effort terminal restore for abnormal exits. Bubble renders into the
|
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* members as their events stream in.
|
|
10
10
|
*/
|
|
11
11
|
import { type SubagentGroup } from "./subagent-view.js";
|
|
12
|
+
import type { BackgroundTaskInfo } from "../tasks/manager.js";
|
|
12
13
|
export type { SubagentGroup };
|
|
13
14
|
export interface SubagentInspectorProps {
|
|
14
15
|
groups: SubagentGroup[];
|
|
15
16
|
onCancel: () => void;
|
|
17
|
+
/** Background tasks of the bound session (design §2.5). */
|
|
18
|
+
tasks?: BackgroundTaskInfo[];
|
|
19
|
+
onKillTask?: (taskId: string) => void;
|
|
16
20
|
}
|
|
17
|
-
export declare function SubagentInspector({ groups, onCancel }: SubagentInspectorProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function SubagentInspector({ groups, onCancel, tasks, onKillTask }: SubagentInspectorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -16,7 +16,7 @@ import { useTheme } from "./theme.js";
|
|
|
16
16
|
import { padVisual, truncateVisual } from "../text-display.js";
|
|
17
17
|
import { latestSubagentNote, subagentDescriptor, subagentLabel, subagentStatusColor, subagentSummary, } from "./subagent-view.js";
|
|
18
18
|
const STATUS_FILTERS = [null, "running", "queued", "completed", "failed"];
|
|
19
|
-
export function SubagentInspector({ groups, onCancel }) {
|
|
19
|
+
export function SubagentInspector({ groups, onCancel, tasks = [], onKillTask }) {
|
|
20
20
|
const theme = useTheme();
|
|
21
21
|
const { stdout } = useStdout();
|
|
22
22
|
const termHeight = stdout?.rows || 24;
|
|
@@ -28,9 +28,13 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
28
28
|
const [filterIdx, setFilterIdx] = useState(0);
|
|
29
29
|
const statusFilter = STATUS_FILTERS[filterIdx];
|
|
30
30
|
const allMembers = useMemo(() => groups.flatMap((g) => g.members), [groups]);
|
|
31
|
-
// Flat row list:
|
|
31
|
+
// Flat row list: background tasks first (design §2.5), then a header per
|
|
32
|
+
// multi-member group and its (filtered) members.
|
|
32
33
|
const rows = useMemo(() => {
|
|
33
34
|
const out = [];
|
|
35
|
+
for (const task of tasks) {
|
|
36
|
+
out.push({ type: "task", task, key: task.id });
|
|
37
|
+
}
|
|
34
38
|
for (const group of groups) {
|
|
35
39
|
const members = statusFilter
|
|
36
40
|
? group.members.filter((m) => (m.status ?? "running") === statusFilter)
|
|
@@ -44,12 +48,13 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
return out;
|
|
47
|
-
}, [groups, statusFilter]);
|
|
48
|
-
const memberRowIndices = useMemo(() => rows.map((row, i) => (row.type === "member" ? i : -1)).filter((i) => i >= 0), [rows]);
|
|
51
|
+
}, [groups, statusFilter, tasks]);
|
|
52
|
+
const memberRowIndices = useMemo(() => rows.map((row, i) => (row.type === "member" || row.type === "task" ? i : -1)).filter((i) => i >= 0), [rows]);
|
|
49
53
|
const clampedIdx = memberRowIndices.length === 0 ? 0 : Math.min(selectedIdx, memberRowIndices.length - 1);
|
|
50
54
|
const selectedRowIndex = memberRowIndices[clampedIdx] ?? -1;
|
|
51
55
|
const selectedRow = rows[selectedRowIndex];
|
|
52
56
|
const selectedMember = selectedRow?.type === "member" ? selectedRow.member : undefined;
|
|
57
|
+
const selectedTask = selectedRow?.type === "task" ? selectedRow.task : undefined;
|
|
53
58
|
useInput((input, key) => {
|
|
54
59
|
if (isKeyReleaseEvent(key))
|
|
55
60
|
return;
|
|
@@ -91,9 +96,13 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
91
96
|
setView("detail");
|
|
92
97
|
return;
|
|
93
98
|
}
|
|
99
|
+
if (input === "x" && selectedTask && selectedTask.status === "running") {
|
|
100
|
+
onKillTask?.(selectedTask.id);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
94
103
|
});
|
|
95
|
-
if (groups.length === 0) {
|
|
96
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Subagents" }), _jsx(Text, { color: theme.muted, children: "No subagents
|
|
104
|
+
if (groups.length === 0 && tasks.length === 0) {
|
|
105
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Subagents & tasks" }), _jsx(Text, { color: theme.muted, children: "No subagents or background tasks yet. Esc to close." })] }));
|
|
97
106
|
}
|
|
98
107
|
if (view === "detail" && selectedMember) {
|
|
99
108
|
return (_jsx(SubagentDetail, { member: selectedMember, group: selectedRow?.type === "member" ? selectedRow.group : undefined, scroll: detailScroll, maxVisible: maxVisible, termWidth: termWidth }));
|
|
@@ -103,8 +112,15 @@ export function SubagentInspector({ groups, onCancel }) {
|
|
|
103
112
|
const visible = rows.slice(start, start + maxVisible);
|
|
104
113
|
const labelWidth = 12;
|
|
105
114
|
const descriptorWidth = Math.max(20, Math.min(46, termWidth - 48));
|
|
106
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Subagents
|
|
115
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: tasks.length > 0 ? "Subagents & tasks · working traces" : "Subagents · working traces" }), _jsxs(Text, { color: theme.muted, children: [allMembers.length, " member", allMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(allMembers), tasks.length > 0 ? ` · ${tasks.length} task${tasks.length === 1 ? "" : "s"}` : "", statusFilter ? ` · filter: ${statusFilter}` : ""] }), _jsxs(Text, { color: theme.muted, children: ["\u2191/\u2193 select \u00B7 Enter/\u2192 open trace", tasks.length > 0 ? " · x kill task" : "", " \u00B7 f filter status \u00B7 Esc close"] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [rows.length === 0 && (_jsx(Text, { color: theme.muted, children: "No members match the current filter." })), visible.map((row, i) => {
|
|
107
116
|
const actualIndex = start + i;
|
|
117
|
+
if (row.type === "task") {
|
|
118
|
+
const task = row.task;
|
|
119
|
+
const isSelected = actualIndex === selectedRowIndex;
|
|
120
|
+
const elapsed = Math.max(0, Math.round(((task.endedAt ?? Date.now()) - task.startedAt) / 1000));
|
|
121
|
+
const statusColor = task.status === "running" ? theme.warning : task.status === "completed" ? theme.success : theme.error;
|
|
122
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? theme.accent : undefined, children: isSelected ? "> " : " " }), _jsx(Text, { color: statusColor, children: padVisual(task.id, labelWidth) }), _jsxs(Text, { color: theme.traceAction, children: [" ", padVisual(truncateVisual(task.description || task.command, descriptorWidth), descriptorWidth)] }), _jsxs(Text, { color: statusColor, children: [" ", padVisual(task.status, 9)] }), _jsxs(Text, { color: theme.traceDetail, children: [" ", elapsed, "s", task.exitCode != null ? ` · exit ${task.exitCode}` : ""] })] }, row.key));
|
|
123
|
+
}
|
|
108
124
|
if (row.type === "header") {
|
|
109
125
|
return (_jsx(Box, { marginTop: i === 0 ? 0 : 1, children: _jsxs(Text, { bold: true, color: theme.muted, children: ["\u25A6 ", row.group.kind, " \u00B7 ", truncateVisual(row.group.label, termWidth - 18), " (", row.group.members.length, ")"] }) }, `h-${actualIndex}`));
|
|
110
126
|
}
|
package/dist/tui-ink/welcome.js
CHANGED
|
@@ -32,6 +32,10 @@ function GradientText({ text, from, to }) {
|
|
|
32
32
|
export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelId, modelLabel, thinkingLabel, }) {
|
|
33
33
|
const theme = useTheme();
|
|
34
34
|
const effectiveWidth = Math.max(24, Math.min(terminalColumns - 2, 96));
|
|
35
|
+
// The model row here refreshes live only while the banner is still in the
|
|
36
|
+
// dynamic region (empty transcript). Once the first settled row commits it
|
|
37
|
+
// into <Static> scrollback it freezes; from then on the accent-colored
|
|
38
|
+
// "Model switched" transcript notices carry model changes.
|
|
35
39
|
const modelLine = formatModelLine({
|
|
36
40
|
providerId,
|
|
37
41
|
modelId,
|