@bubblebrain-ai/bubble 0.0.47 → 0.0.48

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.
@@ -9,9 +9,21 @@
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
13
  export declare const GROK_SUBSCRIPTION_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
13
14
  export declare const GROK_CLIENT_VERSION = "0.2.93";
14
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
+ };
15
27
  export declare function buildGrokSubscriptionHeaders(): Record<string, string>;
16
28
  export interface GrokAuthAdapter {
17
29
  getCredentials: () => OAuthCredentials | undefined | Promise<OAuthCredentials | undefined>;
@@ -24,3 +36,17 @@ export interface GrokAuthAdapter {
24
36
  * proactively refreshes) the stored OAuth credentials per request instead.
25
37
  */
26
38
  export declare function createGrokSubscriptionFetch(auth: GrokAuthAdapter, baseFetch?: ChatGptFetch): ChatGptFetch;
39
+ /**
40
+ * Fetch the model list for a Grok subscription account. The CLI chat proxy
41
+ * gates on the grok-cli identity headers plus a fresh OAuth bearer, so this
42
+ * 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.
45
+ */
46
+ export declare function fetchGrokSubscriptionModels(baseURL: string, auth: GrokAuthAdapter, options?: {
47
+ timeoutMs?: number;
48
+ fetch?: ChatGptFetch;
49
+ }): Promise<Array<{
50
+ id: string;
51
+ name?: string;
52
+ }>>;
@@ -17,6 +17,20 @@ export function isGrokSubscriptionBaseUrl(baseURL) {
17
17
  const normalized = baseURL.trim().replace(/\/+$/, "");
18
18
  return normalized === GROK_SUBSCRIPTION_BASE_URL || normalized.startsWith(`${GROK_SUBSCRIPTION_BASE_URL}/`);
19
19
  }
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
+ }
20
34
  export function buildGrokSubscriptionHeaders() {
21
35
  return {
22
36
  "User-Agent": "grok-cli",
@@ -57,3 +71,28 @@ export function createGrokSubscriptionFetch(auth, baseFetch = getChatGptFetch())
57
71
  return baseFetch(input, { ...(init ?? {}), headers });
58
72
  };
59
73
  }
74
+ /**
75
+ * Fetch the model list for a Grok subscription account. The CLI chat proxy
76
+ * gates on the grok-cli identity headers plus a fresh OAuth bearer, so this
77
+ * 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.
80
+ */
81
+ export async function fetchGrokSubscriptionModels(baseURL, auth, options = {}) {
82
+ const fetchImpl = createGrokSubscriptionFetch(auth, options.fetch);
83
+ const base = baseURL.trim().replace(/\/+$/, "");
84
+ const response = await fetchImpl(`${base}/models`, {
85
+ headers: buildGrokSubscriptionHeaders(),
86
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5_000),
87
+ });
88
+ if (!response.ok)
89
+ throw new Error(`HTTP ${response.status}`);
90
+ const payload = (await response.json());
91
+ const entries = payload.data ?? payload.models ?? [];
92
+ return entries
93
+ .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
+ }));
98
+ }
@@ -9,7 +9,7 @@ import { type ProviderProtocol } from "./model-catalog.js";
9
9
  import { ModelConfig } from "./model-config.js";
10
10
  import { AuthStorage } from "./oauth/index.js";
11
11
  import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
12
- import type { GrokAuthAdapter } from "./provider-grok.js";
12
+ import { type GrokAuthAdapter } from "./provider-grok.js";
13
13
  import type { ThinkingLevel } from "./types.js";
14
14
  export interface ProviderProfile {
15
15
  id: string;
@@ -10,6 +10,7 @@ import { ModelConfig } from "./model-config.js";
10
10
  import { AuthStorage } from "./oauth/index.js";
11
11
  import { fetchGeminiModels } from "./provider-ai-sdk.js";
12
12
  import { extractChatGptAccountId, fetchOpenAICodexModelCatalog } from "./provider-openai-codex.js";
13
+ import { fetchGrokSubscriptionModels, inferGrokReasoningLevels } from "./provider-grok.js";
13
14
  import { refreshOpenAICodex } from "./oauth/openai-codex.js";
14
15
  import { refreshGrok } from "./oauth/grok.js";
15
16
  /** True for providers speaking the OpenAI chat-completions protocol (the default). */
@@ -521,6 +522,45 @@ export class ProviderRegistry {
521
522
  return this.fallbackDiscovery(provider, error);
522
523
  }
523
524
  }
525
+ // Grok subscription: the CLI chat proxy gates on the grok-cli identity
526
+ // headers and a fresh OAuth bearer, neither of which the generic path below
527
+ // sends. Discover through the refreshing subscription fetch, then merge the
528
+ // curated catalog with any remote-only ids so newly-released models surface
529
+ // without a code change. Authoritative because the merged list is what the
530
+ // routing catalog and model picker should treat as the account's catalog.
531
+ if (provider.id === "grok" && provider.authType === "oauth" && provider.apiKey) {
532
+ try {
533
+ await this.prepareProvider("grok");
534
+ const currentProvider = this.getConfigured().find((item) => item.id === "grok") ?? provider;
535
+ const grokAuth = this.createGrokAuthAdapter("grok");
536
+ if (!grokAuth)
537
+ throw new Error("Grok OAuth credentials are unavailable.");
538
+ const remote = await fetchGrokSubscriptionModels(currentProvider.baseURL, grokAuth);
539
+ const local = this.localModelsForProvider(currentProvider);
540
+ const known = new Set(local.map((model) => model.id));
541
+ const extras = remote
542
+ .filter((entry) => !known.has(entry.id) && isLikelyChatModelId(entry.id))
543
+ .map((entry) => {
544
+ const inferred = inferGrokReasoningLevels(entry.id);
545
+ return {
546
+ id: entry.id,
547
+ name: entry.name || entry.id,
548
+ providerId: currentProvider.id,
549
+ reasoningLevels: inferred.levels,
550
+ ...(inferred.defaultLevel ? { defaultReasoningLevel: inferred.defaultLevel } : {}),
551
+ };
552
+ })
553
+ .sort((a, b) => a.id.localeCompare(b.id));
554
+ return {
555
+ models: [...local, ...extras],
556
+ source: "remote",
557
+ authoritative: true,
558
+ };
559
+ }
560
+ catch (error) {
561
+ return this.fallbackDiscovery(provider, error);
562
+ }
563
+ }
524
564
  // Generic OpenAI-compatible discovery: most vendors expose GET /models on
525
565
  // the same base URL. Probed against every configured provider, their lists
526
566
  // turned out to be neither complete nor clean — zhipuai omits glm-5.2,
@@ -642,7 +682,9 @@ export class ProviderRegistry {
642
682
  ? "openai-codex"
643
683
  : provider.id === "google" && provider.protocol === "ai-sdk"
644
684
  ? "google"
645
- : undefined;
685
+ : provider.id === "grok"
686
+ ? "grok"
687
+ : undefined;
646
688
  if (!dynamicProviderId)
647
689
  return;
648
690
  if (!result.authoritative) {
@@ -653,8 +695,11 @@ export class ProviderRegistry {
653
695
  id: model.id,
654
696
  name: model.name,
655
697
  providerId: dynamicProviderId,
656
- // Empty means the remote model exists but declared no trusted effort metadata.
657
- reasoningLevels: model.reasoningLevels ?? [],
698
+ // A remote model with no trusted effort metadata still has to be
699
+ // selectable: default it to the plain "off" level (no reasoning control)
700
+ // rather than an empty ladder, which the model picker would render
701
+ // "unavailable" and skip during arrow-key navigation.
702
+ reasoningLevels: model.reasoningLevels ?? ["off"],
658
703
  defaultReasoningLevel: model.defaultReasoningLevel,
659
704
  contextWindow: model.contextWindow,
660
705
  useResponsesLite: model.useResponsesLite,
@@ -8,7 +8,7 @@ import { useTheme } from "./theme.js";
8
8
  import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
9
9
  import { bareImageFilenameFromPaste, extractImagePathTokens, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
10
10
  import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
11
- import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer, hasActiveComposerPastes, insertComposerPaste, insertComposerText, moveComposerCursor, replaceComposerRange, } from "./composer-buffer.js";
11
+ import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer, insertComposerPaste, insertComposerText, moveComposerCursor, replaceComposerRange, } from "./composer-buffer.js";
12
12
  import { isKeyReleaseEvent } from "./key-events.js";
13
13
  import { PasteOperationTracker } from "./paste-operation-tracker.js";
14
14
  import { stripTerminalMouseSequences } from "./terminal-mouse.js";
@@ -26,6 +26,18 @@ export function createComposerDraftSnapshot(input) {
26
26
  imageLabelStartOverride: input.imageLabelStartOverride,
27
27
  };
28
28
  }
29
+ /**
30
+ * Rebuild a composer buffer from a persisted history entry. Text long enough to
31
+ * have been collapsed at paste time is collapsed again into a `[Pasted text #N …]`
32
+ * marker, but its full content is preserved in the buffer's paste spans so the
33
+ * composer stays compact, no content is lost, and the next submit re-expands it.
34
+ */
35
+ function bufferFromHistoryText(text) {
36
+ if (shouldCollapsePastedContent(text)) {
37
+ return insertComposerPaste(createComposerBuffer(), text);
38
+ }
39
+ return createComposerBuffer(text);
40
+ }
29
41
  /**
30
42
  * Browse persisted history without turning marker-shaped text into live paste
31
43
  * tokens, while preserving the exact unsent draft in memory for the final
@@ -68,7 +80,7 @@ export function stepComposerHistory(state, history, direction) {
68
80
  };
69
81
  }
70
82
  return {
71
- buffer: createComposerBuffer(result.text),
83
+ buffer: bufferFromHistoryText(result.text),
72
84
  attachments: (result.images ?? []).map((attachment) => ({ ...attachment })),
73
85
  imageLabelStartOverride: result.imageDisplayStart ?? null,
74
86
  historyIndex: result.index,
@@ -716,10 +728,10 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
716
728
  return;
717
729
  submittedPayloadFingerprintRef.current = fingerprint;
718
730
  deliver(payload);
719
- // A collapsed text-paste marker cannot be safely replayed once its
720
- // in-memory reference is gone; skip those. Image-label stripping is fine to
721
- // replay (the attachments are stored on the history entry).
722
- if (!hasActiveComposerPastes(submittedBuffer) && (expandedText.trim().length > 0 || attachments.length > 0)) {
731
+ // Always record the submitted text so ↑/↓ can recall it. The full expanded
732
+ // text is persisted (content is never lost); on recall, long text is
733
+ // re-collapsed into a paste marker that still carries its full content.
734
+ if (expandedText.trim().length > 0 || attachments.length > 0) {
723
735
  const historyEntry = {
724
736
  text: expandedText,
725
737
  images: attachments,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {