@bubblebrain-ai/bubble 0.0.16 → 0.0.18

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 (62) hide show
  1. package/dist/agent/internal-reminder-sanitizer.d.ts +2 -0
  2. package/dist/agent/internal-reminder-sanitizer.js +27 -0
  3. package/dist/agent/tool-intent.js +0 -1
  4. package/dist/agent.d.ts +1 -0
  5. package/dist/agent.js +148 -23
  6. package/dist/context/budget.js +15 -0
  7. package/dist/context/prune.d.ts +1 -0
  8. package/dist/context/prune.js +32 -0
  9. package/dist/debug-trace.js +14 -0
  10. package/dist/feishu/agent-host/run-driver.js +2 -2
  11. package/dist/feishu/card/run-state.js +1 -0
  12. package/dist/feishu/serve.js +1 -0
  13. package/dist/main.js +13 -9
  14. package/dist/model-catalog.d.ts +3 -0
  15. package/dist/model-catalog.js +38 -0
  16. package/dist/model-config.d.ts +3 -0
  17. package/dist/model-config.js +3 -0
  18. package/dist/model-pricing.js +2 -1
  19. package/dist/model-selection.d.ts +7 -0
  20. package/dist/model-selection.js +9 -0
  21. package/dist/network/chatgpt-transport.js +1 -0
  22. package/dist/orchestrator/default-hooks.js +1 -1
  23. package/dist/prompt/compose.js +1 -1
  24. package/dist/prompt/environment.js +1 -3
  25. package/dist/prompt/reminders.js +3 -3
  26. package/dist/prompt/runtime.js +2 -1
  27. package/dist/provider-anthropic.d.ts +89 -0
  28. package/dist/provider-anthropic.js +597 -0
  29. package/dist/provider-openai-codex.js +3 -1
  30. package/dist/provider-registry.d.ts +2 -0
  31. package/dist/provider-registry.js +29 -3
  32. package/dist/provider-transform.d.ts +1 -1
  33. package/dist/provider-transform.js +14 -0
  34. package/dist/provider.d.ts +4 -1
  35. package/dist/provider.js +120 -41
  36. package/dist/session-log.js +14 -2
  37. package/dist/session-title.js +3 -6
  38. package/dist/slash-commands/commands.js +8 -2
  39. package/dist/stats/usage.d.ts +1 -0
  40. package/dist/stats/usage.js +28 -3
  41. package/dist/tools/edit.js +75 -1
  42. package/dist/tools/glob.js +77 -12
  43. package/dist/tools/index.d.ts +1 -1
  44. package/dist/tools/index.js +1 -3
  45. package/dist/tools/prompt-metadata.d.ts +3 -0
  46. package/dist/tools/prompt-metadata.js +17 -0
  47. package/dist/tools/write.js +14 -0
  48. package/dist/tui/paste-placeholder.d.ts +10 -0
  49. package/dist/tui/paste-placeholder.js +45 -0
  50. package/dist/tui/run.js +23 -0
  51. package/dist/tui-ink/app.js +2 -0
  52. package/dist/tui-ink/input-box.d.ts +1 -8
  53. package/dist/tui-ink/input-box.js +8 -38
  54. package/dist/tui-opentui/app.js +2 -0
  55. package/dist/tui-opentui/input-box.d.ts +1 -3
  56. package/dist/tui-opentui/input-box.js +17 -26
  57. package/dist/types.d.ts +22 -0
  58. package/package.json +7 -3
  59. package/dist/tools/apply-patch.d.ts +0 -9
  60. package/dist/tools/apply-patch.js +0 -330
  61. package/dist/tools/patch-apply.d.ts +0 -41
  62. package/dist/tools/patch-apply.js +0 -312
@@ -11,7 +11,7 @@ import { fetchOpenAICodexModels } from "./provider-openai-codex.js";
11
11
  import { refreshOpenAICodex } from "./oauth/openai-codex.js";
12
12
  export const BUILTIN_PROVIDERS = CATALOG_PROVIDERS;
13
13
  export const USER_VISIBLE_PROVIDER_IDS = BUILTIN_PROVIDERS
14
- .filter((provider) => provider.id !== "openrouter" && provider.id !== "openai-codex")
14
+ .filter((provider) => !provider.hidden && provider.id !== "openrouter" && provider.id !== "openai-codex")
15
15
  .map((provider) => provider.id);
16
16
  export function isUserVisibleProvider(providerId) {
17
17
  return USER_VISIBLE_PROVIDER_IDS.includes(providerId);
@@ -123,19 +123,27 @@ export class ProviderRegistry {
123
123
  providers = keys.map((id) => {
124
124
  const builtin = getBuiltinProvider(id);
125
125
  const cfg = modelsJsonProviders[id];
126
+ const baseURL = cfg.baseURL || builtin?.baseURL || "";
126
127
  return {
127
128
  id,
128
129
  name: builtin?.name || id,
129
- baseURL: cfg.baseURL || builtin?.baseURL || "",
130
+ baseURL,
130
131
  apiKey: cfg.apiKey || "",
131
132
  enabled: true,
132
133
  authType: "api",
134
+ protocol: resolveConfiguredProtocol(id, baseURL, cfg.protocol),
133
135
  };
134
136
  });
135
137
  }
136
138
  else {
137
139
  // 2. Fall back to config.json providers (interactive TUI style)
138
- providers = this.config.getProviders();
140
+ providers = this.config.getProviders().map((provider) => {
141
+ const builtin = getBuiltinProvider(provider.id);
142
+ return {
143
+ ...provider,
144
+ protocol: resolveConfiguredProtocol(provider.id, provider.baseURL, provider.protocol),
145
+ };
146
+ });
139
147
  }
140
148
  // 3. Inject OAuth access tokens
141
149
  for (const p of providers) {
@@ -283,6 +291,24 @@ export class ProviderRegistry {
283
291
  }));
284
292
  }
285
293
  }
294
+ function resolveConfiguredProtocol(providerId, baseURL, explicitProtocol) {
295
+ if (explicitProtocol)
296
+ return explicitProtocol;
297
+ const builtin = getBuiltinProvider(providerId);
298
+ if (!builtin?.protocol)
299
+ return undefined;
300
+ const normalizedBaseURL = normalizeBaseURL(baseURL);
301
+ if (!normalizedBaseURL || normalizedBaseURL === normalizeBaseURL(builtin.baseURL)) {
302
+ return builtin.protocol;
303
+ }
304
+ if (normalizedBaseURL.includes("/anthropic")) {
305
+ return "anthropic-messages";
306
+ }
307
+ return undefined;
308
+ }
309
+ function normalizeBaseURL(baseURL) {
310
+ return baseURL.trim().replace(/\/+$/, "").toLowerCase();
311
+ }
286
312
  /** Encode a model selection as "providerId:modelId". */
287
313
  export function encodeModel(providerId, modelId) {
288
314
  return `${providerId}:${modelId}`;
@@ -3,7 +3,7 @@ export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
3
3
  export interface ProviderRequestConfig {
4
4
  effectiveThinkingLevel: ThinkingLevel;
5
5
  reasoningEffort?: ThinkingLevel;
6
- reasoningContentEcho?: "tool_calls" | "all" | "none";
6
+ reasoningContentEcho?: "tool_calls" | "all" | "none" | "minimax";
7
7
  parallelToolCalls?: boolean;
8
8
  maxTokens?: number;
9
9
  extraBody?: Record<string, unknown>;
@@ -4,6 +4,7 @@ const MOONSHOT_PROVIDER_IDS = new Set(["moonshot-cn", "moonshot-intl", "kimi-for
4
4
  const KIMI_K25_FAMILY = new Set(["kimi-k2.5", "k2.6-code-preview", "kimi-k2.6"]);
5
5
  const KIMI_THINKING_FAMILY = new Set(["kimi-k2-thinking", "kimi-k2-thinking-turbo"]);
6
6
  const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
7
+ const MINIMAX_M3_FAMILY = new Set(["MiniMax-M3"]);
7
8
  function isFireworksKimi(providerId, modelId) {
8
9
  const model = modelId.toLowerCase();
9
10
  return providerId === "fireworks" && (model.includes("kimi")
@@ -45,6 +46,19 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
45
46
  : { reasoning_effort: effectiveThinkingLevel },
46
47
  };
47
48
  }
49
+ if (providerId === "minimax" || providerId === "minimax-openai") {
50
+ const extraBody = { reasoning_split: true };
51
+ if (MINIMAX_M3_FAMILY.has(modelId)) {
52
+ extraBody.thinking = {
53
+ type: effectiveThinkingLevel === "off" ? "disabled" : "adaptive",
54
+ };
55
+ }
56
+ return {
57
+ effectiveThinkingLevel,
58
+ reasoningContentEcho: "minimax",
59
+ extraBody,
60
+ };
61
+ }
48
62
  // Zhipu/Z.AI OpenAI-compatible endpoints expose reasoning via a provider-specific
49
63
  // `thinking` block rather than OpenAI's `reasoning_effort` shape.
50
64
  if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)) {
@@ -4,12 +4,14 @@
4
4
  * Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
5
5
  */
6
6
  import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
7
+ import type { ProviderProtocol } from "./model-catalog.js";
7
8
  import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
8
- type ReasoningContentEcho = "tool_calls" | "all" | "none";
9
+ type ReasoningContentEcho = "tool_calls" | "all" | "none" | "minimax";
9
10
  export type ToolArgsMergeMode = "delta" | "snapshot";
10
11
  export interface TranslateOpenAIStreamOptions {
11
12
  toolArgsMergeMode?: ToolArgsMergeMode;
12
13
  reasoningMergeMode?: ToolArgsMergeMode;
14
+ textMergeMode?: ToolArgsMergeMode;
13
15
  debugProviderId?: string;
14
16
  debugModelId?: string;
15
17
  }
@@ -24,6 +26,7 @@ export interface ProviderInstanceOptions {
24
26
  thinkingLevel?: ThinkingLevel;
25
27
  /** Stable per-session seed for provider prompt caches. */
26
28
  promptCacheKey?: string;
29
+ protocol?: ProviderProtocol;
27
30
  /** Dynamic OAuth access-token loader/refresh hook for ChatGPT Codex requests. */
28
31
  openAICodexAuth?: OpenAICodexAuthAdapter;
29
32
  }
package/dist/provider.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import OpenAI from "openai";
7
7
  import { appendFileSync } from "node:fs";
8
+ import { createAnthropicMessagesProvider } from "./provider-anthropic.js";
8
9
  import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
9
10
  import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
10
11
  import { resolveProviderRequestConfig } from "./provider-transform.js";
@@ -36,6 +37,15 @@ export function toChatCompletionsMessage(message, options = {}) {
36
37
  // provider field, even when the original value is an empty string.
37
38
  out.reasoning_content = message.reasoning ?? "";
38
39
  }
40
+ if (reasoningContentEcho === "minimax" && message.reasoning) {
41
+ out.reasoning_details = [{
42
+ type: "reasoning.text",
43
+ id: "reasoning-text-1",
44
+ format: "MiniMax-response-v1",
45
+ index: 0,
46
+ text: message.reasoning,
47
+ }];
48
+ }
39
49
  if (message.toolCalls && message.toolCalls.length > 0) {
40
50
  out.tool_calls = message.toolCalls.map((tc) => ({
41
51
  id: tc.id,
@@ -65,6 +75,9 @@ export function createUnavailableProvider(message) {
65
75
  return { streamChat, complete };
66
76
  }
67
77
  export function createProviderInstance(options) {
78
+ if (resolveProviderProtocol(options) === "anthropic-messages") {
79
+ return createAnthropicMessagesProvider(options);
80
+ }
68
81
  if (isOpenAICodexBaseUrl(options.baseURL)) {
69
82
  return createOpenAICodexProvider({
70
83
  ...options,
@@ -93,11 +106,11 @@ export function createProviderInstance(options) {
93
106
  reasoningContentEcho: requestConfig.reasoningContentEcho ?? "tool_calls",
94
107
  })),
95
108
  tools: tools && tools.length > 0 ? tools : undefined,
96
- tool_choice: tools && tools.length > 0 ? "auto" : undefined,
109
+ tool_choice: tools && tools.length > 0 ? chatOptions.toolChoice ?? "auto" : undefined,
97
110
  stream: true,
98
111
  };
99
- // DeepSeek only emits final usage in streaming mode when this flag is set.
100
- if (options.providerId === "deepseek") {
112
+ // DeepSeek and MiniMax only emit final usage in streaming mode when this flag is set.
113
+ if (options.providerId === "deepseek" || isMiniMaxOpenAICompatible(options)) {
101
114
  body.stream_options = { include_usage: true };
102
115
  }
103
116
  if (!requestConfig.omitTemperature) {
@@ -121,6 +134,7 @@ export function createProviderInstance(options) {
121
134
  yield* translateOpenAIStream(stream, {
122
135
  toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL),
123
136
  reasoningMergeMode: resolveReasoningMergeMode(options.providerId || "", options.baseURL),
137
+ textMergeMode: resolveTextMergeMode(options.providerId || "", options.baseURL),
124
138
  debugProviderId: options.providerId || "",
125
139
  debugModelId: chatOptions.model,
126
140
  });
@@ -153,6 +167,26 @@ export function createProviderInstance(options) {
153
167
  }
154
168
  return { streamChat, complete };
155
169
  }
170
+ function resolveProviderProtocol(options) {
171
+ if (options.protocol)
172
+ return options.protocol;
173
+ const providerId = (options.providerId || "").toLowerCase();
174
+ const baseURL = options.baseURL.toLowerCase();
175
+ if (providerId === "anthropic"
176
+ || providerId.endsWith("-anthropic")
177
+ || baseURL.includes("/anthropic")) {
178
+ return "anthropic-messages";
179
+ }
180
+ return "openai-chat";
181
+ }
182
+ function isMiniMaxOpenAICompatible(options) {
183
+ const providerId = (options.providerId || "").toLowerCase();
184
+ const baseURL = options.baseURL.toLowerCase();
185
+ return providerId === "minimax-openai"
186
+ || (providerId === "minimax" && !baseURL.includes("/anthropic"))
187
+ || baseURL.includes("api.minimaxi.com/v1")
188
+ || baseURL.includes("api.minimax.io/v1");
189
+ }
156
190
  export function normalizeToolArgsDetailed(raw) {
157
191
  const s = (raw ?? "").trim();
158
192
  if (!s) {
@@ -210,6 +244,15 @@ function resolveReasoningMergeMode(providerId, baseURL) {
210
244
  const url = baseURL.toLowerCase();
211
245
  if (id === "fireworks" || url.includes("fireworks.ai"))
212
246
  return "snapshot";
247
+ if (id === "minimax" || url.includes("api.minimaxi.com") || url.includes("api.minimax.io"))
248
+ return "snapshot";
249
+ return "delta";
250
+ }
251
+ function resolveTextMergeMode(providerId, baseURL) {
252
+ const id = providerId.toLowerCase();
253
+ const url = baseURL.toLowerCase();
254
+ if (id === "minimax" || url.includes("api.minimaxi.com") || url.includes("api.minimax.io"))
255
+ return "snapshot";
213
256
  return "delta";
214
257
  }
215
258
  function extractBalancedJson(s, start) {
@@ -257,7 +300,9 @@ export async function* translateOpenAIStream(stream, options = {}) {
257
300
  const textFilter = createProviderProtocolArtifactFilter();
258
301
  const toolArgsMergeMode = options.toolArgsMergeMode ?? "delta";
259
302
  const reasoningMergeMode = options.reasoningMergeMode ?? "delta";
303
+ const textMergeMode = options.textMergeMode ?? "delta";
260
304
  let reasoningBuffer = "";
305
+ let textBuffer = "";
261
306
  let rawChunkSeq = 0;
262
307
  // DeepSeek (and some inference re-hosts) sometimes deliver reasoning twice:
263
308
  // once via a dedicated `reasoning_content` / `thinking` field, and again
@@ -318,6 +363,7 @@ export async function* translateOpenAIStream(stream, options = {}) {
318
363
  reasoning: summarizeDebugText(delta?.reasoning),
319
364
  thinking: summarizeDebugText(delta?.thinking),
320
365
  reasoningContent: summarizeDebugText(delta?.reasoning_content),
366
+ reasoningDetails: summarizeDebugText(extractReasoningDetailsText(delta?.reasoning_details)),
321
367
  });
322
368
  if (usage) {
323
369
  yield {
@@ -325,8 +371,16 @@ export async function* translateOpenAIStream(stream, options = {}) {
325
371
  usage: {
326
372
  promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
327
373
  completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
328
- promptCacheHitTokens: typeof usage.prompt_cache_hit_tokens === "number" ? usage.prompt_cache_hit_tokens : undefined,
329
- promptCacheMissTokens: typeof usage.prompt_cache_miss_tokens === "number" ? usage.prompt_cache_miss_tokens : undefined,
374
+ promptCacheHitTokens: typeof usage.prompt_cache_hit_tokens === "number"
375
+ ? usage.prompt_cache_hit_tokens
376
+ : typeof usage.prompt_tokens_details?.cached_tokens === "number"
377
+ ? usage.prompt_tokens_details.cached_tokens
378
+ : undefined,
379
+ promptCacheMissTokens: typeof usage.prompt_cache_miss_tokens === "number"
380
+ ? usage.prompt_cache_miss_tokens
381
+ : typeof usage.prompt_tokens_details?.cached_tokens === "number" && typeof usage.prompt_tokens === "number"
382
+ ? Math.max(0, usage.prompt_tokens - usage.prompt_tokens_details.cached_tokens)
383
+ : undefined,
330
384
  reasoningTokens: typeof usage.completion_tokens_details?.reasoning_tokens === "number"
331
385
  ? usage.completion_tokens_details.reasoning_tokens
332
386
  : undefined,
@@ -334,14 +388,21 @@ export async function* translateOpenAIStream(stream, options = {}) {
334
388
  },
335
389
  };
336
390
  }
337
- const reasoningField = delta?.reasoning !== undefined
338
- ? "reasoning"
339
- : delta?.thinking !== undefined
340
- ? "thinking"
341
- : delta?.reasoning_content !== undefined
342
- ? "reasoning_content"
343
- : undefined;
344
- const reasoning = reasoningField ? delta[reasoningField] : undefined;
391
+ const reasoningDetails = extractReasoningDetailsText(delta?.reasoning_details);
392
+ const reasoningField = reasoningDetails !== undefined
393
+ ? "reasoning_details"
394
+ : delta?.reasoning !== undefined
395
+ ? "reasoning"
396
+ : delta?.thinking !== undefined
397
+ ? "thinking"
398
+ : delta?.reasoning_content !== undefined
399
+ ? "reasoning_content"
400
+ : undefined;
401
+ const reasoning = reasoningDetails !== undefined
402
+ ? reasoningDetails
403
+ : reasoningField
404
+ ? delta[reasoningField]
405
+ : undefined;
345
406
  if (reasoning) {
346
407
  hasDedicatedReasoningChannel = true;
347
408
  const merged = mergeStreamingText(reasoningBuffer, reasoning, reasoningMergeMode);
@@ -362,36 +423,41 @@ export async function* translateOpenAIStream(stream, options = {}) {
362
423
  }
363
424
  }
364
425
  if (delta?.content) {
365
- const thinkMatch = delta.content.match(/<think>([\s\S]*?)<\/think>/);
366
- if (thinkMatch) {
367
- if (thinkMatch[1] && !hasDedicatedReasoningChannel) {
368
- const merged = mergeStreamingText(reasoningBuffer, thinkMatch[1], reasoningMergeMode);
369
- reasoningBuffer = merged.args;
370
- debugReasoningStream({
371
- stage: "provider_emit",
372
- providerId: options.debugProviderId,
373
- modelId: options.debugModelId,
374
- chunkSeq: rawChunkSeq,
375
- source: "content_think",
376
- mergeMode: reasoningMergeMode,
377
- suppressed: !merged.delta,
378
- emitted: summarizeDebugText(merged.delta),
379
- buffer: summarizeDebugText(reasoningBuffer),
380
- });
381
- if (merged.delta) {
382
- yield { type: "reasoning_delta", content: merged.delta };
426
+ const mergedContent = mergeStreamingText(textBuffer, delta.content, textMergeMode);
427
+ textBuffer = mergedContent.args;
428
+ const content = mergedContent.delta;
429
+ if (content) {
430
+ const thinkMatch = content.match(/<think>([\s\S]*?)<\/think>/);
431
+ if (thinkMatch) {
432
+ if (thinkMatch[1] && !hasDedicatedReasoningChannel) {
433
+ const merged = mergeStreamingText(reasoningBuffer, thinkMatch[1], reasoningMergeMode);
434
+ reasoningBuffer = merged.args;
435
+ debugReasoningStream({
436
+ stage: "provider_emit",
437
+ providerId: options.debugProviderId,
438
+ modelId: options.debugModelId,
439
+ chunkSeq: rawChunkSeq,
440
+ source: "content_think",
441
+ mergeMode: reasoningMergeMode,
442
+ suppressed: !merged.delta,
443
+ emitted: summarizeDebugText(merged.delta),
444
+ buffer: summarizeDebugText(reasoningBuffer),
445
+ });
446
+ if (merged.delta) {
447
+ yield { type: "reasoning_delta", content: merged.delta };
448
+ }
449
+ }
450
+ const remaining = content.replace(/<think>[\s\S]*?<\/think>/, "");
451
+ const cleaned = textFilter.push(remaining);
452
+ if (cleaned) {
453
+ yield { type: "text", content: cleaned };
383
454
  }
384
455
  }
385
- const remaining = delta.content.replace(/<think>[\s\S]*?<\/think>/, "");
386
- const cleaned = textFilter.push(remaining);
387
- if (cleaned) {
388
- yield { type: "text", content: cleaned };
389
- }
390
- }
391
- else {
392
- const cleaned = textFilter.push(delta.content);
393
- if (cleaned) {
394
- yield { type: "text", content: cleaned };
456
+ else {
457
+ const cleaned = textFilter.push(content);
458
+ if (cleaned) {
459
+ yield { type: "text", content: cleaned };
460
+ }
395
461
  }
396
462
  }
397
463
  }
@@ -435,6 +501,19 @@ export async function* translateOpenAIStream(stream, options = {}) {
435
501
  }
436
502
  yield* flushToolCalls();
437
503
  }
504
+ function extractReasoningDetailsText(value) {
505
+ if (!value)
506
+ return undefined;
507
+ const details = Array.isArray(value) ? value : [value];
508
+ const parts = details.flatMap((item) => {
509
+ if (!item || typeof item !== "object")
510
+ return [];
511
+ const record = item;
512
+ const text = record.text ?? record.thinking ?? record.content;
513
+ return typeof text === "string" ? [text] : [];
514
+ });
515
+ return parts.length > 0 ? parts.join("") : undefined;
516
+ }
438
517
  function mergeToolArgumentDelta(current, incoming, mode) {
439
518
  if (!current) {
440
519
  debugToolArgs({ stage: "merge", branch: "empty-current", current, incoming, args: incoming, delta: incoming });
@@ -1,4 +1,4 @@
1
- import { sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
1
+ import { sanitizeAssistantProviderMetadata, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
2
2
  export class SessionLog {
3
3
  entries = [];
4
4
  load(lines) {
@@ -128,6 +128,11 @@ export class SessionLog {
128
128
  messages.push({
129
129
  ...entry.message,
130
130
  role: "assistant",
131
+ content: sanitizeInternalReminderBlocks(entry.message.content),
132
+ reasoning: entry.message.reasoning !== undefined
133
+ ? sanitizeInternalReminderBlocks(entry.message.reasoning)
134
+ : undefined,
135
+ providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(entry.message.providerMetadata)),
131
136
  });
132
137
  break;
133
138
  case "tool_call": {
@@ -190,7 +195,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
190
195
  type: "assistant_message",
191
196
  message: {
192
197
  role: "assistant",
193
- content: message.content,
198
+ content: sanitizeInternalReminderBlocks(message.content),
194
199
  reasoning: message.reasoning !== undefined
195
200
  ? sanitizeInternalReminderBlocks(message.reasoning)
196
201
  : undefined,
@@ -199,6 +204,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
199
204
  modelId: message.modelId,
200
205
  usage: message.usage,
201
206
  error: message.error,
207
+ providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(message.providerMetadata)),
202
208
  },
203
209
  timestamp,
204
210
  };
@@ -250,6 +256,7 @@ function cloneMessage(message) {
250
256
  return {
251
257
  ...message,
252
258
  toolCalls: message.toolCalls?.map((toolCall) => ({ ...toolCall })),
259
+ providerMetadata: cloneProviderMetadata(message.providerMetadata),
253
260
  };
254
261
  }
255
262
  if (message.role === "user" && Array.isArray(message.content)) {
@@ -263,6 +270,11 @@ function cloneMessage(message) {
263
270
  }
264
271
  return { ...message };
265
272
  }
273
+ function cloneProviderMetadata(metadata) {
274
+ if (metadata === undefined)
275
+ return undefined;
276
+ return JSON.parse(JSON.stringify(metadata));
277
+ }
266
278
  function pruneIncompleteTail(messages) {
267
279
  let currentTurnStart = -1;
268
280
  let hasCompletedAssistant = false;
@@ -1,6 +1,5 @@
1
1
  import { normalizeSingleLine, truncateVisual } from "./text-display.js";
2
- const LONG_PASTE_CHAR_THRESHOLD = 1000;
3
- const LONG_PASTE_LINE_THRESHOLD = 20;
2
+ import { createPastedContentMarker, shouldCollapsePastedContent } from "./tui/paste-placeholder.js";
4
3
  const TITLE_INPUT_MAX_CHARS = 4000;
5
4
  const TITLE_MAX_WIDTH = 80;
6
5
  const TITLE_SYSTEM_PROMPT = [
@@ -81,10 +80,8 @@ export function deterministicTitleFromUserContent(content) {
81
80
  const text = userContentText(content);
82
81
  if (!text)
83
82
  return "User message";
84
- const charCount = text.length;
85
- const lineCount = text.split(/\r?\n/).length;
86
- if (charCount > LONG_PASTE_CHAR_THRESHOLD || lineCount > LONG_PASTE_LINE_THRESHOLD) {
87
- return `[Pasted Content ${charCount} chars]`;
83
+ if (shouldCollapsePastedContent(text)) {
84
+ return createPastedContentMarker(text);
88
85
  }
89
86
  return truncateVisual(normalizeSingleLine(text), TITLE_MAX_WIDTH) || "User message";
90
87
  }
@@ -4,7 +4,7 @@ import { formatDiagnostics } from "../lsp/index.js";
4
4
  import { normalizeNameForMCP } from "../mcp/name.js";
5
5
  import { parseRule } from "../permissions/rule.js";
6
6
  import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
7
- import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
7
+ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
8
8
  import { buildSystemPrompt } from "../system-prompt.js";
9
9
  import { isThinkingLevel } from "../variant/thinking-level.js";
10
10
  import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
@@ -61,6 +61,9 @@ function persistSelectedModel(model, ctx) {
61
61
  }
62
62
  function syncSystemPrompt(ctx, model) {
63
63
  const { providerId, modelId } = decodeModel(model);
64
+ const toolPromptOptions = typeof ctx.agent.getSystemPromptToolOptions === "function"
65
+ ? ctx.agent.getSystemPromptToolOptions()
66
+ : {};
64
67
  ctx.agent.setSystemPrompt(buildSystemPrompt({
65
68
  agentName: "Bubble",
66
69
  configuredProvider: providerId,
@@ -68,6 +71,7 @@ function syncSystemPrompt(ctx, model) {
68
71
  configuredModelId: model,
69
72
  thinkingLevel: ctx.agent.thinking,
70
73
  workingDir: ctx.cwd,
74
+ ...toolPromptOptions,
71
75
  memoryPrompt: buildMemoryPrompt(ctx.cwd),
72
76
  }));
73
77
  }
@@ -131,7 +135,9 @@ function parseModelArgs(args) {
131
135
  }
132
136
  function displaySelectedModel(model, thinkingLevel) {
133
137
  const label = displayModel(model);
134
- return thinkingLevel === "off" ? label : `${label} (${thinkingLevel})`;
138
+ const { providerId, modelId } = decodeModel(model);
139
+ const defaultLevel = providerId ? getDefaultThinkingLevel(providerId, modelId) : "off";
140
+ return thinkingLevel === "off" || thinkingLevel === defaultLevel ? label : `${label} (${thinkingLevel})`;
135
141
  }
136
142
  function parseMemoryScopeArgs(args) {
137
143
  const tokens = args.trim().split(/\s+/).filter(Boolean);
@@ -20,6 +20,7 @@ export interface ModelUsageStats {
20
20
  completionTokens: number;
21
21
  promptCacheHitTokens: number;
22
22
  promptCacheMissTokens: number;
23
+ cacheCreationTokens: number;
23
24
  reasoningTokens: number;
24
25
  totalTokens: number;
25
26
  cost?: number;
@@ -187,6 +187,7 @@ function addModelUsage(accumulator, model, message, usage) {
187
187
  completionTokens: 0,
188
188
  promptCacheHitTokens: 0,
189
189
  promptCacheMissTokens: 0,
190
+ cacheCreationTokens: 0,
190
191
  reasoningTokens: 0,
191
192
  totalTokens: 0,
192
193
  };
@@ -195,6 +196,7 @@ function addModelUsage(accumulator, model, message, usage) {
195
196
  existing.completionTokens += usage.completionTokens;
196
197
  existing.promptCacheHitTokens += usage.promptCacheHitTokens ?? 0;
197
198
  existing.promptCacheMissTokens += usage.promptCacheMissTokens ?? 0;
199
+ existing.cacheCreationTokens += usage.cacheCreationTokens ?? 0;
198
200
  existing.reasoningTokens += usage.reasoningTokens ?? 0;
199
201
  existing.totalTokens += tokenTotal(usage);
200
202
  if (providerId && modelId) {
@@ -284,6 +286,9 @@ function formatSummaryLines(stats, width) {
284
286
  if (favorite) {
285
287
  lines.push(` Favorite model ${truncate(favorite, Math.max(12, width - 17))}`);
286
288
  }
289
+ const cacheSummary = formatCacheSummary(stats.models);
290
+ if (cacheSummary)
291
+ lines.push(` ${cacheSummary}`);
287
292
  const trackedCostText = formatTrackedCosts(stats);
288
293
  if (trackedCostText)
289
294
  lines.push(` Tracked cost ${trackedCostText}`);
@@ -293,6 +298,17 @@ function formatSummaryLines(stats, width) {
293
298
  }
294
299
  return lines;
295
300
  }
301
+ function formatCacheSummary(models) {
302
+ const read = models.reduce((sum, model) => sum + model.promptCacheHitTokens, 0);
303
+ const create = models.reduce((sum, model) => sum + model.cacheCreationTokens, 0);
304
+ const missWithCreate = models.reduce((sum, model) => sum + model.promptCacheMissTokens, 0);
305
+ const miss = Math.max(0, missWithCreate - create);
306
+ const observed = read + create + miss;
307
+ if (observed === 0)
308
+ return undefined;
309
+ const hitRate = Math.round((read / observed) * 100);
310
+ return `Prompt cache ${formatCompactNumber(read)} read · ${formatCompactNumber(create)} create · ${formatCompactNumber(miss)} miss · ${hitRate}% hit`;
311
+ }
296
312
  function aggregateCosts(models) {
297
313
  const totals = {};
298
314
  for (const model of models) {
@@ -355,15 +371,24 @@ function normalizeUsage(raw) {
355
371
  if (!raw || typeof raw !== "object")
356
372
  return undefined;
357
373
  const value = raw;
358
- const promptTokens = numberValue(value.promptTokens) ?? numberValue(value.input_tokens);
374
+ const rawInputTokens = numberValue(value.input_tokens);
375
+ const cacheReadTokens = numberValue(value.promptCacheHitTokens) ?? numberValue(value.cache_read_input_tokens);
376
+ const cacheCreationTokens = numberValue(value.cacheCreationTokens) ?? numberValue(value.cache_creation_input_tokens);
377
+ const promptTokens = numberValue(value.promptTokens)
378
+ ?? (rawInputTokens !== undefined
379
+ ? rawInputTokens + (cacheReadTokens ?? 0) + (cacheCreationTokens ?? 0)
380
+ : undefined);
359
381
  const completionTokens = numberValue(value.completionTokens) ?? numberValue(value.output_tokens);
360
382
  if (promptTokens === undefined || completionTokens === undefined)
361
383
  return undefined;
362
384
  return {
363
385
  promptTokens,
364
386
  completionTokens,
365
- promptCacheHitTokens: numberValue(value.promptCacheHitTokens) ?? numberValue(value.cache_read_input_tokens),
366
- promptCacheMissTokens: numberValue(value.promptCacheMissTokens) ?? numberValue(value.cache_creation_input_tokens),
387
+ promptCacheHitTokens: cacheReadTokens,
388
+ promptCacheMissTokens: numberValue(value.promptCacheMissTokens)
389
+ ?? numberValue(value.cache_miss_input_tokens)
390
+ ?? (rawInputTokens !== undefined ? rawInputTokens + (cacheCreationTokens ?? 0) : undefined),
391
+ cacheCreationTokens,
367
392
  reasoningTokens: numberValue(value.reasoningTokens),
368
393
  totalTokens: numberValue(value.totalTokens) ?? numberValue(value.total_tokens),
369
394
  };