@bubblebrain-ai/bubble 0.0.7 → 0.0.9
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/categories.d.ts +34 -0
- package/dist/agent/categories.js +98 -0
- package/dist/agent/profiles.d.ts +4 -0
- package/dist/agent/profiles.js +2 -3
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-control.js +4 -0
- package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
- package/dist/agent/subagent-lifecycle-reminder.js +102 -0
- package/dist/agent/subagent-route-format.d.ts +8 -0
- package/dist/agent/subagent-route-format.js +18 -0
- package/dist/agent/subtask-policy.d.ts +0 -1
- package/dist/agent/subtask-policy.js +0 -4
- package/dist/agent.d.ts +18 -0
- package/dist/agent.js +188 -16
- package/dist/config.d.ts +23 -3
- package/dist/config.js +59 -6
- package/dist/context/budget.d.ts +3 -2
- package/dist/context/budget.js +29 -15
- package/dist/context/compact.d.ts +23 -0
- package/dist/context/compact.js +129 -0
- package/dist/context/llm-compactor.d.ts +19 -0
- package/dist/context/llm-compactor.js +200 -0
- package/dist/context/projector.js +28 -12
- package/dist/context/token-estimator.d.ts +14 -0
- package/dist/context/token-estimator.js +106 -0
- package/dist/context/tool-output-truncate.d.ts +8 -0
- package/dist/context/tool-output-truncate.js +59 -0
- package/dist/context/usage.d.ts +34 -0
- package/dist/context/usage.js +213 -0
- package/dist/diff-stats.d.ts +5 -0
- package/dist/diff-stats.js +21 -0
- package/dist/main.js +68 -7
- package/dist/mcp/transports.d.ts +1 -0
- package/dist/mcp/transports.js +8 -0
- package/dist/model-catalog.d.ts +9 -0
- package/dist/model-catalog.js +17 -1
- package/dist/orchestrator/default-hooks.js +24 -18
- package/dist/prompt/compose.js +2 -1
- package/dist/prompt/provider-prompts/kimi.js +3 -1
- package/dist/provider-openai-codex.d.ts +13 -2
- package/dist/provider-openai-codex.js +81 -32
- package/dist/provider-registry.js +22 -6
- package/dist/provider-transform.d.ts +3 -1
- package/dist/provider-transform.js +15 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +89 -4
- package/dist/reasoning-debug.d.ts +7 -0
- package/dist/reasoning-debug.js +30 -0
- package/dist/session-log.js +13 -2
- package/dist/session-types.d.ts +1 -1
- package/dist/slash-commands/commands.js +60 -2
- package/dist/slash-commands/types.d.ts +7 -0
- package/dist/tools/agent-lifecycle.js +22 -4
- package/dist/tools/edit.js +7 -2
- package/dist/tools/file-state.d.ts +19 -0
- package/dist/tools/file-state.js +15 -0
- package/dist/tools/glob.js +2 -1
- package/dist/tools/grep.js +2 -2
- package/dist/tools/lsp.js +2 -2
- package/dist/tools/path-utils.d.ts +2 -0
- package/dist/tools/path-utils.js +16 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +207 -14
- package/dist/tools/write.js +3 -2
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/run.js +93 -23
- package/dist/tui-ink/app.d.ts +52 -0
- package/dist/tui-ink/app.js +1129 -0
- package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
- package/dist/tui-ink/approval/approval-dialog.js +132 -0
- package/dist/tui-ink/approval/diff-view.d.ts +7 -0
- package/dist/tui-ink/approval/diff-view.js +44 -0
- package/dist/tui-ink/approval/select.d.ts +35 -0
- package/dist/tui-ink/approval/select.js +88 -0
- package/dist/tui-ink/code-highlight.d.ts +8 -0
- package/dist/tui-ink/code-highlight.js +122 -0
- package/dist/tui-ink/detect-theme.d.ts +19 -0
- package/dist/tui-ink/detect-theme.js +123 -0
- package/dist/tui-ink/display-history.d.ts +38 -0
- package/dist/tui-ink/display-history.js +130 -0
- package/dist/tui-ink/edit-diff.d.ts +11 -0
- package/dist/tui-ink/edit-diff.js +52 -0
- package/dist/tui-ink/file-mentions.d.ts +29 -0
- package/dist/tui-ink/file-mentions.js +174 -0
- package/dist/tui-ink/footer.d.ts +19 -0
- package/dist/tui-ink/footer.js +45 -0
- package/dist/tui-ink/image-paste.d.ts +54 -0
- package/dist/tui-ink/image-paste.js +288 -0
- package/dist/tui-ink/input-box.d.ts +41 -0
- package/dist/tui-ink/input-box.js +694 -0
- package/dist/tui-ink/input-history.d.ts +16 -0
- package/dist/tui-ink/input-history.js +81 -0
- package/dist/tui-ink/markdown.d.ts +38 -0
- package/dist/tui-ink/markdown.js +394 -0
- package/dist/tui-ink/message-list.d.ts +33 -0
- package/dist/tui-ink/message-list.js +667 -0
- package/dist/tui-ink/model-picker.d.ts +43 -0
- package/dist/tui-ink/model-picker.js +331 -0
- package/dist/tui-ink/plan-confirm.d.ts +7 -0
- package/dist/tui-ink/plan-confirm.js +105 -0
- package/dist/tui-ink/question-dialog.d.ts +8 -0
- package/dist/tui-ink/question-dialog.js +99 -0
- package/dist/tui-ink/recent-activity.d.ts +8 -0
- package/dist/tui-ink/recent-activity.js +71 -0
- package/dist/tui-ink/run.d.ts +37 -0
- package/dist/tui-ink/run.js +53 -0
- package/dist/tui-ink/theme.d.ts +66 -0
- package/dist/tui-ink/theme.js +115 -0
- package/dist/tui-ink/todos.d.ts +7 -0
- package/dist/tui-ink/todos.js +46 -0
- package/dist/tui-ink/trace-groups.d.ts +27 -0
- package/dist/tui-ink/trace-groups.js +389 -0
- package/dist/tui-ink/use-terminal-size.d.ts +4 -0
- package/dist/tui-ink/use-terminal-size.js +21 -0
- package/dist/tui-ink/welcome.d.ts +18 -0
- package/dist/tui-ink/welcome.js +138 -0
- package/dist/types.d.ts +10 -0
- package/package.json +7 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { getEditDiffDetails } from "./edit-diff.js";
|
|
3
|
+
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
4
|
+
const DEFAULT_MAX_ITEMS = 6;
|
|
5
|
+
const DEFAULT_MAX_PREVIEW_LINES = 8;
|
|
6
|
+
export function buildTraceGroups(toolCalls, options = {}) {
|
|
7
|
+
const maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
|
|
8
|
+
const maxPreviewLines = options.maxPreviewLines ?? DEFAULT_MAX_PREVIEW_LINES;
|
|
9
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
10
|
+
const groups = [];
|
|
11
|
+
let bucket = [];
|
|
12
|
+
let bucketClassifier = null;
|
|
13
|
+
const flush = () => {
|
|
14
|
+
if (bucket.length === 0 || !bucketClassifier)
|
|
15
|
+
return;
|
|
16
|
+
groups.push(buildTraceGroup(bucketClassifier, bucket, {
|
|
17
|
+
maxItems,
|
|
18
|
+
maxPreviewLines,
|
|
19
|
+
homeDir,
|
|
20
|
+
}));
|
|
21
|
+
bucket = [];
|
|
22
|
+
bucketClassifier = null;
|
|
23
|
+
};
|
|
24
|
+
for (const toolCall of toolCalls) {
|
|
25
|
+
const classifier = classifyTool(toolCall);
|
|
26
|
+
if (!classifier.groupable) {
|
|
27
|
+
flush();
|
|
28
|
+
groups.push(buildTraceGroup(classifier, [toolCall], {
|
|
29
|
+
maxItems,
|
|
30
|
+
maxPreviewLines,
|
|
31
|
+
homeDir,
|
|
32
|
+
}));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (bucketClassifier?.bucketKey === classifier.bucketKey) {
|
|
36
|
+
bucket.push(toolCall);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
flush();
|
|
40
|
+
bucket = [toolCall];
|
|
41
|
+
bucketClassifier = classifier;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
flush();
|
|
45
|
+
return groups;
|
|
46
|
+
}
|
|
47
|
+
export function formatTracePath(value, homeDir = os.homedir()) {
|
|
48
|
+
const text = String(value ?? "").trim();
|
|
49
|
+
if (!text)
|
|
50
|
+
return "";
|
|
51
|
+
if (text === homeDir)
|
|
52
|
+
return "~";
|
|
53
|
+
if (text.startsWith(homeDir + "/"))
|
|
54
|
+
return "~" + text.slice(homeDir.length);
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
export function formatElapsed(startedAt, now = Date.now()) {
|
|
58
|
+
if (!startedAt)
|
|
59
|
+
return null;
|
|
60
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
61
|
+
if (seconds < 60)
|
|
62
|
+
return `${seconds}s`;
|
|
63
|
+
const minutes = Math.floor(seconds / 60);
|
|
64
|
+
const remainder = seconds % 60;
|
|
65
|
+
return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
|
|
66
|
+
}
|
|
67
|
+
export function traceGroupLabel(group) {
|
|
68
|
+
if (group.command)
|
|
69
|
+
return `${group.title} ${group.command}`;
|
|
70
|
+
if (group.count !== undefined && group.noun)
|
|
71
|
+
return `${group.title} ${group.count} ${group.noun}`;
|
|
72
|
+
return group.title;
|
|
73
|
+
}
|
|
74
|
+
function classifyTool(toolCall) {
|
|
75
|
+
if (toolCall.metadata?.kind === "subagent") {
|
|
76
|
+
return { kind: "subagent", title: "Subagents", bucketKey: `subagent:${toolCall.id}`, groupable: false };
|
|
77
|
+
}
|
|
78
|
+
switch (toolCall.name) {
|
|
79
|
+
case "glob": {
|
|
80
|
+
const pattern = String(toolCall.args.pattern ?? "");
|
|
81
|
+
const title = isDirectoryLikeGlob(pattern) ? "List Directory" : "Find Files";
|
|
82
|
+
return {
|
|
83
|
+
kind: "list",
|
|
84
|
+
title,
|
|
85
|
+
bucketKey: `list:${title}`,
|
|
86
|
+
groupable: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case "read":
|
|
90
|
+
return { kind: "read", title: "Read", bucketKey: "read", groupable: true };
|
|
91
|
+
case "grep":
|
|
92
|
+
return { kind: "search", title: "Search", bucketKey: "search", groupable: true };
|
|
93
|
+
case "bash":
|
|
94
|
+
return { kind: "execute", title: "Execute", bucketKey: `execute:${toolCall.id}`, groupable: false };
|
|
95
|
+
case "edit":
|
|
96
|
+
return { kind: "edit", title: "Edit", bucketKey: `edit:${toolCall.id}`, groupable: false };
|
|
97
|
+
case "write":
|
|
98
|
+
return { kind: "write", title: "Write", bucketKey: "write", groupable: true };
|
|
99
|
+
default:
|
|
100
|
+
return {
|
|
101
|
+
kind: "other",
|
|
102
|
+
title: displayToolName(toolCall.name),
|
|
103
|
+
bucketKey: `${toolCall.name}:${toolCall.id}`,
|
|
104
|
+
groupable: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function buildTraceGroup(classifier, raw, options) {
|
|
109
|
+
const pending = raw.some((tool) => isToolPending(tool));
|
|
110
|
+
const startedAt = raw
|
|
111
|
+
.filter((tool) => isToolPending(tool))
|
|
112
|
+
.map((tool) => tool.startedAt)
|
|
113
|
+
.filter((value) => typeof value === "number")
|
|
114
|
+
.sort((a, b) => a - b)[0];
|
|
115
|
+
const hasError = raw.some((tool) => !!tool.isError);
|
|
116
|
+
const errorCount = raw.filter((tool) => !!tool.isError).length;
|
|
117
|
+
switch (classifier.kind) {
|
|
118
|
+
case "list":
|
|
119
|
+
return buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
120
|
+
case "read":
|
|
121
|
+
return buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, "files");
|
|
122
|
+
case "search":
|
|
123
|
+
return buildSearchGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
124
|
+
case "execute":
|
|
125
|
+
return buildExecuteGroup(classifier, raw[0], options, pending, startedAt, hasError, errorCount);
|
|
126
|
+
case "edit":
|
|
127
|
+
case "write":
|
|
128
|
+
return buildMutationGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
129
|
+
case "subagent":
|
|
130
|
+
return buildSubagentGroup(classifier, raw[0], options, pending, startedAt);
|
|
131
|
+
default:
|
|
132
|
+
return buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
136
|
+
const resultItems = raw.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)));
|
|
137
|
+
const fallbackItems = raw
|
|
138
|
+
.map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.map((item) => formatTracePath(item, options.homeDir));
|
|
141
|
+
const sourceItems = resultItems.length > 0 ? resultItems : fallbackItems;
|
|
142
|
+
const { shown, omitted } = take(sourceItems, options.maxItems);
|
|
143
|
+
const count = resultItems.length > 0 ? resultItems.length : sourceItems.length || raw.length;
|
|
144
|
+
const noun = resultItems.length > 0 ? plural(count, "file", "files") : plural(count, "search", "searches");
|
|
145
|
+
return {
|
|
146
|
+
kind: "list",
|
|
147
|
+
title: classifier.title,
|
|
148
|
+
raw,
|
|
149
|
+
count,
|
|
150
|
+
noun,
|
|
151
|
+
items: shown,
|
|
152
|
+
previewLines: [],
|
|
153
|
+
errorLines: collectErrorLines(raw, options),
|
|
154
|
+
omitted,
|
|
155
|
+
pending,
|
|
156
|
+
hasError,
|
|
157
|
+
errorCount,
|
|
158
|
+
startedAt,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
|
|
162
|
+
const items = unique(raw
|
|
163
|
+
.map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
|
|
164
|
+
.filter(Boolean));
|
|
165
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
166
|
+
const count = items.length || raw.length;
|
|
167
|
+
return {
|
|
168
|
+
kind: classifier.kind,
|
|
169
|
+
title: classifier.title,
|
|
170
|
+
raw,
|
|
171
|
+
count,
|
|
172
|
+
noun: plural(count, nounBase.slice(0, -1), nounBase),
|
|
173
|
+
items: shown,
|
|
174
|
+
previewLines: [],
|
|
175
|
+
errorLines: collectErrorLines(raw, options),
|
|
176
|
+
omitted,
|
|
177
|
+
pending,
|
|
178
|
+
hasError,
|
|
179
|
+
errorCount,
|
|
180
|
+
startedAt,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function buildSearchGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
184
|
+
const items = raw.map((tool) => {
|
|
185
|
+
const pattern = String(tool.args.pattern ?? tool.args.query ?? "").trim();
|
|
186
|
+
const scope = String(tool.args.path ?? tool.args.glob ?? tool.args.include ?? "").trim();
|
|
187
|
+
const patternText = pattern ? `"${pattern}"` : "(pattern)";
|
|
188
|
+
return scope ? `${patternText} in ${formatTracePath(scope, options.homeDir)}` : patternText;
|
|
189
|
+
});
|
|
190
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
191
|
+
const count = raw.length;
|
|
192
|
+
return {
|
|
193
|
+
kind: "search",
|
|
194
|
+
title: classifier.title,
|
|
195
|
+
raw,
|
|
196
|
+
count,
|
|
197
|
+
noun: plural(count, "search", "searches"),
|
|
198
|
+
items: shown,
|
|
199
|
+
previewLines: [],
|
|
200
|
+
errorLines: collectErrorLines(raw, options),
|
|
201
|
+
omitted,
|
|
202
|
+
pending,
|
|
203
|
+
hasError,
|
|
204
|
+
errorCount,
|
|
205
|
+
startedAt,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildExecuteGroup(classifier, tool, options, pending, startedAt, hasError, errorCount) {
|
|
209
|
+
const lines = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
210
|
+
const { shown, omitted } = take(lines, options.maxPreviewLines);
|
|
211
|
+
return {
|
|
212
|
+
kind: "execute",
|
|
213
|
+
title: classifier.title,
|
|
214
|
+
raw: [tool],
|
|
215
|
+
command: normalizeCommand(tool.args.command ?? ""),
|
|
216
|
+
items: [],
|
|
217
|
+
previewLines: shown,
|
|
218
|
+
errorLines: [],
|
|
219
|
+
omitted,
|
|
220
|
+
pending,
|
|
221
|
+
hasError,
|
|
222
|
+
errorCount,
|
|
223
|
+
startedAt,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function buildMutationGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
227
|
+
const items = raw
|
|
228
|
+
.map((tool) => {
|
|
229
|
+
const path = formatTracePath(tool.args.path ?? "", options.homeDir);
|
|
230
|
+
const details = tool.name === "edit" ? getEditDiffDetails(tool) : null;
|
|
231
|
+
const suffix = details ? ` ${formatCompactEditStats(details.added, details.removed)}` : "";
|
|
232
|
+
return path ? `${path}${suffix}` : "";
|
|
233
|
+
})
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
236
|
+
const count = items.length || raw.length;
|
|
237
|
+
const errorPreview = hasError
|
|
238
|
+
? raw
|
|
239
|
+
.filter((tool) => tool.isError)
|
|
240
|
+
.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)))
|
|
241
|
+
.slice(0, options.maxPreviewLines)
|
|
242
|
+
: [];
|
|
243
|
+
return {
|
|
244
|
+
kind: classifier.kind,
|
|
245
|
+
title: classifier.title,
|
|
246
|
+
raw,
|
|
247
|
+
count,
|
|
248
|
+
noun: plural(count, "file", "files"),
|
|
249
|
+
items: shown,
|
|
250
|
+
previewLines: errorPreview,
|
|
251
|
+
errorLines: [],
|
|
252
|
+
omitted,
|
|
253
|
+
pending,
|
|
254
|
+
hasError,
|
|
255
|
+
errorCount,
|
|
256
|
+
startedAt,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function buildSubagentGroup(classifier, tool, options, pending, startedAt) {
|
|
260
|
+
const subagents = subagentsFromMetadata(tool);
|
|
261
|
+
const rows = subagents.length > 0
|
|
262
|
+
? subagents.map(formatSubagentRow)
|
|
263
|
+
: resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
264
|
+
const { shown, omitted } = take(rows, options.maxPreviewLines);
|
|
265
|
+
const errorCount = subagents.filter(isFailedSubagent).length + (tool.isError ? 1 : 0);
|
|
266
|
+
return {
|
|
267
|
+
kind: "subagent",
|
|
268
|
+
title: classifier.title,
|
|
269
|
+
raw: [tool],
|
|
270
|
+
count: subagents.length || 1,
|
|
271
|
+
noun: plural(subagents.length || 1, "agent", "agents"),
|
|
272
|
+
items: [],
|
|
273
|
+
previewLines: shown,
|
|
274
|
+
errorLines: [],
|
|
275
|
+
omitted,
|
|
276
|
+
pending: pending || subagents.some((subagent) => ["queued", "running"].includes(subagent.status ?? "running")),
|
|
277
|
+
hasError: !!tool.isError || errorCount > 0,
|
|
278
|
+
errorCount,
|
|
279
|
+
startedAt,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
283
|
+
const tool = raw[0];
|
|
284
|
+
const header = toolHeader(tool, options.homeDir);
|
|
285
|
+
const preview = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
286
|
+
const { shown, omitted } = take(preview, options.maxPreviewLines);
|
|
287
|
+
return {
|
|
288
|
+
kind: "other",
|
|
289
|
+
title: classifier.title,
|
|
290
|
+
raw,
|
|
291
|
+
count: header ? undefined : raw.length,
|
|
292
|
+
noun: header ? undefined : plural(raw.length, "call", "calls"),
|
|
293
|
+
items: header ? [header] : [],
|
|
294
|
+
previewLines: shown,
|
|
295
|
+
errorLines: [],
|
|
296
|
+
omitted,
|
|
297
|
+
pending,
|
|
298
|
+
hasError,
|
|
299
|
+
errorCount,
|
|
300
|
+
startedAt,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function subagentsFromMetadata(tool) {
|
|
304
|
+
const raw = tool.metadata?.subagents;
|
|
305
|
+
if (!Array.isArray(raw))
|
|
306
|
+
return [];
|
|
307
|
+
return raw.filter((item) => typeof item === "object" && item !== null);
|
|
308
|
+
}
|
|
309
|
+
function formatSubagentRow(subagent) {
|
|
310
|
+
const label = subagent.nickname || subagent.agentName || subagent.subAgentId || "subagent";
|
|
311
|
+
const role = [subagent.agentName, subagent.category ? `/${subagent.category}` : ""].join("") || "default";
|
|
312
|
+
const route = formatSubagentRoute(subagent.route);
|
|
313
|
+
const descriptor = route ? `${role} @ ${route}` : role;
|
|
314
|
+
const status = subagent.status || "running";
|
|
315
|
+
const note = subagent.error
|
|
316
|
+
|| subagent.toolNotes?.filter(Boolean).at(-1)
|
|
317
|
+
|| subagent.summary
|
|
318
|
+
|| subagent.task
|
|
319
|
+
|| "";
|
|
320
|
+
return [label, `(${descriptor})`, status, note].filter(Boolean).join(" ");
|
|
321
|
+
}
|
|
322
|
+
function isFailedSubagent(subagent) {
|
|
323
|
+
return subagent.status === "failed" || subagent.status === "blocked" || subagent.status === "cancelled";
|
|
324
|
+
}
|
|
325
|
+
function isToolPending(tool) {
|
|
326
|
+
return tool.result === undefined;
|
|
327
|
+
}
|
|
328
|
+
function isDirectoryLikeGlob(pattern) {
|
|
329
|
+
const normalized = pattern.trim();
|
|
330
|
+
return normalized === "" || normalized === "*" || normalized === "**" || normalized === "**/*";
|
|
331
|
+
}
|
|
332
|
+
function resultLines(result) {
|
|
333
|
+
if (result === undefined)
|
|
334
|
+
return [];
|
|
335
|
+
return result
|
|
336
|
+
.replace(/\r\n/g, "\n")
|
|
337
|
+
.split("\n")
|
|
338
|
+
.map((line) => line.trimEnd())
|
|
339
|
+
.filter((line) => line.trim() !== "");
|
|
340
|
+
}
|
|
341
|
+
function take(items, max) {
|
|
342
|
+
const shown = items.slice(0, max);
|
|
343
|
+
return { shown, omitted: Math.max(0, items.length - shown.length) };
|
|
344
|
+
}
|
|
345
|
+
function unique(items) {
|
|
346
|
+
return [...new Set(items)];
|
|
347
|
+
}
|
|
348
|
+
function collectErrorLines(raw, options) {
|
|
349
|
+
return raw
|
|
350
|
+
.filter((tool) => tool.isError)
|
|
351
|
+
.flatMap((tool) => resultLines(tool.result).map((line) => formatTraceLine(line, options.homeDir)))
|
|
352
|
+
.slice(0, options.maxPreviewLines);
|
|
353
|
+
}
|
|
354
|
+
function formatTraceLine(value, homeDir) {
|
|
355
|
+
const text = String(value ?? "").trimEnd();
|
|
356
|
+
if (!homeDir)
|
|
357
|
+
return text;
|
|
358
|
+
return text.split(homeDir + "/").join("~/").split(homeDir).join("~");
|
|
359
|
+
}
|
|
360
|
+
function plural(count, singular, pluralValue) {
|
|
361
|
+
return count === 1 ? singular : pluralValue;
|
|
362
|
+
}
|
|
363
|
+
function normalizeCommand(value) {
|
|
364
|
+
const command = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
365
|
+
return command || "(command)";
|
|
366
|
+
}
|
|
367
|
+
function displayToolName(name) {
|
|
368
|
+
if (!name)
|
|
369
|
+
return "Tool";
|
|
370
|
+
return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
|
|
371
|
+
}
|
|
372
|
+
function toolHeader(tool, homeDir) {
|
|
373
|
+
const args = tool.args || {};
|
|
374
|
+
for (const key of ["path", "command", "pattern", "query", "url"]) {
|
|
375
|
+
const value = args[key];
|
|
376
|
+
if (value !== undefined && value !== null && String(value).trim() !== "") {
|
|
377
|
+
return formatTracePath(value, homeDir);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
function formatCompactEditStats(added, removed) {
|
|
383
|
+
const parts = [];
|
|
384
|
+
if (added > 0)
|
|
385
|
+
parts.push(`+${added}`);
|
|
386
|
+
if (removed > 0)
|
|
387
|
+
parts.push(`-${removed}`);
|
|
388
|
+
return parts.length > 0 ? `(${parts.join(" ")})` : "";
|
|
389
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useStdout } from "ink";
|
|
3
|
+
export function useTerminalSize() {
|
|
4
|
+
const { stdout } = useStdout();
|
|
5
|
+
const [size, setSize] = useState(() => ({
|
|
6
|
+
columns: stdout?.columns || 80,
|
|
7
|
+
rows: stdout?.rows || 24,
|
|
8
|
+
}));
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!stdout)
|
|
11
|
+
return;
|
|
12
|
+
const onResize = () => {
|
|
13
|
+
setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
14
|
+
};
|
|
15
|
+
stdout.on("resize", onResize);
|
|
16
|
+
return () => {
|
|
17
|
+
stdout.off("resize", onResize);
|
|
18
|
+
};
|
|
19
|
+
}, [stdout]);
|
|
20
|
+
return size;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DisplayMessage } from "./display-history.js";
|
|
2
|
+
interface WelcomeBannerProps {
|
|
3
|
+
terminalColumns: number;
|
|
4
|
+
modelLabel?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
tips: string[];
|
|
7
|
+
skillsCount?: number;
|
|
8
|
+
mcpConnectedCount?: number;
|
|
9
|
+
mcpTotalCount?: number;
|
|
10
|
+
hasAgentsFile?: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface WelcomeVisibilityInput {
|
|
13
|
+
messages: Pick<DisplayMessage, "role" | "syntheticKind">[];
|
|
14
|
+
startedWithVisibleHistory: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function shouldShowWelcomeBanner({ startedWithVisibleHistory, }: WelcomeVisibilityInput): boolean;
|
|
17
|
+
export declare function WelcomeBanner({ terminalColumns, modelLabel, cwd, tips, skillsCount, mcpConnectedCount, mcpTotalCount, hasAgentsFile, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { useTheme } from "./theme.js";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const PACKAGE_VERSION = readPackageVersion();
|
|
8
|
+
const BUBBLE_LOGO_LETTERS = [
|
|
9
|
+
[
|
|
10
|
+
"██████ ",
|
|
11
|
+
"██ ██",
|
|
12
|
+
"██ ██",
|
|
13
|
+
"██████ ",
|
|
14
|
+
"██ ██",
|
|
15
|
+
"██ ██",
|
|
16
|
+
"██████ ",
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"██ ██",
|
|
20
|
+
"██ ██",
|
|
21
|
+
"██ ██",
|
|
22
|
+
"██ ██",
|
|
23
|
+
"██ ██",
|
|
24
|
+
"██ ██",
|
|
25
|
+
" █████ ",
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"██████ ",
|
|
29
|
+
"██ ██",
|
|
30
|
+
"██ ██",
|
|
31
|
+
"██████ ",
|
|
32
|
+
"██ ██",
|
|
33
|
+
"██ ██",
|
|
34
|
+
"██████ ",
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"██████ ",
|
|
38
|
+
"██ ██",
|
|
39
|
+
"██ ██",
|
|
40
|
+
"██████ ",
|
|
41
|
+
"██ ██",
|
|
42
|
+
"██ ██",
|
|
43
|
+
"██████ ",
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
"██ ",
|
|
47
|
+
"██ ",
|
|
48
|
+
"██ ",
|
|
49
|
+
"██ ",
|
|
50
|
+
"██ ",
|
|
51
|
+
"██ ",
|
|
52
|
+
"███████",
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"███████",
|
|
56
|
+
"██ ",
|
|
57
|
+
"██ ",
|
|
58
|
+
"██████ ",
|
|
59
|
+
"██ ",
|
|
60
|
+
"██ ",
|
|
61
|
+
"███████",
|
|
62
|
+
],
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Derive a 6-step logo gradient from the active theme tokens so the banner
|
|
66
|
+
* stays readable on both dark and light backgrounds.
|
|
67
|
+
*/
|
|
68
|
+
function logoColors(theme) {
|
|
69
|
+
return [
|
|
70
|
+
theme.userMessageText,
|
|
71
|
+
theme.userMessageText,
|
|
72
|
+
theme.inputBorder,
|
|
73
|
+
theme.inputBorder,
|
|
74
|
+
theme.traceCommand,
|
|
75
|
+
theme.traceCommand,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
const COMPACT_LOGO = ["B", "U", "B", "B", "L", "E"];
|
|
79
|
+
const WIDE_LOGO_MIN_WIDTH = 52;
|
|
80
|
+
export function shouldShowWelcomeBanner({ startedWithVisibleHistory, }) {
|
|
81
|
+
// Banner is committed to Static scrollback once at session start. Flipping
|
|
82
|
+
// this flag back to false (e.g. when a picker opens) shrinks the Static
|
|
83
|
+
// items list — when the items grow back, ink replays the banner a second
|
|
84
|
+
// time into scrollback. Keep visibility decided purely by initial history.
|
|
85
|
+
if (startedWithVisibleHistory)
|
|
86
|
+
return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
export function WelcomeBanner({ terminalColumns, modelLabel, cwd, tips, skillsCount = 0, mcpConnectedCount = 0, mcpTotalCount = 0, hasAgentsFile = false, }) {
|
|
90
|
+
const theme = useTheme();
|
|
91
|
+
const effectiveWidth = Math.max(20, Math.min(terminalColumns - 2, 118));
|
|
92
|
+
const useWideLogo = effectiveWidth >= WIDE_LOGO_MIN_WIDTH;
|
|
93
|
+
const actionableTips = tips
|
|
94
|
+
.filter((item) => !item.startsWith("Ready with") && item.trim().length > 0)
|
|
95
|
+
.slice(0, 2);
|
|
96
|
+
const tip = actionableTips.length > 0
|
|
97
|
+
? actionableTips.join(" · ")
|
|
98
|
+
: "Type / for commands and @ to reference files";
|
|
99
|
+
const modelLine = modelLabel ? `${modelLabel}${cwd ? ` · ${cwd}` : ""}` : cwd;
|
|
100
|
+
return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", alignItems: "center", marginBottom: 1, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", children: useWideLogo
|
|
101
|
+
? BUBBLE_LOGO_LETTERS[0].map((_, rowIndex) => (_jsx(LogoRow, { rowIndex: rowIndex }, `logo-row-${rowIndex}`)))
|
|
102
|
+
: _jsx(CompactLogo, {}) }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { bold: true, color: theme.muted, children: PACKAGE_VERSION }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, color: theme.userMessageText, children: "TIP: " }), _jsx(Text, { bold: true, color: theme.userMessageText, children: tip })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "shift+tab to cycle modes \u00B7 ctrl+r for reasoning \u00B7 ctrl+o for trace" }) }), modelLine && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: truncateToWidth(modelLine, effectiveWidth - 4) }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(StatusItem, { label: "Skills", count: skillsCount, ok: skillsCount > 0 }), _jsx(Text, { color: theme.muted, children: " " }), _jsx(StatusItem, { label: "MCPs", count: mcpConnectedCount, total: mcpTotalCount, ok: mcpTotalCount === 0 || mcpConnectedCount === mcpTotalCount }), _jsx(Text, { color: theme.muted, children: " " }), _jsx(StatusItem, { label: "AGENTS.md", ok: hasAgentsFile })] })] }));
|
|
103
|
+
}
|
|
104
|
+
function LogoRow({ rowIndex }) {
|
|
105
|
+
const theme = useTheme();
|
|
106
|
+
const colors = logoColors(theme);
|
|
107
|
+
return (_jsx(Box, { children: BUBBLE_LOGO_LETTERS.map((letter, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { bold: true, color: colors[index], children: letter[rowIndex] }), index < BUBBLE_LOGO_LETTERS.length - 1 && _jsx(Text, { children: " " })] }, `${index}-${rowIndex}`))) }));
|
|
108
|
+
}
|
|
109
|
+
function CompactLogo() {
|
|
110
|
+
const theme = useTheme();
|
|
111
|
+
const colors = logoColors(theme);
|
|
112
|
+
return (_jsx(Box, { children: COMPACT_LOGO.map((letter, index) => (_jsx(Text, { bold: true, color: colors[index], children: letter }, `${letter}-${index}`))) }));
|
|
113
|
+
}
|
|
114
|
+
function StatusItem({ label, count, total, ok, }) {
|
|
115
|
+
const theme = useTheme();
|
|
116
|
+
const countText = count === undefined
|
|
117
|
+
? ""
|
|
118
|
+
: total !== undefined && total > count
|
|
119
|
+
? ` (${count}/${total})`
|
|
120
|
+
: ` (${count})`;
|
|
121
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: theme.muted, children: [label, countText, " "] }), _jsx(Text, { bold: true, color: ok ? theme.success : theme.error, children: ok ? "✓" : "×" })] }));
|
|
122
|
+
}
|
|
123
|
+
function readPackageVersion() {
|
|
124
|
+
try {
|
|
125
|
+
const pkg = require("../../package.json");
|
|
126
|
+
return pkg.version ? `v${pkg.version}` : "v0.0.0";
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return "v0.0.0";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function truncateToWidth(text, maxWidth) {
|
|
133
|
+
if (maxWidth <= 0)
|
|
134
|
+
return "";
|
|
135
|
+
if (text.length <= maxWidth)
|
|
136
|
+
return text;
|
|
137
|
+
return text.slice(0, Math.max(1, maxWidth - 1)) + "…";
|
|
138
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -101,6 +101,9 @@ export interface ToolResultMetadata {
|
|
|
101
101
|
searchFamily?: string;
|
|
102
102
|
reason?: string;
|
|
103
103
|
arbiterNote?: string;
|
|
104
|
+
diff?: string;
|
|
105
|
+
addedLines?: number;
|
|
106
|
+
removedLines?: number;
|
|
104
107
|
[key: string]: unknown;
|
|
105
108
|
}
|
|
106
109
|
export interface ToolResult {
|
|
@@ -117,6 +120,8 @@ export interface ToolUpdate {
|
|
|
117
120
|
subAgentId: string;
|
|
118
121
|
agentName: string;
|
|
119
122
|
nickname?: string;
|
|
123
|
+
category?: string;
|
|
124
|
+
route?: import("./agent/categories.js").ResolvedSubagentRoute;
|
|
120
125
|
status: "queued" | "running" | "completed" | "failed" | "blocked" | "cancelled";
|
|
121
126
|
childEvent?: AgentEvent;
|
|
122
127
|
summaryDelta?: string;
|
|
@@ -144,6 +149,8 @@ export interface ToolContext {
|
|
|
144
149
|
runId: string;
|
|
145
150
|
subAgentId: string;
|
|
146
151
|
parentToolCallId: string;
|
|
152
|
+
category?: string;
|
|
153
|
+
route?: import("./agent/categories.js").ResolvedSubagentRoute;
|
|
147
154
|
approval?: "fail" | "disabled";
|
|
148
155
|
emitUpdate?: (update: ToolUpdate) => void;
|
|
149
156
|
description?: string;
|
|
@@ -154,6 +161,8 @@ export interface ToolContext {
|
|
|
154
161
|
spawnSubAgent?: (input: string | ContentPart[], cwd: string, options: {
|
|
155
162
|
profile: import("./agent/profiles.js").AgentProfile;
|
|
156
163
|
parentToolCallId: string;
|
|
164
|
+
category?: string;
|
|
165
|
+
route?: import("./agent/categories.js").ResolvedSubagentRoute;
|
|
157
166
|
approval?: "fail" | "disabled";
|
|
158
167
|
description?: string;
|
|
159
168
|
abortSignal?: AbortSignal;
|
|
@@ -299,6 +308,7 @@ export type AgentEvent = {
|
|
|
299
308
|
} | {
|
|
300
309
|
type: "turn_end";
|
|
301
310
|
usage?: TokenUsage;
|
|
311
|
+
willContinue?: boolean;
|
|
302
312
|
} | {
|
|
303
313
|
type: "context_recovered";
|
|
304
314
|
droppedMessages: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bubblebrain-ai/bubble",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "A terminal coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -27,14 +27,20 @@
|
|
|
27
27
|
"@opentui/core": "^0.1.99",
|
|
28
28
|
"@opentui/solid": "^0.1.99",
|
|
29
29
|
"@types/better-sqlite3": "^7.6.13",
|
|
30
|
+
"@types/react": "^19.2.14",
|
|
30
31
|
"@vue/language-server": "^3.2.7",
|
|
31
32
|
"better-sqlite3": "^12.9.0",
|
|
32
33
|
"chalk": "^5.3.0",
|
|
33
34
|
"diff": "^7.0.0",
|
|
35
|
+
"ink": "^7.0.3",
|
|
36
|
+
"js-tiktoken": "^1.0.21",
|
|
34
37
|
"openai": "^4.77.0",
|
|
35
38
|
"opentui-spinner": "^0.0.6",
|
|
36
39
|
"picomatch": "^4.0.4",
|
|
40
|
+
"react": "^19.2.6",
|
|
41
|
+
"shiki": "^4.0.2",
|
|
37
42
|
"solid-js": "^1.9.11",
|
|
43
|
+
"string-width": "^8.2.1",
|
|
38
44
|
"typescript-language-server": "^5.1.3",
|
|
39
45
|
"vscode-jsonrpc": "^8.2.1",
|
|
40
46
|
"vscode-langservers-extracted": "^4.10.0"
|