@bubblebrain-ai/bubble 0.0.22 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,38 +1,37 @@
1
1
  # Bubble
2
2
 
3
- Bubble is a terminal coding agent for working inside local project folders. It can read and edit files, run commands with approval controls, use project skills, connect MCP tools, and keep persistent memory across sessions.
3
+ Bubble is a terminal coding agent. It works inside a local project folder: it reads and edits files, runs commands behind configurable approval controls, searches and navigates code with language-server intelligence, browses the web, loads reusable skills, connects MCP tools, fans work out to subagents, and keeps persistent memory across sessions.
4
+
5
+ It is provider-agnostic. Bring an API key for OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Zhipu, Z.AI, MiniMax, Groq, Together, Fireworks, a local OpenAI-compatible endpoint, and more — or sign in to ChatGPT with OAuth and drive the Codex models directly.
6
+
7
+ ---
4
8
 
5
9
  ## Requirements
6
10
 
7
- - Node.js 20+ and npm for installation
8
- - Bun for running Bubble
11
+ - Node.js 20 or newer (used to install the launcher)
12
+ - [Bun](https://bun.sh) (used to run the agent)
9
13
 
10
- Install Bun if it is not already available:
14
+ Install Bun if you do not already have it:
11
15
 
12
16
  ```bash
13
17
  curl -fsSL https://bun.sh/install | bash
14
18
  ```
15
19
 
16
- ## Install
20
+ The `npm install` step puts a small Node.js launcher named `bubble` on your PATH. When you run `bubble`, the launcher locates Bun and starts the real runtime under it. If Bun is missing, it prints the install command above instead of failing with a low-level error.
17
21
 
18
- From npm:
22
+ ## Install
19
23
 
20
24
  ```bash
21
25
  npm install -g @bubblebrain-ai/bubble
22
26
  ```
23
27
 
24
- From a local package tarball:
28
+ To install from a local tarball:
25
29
 
26
30
  ```bash
27
- npm install -g ./bubblebrain-ai-bubble-0.0.3.tgz
31
+ npm install -g ./bubblebrain-ai-bubble-<version>.tgz
28
32
  ```
29
33
 
30
- The npm command installs a small Node.js launcher named `bubble`. When you run
31
- `bubble`, the launcher checks for Bun and starts the real Bubble runtime with
32
- `bun`. If Bun is missing, it prints the install command above instead of failing
33
- with a low-level runtime error.
34
-
35
- ## Usage
34
+ ## Quick start
36
35
 
37
36
  Start Bubble in the current directory:
38
37
 
@@ -40,31 +39,192 @@ Start Bubble in the current directory:
40
39
  bubble
41
40
  ```
42
41
 
43
- Start Bubble for a specific project:
42
+ On first launch, connect a model:
43
+
44
+ - Run `/login` to sign in to ChatGPT (OAuth) and use the Codex models, or
45
+ - Run `/provider` to add any other provider with an API key.
46
+
47
+ Then just type what you want done. Bubble plans, edits files, and runs commands, asking for approval where the current permission mode requires it.
48
+
49
+ Point Bubble at a different project:
44
50
 
45
51
  ```bash
46
52
  bubble --cwd /path/to/project
47
53
  ```
48
54
 
49
- Show CLI options:
55
+ Resume your last conversation:
50
56
 
51
57
  ```bash
52
- bubble --help
58
+ bubble --resume
59
+ ```
60
+
61
+ ## Model providers
62
+
63
+ Bubble ships with a catalog of built-in providers. Configure them inside the app — no environment variables required.
64
+
65
+ | How | What it does |
66
+ | --- | --- |
67
+ | `/login` | OAuth sign-in for ChatGPT; unlocks the OpenAI Codex models without an API key. |
68
+ | `/provider` | Open a picker to connect, switch, add, or remove a provider. |
69
+ | `/key <provider> <key>` | Set the API key for a provider. |
70
+ | `/model` | Pick the active model and reasoning effort. |
71
+
72
+ Built-in providers include OpenAI, Anthropic, Google, DeepSeek, Moonshot (CN and international), Kimi for Coding, Zhipu AI, Z.AI, Alibaba DashScope, MiniMax, StepFun, Groq, Together AI, Fireworks, and a `local` profile for any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.).
73
+
74
+ ### Custom providers and models
75
+
76
+ For full control — custom base URLs, self-hosted gateways, extra models, or pinning a protocol — define providers in `~/.bubble/models.json`:
77
+
78
+ ```json
79
+ {
80
+ "providers": {
81
+ "my-gateway": {
82
+ "baseURL": "https://gateway.internal/v1",
83
+ "apiKey": "sk-...",
84
+ "protocol": "openai-chat",
85
+ "models": [
86
+ { "id": "my-model", "name": "My Model" }
87
+ ]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ `protocol` accepts `openai-chat` (default) or `anthropic-messages`. Entries in `models.json` take precedence over the built-in catalog.
94
+
95
+ ### Reasoning effort
96
+
97
+ Models that support it expose a reasoning-effort control: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Set it with `/model <id> --reasoning-effort <level>` in the app, or at launch with `--reasoning` (medium) or `--reasoning-effort <level>`.
98
+
99
+ ## Permission modes
100
+
101
+ Bubble gates risky actions behind a permission mode. Press `Tab` to cycle modes during a session, or set a default with `--plan` / `--dangerously-skip-permissions` / project settings.
102
+
103
+ | Mode | Behavior |
104
+ | --- | --- |
105
+ | Default (Build) | File edits and writes auto-approve; bash and other tools prompt unless covered by an allow rule. |
106
+ | Plan | Read-only investigation. The agent proposes a plan and waits for your approval before making changes. |
107
+ | Bypass | Auto-approves every tool and disables all safety prompts. Enable deliberately with `--dangerously-skip-permissions`. |
108
+
109
+ Allow/deny rules are configured per scope and persisted across sessions. Manage them in-app with `/permissions`, or edit the settings files directly:
110
+
111
+ - `~/.bubble/settings.json` — user scope (applies everywhere)
112
+ - `<project>/.bubble/settings.json` — project scope (commit to share with your team)
113
+ - `<project>/.bubble/settings.local.json` — local overrides (gitignore)
114
+
115
+ Rules use a simple pattern syntax, for example:
116
+
117
+ ```json
118
+ {
119
+ "permissions": {
120
+ "defaultMode": "default",
121
+ "allow": [
122
+ "Bash(git status)",
123
+ "Bash(npm run:*)",
124
+ "Read(./src/**)",
125
+ "WebFetch(domain:github.com)"
126
+ ],
127
+ "deny": ["Read(~/.ssh/**)"]
128
+ }
129
+ }
53
130
  ```
54
131
 
55
- ## Configuration
132
+ ## What Bubble can do
56
133
 
57
- Bubble stores user configuration, sessions, permissions, skills, and memory under:
134
+ **Files and code.** Read, write, and make targeted edits; find files by glob; search contents with ripgrep; and navigate with language-server operations (go-to-definition, find references, hover, document/workspace symbols, call hierarchy).
135
+
136
+ **Shell and dev servers.** Run bounded bash commands with streaming output, and start/stop/inspect long-running dev servers (`npm run dev`, Vite, Next, etc.) with readiness checks and captured logs.
137
+
138
+ **Web.** Search the web and fetch/extract page contents on demand.
139
+
140
+ **Skills.** Drop reusable instructions and assets into a `SKILL.md`-based directory and invoke them with `/<skill-name>`. Bubble discovers skills from `~/.bubble/skills`, `~/.agents/skills`, `~/.claude/skills`, and the project's `.bubble/skills`. Browse them with `/skills`.
141
+
142
+ **MCP tools.** Connect Model Context Protocol servers (stdio, HTTP, or SSE) under the `mcpServers` key of any settings file. Their tools and prompts become available to the agent; manage connections with `/mcp`.
143
+
144
+ **Subagents.** Bubble can spawn background subagents with independent context, send them follow-ups, wait on their results, and fan a task out across a team — with concurrency limits and token budgets. Define custom agent profiles in `~/.bubble/agents` or a project's `.bubble/agents`.
145
+
146
+ **Persistent memory.** A background pipeline distills durable facts, preferences, and decisions from past sessions and recalls them later. Inspect and maintain it with `/memory status`, `/memory search <query>`, and `/memory refresh`.
147
+
148
+ **Sessions.** Every conversation is saved. Resume the latest with `bubble --resume`, or browse and switch sessions in-app with `/session`. Use `/rewind` to roll the conversation — and optionally your file edits — back to an earlier point.
149
+
150
+ ## Useful slash commands
151
+
152
+ | Command | Description |
153
+ | --- | --- |
154
+ | `/help` | List available commands. |
155
+ | `/model` | Switch model and reasoning effort. |
156
+ | `/provider`, `/login`, `/logout`, `/key` | Connect and manage providers. |
157
+ | `/session`, `/rewind`, `/clear` | Manage conversation history. |
158
+ | `/skills` | Open the searchable skills picker. |
159
+ | `/mcp` | List or reconnect MCP servers. |
160
+ | `/memory` | Inspect and maintain persistent memory. |
161
+ | `/permissions` | View or edit allow/deny rules. |
162
+ | `/context`, `/stats`, `/compact` | Inspect context usage, model stats, and compact the session. |
163
+ | `/lsp`, `/hooks` | Manage language servers and lifecycle hooks. |
164
+ | `/theme`, `/sidebar` | Adjust the interface. |
165
+ | `/feedback` | Send feedback or report a bug. |
166
+
167
+ ## Non-interactive mode
168
+
169
+ Run a single prompt and print the result — useful for scripts and pipelines:
170
+
171
+ ```bash
172
+ bubble --print "summarize what this repo does"
173
+ echo "fix the failing test" | bubble --print
174
+ ```
175
+
176
+ Combine with `--model`, `--cwd`, and `--plan` as needed.
177
+
178
+ ## CLI reference
58
179
 
59
180
  ```text
60
- ~/.bubble
181
+ Usage:
182
+ bubble [options] [prompt] Start interactive TUI
183
+ bubble update [--check] Update to the latest version (alias: upgrade)
184
+ bubble serve --feishu [options] Run as a Feishu bot host
185
+
186
+ Options (default):
187
+ -m, --model <model> Model to use
188
+ --cwd <dir> Working directory (default: current)
189
+ -k, --api-key <key> API key for the active provider
190
+ -r, --resume Resume a previous session (latest by default)
191
+ --session <name> Session name to create or resume
192
+ --reasoning Enable reasoning mode at medium effort
193
+ --reasoning-effort <l> Set reasoning effort: off|minimal|low|medium|high|xhigh|max
194
+ --plan Start in plan mode (read-only investigation; propose before executing)
195
+ --dangerously-skip-permissions
196
+ Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
197
+ -p, --print Non-interactive mode (single prompt)
198
+ -v, --version Print the installed version and exit
199
+ -h, --help Show this help
61
200
  ```
62
201
 
63
- In the app, use `/login` or provider commands to configure model access.
202
+ Run `bubble update` at any time to upgrade to the latest published version.
203
+
204
+ ## Configuration and storage
64
205
 
65
- ### ChatGPT Network Configuration
206
+ Bubble keeps everything under `~/.bubble`:
207
+
208
+ ```text
209
+ ~/.bubble/
210
+ config.json User preferences (theme, default model, recent models)
211
+ models.json Custom providers and models
212
+ auth.json OAuth credentials (file mode 0600)
213
+ settings.json User-scope permissions, MCP servers, hooks
214
+ sessions/ Saved conversations, grouped by project directory
215
+ memories/ Persistent memory store
216
+ skills/ User skills
217
+ agents/ Custom subagent profiles
218
+ ```
66
219
 
67
- ChatGPT OAuth and GPT/Codex requests respect standard proxy variables:
220
+ Environment variables:
221
+
222
+ - `BUBBLE_HOME` — override the data directory (defaults to `~/.bubble`).
223
+ - `BUBBLE_DEV=1` — use `~/.bubble-dev` instead, for development.
224
+
225
+ ### Network configuration
226
+
227
+ ChatGPT OAuth and GPT/Codex requests respect the standard proxy variables:
68
228
 
69
229
  ```bash
70
230
  export HTTPS_PROXY=http://proxy.example.com:8080
@@ -72,28 +232,31 @@ export HTTP_PROXY=http://proxy.example.com:8080
72
232
  export NO_PROXY=localhost,127.0.0.1
73
233
  ```
74
234
 
75
- If your network uses a corporate or custom HTTPS CA, start Bubble with:
235
+ If your network uses a corporate or custom HTTPS CA:
76
236
 
77
237
  ```bash
78
238
  NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
79
239
  ```
80
240
 
81
- You can also use `BUBBLE_EXTRA_CA_CERTS` for Bubble's ChatGPT requests:
241
+ `BUBBLE_EXTRA_CA_CERTS` applies the same trust to Bubble's ChatGPT requests specifically. Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
242
+
243
+ ## Development
82
244
 
83
245
  ```bash
84
- BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
246
+ bun install # install dependencies
247
+ npm run build # compile TypeScript to dist/
248
+ npm test # run the test suite (vitest)
249
+ npm start # run the built agent
85
250
  ```
86
251
 
87
- Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
252
+ `npm run dev` compiles and launches in one step. The TUI is built on [OpenTUI](https://github.com/anomalyco/opentui) and Solid.
88
253
 
89
- ## Memory
254
+ ## Feishu host (optional)
90
255
 
91
- Bubble maintains persistent memory automatically from prior sessions. Useful commands:
256
+ Bubble can also run as a Feishu (Lark) bot host:
92
257
 
93
- ```text
94
- /memory status
95
- /memory search <query>
96
- /memory refresh
258
+ ```bash
259
+ bubble serve --feishu --setup
97
260
  ```
98
261
 
99
- Memory is maintained by a background pipeline and can be refreshed manually when you want new session information to be indexed immediately.
262
+ `--setup` runs the binding wizard, `--kill-old` replaces a conflicting instance for the same App ID, and `--dry-run` connects once and exits as a smoke test.
@@ -54,17 +54,37 @@ export function sanitizeAssistantProviderMetadata(metadata) {
54
54
  if (!metadata || !anthropic || !blocks?.length)
55
55
  return metadata;
56
56
  let changed = false;
57
- const sanitizedBlocks = blocks.map((block) => {
58
- if (block.type !== "text" || typeof block.text !== "string") {
59
- return block;
57
+ const sanitizedBlocks = [];
58
+ for (const block of blocks) {
59
+ // Plaintext text blocks are unsigned, so rewriting them in place is safe.
60
+ if (block.type === "text" && typeof block.text === "string") {
61
+ const sanitizedText = sanitizeInternalReminderBlocks(block.text);
62
+ if (sanitizedText !== block.text) {
63
+ changed = true;
64
+ sanitizedBlocks.push({ ...block, text: sanitizedText });
65
+ }
66
+ else {
67
+ sanitizedBlocks.push(block);
68
+ }
69
+ continue;
60
70
  }
61
- const sanitizedText = sanitizeInternalReminderBlocks(block.text);
62
- if (sanitizedText === block.text) {
63
- return block;
71
+ // Extended-thinking blocks carry an Anthropic signature over their exact
72
+ // text; rewriting the text would invalidate the signature and the API
73
+ // would reject the replayed block. So when a thinking block's text carries
74
+ // internal markup (e.g. an echoed system reminder), DROP the whole block
75
+ // rather than mutate it. Thinking text is never user-visible — the display
76
+ // path renders message.reasoning, not contentBlocks — so dropping loses
77
+ // nothing on screen; it only keeps the verbatim reminder out of the
78
+ // persisted metadata and the Anthropic replay payload. redacted_thinking
79
+ // holds encrypted `data` (no plaintext field) and cannot carry a reminder.
80
+ if (block.type === "thinking" && typeof block.thinking === "string") {
81
+ if (sanitizeInternalReminderBlocks(block.thinking) !== block.thinking) {
82
+ changed = true;
83
+ continue;
84
+ }
64
85
  }
65
- changed = true;
66
- return { ...block, text: sanitizedText };
67
- });
86
+ sanitizedBlocks.push(block);
87
+ }
68
88
  if (!changed)
69
89
  return metadata;
70
90
  return {
package/dist/main.js CHANGED
@@ -563,8 +563,9 @@ async function main() {
563
563
  runMemorySummary,
564
564
  runMemoryRefresh,
565
565
  };
566
- const { getStartupUpdateNotice } = await import("./update/index.js");
567
- const updateNotice = await getStartupUpdateNotice();
566
+ const { startStartupUpdateCheck } = await import("./update/index.js");
567
+ const updateCheck = await startStartupUpdateCheck();
568
+ const updateNotice = updateCheck.notice;
568
569
  // Two explicit branches (not a dynamic ternary import) so TypeScript
569
570
  // checks each renderer's RunTuiOptions shape independently.
570
571
  let exitWallMs;
@@ -577,6 +578,7 @@ async function main() {
577
578
  detectedTheme,
578
579
  onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
579
580
  updateNotice: updateNotice ?? undefined,
581
+ updateNoticeRefresh: updateCheck.refreshed,
580
582
  });
581
583
  }
582
584
  else {
@@ -28,6 +28,9 @@ const GPT51_CODEX_MAX_LEVELS = ["off", "low", "medium", "high", "xhigh"];
28
28
  const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
29
29
  const OPENAI_CHAT_LEVELS = ["off"];
30
30
  const TOGGLE_THINKING_LEVELS = ["off", "medium"];
31
+ // kimi-k2.7-code only supports thinking mode (disabling it errors), so "off" is
32
+ // not offered — the model is always in its thinking variant.
33
+ const KIMI_THINKING_ONLY_LEVELS = ["medium"];
31
34
  const DEEPSEEK_V4_LEVELS = ["high", "max"];
32
35
  const STEPFUN_REASONING_LEVELS = ["off", "low", "medium", "high"];
33
36
  const MINIMAX_M3_REASONING_LEVELS = ["off", "medium"];
@@ -105,18 +108,21 @@ export const BUILTIN_MODELS = [
105
108
  { id: "step-3.5-flash-2603", name: "Step 3.5 Flash 2603", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
106
109
  { id: "step-3.5-flash", name: "Step 3.5 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
107
110
  { id: "step-router-v1", name: "Step Router V1", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
111
+ { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", providerId: "moonshot-cn", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
108
112
  { id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
109
113
  { id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
110
114
  { id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
111
115
  { id: "kimi-k2-turbo-preview", name: "Kimi K2 Turbo", providerId: "moonshot-cn", reasoningLevels: ["off"], contextWindow: 256000 },
112
116
  { id: "kimi-k2-0905-preview", name: "Kimi K2 0905", providerId: "moonshot-cn", reasoningLevels: ["off"], contextWindow: 256000 },
113
117
  { id: "kimi-k2-thinking", name: "Kimi K2 Thinking", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
118
+ { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", providerId: "moonshot-intl", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
114
119
  { id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
115
120
  { id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
116
121
  { id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
117
122
  { id: "kimi-k2-turbo-preview", name: "Kimi K2 Turbo", providerId: "moonshot-intl", reasoningLevels: ["off"], contextWindow: 256000 },
118
123
  { id: "kimi-k2-0905-preview", name: "Kimi K2 0905", providerId: "moonshot-intl", reasoningLevels: ["off"], contextWindow: 256000 },
119
124
  { id: "kimi-k2-thinking", name: "Kimi K2 Thinking", providerId: "moonshot-intl", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
125
+ { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", providerId: "kimi-for-coding", reasoningLevels: KIMI_THINKING_ONLY_LEVELS, contextWindow: 262144 },
120
126
  { id: "kimi-k2.6", name: "Kimi K2.6", providerId: "kimi-for-coding", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
121
127
  { id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "kimi-for-coding", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
122
128
  { id: "kimi-k2-turbo-preview", name: "Kimi K2 Turbo", providerId: "kimi-for-coding", reasoningLevels: ["off"], contextWindow: 256000 },
@@ -1,6 +1,7 @@
1
1
  import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./variant/variant-resolver.js";
2
2
  export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
3
3
  const MOONSHOT_PROVIDER_IDS = new Set(["moonshot-cn", "moonshot-intl", "kimi-for-coding"]);
4
+ const KIMI_K27_FAMILY = new Set(["kimi-k2.7-code"]);
4
5
  const KIMI_K25_FAMILY = new Set(["kimi-k2.5", "k2.6-code-preview", "kimi-k2.6"]);
5
6
  const KIMI_THINKING_FAMILY = new Set(["kimi-k2-thinking", "kimi-k2-thinking-turbo"]);
6
7
  const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
@@ -78,6 +79,19 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
78
79
  // temperature/top_p/n/penalties and exposes thinking via extra_body.thinking;
79
80
  // kimi-k2-thinking family locks temperature=1.
80
81
  if (MOONSHOT_PROVIDER_IDS.has(providerId)) {
82
+ // kimi-k2.7-code is thinking-only: temperature is locked to 1.0 server-side
83
+ // (any explicit value errors), thinking can never be disabled, and
84
+ // reasoning_content must be echoed back on tool-call turns.
85
+ if (KIMI_K27_FAMILY.has(modelId)) {
86
+ return {
87
+ effectiveThinkingLevel,
88
+ omitTemperature: true,
89
+ reasoningContentEcho: "tool_calls",
90
+ extraBody: {
91
+ thinking: { type: "enabled" },
92
+ },
93
+ };
94
+ }
81
95
  if (KIMI_K25_FAMILY.has(modelId)) {
82
96
  return {
83
97
  effectiveThinkingLevel,
package/dist/tui/run.d.ts CHANGED
@@ -45,6 +45,12 @@ 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
+ * Background registry check started before the TUI. Resolves with a late
50
+ * "update available" notice (or null); the TUI surfaces it live — on the
51
+ * home screen when still there, otherwise as a composer notice.
52
+ */
53
+ updateNoticeRefresh?: Promise<string | null>;
48
54
  /**
49
55
  * Swap the active session in place (driven by the /session picker).
50
56
  * Rebinds persistence to the picked session file and replaces the agent's
package/dist/tui/run.js CHANGED
@@ -101,6 +101,7 @@ const DEFAULT_THEME = {
101
101
  toolRead: "#9d7cd8",
102
102
  toolWrite: "#f5a742",
103
103
  toolSearch: "#5c9cf5",
104
+ toolMcp: "#d479c9",
104
105
  diffAdded: "#7fd88f",
105
106
  diffRemoved: "#e06c75",
106
107
  diffContext: "#a6acb8",
@@ -145,6 +146,7 @@ const LIGHT_THEME = {
145
146
  toolRead: "#6F55AE",
146
147
  toolWrite: "#8B4A00",
147
148
  toolSearch: "#356FD2",
149
+ toolMcp: "#A03595",
148
150
  diffAdded: "#1E725C",
149
151
  diffRemoved: "#B62633",
150
152
  diffContext: "#6F7377",
@@ -564,6 +566,9 @@ function OpenTuiApp(props) {
564
566
  let rootBox;
565
567
  let sidebarShell;
566
568
  let homeSurfaceShell;
569
+ let homeUpdateNotice = props.options.updateNotice;
570
+ let homeUpdateNoticeBox;
571
+ let homeUpdateNoticeText;
567
572
  let transcriptHost;
568
573
  const transcriptState = {
569
574
  entries: [],
@@ -5922,11 +5927,49 @@ function OpenTuiApp(props) {
5922
5927
  }, [
5923
5928
  h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, ...logoLines.map((line) => renderHomeLogoLine(line))),
5924
5929
  h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center", paddingTop: 1 }, h("text", { fg: theme.textMuted, content: `v${getCurrentVersion()}` })),
5925
- ...(props.options.updateNotice
5926
- ? [h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, h("text", { fg: theme.accent, content: props.options.updateNotice }))]
5927
- : []),
5930
+ // Always mounted so a late registry check can reveal it mid-session.
5931
+ h("box", {
5932
+ ref: (ref) => {
5933
+ homeUpdateNoticeBox = ref;
5934
+ ref.visible = !!homeUpdateNotice;
5935
+ },
5936
+ visible: !!homeUpdateNotice,
5937
+ flexShrink: 0,
5938
+ flexDirection: "column",
5939
+ alignItems: "center",
5940
+ }, h("text", {
5941
+ ref: (ref) => { homeUpdateNoticeText = ref; },
5942
+ fg: theme.accent,
5943
+ content: homeUpdateNotice ?? "",
5944
+ })),
5928
5945
  ]);
5929
5946
  }
5947
+ function watchUpdateNoticeRefresh() {
5948
+ const refresh = props.options.updateNoticeRefresh;
5949
+ if (!refresh)
5950
+ return;
5951
+ refresh.then((notice) => {
5952
+ if (!notice || uiDisposed)
5953
+ return;
5954
+ homeUpdateNotice = notice;
5955
+ if (homeUpdateNoticeText)
5956
+ homeUpdateNoticeText.content = notice;
5957
+ if (homeUpdateNoticeBox)
5958
+ homeUpdateNoticeBox.visible = true;
5959
+ // Already chatting (or resumed straight into a transcript): the home
5960
+ // banner is hidden, so surface the nudge as a transcript line instead.
5961
+ // (Not setNotice: the notice() row in renderSessionView is evaluated
5962
+ // once at initial render and never materializes afterwards.)
5963
+ if (!isHomeSurfaceActive(streamingDisplay))
5964
+ addMessage("assistant", notice);
5965
+ rootBox?.requestRender();
5966
+ }).catch(() => {
5967
+ // The check is best-effort; never disturb the session over it.
5968
+ });
5969
+ }
5970
+ // Component body, not onMount: the onMount callback never fires under the
5971
+ // current @opentui/solid runtime, so anything registered there is dead code.
5972
+ watchUpdateNoticeRefresh();
5930
5973
  function renderQuestionPanelHost() {
5931
5974
  return h("box", {
5932
5975
  ref: (ref) => {
@@ -7864,7 +7907,7 @@ function createTraceGroupRenderable(ctx, group, syntaxStyle, width = 80) {
7864
7907
  }))));
7865
7908
  }
7866
7909
  if (group.omitted > 0) {
7867
- children.push(createText(ctx, ` ... ${group.omitted} more, Ctrl+O to view`, {
7910
+ children.push(createText(ctx, traceGroupOmittedLabel(group), {
7868
7911
  fg: theme.textMuted,
7869
7912
  wrapMode: "word",
7870
7913
  }));
@@ -7882,6 +7925,15 @@ function shouldRenderTraceGroupAsRawTool(tool) {
7882
7925
  function traceGroupDetailLines(group) {
7883
7926
  return group.previewLines.length > 0 ? group.previewLines : group.items;
7884
7927
  }
7928
+ // Overflow hint under a trace group. Line-based details (tool output) read as
7929
+ // "N more lines"; item-based details (file lists) stay as "N more".
7930
+ function traceGroupOmittedLabel(group) {
7931
+ if (group.previewLines.length > 0) {
7932
+ const noun = group.omitted === 1 ? "line" : "lines";
7933
+ return ` ... ${group.omitted} more ${noun}, Ctrl+O to expand`;
7934
+ }
7935
+ return ` ... ${group.omitted} more, Ctrl+O to expand`;
7936
+ }
7885
7937
  const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
7886
7938
  function executeInlineBudget(group, width) {
7887
7939
  return Math.max(14, width - group.title.length - 20);
@@ -7958,9 +8010,14 @@ function traceGroupTitleColor(group) {
7958
8010
  case "edit": return theme.toolWrite;
7959
8011
  case "subagent": return theme.accent;
7960
8012
  case "list": return theme.secondary;
7961
- default: return theme.toolText;
8013
+ default: return isMcpTraceGroup(group) ? theme.toolMcp : theme.toolText;
7962
8014
  }
7963
8015
  }
8016
+ // An "other" group whose single tool is an MCP call (`mcp__<server>__<tool>`).
8017
+ function isMcpTraceGroup(group) {
8018
+ const name = group.raw[0]?.name;
8019
+ return typeof name === "string" && name.startsWith("mcp__");
8020
+ }
7964
8021
  function traceGroupKey(group) {
7965
8022
  return `group:${group.kind}:${group.raw.map((tool) => tool.id).join(":")}`;
7966
8023
  }
@@ -8749,7 +8806,7 @@ function renderTraceGroup(group, syntaxStyle, width = 80) {
8749
8806
  wrapMode: "word",
8750
8807
  }, `${index === 0 ? "↳ " : " "}${truncate(line, detailWidth)}`)))
8751
8808
  : null, group.omitted > 0
8752
- ? h("text", { fg: theme.textMuted, wrapMode: "word" }, ` ... ${group.omitted} more, Ctrl+O to view`)
8809
+ ? h("text", { fg: theme.textMuted, wrapMode: "word" }, traceGroupOmittedLabel(group))
8753
8810
  : null);
8754
8811
  }
8755
8812
  function renderTool(tool, syntaxStyle, width = 80) {
@@ -1,6 +1,7 @@
1
1
  import os from "node:os";
2
2
  import { getEditDiffDetails } from "./edit-diff.js";
3
3
  import { formatSubagentRoute } from "../agent/subagent-route-format.js";
4
+ import { mcpInfoFromString } from "../mcp/name.js";
4
5
  const DEFAULT_MAX_ITEMS = 6;
5
6
  const DEFAULT_MAX_PREVIEW_LINES = 8;
6
7
  export function buildTraceGroups(toolCalls, options = {}) {
@@ -120,13 +121,18 @@ function classifyTool(toolCall) {
120
121
  return { kind: "edit", title: "Edit", bucketKey: `edit:${toolCall.id}`, groupable: false };
121
122
  case "write":
122
123
  return { kind: "write", title: "Write", bucketKey: "write", groupable: true };
123
- default:
124
+ default: {
125
+ const mcp = mcpInfoFromString(toolCall.name);
126
+ const title = mcp
127
+ ? `${mcp.serverName.toUpperCase()}: ${mcp.toolName}`
128
+ : displayToolName(toolCall.name);
124
129
  return {
125
130
  kind: "other",
126
- title: displayToolName(toolCall.name),
131
+ title,
127
132
  bucketKey: `${toolCall.name}:${toolCall.id}`,
128
133
  groupable: false,
129
134
  };
135
+ }
130
136
  }
131
137
  }
132
138
  function buildTraceGroup(classifier, raw, options) {
@@ -345,15 +351,23 @@ function buildSubagentGroup(classifier, tool, options, pending, startedAt) {
345
351
  }
346
352
  function buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
347
353
  const tool = raw[0];
348
- const header = toolHeader(tool, options.homeDir);
354
+ const mcp = mcpInfoFromString(tool.name);
355
+ // MCP tools carry arbitrary args, so render them as `key: value` pairs inline
356
+ // (via the `command` slot) instead of the path-based header used for builtins.
357
+ const header = mcp ? undefined : toolHeader(tool, options.homeDir);
358
+ const argsLabel = mcp ? mcpArgsLabel(tool.args) : "";
359
+ // Suppress the "N calls" fallback for MCP tools — the title already names the
360
+ // tool, and args (when present) ride alongside it.
361
+ const hasInline = mcp || !!header;
349
362
  const preview = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
350
363
  const { shown, omitted } = take(preview, options.maxPreviewLines);
351
364
  return {
352
365
  kind: "other",
353
366
  title: classifier.title,
354
367
  raw,
355
- count: header ? undefined : raw.length,
356
- noun: header ? undefined : plural(raw.length, "call", "calls"),
368
+ command: argsLabel || undefined,
369
+ count: hasInline ? undefined : raw.length,
370
+ noun: hasInline ? undefined : plural(raw.length, "call", "calls"),
357
371
  items: header ? [header] : [],
358
372
  previewLines: shown,
359
373
  errorLines: [],
@@ -469,6 +483,28 @@ function displayToolName(name) {
469
483
  return "Tool";
470
484
  return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
471
485
  }
486
+ /** Compact `key: value, key: value` rendering of an MCP tool's arguments. */
487
+ function mcpArgsLabel(args) {
488
+ if (!args || typeof args !== "object")
489
+ return "";
490
+ return Object.entries(args)
491
+ .filter(([, value]) => value !== undefined)
492
+ .map(([key, value]) => `${key}: ${formatMcpArgValue(value)}`)
493
+ .join(", ");
494
+ }
495
+ function formatMcpArgValue(value) {
496
+ if (typeof value === "string")
497
+ return JSON.stringify(value);
498
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
499
+ return String(value);
500
+ }
501
+ try {
502
+ return JSON.stringify(value);
503
+ }
504
+ catch {
505
+ return String(value);
506
+ }
507
+ }
472
508
  function toolHeader(tool, homeDir) {
473
509
  const args = tool.args || {};
474
510
  for (const key of ["path", "command", "pattern", "query", "url"]) {
@@ -37,10 +37,24 @@ export declare function upgradeCommandFor(manager: PackageManager): {
37
37
  export declare function runUpdateCommand(opts?: {
38
38
  checkOnly?: boolean;
39
39
  }): Promise<number>;
40
+ export interface StartupUpdateCheck {
41
+ /** Notice derived from the local cache — available immediately, no network. */
42
+ notice: string | null;
43
+ /**
44
+ * Resolves once the background registry check completes: a notice string
45
+ * when it finds a version newer than both the running one and the cached
46
+ * `notice`, otherwise null. Never rejects.
47
+ */
48
+ refreshed: Promise<string | null>;
49
+ }
40
50
  /**
41
- * Returns a one-line "update available" notice if the cached latest version is
42
- * newer than the running one. Reads only a local cache file (fast, no network
43
- * on the hot path); a stale cache triggers a fire-and-forget refresh so the
44
- * next launch is accurate. Never throws.
51
+ * Startup "update available" check. The immediate `notice` comes from the
52
+ * local cache file (fast, no network on the hot path). A registry refresh
53
+ * always runs in the background (throttled to once per 30 minutes) so a
54
+ * release published since the last launch surfaces in the *current* session
55
+ * via `refreshed`, instead of only after the cache TTL plus another restart.
56
+ * Never throws.
45
57
  */
58
+ export declare function startStartupUpdateCheck(): Promise<StartupUpdateCheck>;
59
+ /** Cache-only variant of {@link startStartupUpdateCheck} (still refreshes in the background). */
46
60
  export declare function getStartupUpdateNotice(): Promise<string | null>;
@@ -16,7 +16,9 @@ import { getBubbleHome } from "../bubble-home.js";
16
16
  const require = createRequire(import.meta.url);
17
17
  export const PACKAGE_NAME = "@bubblebrain-ai/bubble";
18
18
  const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
19
- const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // once a day
19
+ // Throttle for the startup registry check. Short on purpose: with frequent
20
+ // releases, a long TTL means users only learn about a new version a day late.
21
+ const REFRESH_THROTTLE_MS = 30 * 60 * 1000;
20
22
  export function getCurrentVersion() {
21
23
  try {
22
24
  const pkg = require("../../package.json");
@@ -209,32 +211,52 @@ async function writeCache(cache) {
209
211
  // best-effort; never fail startup over a cache write
210
212
  }
211
213
  }
212
- async function refreshCacheInBackground(now) {
213
- const latest = await fetchLatestVersion(4000);
214
- if (latest) {
215
- await writeCache({ lastCheck: now, latest });
216
- }
214
+ function formatUpdateNotice(current, latest) {
215
+ return `Update available: v${current} → v${latest} · run \`bubble update\``;
217
216
  }
218
217
  /**
219
- * Returns a one-line "update available" notice if the cached latest version is
220
- * newer than the running one. Reads only a local cache file (fast, no network
221
- * on the hot path); a stale cache triggers a fire-and-forget refresh so the
222
- * next launch is accurate. Never throws.
218
+ * Startup "update available" check. The immediate `notice` comes from the
219
+ * local cache file (fast, no network on the hot path). A registry refresh
220
+ * always runs in the background (throttled to once per 30 minutes) so a
221
+ * release published since the last launch surfaces in the *current* session
222
+ * via `refreshed`, instead of only after the cache TTL plus another restart.
223
+ * Never throws.
223
224
  */
224
- export async function getStartupUpdateNotice() {
225
+ export async function startStartupUpdateCheck() {
225
226
  try {
226
227
  const current = getCurrentVersion();
227
228
  const now = Date.now();
228
229
  const cache = await readCache();
229
- if (!cache || now - cache.lastCheck > CHECK_INTERVAL_MS) {
230
- void refreshCacheInBackground(now);
231
- }
232
- if (cache && compareVersions(cache.latest, current) > 0) {
233
- return `Update available: v${current} → v${cache.latest} · run \`bubble update\``;
234
- }
235
- return null;
230
+ const notice = cache && compareVersions(cache.latest, current) > 0
231
+ ? formatUpdateNotice(current, cache.latest)
232
+ : null;
233
+ const refreshed = (async () => {
234
+ try {
235
+ if (cache && now - cache.lastCheck < REFRESH_THROTTLE_MS)
236
+ return null;
237
+ const latest = await fetchLatestVersion(4000);
238
+ if (!latest)
239
+ return null;
240
+ await writeCache({ lastCheck: now, latest });
241
+ if (compareVersions(latest, current) <= 0)
242
+ return null;
243
+ // The cache already surfaced this version in `notice` — stay quiet.
244
+ if (notice && cache && compareVersions(latest, cache.latest) <= 0)
245
+ return null;
246
+ return formatUpdateNotice(current, latest);
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ })();
252
+ return { notice, refreshed };
236
253
  }
237
254
  catch {
238
- return null;
255
+ return { notice: null, refreshed: Promise.resolve(null) };
239
256
  }
240
257
  }
258
+ /** Cache-only variant of {@link startStartupUpdateCheck} (still refreshes in the background). */
259
+ export async function getStartupUpdateNotice() {
260
+ const check = await startStartupUpdateCheck();
261
+ return check.notice;
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {