@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 +197 -34
- package/dist/agent/internal-reminder-sanitizer.js +29 -9
- package/dist/main.js +4 -2
- package/dist/model-catalog.js +6 -0
- package/dist/provider-transform.js +14 -0
- package/dist/tui/run.d.ts +6 -0
- package/dist/tui/run.js +63 -6
- package/dist/tui/trace-groups.js +41 -5
- package/dist/update/index.d.ts +18 -4
- package/dist/update/index.js +41 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
1
|
# Bubble
|
|
2
2
|
|
|
3
|
-
Bubble is a terminal coding agent
|
|
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
|
|
8
|
-
- Bun
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
## Install
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
25
|
npm install -g @bubblebrain-ai/bubble
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
To install from a local tarball:
|
|
25
29
|
|
|
26
30
|
```bash
|
|
27
|
-
npm install -g ./bubblebrain-ai-bubble
|
|
31
|
+
npm install -g ./bubblebrain-ai-bubble-<version>.tgz
|
|
28
32
|
```
|
|
29
33
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
Resume your last conversation:
|
|
50
56
|
|
|
51
57
|
```bash
|
|
52
|
-
bubble --
|
|
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
|
-
##
|
|
132
|
+
## What Bubble can do
|
|
56
133
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
Run `bubble update` at any time to upgrade to the latest published version.
|
|
203
|
+
|
|
204
|
+
## Configuration and storage
|
|
64
205
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
254
|
+
## Feishu host (optional)
|
|
90
255
|
|
|
91
|
-
Bubble
|
|
256
|
+
Bubble can also run as a Feishu (Lark) bot host:
|
|
92
257
|
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
/memory search <query>
|
|
96
|
-
/memory refresh
|
|
258
|
+
```bash
|
|
259
|
+
bubble serve --feishu --setup
|
|
97
260
|
```
|
|
98
261
|
|
|
99
|
-
|
|
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 =
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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 {
|
|
567
|
-
const
|
|
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 {
|
package/dist/model-catalog.js
CHANGED
|
@@ -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
|
-
|
|
5926
|
-
|
|
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,
|
|
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" },
|
|
8809
|
+
? h("text", { fg: theme.textMuted, wrapMode: "word" }, traceGroupOmittedLabel(group))
|
|
8753
8810
|
: null);
|
|
8754
8811
|
}
|
|
8755
8812
|
function renderTool(tool, syntaxStyle, width = 80) {
|
package/dist/tui/trace-groups.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
356
|
-
|
|
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"]) {
|
package/dist/update/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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>;
|
package/dist/update/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}
|