@bubblebrain-ai/bubble 0.0.28 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +23 -3
  2. package/dist/agent/categories.d.ts +2 -0
  3. package/dist/agent/categories.js +4 -0
  4. package/dist/agent/child-runner.d.ts +5 -1
  5. package/dist/agent/child-runner.js +35 -2
  6. package/dist/agent/profiles.js +3 -0
  7. package/dist/agent/structured-output.d.ts +37 -0
  8. package/dist/agent/structured-output.js +193 -0
  9. package/dist/agent/subagent-control.d.ts +3 -0
  10. package/dist/agent/subagent-scheduler.d.ts +10 -0
  11. package/dist/agent/subagent-scheduler.js +31 -0
  12. package/dist/agent/workflow/control.d.ts +37 -0
  13. package/dist/agent/workflow/control.js +20 -0
  14. package/dist/agent/workflow/errors.d.ts +16 -0
  15. package/dist/agent/workflow/errors.js +24 -0
  16. package/dist/agent/workflow/runtime.d.ts +75 -0
  17. package/dist/agent/workflow/runtime.js +237 -0
  18. package/dist/agent.d.ts +105 -0
  19. package/dist/agent.js +425 -17
  20. package/dist/context/compact-llm.d.ts +10 -1
  21. package/dist/context/compact-llm.js +13 -5
  22. package/dist/context/compact.d.ts +30 -0
  23. package/dist/context/compact.js +34 -17
  24. package/dist/goal/format.d.ts +1 -1
  25. package/dist/goal/format.js +1 -1
  26. package/dist/network/provider-transport.d.ts +9 -0
  27. package/dist/network/provider-transport.js +19 -1
  28. package/dist/provider.d.ts +14 -0
  29. package/dist/provider.js +24 -0
  30. package/dist/session.d.ts +16 -0
  31. package/dist/session.js +33 -1
  32. package/dist/slash-commands/commands.js +41 -113
  33. package/dist/slash-commands/types.d.ts +14 -9
  34. package/dist/tools/agent-lifecycle.d.ts +6 -0
  35. package/dist/tools/agent-lifecycle.js +285 -0
  36. package/dist/tools/child-tools.d.ts +10 -0
  37. package/dist/tools/child-tools.js +12 -0
  38. package/dist/tools/read.d.ts +1 -1
  39. package/dist/tools/read.js +9 -0
  40. package/dist/tui/image-display.d.ts +6 -0
  41. package/dist/tui/image-display.js +26 -1
  42. package/dist/tui-ink/app.d.ts +0 -18
  43. package/dist/tui-ink/app.js +168 -230
  44. package/dist/tui-ink/compaction-progress.d.ts +19 -0
  45. package/dist/tui-ink/compaction-progress.js +74 -0
  46. package/dist/tui-ink/input-box.d.ts +10 -1
  47. package/dist/tui-ink/input-box.js +56 -16
  48. package/dist/tui-ink/markdown.d.ts +18 -0
  49. package/dist/tui-ink/markdown.js +172 -16
  50. package/dist/tui-ink/message-list.d.ts +1 -2
  51. package/dist/tui-ink/message-list.js +50 -107
  52. package/dist/tui-ink/run.js +5 -0
  53. package/dist/tui-ink/subagent-inspector.d.ts +17 -0
  54. package/dist/tui-ink/subagent-inspector.js +189 -0
  55. package/dist/tui-ink/subagent-view.d.ts +47 -0
  56. package/dist/tui-ink/subagent-view.js +163 -0
  57. package/dist/tui-ink/terminal-env.d.ts +15 -0
  58. package/dist/tui-ink/terminal-env.js +22 -0
  59. package/dist/tui-ink/use-terminal-size.js +33 -6
  60. package/dist/tui-ink/width.d.ts +18 -0
  61. package/dist/tui-ink/width.js +130 -0
  62. package/dist/types.d.ts +35 -0
  63. package/package.json +2 -1
@@ -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;
@@ -0,0 +1,22 @@
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 function isMultiplexedTerminal(env = process.env) {
16
+ if (env.TMUX)
17
+ return true; // inside tmux
18
+ if (env.STY)
19
+ return true; // inside GNU screen
20
+ const term = env.TERM ?? "";
21
+ return /^(screen|tmux)(-|\.|$)/.test(term); // e.g. screen-256color, tmux-256color
22
+ }
@@ -1,5 +1,31 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { useStdout } from "ink";
3
+ const subscribers = new Set();
4
+ let attachedStream = null;
5
+ function notifyAll() {
6
+ for (const cb of subscribers)
7
+ cb();
8
+ }
9
+ function detach() {
10
+ if (!attachedStream)
11
+ return;
12
+ const off = attachedStream.off ?? attachedStream.removeListener;
13
+ off?.call(attachedStream, "resize", notifyAll);
14
+ attachedStream = null;
15
+ }
16
+ function subscribe(stream, cb) {
17
+ subscribers.add(cb);
18
+ if (attachedStream !== stream) {
19
+ detach();
20
+ stream.on("resize", notifyAll);
21
+ attachedStream = stream;
22
+ }
23
+ return () => {
24
+ subscribers.delete(cb);
25
+ if (subscribers.size === 0)
26
+ detach();
27
+ };
28
+ }
3
29
  export function useTerminalSize() {
4
30
  const { stdout } = useStdout();
5
31
  const [size, setSize] = useState(() => ({
@@ -9,13 +35,14 @@ export function useTerminalSize() {
9
35
  useEffect(() => {
10
36
  if (!stdout)
11
37
  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);
38
+ const update = () => {
39
+ setSize((prev) => {
40
+ const next = { columns: stdout.columns || 80, rows: stdout.rows || 24 };
41
+ return prev.columns === next.columns && prev.rows === next.rows ? prev : next;
42
+ });
18
43
  };
44
+ update(); // sync in case the size changed between initial state and mount
45
+ return subscribe(stdout, update);
19
46
  }, [stdout]);
20
47
  return size;
21
48
  }
@@ -0,0 +1,18 @@
1
+ /** Current verdict — true when ambiguous-width chars occupy 2 terminal cells. */
2
+ export declare function ambiguousIsWide(): boolean;
3
+ /** Deterministic override for tests (force CJK-wide / narrow). */
4
+ export declare function setAmbiguousWide(v: boolean): void;
5
+ export declare function visualWidth(str: string): number;
6
+ export declare function graphemeWidth(grapheme: string): number;
7
+ /**
8
+ * Probe the real terminal once at startup, before Ink owns the TTY: print a
9
+ * single ambiguous-width glyph at column 1, ask where the cursor landed via the
10
+ * DSR cursor-position report (`CSI 6n` → `ESC [ row ; col R`), and read back the
11
+ * glyph's rendered width. Width 2 → ambiguous-wide; width 1 → narrow.
12
+ *
13
+ * An explicit env override wins and skips the probe entirely. A non-TTY (pipe,
14
+ * CI) or an unresponsive terminal (the `setTimeout` fires) leaves the locale
15
+ * guess untouched. The probe glyph is erased before returning so the first Ink
16
+ * paint sees a clean line.
17
+ */
18
+ export declare function detectAmbiguousWidth(timeoutMs?: number): Promise<void>;
@@ -0,0 +1,130 @@
1
+ import stringWidth from "string-width";
2
+ /**
3
+ * One verdict for the whole TUI: does THIS terminal render East Asian
4
+ * *Ambiguous*-width characters as 2 cells?
5
+ *
6
+ * Ambiguous-width (Unicode EastAsianWidth=A) covers the curly quotes “ ”, the
7
+ * em dash —, the ● bullet, the ellipsis …, box-drawing ─│┼ and more. `string-
8
+ * width`'s default counts them as 1, but a terminal can render them as 2 — and
9
+ * crucially that choice is a property of the *terminal + font*, NOT the locale:
10
+ * this project's own author hits wide rendering under `LANG=en_US`. When our
11
+ * width math disagrees with the terminal, a line we packed to "exactly fits"
12
+ * overflows and the terminal applies its own hard wrap, dropping the overflow
13
+ * tail onto a stray physical row (the lone "顺" + vertical-gap corruption).
14
+ *
15
+ * So the verdict is resolved, in priority order:
16
+ * 1. explicit env override `BUBBLE_AMBIGUOUS_WIDTH=wide|narrow`,
17
+ * 2. a one-shot CSI 6n cursor probe of the real terminal at startup,
18
+ * 3. a CJK-locale guess as the last resort.
19
+ *
20
+ * EVERYTHING that measures display width for wrapping, cursor mapping, padding,
21
+ * truncation or gutter budgeting must go through `visualWidth`/`graphemeWidth`
22
+ * here, so the entire UI shares the single verdict and stays self-consistent.
23
+ */
24
+ let ambiguousWide = initialGuess();
25
+ function envOverride() {
26
+ const v = process.env.BUBBLE_AMBIGUOUS_WIDTH?.trim().toLowerCase();
27
+ if (!v)
28
+ return undefined;
29
+ if (/^(wide|double|full|2)$/.test(v))
30
+ return true;
31
+ if (/^(narrow|single|half|1)$/.test(v))
32
+ return false;
33
+ return undefined;
34
+ }
35
+ function localeIsCJK() {
36
+ const lang = process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "";
37
+ return /(^|[._-])(zh|ja|ko)/i.test(lang);
38
+ }
39
+ function initialGuess() {
40
+ const override = envOverride();
41
+ return override !== undefined ? override : localeIsCJK();
42
+ }
43
+ /** Current verdict — true when ambiguous-width chars occupy 2 terminal cells. */
44
+ export function ambiguousIsWide() {
45
+ return ambiguousWide;
46
+ }
47
+ /** Deterministic override for tests (force CJK-wide / narrow). */
48
+ export function setAmbiguousWide(v) {
49
+ ambiguousWide = v;
50
+ }
51
+ export function visualWidth(str) {
52
+ if (!str)
53
+ return 0;
54
+ return stringWidth(str, { ambiguousIsNarrow: !ambiguousWide });
55
+ }
56
+ export function graphemeWidth(grapheme) {
57
+ if (!grapheme)
58
+ return 0;
59
+ return stringWidth(grapheme, { ambiguousIsNarrow: !ambiguousWide });
60
+ }
61
+ /**
62
+ * Probe the real terminal once at startup, before Ink owns the TTY: print a
63
+ * single ambiguous-width glyph at column 1, ask where the cursor landed via the
64
+ * DSR cursor-position report (`CSI 6n` → `ESC [ row ; col R`), and read back the
65
+ * glyph's rendered width. Width 2 → ambiguous-wide; width 1 → narrow.
66
+ *
67
+ * An explicit env override wins and skips the probe entirely. A non-TTY (pipe,
68
+ * CI) or an unresponsive terminal (the `setTimeout` fires) leaves the locale
69
+ * guess untouched. The probe glyph is erased before returning so the first Ink
70
+ * paint sees a clean line.
71
+ */
72
+ export async function detectAmbiguousWidth(timeoutMs = 150) {
73
+ if (envOverride() !== undefined)
74
+ return; // explicit choice already applied
75
+ const { stdin, stdout } = process;
76
+ if (!stdout.isTTY || !stdin.isTTY || typeof stdin.setRawMode !== "function")
77
+ return;
78
+ const wasRaw = stdin.isRaw ?? false;
79
+ const wasFlowing = stdin.readableFlowing ?? false;
80
+ const measured = await new Promise((resolve) => {
81
+ let buf = "";
82
+ let settled = false;
83
+ const finish = (result) => {
84
+ if (settled)
85
+ return;
86
+ settled = true;
87
+ clearTimeout(timer);
88
+ stdin.removeListener("data", onData);
89
+ try {
90
+ stdin.setRawMode(wasRaw);
91
+ }
92
+ catch {
93
+ // teardown best-effort
94
+ }
95
+ if (!wasFlowing)
96
+ stdin.pause();
97
+ resolve(result);
98
+ };
99
+ const onData = (chunk) => {
100
+ buf += chunk.toString("latin1");
101
+ const m = /\x1b\[\d+;(\d+)R/.exec(buf);
102
+ if (m)
103
+ finish(Number(m[1]) - 1); // col is 1-based; col-1 = glyph width
104
+ };
105
+ const timer = setTimeout(() => finish(null), timeoutMs);
106
+ try {
107
+ stdin.setRawMode(true);
108
+ stdin.resume();
109
+ stdin.on("data", onData);
110
+ // \r → column 1, print the probe glyph (U+201C “), then request the cursor
111
+ // column. The reported column minus 1 is the glyph's rendered cell width.
112
+ stdout.write("\r“\x1b[6n");
113
+ }
114
+ catch {
115
+ finish(null);
116
+ }
117
+ });
118
+ if (measured === 2)
119
+ ambiguousWide = true;
120
+ else if (measured === 1)
121
+ ambiguousWide = false;
122
+ // measured === null → probe failed; keep the locale-based guess.
123
+ try {
124
+ if (stdout.isTTY)
125
+ stdout.write("\r\x1b[K"); // wipe the probe glyph
126
+ }
127
+ catch {
128
+ // stdout best-effort
129
+ }
130
+ }
package/dist/types.d.ts CHANGED
@@ -182,6 +182,8 @@ export interface ToolContext {
182
182
  profile: import("./agent/profiles.js").AgentProfile;
183
183
  parentToolCallId: string;
184
184
  category?: string;
185
+ model?: string;
186
+ effort?: ThinkingLevel;
185
187
  route?: import("./agent/categories.js").ResolvedSubagentRoute;
186
188
  approval?: "fail" | "disabled";
187
189
  description?: string;
@@ -202,6 +204,8 @@ export interface ToolContext {
202
204
  runAgentTeam?: (cwd: string, options: {
203
205
  profile: import("./agent/profiles.js").AgentProfile;
204
206
  category?: string;
207
+ model?: string;
208
+ effort?: ThinkingLevel;
205
209
  promptTemplate: string;
206
210
  items: string[];
207
211
  parentToolCallId: string;
@@ -209,11 +213,42 @@ export interface ToolContext {
209
213
  abortSignal?: AbortSignal;
210
214
  approval?: "fail" | "disabled";
211
215
  }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
216
+ runAgentBatch?: (cwd: string, options: {
217
+ specs: Array<{
218
+ task: string;
219
+ profile: import("./agent/profiles.js").AgentProfile;
220
+ category?: string;
221
+ model?: string;
222
+ effort?: ThinkingLevel;
223
+ outputSchema?: unknown;
224
+ }>;
225
+ parentToolCallId: string;
226
+ emitUpdate?: (update: ToolUpdate) => void;
227
+ abortSignal?: AbortSignal;
228
+ approval?: "fail" | "disabled";
229
+ }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
230
+ startWorkflow?: (cwd: string, options: {
231
+ script: string;
232
+ args?: unknown;
233
+ title?: string;
234
+ parentToolCallId: string;
235
+ abortSignal?: AbortSignal;
236
+ }) => {
237
+ runId: string;
238
+ title: string;
239
+ };
240
+ waitWorkflow?: (runId: string, timeoutMs?: number) => Promise<import("./agent/workflow/control.js").WorkflowRunSnapshot | undefined>;
212
241
  };
213
242
  emitUpdate?: (update: ToolUpdate) => void;
214
243
  }
215
244
  export interface ToolRegistryEntry extends ToolDefinition {
216
245
  execute: ToolExecutor;
246
+ /**
247
+ * Optional per-child isolation hook: returns a fresh instance with its own
248
+ * mutable state (e.g. a FileStateTracker) so concurrent subagents in a
249
+ * fan-out never share it (design v2 §2). Tools without it are shared as-is.
250
+ */
251
+ cloneForChild?: () => ToolRegistryEntry;
217
252
  /** Optional one-line summary for the Available tools section. */
218
253
  promptSnippet?: string;
219
254
  /** Optional tool-specific rules appended to the system prompt when this tool is active. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,6 +36,7 @@
36
36
  "openai": "^4.77.0",
37
37
  "picomatch": "^4.0.4",
38
38
  "qrcode-terminal": "^0.12.0",
39
+ "quickjs-emscripten": "^0.32.0",
39
40
  "react": "^19.2.6",
40
41
  "shiki": "^4.0.2",
41
42
  "string-width": "^8.2.1",