@bubblebrain-ai/bubble 0.0.48 → 0.0.50

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 (51) hide show
  1. package/README.md +17 -0
  2. package/dist/agent/abort-errors.d.ts +4 -0
  3. package/dist/agent/abort-errors.js +30 -0
  4. package/dist/agent/provider-fingerprint.d.ts +6 -0
  5. package/dist/agent/provider-fingerprint.js +89 -0
  6. package/dist/agent/resident-history.d.ts +7 -0
  7. package/dist/agent/resident-history.js +61 -0
  8. package/dist/agent/transcript-helpers.d.ts +20 -0
  9. package/dist/agent/transcript-helpers.js +62 -0
  10. package/dist/agent/update-queue.d.ts +13 -0
  11. package/dist/agent/update-queue.js +69 -0
  12. package/dist/agent.d.ts +33 -16
  13. package/dist/agent.js +90 -300
  14. package/dist/external-runtime/grok-runtime.js +1 -1
  15. package/dist/feishu/agent-host/run-driver.js +1 -1
  16. package/dist/lsp/service.js +2 -2
  17. package/dist/model-catalog.d.ts +13 -0
  18. package/dist/model-catalog.js +52 -8
  19. package/dist/permissions/settings.d.ts +0 -1
  20. package/dist/permissions/settings.js +0 -2
  21. package/dist/provider-grok.d.ts +16 -18
  22. package/dist/provider-grok.js +28 -20
  23. package/dist/provider-registry.d.ts +9 -0
  24. package/dist/provider-registry.js +109 -6
  25. package/dist/provider-transform.js +7 -8
  26. package/dist/provider.js +11 -0
  27. package/dist/session-log.js +1 -0
  28. package/dist/slash-commands/commands.js +13 -9
  29. package/dist/tools/grep.js +1 -1
  30. package/dist/tui-ink/app-helpers.d.ts +43 -0
  31. package/dist/tui-ink/app-helpers.js +150 -0
  32. package/dist/tui-ink/app.d.ts +4 -11
  33. package/dist/tui-ink/app.js +104 -635
  34. package/dist/tui-ink/command-palette.d.ts +32 -0
  35. package/dist/tui-ink/command-palette.js +158 -0
  36. package/dist/tui-ink/display-history.d.ts +2 -0
  37. package/dist/tui-ink/display-reconstruct.d.ts +3 -0
  38. package/dist/tui-ink/display-reconstruct.js +82 -0
  39. package/dist/tui-ink/footer.d.ts +14 -3
  40. package/dist/tui-ink/footer.js +19 -5
  41. package/dist/tui-ink/rewind-picker.d.ts +8 -0
  42. package/dist/tui-ink/rewind-picker.js +102 -0
  43. package/dist/tui-ink/session-picker.js +1 -1
  44. package/dist/tui-ink/stats-panel.d.ts +11 -0
  45. package/dist/tui-ink/stats-panel.js +65 -0
  46. package/dist/tui-ink/waiting-indicator.d.ts +12 -0
  47. package/dist/tui-ink/waiting-indicator.js +129 -0
  48. package/dist/tui-ink/wordmark.d.ts +8 -0
  49. package/dist/tui-ink/wordmark.js +103 -0
  50. package/dist/types.d.ts +6 -0
  51. package/package.json +1 -1
@@ -25,7 +25,6 @@ const KNOWN_MODES = new Set([
25
25
  "bypassPermissions",
26
26
  ]);
27
27
  export class SettingsManager {
28
- cwd;
29
28
  paths;
30
29
  raw = {
31
30
  user: null,
@@ -34,7 +33,6 @@ export class SettingsManager {
34
33
  };
35
34
  fileDiagnostics = [];
36
35
  constructor(cwd, options = {}) {
37
- this.cwd = cwd;
38
36
  const bubbleHome = options.bubbleHome ?? getBubbleHome();
39
37
  this.paths = {
40
38
  user: join(bubbleHome, "settings.json"),
@@ -9,21 +9,10 @@
9
9
  */
10
10
  import type { OAuthCredentials } from "./oauth/types.js";
11
11
  import { type ChatGptFetch } from "./network/chatgpt-transport.js";
12
- import type { ThinkingLevel } from "./types.js";
12
+ import { type ThinkingLevel } from "./types.js";
13
13
  export declare const GROK_SUBSCRIPTION_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
14
14
  export declare const GROK_CLIENT_VERSION = "0.2.93";
15
15
  export declare function isGrokSubscriptionBaseUrl(baseURL: string): boolean;
16
- /**
17
- * Infer reasoning levels for a Grok model id the /models endpoint returned
18
- * without metadata. Flagship reasoning models (grok-N.M) expose
19
- * low/medium/high with thinking always on — matching the curated grok-4.5
20
- * entry — while composer/fast/mini/flash variants have no effort control and
21
- * map to the plain "off" ladder.
22
- */
23
- export declare function inferGrokReasoningLevels(modelId: string): {
24
- levels: ThinkingLevel[];
25
- defaultLevel?: ThinkingLevel;
26
- };
27
16
  export declare function buildGrokSubscriptionHeaders(): Record<string, string>;
28
17
  export interface GrokAuthAdapter {
29
18
  getCredentials: () => OAuthCredentials | undefined | Promise<OAuthCredentials | undefined>;
@@ -36,17 +25,26 @@ export interface GrokAuthAdapter {
36
25
  * proactively refreshes) the stored OAuth credentials per request instead.
37
26
  */
38
27
  export declare function createGrokSubscriptionFetch(auth: GrokAuthAdapter, baseFetch?: ChatGptFetch): ChatGptFetch;
28
+ export interface GrokModelDescriptor {
29
+ id: string;
30
+ name?: string;
31
+ /** Server-declared context window (the /models `context_window` field). */
32
+ contextWindow?: number;
33
+ /** Server-declared reasoning effort ladder, sorted ascending. */
34
+ reasoningEfforts?: ThinkingLevel[];
35
+ /** Server-declared default effort (the /models `reasoning_effort` field). */
36
+ defaultReasoningEffort?: ThinkingLevel;
37
+ }
39
38
  /**
40
39
  * Fetch the model list for a Grok subscription account. The CLI chat proxy
41
40
  * gates on the grok-cli identity headers plus a fresh OAuth bearer, so this
42
41
  * rides the same refreshing fetch used for chat requests instead of a raw
43
- * global fetch. Throws on a non-2xx so callers fall back to the curated
44
- * catalog when the proxy exposes no /models endpoint.
42
+ * global fetch. The /models payload carries richer metadata than id+name —
43
+ * `context_window`, `reasoning_efforts`, and the default `reasoning_effort` —
44
+ * which this surfaces so discovery can stop inferring them. Throws on a
45
+ * non-2xx so callers fall back to the curated catalog.
45
46
  */
46
47
  export declare function fetchGrokSubscriptionModels(baseURL: string, auth: GrokAuthAdapter, options?: {
47
48
  timeoutMs?: number;
48
49
  fetch?: ChatGptFetch;
49
- }): Promise<Array<{
50
- id: string;
51
- name?: string;
52
- }>>;
50
+ }): Promise<GrokModelDescriptor[]>;
@@ -8,6 +8,7 @@
8
8
  * OAuth bearer fresh across long sessions.
9
9
  */
10
10
  import { getChatGptFetch } from "./network/chatgpt-transport.js";
11
+ import { THINKING_LEVELS } from "./types.js";
11
12
  export const GROK_SUBSCRIPTION_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
12
13
  // The proxy rejects requests below a minimum client version; identify as the
13
14
  // same pinned CLI build Bubble verifies elsewhere.
@@ -17,20 +18,6 @@ export function isGrokSubscriptionBaseUrl(baseURL) {
17
18
  const normalized = baseURL.trim().replace(/\/+$/, "");
18
19
  return normalized === GROK_SUBSCRIPTION_BASE_URL || normalized.startsWith(`${GROK_SUBSCRIPTION_BASE_URL}/`);
19
20
  }
20
- /**
21
- * Infer reasoning levels for a Grok model id the /models endpoint returned
22
- * without metadata. Flagship reasoning models (grok-N.M) expose
23
- * low/medium/high with thinking always on — matching the curated grok-4.5
24
- * entry — while composer/fast/mini/flash variants have no effort control and
25
- * map to the plain "off" ladder.
26
- */
27
- export function inferGrokReasoningLevels(modelId) {
28
- const id = modelId.toLowerCase();
29
- if (/(composer|fast|mini|flash)/.test(id)) {
30
- return { levels: ["off"] };
31
- }
32
- return { levels: ["low", "medium", "high"], defaultLevel: "high" };
33
- }
34
21
  export function buildGrokSubscriptionHeaders() {
35
22
  return {
36
23
  "User-Agent": "grok-cli",
@@ -75,8 +62,10 @@ export function createGrokSubscriptionFetch(auth, baseFetch = getChatGptFetch())
75
62
  * Fetch the model list for a Grok subscription account. The CLI chat proxy
76
63
  * gates on the grok-cli identity headers plus a fresh OAuth bearer, so this
77
64
  * rides the same refreshing fetch used for chat requests instead of a raw
78
- * global fetch. Throws on a non-2xx so callers fall back to the curated
79
- * catalog when the proxy exposes no /models endpoint.
65
+ * global fetch. The /models payload carries richer metadata than id+name —
66
+ * `context_window`, `reasoning_efforts`, and the default `reasoning_effort` —
67
+ * which this surfaces so discovery can stop inferring them. Throws on a
68
+ * non-2xx so callers fall back to the curated catalog.
80
69
  */
81
70
  export async function fetchGrokSubscriptionModels(baseURL, auth, options = {}) {
82
71
  const fetchImpl = createGrokSubscriptionFetch(auth, options.fetch);
@@ -91,8 +80,27 @@ export async function fetchGrokSubscriptionModels(baseURL, auth, options = {}) {
91
80
  const entries = payload.data ?? payload.models ?? [];
92
81
  return entries
93
82
  .filter((entry) => typeof entry?.id === "string" && entry.id.trim().length > 0)
94
- .map((entry) => ({
95
- id: entry.id,
96
- name: typeof entry.name === "string" ? entry.name : undefined,
97
- }));
83
+ .map((entry) => {
84
+ const id = entry.id;
85
+ const rawEfforts = Array.isArray(entry.reasoning_efforts) ? entry.reasoning_efforts : [];
86
+ const reasoningEfforts = rawEfforts
87
+ .map((effort) => (effort && typeof effort === "object" && typeof effort.value === "string"
88
+ ? effort.value
89
+ : undefined))
90
+ .filter((value) => typeof value === "string" && THINKING_LEVELS.includes(value))
91
+ .sort((a, b) => THINKING_LEVELS.indexOf(a) - THINKING_LEVELS.indexOf(b));
92
+ const defaultReasoningEffort = typeof entry.reasoning_effort === "string"
93
+ && THINKING_LEVELS.includes(entry.reasoning_effort)
94
+ ? entry.reasoning_effort
95
+ : undefined;
96
+ return {
97
+ id,
98
+ name: typeof entry.name === "string" ? entry.name : undefined,
99
+ contextWindow: typeof entry.context_window === "number" && entry.context_window > 0
100
+ ? entry.context_window
101
+ : undefined,
102
+ reasoningEfforts: reasoningEfforts.length > 0 ? reasoningEfforts : undefined,
103
+ defaultReasoningEffort,
104
+ };
105
+ });
98
106
  }
@@ -77,6 +77,7 @@ export declare class ProviderRegistry {
77
77
  private modelConfig;
78
78
  private authStorage;
79
79
  private modelDiscoveryCache;
80
+ private readonly discoveryDiskCachePath;
80
81
  private modelDiscoveryInFlight;
81
82
  private modelDiscoveryGeneration;
82
83
  /** Last membership seen per discovery key — survives cache eviction/TTL so
@@ -85,6 +86,14 @@ export declare class ProviderRegistry {
85
86
  private routingRevision;
86
87
  private identityFingerprints;
87
88
  constructor(config: UserConfig);
89
+ /** The on-disk cache is production-only; disable it under the test runner so
90
+ * tests sharing one isolated BUBBLE_HOME never contaminate each other. */
91
+ private get discoveryDiskCacheEnabled();
92
+ /** Restore a prior successful discovery so dynamic models resolve without a
93
+ * network round-trip right after startup (no fetch on every launch). */
94
+ private loadDiscoveryDiskCache;
95
+ /** Persist successful discoveries so the next launch can reuse them. */
96
+ private saveDiscoveryDiskCache;
88
97
  /**
89
98
  * Monotonic counter of semantic routing-world changes: provider set,
90
99
  * credential identity, provider key/config, discovery membership.
@@ -5,12 +5,15 @@
5
5
  * Reads provider configuration from models.json first, then falls back to config.json.
6
6
  */
7
7
  import { createHash } from "node:crypto";
8
- import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, clearDynamicModelMetadata, getBuiltinModel, getBuiltinProvider, listBuiltinModels, replaceDynamicModelMetadata, } from "./model-catalog.js";
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { getBubbleHome } from "./bubble-home.js";
11
+ import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, clearDynamicModelMetadata, getBuiltinModel, getBuiltinProvider, inferGrokModelMetadata, listBuiltinModels, replaceDynamicModelMetadata, } from "./model-catalog.js";
9
12
  import { ModelConfig } from "./model-config.js";
10
13
  import { AuthStorage } from "./oauth/index.js";
11
14
  import { fetchGeminiModels } from "./provider-ai-sdk.js";
12
15
  import { extractChatGptAccountId, fetchOpenAICodexModelCatalog } from "./provider-openai-codex.js";
13
- import { fetchGrokSubscriptionModels, inferGrokReasoningLevels } from "./provider-grok.js";
16
+ import { fetchGrokSubscriptionModels } from "./provider-grok.js";
14
17
  import { refreshOpenAICodex } from "./oauth/openai-codex.js";
15
18
  import { refreshGrok } from "./oauth/grok.js";
16
19
  /** True for providers speaking the OpenAI chat-completions protocol (the default). */
@@ -60,6 +63,8 @@ export function sanitizeProviderHeaders(value) {
60
63
  }
61
64
  const MODEL_DISCOVERY_SUCCESS_TTL_MS = 60_000;
62
65
  const MODEL_DISCOVERY_FAILURE_TTL_MS = 10_000;
66
+ /** How long a successful discovery survives on disk before a fresh fetch. */
67
+ const MODEL_DISCOVERY_DISK_TTL_MS = 24 * 60 * 60 * 1000;
63
68
  /** Discovery must never delay startup or a model picker for long. */
64
69
  const MODEL_DISCOVERY_TIMEOUT_MS = 5_000;
65
70
  export const BUILTIN_PROVIDERS = CATALOG_PROVIDERS;
@@ -74,6 +79,7 @@ export class ProviderRegistry {
74
79
  modelConfig;
75
80
  authStorage;
76
81
  modelDiscoveryCache = new Map();
82
+ discoveryDiskCachePath = join(getBubbleHome(), "model-discovery-cache.json");
77
83
  modelDiscoveryInFlight = new Map();
78
84
  modelDiscoveryGeneration = new Map();
79
85
  /** Last membership seen per discovery key — survives cache eviction/TTL so
@@ -91,6 +97,72 @@ export class ProviderRegistry {
91
97
  this.identityFingerprints.set(authKey, fingerprint);
92
98
  }
93
99
  this.authStorage.onMutation((authKey) => this.handleAuthMutation(authKey));
100
+ this.loadDiscoveryDiskCache();
101
+ }
102
+ /** The on-disk cache is production-only; disable it under the test runner so
103
+ * tests sharing one isolated BUBBLE_HOME never contaminate each other. */
104
+ get discoveryDiskCacheEnabled() {
105
+ const v = process.env.VITEST?.trim().toLowerCase();
106
+ return v !== "true" && v !== "1";
107
+ }
108
+ /** Restore a prior successful discovery so dynamic models resolve without a
109
+ * network round-trip right after startup (no fetch on every launch). */
110
+ loadDiscoveryDiskCache() {
111
+ if (!this.discoveryDiskCacheEnabled)
112
+ return;
113
+ try {
114
+ if (!existsSync(this.discoveryDiskCachePath))
115
+ return;
116
+ const parsed = JSON.parse(readFileSync(this.discoveryDiskCachePath, "utf8"));
117
+ const now = Date.now();
118
+ for (const [key, entry] of Object.entries(parsed)) {
119
+ if (!entry || typeof entry.expiresAt !== "number" || entry.expiresAt <= now)
120
+ continue;
121
+ if (!entry.result || !Array.isArray(entry.result.models))
122
+ continue;
123
+ this.modelDiscoveryCache.set(key, {
124
+ result: entry.result,
125
+ expiresAt: entry.expiresAt,
126
+ identityKey: entry.identityKey ?? "unknown",
127
+ providerId: entry.providerId ?? "",
128
+ authType: entry.authType,
129
+ protocol: entry.protocol,
130
+ });
131
+ // Rebuild the dynamic overlay so context window / reasoning levels
132
+ // resolve from the cached catalog at startup, not only on /model open.
133
+ if (entry.providerId) {
134
+ this.applyDynamicDiscoveryMetadata({ id: entry.providerId, authType: entry.authType, protocol: entry.protocol }, entry.result);
135
+ }
136
+ }
137
+ }
138
+ catch {
139
+ // Missing/corrupt cache is fine; the next discovery re-fetches.
140
+ }
141
+ }
142
+ /** Persist successful discoveries so the next launch can reuse them. */
143
+ saveDiscoveryDiskCache() {
144
+ if (!this.discoveryDiskCacheEnabled)
145
+ return;
146
+ try {
147
+ const data = {};
148
+ for (const [key, entry] of this.modelDiscoveryCache) {
149
+ if (entry.result.error)
150
+ continue;
151
+ data[key] = {
152
+ result: entry.result,
153
+ expiresAt: Date.now() + MODEL_DISCOVERY_DISK_TTL_MS,
154
+ identityKey: entry.identityKey,
155
+ providerId: entry.providerId,
156
+ authType: entry.authType,
157
+ protocol: entry.protocol,
158
+ };
159
+ }
160
+ mkdirSync(dirname(this.discoveryDiskCachePath), { recursive: true });
161
+ writeFileSync(this.discoveryDiskCachePath, JSON.stringify(data, null, 2), { mode: 0o600 });
162
+ }
163
+ catch {
164
+ // Persistence is best-effort.
165
+ }
94
166
  }
95
167
  /**
96
168
  * Monotonic counter of semantic routing-world changes: provider set,
@@ -386,7 +458,19 @@ export class ProviderRegistry {
386
458
  if (!options.forceRefresh) {
387
459
  const cached = this.modelDiscoveryCache.get(key);
388
460
  if (cached && cached.expiresAt > now) {
389
- const result = { ...cached.result, source: "cache" };
461
+ // A disk cache can outlive the application version that wrote it. Keep
462
+ // the current curated catalog authoritative over stale cached metadata,
463
+ // then retain cached remote-only ids for non-authoritative union results.
464
+ // This lets newly-added builtins surface immediately after an upgrade
465
+ // without waiting up to 24 hours or requiring a manual Ctrl+R refresh.
466
+ const local = this.localModelsForProvider(provider);
467
+ const localIds = new Set(local.map((model) => model.id));
468
+ const models = cached.result.source === "static"
469
+ ? local
470
+ : cached.result.authoritative
471
+ ? cached.result.models
472
+ : [...local, ...cached.result.models.filter((model) => !localIds.has(model.id))];
473
+ const result = { ...cached.result, models, source: "cache" };
390
474
  const current = this.getConfigured().find((item) => item.id === provider.id);
391
475
  if (!current || this.modelDiscoveryKey(current) === key) {
392
476
  this.applyDynamicDiscoveryMetadata(provider, result);
@@ -420,7 +504,12 @@ export class ProviderRegistry {
420
504
  result: { ...result, source: result.source === "cache" ? "remote" : result.source },
421
505
  expiresAt: Date.now() + (result.error ? MODEL_DISCOVERY_FAILURE_TTL_MS : MODEL_DISCOVERY_SUCCESS_TTL_MS),
422
506
  identityKey: this.discoveryIdentity(provider),
507
+ providerId: provider.id,
508
+ authType: provider.authType,
509
+ protocol: provider.protocol,
423
510
  });
511
+ if (!result.error)
512
+ this.saveDiscoveryDiskCache();
424
513
  this.lastDiscoveryMembership.set(key, {
425
514
  ids: result.models.map((model) => model.id),
426
515
  authoritative: result.authoritative,
@@ -541,13 +630,27 @@ export class ProviderRegistry {
541
630
  const extras = remote
542
631
  .filter((entry) => !known.has(entry.id) && isLikelyChatModelId(entry.id))
543
632
  .map((entry) => {
544
- const inferred = inferGrokReasoningLevels(entry.id);
633
+ const inferred = inferGrokModelMetadata(entry.id);
634
+ // Prefer the server-declared effort ladder and context window over
635
+ // id-based inference; fall back to inference only when the endpoint
636
+ // omitted them (or for ids that don't match a known family).
637
+ const levels = entry.reasoningEfforts && entry.reasoningEfforts.length > 0
638
+ ? entry.reasoningEfforts
639
+ : inferred.levels;
640
+ const defaultLevel = entry.defaultReasoningEffort && levels.includes(entry.defaultReasoningEffort)
641
+ ? entry.defaultReasoningEffort
642
+ : inferred.defaultLevel;
545
643
  return {
546
644
  id: entry.id,
547
645
  name: entry.name || entry.id,
548
646
  providerId: currentProvider.id,
549
- reasoningLevels: inferred.levels,
550
- ...(inferred.defaultLevel ? { defaultReasoningLevel: inferred.defaultLevel } : {}),
647
+ reasoningLevels: levels,
648
+ ...(defaultLevel ? { defaultReasoningLevel: defaultLevel } : {}),
649
+ ...(entry.contextWindow !== undefined
650
+ ? { contextWindow: entry.contextWindow }
651
+ : inferred.contextWindow !== undefined
652
+ ? { contextWindow: inferred.contextWindow }
653
+ : {}),
551
654
  };
552
655
  })
553
656
  .sort((a, b) => a.id.localeCompare(b.id));
@@ -114,16 +114,15 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
114
114
  // Zhipu/Z.AI OpenAI-compatible endpoints expose reasoning via a provider-specific
115
115
  // `thinking` block rather than OpenAI's `reasoning_effort` shape.
116
116
  if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)) {
117
- // GLM-5.2 is the only GLM that also accepts `reasoning_effort` (we expose
118
- // high/max, which map 1:1 onto the API enum). "off" disables thinking via
119
- // `thinking: {type: "disabled"}` — otherwise the server default (thinking
120
- // on, effort max) would make "off" a no-op. The effort field rides inside
121
- // the body alongside `thinking`, so it goes in extraBody, not the
122
- // OpenRouter-style `reasoningEffort` config field.
123
- if (modelId === "glm-5.2") {
117
+ // GLM-5.2+ also accept `reasoning_effort`. GLM-5.2 can disable thinking;
118
+ // GLM-5.3 requires thinking and Coding Plan normalizes its effort into
119
+ // low/high/max. The catalog clamps 5.3 choices before this serialization
120
+ // boundary. The effort field rides inside the body alongside `thinking`,
121
+ // so it goes in extraBody, not the OpenRouter-style config field.
122
+ if (modelId === "glm-5.2" || modelId === "glm-5.3") {
124
123
  return {
125
124
  effectiveThinkingLevel,
126
- extraBody: effectiveThinkingLevel === "off"
125
+ extraBody: modelId === "glm-5.2" && effectiveThinkingLevel === "off"
127
126
  ? { thinking: { type: "disabled" } }
128
127
  : {
129
128
  thinking: { type: "enabled", clear_thinking: false },
package/dist/provider.js CHANGED
@@ -397,6 +397,9 @@ function extractBalancedJson(s, start) {
397
397
  * execute safely.
398
398
  */
399
399
  export async function* translateOpenAIFullResponse(response) {
400
+ if (typeof response?.system_fingerprint === "string" && response.system_fingerprint) {
401
+ yield { type: "response_metadata", systemFingerprint: response.system_fingerprint };
402
+ }
400
403
  const usageChunk = usageToStreamChunk(response?.usage);
401
404
  if (usageChunk)
402
405
  yield usageChunk;
@@ -502,6 +505,7 @@ export async function* translateOpenAIStream(stream, options = {}) {
502
505
  let reasoningBuffer = "";
503
506
  let textBuffer = "";
504
507
  let rawChunkSeq = 0;
508
+ let systemFingerprint;
505
509
  // DeepSeek (and some inference re-hosts) sometimes deliver reasoning twice:
506
510
  // once via a dedicated `reasoning_content` / `thinking` field, and again
507
511
  // embedded as `<think>...</think>` inside `delta.content`. Track whether we
@@ -553,6 +557,13 @@ export async function* translateOpenAIStream(stream, options = {}) {
553
557
  const delta = choice?.delta;
554
558
  const usage = chunk.usage ?? choice?.usage;
555
559
  const finishReason = choice?.finish_reason;
560
+ if (typeof chunk?.system_fingerprint === "string"
561
+ && chunk.system_fingerprint
562
+ && chunk.system_fingerprint !== systemFingerprint) {
563
+ const nextSystemFingerprint = chunk.system_fingerprint;
564
+ systemFingerprint = nextSystemFingerprint;
565
+ yield { type: "response_metadata", systemFingerprint: nextSystemFingerprint };
566
+ }
556
567
  debugReasoningStream({
557
568
  stage: "provider_raw",
558
569
  providerId: options.debugProviderId,
@@ -203,6 +203,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
203
203
  providerId: message.providerId,
204
204
  modelId: message.modelId,
205
205
  usage: message.usage,
206
+ systemFingerprint: message.systemFingerprint,
206
207
  error: message.error,
207
208
  providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(message.providerMetadata)),
208
209
  },
@@ -16,7 +16,7 @@ import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
16
16
  import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
17
17
  import { feishuCommand } from "./feishu.js";
18
18
  import { GROK_LOCAL_COMMAND_HELP } from "../external-runtime/grok-input-policy.js";
19
- import { isGrokSubscriptionProviderId, } from "../external-runtime/grok-provider.js";
19
+ import { isGrokSubscriptionProviderId } from "../external-runtime/grok-provider.js";
20
20
  import { classifyExternalRuntimeBinding } from "../external-runtime/session-policy.js";
21
21
  const VALID_SCOPES = ["user", "project", "local"];
22
22
  const VALID_LISTS = ["allow", "deny"];
@@ -107,7 +107,7 @@ function persistSelectedModel(model, ctx) {
107
107
  }
108
108
  }
109
109
  function syncSystemPrompt(ctx, model) {
110
- const { providerId, modelId } = decodeModel(model);
110
+ const { providerId } = decodeModel(model);
111
111
  const toolPromptOptions = typeof ctx.agent.getSystemPromptToolOptions === "function"
112
112
  ? ctx.agent.getSystemPromptToolOptions()
113
113
  : {};
@@ -412,14 +412,14 @@ const builtinSlashCommandEntries = [
412
412
  {
413
413
  name: "skills",
414
414
  description: "Open the searchable skills picker",
415
- async handler(args, ctx) {
415
+ async handler(_args, ctx) {
416
416
  ctx.openPicker("skill");
417
417
  },
418
418
  },
419
419
  {
420
420
  name: "help",
421
421
  description: "Show available slash commands",
422
- async handler(args, ctx) {
422
+ async handler(_args, ctx) {
423
423
  if (hasExternalRuntimeSession(ctx)) {
424
424
  const grok = isGrokSessionActive(ctx);
425
425
  return [
@@ -448,14 +448,14 @@ const builtinSlashCommandEntries = [
448
448
  {
449
449
  name: "context",
450
450
  description: "Show current context window usage and breakdown",
451
- async handler(args, ctx) {
451
+ async handler(_args, ctx) {
452
452
  return `${formatContextUsage(ctx.agent.getContextUsageSnapshot())}\n\n${formatMcpContextStatus(ctx)}`;
453
453
  },
454
454
  },
455
455
  {
456
456
  name: "quit",
457
457
  description: "Exit the application",
458
- async handler(args, ctx) {
458
+ async handler(_args, ctx) {
459
459
  ctx.exit();
460
460
  },
461
461
  },
@@ -482,8 +482,12 @@ const builtinSlashCommandEntries = [
482
482
  {
483
483
  name: "clear",
484
484
  description: "Clear the current conversation history",
485
- async handler(args, ctx) {
485
+ async handler(_args, ctx) {
486
486
  ctx.agent.messages = ctx.agent.messages.filter((m) => m.role === "system" || m.role === "meta");
487
+ // The resident history just shrank: drop the incremental usage anchor
488
+ // too (same as /compact and /rewind), or budget accounting keeps
489
+ // measuring against the pre-clear message count.
490
+ ctx.agent.resetContextUsageAnchor();
487
491
  ctx.sessionManager?.appendMarker("conversation_clear", "");
488
492
  ctx.sessionManager?.clearTitleMetadata?.();
489
493
  if (ctx.agent.getTodos().length > 0) {
@@ -845,7 +849,7 @@ const builtinSlashCommandEntries = [
845
849
  {
846
850
  name: "plan",
847
851
  description: "Toggle plan mode on/off (Tab switches Build/Plan)",
848
- async handler(args, ctx) {
852
+ async handler(_args, ctx) {
849
853
  const next = ctx.agent.mode === "plan" ? "default" : "plan";
850
854
  ctx.agent.setMode(next);
851
855
  return next === "plan"
@@ -1069,7 +1073,7 @@ const builtinSlashCommandEntries = [
1069
1073
  {
1070
1074
  name: "compact",
1071
1075
  description: "Manually compact the current session context",
1072
- async handler(args, ctx) {
1076
+ async handler(_args, ctx) {
1073
1077
  if (!ctx.sessionManager) {
1074
1078
  return "Compaction requires session persistence. Start an interactive session first.";
1075
1079
  }
@@ -54,7 +54,7 @@ export function createGrepTool(cwd) {
54
54
  }
55
55
  rgArgs.push(searchPath);
56
56
  return new Promise((resolve) => {
57
- execFile("rg", rgArgs, { cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
57
+ execFile("rg", rgArgs, { cwd, maxBuffer: 10 * 1024 * 1024 }, (_error, stdout, _stderr) => {
58
58
  // rg returns exit code 1 when no matches found, which is not an error for us
59
59
  const lines = stdout.split("\n").filter((l) => l.trim() !== "");
60
60
  const matches = [];
@@ -0,0 +1,43 @@
1
+ import type { Agent } from "../agent.js";
2
+ import { type ProviderRegistry } from "../provider-registry.js";
3
+ import type { BackgroundTaskInfo } from "../tasks/manager.js";
4
+ import { type DisplayMessage } from "./display-history.js";
5
+ export declare const TASK_WAKE_DEBOUNCE_MS = 2000;
6
+ export declare function buildTips(agent: Agent, registry: ProviderRegistry, grokActive?: boolean): string[];
7
+ /**
8
+ * Slash-command results arrive as plain strings; recognize the /model
9
+ * confirmations by shape so they render as accent-colored UI notices — the
10
+ * same treatment the picker path applies — instead of assistant prose.
11
+ */
12
+ export declare function slashResultNoticeKind(result: string): DisplayMessage["syntheticKind"] | undefined;
13
+ /** Status-row summary for running background tasks (design §2.5). */
14
+ export declare function taskRowSummary(tasks: BackgroundTaskInfo[], nowTick: number): string;
15
+ export declare function friendlyCwd(cwd: string): string;
16
+ export declare function sessionBasename(sessionFile: string | undefined): string | undefined;
17
+ export declare function truncate(value: string, max: number): string;
18
+ export declare function formatCompactTokens(n: number): string;
19
+ export declare function formatContextUsageLabel(snapshot: {
20
+ usedTokens: number;
21
+ contextWindow?: number;
22
+ }): string;
23
+ /**
24
+ * Streaming tool arguments arrive as an incomplete JSON buffer. We can't
25
+ * JSON.parse() until the closing brace lands, but the user wants to see the
26
+ * short identifying fields (path, command, …) as soon as the model emits
27
+ * them so the tool row header reflects what's happening.
28
+ *
29
+ * Intentionally limited to short, single-line fields. Long fields like
30
+ * `content` are *not* surfaced live: rendering thousands of partial lines
31
+ * per delta floods the terminal and the partial value can break around
32
+ * unescaped sequences. The final value lands when the tool actually
33
+ * executes and tool_start delivers canonical args.
34
+ */
35
+ export declare function parsePartialArgs(buffer: string, previous: Record<string, any>): Record<string, any>;
36
+ /**
37
+ * Coerce a freshly-constructed DisplayMessage into one that carries a stable
38
+ * `key`. Centralizes the safety net so callers don't have to remember to call
39
+ * nextDisplayMessageKey on every push.
40
+ */
41
+ export declare function withMessageKey(message: DisplayMessage): DisplayMessage;
42
+ export declare const STREAMING_FLUSH_INTERVAL_MS = 40;
43
+ export declare function clampWindowStartForIndex(total: number, selectedIndex: number, maxVisible: number): number;