@bubblebrain-ai/bubble 0.0.21 → 0.0.22

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 (58) hide show
  1. package/dist/agent/abort-errors.d.ts +14 -0
  2. package/dist/agent/abort-errors.js +21 -0
  3. package/dist/agent/budget-ledger.d.ts +41 -0
  4. package/dist/agent/budget-ledger.js +64 -0
  5. package/dist/agent/child-runner.d.ts +55 -0
  6. package/dist/agent/child-runner.js +312 -0
  7. package/dist/agent/profiles.d.ts +8 -0
  8. package/dist/agent/profiles.js +27 -5
  9. package/dist/agent/result-integrator.d.ts +22 -0
  10. package/dist/agent/result-integrator.js +50 -0
  11. package/dist/agent/subagent-control.d.ts +31 -0
  12. package/dist/agent/subagent-control.js +27 -0
  13. package/dist/agent/subagent-lifecycle-reminder.js +11 -2
  14. package/dist/agent/subagent-scheduler.d.ts +95 -0
  15. package/dist/agent/subagent-scheduler.js +256 -0
  16. package/dist/agent/subagent-store.d.ts +41 -0
  17. package/dist/agent/subagent-store.js +149 -0
  18. package/dist/agent/subagent-summary.d.ts +30 -0
  19. package/dist/agent/subagent-summary.js +74 -0
  20. package/dist/agent/worktree.d.ts +29 -0
  21. package/dist/agent/worktree.js +73 -0
  22. package/dist/agent.d.ts +63 -5
  23. package/dist/agent.js +360 -287
  24. package/dist/approval/controller.js +9 -1
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +17 -1
  27. package/dist/config.d.ts +8 -0
  28. package/dist/config.js +17 -0
  29. package/dist/feishu/agent-host/approval-card.js +9 -0
  30. package/dist/feishu/agent-host/run-driver.js +1 -0
  31. package/dist/main.js +34 -0
  32. package/dist/network/errors.d.ts +28 -0
  33. package/dist/network/errors.js +24 -0
  34. package/dist/orchestrator/default-hooks.js +5 -1
  35. package/dist/prompt/compose.js +3 -0
  36. package/dist/prompt/delegation.d.ts +14 -0
  37. package/dist/prompt/delegation.js +64 -0
  38. package/dist/prompt/task-reminders.d.ts +5 -1
  39. package/dist/prompt/task-reminders.js +10 -2
  40. package/dist/provider-anthropic.js +23 -0
  41. package/dist/provider.js +23 -3
  42. package/dist/slash-commands/commands.js +29 -2
  43. package/dist/slash-commands/types.d.ts +2 -0
  44. package/dist/tools/agent-lifecycle.d.ts +29 -3
  45. package/dist/tools/agent-lifecycle.js +394 -40
  46. package/dist/tools/child-tools.d.ts +31 -0
  47. package/dist/tools/child-tools.js +106 -0
  48. package/dist/tools/index.js +1 -1
  49. package/dist/tui/run.d.ts +11 -1
  50. package/dist/tui/run.js +92 -4
  51. package/dist/tui/session-picker-data.d.ts +18 -0
  52. package/dist/tui/session-picker-data.js +21 -0
  53. package/dist/tui/wordmark.d.ts +2 -0
  54. package/dist/tui/wordmark.js +31 -4
  55. package/dist/tui-ink/approval/approval-dialog.js +10 -0
  56. package/dist/tui-opentui/approval/approval-dialog.js +10 -0
  57. package/dist/types.d.ts +17 -0
  58. package/package.json +1 -1
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Per-child tool factory for write_worktree subagents (design doc §8).
3
+ *
4
+ * Parent tools close over the parent cwd at creation, so a write child needs
5
+ * fresh instances bound to its worktree — with their own FileStateTracker —
6
+ * plus a worktree-scoped approval policy: file operations are runtime-checked
7
+ * to stay under the worktree root (the tools' own workspace fence does this
8
+ * structurally), bash auto-approves inside the worktree when the command
9
+ * passes a deny-list of escaping operations, and everything else fails fast.
10
+ */
11
+ import { isAbsolute, resolve, sep } from "node:path";
12
+ import { createBashTool } from "./bash.js";
13
+ import { createEditTool } from "./edit.js";
14
+ import { createGlobTool } from "./glob.js";
15
+ import { createGrepTool } from "./grep.js";
16
+ import { createReadTool } from "./read.js";
17
+ import { createWriteTool } from "./write.js";
18
+ import { FileStateTracker } from "./file-state.js";
19
+ /** Operations a worktree child may never run, regardless of cwd. */
20
+ const WORKTREE_BASH_DENY_PATTERNS = [
21
+ { pattern: /\bgit\s+push\b/, reason: "pushing from a subagent worktree is not allowed; the parent reviews and applies changes." },
22
+ { pattern: /\bgit\s+remote\b/, reason: "remote configuration is not allowed inside a subagent worktree." },
23
+ { pattern: /\bgit\s+worktree\b/, reason: "managing worktrees from inside a subagent worktree is not allowed." },
24
+ { pattern: /\bsudo\b/, reason: "privileged commands are not allowed inside a subagent worktree." },
25
+ ];
26
+ export function isPathInsideWorktree(worktreeRoot, candidate) {
27
+ const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(worktreeRoot, candidate);
28
+ const root = resolve(worktreeRoot);
29
+ return resolved === root || resolved.startsWith(root + sep);
30
+ }
31
+ /**
32
+ * Approval policy for a worktree child: containment is enforced by code
33
+ * (path checks, deny-list), never by prompt text. There is no interactive
34
+ * fallback — anything outside the policy fails fast (design §11).
35
+ */
36
+ export class WorktreeApprovalController {
37
+ worktreeRoot;
38
+ constructor(worktreeRoot) {
39
+ this.worktreeRoot = worktreeRoot;
40
+ }
41
+ async request(req) {
42
+ switch (req.type) {
43
+ case "bash": {
44
+ for (const { pattern, reason } of WORKTREE_BASH_DENY_PATTERNS) {
45
+ if (pattern.test(req.command)) {
46
+ return { action: "reject", feedback: `Blocked by worktree policy: ${reason}` };
47
+ }
48
+ }
49
+ if (!isPathInsideWorktree(this.worktreeRoot, req.cwd)) {
50
+ return { action: "reject", feedback: "Blocked by worktree policy: commands must run inside the subagent worktree." };
51
+ }
52
+ // Absolute paths reaching outside the worktree are an escape attempt.
53
+ const absolutePaths = req.command.match(/(?<=^|[\s"'=])\/[^\s"';|&]+/g) ?? [];
54
+ for (const path of absolutePaths) {
55
+ if (path.startsWith("/dev/") || path.startsWith("/tmp/") || path.startsWith("/usr/") || path.startsWith("/bin/") || path.startsWith("/opt/") || path.startsWith("/etc/"))
56
+ continue;
57
+ if (!isPathInsideWorktree(this.worktreeRoot, path)) {
58
+ return {
59
+ action: "reject",
60
+ feedback: `Blocked by worktree policy: the command references a path outside the worktree (${path}).`,
61
+ };
62
+ }
63
+ }
64
+ return { action: "approve" };
65
+ }
66
+ case "edit":
67
+ case "write":
68
+ return isPathInsideWorktree(this.worktreeRoot, req.path)
69
+ ? { action: "approve" }
70
+ : { action: "reject", feedback: `Blocked by worktree policy: ${req.path} is outside the subagent worktree.` };
71
+ case "patch":
72
+ return req.paths.every((path) => isPathInsideWorktree(this.worktreeRoot, path))
73
+ ? { action: "approve" }
74
+ : { action: "reject", feedback: "Blocked by worktree policy: the patch touches paths outside the subagent worktree." };
75
+ case "lsp":
76
+ return { action: "approve" };
77
+ case "agent_profile":
78
+ return { action: "reject", feedback: "Subagents cannot approve agent profiles." };
79
+ }
80
+ }
81
+ checkRules() {
82
+ return { decision: "ask" };
83
+ }
84
+ }
85
+ const WORKTREE_TOOL_NAMES = new Set(["read", "glob", "grep", "edit", "write", "bash"]);
86
+ /**
87
+ * Builds the write child's toolset bound to its worktree: fresh instances
88
+ * with their own FileStateTracker and the worktree approval policy. A
89
+ * profile's tools list can narrow the set but never widen it.
90
+ */
91
+ export function createWorktreeChildTools(worktreeCwd, include) {
92
+ const approval = new WorktreeApprovalController(worktreeCwd);
93
+ const fileState = new FileStateTracker(worktreeCwd);
94
+ const tools = [
95
+ createReadTool(worktreeCwd, approval, undefined, fileState),
96
+ createGlobTool(worktreeCwd),
97
+ createGrepTool(worktreeCwd),
98
+ createEditTool(worktreeCwd, approval, undefined, fileState),
99
+ createWriteTool(worktreeCwd, {}, approval, undefined, fileState),
100
+ createBashTool(worktreeCwd, approval, fileState),
101
+ ];
102
+ if (!include || include.length === 0)
103
+ return tools;
104
+ const requested = new Set(include.filter((name) => WORKTREE_TOOL_NAMES.has(name)));
105
+ return requested.size > 0 ? tools.filter((tool) => requested.has(tool.name)) : tools;
106
+ }
@@ -57,7 +57,7 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
57
57
  createWebFetchTool(approval),
58
58
  createMemorySearchTool(cwd),
59
59
  createMemoryReadSummaryTool(cwd),
60
- ...createAgentLifecycleTools(),
60
+ ...createAgentLifecycleTools({ cwd, approval }),
61
61
  ...(options.questionController ? [createQuestionTool(options.questionController)] : []),
62
62
  ...(skillRegistry ? [createSkillSearchTool(skillRegistry), createSkillTool(skillRegistry)] : []),
63
63
  ...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
package/dist/tui/run.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type Agent } from "../agent.js";
2
2
  import type { CliArgs } from "../cli.js";
3
3
  import type { ThemeMode } from "../config.js";
4
4
  import type { ExternalHookController } from "../hooks/controller.js";
5
- import type { SessionManager } from "../session.js";
5
+ import { SessionManager } from "../session.js";
6
6
  import type { PlanDecision, Provider } from "../types.js";
7
7
  import type { ProviderRegistry } from "../provider-registry.js";
8
8
  import type { SkillRegistry } from "../skills/registry.js";
@@ -45,5 +45,15 @@ export interface RunTuiOptions {
45
45
  runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
46
46
  /** One-line "update available" notice shown on the home screen, if any. */
47
47
  updateNotice?: string;
48
+ /**
49
+ * Swap the active session in place (driven by the /session picker).
50
+ * Rebinds persistence to the picked session file and replaces the agent's
51
+ * message history; the TUI rebuilds its transcript from the result.
52
+ */
53
+ switchSession?: (sessionFile: string) => {
54
+ manager: SessionManager;
55
+ } | {
56
+ error: string;
57
+ };
48
58
  }
49
59
  export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<void>;
package/dist/tui/run.js CHANGED
@@ -12,6 +12,8 @@ import { debugReasoningStream, summarizeDebugText } from "../reasoning-debug.js"
12
12
  import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
13
13
  import { createStreamingInternalReminderSanitizer, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "../agent/internal-reminder-sanitizer.js";
14
14
  import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceValue, traceEvent, } from "../debug-trace.js";
15
+ import { SessionManager } from "../session.js";
16
+ import { buildSessionPickerEntries, preferredSessionPickerIndex } from "./session-picker-data.js";
15
17
  import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
16
18
  import { calculateUsageCost } from "../model-pricing.js";
17
19
  import { getAvailableThinkingLevels } from "../provider-transform.js";
@@ -1177,7 +1179,13 @@ function OpenTuiApp(props) {
1177
1179
  promptModelLabels.delete(ref);
1178
1180
  };
1179
1181
  const cycleMode = () => {
1180
- if (picker || pendingPlan() || isRunning())
1182
+ // Mode switching is intentionally allowed while the agent is running:
1183
+ // Agent.setMode() is safe mid-run and the approval controller reads the
1184
+ // live mode on every request, so flipping to bypass (or into plan) takes
1185
+ // effect from the very next tool call — no need to wait for the turn to
1186
+ // finish. Only pickers and the plan-approval dialog still block it,
1187
+ // because those surfaces own the keyboard.
1188
+ if (picker || pendingPlan())
1181
1189
  return false;
1182
1190
  const next = getNextPermissionMode(props.agent.mode);
1183
1191
  props.agent.setMode(next);
@@ -3042,7 +3050,10 @@ function OpenTuiApp(props) {
3042
3050
  // "(current)" sits at the bottom of the rewind list and is the safe default.
3043
3051
  : step === "rewind"
3044
3052
  ? Math.max(0, items.length - 1)
3045
- : 0,
3053
+ // Sessions: start on the most recent conversation that is not the active one.
3054
+ : step === "sessions"
3055
+ ? preferredSessionPickerIndex(items)
3056
+ : 0,
3046
3057
  apiKey: "",
3047
3058
  };
3048
3059
  activePrompt()?.clear();
@@ -3077,6 +3088,8 @@ function OpenTuiApp(props) {
3077
3088
  return buildRewindPickerItems();
3078
3089
  if (step === "rewind-action")
3079
3090
  return buildRewindActionItems(providerId);
3091
+ if (step === "sessions")
3092
+ return buildSessionPickerItems();
3080
3093
  if (step === "models") {
3081
3094
  if (providerDialogModelItems?.key === modelPickerCacheKey(providerId)) {
3082
3095
  return providerDialogModelItems.items;
@@ -3319,6 +3332,8 @@ function OpenTuiApp(props) {
3319
3332
  return "Rewind — restore to the point before…";
3320
3333
  if (state.step === "rewind-action")
3321
3334
  return "Rewind — what to restore?";
3335
+ if (state.step === "sessions")
3336
+ return "Resume a session";
3322
3337
  const provider = providerDisplayName(state.providerId);
3323
3338
  if (state.step === "auth")
3324
3339
  return `${provider} auth method`;
@@ -3343,6 +3358,8 @@ function OpenTuiApp(props) {
3343
3358
  return `↑/↓ move · enter continue · esc cancel${count}`;
3344
3359
  if (state.step === "rewind-action")
3345
3360
  return "↑/↓ move · enter confirm · esc back";
3361
+ if (state.step === "sessions")
3362
+ return `↑/↓ move · enter resume · esc close${count}`;
3346
3363
  const escLabel = state.step === "providers" ? "esc close" : "esc back";
3347
3364
  return `↑/↓ move · enter select · ${escLabel}${count}`;
3348
3365
  }
@@ -3360,7 +3377,7 @@ function OpenTuiApp(props) {
3360
3377
  }
3361
3378
  function providerDialogColumnWidths(state, panelWidth) {
3362
3379
  const contentWidth = Math.max(24, panelWidth - PROVIDER_DIALOG_ROW_RESERVED_WIDTH);
3363
- const footer = state.step === "skills" ? 10 : state.step === "providers" ? 9 : 8;
3380
+ const footer = state.step === "skills" || state.step === "sessions" ? 10 : state.step === "providers" ? 9 : 8;
3364
3381
  const minLabel = state.step === "skills" ? 18 : 24;
3365
3382
  const desiredDetail = state.step === "skills"
3366
3383
  ? 30
@@ -3370,7 +3387,9 @@ function OpenTuiApp(props) {
3370
3387
  ? 40
3371
3388
  : state.step === "rewind"
3372
3389
  ? 18
3373
- : 16;
3390
+ : state.step === "sessions"
3391
+ ? 14
3392
+ : 16;
3374
3393
  const detail = Math.max(8, Math.min(desiredDetail, contentWidth - footer - minLabel));
3375
3394
  const label = Math.max(8, contentWidth - detail - footer);
3376
3395
  return { label, detail, footer };
@@ -3579,6 +3598,15 @@ function OpenTuiApp(props) {
3579
3598
  openProviderDialog("rewind-action", item.value);
3580
3599
  return;
3581
3600
  }
3601
+ if (state.step === "sessions") {
3602
+ closeProviderDialog();
3603
+ if (!item.value || item.value === props.options.sessionManager?.getSessionFile()) {
3604
+ // Selecting the active session keeps everything as is.
3605
+ return;
3606
+ }
3607
+ await switchToSession(item.value);
3608
+ return;
3609
+ }
3582
3610
  if (state.step === "rewind-action") {
3583
3611
  closeProviderDialog();
3584
3612
  await executeSlash(item.command);
@@ -4909,6 +4937,9 @@ function OpenTuiApp(props) {
4909
4937
  openRewindPicker: () => {
4910
4938
  openProviderDialog("rewind");
4911
4939
  },
4940
+ openSessionPicker: () => {
4941
+ openProviderDialog("sessions");
4942
+ },
4912
4943
  fillComposer: (text) => {
4913
4944
  resetPromptHistoryBrowse();
4914
4945
  setPromptText(text);
@@ -5256,6 +5287,44 @@ function OpenTuiApp(props) {
5256
5287
  items.push({ label: "(current)", value: "", command: "" });
5257
5288
  return items;
5258
5289
  }
5290
+ function buildSessionPickerItems() {
5291
+ const activeFile = props.options.sessionManager?.getSessionFile();
5292
+ const summaries = SessionManager.summarizeSessionsForCwd(props.args.cwd);
5293
+ return buildSessionPickerEntries(summaries, activeFile).map((entry) => ({
5294
+ label: entry.label,
5295
+ detail: entry.detail,
5296
+ value: entry.value,
5297
+ command: "",
5298
+ footer: entry.footer,
5299
+ gutter: entry.gutter,
5300
+ }));
5301
+ }
5302
+ async function switchToSession(sessionFile) {
5303
+ const switchSession = props.options.switchSession;
5304
+ if (!switchSession) {
5305
+ addMessage("error", "Session switching is not available in this mode.");
5306
+ return;
5307
+ }
5308
+ if (isRunning()) {
5309
+ setNotice("Stop the current run before switching sessions.");
5310
+ return;
5311
+ }
5312
+ const result = switchSession(sessionFile);
5313
+ if ("error" in result) {
5314
+ addMessage("error", `Failed to switch session: ${result.error}`);
5315
+ return;
5316
+ }
5317
+ props.options.sessionManager = result.manager;
5318
+ // Same rebuild path as /rewind: the agent history was replaced wholesale,
5319
+ // so reconstruct the transcript from it instead of patching the display.
5320
+ displayMessages = reconstructDisplayMessages(props.agent.messages);
5321
+ streamingDisplay = undefined;
5322
+ redrawTranscript(undefined, displayMessages);
5323
+ syncTodosFromAgent();
5324
+ bumpSidebar();
5325
+ syncPromptSurfaces(true);
5326
+ addMessage("assistant", `⤷ Resumed session: ${sessionDisplayName(result.manager)}`);
5327
+ }
5259
5328
  function buildRewindActionItems(turnNumber) {
5260
5329
  if (!turnNumber)
5261
5330
  return [];
@@ -9340,6 +9409,17 @@ function getApprovalPanelMeta(request) {
9340
9409
  path: request.path,
9341
9410
  };
9342
9411
  }
9412
+ if (request.type === "agent_profile") {
9413
+ return {
9414
+ icon: "@",
9415
+ title: `Trust project agent profile "${request.name}"`,
9416
+ subtitle: "from .bubble/agents — its prompt will drive a subagent",
9417
+ preview: `${shortCwd(request.path)}\n${request.promptPreview}`,
9418
+ previewHeight: 8,
9419
+ previewColor: theme.toolText,
9420
+ path: request.path,
9421
+ };
9422
+ }
9343
9423
  const path = shortCwd(request.path);
9344
9424
  if (request.type === "edit") {
9345
9425
  return {
@@ -9454,6 +9534,8 @@ function displayToolName(name) {
9454
9534
  wait_agent: "WaitAgent",
9455
9535
  send_input: "SendInput",
9456
9536
  close_agent: "CloseAgent",
9537
+ list_agents: "ListAgents",
9538
+ agent_team: "AgentTeam",
9457
9539
  task: "Task",
9458
9540
  todo: "Todo",
9459
9541
  question: "Questions",
@@ -9476,6 +9558,12 @@ function toolHeader(tool) {
9476
9558
  const agentId = args.agent_id ?? (Array.isArray(args.agent_ids) ? `${args.agent_ids.length} agents` : undefined);
9477
9559
  return agentId ? `(${truncate(String(agentId), 64)})` : "";
9478
9560
  }
9561
+ if (tool.name === "agent_team") {
9562
+ const items = Array.isArray(args.items) ? `${args.items.length} items` : "";
9563
+ const description = typeof args.description === "string" ? args.description : "";
9564
+ const label = [description, items].filter(Boolean).join(", ");
9565
+ return label ? `(${truncate(label, 64)})` : "";
9566
+ }
9479
9567
  const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query ?? toolPath(tool);
9480
9568
  return value ? `(${truncate(String(value).replace(/\n/g, " "), 64)})` : "";
9481
9569
  }
@@ -0,0 +1,18 @@
1
+ import type { SessionSummary } from "../session.js";
2
+ export interface SessionPickerEntry {
3
+ /** Session title (or first-message preview when untitled). */
4
+ label: string;
5
+ /** Message count, e.g. "12 messages". */
6
+ detail: string;
7
+ /** Absolute path to the session .jsonl file. */
8
+ value: string;
9
+ /** "current" for the active session, otherwise a relative timestamp. */
10
+ footer: string;
11
+ /** "●" marks the active session. */
12
+ gutter?: string;
13
+ }
14
+ export declare function buildSessionPickerEntries(summaries: SessionSummary[], activeFile: string | undefined, now?: number): SessionPickerEntry[];
15
+ /** Default selection: the most recent session that is not the active one. */
16
+ export declare function preferredSessionPickerIndex(entries: Array<{
17
+ gutter?: string;
18
+ }>): number;
@@ -0,0 +1,21 @@
1
+ import { normalizeSingleLine, truncateVisual } from "../text-display.js";
2
+ import { formatRelativeTime } from "./recent-activity.js";
3
+ const SESSION_PICKER_LABEL_MAX_WIDTH = 72;
4
+ export function buildSessionPickerEntries(summaries, activeFile, now = Date.now()) {
5
+ return summaries.map((summary) => {
6
+ const isCurrent = summary.file === activeFile;
7
+ const label = truncateVisual(normalizeSingleLine(summary.title || summary.preview || summary.name), SESSION_PICKER_LABEL_MAX_WIDTH) || summary.name;
8
+ return {
9
+ label,
10
+ detail: `${summary.messageCount} message${summary.messageCount === 1 ? "" : "s"}`,
11
+ value: summary.file,
12
+ footer: isCurrent ? "current" : formatRelativeTime(summary.mtime, now),
13
+ gutter: isCurrent ? "●" : undefined,
14
+ };
15
+ });
16
+ }
17
+ /** Default selection: the most recent session that is not the active one. */
18
+ export function preferredSessionPickerIndex(entries) {
19
+ const firstOther = entries.findIndex((entry) => entry.gutter !== "●");
20
+ return firstOther >= 0 ? firstOther : 0;
21
+ }
@@ -8,6 +8,8 @@ export interface BubbleWordmarkLine {
8
8
  tone?: BubbleWordmarkTone;
9
9
  segments?: BubbleWordmarkSegment[];
10
10
  }
11
+ export declare const BUBBLE_CAT: BubbleWordmarkLine[];
12
+ export declare const BUBBLE_CAT_LARGE: BubbleWordmarkLine[];
11
13
  export declare const BUBBLE_WORDMARK: BubbleWordmarkLine[];
12
14
  export declare const BUBBLE_WORDMARK_LARGE: BubbleWordmarkLine[];
13
15
  export declare const BUBBLE_COMPACT_WORDMARK: BubbleWordmarkLine[];
@@ -22,6 +22,27 @@ const LOWER_B = {
22
22
  " ",
23
23
  ],
24
24
  };
25
+ // Pixel cat mascot, drawn on the same half-block pixel grid as the letters:
26
+ // pointy ears, 2x2-pixel eyes, tiny mouth, round chin (10x14 pixels). It is
27
+ // stacked above the wordmark (icon-over-name lockup) rather than inlined, so
28
+ // its solid fill doesn't compete with the thin letter strokes.
29
+ const CAT_LINES = [
30
+ " █▄ ▄█ ",
31
+ " ███▄▄███ ",
32
+ "██████████",
33
+ "█ ████ █",
34
+ "████▀▀████",
35
+ "██████████",
36
+ " ▀██████▀ ",
37
+ ];
38
+ export const BUBBLE_CAT = CAT_LINES.map((text) => ({
39
+ text,
40
+ tone: "brand",
41
+ }));
42
+ export const BUBBLE_CAT_LARGE = CAT_LINES.map((text) => ({
43
+ text: text.split("").map((ch) => ch + ch).join(""),
44
+ tone: "brand",
45
+ }));
25
46
  const GLYPHS = {
26
47
  u: {
27
48
  tone: "ink",
@@ -172,10 +193,16 @@ export function bubbleWordmarkLineText(line) {
172
193
  export function bubbleWordmarkMaxWidth(lines = BUBBLE_WORDMARK) {
173
194
  return Math.max(...lines.map((line) => bubbleWordmarkLineText(line).length));
174
195
  }
196
+ const LOGO_GAP = { text: "", tone: "caption" };
197
+ // Icon-over-name lockup: pixel cat centered above the wordmark. Both render
198
+ // sites center every line independently, which is what stacks the cat over
199
+ // the text without any per-line padding here.
175
200
  export function bubbleWordmarkForWidth(width) {
176
- if (width >= bubbleWordmarkMaxWidth(BUBBLE_WORDMARK_LARGE) + 4)
177
- return BUBBLE_WORDMARK_LARGE;
178
- if (width >= bubbleWordmarkMaxWidth() + 4)
179
- return BUBBLE_WORDMARK;
201
+ if (width >= bubbleWordmarkMaxWidth(BUBBLE_WORDMARK_LARGE) + 4) {
202
+ return [...BUBBLE_CAT_LARGE, LOGO_GAP, ...BUBBLE_WORDMARK_LARGE];
203
+ }
204
+ if (width >= bubbleWordmarkMaxWidth() + 4) {
205
+ return [...BUBBLE_CAT, LOGO_GAP, ...BUBBLE_WORDMARK];
206
+ }
180
207
  return BUBBLE_COMPACT_WORDMARK;
181
208
  }
@@ -77,6 +77,8 @@ function dialogTitle(req) {
77
77
  return "Bash command";
78
78
  case "lsp":
79
79
  return "Language server operation";
80
+ case "agent_profile":
81
+ return "Project agent profile";
80
82
  }
81
83
  }
82
84
  function dialogQuestion(req) {
@@ -91,6 +93,8 @@ function dialogQuestion(req) {
91
93
  return "Do you want to proceed?";
92
94
  case "lsp":
93
95
  return `Do you want to run ${req.operation} on ${basename(req.path)}?`;
96
+ case "agent_profile":
97
+ return `Trust the repository profile "${req.name}" to drive a subagent? It is remembered for this session until the file changes.`;
94
98
  }
95
99
  }
96
100
  function basename(p) {
@@ -107,8 +111,14 @@ function RequestPreview({ request }) {
107
111
  return _jsx(DiffView, { diff: request.diff });
108
112
  case "write":
109
113
  return _jsx(WritePreview, { path: request.path, content: request.content });
114
+ case "agent_profile":
115
+ return _jsx(AgentProfilePreview, { path: request.path, promptPreview: request.promptPreview });
110
116
  }
111
117
  }
118
+ function AgentProfilePreview({ path, promptPreview }) {
119
+ const theme = useTheme();
120
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: compressHome(path) }), _jsx(Text, { children: promptPreview }), _jsx(Text, { color: theme.warning, children: "This prompt comes from the repository's .bubble/agents and will drive a subagent." })] }));
121
+ }
112
122
  function BashPreview({ command, cwd }) {
113
123
  const theme = useTheme();
114
124
  const danger = classifyBashDanger(command);
@@ -84,6 +84,8 @@ function dialogTitle(req) {
84
84
  return "Bash command";
85
85
  case "lsp":
86
86
  return "Language server operation";
87
+ case "agent_profile":
88
+ return "Project agent profile";
87
89
  }
88
90
  }
89
91
  function dialogQuestion(req) {
@@ -98,6 +100,8 @@ function dialogQuestion(req) {
98
100
  return "Do you want to proceed?";
99
101
  case "lsp":
100
102
  return `Do you want to run ${req.operation} on ${basename(req.path)}?`;
103
+ case "agent_profile":
104
+ return `Trust the repository profile "${req.name}" to drive a subagent? It is remembered for this session until the file changes.`;
101
105
  }
102
106
  }
103
107
  function basename(p) {
@@ -114,8 +118,14 @@ function RequestPreview({ request }) {
114
118
  return _jsx(DiffView, { diff: request.diff });
115
119
  case "write":
116
120
  return _jsx(WritePreview, { path: request.path, content: request.content });
121
+ case "agent_profile":
122
+ return _jsx(AgentProfilePreview, { path: request.path, promptPreview: request.promptPreview });
117
123
  }
118
124
  }
125
+ function AgentProfilePreview({ path, promptPreview }) {
126
+ const theme = useTheme();
127
+ return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsx("text", { fg: theme.muted, children: compressHome(path) }), _jsx("text", { children: promptPreview }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: theme.warning, children: "This prompt comes from the repository's .bubble/agents and will drive a subagent." }) })] }));
128
+ }
119
129
  function BashPreview({ command, cwd }) {
120
130
  const theme = useTheme();
121
131
  const danger = classifyBashDanger(command);
package/dist/types.d.ts CHANGED
@@ -199,6 +199,16 @@ export interface ToolContext {
199
199
  }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
200
200
  closeSubAgent?: (agentId: string) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
201
201
  listSubAgents?: () => import("./agent/subagent-control.js").SubagentThreadSnapshot[];
202
+ runAgentTeam?: (cwd: string, options: {
203
+ profile: import("./agent/profiles.js").AgentProfile;
204
+ category?: string;
205
+ promptTemplate: string;
206
+ items: string[];
207
+ parentToolCallId: string;
208
+ emitUpdate?: (update: ToolUpdate) => void;
209
+ abortSignal?: AbortSignal;
210
+ approval?: "fail" | "disabled";
211
+ }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
202
212
  };
203
213
  emitUpdate?: (update: ToolUpdate) => void;
204
214
  }
@@ -291,6 +301,13 @@ export interface Provider {
291
301
  temperature?: number;
292
302
  thinkingLevel?: ThinkingLevel;
293
303
  abortSignal?: AbortSignal;
304
+ /**
305
+ * How the transport treats HTTP 429 (design doc §4.5). "handle"
306
+ * (default): retry inside the transport. "defer": throw a typed
307
+ * RateLimitError immediately so the caller owns the backoff — used by
308
+ * subagent routes where the scheduler is the single 429 backoff layer.
309
+ */
310
+ rateLimitPolicy?: import("./network/errors.js").RateLimitPolicy;
294
311
  }): AsyncIterable<StreamChunk>;
295
312
  complete(messages: ProviderMessage[], options?: {
296
313
  model?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {