@bubblebrain-ai/bubble 0.0.34 → 0.0.36

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 (168) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/abort-errors.d.ts +2 -2
  3. package/dist/agent/budget-ledger.d.ts +4 -45
  4. package/dist/agent/budget-ledger.js +5 -75
  5. package/dist/agent/categories.d.ts +66 -2
  6. package/dist/agent/categories.js +120 -19
  7. package/dist/agent/child-runner.d.ts +1 -3
  8. package/dist/agent/child-runner.js +6 -37
  9. package/dist/agent/discovery-barrier.js +1 -1
  10. package/dist/agent/profiles.d.ts +1 -2
  11. package/dist/agent/profiles.js +25 -14
  12. package/dist/agent/routing-catalog.d.ts +113 -0
  13. package/dist/agent/routing-catalog.js +240 -0
  14. package/dist/agent/subagent-control.d.ts +5 -12
  15. package/dist/agent/subagent-control.js +0 -2
  16. package/dist/agent/subagent-store.d.ts +1 -1
  17. package/dist/agent/subagent-store.js +1 -3
  18. package/dist/agent/workflow/control.d.ts +21 -0
  19. package/dist/agent/workflow/control.js +50 -8
  20. package/dist/agent/workflow/runtime.d.ts +15 -2
  21. package/dist/agent/workflow/runtime.js +112 -30
  22. package/dist/agent.d.ts +82 -48
  23. package/dist/agent.js +360 -232
  24. package/dist/approval/controller.js +28 -0
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +21 -1
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +2 -1
  29. package/dist/config.d.ts +5 -3
  30. package/dist/config.js +10 -5
  31. package/dist/context/compact-llm.d.ts +2 -2
  32. package/dist/external-runtime/grok-acp.d.ts +56 -0
  33. package/dist/external-runtime/grok-acp.js +386 -0
  34. package/dist/external-runtime/grok-binary.d.ts +25 -0
  35. package/dist/external-runtime/grok-binary.js +108 -0
  36. package/dist/external-runtime/grok-errors.d.ts +7 -0
  37. package/dist/external-runtime/grok-errors.js +15 -0
  38. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  39. package/dist/external-runtime/grok-input-policy.js +52 -0
  40. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  41. package/dist/external-runtime/grok-ndjson.js +193 -0
  42. package/dist/external-runtime/grok-network.d.ts +15 -0
  43. package/dist/external-runtime/grok-network.js +81 -0
  44. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  45. package/dist/external-runtime/grok-preflight.js +194 -0
  46. package/dist/external-runtime/grok-process.d.ts +26 -0
  47. package/dist/external-runtime/grok-process.js +184 -0
  48. package/dist/external-runtime/grok-profile.d.ts +32 -0
  49. package/dist/external-runtime/grok-profile.js +419 -0
  50. package/dist/external-runtime/grok-provider.d.ts +31 -0
  51. package/dist/external-runtime/grok-provider.js +41 -0
  52. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  53. package/dist/external-runtime/grok-runtime.js +882 -0
  54. package/dist/external-runtime/index.d.ts +13 -0
  55. package/dist/external-runtime/index.js +13 -0
  56. package/dist/external-runtime/session-policy.d.ts +11 -0
  57. package/dist/external-runtime/session-policy.js +30 -0
  58. package/dist/external-runtime/types.d.ts +65 -0
  59. package/dist/external-runtime/types.js +1 -0
  60. package/dist/feishu/agent-host/approval-card.js +21 -0
  61. package/dist/feishu/agent-host/run-driver.js +19 -3
  62. package/dist/feishu/serve.js +1 -0
  63. package/dist/goal/tools.d.ts +6 -3
  64. package/dist/goal/tools.js +7 -19
  65. package/dist/main.js +114 -28
  66. package/dist/memory/db.d.ts +2 -0
  67. package/dist/memory/db.js +10 -0
  68. package/dist/memory/index.d.ts +1 -0
  69. package/dist/memory/index.js +1 -0
  70. package/dist/memory/phase1.js +14 -2
  71. package/dist/memory/phase2.js +21 -2
  72. package/dist/memory/prompts.js +1 -1
  73. package/dist/memory/session-policy.d.ts +25 -0
  74. package/dist/memory/session-policy.js +55 -0
  75. package/dist/memory/storage.d.ts +1 -0
  76. package/dist/memory/storage.js +13 -0
  77. package/dist/model-catalog.d.ts +18 -0
  78. package/dist/model-catalog.js +92 -52
  79. package/dist/model-config.d.ts +3 -1
  80. package/dist/model-config.js +11 -0
  81. package/dist/oauth/grok.d.ts +25 -0
  82. package/dist/oauth/grok.js +218 -0
  83. package/dist/oauth/index.d.ts +1 -0
  84. package/dist/oauth/index.js +1 -0
  85. package/dist/oauth/storage.d.ts +9 -0
  86. package/dist/oauth/storage.js +25 -0
  87. package/dist/orchestrator/default-hooks.js +23 -7
  88. package/dist/prompt/compose.d.ts +3 -0
  89. package/dist/prompt/compose.js +3 -0
  90. package/dist/prompt/delegation.d.ts +1 -1
  91. package/dist/prompt/delegation.js +41 -8
  92. package/dist/prompt/environment.js +0 -4
  93. package/dist/prompt/reminders.d.ts +0 -1
  94. package/dist/prompt/reminders.js +1 -11
  95. package/dist/prompt/routing.d.ts +14 -0
  96. package/dist/prompt/routing.js +71 -0
  97. package/dist/prompt/runtime.js +1 -1
  98. package/dist/prompt/task-reminders.d.ts +20 -0
  99. package/dist/prompt/task-reminders.js +73 -1
  100. package/dist/provider-ai-sdk.js +2 -2
  101. package/dist/provider-grok.d.ts +26 -0
  102. package/dist/provider-grok.js +59 -0
  103. package/dist/provider-openai-codex.d.ts +17 -1
  104. package/dist/provider-openai-codex.js +101 -29
  105. package/dist/provider-registry.d.ts +72 -0
  106. package/dist/provider-registry.js +336 -59
  107. package/dist/provider-transform.d.ts +1 -1
  108. package/dist/provider-transform.js +35 -17
  109. package/dist/provider.d.ts +3 -0
  110. package/dist/provider.js +14 -0
  111. package/dist/session-types.d.ts +19 -1
  112. package/dist/session.d.ts +1 -0
  113. package/dist/session.js +4 -0
  114. package/dist/slash-commands/commands.js +224 -14
  115. package/dist/slash-commands/types.d.ts +14 -1
  116. package/dist/system-prompt.d.ts +2 -0
  117. package/dist/tools/agent-lifecycle.d.ts +1 -9
  118. package/dist/tools/agent-lifecycle.js +42 -272
  119. package/dist/tools/bash.js +1 -1
  120. package/dist/tools/child-tools.js +2 -0
  121. package/dist/tools/index.d.ts +2 -2
  122. package/dist/tools/index.js +7 -5
  123. package/dist/tools/lsp.js +1 -0
  124. package/dist/tools/memory.d.ts +7 -2
  125. package/dist/tools/memory.js +28 -43
  126. package/dist/tools/server.js +4 -0
  127. package/dist/tools/tool-search.d.ts +1 -1
  128. package/dist/tools/tool-search.js +23 -3
  129. package/dist/tui/model-switch.d.ts +7 -0
  130. package/dist/tui/model-switch.js +15 -5
  131. package/dist/tui-ink/app.d.ts +3 -1
  132. package/dist/tui-ink/app.js +672 -243
  133. package/dist/tui-ink/approval/approval-dialog.js +37 -1
  134. package/dist/tui-ink/approval/diff-view.js +4 -4
  135. package/dist/tui-ink/approval/select.js +2 -2
  136. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  137. package/dist/tui-ink/composer-buffer.js +317 -0
  138. package/dist/tui-ink/feedback-dialog.js +2 -2
  139. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  140. package/dist/tui-ink/footer.d.ts +1 -0
  141. package/dist/tui-ink/footer.js +7 -2
  142. package/dist/tui-ink/input-box.d.ts +36 -7
  143. package/dist/tui-ink/input-box.js +229 -157
  144. package/dist/tui-ink/markdown.js +11 -9
  145. package/dist/tui-ink/message-list.d.ts +5 -1
  146. package/dist/tui-ink/message-list.js +15 -2
  147. package/dist/tui-ink/model-picker.d.ts +61 -4
  148. package/dist/tui-ink/model-picker.js +464 -128
  149. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  150. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  151. package/dist/tui-ink/plan-confirm.js +1 -1
  152. package/dist/tui-ink/run.d.ts +3 -0
  153. package/dist/tui-ink/run.js +1 -1
  154. package/dist/tui-ink/subagent-view.d.ts +34 -1
  155. package/dist/tui-ink/subagent-view.js +82 -1
  156. package/dist/tui-ink/theme.d.ts +19 -2
  157. package/dist/tui-ink/theme.js +25 -8
  158. package/dist/tui-ink/welcome.d.ts +3 -2
  159. package/dist/tui-ink/welcome.js +10 -6
  160. package/dist/types.d.ts +8 -31
  161. package/dist/types.js +1 -1
  162. package/dist/variant/thinking-level.d.ts +2 -2
  163. package/dist/variant/thinking-level.js +2 -1
  164. package/dist/variant/variant-resolver.d.ts +22 -0
  165. package/dist/variant/variant-resolver.js +43 -0
  166. package/package.json +6 -2
  167. package/dist/tools/task.d.ts +0 -2
  168. package/dist/tools/task.js +0 -58
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Tracks overlapping paste operations by identity. Finishing an older paste
3
+ * must never clear the pending state of a newer paste.
4
+ */
5
+ export declare class PasteOperationTracker {
6
+ private nextId;
7
+ private readonly active;
8
+ begin(): number;
9
+ finish(id: number): void;
10
+ invalidateAll(): void;
11
+ get hasPending(): boolean;
12
+ get pendingCount(): number;
13
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Tracks overlapping paste operations by identity. Finishing an older paste
3
+ * must never clear the pending state of a newer paste.
4
+ */
5
+ export class PasteOperationTracker {
6
+ nextId = 1;
7
+ active = new Set();
8
+ begin() {
9
+ const id = this.nextId++;
10
+ this.active.add(id);
11
+ return id;
12
+ }
13
+ finish(id) {
14
+ this.active.delete(id);
15
+ }
16
+ invalidateAll() {
17
+ this.active.clear();
18
+ }
19
+ get hasPending() {
20
+ return this.active.size > 0;
21
+ }
22
+ get pendingCount() {
23
+ return this.active.size;
24
+ }
25
+ }
@@ -110,6 +110,6 @@ export function PlanConfirm({ initialPlan, onApprove, onReject }) {
110
110
  return (_jsx(Text, { children: line || " " }, index));
111
111
  }
112
112
  const safe = line || " ";
113
- return (_jsxs(Box, { children: [_jsx(Text, { children: safe.slice(0, cursorCol) }), _jsx(Text, { backgroundColor: "white", color: "black", children: safe[cursorCol] || " " }), _jsx(Text, { children: safe.slice(cursorCol + 1) })] }, index));
113
+ return (_jsxs(Box, { children: [_jsx(Text, { children: safe.slice(0, cursorCol) }), _jsx(Text, { inverse: true, children: safe[cursorCol] || " " }), _jsx(Text, { children: safe.slice(cursorCol + 1) })] }, index));
114
114
  }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u2303S" }), " save & approve \u00A0\u00A0", _jsx(Text, { color: theme.accent, bold: true, children: "esc" }), " cancel edit"] }) })] }));
115
115
  }
@@ -14,6 +14,7 @@ import type { QuestionController } from "../question/index.js";
14
14
  import type { MemoryScope } from "../memory/index.js";
15
15
  import type { ExternalHookController } from "../hooks/controller.js";
16
16
  import type { ResolvedTheme, ThemeMode } from "./theme.js";
17
+ import type { ExternalRuntimeManager } from "../external-runtime/types.js";
17
18
  export interface RunTuiOptions {
18
19
  sessionManager?: SessionManager;
19
20
  createProvider?: (providerId: string, apiKey: string, baseURL: string) => Provider;
@@ -48,6 +49,8 @@ export interface RunTuiOptions {
48
49
  updateNoticeRefresh?: Promise<string | null>;
49
50
  /** External lifecycle hooks, threaded into slash-command execution. */
50
51
  hookController?: ExternalHookController;
52
+ /** Subscription-backed external agent runtime used by the interactive TUI. */
53
+ externalRuntime?: ExternalRuntimeManager;
51
54
  }
52
55
  export declare function createInkAppElement(agent: Agent, args: CliArgs, options: RunTuiOptions, onExit: (summary: ExitSummary) => void): React.ReactElement;
53
56
  export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<ExitSummary | undefined>;
@@ -3,7 +3,7 @@ import { render } from "ink";
3
3
  import { App } from "./app.js";
4
4
  import { warmHighlighter } from "./code-highlight.js";
5
5
  export function createInkAppElement(agent, args, options, onExit) {
6
- return (_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, switchSession: options.switchSession, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, goalStore: options.goalStore, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, updateNotice: options.updateNotice, updateNoticeRefresh: options.updateNoticeRefresh, hookController: options.hookController, onExit: onExit }));
6
+ return (_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, switchSession: options.switchSession, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, goalStore: options.goalStore, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, updateNotice: options.updateNotice, updateNoticeRefresh: options.updateNoticeRefresh, hookController: options.hookController, externalRuntime: options.externalRuntime, onExit: onExit }));
7
7
  }
8
8
  /**
9
9
  * Best-effort terminal restore for abnormal exits. Bubble renders into the
@@ -6,6 +6,7 @@
6
6
  import { type SubagentRouteLike } from "../agent/subagent-route-format.js";
7
7
  import type { Theme } from "./theme.js";
8
8
  import type { DisplayMessage, DisplayToolCall } from "./display-history.js";
9
+ import type { ToolResultMetadata } from "../types.js";
9
10
  export interface SubagentDisplay {
10
11
  subAgentId?: string;
11
12
  agentName?: string;
@@ -26,13 +27,45 @@ export declare function subagentDescriptor(subagent: SubagentDisplay, includeThi
26
27
  export declare function subagentStatusColor(status: string | undefined, theme: Theme): string;
27
28
  export declare function subagentSummary(subagents: SubagentDisplay[]): string;
28
29
  export declare function sortSubagents(subagents: SubagentDisplay[]): SubagentDisplay[];
29
- /** A spawn_agent (one member), an agent_team/agent_batch, or a run_workflow (a group of members). */
30
+ /** A spawn_agent (one member) or a run_workflow (a group of members). The
31
+ * "team"/"batch" kinds survive only to render transcripts recorded before
32
+ * those tools were removed (2026-07-06). */
30
33
  export interface SubagentGroup {
31
34
  id: string;
32
35
  kind: "single" | "team" | "batch" | "workflow";
33
36
  label: string;
34
37
  members: SubagentDisplay[];
35
38
  }
39
+ /**
40
+ * Merges subagent tool metadata, deduping member snapshots by subAgentId so a
41
+ * stream of per-child updates accumulates into one member list instead of each
42
+ * update replacing the last.
43
+ */
44
+ export declare function mergeToolMetadata(current: ToolResultMetadata | undefined, incoming: ToolResultMetadata | undefined): ToolResultMetadata | undefined;
45
+ /**
46
+ * Drops accumulator entries whose every member reached a final status: by then
47
+ * the authoritative snapshot lives in the settled transcript (wait_workflow /
48
+ * wait_agent result), so the entry is dead weight that would otherwise pile up
49
+ * for the life of the process. Entries with running/queued members stay — for
50
+ * a workflow spanning turns they are the only live view. Returns whether the
51
+ * map changed (callers bump their version counter on true).
52
+ */
53
+ export declare function pruneSettledLiveSubagentTools(map: Map<string, DisplayToolCall>): boolean;
54
+ /**
55
+ * Accumulates a subagent tool_update whose originating tool call has already
56
+ * settled out of the current streaming round. The TUI clears its streaming
57
+ * toolCalls on every turn_start, but background children keep reporting
58
+ * against the launching run_workflow/spawn_agent call id across later rounds
59
+ * (e.g. while a wait_workflow blocks) — without this side-channel those
60
+ * updates are dropped and traces only appear after the whole team finishes.
61
+ * Entries act as synthetic tool calls feeding collectSubagentGroups.
62
+ * Returns true when the update was absorbed.
63
+ */
64
+ export declare function accumulateLiveSubagentUpdate(map: Map<string, DisplayToolCall>, event: {
65
+ id: string;
66
+ name: string;
67
+ metadata?: ToolResultMetadata;
68
+ }): boolean;
36
69
  /**
37
70
  * Collects every spawned subagent from the live transcript + streaming tools,
38
71
  * grouped by their originating tool call, for the inspector. Pure.
@@ -75,6 +75,84 @@ function subagentFreshness(member) {
75
75
  function memberKey(member) {
76
76
  return member.subAgentId || `${member.nickname ?? ""}|${member.task ?? ""}`;
77
77
  }
78
+ /**
79
+ * Merges subagent tool metadata, deduping member snapshots by subAgentId so a
80
+ * stream of per-child updates accumulates into one member list instead of each
81
+ * update replacing the last.
82
+ */
83
+ export function mergeToolMetadata(current, incoming) {
84
+ if (!incoming)
85
+ return current;
86
+ if (current?.kind !== "subagent" || incoming.kind !== "subagent") {
87
+ return incoming;
88
+ }
89
+ const currentSubagents = Array.isArray(current.subagents) ? current.subagents : [];
90
+ const incomingSubagents = Array.isArray(incoming.subagents) ? incoming.subagents : [];
91
+ const byId = new Map();
92
+ for (const item of currentSubagents) {
93
+ const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
94
+ ? String(item.subAgentId)
95
+ : "";
96
+ byId.set(subAgentId || `current:${byId.size}`, item);
97
+ }
98
+ for (const item of incomingSubagents) {
99
+ const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
100
+ ? String(item.subAgentId)
101
+ : "";
102
+ byId.set(subAgentId || `incoming:${byId.size}`, item);
103
+ }
104
+ return {
105
+ ...current,
106
+ ...incoming,
107
+ subagents: [...byId.values()],
108
+ };
109
+ }
110
+ const FINAL_MEMBER_STATUSES = new Set(["completed", "failed", "blocked", "cancelled", "closed"]);
111
+ /**
112
+ * Drops accumulator entries whose every member reached a final status: by then
113
+ * the authoritative snapshot lives in the settled transcript (wait_workflow /
114
+ * wait_agent result), so the entry is dead weight that would otherwise pile up
115
+ * for the life of the process. Entries with running/queued members stay — for
116
+ * a workflow spanning turns they are the only live view. Returns whether the
117
+ * map changed (callers bump their version counter on true).
118
+ */
119
+ export function pruneSettledLiveSubagentTools(map) {
120
+ let changed = false;
121
+ for (const [id, tc] of map) {
122
+ const members = Array.isArray(tc.metadata?.subagents) ? tc.metadata.subagents : [];
123
+ const allFinal = members.every((member) => typeof member === "object" && member !== null
124
+ && FINAL_MEMBER_STATUSES.has(String(member.status)));
125
+ if (allFinal) {
126
+ map.delete(id);
127
+ changed = true;
128
+ }
129
+ }
130
+ return changed;
131
+ }
132
+ /**
133
+ * Accumulates a subagent tool_update whose originating tool call has already
134
+ * settled out of the current streaming round. The TUI clears its streaming
135
+ * toolCalls on every turn_start, but background children keep reporting
136
+ * against the launching run_workflow/spawn_agent call id across later rounds
137
+ * (e.g. while a wait_workflow blocks) — without this side-channel those
138
+ * updates are dropped and traces only appear after the whole team finishes.
139
+ * Entries act as synthetic tool calls feeding collectSubagentGroups.
140
+ * Returns true when the update was absorbed.
141
+ */
142
+ export function accumulateLiveSubagentUpdate(map, event) {
143
+ if (event.metadata?.kind !== "subagent")
144
+ return false;
145
+ const prev = map.get(event.id);
146
+ const metadata = mergeToolMetadata(prev?.metadata, event.metadata);
147
+ if (!metadata)
148
+ return false;
149
+ // Per-child updates carry no mode; group them under the launching workflow
150
+ // call rather than falling back to one single-agent group per member.
151
+ if (event.name === "run_workflow" && metadata.mode === undefined)
152
+ metadata.mode = "workflow";
153
+ map.set(event.id, { id: event.id, name: event.name, args: prev?.args ?? {}, metadata });
154
+ return true;
155
+ }
78
156
  /**
79
157
  * Collects every spawned subagent from the live transcript + streaming tools,
80
158
  * grouped by their originating tool call, for the inspector. Pure.
@@ -159,5 +237,8 @@ export function collectSubagentGroups(messages, streamingTools) {
159
237
  kind: g.kind,
160
238
  label: g.label,
161
239
  members: g.memberKeys.map((k) => freshest.get(k)).filter((m) => !!m),
162
- }));
240
+ }))
241
+ // A group can end up empty when a later echo of the same run (e.g. the
242
+ // wait_workflow result vs the live accumulator) claimed all its members.
243
+ .filter((group) => group.members.length > 0);
163
244
  }
@@ -15,7 +15,13 @@ export interface Theme {
15
15
  error: string;
16
16
  warning: string;
17
17
  success: string;
18
- background: string;
18
+ /**
19
+ * Painted canvas color. `undefined` means "do not paint" — the terminal's
20
+ * own background shows through, so the app never fights the user's terminal
21
+ * palette (e.g. forcing pure black inside a soft-dark terminal). Users can
22
+ * still force a painted canvas via `theme.overrides.background` in config.
23
+ */
24
+ background: string | undefined;
19
25
  accent: string;
20
26
  /** Welcome banner border. */
21
27
  bannerBorder: string;
@@ -40,7 +46,8 @@ export interface Theme {
40
46
  toolResult: string;
41
47
  toolError: string;
42
48
  toolPending: string;
43
- code: string;
49
+ /** Markdown inline `code` spans. */
50
+ inlineCode: string;
44
51
  traceAction: string;
45
52
  traceCount: string;
46
53
  traceDetail: string;
@@ -50,8 +57,10 @@ export interface Theme {
50
57
  userMessageBg: string;
51
58
  userMessageText: string;
52
59
  userRail: string;
60
+ /** Painted band backgrounds for diff lines inside user-message cards. */
53
61
  diffAdd: string;
54
62
  diffRemove: string;
63
+ /** Foregrounds for +/- diff lines rendered without a painted background. */
55
64
  diffAddFg: string;
56
65
  diffRemoveFg: string;
57
66
  }
@@ -62,6 +71,14 @@ export declare const darkTheme: Theme;
62
71
  * terminal background.
63
72
  */
64
73
  export declare const lightTheme: Theme;
74
+ /**
75
+ * Decide whether the canvas needs painting. Auto mode always inherits the
76
+ * terminal's own background, and so does a forced theme that matches the
77
+ * detected terminal. A forced theme that mismatches (e.g. `/theme light`
78
+ * inside a dark terminal) paints its canvas, because its foregrounds are
79
+ * tuned for the opposite background and would otherwise be unreadable.
80
+ */
81
+ export declare function canvasBackgroundFor(mode: ThemeMode, resolved: ResolvedTheme, terminalTheme: ResolvedTheme): string | undefined;
65
82
  export declare const ThemeProvider: import("react").Provider<Theme>;
66
83
  export declare function useTheme(): Theme;
67
84
  /** Build the active palette given a resolved mode and optional overrides. */
@@ -14,7 +14,7 @@ export const darkTheme = {
14
14
  error: "red",
15
15
  warning: "yellow",
16
16
  success: "green",
17
- background: "#0A0A0A",
17
+ background: undefined,
18
18
  accent: "cyan",
19
19
  bannerBorder: "#38bdf8",
20
20
  bannerGradientFrom: "#67e8f9",
@@ -37,7 +37,7 @@ export const darkTheme = {
37
37
  toolResult: "gray",
38
38
  toolError: "red",
39
39
  toolPending: "yellow",
40
- code: "yellow",
40
+ inlineCode: "#a78bfa",
41
41
  traceAction: "#E89A6B",
42
42
  traceCount: "#c9c1bd",
43
43
  traceDetail: "gray",
@@ -49,8 +49,8 @@ export const darkTheme = {
49
49
  userRail: "#8A7FC6",
50
50
  diffAdd: "#1a3d1a",
51
51
  diffRemove: "#3d1a1a",
52
- diffAddFg: "#9CDCFE",
53
- diffRemoveFg: "#F48771",
52
+ diffAddFg: "green",
53
+ diffRemoveFg: "red",
54
54
  };
55
55
  /**
56
56
  * Light palette tuned for paper-neutral surfaces, blue focus/user rails, warm
@@ -63,7 +63,7 @@ export const lightTheme = {
63
63
  error: "#B62633",
64
64
  warning: "#8B4A00",
65
65
  success: "#2F7D4A",
66
- background: "#FCFCFA",
66
+ background: undefined,
67
67
  accent: "#8B4A00",
68
68
  bannerBorder: "#356FD2",
69
69
  bannerGradientFrom: "#0E7490",
@@ -86,7 +86,7 @@ export const lightTheme = {
86
86
  toolResult: "#171717",
87
87
  toolError: "#B62633",
88
88
  toolPending: "#8B4A00",
89
- code: "#2F7D4A",
89
+ inlineCode: "#6D28D9",
90
90
  traceAction: "#8B4A00",
91
91
  traceCount: "#6F7377",
92
92
  traceDetail: "#8B9094",
@@ -98,9 +98,26 @@ export const lightTheme = {
98
98
  userRail: "#356FD2",
99
99
  diffAdd: "#D7E8D8",
100
100
  diffRemove: "#F7DADC",
101
- diffAddFg: "#173D2D",
102
- diffRemoveFg: "#5D1922",
101
+ diffAddFg: "#2F7D4A",
102
+ diffRemoveFg: "#B62633",
103
103
  };
104
+ /** Canvas colors painted only when a forced theme mismatches the terminal. */
105
+ const paintedCanvas = {
106
+ dark: "#0A0A0A",
107
+ light: "#FCFCFA",
108
+ };
109
+ /**
110
+ * Decide whether the canvas needs painting. Auto mode always inherits the
111
+ * terminal's own background, and so does a forced theme that matches the
112
+ * detected terminal. A forced theme that mismatches (e.g. `/theme light`
113
+ * inside a dark terminal) paints its canvas, because its foregrounds are
114
+ * tuned for the opposite background and would otherwise be unreadable.
115
+ */
116
+ export function canvasBackgroundFor(mode, resolved, terminalTheme) {
117
+ if (mode === "auto" || resolved === terminalTheme)
118
+ return undefined;
119
+ return paintedCanvas[resolved];
120
+ }
104
121
  const ThemeContext = createContext(darkTheme);
105
122
  export const ThemeProvider = ThemeContext.Provider;
106
123
  export function useTheme() {
@@ -9,6 +9,7 @@ interface WelcomeBannerProps {
9
9
  /** Session identifier (session file basename). */
10
10
  sessionLabel?: string;
11
11
  providerId?: string;
12
+ modelId?: string;
12
13
  modelLabel?: string;
13
14
  /** Active thinking level, rendered as part of the model unit (e.g. "xhigh"). */
14
15
  thinkingLabel?: string;
@@ -19,6 +20,6 @@ interface WelcomeVisibilityInput {
19
20
  }
20
21
  export declare function shouldShowWelcomeBanner({ startedWithVisibleHistory, }: WelcomeVisibilityInput): boolean;
21
22
  export declare function lerpColor(from: string, to: string, t: number): string;
22
- export declare function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelLabel, thinkingLabel, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
23
- export declare function formatModelLine({ providerId, modelLabel, thinkingLabel, tips, }: Pick<WelcomeBannerProps, "providerId" | "modelLabel" | "thinkingLabel" | "tips">): string;
23
+ export declare function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelId, modelLabel, thinkingLabel, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function formatModelLine({ providerId, modelId, modelLabel, thinkingLabel, tips, }: Pick<WelcomeBannerProps, "providerId" | "modelId" | "modelLabel" | "thinkingLabel" | "tips">): string;
24
25
  export {};
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Box, Text } from "ink";
3
3
  import { createRequire } from "node:module";
4
4
  import { useTheme } from "./theme.js";
5
+ import { isThinkingOnlyModel, isThinkingToggleModel } from "../provider-transform.js";
5
6
  const require = createRequire(import.meta.url);
6
7
  const PACKAGE_VERSION = readPackageVersion();
7
8
  const COMPACT_LOGO = [
@@ -28,11 +29,12 @@ function GradientText({ text, from, to }) {
28
29
  const chars = [...text];
29
30
  return (_jsx(_Fragment, { children: chars.map((ch, i) => (_jsx(Text, { bold: true, color: lerpColor(from, to, chars.length <= 1 ? 0 : i / (chars.length - 1)), children: ch }, `ch-${i}`))) }));
30
31
  }
31
- export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelLabel, thinkingLabel, }) {
32
+ export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelId, modelLabel, thinkingLabel, }) {
32
33
  const theme = useTheme();
33
34
  const effectiveWidth = Math.max(24, Math.min(terminalColumns - 2, 96));
34
35
  const modelLine = formatModelLine({
35
36
  providerId,
37
+ modelId,
36
38
  modelLabel,
37
39
  thinkingLabel,
38
40
  tips,
@@ -48,14 +50,16 @@ export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessio
48
50
  const labelWidth = Math.max(...infoRows.map((row) => row.label.length)) + 1;
49
51
  return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: theme.bannerBorder, paddingX: 2, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", marginRight: 2, flexShrink: 0, children: COMPACT_LOGO.map((line, rowIndex) => (_jsx(Text, { bold: true, color: lerpColor(theme.bannerGradientFrom, theme.bannerGradientTo, COMPACT_LOGO.length <= 1 ? 0 : rowIndex / (COMPACT_LOGO.length - 1)), children: line }, `logo-row-${rowIndex}`))) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, flexShrink: 1, children: [_jsx(Box, { children: _jsx(GradientText, { text: "Welcome to Bubble!", from: theme.bannerGradientFrom, to: theme.bannerGradientTo }) }), _jsx(Text, { color: theme.muted, wrap: "wrap", children: "I am a cat and you can send /help for help information." })] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: infoRows.map((row) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: theme.dim, children: row.label.padEnd(labelWidth) }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { color: row.color, wrap: "wrap", children: row.value }) })] }, row.label))) }), updateNotice && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.accent, children: updateNotice }) }))] }));
50
52
  }
51
- export function formatModelLine({ providerId, modelLabel, thinkingLabel, tips, }) {
53
+ export function formatModelLine({ providerId, modelId, modelLabel, thinkingLabel, tips, }) {
52
54
  const parts = [];
53
- // MiniMax thinking is a binary toggle (adaptive thinking), so label it
54
- // "thinking mode" rather than "<level> effort"; and its provider id
55
- // ("minimax-anthropic") is redundant with the model name, so omit it.
55
+ // Binary thinking toggles use an internal placeholder level, so label them
56
+ // "thinking mode" rather than "<level> effort". MiniMax provider ids are
57
+ // redundant with the model name, so omit them.
56
58
  const isMiniMax = (providerId || "").toLowerCase().includes("minimax");
59
+ const isThinkingToggle = isThinkingToggleModel(providerId, modelId);
60
+ const isThinkingOnly = isThinkingOnlyModel(providerId, modelId);
57
61
  if (modelLabel) {
58
- if (thinkingLabel && isMiniMax)
62
+ if (thinkingLabel && (isThinkingToggle || isThinkingOnly))
59
63
  parts.push(modelLabel, "thinking mode");
60
64
  else if (thinkingLabel)
61
65
  parts.push(`${modelLabel} with ${thinkingLabel} effort`);
package/dist/types.d.ts CHANGED
@@ -12,7 +12,8 @@ export interface ImageContent {
12
12
  };
13
13
  }
14
14
  export type ContentPart = TextContent | ImageContent;
15
- export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
15
+ export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
16
+ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
16
17
  export type ReasoningEffort = ThinkingLevel;
17
18
  export type ProviderRawContentBlock = Record<string, unknown> & {
18
19
  type: string;
@@ -169,10 +170,9 @@ export interface ToolContext {
169
170
  name: string;
170
171
  };
171
172
  agent?: {
172
- runSubtask: (input: string | ContentPart[], cwd: string, options?: {
173
- subtaskType?: string;
174
- description?: string;
175
- }) => Promise<ToolResult>;
173
+ /** Deferred-tool surface; backs tool_search when no controller is wired. */
174
+ listDeferredTools?: () => ToolRegistryEntry[];
175
+ unlockDeferredTools?: (names: string[]) => void;
176
176
  runSubAgent?: (input: string | ContentPart[], cwd: string, options: {
177
177
  profile: import("./agent/profiles.js").AgentProfile;
178
178
  runId: string;
@@ -210,38 +210,15 @@ export interface ToolContext {
210
210
  }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
211
211
  closeSubAgent?: (agentId: string) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
212
212
  listSubAgents?: () => import("./agent/subagent-control.js").SubagentThreadSnapshot[];
213
- runAgentTeam?: (cwd: string, options: {
214
- profile: import("./agent/profiles.js").AgentProfile;
215
- category?: string;
216
- model?: string;
217
- effort?: ThinkingLevel;
218
- promptTemplate: string;
219
- items: string[];
220
- parentToolCallId: string;
221
- emitUpdate?: (update: ToolUpdate) => void;
222
- abortSignal?: AbortSignal;
223
- approval?: "fail" | "disabled";
224
- }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
225
- runAgentBatch?: (cwd: string, options: {
226
- specs: Array<{
227
- task: string;
228
- profile: import("./agent/profiles.js").AgentProfile;
229
- category?: string;
230
- model?: string;
231
- effort?: ThinkingLevel;
232
- outputSchema?: unknown;
233
- }>;
234
- parentToolCallId: string;
235
- emitUpdate?: (update: ToolUpdate) => void;
236
- abortSignal?: AbortSignal;
237
- approval?: "fail" | "disabled";
238
- }) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
239
213
  startWorkflow?: (cwd: string, options: {
240
214
  script: string;
241
215
  args?: unknown;
242
216
  title?: string;
243
217
  parentToolCallId: string;
244
218
  abortSignal?: AbortSignal;
219
+ ensureProfileTrusted?: (profile: import("./agent/profiles.js").AgentProfile) => Promise<{
220
+ content: string | unknown;
221
+ } | undefined>;
245
222
  }) => {
246
223
  runId: string;
247
224
  title: string;
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Core types for the coding agent.
3
3
  */
4
- export {};
4
+ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
@@ -1,5 +1,5 @@
1
- import type { ThinkingLevel } from "../types.js";
2
- export declare const THINKING_LEVELS: ThinkingLevel[];
1
+ import { type ThinkingLevel } from "../types.js";
2
+ export { THINKING_LEVELS } from "../types.js";
3
3
  export declare function isThinkingLevel(value: string | undefined): value is ThinkingLevel;
4
4
  export declare function clampThinkingLevel(requestedLevel: ThinkingLevel, supportedLevels: readonly ThinkingLevel[]): ThinkingLevel;
5
5
  export declare function getNextThinkingLevel(currentLevel: ThinkingLevel, supportedLevels: readonly ThinkingLevel[]): ThinkingLevel;
@@ -1,4 +1,5 @@
1
- export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
1
+ import { THINKING_LEVELS } from "../types.js";
2
+ export { THINKING_LEVELS } from "../types.js";
2
3
  export function isThinkingLevel(value) {
3
4
  return !!value && THINKING_LEVELS.includes(value);
4
5
  }
@@ -1,4 +1,26 @@
1
1
  import type { ThinkingLevel } from "../types.js";
2
2
  export declare function getAvailableThinkingLevels(providerId: string, modelId: string): ThinkingLevel[];
3
3
  export declare function getDefaultThinkingLevel(providerId: string, modelId: string): ThinkingLevel;
4
+ /**
5
+ * A single non-"off" level means the model is always in thinking mode with no
6
+ * user-facing grades (e.g. kimi-k2.7-code) — the level value is an internal
7
+ * placeholder and must not be shown to the user as an effort grade.
8
+ */
9
+ export declare function isThinkingOnlyLevels(levels: readonly ThinkingLevel[]): boolean;
10
+ export declare function isThinkingOnlyModel(providerId?: string, modelId?: string): boolean;
11
+ /**
12
+ * Some providers expose thinking as a true enabled/disabled switch. We may store
13
+ * the enabled state as "medium" internally for compatibility with ThinkingLevel,
14
+ * but it is not a real user-facing effort grade.
15
+ */
16
+ export declare function isThinkingToggleModel(providerId?: string, modelId?: string): boolean;
4
17
  export declare function normalizeThinkingLevel(level: ThinkingLevel, supportedLevels: readonly ThinkingLevel[]): ThinkingLevel;
18
+ /**
19
+ * Normalizes effort inherited from a previous model or restored session.
20
+ *
21
+ * Unlike an explicit CLI/config choice, an inherited unsupported value does
22
+ * not express a direction the user just chose for the target model. Prefer the
23
+ * target model's declared default in that case, then fall back to the canonical
24
+ * downward clamp for legacy models without an explicit default.
25
+ */
26
+ export declare function normalizeInheritedThinkingLevel(providerId: string, modelId: string, inheritedLevel: ThinkingLevel): ThinkingLevel;
@@ -1,5 +1,7 @@
1
1
  import { getBuiltinModel, getModelDefaultReasoningLevel } from "../model-catalog.js";
2
2
  import { clampThinkingLevel } from "./thinking-level.js";
3
+ const KIMI_TOGGLE_PROVIDER_IDS = new Set(["moonshot-cn", "moonshot-intl", "kimi-for-coding"]);
4
+ const KIMI_THINKING_TOGGLE_MODELS = new Set(["kimi-k2.5", "kimi-k2.6"]);
3
5
  export function getAvailableThinkingLevels(providerId, modelId) {
4
6
  return getBuiltinModel(providerId, modelId)?.reasoningLevels ?? ["off"];
5
7
  }
@@ -10,6 +12,47 @@ export function getDefaultThinkingLevel(providerId, modelId) {
10
12
  return explicitDefault;
11
13
  return levels.includes("medium") ? "medium" : levels[0] || "off";
12
14
  }
15
+ /**
16
+ * A single non-"off" level means the model is always in thinking mode with no
17
+ * user-facing grades (e.g. kimi-k2.7-code) — the level value is an internal
18
+ * placeholder and must not be shown to the user as an effort grade.
19
+ */
20
+ export function isThinkingOnlyLevels(levels) {
21
+ return levels.length === 1 && levels[0] !== "off";
22
+ }
23
+ export function isThinkingOnlyModel(providerId = "", modelId = "") {
24
+ return isThinkingOnlyLevels(getAvailableThinkingLevels(providerId, modelId));
25
+ }
26
+ /**
27
+ * Some providers expose thinking as a true enabled/disabled switch. We may store
28
+ * the enabled state as "medium" internally for compatibility with ThinkingLevel,
29
+ * but it is not a real user-facing effort grade.
30
+ */
31
+ export function isThinkingToggleModel(providerId = "", modelId = "") {
32
+ const normalizedProvider = providerId.toLowerCase();
33
+ const normalizedModel = modelId.toLowerCase();
34
+ if (normalizedProvider.includes("minimax") || normalizedModel.includes("minimax"))
35
+ return true;
36
+ return KIMI_TOGGLE_PROVIDER_IDS.has(normalizedProvider)
37
+ && KIMI_THINKING_TOGGLE_MODELS.has(normalizedModel);
38
+ }
13
39
  export function normalizeThinkingLevel(level, supportedLevels) {
14
40
  return clampThinkingLevel(level, supportedLevels);
15
41
  }
42
+ /**
43
+ * Normalizes effort inherited from a previous model or restored session.
44
+ *
45
+ * Unlike an explicit CLI/config choice, an inherited unsupported value does
46
+ * not express a direction the user just chose for the target model. Prefer the
47
+ * target model's declared default in that case, then fall back to the canonical
48
+ * downward clamp for legacy models without an explicit default.
49
+ */
50
+ export function normalizeInheritedThinkingLevel(providerId, modelId, inheritedLevel) {
51
+ const supportedLevels = getAvailableThinkingLevels(providerId, modelId);
52
+ if (supportedLevels.includes(inheritedLevel))
53
+ return inheritedLevel;
54
+ const modelDefault = getModelDefaultReasoningLevel(providerId, modelId);
55
+ if (modelDefault && supportedLevels.includes(modelDefault))
56
+ return modelDefault;
57
+ return clampThinkingLevel(inheritedLevel, supportedLevels);
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {
@@ -24,6 +24,7 @@
24
24
  "test:watch": "vitest"
25
25
  },
26
26
  "dependencies": {
27
+ "@agentclientprotocol/sdk": "1.2.1",
27
28
  "@ai-sdk/google": "^3.0.88",
28
29
  "@ai-sdk/provider": "^3.0.13",
29
30
  "@larksuiteoapi/node-sdk": "^1.65.0",
@@ -59,6 +60,9 @@
59
60
  "overrides": {
60
61
  "axios": "^1.17.0",
61
62
  "postcss": "^8.5.15",
62
- "ws": "^8.21.0"
63
+ "ws": "^8.21.0",
64
+ "openai": {
65
+ "zod": "$zod"
66
+ }
63
67
  }
64
68
  }
@@ -1,2 +0,0 @@
1
- import type { ToolRegistryEntry } from "../types.js";
2
- export declare function createTaskTool(): ToolRegistryEntry;