@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
@@ -11,12 +11,12 @@ import { MessageList } from "./message-list.js";
11
11
  import { isMultiplexedTerminal } from "./terminal-env.js";
12
12
  import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
13
13
  import { AgentRunInputQueue } from "../agent/input-controller.js";
14
- import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
14
+ import { canvasBackgroundFor, paletteFor, ThemeProvider, useTheme } from "./theme.js";
15
15
  import { isPrintablePickerInput, ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
16
16
  import { FeishuSetupPicker } from "./feishu-setup-picker.js";
17
- import { BUILTIN_PROVIDERS, ProviderRegistry, displayModel, isUserVisibleProvider } from "../provider-registry.js";
17
+ import { BUILTIN_PROVIDERS, ProviderRegistry, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
18
18
  import { buildSystemPrompt } from "../system-prompt.js";
19
- import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
19
+ import { getAvailableThinkingLevels, isThinkingOnlyLevels, isThinkingToggleModel, normalizeThinkingLevel } from "../provider-transform.js";
20
20
  import { FooterBar, buildFooterData } from "./footer.js";
21
21
  import { SkillRegistry } from "../skills/registry.js";
22
22
  import { parseSkillInvocation } from "../skills/invocation.js";
@@ -32,13 +32,13 @@ import { QuestionDialog } from "./question-dialog.js";
32
32
  import { FeedbackDialog } from "./feedback-dialog.js";
33
33
  import { collectFeedback } from "../feedback/collect.js";
34
34
  import { isKeyReleaseEvent } from "./key-events.js";
35
- import { errorMessage, formatModelSwitchError, switchAgentModel } from "../tui/model-switch.js";
35
+ import { errorMessage, formatModelSwitchError, modelSwitchTarget, switchAgentModel } from "../tui/model-switch.js";
36
36
  import { formatImageUserDisplayText, nextImageDisplayLabelStart } from "../tui/image-display.js";
37
37
  import { decideStartingSubmitFingerprint, submitPayloadFingerprint } from "./submit-dedupe.js";
38
38
  import { isQueuedInputForCurrentSession, queuedAndPendingDisplayKeys, } from "./input-queue.js";
39
39
  import { SessionPicker } from "./session-picker.js";
40
40
  import { SubagentInspector } from "./subagent-inspector.js";
41
- import { collectSubagentGroups, subagentSummary } from "./subagent-view.js";
41
+ import { accumulateLiveSubagentUpdate, collectSubagentGroups, mergeToolMetadata, pruneSettledLiveSubagentTools, subagentSummary } from "./subagent-view.js";
42
42
  import { sessionDisplayName } from "../tui/session-display.js";
43
43
  import { parseGoalCommand } from "../goal/command.js";
44
44
  import { continuationPrompt, initialPrompt } from "../goal/prompts.js";
@@ -47,9 +47,21 @@ import { goalCompleteNotice, goalIndicatorLine, goalSummaryText } from "../goal/
47
47
  import { tokenUsageTotal } from "../goal/usage.js";
48
48
  import { formatInternalContextBlock } from "../agent/internal-reminder-sanitizer.js";
49
49
  import { collectUsageStatsBundle, formatStatsPanelBody, rangeLabel } from "../stats/usage.js";
50
+ import { GrokRuntimeError } from "../external-runtime/grok-errors.js";
51
+ import { GROK_LOCAL_SLASH_COMMANDS, classifyGrokInput, isGrokLocalSlashCommand, } from "../external-runtime/grok-input-policy.js";
52
+ import { GROK_SUBSCRIPTION_PROVIDER_ID, isGrokSubscriptionProviderId, withGrokSubscriptionProvider, } from "../external-runtime/grok-provider.js";
53
+ import { classifyExternalRuntimeBinding, stopExternalRuntimeForSessionSwitch, } from "../external-runtime/session-policy.js";
50
54
  import os from "node:os";
51
- function buildTips(agent, registry) {
55
+ import { rmSync } from "node:fs";
56
+ function buildTips(agent, registry, grokActive = false) {
52
57
  const tips = [];
58
+ if (grokActive) {
59
+ return [
60
+ "Grok Subscription is active with workspace tools and Bubble approvals",
61
+ "Use /model to choose the Grok model and reasoning effort",
62
+ "Use /logout grok to return to a fresh native session",
63
+ ];
64
+ }
53
65
  const hasProvider = registry.getEnabled().length > 0;
54
66
  if (!hasProvider) {
55
67
  tips.push("Run /login or /provider --add to configure a model");
@@ -189,33 +201,6 @@ function parsePartialArgs(buffer, previous) {
189
201
  }
190
202
  return result;
191
203
  }
192
- function mergeToolMetadata(current, incoming) {
193
- if (!incoming)
194
- return current;
195
- if (current?.kind !== "subagent" || incoming.kind !== "subagent") {
196
- return incoming;
197
- }
198
- const currentSubagents = Array.isArray(current.subagents) ? current.subagents : [];
199
- const incomingSubagents = Array.isArray(incoming.subagents) ? incoming.subagents : [];
200
- const byId = new Map();
201
- for (const item of currentSubagents) {
202
- const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
203
- ? String(item.subAgentId)
204
- : "";
205
- byId.set(subAgentId || `current:${byId.size}`, item);
206
- }
207
- for (const item of incomingSubagents) {
208
- const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
209
- ? String(item.subAgentId)
210
- : "";
211
- byId.set(subAgentId || `incoming:${byId.size}`, item);
212
- }
213
- return {
214
- ...current,
215
- ...incoming,
216
- subagents: [...byId.values()],
217
- };
218
- }
219
204
  /**
220
205
  * Coerce a freshly-constructed DisplayMessage into one that carries a stable
221
206
  * `key`. Centralizes the safety net so callers don't have to remember to call
@@ -238,8 +223,13 @@ export const INK_LOCAL_SLASH_COMMANDS = [
238
223
  description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
239
224
  },
240
225
  ];
241
- export function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, onExit }) {
226
+ export function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }) {
242
227
  const [sessionManager, setSessionManager] = useState(initialSessionManager);
228
+ const [externalRuntimeBinding, setExternalRuntimeBinding] = useState(() => initialSessionManager?.getMetadata().externalRuntime);
229
+ const sessionManagerRef = useRef(sessionManager);
230
+ sessionManagerRef.current = sessionManager;
231
+ const externalRuntimeBindingRef = useRef(externalRuntimeBinding);
232
+ externalRuntimeBindingRef.current = externalRuntimeBinding;
243
233
  const [themeMode, setThemeMode] = useState(initialThemeMode ?? "auto");
244
234
  // `detectedTheme` is captured once at startup in main.ts. We keep it in state
245
235
  // so future re-detection (e.g. if a user runs `/theme auto` after switching
@@ -248,7 +238,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
248
238
  const [autoResolved] = useState(detectedTheme ?? "dark");
249
239
  const palette = useMemo(() => {
250
240
  const resolved = themeMode === "auto" ? autoResolved : themeMode;
251
- return paletteFor(resolved, themeOverrides);
241
+ const base = paletteFor(resolved, themeOverrides);
242
+ // A user override wins; otherwise paint the canvas only when a forced
243
+ // theme mismatches the terminal it is running in.
244
+ if (base.background === undefined) {
245
+ const canvas = canvasBackgroundFor(themeMode, resolved, autoResolved);
246
+ if (canvas !== undefined)
247
+ return { ...base, background: canvas };
248
+ }
249
+ return base;
252
250
  }, [themeMode, autoResolved, themeOverrides]);
253
251
  const applyThemeMode = useCallback((mode) => {
254
252
  setThemeMode(mode);
@@ -268,9 +266,27 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
268
266
  const [streamingReasoning, setStreamingReasoning] = useState("");
269
267
  const [streamingTools, setStreamingTools] = useState([]);
270
268
  const [streamingParts, setStreamingParts] = useState([]);
269
+ // Background-child updates that outlived their launching tool call's
270
+ // streaming round (toolCalls is cleared on every turn_start): keyed by the
271
+ // launching call id, they feed the inspector as synthetic tool calls so
272
+ // traces stay live e.g. while a wait_workflow blocks. The version counter
273
+ // triggers recompute since the ref mutates in place.
274
+ const liveSubagentToolsRef = useRef(new Map());
275
+ const [liveSubagentVersion, setLiveSubagentVersion] = useState(0);
276
+ // Transcript-reset paths (/clear, session switch, /rewind) must drop the
277
+ // accumulator too, or ghost subagent groups from the wiped conversation keep
278
+ // feeding the entry line and inspector for the rest of the process.
279
+ const clearLiveSubagentTools = useCallback(() => {
280
+ if (liveSubagentToolsRef.current.size === 0)
281
+ return;
282
+ liveSubagentToolsRef.current.clear();
283
+ setLiveSubagentVersion((version) => version + 1);
284
+ }, []);
271
285
  // Live subagent groups for the inspector opened from the subagent entry line;
272
286
  // recomputed each render so it reflects members as their events stream into the transcript.
273
- const subagentGroups = useMemo(() => collectSubagentGroups(messages, streamingTools), [messages, streamingTools]);
287
+ const subagentGroups = useMemo(() => collectSubagentGroups(messages, [...streamingTools, ...liveSubagentToolsRef.current.values()]),
288
+ // eslint-disable-next-line react-hooks/exhaustive-deps
289
+ [messages, streamingTools, liveSubagentVersion]);
274
290
  const subagentMembers = useMemo(() => subagentGroups.flatMap((g) => g.members), [subagentGroups]);
275
291
  // Down-arrow from the composer focuses the subagent entry line; Enter then
276
292
  // opens the inspector, Esc/Up returns to the composer (Claude Code parity).
@@ -302,6 +318,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
302
318
  const [pendingQuestion, setPendingQuestion] = useState(null);
303
319
  const [pendingFeedback, setPendingFeedback] = useState(null);
304
320
  const [pickerMode, setPickerMode] = useState(null);
321
+ const [grokModels, setGrokModels] = useState([]);
305
322
  const [statsPanel, setStatsPanel] = useState(null);
306
323
  const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
307
324
  const [composerDraft, setComposerDraft] = useState(null);
@@ -341,6 +358,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
341
358
  const startingSubmitFingerprintRef = useRef(null);
342
359
  const [startingSubmitFingerprint, setStartingSubmitFingerprint] = useState(null);
343
360
  const nextRunIdRef = useRef(0);
361
+ // Invalidates late external-runtime events whenever the Bubble session or
362
+ // runtime binding changes. Native Agent events retain their existing path.
363
+ const externalRuntimeGenerationRef = useRef(0);
344
364
  // Set true the moment /quit is invoked so we can hide dynamic UI (composer,
345
365
  // waiting indicator, footer) before Ink snapshots its final frame into the
346
366
  // shell scrollback. Without this, the last visible "> " input row stays
@@ -366,6 +386,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
366
386
  cwd: args.cwd,
367
387
  skillPaths: userConfig.getSkillPaths(),
368
388
  });
389
+ const externalRuntimeBindingKind = classifyExternalRuntimeBinding(externalRuntimeBinding);
390
+ const externalSessionBound = externalRuntimeBindingKind !== "none";
391
+ const grokSessionBound = externalRuntimeBindingKind === "grok";
392
+ const unsupportedExternalSessionBound = externalRuntimeBindingKind === "unsupported";
393
+ const refreshExternalRuntimeBinding = useCallback((manager) => {
394
+ const next = (manager ?? sessionManagerRef.current)?.getMetadata().externalRuntime;
395
+ externalRuntimeBindingRef.current = next;
396
+ setExternalRuntimeBinding(next);
397
+ }, []);
369
398
  useEffect(() => {
370
399
  setCurrentUpdateNotice(updateNotice);
371
400
  }, [updateNotice]);
@@ -564,6 +593,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
564
593
  return;
565
594
  }
566
595
  if (key.ctrl && key.shift && input.toLowerCase() === "m" && !pickerMode) {
596
+ if (externalSessionBound) {
597
+ addMessage("assistant", "MCP is unavailable in external runtime sessions (no Bubble workspace access).");
598
+ return;
599
+ }
567
600
  if (!mcpManager || mcpManager.getStates().length === 0) {
568
601
  addMessage("assistant", "No MCP servers configured.");
569
602
  }
@@ -588,6 +621,35 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
588
621
  }
589
622
  // Ctrl+R: cycle thinking level (formerly Shift+Tab)
590
623
  if (isCtrlLetterInput(input, key, "r") && !pickerMode) {
624
+ if (externalSessionBound) {
625
+ if (!grokSessionBound || !externalRuntime) {
626
+ addMessage("assistant", "The external runtime manages its own model and reasoning settings.");
627
+ return;
628
+ }
629
+ const bound = externalRuntimeBindingRef.current;
630
+ void (bound?.sessionId
631
+ ? externalRuntime.hydrateSession(bound.sessionId, bound.modelId, bound.reasoningEffort)
632
+ : Promise.resolve()).then(async () => await externalRuntime.listModels()).then(async (models) => {
633
+ const current = externalRuntime.getModelSelection();
634
+ const model = models.find((candidate) => candidate.id === current.modelId);
635
+ if (!model || model.reasoningLevels.length < 2) {
636
+ addMessage("assistant", `${current.modelId ?? "The current Grok model"} has no alternate reasoning effort.`);
637
+ return;
638
+ }
639
+ const index = Math.max(0, model.reasoningLevels.indexOf(current.reasoningEffort));
640
+ const next = model.reasoningLevels[(index + 1) % model.reasoningLevels.length];
641
+ const selection = await externalRuntime.setModel(model.id, next);
642
+ const manager = sessionManagerRef.current;
643
+ const binding = manager?.getMetadata().externalRuntime;
644
+ if (manager && binding && isGrokSubscriptionProviderId(binding.id)) {
645
+ manager.updateMetadata({ externalRuntime: { ...binding, ...selection } });
646
+ manager.appendMarker("thinking_level_switch", selection.reasoningEffort);
647
+ refreshExternalRuntimeBinding(manager);
648
+ }
649
+ addMessage("assistant", `Grok reasoning effort set to ${selection.reasoningEffort}.`);
650
+ }).catch((error) => addMessage("error", `Failed to switch Grok reasoning: ${errorMessage(error)}`));
651
+ return;
652
+ }
591
653
  const modelParts = agent.model.includes(":")
592
654
  ? agent.model.split(":")
593
655
  : [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
@@ -681,7 +743,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
681
743
  // messages alone leaves a stale To-Do list on screen. /clear already reset
682
744
  // the agent's todos; mirror that into the UI (same as session switch).
683
745
  setTodos(agent.getTodos());
684
- }, [resetTranscript, agent]);
746
+ clearLiveSubagentTools();
747
+ }, [resetTranscript, agent, clearLiveSubagentTools]);
685
748
  // Render a placeholder user row for input waiting to enter the run.
686
749
  const addStatusUserMessage = useCallback((content, status) => {
687
750
  const key = nextDisplayMessageKey("user");
@@ -700,7 +763,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
700
763
  return { ...payload, imageDisplayStart };
701
764
  }, []);
702
765
  const submitDisplayText = useCallback((payload) => (formatImageUserDisplayText(payload.displayText ?? payload.text, payload.images.length, payload.imageDisplayStart)), []);
703
- const currentSessionFile = useCallback(() => sessionManager?.getSessionFile(), [sessionManager]);
766
+ const currentSessionFile = useCallback(() => sessionManagerRef.current?.getSessionFile(), []);
704
767
  const queueInput = useCallback((payload) => {
705
768
  const preparedPayload = prepareSubmitDisplay(payload);
706
769
  const displayKey = addStatusUserMessage(submitDisplayText(preparedPayload), "queued");
@@ -720,6 +783,19 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
720
783
  setPendingSteerCount(pendingSteersRef.current.size);
721
784
  }, [addStatusUserMessage, currentSessionFile, prepareSubmitDisplay, queueInput, submitDisplayText]);
722
785
  const openPicker = useCallback((mode, providerId) => {
786
+ if (mode === "model" && grokSessionBound && externalRuntime) {
787
+ setStatsPanel(null);
788
+ setGrokModels([]);
789
+ setPickerMode("grok-model");
790
+ const bound = externalRuntimeBindingRef.current;
791
+ void (bound?.sessionId
792
+ ? externalRuntime.hydrateSession(bound.sessionId, bound.modelId, bound.reasoningEffort)
793
+ : Promise.resolve()).then(async () => await externalRuntime.listModels()).then(setGrokModels).catch((error) => {
794
+ addMessage("error", `Failed to load Grok models: ${errorMessage(error)}`);
795
+ setPickerMode(null);
796
+ });
797
+ return;
798
+ }
723
799
  if (mode === "key") {
724
800
  setKeyProviderId(providerId ?? null);
725
801
  }
@@ -728,11 +804,36 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
728
804
  }
729
805
  setStatsPanel(null);
730
806
  setPickerMode(mode);
731
- }, []);
807
+ }, [addMessage, externalRuntime, grokSessionBound]);
732
808
  const closePicker = useCallback(() => {
733
809
  setPickerMode(null);
734
810
  setCursorResetEpoch((epoch) => epoch + 1);
735
811
  }, []);
812
+ const handleGrokModelSelect = useCallback((encoded) => {
813
+ if (!externalRuntime)
814
+ return;
815
+ const run = async () => {
816
+ const parsed = JSON.parse(encoded);
817
+ const boundSessionId = externalRuntimeBindingRef.current?.sessionId;
818
+ if (boundSessionId)
819
+ await externalRuntime.hydrateSession(boundSessionId);
820
+ const selection = await externalRuntime.setModel(parsed.modelId, parsed.reasoningEffort);
821
+ const manager = sessionManagerRef.current;
822
+ const binding = manager?.getMetadata().externalRuntime;
823
+ if (manager && binding && isGrokSubscriptionProviderId(binding.id)) {
824
+ manager.updateMetadata({ externalRuntime: { ...binding, ...selection } });
825
+ manager.appendMarker("model_switch", selection.modelId ?? parsed.modelId);
826
+ manager.appendMarker("thinking_level_switch", selection.reasoningEffort);
827
+ refreshExternalRuntimeBinding(manager);
828
+ }
829
+ addMessage("assistant", `Grok model switched to ${selection.modelId ?? parsed.modelId}${selection.reasoningEffort !== "off" ? ` (${selection.reasoningEffort})` : ""}.`);
830
+ closePicker();
831
+ };
832
+ void run().catch((error) => {
833
+ addMessage("error", `Failed to switch Grok model: ${errorMessage(error)}`);
834
+ closePicker();
835
+ });
836
+ }, [addMessage, closePicker, externalRuntime, refreshExternalRuntimeBinding]);
736
837
  const fillComposer = useCallback((text) => {
737
838
  setComposerDraft((current) => ({
738
839
  text,
@@ -782,23 +883,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
782
883
  setStatsPanel(null);
783
884
  setCursorResetEpoch((epoch) => epoch + 1);
784
885
  }, []);
785
- const handleSessionSelect = useCallback((sessionFile) => {
786
- if (!switchSession) {
787
- addMessage("error", "Session switching is not available in this mode.");
788
- closePicker();
789
- return;
790
- }
791
- if (activeAbortRef.current) {
792
- addMessage("error", "Stop the current run before switching sessions.");
793
- closePicker();
794
- return;
795
- }
796
- const result = switchSession(sessionFile);
797
- if ("error" in result) {
798
- addMessage("error", `Failed to switch session: ${result.error}`);
799
- closePicker();
800
- return;
801
- }
886
+ const applySessionSwitch = useCallback((result, notice) => {
887
+ externalRuntimeGenerationRef.current += 1;
802
888
  const queuedDisplayKeys = queuedAndPendingDisplayKeys(queuedInputsRef.current, pendingSteersRef.current.values());
803
889
  queuedInputsRef.current = [];
804
890
  pendingSteersRef.current.clear();
@@ -807,33 +893,131 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
807
893
  setPendingSteerCount(0);
808
894
  setStartingSubmit(null);
809
895
  clearComposerDraft();
896
+ sessionManagerRef.current = result.manager;
810
897
  setSessionManager(result.manager);
898
+ const nextExternalBinding = result.manager.getMetadata().externalRuntime;
899
+ externalRuntimeBindingRef.current = nextExternalBinding;
900
+ setExternalRuntimeBinding(nextExternalBinding);
811
901
  setTodos(agent.getTodos());
902
+ clearLiveSubagentTools();
812
903
  resetTranscript(() => [
813
904
  ...reconstructDisplayMessages(agent.messages).filter((message) => !queuedDisplayKeys.has(message.key ?? "")),
814
- withMessageKey({ role: "assistant", content: `⤷ Resumed session: ${sessionDisplayName(result.manager)}` }),
905
+ ...(notice ? [withMessageKey({ role: "assistant", content: notice })] : []),
815
906
  ]);
816
907
  closePicker();
817
- }, [addMessage, agent, clearComposerDraft, closePicker, setStartingSubmit, switchSession, resetTranscript]);
908
+ return result.manager;
909
+ }, [agent, clearComposerDraft, clearLiveSubagentTools, closePicker, resetTranscript, setStartingSubmit]);
910
+ const prepareFreshSession = useCallback((metadata) => {
911
+ const fresh = SessionManager.createFresh(args.cwd);
912
+ try {
913
+ if (metadata && Object.keys(metadata).length > 0) {
914
+ fresh.updateMetadata(metadata);
915
+ }
916
+ return fresh;
917
+ }
918
+ catch (error) {
919
+ rmSync(fresh.getSessionFile(), { force: true });
920
+ throw error;
921
+ }
922
+ }, [args.cwd]);
923
+ const commitFreshSession = useCallback((fresh) => {
924
+ if (!switchSession) {
925
+ rmSync(fresh.getSessionFile(), { force: true });
926
+ throw new Error("Starting a fresh session is not available in this mode.");
927
+ }
928
+ const result = switchSession(fresh.getSessionFile());
929
+ if ("error" in result) {
930
+ rmSync(fresh.getSessionFile(), { force: true });
931
+ throw new Error(result.error);
932
+ }
933
+ return applySessionSwitch(result);
934
+ }, [applySessionSwitch, switchSession]);
935
+ const startFreshSession = useCallback(async (metadata) => {
936
+ return commitFreshSession(prepareFreshSession(metadata));
937
+ }, [commitFreshSession, prepareFreshSession]);
938
+ const transitionToNative = useCallback(async () => {
939
+ if (!externalSessionBound)
940
+ return sessionManagerRef.current;
941
+ const candidate = prepareFreshSession();
942
+ externalRuntimeGenerationRef.current += 1;
943
+ const boundSessionId = externalRuntimeBinding?.sessionId;
944
+ try {
945
+ // Validate/prepare the native provider before calling this function.
946
+ // The external sidecar must be fully stopped before the new Bubble
947
+ // session is committed. If commit then fails, the old binding remains
948
+ // authoritative and the reusable manager will lazy-load it next time.
949
+ await stopExternalRuntimeForSessionSwitch(externalRuntime, boundSessionId);
950
+ return commitFreshSession(candidate);
951
+ }
952
+ catch (error) {
953
+ rmSync(candidate.getSessionFile(), { force: true });
954
+ throw error;
955
+ }
956
+ }, [commitFreshSession, externalRuntime, externalRuntimeBinding?.sessionId, externalSessionBound, prepareFreshSession]);
957
+ const handleSessionSelect = useCallback((sessionFile) => {
958
+ const run = async () => {
959
+ if (!switchSession) {
960
+ addMessage("error", "Session switching is not available in this mode.");
961
+ closePicker();
962
+ return;
963
+ }
964
+ if (activeAbortRef.current) {
965
+ addMessage("error", "Stop the current run before switching sessions.");
966
+ closePicker();
967
+ return;
968
+ }
969
+ if (externalSessionBound) {
970
+ externalRuntimeGenerationRef.current += 1;
971
+ const boundSessionId = externalRuntimeBinding?.sessionId;
972
+ await stopExternalRuntimeForSessionSwitch(externalRuntime, boundSessionId);
973
+ }
974
+ const result = switchSession(sessionFile);
975
+ if ("error" in result) {
976
+ addMessage("error", `Failed to switch session: ${result.error}`);
977
+ closePicker();
978
+ return;
979
+ }
980
+ applySessionSwitch(result, `⤷ Resumed session: ${sessionDisplayName(result.manager)}`);
981
+ };
982
+ void run().catch((error) => {
983
+ addMessage("error", `Failed to switch session: ${errorMessage(error)}`);
984
+ closePicker();
985
+ });
986
+ }, [activeAbortRef, addMessage, applySessionSwitch, closePicker, externalRuntime, externalRuntimeBinding?.sessionId, externalSessionBound, switchSession]);
818
987
  const handleModelSelect = useCallback((model, selectedThinkingLevel) => {
819
988
  const run = async () => {
989
+ const target = modelSwitchTarget(model, agent.providerId || safeRegistry.getDefault()?.id);
990
+ await safeRegistry.prepareProvider(target.providerId);
991
+ const targetProvider = safeRegistry.getConfigured().find((item) => item.id === target.providerId);
992
+ if (!targetProvider?.apiKey || !createProvider) {
993
+ throw new Error(`Provider ${target.providerId} is not configured or has no active credentials.`);
994
+ }
995
+ // Construct once as a side-effect-free preflight before stopping Grok.
996
+ // switchAgentModel constructs the committed instance after the fresh
997
+ // native session has been installed.
998
+ const preparedProvider = createProvider(target.providerId, targetProvider.apiKey, targetProvider.baseURL);
999
+ const targetSessionManager = await transitionToNative();
820
1000
  const nextThinkingLevel = await switchAgentModel({
821
1001
  model,
822
1002
  agent,
823
1003
  registry: safeRegistry,
824
1004
  createProvider,
1005
+ preparedProvider,
825
1006
  workingDir: args.cwd,
826
1007
  systemPromptOptions: agent.getSystemPromptToolOptions(),
827
1008
  thinkingLevel: selectedThinkingLevel,
828
1009
  rememberModel: (nextModel) => userConfig.pushRecentModel(nextModel),
829
1010
  setThinkingLevel,
830
- sessionManager,
1011
+ sessionManager: targetSessionManager,
831
1012
  });
832
- // MiniMax thinking is a binary toggle (adaptive thinking), not a graded
833
- // effort — show it as "thinking mode" rather than "medium effort".
834
- const isMiniMaxModel = model.toLowerCase().includes("minimax");
1013
+ // Binary thinking toggles and thinking-only models use internal level
1014
+ // placeholders; do not present those placeholders as real effort grades.
1015
+ const decodedModel = decodeModel(model);
1016
+ const switchedProviderId = decodedModel.providerId || agent.providerId || safeRegistry.getDefault()?.id || "openai";
1017
+ const isThinkingToggle = isThinkingToggleModel(switchedProviderId, decodedModel.modelId);
1018
+ const isThinkingOnly = isThinkingOnlyLevels(getAvailableThinkingLevels(switchedProviderId, decodedModel.modelId));
835
1019
  const effortNote = nextThinkingLevel && nextThinkingLevel !== "off"
836
- ? (isMiniMaxModel ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
1020
+ ? (isThinkingToggle || isThinkingOnly ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
837
1021
  : "";
838
1022
  addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}.`);
839
1023
  closePicker();
@@ -843,7 +1027,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
843
1027
  addMessage("error", formatModelSwitchError(model, error));
844
1028
  closePicker();
845
1029
  });
846
- }, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
1030
+ }, [agent, addMessage, closePicker, sessionManager, transitionToNative, userConfig, safeRegistry, createProvider]);
847
1031
  const handleThemeHighlight = useCallback((mode) => {
848
1032
  setThemeMode(mode);
849
1033
  }, []);
@@ -857,58 +1041,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
857
1041
  setThemeMode(themePickerRevertRef.current);
858
1042
  closePicker();
859
1043
  }, [closePicker]);
860
- const handleProviderSelect = useCallback((providerId) => {
861
- const run = async () => {
862
- await safeRegistry.prepareProvider(providerId);
863
- const configured = safeRegistry.getConfigured();
864
- const p = configured.find((x) => x.id === providerId);
865
- const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
866
- if (!p && !builtin) {
867
- addMessage("error", `Provider ${providerId} not found.`);
868
- closePicker();
869
- return;
870
- }
871
- if (!p?.apiKey) {
872
- if (!p && builtin) {
873
- safeRegistry.addProvider(providerId, "");
874
- }
875
- safeRegistry.setDefault(providerId);
876
- setKeyProviderId(providerId);
877
- setPickerMode("key");
878
- return;
879
- }
880
- safeRegistry.setDefault(providerId);
881
- agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
882
- agent.providerId = providerId;
883
- addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
884
- closePicker();
885
- };
886
- void run().catch((error) => {
887
- addMessage("error", `Failed to switch provider ${providerId}: ${errorMessage(error)}`);
888
- closePicker();
889
- });
890
- }, [addMessage, agent, closePicker, createProvider, safeRegistry]);
891
- const handleProviderAddSelect = useCallback((providerId) => {
892
- const ok = safeRegistry.addProvider(providerId, "");
893
- if (!ok) {
894
- addMessage("error", `Provider ${providerId} could not be added.`);
895
- closePicker();
896
- return;
897
- }
898
- safeRegistry.setDefault(providerId);
899
- setKeyProviderId(providerId);
900
- setPickerMode("key");
901
- }, [addMessage, closePicker, safeRegistry]);
902
- const handleLoginProviderSelect = useCallback(async (providerId) => {
1044
+ const executeProviderCommand = useCallback(async (command) => {
903
1045
  closePicker();
904
- const command = `/login ${providerId}`;
905
1046
  const { handled, result } = await slashRegistry.execute(command, {
906
1047
  agent,
907
1048
  addMessage,
908
1049
  clearMessages,
909
1050
  cwd: args.cwd,
910
- exit: () => { requestExit(); },
911
- sessionManager,
1051
+ exit: requestExit,
1052
+ sessionManager: sessionManagerRef.current,
912
1053
  createProvider: createProvider ?? (() => {
913
1054
  throw new Error("Provider creation not available");
914
1055
  }),
@@ -932,67 +1073,157 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
932
1073
  getResolvedTheme: () => themeResolved,
933
1074
  setThemeMode: applyThemeMode,
934
1075
  openStats: openStatsPanel,
1076
+ externalRuntime,
1077
+ startFreshSession,
1078
+ transitionToNative,
1079
+ onExternalRuntimeChange: refreshExternalRuntimeBinding,
935
1080
  });
936
- if (handled && result) {
1081
+ if (handled && result)
937
1082
  addMessage("assistant", result);
938
- }
939
- }, [agent, addMessage, clearMessages, closePicker, createProvider, exit, fillComposer, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, safeRegistry, sessionManager]);
940
- const handleLogoutProviderSelect = useCallback(async (providerId) => {
941
- closePicker();
942
- const command = `/logout ${providerId}`;
943
- const { handled, result } = await slashRegistry.execute(command, {
944
- agent,
945
- addMessage,
946
- clearMessages,
947
- cwd: args.cwd,
948
- exit: () => { requestExit(); },
949
- sessionManager,
950
- createProvider: createProvider ?? (() => {
1083
+ }, [
1084
+ addMessage,
1085
+ agent,
1086
+ applyThemeMode,
1087
+ args.cwd,
1088
+ bashAllowlist,
1089
+ clearMessages,
1090
+ closePicker,
1091
+ createProvider,
1092
+ externalRuntime,
1093
+ fillComposer,
1094
+ flushMemory,
1095
+ hookController,
1096
+ lspService,
1097
+ mcpManager,
1098
+ openFeedback,
1099
+ openPicker,
1100
+ openRewindPicker,
1101
+ openSessionPicker,
1102
+ openStatsPanel,
1103
+ refreshExternalRuntimeBinding,
1104
+ requestExit,
1105
+ runMemoryCompaction,
1106
+ runMemoryRefresh,
1107
+ runMemorySummary,
1108
+ safeRegistry,
1109
+ safeSkillRegistry,
1110
+ settingsManager,
1111
+ startFreshSession,
1112
+ themeMode,
1113
+ themeResolved,
1114
+ transitionToNative,
1115
+ ]);
1116
+ const handleProviderSelect = useCallback((providerId) => {
1117
+ const run = async () => {
1118
+ if (isGrokSubscriptionProviderId(providerId)) {
1119
+ if (grokSessionBound) {
1120
+ closePicker();
1121
+ return;
1122
+ }
1123
+ await executeProviderCommand(`/provider --set ${GROK_SUBSCRIPTION_PROVIDER_ID}`);
1124
+ return;
1125
+ }
1126
+ await safeRegistry.prepareProvider(providerId);
1127
+ const configured = safeRegistry.getConfigured();
1128
+ const p = configured.find((x) => x.id === providerId);
1129
+ const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
1130
+ if (!p && !builtin) {
1131
+ addMessage("error", `Provider ${providerId} not found.`);
1132
+ closePicker();
1133
+ return;
1134
+ }
1135
+ if (!p?.apiKey) {
1136
+ if (!p && builtin) {
1137
+ safeRegistry.addProvider(providerId, "");
1138
+ }
1139
+ setKeyProviderId(providerId);
1140
+ setPickerMode("key");
1141
+ return;
1142
+ }
1143
+ if (!createProvider)
951
1144
  throw new Error("Provider creation not available");
952
- }),
953
- openPicker,
954
- openSessionPicker,
955
- openRewindPicker,
956
- openFeedback,
957
- fillComposer,
958
- registry: safeRegistry,
959
- skillRegistry: safeSkillRegistry,
960
- bashAllowlist,
961
- settingsManager,
962
- lspService,
963
- mcpManager,
964
- hookController,
965
- flushMemory,
966
- runMemoryCompaction,
967
- runMemorySummary,
968
- runMemoryRefresh,
969
- getThemeMode: () => themeMode,
970
- getResolvedTheme: () => themeResolved,
971
- setThemeMode: applyThemeMode,
972
- openStats: openStatsPanel,
1145
+ // Construct the target before stopping a Grok sidecar. A bad provider
1146
+ // configuration must leave the current external session untouched.
1147
+ const nextProvider = createProvider(providerId, p.apiKey, p.baseURL);
1148
+ await transitionToNative();
1149
+ agent.setProvider(nextProvider);
1150
+ agent.providerId = providerId;
1151
+ try {
1152
+ safeRegistry.setDefault(providerId);
1153
+ }
1154
+ catch (error) {
1155
+ addMessage("error", `Switched providers, but could not remember the default: ${errorMessage(error)}`);
1156
+ }
1157
+ addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
1158
+ closePicker();
1159
+ };
1160
+ void run().catch((error) => {
1161
+ addMessage("error", `Failed to switch provider ${providerId}: ${errorMessage(error)}`);
1162
+ closePicker();
973
1163
  });
974
- if (handled && result) {
975
- addMessage("assistant", result);
976
- }
977
- }, [agent, addMessage, clearMessages, closePicker, createProvider, exit, fillComposer, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, safeRegistry, sessionManager]);
1164
+ }, [addMessage, agent, closePicker, createProvider, executeProviderCommand, grokSessionBound, safeRegistry, transitionToNative]);
1165
+ const handleProviderAddSelect = useCallback((providerId) => {
1166
+ const run = async () => {
1167
+ if (isGrokSubscriptionProviderId(providerId)) {
1168
+ await executeProviderCommand(`/provider --add ${GROK_SUBSCRIPTION_PROVIDER_ID}`);
1169
+ return;
1170
+ }
1171
+ const ok = safeRegistry.addProvider(providerId, "");
1172
+ if (!ok) {
1173
+ addMessage("error", `Provider ${providerId} could not be added.`);
1174
+ closePicker();
1175
+ return;
1176
+ }
1177
+ setKeyProviderId(providerId);
1178
+ setPickerMode("key");
1179
+ };
1180
+ void run().catch((error) => {
1181
+ addMessage("error", `Failed to add provider ${providerId}: ${errorMessage(error)}`);
1182
+ closePicker();
1183
+ });
1184
+ }, [addMessage, closePicker, executeProviderCommand, safeRegistry]);
1185
+ const handleLoginProviderSelect = useCallback((providerId) => {
1186
+ void executeProviderCommand(`/login ${providerId}`).catch((error) => {
1187
+ addMessage("error", `Failed to log in to ${providerId}: ${errorMessage(error)}`);
1188
+ });
1189
+ }, [addMessage, executeProviderCommand]);
1190
+ const handleLogoutProviderSelect = useCallback((providerId) => {
1191
+ void executeProviderCommand(`/logout ${providerId}`).catch((error) => {
1192
+ addMessage("error", `Failed to log out of ${providerId}: ${errorMessage(error)}`);
1193
+ });
1194
+ }, [addMessage, executeProviderCommand]);
978
1195
  const handleKeySubmit = useCallback((key) => {
979
- const targetId = keyProviderId || safeRegistry.getDefault()?.id;
980
- if (!targetId) {
981
- addMessage("error", "No provider selected.");
1196
+ const run = async () => {
1197
+ const targetId = keyProviderId || safeRegistry.getDefault()?.id;
1198
+ if (!targetId) {
1199
+ addMessage("error", "No provider selected.");
1200
+ closePicker();
1201
+ setKeyProviderId(null);
1202
+ return;
1203
+ }
1204
+ safeRegistry.updateProviderKey(targetId, key);
1205
+ const p = safeRegistry.getConfigured().find((x) => x.id === targetId);
1206
+ if (p && createProvider) {
1207
+ const nextProvider = createProvider(targetId, key, p.baseURL);
1208
+ await transitionToNative();
1209
+ agent.setProvider(nextProvider);
1210
+ agent.providerId = targetId;
1211
+ try {
1212
+ safeRegistry.setDefault(targetId);
1213
+ }
1214
+ catch (error) {
1215
+ addMessage("error", `API key saved, but could not remember the default: ${errorMessage(error)}`);
1216
+ }
1217
+ }
1218
+ addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
982
1219
  closePicker();
983
1220
  setKeyProviderId(null);
984
- return;
985
- }
986
- safeRegistry.updateProviderKey(targetId, key);
987
- const p = safeRegistry.getConfigured().find((x) => x.id === targetId);
988
- if (p && createProvider) {
989
- agent.setProvider(createProvider(targetId, key, p.baseURL));
990
- agent.providerId = targetId;
991
- }
992
- addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
993
- closePicker();
994
- setKeyProviderId(null);
995
- }, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry]);
1221
+ };
1222
+ void run().catch((error) => {
1223
+ addMessage("error", `Failed to update provider key: ${errorMessage(error)}`);
1224
+ closePicker();
1225
+ });
1226
+ }, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry, transitionToNative]);
996
1227
  const handleSubmit = useCallback(async (payload) => {
997
1228
  const initialPayload = typeof payload === "string" ? { text: payload, images: [] } : payload;
998
1229
  const input = initialPayload.text;
@@ -1008,6 +1239,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1008
1239
  requestExit();
1009
1240
  return;
1010
1241
  }
1242
+ if (externalSessionBound) {
1243
+ // External ACP turns do not support Bubble's boundary steer
1244
+ // protocol. Preserve ordering by sending every additional input to
1245
+ // the next turn instead.
1246
+ queueInput(initialPayload);
1247
+ return;
1248
+ }
1011
1249
  const steerEligible = !displayInput.trim().startsWith("/") &&
1012
1250
  !input.includes("@") &&
1013
1251
  images.length === 0;
@@ -1019,6 +1257,21 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1019
1257
  }
1020
1258
  return;
1021
1259
  }
1260
+ const grokInputDecision = externalSessionBound
1261
+ ? classifyGrokInput({ text: input, imageCount: images.length })
1262
+ : undefined;
1263
+ if (grokInputDecision?.kind === "blocked") {
1264
+ addMessage("error", grokInputDecision.message);
1265
+ return;
1266
+ }
1267
+ if (grokInputDecision?.kind === "prompt" && unsupportedExternalSessionBound) {
1268
+ addMessage("error", "This session has an unsupported external runtime binding. Native tools and providers are disabled; use /session, /provider, or /model to enter a fresh native session.");
1269
+ return;
1270
+ }
1271
+ if (grokInputDecision?.kind === "prompt" && grokSessionBound && !externalRuntime) {
1272
+ addMessage("error", "This session belongs to Grok Subscription, but its runtime is unavailable in this TUI. Use /model, /provider, or /session to enter a fresh native session.");
1273
+ return;
1274
+ }
1022
1275
  const submitFingerprint = submitPayloadFingerprint(initialPayload);
1023
1276
  const startingDecision = decideStartingSubmitFingerprint(startingSubmitFingerprintRef.current, submitFingerprint);
1024
1277
  if (startingDecision === "ignore")
@@ -1032,23 +1285,50 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1032
1285
  try {
1033
1286
  const runAgentInput = async (actualInput, displayInput, attachedImages = [], runOptions = {}) => {
1034
1287
  const runSessionFile = currentSessionFile();
1035
- const activeProviderId = agent.providerId || safeRegistry.getDefault()?.id;
1036
- const hasActiveProvider = !!activeProviderId && safeRegistry.getEnabled().some((provider) => provider.id === activeProviderId);
1037
- if (!hasActiveProvider) {
1038
- addMessage("error", "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.");
1039
- if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1040
- goalStore.pause();
1041
- addMessage("assistant", stopReasonNotice("error"));
1288
+ const runSessionManager = sessionManagerRef.current;
1289
+ const runExternalRuntime = isGrokSubscriptionProviderId(externalRuntimeBindingRef.current?.id);
1290
+ const externalSessionId = runExternalRuntime
1291
+ ? externalRuntimeBindingRef.current?.sessionId
1292
+ : undefined;
1293
+ const externalModelId = runExternalRuntime
1294
+ ? externalRuntimeBindingRef.current?.modelId ?? "subscription-default"
1295
+ : undefined;
1296
+ const externalGeneration = runExternalRuntime
1297
+ ? ++externalRuntimeGenerationRef.current
1298
+ : undefined;
1299
+ const isCurrentExternalRun = () => !runExternalRuntime || (externalRuntimeGenerationRef.current === externalGeneration
1300
+ && currentSessionFile() === runSessionFile
1301
+ && isGrokSubscriptionProviderId(externalRuntimeBindingRef.current?.id)
1302
+ && externalRuntimeBindingRef.current?.sessionId === externalSessionId);
1303
+ if (runExternalRuntime) {
1304
+ if (!externalRuntime || typeof actualInput !== "string" || !externalSessionId || !runSessionManager) {
1305
+ addMessage("error", "Grok subscription session binding is unavailable. Start a fresh session with /login grok.");
1306
+ return;
1307
+ }
1308
+ const boundModelId = externalRuntimeBindingRef.current?.modelId;
1309
+ if (boundModelId) {
1310
+ await externalRuntime.hydrateSession(externalSessionId, boundModelId, externalRuntimeBindingRef.current?.reasoningEffort);
1042
1311
  }
1043
- return;
1044
1312
  }
1045
- if (!agent.model) {
1046
- addMessage("error", "No model selected. Use /model after /login or provider setup.");
1047
- if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1048
- goalStore.pause();
1049
- addMessage("assistant", stopReasonNotice("error"));
1313
+ else {
1314
+ const activeProviderId = agent.providerId || safeRegistry.getDefault()?.id;
1315
+ const hasActiveProvider = !!activeProviderId && safeRegistry.getEnabled().some((provider) => provider.id === activeProviderId);
1316
+ if (!hasActiveProvider) {
1317
+ addMessage("error", "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.");
1318
+ if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1319
+ goalStore.pause();
1320
+ addMessage("assistant", stopReasonNotice("error"));
1321
+ }
1322
+ return;
1323
+ }
1324
+ if (!agent.model) {
1325
+ addMessage("error", "No model selected. Use /model after /login or provider setup.");
1326
+ if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1327
+ goalStore.pause();
1328
+ addMessage("assistant", stopReasonNotice("error"));
1329
+ }
1330
+ return;
1050
1331
  }
1051
- return;
1052
1332
  }
1053
1333
  const displayContent = formatImageUserDisplayText(displayInput, attachedImages.length, runOptions.imageDisplayStart);
1054
1334
  if (!runOptions.hidden) {
@@ -1059,6 +1339,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1059
1339
  // The new user row commits to native scrollback; the terminal keeps
1060
1340
  // the prompt in view, so there is no app-side "snap to bottom" to do.
1061
1341
  }
1342
+ if (runExternalRuntime) {
1343
+ runSessionManager.appendMessage({ role: "user", content: actualInput });
1344
+ }
1062
1345
  setIsRunning(true);
1063
1346
  runStartRef.current = Date.now();
1064
1347
  setStreamingContent("");
@@ -1073,6 +1356,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1073
1356
  let runErrored = false;
1074
1357
  const toolCalls = [];
1075
1358
  const assistantParts = [];
1359
+ // Entries whose members all reached a final status are now covered by
1360
+ // the settled transcript; drop them so the accumulator stays bounded
1361
+ // across a long session. Running entries survive — for a workflow
1362
+ // spanning turns they are the only live view of its members.
1363
+ if (pruneSettledLiveSubagentTools(liveSubagentToolsRef.current)) {
1364
+ setLiveSubagentVersion((version) => version + 1);
1365
+ }
1076
1366
  const abortController = new AbortController();
1077
1367
  activeAbortRef.current = abortController;
1078
1368
  setStartingSubmit(null);
@@ -1106,7 +1396,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1106
1396
  toolCalls.length > 0 ||
1107
1397
  assistantParts.length > 0);
1108
1398
  const commitAssistantMessage = (taskElapsedMs) => {
1109
- if (!hasAssistantOutput())
1399
+ if (!hasAssistantOutput() || !isCurrentExternalRun())
1110
1400
  return;
1111
1401
  const currentParts = snapshotDisplayParts(assistantParts);
1112
1402
  const currentToolCalls = [...toolCalls];
@@ -1132,6 +1422,16 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1132
1422
  msg.taskElapsedMs = taskElapsedMs;
1133
1423
  }
1134
1424
  updateDisplayMessages((prev) => [...prev, msg]);
1425
+ if (runExternalRuntime) {
1426
+ runSessionManager.appendMessage({
1427
+ role: "assistant",
1428
+ content: partContent,
1429
+ ...(assistantReasoning ? { reasoning: assistantReasoning } : {}),
1430
+ providerId: "grok",
1431
+ model: "grok-subscription",
1432
+ modelId: externalModelId,
1433
+ });
1434
+ }
1135
1435
  };
1136
1436
  const clearAssistantStream = () => {
1137
1437
  // A timer firing after this reset would resurrect the just-committed
@@ -1147,10 +1447,33 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1147
1447
  assistantParts.length = 0;
1148
1448
  };
1149
1449
  try {
1150
- for await (const event of agent.run(actualInput, args.cwd, {
1151
- abortSignal: abortController.signal,
1152
- inputController,
1153
- })) {
1450
+ const eventStream = runExternalRuntime
1451
+ ? externalRuntime.run(actualInput, {
1452
+ sessionId: externalSessionId,
1453
+ signal: abortController.signal,
1454
+ generation: externalGeneration,
1455
+ })
1456
+ : agent.run(actualInput, args.cwd, {
1457
+ abortSignal: abortController.signal,
1458
+ inputController,
1459
+ });
1460
+ for await (const event of eventStream) {
1461
+ if (runExternalRuntime) {
1462
+ if (!isCurrentExternalRun()) {
1463
+ abortController.abort(new AgentAbortError("Stale Grok runtime generation."));
1464
+ break;
1465
+ }
1466
+ if (event.type !== "turn_start"
1467
+ && event.type !== "text_delta"
1468
+ && event.type !== "reasoning_delta"
1469
+ && event.type !== "tool_start"
1470
+ && event.type !== "tool_update"
1471
+ && event.type !== "tool_end"
1472
+ && event.type !== "turn_end") {
1473
+ await externalRuntime.cancel(externalSessionId).catch(() => undefined);
1474
+ throw new GrokRuntimeError("policy_violation", "Grok emitted an unsupported external runtime event.");
1475
+ }
1476
+ }
1154
1477
  switch (event.type) {
1155
1478
  case "turn_start":
1156
1479
  // A fresh provider call is starting. Everything worth keeping
@@ -1257,6 +1580,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1257
1580
  setStreamingTools([...toolCalls]);
1258
1581
  syncStreamingParts();
1259
1582
  }
1583
+ else if (accumulateLiveSubagentUpdate(liveSubagentToolsRef.current, {
1584
+ id: event.id,
1585
+ name: event.name,
1586
+ metadata: event.update.metadata,
1587
+ })) {
1588
+ // The launching call already settled out of this round's
1589
+ // toolCalls; absorbed into the live accumulator instead.
1590
+ setLiveSubagentVersion((version) => version + 1);
1591
+ }
1260
1592
  break;
1261
1593
  }
1262
1594
  case "todos_updated": {
@@ -1341,15 +1673,23 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1341
1673
  }
1342
1674
  }
1343
1675
  catch (err) {
1344
- commitAssistantMessage();
1345
- if (err instanceof AgentAbortError || err?.name === "AbortError") {
1676
+ const staleExternalRun = runExternalRuntime && !isCurrentExternalRun();
1677
+ if (!staleExternalRun)
1678
+ commitAssistantMessage();
1679
+ const cancelledError = err instanceof AgentAbortError
1680
+ || err?.name === "AbortError"
1681
+ || (err instanceof GrokRuntimeError && err.code === "cancelled");
1682
+ if (staleExternalRun) {
1683
+ runCancelled = true;
1684
+ }
1685
+ else if (cancelledError) {
1346
1686
  runCancelled = true;
1347
1687
  // commitAssistantMessage already appended the partial answer; the
1348
1688
  // interrupt is otherwise a pure append (the partial + a "Interrupted"
1349
1689
  // row). Off a multiplexer, append just the interrupt row so settled
1350
1690
  // history is never reprinted — no flash. Under tmux/screen, fall back
1351
1691
  // to the full reprint that rebuilds from the canonical agent.messages.
1352
- if (isMultiplexedTerminal()) {
1692
+ if (!runExternalRuntime && isMultiplexedTerminal()) {
1353
1693
  resetTranscript(() => reconstructDisplayMessages(agent.messages));
1354
1694
  }
1355
1695
  else {
@@ -1367,7 +1707,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1367
1707
  runErrored = true;
1368
1708
  updateDisplayMessages((prev) => [
1369
1709
  ...prev,
1370
- withMessageKey({ role: "error", content: err.message }),
1710
+ withMessageKey({ role: "error", content: errorMessage(err) }),
1371
1711
  ]);
1372
1712
  }
1373
1713
  }
@@ -1379,6 +1719,23 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1379
1719
  const cancelled = abortController.signal.aborted;
1380
1720
  if (cancelled)
1381
1721
  runCancelled = true;
1722
+ if (runExternalRuntime && runCancelled) {
1723
+ // External ACP owns its own conversation state, so Bubble persists
1724
+ // an explicit boundary for its local transcript. Without this,
1725
+ // a partial response looks successfully completed after resume.
1726
+ runSessionManager.appendMessage({
1727
+ role: "assistant",
1728
+ content: INTERRUPTED_ASSISTANT_CONTENT,
1729
+ error: {
1730
+ name: "MessageAbortedError",
1731
+ message: "Assistant response was interrupted by the user.",
1732
+ aborted: true,
1733
+ },
1734
+ providerId: "grok",
1735
+ model: "grok-subscription",
1736
+ modelId: externalModelId,
1737
+ });
1738
+ }
1382
1739
  for (const leftover of inputController.clear()) {
1383
1740
  const steer = pendingSteersRef.current.get(leftover.id);
1384
1741
  pendingSteersRef.current.delete(leftover.id);
@@ -1397,25 +1754,32 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1397
1754
  sessionFile: steer?.sessionFile ?? runSessionFile,
1398
1755
  });
1399
1756
  }
1400
- setPendingSteerCount(0);
1401
- setQueuedCount(queuedInputsRef.current.length);
1757
+ const ownsCurrentUiGeneration = !runExternalRuntime || isCurrentExternalRun();
1758
+ if (ownsCurrentUiGeneration) {
1759
+ setPendingSteerCount(0);
1760
+ setQueuedCount(queuedInputsRef.current.length);
1761
+ }
1402
1762
  if (inputControllerRef.current === inputController)
1403
1763
  inputControllerRef.current = null;
1404
1764
  if (activeAbortRef.current === abortController)
1405
1765
  activeAbortRef.current = null;
1406
- setIsRunning(false);
1407
- runStartRef.current = null;
1408
- setStreamingContent("");
1409
- setStreamingReasoning("");
1410
- setStreamingTools([]);
1411
- setStreamingParts([]);
1412
- maybeContinueGoal({
1413
- runCancelled,
1414
- runErrored,
1415
- isGoalRun: !!runOptions.goalRun,
1416
- runTokens: goalRunTokens,
1417
- usageReported: goalRunUsageReported,
1418
- });
1766
+ if (ownsCurrentUiGeneration) {
1767
+ setIsRunning(false);
1768
+ runStartRef.current = null;
1769
+ setStreamingContent("");
1770
+ setStreamingReasoning("");
1771
+ setStreamingTools([]);
1772
+ setStreamingParts([]);
1773
+ }
1774
+ if (!runExternalRuntime) {
1775
+ maybeContinueGoal({
1776
+ runCancelled,
1777
+ runErrored,
1778
+ isGoalRun: !!runOptions.goalRun,
1779
+ runTokens: goalRunTokens,
1780
+ usageReported: goalRunUsageReported,
1781
+ });
1782
+ }
1419
1783
  }
1420
1784
  };
1421
1785
  const kickGoalTurn = (prompt, visibleInput) => {
@@ -1542,7 +1906,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1542
1906
  };
1543
1907
  // Slash commands and skill invocations drop any attached images —
1544
1908
  // they're meant for pure command routing.
1545
- if (displayInput.startsWith("/")) {
1909
+ if (input.trimStart().startsWith("/")) {
1546
1910
  // Fast-path `/quit` and `/exit` before slash-registry / skill
1547
1911
  // resolution. This guarantees a literal "/quit" always exits even if
1548
1912
  // a skill or alias of the same name is later registered. The
@@ -1553,14 +1917,16 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1553
1917
  requestExit();
1554
1918
  return;
1555
1919
  }
1556
- if (/^\/goal(?:\s|$)/.test(input.trim())) {
1557
- await handleGoalCommand(input);
1558
- return;
1559
- }
1560
- const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
1561
- if (skillInvocation) {
1562
- await runAgentInput(skillInvocation.actualPrompt, displayInput);
1563
- return;
1920
+ if (!externalSessionBound) {
1921
+ if (/^\/goal(?:\s|$)/.test(input.trim())) {
1922
+ await handleGoalCommand(input);
1923
+ return;
1924
+ }
1925
+ const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
1926
+ if (skillInvocation) {
1927
+ await runAgentInput(skillInvocation.actualPrompt, displayInput);
1928
+ return;
1929
+ }
1564
1930
  }
1565
1931
  const { handled, result, inject } = await slashRegistry.execute(input, {
1566
1932
  agent,
@@ -1593,6 +1959,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1593
1959
  setThemeMode: applyThemeMode,
1594
1960
  openStats: openStatsPanel,
1595
1961
  compactionProgress: setCompaction,
1962
+ externalRuntime,
1963
+ startFreshSession,
1964
+ transitionToNative,
1965
+ onExternalRuntimeChange: refreshExternalRuntimeBinding,
1596
1966
  });
1597
1967
  if (handled) {
1598
1968
  if (agent.mode !== permissionMode) {
@@ -1616,7 +1986,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1616
1986
  }
1617
1987
  else if (result.startsWith("⏪")) {
1618
1988
  // /rewind truncated agent.messages — rebuild the transcript from
1619
- // the rewound state before appending the summary.
1989
+ // the rewound state before appending the summary. Subagents
1990
+ // rewound out of history must not linger in the live accumulator.
1991
+ clearLiveSubagentTools();
1620
1992
  resetTranscript(() => [
1621
1993
  ...reconstructDisplayMessages(agent.messages),
1622
1994
  { role: "assistant", content: result },
@@ -1631,6 +2003,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1631
2003
  }
1632
2004
  return;
1633
2005
  }
2006
+ if (externalSessionBound) {
2007
+ addMessage("error", "That command is unavailable in this external runtime session.");
2008
+ return;
2009
+ }
2010
+ }
2011
+ if (grokSessionBound) {
2012
+ await runAgentInput(input, displayInput);
2013
+ return;
1634
2014
  }
1635
2015
  const expansion = await expandAtMentions(input, args.cwd);
1636
2016
  if (expansion.missing.length > 0) {
@@ -1656,7 +2036,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1656
2036
  setStartingSubmit(null);
1657
2037
  }
1658
2038
  }
1659
- }, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, fillComposer, prepareSubmitDisplay, safeRegistry, safeSkillRegistry, updateDisplayMessages, queueInput, submitSteer, requestExit, setStartingSubmit]);
2039
+ }, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, externalRuntime, externalRuntimeBinding, externalSessionBound, fillComposer, grokSessionBound, prepareSubmitDisplay, refreshExternalRuntimeBinding, safeRegistry, safeSkillRegistry, startFreshSession, transitionToNative, unsupportedExternalSessionBound, updateDisplayMessages, queueInput, submitSteer, requestExit, setStartingSubmit]);
1660
2040
  // Drain the queue once the run ends and no modal needs the user first.
1661
2041
  // The placeholder row is removed right before resubmitting — handleSubmit
1662
2042
  // renders the message again as a regular user row.
@@ -1688,7 +2068,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1688
2068
  const timer = setTimeout(drainQueuedInput, 0);
1689
2069
  return () => clearTimeout(timer);
1690
2070
  }, [isRunning, queuedCount, startingSubmitFingerprint, pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, statsPanel, drainQueuedInput]);
1691
- const currentProviderId = agent.providerId || safeRegistry.getDefault()?.id;
2071
+ const currentProviderId = externalSessionBound
2072
+ ? (grokSessionBound ? GROK_SUBSCRIPTION_PROVIDER_ID : undefined)
2073
+ : agent.providerId || safeRegistry.getDefault()?.id;
1692
2074
  const keyTarget = keyProviderId
1693
2075
  ? safeRegistry.getConfigured().find((p) => p.id === keyProviderId)
1694
2076
  : safeRegistry.getDefault();
@@ -1707,14 +2089,22 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1707
2089
  return null;
1708
2090
  })()
1709
2091
  : null;
1710
- // MiniMax has only off/on, so the graded ">2 levels" gate would hide its label;
2092
+ // Binary thinking toggles have only off/on, so the graded ">2 levels" gate would hide their label;
1711
2093
  // surface it too (rendered as "thinking mode" by formatModelLine).
1712
- const isMiniMaxProvider = (agent.providerId || "").toLowerCase().includes("minimax");
2094
+ const isThinkingToggle = isThinkingToggleModel(agent.providerId, agent.apiModel);
2095
+ const availableThinkingLevels = getAvailableThinkingLevels(agent.providerId, agent.apiModel);
2096
+ const isThinkingOnly = isThinkingOnlyLevels(availableThinkingLevels);
1713
2097
  const showThinkingLabel = Boolean(thinkingLevel)
1714
2098
  && thinkingLevel !== "off"
1715
- && (isMiniMaxProvider || getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2);
1716
- const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry), updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), sessionLabel: sessionBasename(currentSessionFile()), providerId: agent.providerId || safeRegistry.getDefault()?.id, modelLabel: agent.model ? displayModel(agent.model) : undefined, thinkingLabel: showThinkingLabel ? thinkingLevel : undefined })) : null;
1717
- const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry), [safeSkillRegistry]);
2099
+ && (isThinkingToggle || isThinkingOnly || availableThinkingLevels.length > 2);
2100
+ const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry, grokSessionBound), updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), sessionLabel: sessionBasename(currentSessionFile()), providerId: grokSessionBound ? "grok" : externalSessionBound ? undefined : agent.providerId || safeRegistry.getDefault()?.id, modelId: grokSessionBound ? externalRuntimeBinding?.modelId : externalSessionBound ? undefined : agent.apiModel, modelLabel: externalSessionBound
2101
+ ? (grokSessionBound
2102
+ ? `Grok Subscription${externalRuntimeBinding?.modelId ? ` · ${externalRuntimeBinding.modelId}` : ""}`
2103
+ : "Unsupported external runtime · recovery-only mode")
2104
+ : agent.model ? displayModel(agent.model) : undefined, thinkingLabel: grokSessionBound
2105
+ ? externalRuntimeBinding?.reasoningEffort
2106
+ : !externalSessionBound && showThinkingLabel ? thinkingLevel : undefined })) : null;
2107
+ const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry, externalSessionBound), [externalSessionBound, safeSkillRegistry]);
1718
2108
  const mcpReconnectItems = useMemo(() => buildMcpReconnectItems(mcpManager), [mcpManager]);
1719
2109
  // No fixed-height frame: settled rows flow into the terminal's native
1720
2110
  // scrollback via <Static>, and only the dynamic bottom stack (streaming
@@ -1722,7 +2112,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1722
2112
  // to its content keeps the composer pinned just below the latest output the
1723
2113
  // way ordinary shell programs do.
1724
2114
  const mainWidth = Math.max(40, terminalColumns);
1725
- return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", width: mainWidth, backgroundColor: palette.background, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode, staticGeneration: staticGeneration, paddingX: 1, maxStreamRows: Math.max(6, terminalRows - 10) }), pickerMode === "model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ModelPicker, { registry: safeRegistry, current: agent.model, currentThinkingLevel: thinkingLevel, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker }) })), pickerMode === "provider" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
2115
+ return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", width: mainWidth, backgroundColor: palette.background, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode, staticGeneration: staticGeneration, paddingX: 1, maxStreamRows: Math.max(6, terminalRows - 10) }), pickerMode === "grok-model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: grokModels.length === 0 ? (_jsx(Text, { children: "Loading Grok subscription models\u2026" })) : (_jsx(ProviderPicker, { title: "Select Grok Model and Reasoning", providers: grokModels.flatMap((model) => model.reasoningLevels.map((effort) => ({
2116
+ id: JSON.stringify({ modelId: model.id, reasoningEffort: effort }),
2117
+ name: `${model.name}${effort === "off" ? "" : ` · ${effort}`}`,
2118
+ enabled: true,
2119
+ }))), current: JSON.stringify({
2120
+ modelId: externalRuntimeBinding?.modelId,
2121
+ reasoningEffort: externalRuntimeBinding?.reasoningEffort ?? "high",
2122
+ }), onSelect: handleGrokModelSelect, onCancel: closePicker })) })), pickerMode === "model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ModelPicker, { registry: safeRegistry, current: agent.model, currentThinkingLevel: thinkingLevel, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker }) })), pickerMode === "provider" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: withGrokSubscriptionProvider(BUILTIN_PROVIDERS
1726
2123
  .filter((p) => isUserVisibleProvider(p.id))
1727
2124
  .map((p) => {
1728
2125
  const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
@@ -1732,13 +2129,21 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1732
2129
  name: `${p.name} [${configuredLabel}]`,
1733
2130
  enabled: true,
1734
2131
  };
1735
- }), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
2132
+ })), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: withGrokSubscriptionProvider(BUILTIN_PROVIDERS
1736
2133
  .filter((p) => isUserVisibleProvider(p.id))
1737
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" }) })), pickerMode === "login" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
2134
+ .map((p) => ({ id: p.id, name: p.name, enabled: true }))), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" }) })), pickerMode === "login" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers:
2135
+ // The native grok provider is OAuth-capable, so it appears via
2136
+ // supportsOAuth like OpenAI — no extra hardcoded row.
2137
+ BUILTIN_PROVIDERS
1738
2138
  .filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
1739
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" }) })), pickerMode === "logout" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
1740
- .filter((p) => safeRegistry.getAuthStorage().has(p.id))
1741
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" }) })), pickerMode === "key" && keyTarget && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
2139
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" }) })), pickerMode === "logout" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: [
2140
+ ...safeRegistry.getConfigured()
2141
+ .filter((p) => safeRegistry.getAuthStorage().has(p.id))
2142
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })),
2143
+ ...(grokSessionBound
2144
+ ? [{ id: GROK_SUBSCRIPTION_PROVIDER_ID, name: "Grok Subscription [local login]", enabled: true }]
2145
+ : []),
2146
+ ], current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" }) })), pickerMode === "key" && keyTarget && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
1742
2147
  closePicker();
1743
2148
  setKeyProviderId(null);
1744
2149
  } }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
@@ -1795,27 +2200,49 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1795
2200
  else if (result.kind === "error") {
1796
2201
  addMessage("error", `Feedback failed: ${result.message}`);
1797
2202
  }
1798
- } }) })), !isExiting && compaction && (_jsx(Box, { flexShrink: 0, backgroundColor: palette.background, children: _jsx(CompactionProgressCard, { progress: compaction }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
2203
+ } }) })), !isExiting && compaction && (_jsx(Box, { flexShrink: 0, backgroundColor: palette.background, children: _jsx(CompactionProgressCard, { progress: compaction }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (
2204
+ // The streaming tail supplies its own bottom margin once it has
2205
+ // visible output, but right after a prompt is sent the tail renders
2206
+ // nothing and the spinner would hug the user's message bubble — give
2207
+ // it a top gap only in that empty-tail state to avoid double spacing.
2208
+ _jsx(Box, { paddingX: 1, paddingBottom: 1, paddingTop: streamingParts.length === 0
2209
+ && streamingContent.length === 0
2210
+ && streamingTools.length === 0
2211
+ && (streamingReasoning.length === 0 || !(showThinking || verboseTrace))
2212
+ ? 1
2213
+ : 0, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
1799
2214
  if (subagentMembers.length > 0 && !pickerMode)
1800
2215
  setSubagentEntryFocused(true);
1801
- }, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, localSlashCommands: [...INK_LOCAL_SLASH_COMMANDS], terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, backgroundColor: palette.background, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({ mode: permissionMode, goalLine }) }) }))] }) }));
2216
+ }, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: externalSessionBound ? undefined : safeSkillRegistry, localSlashCommands: externalSessionBound ? [] : [...INK_LOCAL_SLASH_COMMANDS], allowedSlashCommands: unsupportedExternalSessionBound ? GROK_LOCAL_SLASH_COMMANDS : undefined, allowWorkspaceMentions: !unsupportedExternalSessionBound, allowImageAttachments: !externalSessionBound, terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, backgroundColor: palette.background, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
2217
+ mode: permissionMode,
2218
+ goalLine: externalSessionBound ? undefined : goalLine,
2219
+ runtimeLabel: externalSessionBound
2220
+ ? (grokSessionBound
2221
+ ? `Grok Subscription${externalRuntimeBinding?.modelId ? ` · ${externalRuntimeBinding.modelId}` : ""}${externalRuntimeBinding?.reasoningEffort && externalRuntimeBinding.reasoningEffort !== "off" ? ` · ${externalRuntimeBinding.reasoningEffort}` : ""} · workspace`
2222
+ : "Unsupported external runtime · recovery-only")
2223
+ : undefined,
2224
+ }) }) }))] }) }));
1802
2225
  }
1803
- function buildCommandPaletteItems(skillRegistry) {
2226
+ function buildCommandPaletteItems(skillRegistry, grokSessionBound = false) {
1804
2227
  const items = new Map();
1805
2228
  const add = (item) => {
1806
2229
  const key = `${item.action ?? "command"}:${item.value}`;
1807
2230
  if (!items.has(key))
1808
2231
  items.set(key, item);
1809
2232
  };
1810
- for (const command of INK_LOCAL_SLASH_COMMANDS) {
1811
- add({
1812
- label: `/${command.name}`,
1813
- detail: command.description,
1814
- value: command.name,
1815
- command: `/${command.name}`,
1816
- });
2233
+ if (!grokSessionBound) {
2234
+ for (const command of INK_LOCAL_SLASH_COMMANDS) {
2235
+ add({
2236
+ label: `/${command.name}`,
2237
+ detail: command.description,
2238
+ value: command.name,
2239
+ command: `/${command.name}`,
2240
+ });
2241
+ }
1817
2242
  }
1818
2243
  for (const command of slashRegistry.list()) {
2244
+ if (grokSessionBound && !isGrokLocalSlashCommand(command.name))
2245
+ continue;
1819
2246
  const source = command.source === "mcp" ? " :mcp" : "";
1820
2247
  const sourceLabel = command.sourceLabel ? `[${command.sourceLabel}] ` : "";
1821
2248
  add({
@@ -1825,14 +2252,16 @@ function buildCommandPaletteItems(skillRegistry) {
1825
2252
  command: `/${command.name}`,
1826
2253
  });
1827
2254
  }
1828
- for (const skill of skillRegistry.summaries()) {
1829
- add({
1830
- label: `/${skill.name} :skill`,
1831
- detail: `[${skill.source}] ${skill.description}`,
1832
- value: skill.name,
1833
- command: `/${skill.name}`,
1834
- action: "insert-skill",
1835
- });
2255
+ if (!grokSessionBound) {
2256
+ for (const skill of skillRegistry.summaries()) {
2257
+ add({
2258
+ label: `/${skill.name} :skill`,
2259
+ detail: `[${skill.source}] ${skill.description}`,
2260
+ value: skill.name,
2261
+ command: `/${skill.name}`,
2262
+ action: "insert-skill",
2263
+ });
2264
+ }
1836
2265
  }
1837
2266
  return [...items.values()];
1838
2267
  }