@bubblebrain-ai/bubble 0.0.34 → 0.0.36

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 (168) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/abort-errors.d.ts +2 -2
  3. package/dist/agent/budget-ledger.d.ts +4 -45
  4. package/dist/agent/budget-ledger.js +5 -75
  5. package/dist/agent/categories.d.ts +66 -2
  6. package/dist/agent/categories.js +120 -19
  7. package/dist/agent/child-runner.d.ts +1 -3
  8. package/dist/agent/child-runner.js +6 -37
  9. package/dist/agent/discovery-barrier.js +1 -1
  10. package/dist/agent/profiles.d.ts +1 -2
  11. package/dist/agent/profiles.js +25 -14
  12. package/dist/agent/routing-catalog.d.ts +113 -0
  13. package/dist/agent/routing-catalog.js +240 -0
  14. package/dist/agent/subagent-control.d.ts +5 -12
  15. package/dist/agent/subagent-control.js +0 -2
  16. package/dist/agent/subagent-store.d.ts +1 -1
  17. package/dist/agent/subagent-store.js +1 -3
  18. package/dist/agent/workflow/control.d.ts +21 -0
  19. package/dist/agent/workflow/control.js +50 -8
  20. package/dist/agent/workflow/runtime.d.ts +15 -2
  21. package/dist/agent/workflow/runtime.js +112 -30
  22. package/dist/agent.d.ts +82 -48
  23. package/dist/agent.js +360 -232
  24. package/dist/approval/controller.js +28 -0
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +21 -1
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +2 -1
  29. package/dist/config.d.ts +5 -3
  30. package/dist/config.js +10 -5
  31. package/dist/context/compact-llm.d.ts +2 -2
  32. package/dist/external-runtime/grok-acp.d.ts +56 -0
  33. package/dist/external-runtime/grok-acp.js +386 -0
  34. package/dist/external-runtime/grok-binary.d.ts +25 -0
  35. package/dist/external-runtime/grok-binary.js +108 -0
  36. package/dist/external-runtime/grok-errors.d.ts +7 -0
  37. package/dist/external-runtime/grok-errors.js +15 -0
  38. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  39. package/dist/external-runtime/grok-input-policy.js +52 -0
  40. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  41. package/dist/external-runtime/grok-ndjson.js +193 -0
  42. package/dist/external-runtime/grok-network.d.ts +15 -0
  43. package/dist/external-runtime/grok-network.js +81 -0
  44. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  45. package/dist/external-runtime/grok-preflight.js +194 -0
  46. package/dist/external-runtime/grok-process.d.ts +26 -0
  47. package/dist/external-runtime/grok-process.js +184 -0
  48. package/dist/external-runtime/grok-profile.d.ts +32 -0
  49. package/dist/external-runtime/grok-profile.js +419 -0
  50. package/dist/external-runtime/grok-provider.d.ts +31 -0
  51. package/dist/external-runtime/grok-provider.js +41 -0
  52. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  53. package/dist/external-runtime/grok-runtime.js +882 -0
  54. package/dist/external-runtime/index.d.ts +13 -0
  55. package/dist/external-runtime/index.js +13 -0
  56. package/dist/external-runtime/session-policy.d.ts +11 -0
  57. package/dist/external-runtime/session-policy.js +30 -0
  58. package/dist/external-runtime/types.d.ts +65 -0
  59. package/dist/external-runtime/types.js +1 -0
  60. package/dist/feishu/agent-host/approval-card.js +21 -0
  61. package/dist/feishu/agent-host/run-driver.js +19 -3
  62. package/dist/feishu/serve.js +1 -0
  63. package/dist/goal/tools.d.ts +6 -3
  64. package/dist/goal/tools.js +7 -19
  65. package/dist/main.js +114 -28
  66. package/dist/memory/db.d.ts +2 -0
  67. package/dist/memory/db.js +10 -0
  68. package/dist/memory/index.d.ts +1 -0
  69. package/dist/memory/index.js +1 -0
  70. package/dist/memory/phase1.js +14 -2
  71. package/dist/memory/phase2.js +21 -2
  72. package/dist/memory/prompts.js +1 -1
  73. package/dist/memory/session-policy.d.ts +25 -0
  74. package/dist/memory/session-policy.js +55 -0
  75. package/dist/memory/storage.d.ts +1 -0
  76. package/dist/memory/storage.js +13 -0
  77. package/dist/model-catalog.d.ts +18 -0
  78. package/dist/model-catalog.js +92 -52
  79. package/dist/model-config.d.ts +3 -1
  80. package/dist/model-config.js +11 -0
  81. package/dist/oauth/grok.d.ts +25 -0
  82. package/dist/oauth/grok.js +218 -0
  83. package/dist/oauth/index.d.ts +1 -0
  84. package/dist/oauth/index.js +1 -0
  85. package/dist/oauth/storage.d.ts +9 -0
  86. package/dist/oauth/storage.js +25 -0
  87. package/dist/orchestrator/default-hooks.js +23 -7
  88. package/dist/prompt/compose.d.ts +3 -0
  89. package/dist/prompt/compose.js +3 -0
  90. package/dist/prompt/delegation.d.ts +1 -1
  91. package/dist/prompt/delegation.js +41 -8
  92. package/dist/prompt/environment.js +0 -4
  93. package/dist/prompt/reminders.d.ts +0 -1
  94. package/dist/prompt/reminders.js +1 -11
  95. package/dist/prompt/routing.d.ts +14 -0
  96. package/dist/prompt/routing.js +71 -0
  97. package/dist/prompt/runtime.js +1 -1
  98. package/dist/prompt/task-reminders.d.ts +20 -0
  99. package/dist/prompt/task-reminders.js +73 -1
  100. package/dist/provider-ai-sdk.js +2 -2
  101. package/dist/provider-grok.d.ts +26 -0
  102. package/dist/provider-grok.js +59 -0
  103. package/dist/provider-openai-codex.d.ts +17 -1
  104. package/dist/provider-openai-codex.js +101 -29
  105. package/dist/provider-registry.d.ts +72 -0
  106. package/dist/provider-registry.js +336 -59
  107. package/dist/provider-transform.d.ts +1 -1
  108. package/dist/provider-transform.js +35 -17
  109. package/dist/provider.d.ts +3 -0
  110. package/dist/provider.js +14 -0
  111. package/dist/session-types.d.ts +19 -1
  112. package/dist/session.d.ts +1 -0
  113. package/dist/session.js +4 -0
  114. package/dist/slash-commands/commands.js +224 -14
  115. package/dist/slash-commands/types.d.ts +14 -1
  116. package/dist/system-prompt.d.ts +2 -0
  117. package/dist/tools/agent-lifecycle.d.ts +1 -9
  118. package/dist/tools/agent-lifecycle.js +42 -272
  119. package/dist/tools/bash.js +1 -1
  120. package/dist/tools/child-tools.js +2 -0
  121. package/dist/tools/index.d.ts +2 -2
  122. package/dist/tools/index.js +7 -5
  123. package/dist/tools/lsp.js +1 -0
  124. package/dist/tools/memory.d.ts +7 -2
  125. package/dist/tools/memory.js +28 -43
  126. package/dist/tools/server.js +4 -0
  127. package/dist/tools/tool-search.d.ts +1 -1
  128. package/dist/tools/tool-search.js +23 -3
  129. package/dist/tui/model-switch.d.ts +7 -0
  130. package/dist/tui/model-switch.js +15 -5
  131. package/dist/tui-ink/app.d.ts +3 -1
  132. package/dist/tui-ink/app.js +672 -243
  133. package/dist/tui-ink/approval/approval-dialog.js +37 -1
  134. package/dist/tui-ink/approval/diff-view.js +4 -4
  135. package/dist/tui-ink/approval/select.js +2 -2
  136. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  137. package/dist/tui-ink/composer-buffer.js +317 -0
  138. package/dist/tui-ink/feedback-dialog.js +2 -2
  139. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  140. package/dist/tui-ink/footer.d.ts +1 -0
  141. package/dist/tui-ink/footer.js +7 -2
  142. package/dist/tui-ink/input-box.d.ts +36 -7
  143. package/dist/tui-ink/input-box.js +229 -157
  144. package/dist/tui-ink/markdown.js +11 -9
  145. package/dist/tui-ink/message-list.d.ts +5 -1
  146. package/dist/tui-ink/message-list.js +15 -2
  147. package/dist/tui-ink/model-picker.d.ts +61 -4
  148. package/dist/tui-ink/model-picker.js +464 -128
  149. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  150. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  151. package/dist/tui-ink/plan-confirm.js +1 -1
  152. package/dist/tui-ink/run.d.ts +3 -0
  153. package/dist/tui-ink/run.js +1 -1
  154. package/dist/tui-ink/subagent-view.d.ts +34 -1
  155. package/dist/tui-ink/subagent-view.js +82 -1
  156. package/dist/tui-ink/theme.d.ts +19 -2
  157. package/dist/tui-ink/theme.js +25 -8
  158. package/dist/tui-ink/welcome.d.ts +3 -2
  159. package/dist/tui-ink/welcome.js +10 -6
  160. package/dist/types.d.ts +8 -31
  161. package/dist/types.js +1 -1
  162. package/dist/variant/thinking-level.d.ts +2 -2
  163. package/dist/variant/thinking-level.js +2 -1
  164. package/dist/variant/variant-resolver.d.ts +22 -0
  165. package/dist/variant/variant-resolver.js +43 -0
  166. package/package.json +6 -2
  167. package/dist/tools/task.d.ts +0 -2
  168. package/dist/tools/task.js +0 -58
package/dist/session.d.ts CHANGED
@@ -51,6 +51,7 @@ export declare class SessionManager {
51
51
  setMetadata(metadata: SessionMetadata): void;
52
52
  updateMetadata(patch: Partial<SessionMetadata>): void;
53
53
  clearTitleMetadata(): void;
54
+ clearExternalRuntimeMetadata(): void;
54
55
  appendMessage(message: Message): void;
55
56
  appendCompaction(summary: string): void;
56
57
  appendMarker(kind: SessionMarkerKind, value: string): void;
package/dist/session.js CHANGED
@@ -134,6 +134,10 @@ export class SessionManager {
134
134
  const { title: _title, titleSource: _titleSource, titleUpdatedAt: _titleUpdatedAt, titleUserMessageId: _titleUserMessageId, ...metadata } = this.log.getMetadata();
135
135
  this.setMetadata(metadata);
136
136
  }
137
+ clearExternalRuntimeMetadata() {
138
+ const { externalRuntime: _externalRuntime, ...metadata } = this.log.getMetadata();
139
+ this.setMetadata(metadata);
140
+ }
137
141
  appendMessage(message) {
138
142
  const entries = this.log.appendMessage(message);
139
143
  this.persist(entries);
@@ -11,9 +11,13 @@ import { normalizeSingleLine } from "../text-display.js";
11
11
  import { formatRelativeTime } from "../tui/recent-activity.js";
12
12
  import { HOOK_EVENT_NAMES, isHookEventName } from "../hooks/index.js";
13
13
  import { isThinkingLevel } from "../variant/thinking-level.js";
14
+ import { normalizeInheritedThinkingLevel } from "../variant/variant-resolver.js";
14
15
  import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
15
16
  import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
16
17
  import { feishuCommand } from "./feishu.js";
18
+ import { GROK_LOCAL_COMMAND_HELP } from "../external-runtime/grok-input-policy.js";
19
+ import { isGrokSubscriptionProviderId, } from "../external-runtime/grok-provider.js";
20
+ import { classifyExternalRuntimeBinding } from "../external-runtime/session-policy.js";
17
21
  const VALID_SCOPES = ["user", "project", "local"];
18
22
  const VALID_LISTS = ["allow", "deny"];
19
23
  function isScope(value) {
@@ -141,13 +145,15 @@ function formatMcpContextStatus(ctx) {
141
145
  lines.push("- MCP prompts are slash commands; they do not enter context until invoked.");
142
146
  return lines.join("\n");
143
147
  }
144
- function switchToProviderModel(providerId, modelId, ctx, thinkingLevel) {
148
+ function switchToProviderModel(providerId, modelId, ctx, thinkingLevel, preparedProvider) {
145
149
  const provider = ctx.registry.getConfigured().find((item) => item.id === providerId);
146
150
  if (!provider?.apiKey) {
147
151
  return false;
148
152
  }
149
- ctx.agent.thinking = normalizeThinkingLevel(thinkingLevel ?? ctx.agent.thinking, getAvailableThinkingLevels(providerId, modelId));
150
- ctx.agent.setProvider(ctx.createProvider(providerId, provider.apiKey, provider.baseURL));
153
+ ctx.agent.thinking = thinkingLevel !== undefined
154
+ ? normalizeThinkingLevel(thinkingLevel, getAvailableThinkingLevels(providerId, modelId))
155
+ : normalizeInheritedThinkingLevel(providerId, modelId, ctx.agent.thinking);
156
+ ctx.agent.setProvider(preparedProvider ?? ctx.createProvider(providerId, provider.apiKey, provider.baseURL));
151
157
  ctx.agent.providerId = providerId;
152
158
  ctx.agent.model = encodeModel(providerId, modelId);
153
159
  syncSystemPrompt(ctx, ctx.agent.model);
@@ -182,6 +188,122 @@ function displaySelectedModel(model, thinkingLevel) {
182
188
  const defaultLevel = providerId ? getDefaultThinkingLevel(providerId, modelId) : "off";
183
189
  return thinkingLevel === "off" || thinkingLevel === defaultLevel ? label : `${label} (${thinkingLevel})`;
184
190
  }
191
+ function getGrokSessionId(ctx) {
192
+ const binding = ctx.sessionManager?.getMetadata().externalRuntime;
193
+ return binding && isGrokSubscriptionProviderId(binding.id) ? binding.sessionId : undefined;
194
+ }
195
+ function isGrokSessionActive(ctx) {
196
+ return classifyExternalRuntimeBinding(ctx.sessionManager?.getMetadata().externalRuntime) === "grok";
197
+ }
198
+ function hasExternalRuntimeSession(ctx) {
199
+ return classifyExternalRuntimeBinding(ctx.sessionManager?.getMetadata().externalRuntime) !== "none";
200
+ }
201
+ async function transitionGrokSessionToNative(ctx) {
202
+ if (!hasExternalRuntimeSession(ctx))
203
+ return;
204
+ if (!ctx.transitionToNative) {
205
+ throw new Error("Switching from Grok subscription to a native Bubble session is unavailable in this mode.");
206
+ }
207
+ const next = await ctx.transitionToNative();
208
+ if (!next) {
209
+ throw new Error("Bubble could not start a fresh native session.");
210
+ }
211
+ // Handlers below may persist provider/model metadata after the transition.
212
+ // Keep those writes on the new native session rather than the old Grok log.
213
+ ctx.sessionManager = next;
214
+ }
215
+ /**
216
+ * Sign in to the Grok subscription as a native model provider (like ChatGPT
217
+ * OAuth): browser PKCE against auth.x.ai, tokens in ~/.bubble/auth.json, and
218
+ * grok models served through Bubble's own agent loop. An existing Grok CLI
219
+ * login is imported so users who already ran `grok login` skip the browser.
220
+ */
221
+ async function loginGrokSubscription(ctx) {
222
+ const providerId = "grok";
223
+ const { loginGrok, importGrokCliCredentials } = await import("../oauth/grok.js");
224
+ const storage = ctx.registry.getAuthStorage();
225
+ const runBrowserLogin = async () => {
226
+ const tokens = await loginGrok({
227
+ onStatus: (msg) => ctx.addMessage("assistant", msg),
228
+ });
229
+ storage.set(providerId, {
230
+ type: "oauth",
231
+ accessToken: tokens.accessToken,
232
+ refreshToken: tokens.refreshToken,
233
+ expiresAt: tokens.expiresAt,
234
+ });
235
+ };
236
+ let importedExisting = false;
237
+ if (!storage.has(providerId)) {
238
+ const imported = importGrokCliCredentials();
239
+ if (imported) {
240
+ storage.set(providerId, {
241
+ type: "oauth",
242
+ accessToken: imported.accessToken,
243
+ refreshToken: imported.refreshToken,
244
+ expiresAt: imported.expiresAt,
245
+ });
246
+ importedExisting = true;
247
+ ctx.addMessage("assistant", "Imported the existing Grok CLI sign-in.");
248
+ }
249
+ }
250
+ const hadStoredLogin = storage.has(providerId);
251
+ if (!hadStoredLogin) {
252
+ await runBrowserLogin();
253
+ }
254
+ try {
255
+ await ctx.registry.prepareProvider(providerId);
256
+ }
257
+ catch (error) {
258
+ // Stored/imported credentials whose refresh token was revoked get exactly
259
+ // one browser repair attempt; a failure right after browser login is real.
260
+ if (!hadStoredLogin)
261
+ throw error;
262
+ ctx.addMessage("assistant", `Stored Grok credentials could not be refreshed (${error instanceof Error ? error.message : String(error)}). Signing in again…`);
263
+ storage.remove(providerId);
264
+ await runBrowserLogin();
265
+ await ctx.registry.prepareProvider(providerId);
266
+ }
267
+ const provider = ctx.registry.getConfigured().find((item) => item.id === providerId);
268
+ const defaultModel = ctx.registry.getDefaultModel(providerId, "oauth");
269
+ if (!provider?.apiKey || !defaultModel) {
270
+ return `Grok subscription login succeeded, but the provider could not be activated. Tokens saved to ${storage.getPath()}`;
271
+ }
272
+ // Complete every fallible preparation step before leaving a legacy
273
+ // Grok-runtime session; commit only after the native switch succeeds.
274
+ const preparedProvider = ctx.createProvider(providerId, provider.apiKey, provider.baseURL);
275
+ await transitionGrokSessionToNative(ctx);
276
+ const switched = switchToProviderModel(providerId, defaultModel, ctx, undefined, preparedProvider);
277
+ if (!switched) {
278
+ return `Grok subscription login succeeded, but the provider could not be activated. Tokens saved to ${storage.getPath()}`;
279
+ }
280
+ ctx.registry.setDefault(providerId);
281
+ const via = importedExisting ? " (reused your Grok CLI sign-in)" : "";
282
+ return `Grok subscription login successful${via}. Switched to ${displayModel(ctx.agent.model)}. Tokens saved to ${storage.getPath()}`;
283
+ }
284
+ async function logoutGrokSubscription(ctx) {
285
+ const storage = ctx.registry.getAuthStorage();
286
+ const hadNativeLogin = storage.has("grok");
287
+ storage.remove("grok");
288
+ // Legacy cleanup: sessions bound to the old Grok ACP runtime, and the
289
+ // isolated CLI profile's credentials.
290
+ if (isGrokSessionActive(ctx) && ctx.externalRuntime && ctx.startFreshSession) {
291
+ await ctx.externalRuntime.cancel(getGrokSessionId(ctx)).catch(() => undefined);
292
+ await ctx.externalRuntime.dispose().catch(() => undefined);
293
+ await ctx.externalRuntime.logout().catch(() => undefined);
294
+ const freshSession = await ctx.startFreshSession();
295
+ freshSession.clearExternalRuntimeMetadata();
296
+ freshSession.appendMarker("runtime_switch", "native");
297
+ ctx.sessionManager = freshSession;
298
+ ctx.onExternalRuntimeChange?.(freshSession);
299
+ return "Grok subscription logged out. Started a fresh native Bubble session.";
300
+ }
301
+ await ctx.externalRuntime?.logout().catch(() => undefined);
302
+ if (!hadNativeLogin) {
303
+ return "No Grok subscription login was stored on this device.";
304
+ }
305
+ return "Grok subscription logged out. Only this device's local login was removed.";
306
+ }
185
307
  function parseMemoryScopeArgs(args) {
186
308
  const tokens = args.trim().split(/\s+/).filter(Boolean);
187
309
  let scope = "project";
@@ -298,6 +420,16 @@ const builtinSlashCommandEntries = [
298
420
  name: "help",
299
421
  description: "Show available slash commands",
300
422
  async handler(args, ctx) {
423
+ if (hasExternalRuntimeSession(ctx)) {
424
+ const grok = isGrokSessionActive(ctx);
425
+ return [
426
+ grok
427
+ ? "Grok Subscription · workspace tools · Bubble approvals"
428
+ : "Unsupported external runtime session · recovery-only mode",
429
+ "Available commands:",
430
+ ...GROK_LOCAL_COMMAND_HELP.map((command) => (` ${command.usage} - ${command.description}`)),
431
+ ].join("\n");
432
+ }
301
433
  const { registry } = await import("./index.js");
302
434
  const lines = ["Available commands:"];
303
435
  for (const cmd of registry.list()) {
@@ -475,11 +607,12 @@ const builtinSlashCommandEntries = [
475
607
  name: "provider",
476
608
  description: "Manage providers. /provider to switch, /provider --add [id] to add, /provider --remove <id>, /provider --set <id>",
477
609
  async handler(args, ctx) {
478
- if (!args) {
610
+ const trimmed = args.trim();
611
+ if (!trimmed) {
479
612
  ctx.openPicker("provider");
480
613
  return;
481
614
  }
482
- const parts = args.trim().split(/\s+/);
615
+ const parts = trimmed.split(/\s+/);
483
616
  const flag = parts[0];
484
617
  const value = parts[1];
485
618
  if (flag === "--add") {
@@ -487,6 +620,9 @@ const builtinSlashCommandEntries = [
487
620
  ctx.openPicker("provider-add");
488
621
  return;
489
622
  }
623
+ if (isGrokSubscriptionProviderId(value)) {
624
+ return "Grok Subscription is built in. Use /provider --set grok or /login grok to activate it.";
625
+ }
490
626
  const builtin = BUILTIN_PROVIDERS.find((p) => p.id === value && isUserVisibleProvider(p.id));
491
627
  if (!builtin) {
492
628
  const ids = BUILTIN_PROVIDERS.filter((p) => isUserVisibleProvider(p.id)).map((p) => p.id).join(", ");
@@ -498,17 +634,27 @@ const builtinSlashCommandEntries = [
498
634
  return;
499
635
  }
500
636
  if (flag === "--remove" && value) {
637
+ if (isGrokSubscriptionProviderId(value)) {
638
+ return "Grok Subscription is built in and cannot be removed. Use /logout grok to remove its local login.";
639
+ }
501
640
  if (ctx.registry.getModelConfig().hasProvider(value)) {
502
641
  return `Provider ${value} is defined in ~/.bubble/models.json. Please edit that file directly.`;
503
642
  }
504
643
  ctx.registry.removeProvider(value);
644
+ // Registry mutation without a model switch still changes the routing
645
+ // world (design §1.6): refresh the routing menu in the system prompt.
646
+ syncSystemPrompt(ctx, ctx.agent.model);
505
647
  return `Provider ${value} removed.`;
506
648
  }
507
649
  if (flag === "--set" && value) {
650
+ if (isGrokSubscriptionProviderId(value)) {
651
+ return loginGrokSubscription(ctx);
652
+ }
508
653
  const providers = ctx.registry.getConfigured();
509
654
  const p = providers.find((x) => x.id === value);
510
655
  if (!p)
511
656
  return `Provider ${value} is not configured.`;
657
+ await transitionGrokSessionToNative(ctx);
512
658
  ctx.registry.setDefault(value);
513
659
  if (ctx.registry.getModelConfig().hasProvider(value)) {
514
660
  return `Default provider set to ${p.name}. Note: config is managed via ~/.bubble/models.json.`;
@@ -517,13 +663,17 @@ const builtinSlashCommandEntries = [
517
663
  }
518
664
  if (flag === "--list") {
519
665
  const providers = ctx.registry.getConfigured();
520
- const lines = ["Configured providers:"];
666
+ const grokActive = isGrokSessionActive(ctx);
667
+ const lines = ["Available providers:"];
521
668
  for (const p of providers) {
522
- const marker = p.id === ctx.registry.getDefault()?.id ? "* " : " ";
669
+ const marker = !grokActive && p.id === ctx.registry.getDefault()?.id ? "* " : " ";
523
670
  const source = ctx.registry.getModelConfig().hasProvider(p.id) ? " [models.json]" : "";
524
671
  const oauth = ctx.registry.getAuthStorage().has(p.id) ? " [oauth]" : "";
525
672
  lines.push(`${marker}${p.name} (${p.id}) ${p.enabled ? "" : "[disabled]"}${oauth}${source}`);
526
673
  }
674
+ if (!providers.some((provider) => provider.id === "grok")) {
675
+ lines.push(`${grokActive ? "* " : " "}Grok Subscription (grok) [not signed in — /login grok]`);
676
+ }
527
677
  if (ctx.registry.getModelConfig().getLoadError()) {
528
678
  lines.push(`Warning: failed to load models.json: ${ctx.registry.getModelConfig().getLoadError()}`);
529
679
  }
@@ -534,15 +684,18 @@ const builtinSlashCommandEntries = [
534
684
  },
535
685
  {
536
686
  name: "login",
537
- description: "OAuth login for supported providers. Usage: /login [openai]",
687
+ description: "Login to OpenAI OAuth or Grok Subscription. Usage: /login [openai|grok]",
538
688
  async handler(args, ctx) {
539
689
  const providerId = args?.trim() || "openai";
540
690
  if (!providerId) {
541
691
  ctx.openPicker("login");
542
692
  return;
543
693
  }
694
+ if (isGrokSubscriptionProviderId(providerId)) {
695
+ return loginGrokSubscription(ctx);
696
+ }
544
697
  if (!ctx.registry.supportsOAuth(providerId)) {
545
- return `Unsupported OAuth provider: ${providerId}. Currently only 'openai' is supported.`;
698
+ return `Unsupported login provider: ${providerId}. Supported providers: 'openai' and 'grok'.`;
546
699
  }
547
700
  const { loginOpenAICodex } = await import("../oauth/openai-codex.js");
548
701
  const tokens = await loginOpenAICodex({
@@ -557,17 +710,25 @@ const builtinSlashCommandEntries = [
557
710
  accountId: tokens.accountId,
558
711
  });
559
712
  await ctx.registry.prepareProvider(providerId);
560
- ctx.registry.setDefault(providerId);
561
713
  const provider = ctx.registry.getConfigured().find((item) => item.id === providerId);
562
714
  const discoveredModels = provider ? await ctx.registry.listModels(provider) : [];
563
715
  const defaultModel = discoveredModels[0]?.id || ctx.registry.getDefaultModel(providerId, "oauth");
564
716
  if (!defaultModel) {
565
717
  return `OpenAI Codex OAuth login succeeded, but no default model is configured for ${providerId}.`;
566
718
  }
567
- const switched = switchToProviderModel(providerId, defaultModel, ctx);
719
+ if (!provider?.apiKey) {
720
+ return `OpenAI Codex OAuth login succeeded, but the provider could not be activated. Tokens saved to ${ctx.registry.getAuthStorage().getPath()}`;
721
+ }
722
+ // Complete every fallible OAuth/provider/model preparation step before
723
+ // leaving a Grok-bound session. The provider object is created once and
724
+ // committed only after the fresh native session switch succeeds.
725
+ const preparedProvider = ctx.createProvider(providerId, provider.apiKey, provider.baseURL);
726
+ await transitionGrokSessionToNative(ctx);
727
+ const switched = switchToProviderModel(providerId, defaultModel, ctx, undefined, preparedProvider);
568
728
  if (!switched) {
569
729
  return `OpenAI Codex OAuth login succeeded, but the provider could not be activated. Tokens saved to ${ctx.registry.getAuthStorage().getPath()}`;
570
730
  }
731
+ ctx.registry.setDefault(providerId);
571
732
  return `OpenAI Codex OAuth login successful. Switched to ${displayModel(ctx.agent.model)}. Account: ${tokens.accountId || "unknown"}. Tokens saved to ${ctx.registry.getAuthStorage().getPath()}`;
572
733
  },
573
734
  },
@@ -576,6 +737,13 @@ const builtinSlashCommandEntries = [
576
737
  description: "Switch model. Use /model <id> [--reasoning-effort <level>] or just /model to open picker.",
577
738
  async handler(args, ctx) {
578
739
  if (!args) {
740
+ if (hasExternalRuntimeSession(ctx)) {
741
+ if (isGrokSessionActive(ctx)) {
742
+ ctx.openPicker("model");
743
+ return;
744
+ }
745
+ return "This external runtime manages its model and reasoning settings. Bubble's model picker is unavailable until you switch to a native session.";
746
+ }
579
747
  if (ctx.registry.getEnabled().length === 0) {
580
748
  ctx.openPicker("model");
581
749
  return;
@@ -588,30 +756,69 @@ const builtinSlashCommandEntries = [
588
756
  return parsed.error;
589
757
  }
590
758
  if (!parsed.model) {
759
+ if (hasExternalRuntimeSession(ctx)) {
760
+ if (isGrokSessionActive(ctx)) {
761
+ ctx.openPicker("model");
762
+ return;
763
+ }
764
+ return "This external runtime manages its model and reasoning settings. Bubble's model picker is unavailable until you switch to a native session.";
765
+ }
591
766
  ctx.openPicker("model");
592
767
  return;
593
768
  }
769
+ const explicitModelProvider = parsed.model.includes(":") ? parsed.model.split(":", 1)[0] : undefined;
770
+ if (isGrokSessionActive(ctx) && (!explicitModelProvider || isGrokSubscriptionProviderId(explicitModelProvider))) {
771
+ if (!ctx.externalRuntime)
772
+ return "Grok Subscription runtime is unavailable.";
773
+ const boundSessionId = getGrokSessionId(ctx);
774
+ if (boundSessionId)
775
+ await ctx.externalRuntime.hydrateSession(boundSessionId);
776
+ const requestedModel = parsed.model.includes(":")
777
+ ? parsed.model.split(":").slice(1).join(":")
778
+ : parsed.model;
779
+ const selection = await ctx.externalRuntime.setModel(requestedModel, parsed.thinkingLevel);
780
+ const metadata = ctx.sessionManager?.getMetadata().externalRuntime;
781
+ if (metadata && isGrokSubscriptionProviderId(metadata.id)) {
782
+ ctx.sessionManager?.updateMetadata({
783
+ externalRuntime: {
784
+ ...metadata,
785
+ modelId: selection.modelId,
786
+ reasoningEffort: selection.reasoningEffort,
787
+ },
788
+ });
789
+ }
790
+ ctx.sessionManager?.appendMarker("model_switch", selection.modelId ?? requestedModel);
791
+ ctx.sessionManager?.appendMarker("thinking_level_switch", selection.reasoningEffort);
792
+ ctx.onExternalRuntimeChange?.(ctx.sessionManager);
793
+ return `Grok model switched to ${selection.modelId ?? requestedModel}${selection.reasoningEffort !== "off" ? ` (${selection.reasoningEffort})` : ""}.`;
794
+ }
594
795
  const defaultProvider = ctx.registry.getDefault()?.id || "openai";
595
796
  const next = parsed.model.includes(":") ? parsed.model : encodeModel(defaultProvider, parsed.model);
596
797
  const { providerId, modelId } = decodeModel(next);
597
798
  const targetProviderId = providerId || defaultProvider;
598
799
  await ctx.registry.prepareProvider(targetProviderId);
599
- const switched = switchToProviderModel(targetProviderId, modelId, ctx, parsed.thinkingLevel);
600
- if (!switched) {
800
+ const targetProvider = ctx.registry.getConfigured().find((item) => item.id === targetProviderId);
801
+ if (!targetProvider?.apiKey) {
601
802
  return `Provider ${targetProviderId} is not configured or has no active credentials.`;
602
803
  }
804
+ const preparedProvider = ctx.createProvider(targetProviderId, targetProvider.apiKey, targetProvider.baseURL);
805
+ await transitionGrokSessionToNative(ctx);
806
+ switchToProviderModel(targetProviderId, modelId, ctx, parsed.thinkingLevel, preparedProvider);
603
807
  return `Model switched to ${displaySelectedModel(next, ctx.agent.thinking)}.`;
604
808
  },
605
809
  },
606
810
  {
607
811
  name: "logout",
608
- description: "Remove OAuth credentials for a provider. Usage: /logout [openai]",
812
+ description: "Remove local login credentials. Usage: /logout [openai|grok]",
609
813
  async handler(args, ctx) {
610
814
  const providerId = args?.trim() || "openai";
611
815
  if (!providerId) {
612
816
  ctx.openPicker("logout");
613
817
  return;
614
818
  }
819
+ if (isGrokSubscriptionProviderId(providerId)) {
820
+ return logoutGrokSubscription(ctx);
821
+ }
615
822
  if (!ctx.registry.getAuthStorage().has(providerId)) {
616
823
  return `No OAuth credentials found for ${providerId}.`;
617
824
  }
@@ -629,6 +836,9 @@ const builtinSlashCommandEntries = [
629
836
  else if (ctx.agent.providerId === providerId) {
630
837
  ctx.agent.providerId = "";
631
838
  }
839
+ // No-fallback logout mutates the routing world without a model switch
840
+ // (design §1.6): refresh the menu so it stops advertising the provider.
841
+ syncSystemPrompt(ctx, ctx.agent.model);
632
842
  return `OAuth credentials for ${providerId} removed.`;
633
843
  },
634
844
  },
@@ -1,5 +1,5 @@
1
1
  import type { Agent } from "../agent.js";
2
- import type { SessionManager } from "../session.js";
2
+ import type { SessionManager, SessionMetadata } from "../session.js";
3
3
  import type { Provider } from "../types.js";
4
4
  import type { ProviderRegistry } from "../provider-registry.js";
5
5
  import type { SkillRegistry } from "../skills/registry.js";
@@ -10,6 +10,7 @@ import type { LspService } from "../lsp/index.js";
10
10
  import type { MemoryScope } from "../memory/index.js";
11
11
  import type { ThemeMode } from "../config.js";
12
12
  import type { ExternalHookController } from "../hooks/controller.js";
13
+ import type { ExternalRuntimeManager } from "../external-runtime/types.js";
13
14
  /**
14
15
  * Live progress for a manual `/compact` run, pushed to the TUI so it can render
15
16
  * a progress bar. `phase` advances collecting → summarizing → applying;
@@ -61,6 +62,18 @@ export interface SlashCommandContext {
61
62
  * while compacting and `null` to clear the indicator. Absent in non-TUI hosts.
62
63
  */
63
64
  compactionProgress?: (progress: CompactionProgress | null) => void;
65
+ /** External agent runtime used by Grok subscription chat. */
66
+ externalRuntime?: ExternalRuntimeManager;
67
+ /**
68
+ * Atomically create and bind a fresh Bubble session with optional metadata.
69
+ * External runtimes are session-affine, so crossing the native/external
70
+ * boundary never reuses history.
71
+ */
72
+ startFreshSession?: (metadata?: Partial<SessionMetadata>) => Promise<SessionManager>;
73
+ /** Stop the external runtime and atomically enter a fresh native session. */
74
+ transitionToNative?: () => Promise<SessionManager | undefined>;
75
+ /** Refresh TUI state after a command changes the external session binding. */
76
+ onExternalRuntimeChange?: (manager?: SessionManager) => void;
64
77
  }
65
78
  /**
66
79
  * Return types for a slash command handler:
@@ -32,5 +32,7 @@ export interface SystemPromptOptions {
32
32
  memoryPrompt?: string;
33
33
  /** Durable child-agent profile prompt used for subagents. */
34
34
  agentProfilePrompt?: string;
35
+ /** Rendered subagent model-routing menu (design §4); parents only. */
36
+ modelRoutingPrompt?: string;
35
37
  }
36
38
  export declare function buildSystemPrompt(options?: SystemPromptOptions): string;
@@ -24,15 +24,7 @@ export declare function createWaitAgentTool(): ToolRegistryEntry;
24
24
  export declare function createSendInputTool(): ToolRegistryEntry;
25
25
  export declare function createCloseAgentTool(): ToolRegistryEntry;
26
26
  export declare function createListAgentsTool(): ToolRegistryEntry;
27
- /** Items bound for one agent_team call (design §1.2). */
28
- export declare const AGENT_TEAM_MIN_ITEMS = 2;
29
- export declare const AGENT_TEAM_MAX_ITEMS = 32;
30
- export declare function createAgentTeamTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
31
- /** Specs bound for one agent_batch call (design v2 §1.3). */
32
- export declare const AGENT_BATCH_MIN_SPECS = 2;
33
- export declare const AGENT_BATCH_MAX_SPECS = 32;
34
- export declare function createAgentBatchTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
35
- export declare function createRunWorkflowTool(options?: AgentLifecycleToolOptions): ToolRegistryEntry;
27
+ export declare function createRunWorkflowTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
36
28
  export declare function createWaitWorkflowTool(): ToolRegistryEntry;
37
29
  export declare function createAgentLifecycleTools(options?: AgentLifecycleToolOptions): ToolRegistryEntry[];
38
30
  export {};