@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.
Files changed (119) hide show
  1. package/dist/agent/categories.d.ts +34 -0
  2. package/dist/agent/categories.js +98 -0
  3. package/dist/agent/profiles.d.ts +4 -0
  4. package/dist/agent/profiles.js +2 -3
  5. package/dist/agent/subagent-control.d.ts +5 -0
  6. package/dist/agent/subagent-control.js +4 -0
  7. package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
  8. package/dist/agent/subagent-lifecycle-reminder.js +102 -0
  9. package/dist/agent/subagent-route-format.d.ts +8 -0
  10. package/dist/agent/subagent-route-format.js +18 -0
  11. package/dist/agent/subtask-policy.d.ts +0 -1
  12. package/dist/agent/subtask-policy.js +0 -4
  13. package/dist/agent.d.ts +18 -0
  14. package/dist/agent.js +188 -16
  15. package/dist/config.d.ts +23 -3
  16. package/dist/config.js +59 -6
  17. package/dist/context/budget.d.ts +3 -2
  18. package/dist/context/budget.js +29 -15
  19. package/dist/context/compact.d.ts +23 -0
  20. package/dist/context/compact.js +129 -0
  21. package/dist/context/llm-compactor.d.ts +19 -0
  22. package/dist/context/llm-compactor.js +200 -0
  23. package/dist/context/projector.js +28 -12
  24. package/dist/context/token-estimator.d.ts +14 -0
  25. package/dist/context/token-estimator.js +106 -0
  26. package/dist/context/tool-output-truncate.d.ts +8 -0
  27. package/dist/context/tool-output-truncate.js +59 -0
  28. package/dist/context/usage.d.ts +34 -0
  29. package/dist/context/usage.js +213 -0
  30. package/dist/diff-stats.d.ts +5 -0
  31. package/dist/diff-stats.js +21 -0
  32. package/dist/main.js +68 -7
  33. package/dist/mcp/transports.d.ts +1 -0
  34. package/dist/mcp/transports.js +8 -0
  35. package/dist/model-catalog.d.ts +9 -0
  36. package/dist/model-catalog.js +17 -1
  37. package/dist/orchestrator/default-hooks.js +24 -18
  38. package/dist/prompt/compose.js +2 -1
  39. package/dist/prompt/provider-prompts/kimi.js +3 -1
  40. package/dist/provider-openai-codex.d.ts +13 -2
  41. package/dist/provider-openai-codex.js +81 -32
  42. package/dist/provider-registry.js +22 -6
  43. package/dist/provider-transform.d.ts +3 -1
  44. package/dist/provider-transform.js +15 -0
  45. package/dist/provider.d.ts +4 -1
  46. package/dist/provider.js +89 -4
  47. package/dist/reasoning-debug.d.ts +7 -0
  48. package/dist/reasoning-debug.js +30 -0
  49. package/dist/session-log.js +13 -2
  50. package/dist/session-types.d.ts +1 -1
  51. package/dist/slash-commands/commands.js +60 -2
  52. package/dist/slash-commands/types.d.ts +7 -0
  53. package/dist/tools/agent-lifecycle.js +22 -4
  54. package/dist/tools/edit.js +7 -2
  55. package/dist/tools/file-state.d.ts +19 -0
  56. package/dist/tools/file-state.js +15 -0
  57. package/dist/tools/glob.js +2 -1
  58. package/dist/tools/grep.js +2 -2
  59. package/dist/tools/lsp.js +2 -2
  60. package/dist/tools/path-utils.d.ts +2 -0
  61. package/dist/tools/path-utils.js +16 -0
  62. package/dist/tools/read.d.ts +1 -1
  63. package/dist/tools/read.js +207 -14
  64. package/dist/tools/write.js +3 -2
  65. package/dist/tui/escape-confirmation.d.ts +15 -0
  66. package/dist/tui/escape-confirmation.js +30 -0
  67. package/dist/tui/run.js +93 -23
  68. package/dist/tui-ink/app.d.ts +52 -0
  69. package/dist/tui-ink/app.js +1129 -0
  70. package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
  71. package/dist/tui-ink/approval/approval-dialog.js +132 -0
  72. package/dist/tui-ink/approval/diff-view.d.ts +7 -0
  73. package/dist/tui-ink/approval/diff-view.js +44 -0
  74. package/dist/tui-ink/approval/select.d.ts +35 -0
  75. package/dist/tui-ink/approval/select.js +88 -0
  76. package/dist/tui-ink/code-highlight.d.ts +8 -0
  77. package/dist/tui-ink/code-highlight.js +122 -0
  78. package/dist/tui-ink/detect-theme.d.ts +19 -0
  79. package/dist/tui-ink/detect-theme.js +123 -0
  80. package/dist/tui-ink/display-history.d.ts +38 -0
  81. package/dist/tui-ink/display-history.js +130 -0
  82. package/dist/tui-ink/edit-diff.d.ts +11 -0
  83. package/dist/tui-ink/edit-diff.js +52 -0
  84. package/dist/tui-ink/file-mentions.d.ts +29 -0
  85. package/dist/tui-ink/file-mentions.js +174 -0
  86. package/dist/tui-ink/footer.d.ts +19 -0
  87. package/dist/tui-ink/footer.js +45 -0
  88. package/dist/tui-ink/image-paste.d.ts +54 -0
  89. package/dist/tui-ink/image-paste.js +288 -0
  90. package/dist/tui-ink/input-box.d.ts +41 -0
  91. package/dist/tui-ink/input-box.js +694 -0
  92. package/dist/tui-ink/input-history.d.ts +16 -0
  93. package/dist/tui-ink/input-history.js +81 -0
  94. package/dist/tui-ink/markdown.d.ts +38 -0
  95. package/dist/tui-ink/markdown.js +394 -0
  96. package/dist/tui-ink/message-list.d.ts +33 -0
  97. package/dist/tui-ink/message-list.js +667 -0
  98. package/dist/tui-ink/model-picker.d.ts +43 -0
  99. package/dist/tui-ink/model-picker.js +331 -0
  100. package/dist/tui-ink/plan-confirm.d.ts +7 -0
  101. package/dist/tui-ink/plan-confirm.js +105 -0
  102. package/dist/tui-ink/question-dialog.d.ts +8 -0
  103. package/dist/tui-ink/question-dialog.js +99 -0
  104. package/dist/tui-ink/recent-activity.d.ts +8 -0
  105. package/dist/tui-ink/recent-activity.js +71 -0
  106. package/dist/tui-ink/run.d.ts +37 -0
  107. package/dist/tui-ink/run.js +53 -0
  108. package/dist/tui-ink/theme.d.ts +66 -0
  109. package/dist/tui-ink/theme.js +115 -0
  110. package/dist/tui-ink/todos.d.ts +7 -0
  111. package/dist/tui-ink/todos.js +46 -0
  112. package/dist/tui-ink/trace-groups.d.ts +27 -0
  113. package/dist/tui-ink/trace-groups.js +389 -0
  114. package/dist/tui-ink/use-terminal-size.d.ts +4 -0
  115. package/dist/tui-ink/use-terminal-size.js +21 -0
  116. package/dist/tui-ink/welcome.d.ts +18 -0
  117. package/dist/tui-ink/welcome.js +138 -0
  118. package/dist/types.d.ts +10 -0
  119. 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,4 @@
1
+ export declare function useTerminalSize(): {
2
+ columns: number;
3
+ rows: number;
4
+ };
@@ -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.7",
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"