@bubblebrain-ai/bubble 0.0.35 → 0.0.37

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 (144) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/categories.d.ts +66 -2
  3. package/dist/agent/categories.js +120 -19
  4. package/dist/agent/discovery-barrier.js +1 -1
  5. package/dist/agent/profiles.d.ts +1 -0
  6. package/dist/agent/profiles.js +25 -11
  7. package/dist/agent/routing-catalog.d.ts +113 -0
  8. package/dist/agent/routing-catalog.js +240 -0
  9. package/dist/agent/workflow/control.d.ts +14 -0
  10. package/dist/agent/workflow/control.js +30 -9
  11. package/dist/agent.d.ts +75 -5
  12. package/dist/agent.js +313 -61
  13. package/dist/approval/controller.js +28 -0
  14. package/dist/approval/tool-helper.js +2 -0
  15. package/dist/approval/types.d.ts +21 -1
  16. package/dist/cli.d.ts +1 -1
  17. package/dist/cli.js +2 -1
  18. package/dist/config.d.ts +4 -0
  19. package/dist/config.js +4 -0
  20. package/dist/context/compact-llm.d.ts +2 -2
  21. package/dist/external-runtime/grok-acp.d.ts +56 -0
  22. package/dist/external-runtime/grok-acp.js +386 -0
  23. package/dist/external-runtime/grok-binary.d.ts +25 -0
  24. package/dist/external-runtime/grok-binary.js +108 -0
  25. package/dist/external-runtime/grok-errors.d.ts +7 -0
  26. package/dist/external-runtime/grok-errors.js +15 -0
  27. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  28. package/dist/external-runtime/grok-input-policy.js +52 -0
  29. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  30. package/dist/external-runtime/grok-ndjson.js +193 -0
  31. package/dist/external-runtime/grok-network.d.ts +15 -0
  32. package/dist/external-runtime/grok-network.js +81 -0
  33. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  34. package/dist/external-runtime/grok-preflight.js +194 -0
  35. package/dist/external-runtime/grok-process.d.ts +26 -0
  36. package/dist/external-runtime/grok-process.js +184 -0
  37. package/dist/external-runtime/grok-profile.d.ts +32 -0
  38. package/dist/external-runtime/grok-profile.js +419 -0
  39. package/dist/external-runtime/grok-provider.d.ts +31 -0
  40. package/dist/external-runtime/grok-provider.js +41 -0
  41. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  42. package/dist/external-runtime/grok-runtime.js +882 -0
  43. package/dist/external-runtime/index.d.ts +13 -0
  44. package/dist/external-runtime/index.js +13 -0
  45. package/dist/external-runtime/session-policy.d.ts +11 -0
  46. package/dist/external-runtime/session-policy.js +30 -0
  47. package/dist/external-runtime/types.d.ts +65 -0
  48. package/dist/external-runtime/types.js +1 -0
  49. package/dist/feishu/agent-host/approval-card.js +21 -0
  50. package/dist/feishu/agent-host/run-driver.js +19 -3
  51. package/dist/feishu/serve.js +1 -0
  52. package/dist/goal/tools.d.ts +6 -3
  53. package/dist/goal/tools.js +7 -19
  54. package/dist/main.js +105 -27
  55. package/dist/memory/db.d.ts +2 -0
  56. package/dist/memory/db.js +10 -0
  57. package/dist/memory/index.d.ts +1 -0
  58. package/dist/memory/index.js +1 -0
  59. package/dist/memory/phase1.js +14 -2
  60. package/dist/memory/phase2.js +21 -2
  61. package/dist/memory/prompts.js +1 -1
  62. package/dist/memory/session-policy.d.ts +25 -0
  63. package/dist/memory/session-policy.js +55 -0
  64. package/dist/memory/storage.d.ts +1 -0
  65. package/dist/memory/storage.js +13 -0
  66. package/dist/model-catalog.d.ts +18 -0
  67. package/dist/model-catalog.js +87 -41
  68. package/dist/model-config.d.ts +3 -1
  69. package/dist/model-config.js +11 -0
  70. package/dist/oauth/grok.d.ts +25 -0
  71. package/dist/oauth/grok.js +218 -0
  72. package/dist/oauth/index.d.ts +1 -0
  73. package/dist/oauth/index.js +1 -0
  74. package/dist/oauth/storage.d.ts +9 -0
  75. package/dist/oauth/storage.js +25 -0
  76. package/dist/orchestrator/default-hooks.js +17 -7
  77. package/dist/prompt/compose.d.ts +3 -0
  78. package/dist/prompt/compose.js +3 -0
  79. package/dist/prompt/delegation.js +22 -3
  80. package/dist/prompt/environment.js +0 -4
  81. package/dist/prompt/reminders.d.ts +0 -1
  82. package/dist/prompt/reminders.js +1 -11
  83. package/dist/prompt/routing.d.ts +14 -0
  84. package/dist/prompt/routing.js +71 -0
  85. package/dist/prompt/runtime.js +1 -1
  86. package/dist/prompt/task-reminders.d.ts +19 -0
  87. package/dist/prompt/task-reminders.js +49 -0
  88. package/dist/provider-ai-sdk.js +2 -2
  89. package/dist/provider-grok.d.ts +26 -0
  90. package/dist/provider-grok.js +59 -0
  91. package/dist/provider-openai-codex.d.ts +17 -1
  92. package/dist/provider-openai-codex.js +101 -29
  93. package/dist/provider-registry.d.ts +72 -0
  94. package/dist/provider-registry.js +336 -59
  95. package/dist/provider-transform.d.ts +1 -1
  96. package/dist/provider-transform.js +28 -5
  97. package/dist/provider.d.ts +3 -0
  98. package/dist/provider.js +14 -0
  99. package/dist/session-types.d.ts +19 -1
  100. package/dist/session.d.ts +1 -0
  101. package/dist/session.js +4 -0
  102. package/dist/slash-commands/commands.js +224 -14
  103. package/dist/slash-commands/types.d.ts +14 -1
  104. package/dist/system-prompt.d.ts +2 -0
  105. package/dist/tools/agent-lifecycle.js +9 -11
  106. package/dist/tools/bash.js +1 -1
  107. package/dist/tools/child-tools.js +2 -0
  108. package/dist/tools/index.d.ts +2 -2
  109. package/dist/tools/index.js +7 -5
  110. package/dist/tools/lsp.js +1 -0
  111. package/dist/tools/memory.d.ts +7 -2
  112. package/dist/tools/memory.js +28 -43
  113. package/dist/tools/server.js +4 -0
  114. package/dist/tools/tool-search.d.ts +1 -1
  115. package/dist/tools/tool-search.js +23 -3
  116. package/dist/tui/model-switch.d.ts +7 -0
  117. package/dist/tui/model-switch.js +15 -5
  118. package/dist/tui-ink/app.d.ts +3 -1
  119. package/dist/tui-ink/app.js +649 -233
  120. package/dist/tui-ink/approval/approval-dialog.js +36 -0
  121. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  122. package/dist/tui-ink/composer-buffer.js +317 -0
  123. package/dist/tui-ink/footer.d.ts +1 -0
  124. package/dist/tui-ink/footer.js +7 -2
  125. package/dist/tui-ink/input-box.d.ts +35 -1
  126. package/dist/tui-ink/input-box.js +215 -122
  127. package/dist/tui-ink/message-list.js +5 -5
  128. package/dist/tui-ink/model-picker.d.ts +61 -4
  129. package/dist/tui-ink/model-picker.js +486 -133
  130. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  131. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  132. package/dist/tui-ink/run.d.ts +3 -0
  133. package/dist/tui-ink/run.js +1 -1
  134. package/dist/tui-ink/subagent-view.d.ts +31 -0
  135. package/dist/tui-ink/subagent-view.js +82 -1
  136. package/dist/types.d.ts +5 -5
  137. package/dist/types.js +1 -1
  138. package/dist/variant/thinking-level.d.ts +2 -2
  139. package/dist/variant/thinking-level.js +2 -1
  140. package/dist/variant/variant-resolver.d.ts +9 -0
  141. package/dist/variant/variant-resolver.js +17 -0
  142. package/package.json +2 -1
  143. package/dist/tools/task.d.ts +0 -2
  144. package/dist/tools/task.js +0 -58
@@ -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
@@ -276,9 +266,27 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
276
266
  const [streamingReasoning, setStreamingReasoning] = useState("");
277
267
  const [streamingTools, setStreamingTools] = useState([]);
278
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
+ }, []);
279
285
  // Live subagent groups for the inspector opened from the subagent entry line;
280
286
  // recomputed each render so it reflects members as their events stream into the transcript.
281
- 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]);
282
290
  const subagentMembers = useMemo(() => subagentGroups.flatMap((g) => g.members), [subagentGroups]);
283
291
  // Down-arrow from the composer focuses the subagent entry line; Enter then
284
292
  // opens the inspector, Esc/Up returns to the composer (Claude Code parity).
@@ -310,6 +318,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
310
318
  const [pendingQuestion, setPendingQuestion] = useState(null);
311
319
  const [pendingFeedback, setPendingFeedback] = useState(null);
312
320
  const [pickerMode, setPickerMode] = useState(null);
321
+ const [grokModels, setGrokModels] = useState([]);
313
322
  const [statsPanel, setStatsPanel] = useState(null);
314
323
  const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
315
324
  const [composerDraft, setComposerDraft] = useState(null);
@@ -333,7 +342,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
333
342
  const [staticGeneration, setStaticGeneration] = useState(0);
334
343
  const reprintTranscript = useCallback(() => {
335
344
  if (process.stdout.isTTY) {
336
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
345
+ process.stdout.write("\x1b[0m\x1b[2J\x1b[3J\x1b[H");
337
346
  }
338
347
  setStaticGeneration((generation) => generation + 1);
339
348
  }, []);
@@ -349,6 +358,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
349
358
  const startingSubmitFingerprintRef = useRef(null);
350
359
  const [startingSubmitFingerprint, setStartingSubmitFingerprint] = useState(null);
351
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);
352
364
  // Set true the moment /quit is invoked so we can hide dynamic UI (composer,
353
365
  // waiting indicator, footer) before Ink snapshots its final frame into the
354
366
  // shell scrollback. Without this, the last visible "> " input row stays
@@ -374,6 +386,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
374
386
  cwd: args.cwd,
375
387
  skillPaths: userConfig.getSkillPaths(),
376
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
+ }, []);
377
398
  useEffect(() => {
378
399
  setCurrentUpdateNotice(updateNotice);
379
400
  }, [updateNotice]);
@@ -572,6 +593,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
572
593
  return;
573
594
  }
574
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
+ }
575
600
  if (!mcpManager || mcpManager.getStates().length === 0) {
576
601
  addMessage("assistant", "No MCP servers configured.");
577
602
  }
@@ -596,6 +621,35 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
596
621
  }
597
622
  // Ctrl+R: cycle thinking level (formerly Shift+Tab)
598
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
+ }
599
653
  const modelParts = agent.model.includes(":")
600
654
  ? agent.model.split(":")
601
655
  : [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
@@ -689,7 +743,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
689
743
  // messages alone leaves a stale To-Do list on screen. /clear already reset
690
744
  // the agent's todos; mirror that into the UI (same as session switch).
691
745
  setTodos(agent.getTodos());
692
- }, [resetTranscript, agent]);
746
+ clearLiveSubagentTools();
747
+ }, [resetTranscript, agent, clearLiveSubagentTools]);
693
748
  // Render a placeholder user row for input waiting to enter the run.
694
749
  const addStatusUserMessage = useCallback((content, status) => {
695
750
  const key = nextDisplayMessageKey("user");
@@ -708,7 +763,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
708
763
  return { ...payload, imageDisplayStart };
709
764
  }, []);
710
765
  const submitDisplayText = useCallback((payload) => (formatImageUserDisplayText(payload.displayText ?? payload.text, payload.images.length, payload.imageDisplayStart)), []);
711
- const currentSessionFile = useCallback(() => sessionManager?.getSessionFile(), [sessionManager]);
766
+ const currentSessionFile = useCallback(() => sessionManagerRef.current?.getSessionFile(), []);
712
767
  const queueInput = useCallback((payload) => {
713
768
  const preparedPayload = prepareSubmitDisplay(payload);
714
769
  const displayKey = addStatusUserMessage(submitDisplayText(preparedPayload), "queued");
@@ -728,6 +783,19 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
728
783
  setPendingSteerCount(pendingSteersRef.current.size);
729
784
  }, [addStatusUserMessage, currentSessionFile, prepareSubmitDisplay, queueInput, submitDisplayText]);
730
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
+ }
731
799
  if (mode === "key") {
732
800
  setKeyProviderId(providerId ?? null);
733
801
  }
@@ -736,11 +804,36 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
736
804
  }
737
805
  setStatsPanel(null);
738
806
  setPickerMode(mode);
739
- }, []);
807
+ }, [addMessage, externalRuntime, grokSessionBound]);
740
808
  const closePicker = useCallback(() => {
741
809
  setPickerMode(null);
742
810
  setCursorResetEpoch((epoch) => epoch + 1);
743
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]);
744
837
  const fillComposer = useCallback((text) => {
745
838
  setComposerDraft((current) => ({
746
839
  text,
@@ -790,23 +883,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
790
883
  setStatsPanel(null);
791
884
  setCursorResetEpoch((epoch) => epoch + 1);
792
885
  }, []);
793
- const handleSessionSelect = useCallback((sessionFile) => {
794
- if (!switchSession) {
795
- addMessage("error", "Session switching is not available in this mode.");
796
- closePicker();
797
- return;
798
- }
799
- if (activeAbortRef.current) {
800
- addMessage("error", "Stop the current run before switching sessions.");
801
- closePicker();
802
- return;
803
- }
804
- const result = switchSession(sessionFile);
805
- if ("error" in result) {
806
- addMessage("error", `Failed to switch session: ${result.error}`);
807
- closePicker();
808
- return;
809
- }
886
+ const applySessionSwitch = useCallback((result, notice) => {
887
+ externalRuntimeGenerationRef.current += 1;
810
888
  const queuedDisplayKeys = queuedAndPendingDisplayKeys(queuedInputsRef.current, pendingSteersRef.current.values());
811
889
  queuedInputsRef.current = [];
812
890
  pendingSteersRef.current.clear();
@@ -815,27 +893,122 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
815
893
  setPendingSteerCount(0);
816
894
  setStartingSubmit(null);
817
895
  clearComposerDraft();
896
+ sessionManagerRef.current = result.manager;
818
897
  setSessionManager(result.manager);
898
+ const nextExternalBinding = result.manager.getMetadata().externalRuntime;
899
+ externalRuntimeBindingRef.current = nextExternalBinding;
900
+ setExternalRuntimeBinding(nextExternalBinding);
819
901
  setTodos(agent.getTodos());
902
+ clearLiveSubagentTools();
820
903
  resetTranscript(() => [
821
904
  ...reconstructDisplayMessages(agent.messages).filter((message) => !queuedDisplayKeys.has(message.key ?? "")),
822
- withMessageKey({ role: "assistant", content: `⤷ Resumed session: ${sessionDisplayName(result.manager)}` }),
905
+ ...(notice ? [withMessageKey({ role: "assistant", content: notice })] : []),
823
906
  ]);
824
907
  closePicker();
825
- }, [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]);
826
987
  const handleModelSelect = useCallback((model, selectedThinkingLevel) => {
827
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();
828
1000
  const nextThinkingLevel = await switchAgentModel({
829
1001
  model,
830
1002
  agent,
831
1003
  registry: safeRegistry,
832
1004
  createProvider,
1005
+ preparedProvider,
833
1006
  workingDir: args.cwd,
834
1007
  systemPromptOptions: agent.getSystemPromptToolOptions(),
835
1008
  thinkingLevel: selectedThinkingLevel,
836
1009
  rememberModel: (nextModel) => userConfig.pushRecentModel(nextModel),
837
1010
  setThinkingLevel,
838
- sessionManager,
1011
+ sessionManager: targetSessionManager,
839
1012
  });
840
1013
  // Binary thinking toggles and thinking-only models use internal level
841
1014
  // placeholders; do not present those placeholders as real effort grades.
@@ -854,7 +1027,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
854
1027
  addMessage("error", formatModelSwitchError(model, error));
855
1028
  closePicker();
856
1029
  });
857
- }, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
1030
+ }, [agent, addMessage, closePicker, sessionManager, transitionToNative, userConfig, safeRegistry, createProvider]);
858
1031
  const handleThemeHighlight = useCallback((mode) => {
859
1032
  setThemeMode(mode);
860
1033
  }, []);
@@ -868,58 +1041,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
868
1041
  setThemeMode(themePickerRevertRef.current);
869
1042
  closePicker();
870
1043
  }, [closePicker]);
871
- const handleProviderSelect = useCallback((providerId) => {
872
- const run = async () => {
873
- await safeRegistry.prepareProvider(providerId);
874
- const configured = safeRegistry.getConfigured();
875
- const p = configured.find((x) => x.id === providerId);
876
- const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
877
- if (!p && !builtin) {
878
- addMessage("error", `Provider ${providerId} not found.`);
879
- closePicker();
880
- return;
881
- }
882
- if (!p?.apiKey) {
883
- if (!p && builtin) {
884
- safeRegistry.addProvider(providerId, "");
885
- }
886
- safeRegistry.setDefault(providerId);
887
- setKeyProviderId(providerId);
888
- setPickerMode("key");
889
- return;
890
- }
891
- safeRegistry.setDefault(providerId);
892
- agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
893
- agent.providerId = providerId;
894
- addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
895
- closePicker();
896
- };
897
- void run().catch((error) => {
898
- addMessage("error", `Failed to switch provider ${providerId}: ${errorMessage(error)}`);
899
- closePicker();
900
- });
901
- }, [addMessage, agent, closePicker, createProvider, safeRegistry]);
902
- const handleProviderAddSelect = useCallback((providerId) => {
903
- const ok = safeRegistry.addProvider(providerId, "");
904
- if (!ok) {
905
- addMessage("error", `Provider ${providerId} could not be added.`);
906
- closePicker();
907
- return;
908
- }
909
- safeRegistry.setDefault(providerId);
910
- setKeyProviderId(providerId);
911
- setPickerMode("key");
912
- }, [addMessage, closePicker, safeRegistry]);
913
- const handleLoginProviderSelect = useCallback(async (providerId) => {
1044
+ const executeProviderCommand = useCallback(async (command) => {
914
1045
  closePicker();
915
- const command = `/login ${providerId}`;
916
1046
  const { handled, result } = await slashRegistry.execute(command, {
917
1047
  agent,
918
1048
  addMessage,
919
1049
  clearMessages,
920
1050
  cwd: args.cwd,
921
- exit: () => { requestExit(); },
922
- sessionManager,
1051
+ exit: requestExit,
1052
+ sessionManager: sessionManagerRef.current,
923
1053
  createProvider: createProvider ?? (() => {
924
1054
  throw new Error("Provider creation not available");
925
1055
  }),
@@ -943,67 +1073,157 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
943
1073
  getResolvedTheme: () => themeResolved,
944
1074
  setThemeMode: applyThemeMode,
945
1075
  openStats: openStatsPanel,
1076
+ externalRuntime,
1077
+ startFreshSession,
1078
+ transitionToNative,
1079
+ onExternalRuntimeChange: refreshExternalRuntimeBinding,
946
1080
  });
947
- if (handled && result) {
1081
+ if (handled && result)
948
1082
  addMessage("assistant", result);
949
- }
950
- }, [agent, addMessage, clearMessages, closePicker, createProvider, exit, fillComposer, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, safeRegistry, sessionManager]);
951
- const handleLogoutProviderSelect = useCallback(async (providerId) => {
952
- closePicker();
953
- const command = `/logout ${providerId}`;
954
- const { handled, result } = await slashRegistry.execute(command, {
955
- agent,
956
- addMessage,
957
- clearMessages,
958
- cwd: args.cwd,
959
- exit: () => { requestExit(); },
960
- sessionManager,
961
- 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)
962
1144
  throw new Error("Provider creation not available");
963
- }),
964
- openPicker,
965
- openSessionPicker,
966
- openRewindPicker,
967
- openFeedback,
968
- fillComposer,
969
- registry: safeRegistry,
970
- skillRegistry: safeSkillRegistry,
971
- bashAllowlist,
972
- settingsManager,
973
- lspService,
974
- mcpManager,
975
- hookController,
976
- flushMemory,
977
- runMemoryCompaction,
978
- runMemorySummary,
979
- runMemoryRefresh,
980
- getThemeMode: () => themeMode,
981
- getResolvedTheme: () => themeResolved,
982
- setThemeMode: applyThemeMode,
983
- 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();
984
1163
  });
985
- if (handled && result) {
986
- addMessage("assistant", result);
987
- }
988
- }, [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]);
989
1195
  const handleKeySubmit = useCallback((key) => {
990
- const targetId = keyProviderId || safeRegistry.getDefault()?.id;
991
- if (!targetId) {
992
- 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)}.`);
993
1219
  closePicker();
994
1220
  setKeyProviderId(null);
995
- return;
996
- }
997
- safeRegistry.updateProviderKey(targetId, key);
998
- const p = safeRegistry.getConfigured().find((x) => x.id === targetId);
999
- if (p && createProvider) {
1000
- agent.setProvider(createProvider(targetId, key, p.baseURL));
1001
- agent.providerId = targetId;
1002
- }
1003
- addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
1004
- closePicker();
1005
- setKeyProviderId(null);
1006
- }, [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]);
1007
1227
  const handleSubmit = useCallback(async (payload) => {
1008
1228
  const initialPayload = typeof payload === "string" ? { text: payload, images: [] } : payload;
1009
1229
  const input = initialPayload.text;
@@ -1019,6 +1239,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1019
1239
  requestExit();
1020
1240
  return;
1021
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
+ }
1022
1249
  const steerEligible = !displayInput.trim().startsWith("/") &&
1023
1250
  !input.includes("@") &&
1024
1251
  images.length === 0;
@@ -1030,6 +1257,21 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1030
1257
  }
1031
1258
  return;
1032
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
+ }
1033
1275
  const submitFingerprint = submitPayloadFingerprint(initialPayload);
1034
1276
  const startingDecision = decideStartingSubmitFingerprint(startingSubmitFingerprintRef.current, submitFingerprint);
1035
1277
  if (startingDecision === "ignore")
@@ -1043,23 +1285,50 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1043
1285
  try {
1044
1286
  const runAgentInput = async (actualInput, displayInput, attachedImages = [], runOptions = {}) => {
1045
1287
  const runSessionFile = currentSessionFile();
1046
- const activeProviderId = agent.providerId || safeRegistry.getDefault()?.id;
1047
- const hasActiveProvider = !!activeProviderId && safeRegistry.getEnabled().some((provider) => provider.id === activeProviderId);
1048
- if (!hasActiveProvider) {
1049
- addMessage("error", "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.");
1050
- if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1051
- goalStore.pause();
1052
- 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);
1053
1311
  }
1054
- return;
1055
1312
  }
1056
- if (!agent.model) {
1057
- addMessage("error", "No model selected. Use /model after /login or provider setup.");
1058
- if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1059
- goalStore.pause();
1060
- 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;
1061
1331
  }
1062
- return;
1063
1332
  }
1064
1333
  const displayContent = formatImageUserDisplayText(displayInput, attachedImages.length, runOptions.imageDisplayStart);
1065
1334
  if (!runOptions.hidden) {
@@ -1070,6 +1339,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1070
1339
  // The new user row commits to native scrollback; the terminal keeps
1071
1340
  // the prompt in view, so there is no app-side "snap to bottom" to do.
1072
1341
  }
1342
+ if (runExternalRuntime) {
1343
+ runSessionManager.appendMessage({ role: "user", content: actualInput });
1344
+ }
1073
1345
  setIsRunning(true);
1074
1346
  runStartRef.current = Date.now();
1075
1347
  setStreamingContent("");
@@ -1084,6 +1356,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1084
1356
  let runErrored = false;
1085
1357
  const toolCalls = [];
1086
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
+ }
1087
1366
  const abortController = new AbortController();
1088
1367
  activeAbortRef.current = abortController;
1089
1368
  setStartingSubmit(null);
@@ -1117,7 +1396,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1117
1396
  toolCalls.length > 0 ||
1118
1397
  assistantParts.length > 0);
1119
1398
  const commitAssistantMessage = (taskElapsedMs) => {
1120
- if (!hasAssistantOutput())
1399
+ if (!hasAssistantOutput() || !isCurrentExternalRun())
1121
1400
  return;
1122
1401
  const currentParts = snapshotDisplayParts(assistantParts);
1123
1402
  const currentToolCalls = [...toolCalls];
@@ -1143,6 +1422,16 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1143
1422
  msg.taskElapsedMs = taskElapsedMs;
1144
1423
  }
1145
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
+ }
1146
1435
  };
1147
1436
  const clearAssistantStream = () => {
1148
1437
  // A timer firing after this reset would resurrect the just-committed
@@ -1158,10 +1447,33 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1158
1447
  assistantParts.length = 0;
1159
1448
  };
1160
1449
  try {
1161
- for await (const event of agent.run(actualInput, args.cwd, {
1162
- abortSignal: abortController.signal,
1163
- inputController,
1164
- })) {
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
+ }
1165
1477
  switch (event.type) {
1166
1478
  case "turn_start":
1167
1479
  // A fresh provider call is starting. Everything worth keeping
@@ -1268,6 +1580,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1268
1580
  setStreamingTools([...toolCalls]);
1269
1581
  syncStreamingParts();
1270
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
+ }
1271
1592
  break;
1272
1593
  }
1273
1594
  case "todos_updated": {
@@ -1352,15 +1673,23 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1352
1673
  }
1353
1674
  }
1354
1675
  catch (err) {
1355
- commitAssistantMessage();
1356
- 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) {
1357
1686
  runCancelled = true;
1358
1687
  // commitAssistantMessage already appended the partial answer; the
1359
1688
  // interrupt is otherwise a pure append (the partial + a "Interrupted"
1360
1689
  // row). Off a multiplexer, append just the interrupt row so settled
1361
1690
  // history is never reprinted — no flash. Under tmux/screen, fall back
1362
1691
  // to the full reprint that rebuilds from the canonical agent.messages.
1363
- if (isMultiplexedTerminal()) {
1692
+ if (!runExternalRuntime && isMultiplexedTerminal()) {
1364
1693
  resetTranscript(() => reconstructDisplayMessages(agent.messages));
1365
1694
  }
1366
1695
  else {
@@ -1378,7 +1707,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1378
1707
  runErrored = true;
1379
1708
  updateDisplayMessages((prev) => [
1380
1709
  ...prev,
1381
- withMessageKey({ role: "error", content: err.message }),
1710
+ withMessageKey({ role: "error", content: errorMessage(err) }),
1382
1711
  ]);
1383
1712
  }
1384
1713
  }
@@ -1390,6 +1719,23 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1390
1719
  const cancelled = abortController.signal.aborted;
1391
1720
  if (cancelled)
1392
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
+ }
1393
1739
  for (const leftover of inputController.clear()) {
1394
1740
  const steer = pendingSteersRef.current.get(leftover.id);
1395
1741
  pendingSteersRef.current.delete(leftover.id);
@@ -1408,25 +1754,32 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1408
1754
  sessionFile: steer?.sessionFile ?? runSessionFile,
1409
1755
  });
1410
1756
  }
1411
- setPendingSteerCount(0);
1412
- setQueuedCount(queuedInputsRef.current.length);
1757
+ const ownsCurrentUiGeneration = !runExternalRuntime || isCurrentExternalRun();
1758
+ if (ownsCurrentUiGeneration) {
1759
+ setPendingSteerCount(0);
1760
+ setQueuedCount(queuedInputsRef.current.length);
1761
+ }
1413
1762
  if (inputControllerRef.current === inputController)
1414
1763
  inputControllerRef.current = null;
1415
1764
  if (activeAbortRef.current === abortController)
1416
1765
  activeAbortRef.current = null;
1417
- setIsRunning(false);
1418
- runStartRef.current = null;
1419
- setStreamingContent("");
1420
- setStreamingReasoning("");
1421
- setStreamingTools([]);
1422
- setStreamingParts([]);
1423
- maybeContinueGoal({
1424
- runCancelled,
1425
- runErrored,
1426
- isGoalRun: !!runOptions.goalRun,
1427
- runTokens: goalRunTokens,
1428
- usageReported: goalRunUsageReported,
1429
- });
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
+ }
1430
1783
  }
1431
1784
  };
1432
1785
  const kickGoalTurn = (prompt, visibleInput) => {
@@ -1553,7 +1906,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1553
1906
  };
1554
1907
  // Slash commands and skill invocations drop any attached images —
1555
1908
  // they're meant for pure command routing.
1556
- if (displayInput.startsWith("/")) {
1909
+ if (input.trimStart().startsWith("/")) {
1557
1910
  // Fast-path `/quit` and `/exit` before slash-registry / skill
1558
1911
  // resolution. This guarantees a literal "/quit" always exits even if
1559
1912
  // a skill or alias of the same name is later registered. The
@@ -1564,14 +1917,16 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1564
1917
  requestExit();
1565
1918
  return;
1566
1919
  }
1567
- if (/^\/goal(?:\s|$)/.test(input.trim())) {
1568
- await handleGoalCommand(input);
1569
- return;
1570
- }
1571
- const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
1572
- if (skillInvocation) {
1573
- await runAgentInput(skillInvocation.actualPrompt, displayInput);
1574
- 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
+ }
1575
1930
  }
1576
1931
  const { handled, result, inject } = await slashRegistry.execute(input, {
1577
1932
  agent,
@@ -1604,6 +1959,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1604
1959
  setThemeMode: applyThemeMode,
1605
1960
  openStats: openStatsPanel,
1606
1961
  compactionProgress: setCompaction,
1962
+ externalRuntime,
1963
+ startFreshSession,
1964
+ transitionToNative,
1965
+ onExternalRuntimeChange: refreshExternalRuntimeBinding,
1607
1966
  });
1608
1967
  if (handled) {
1609
1968
  if (agent.mode !== permissionMode) {
@@ -1627,7 +1986,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1627
1986
  }
1628
1987
  else if (result.startsWith("⏪")) {
1629
1988
  // /rewind truncated agent.messages — rebuild the transcript from
1630
- // 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();
1631
1992
  resetTranscript(() => [
1632
1993
  ...reconstructDisplayMessages(agent.messages),
1633
1994
  { role: "assistant", content: result },
@@ -1642,6 +2003,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1642
2003
  }
1643
2004
  return;
1644
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;
1645
2014
  }
1646
2015
  const expansion = await expandAtMentions(input, args.cwd);
1647
2016
  if (expansion.missing.length > 0) {
@@ -1667,7 +2036,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1667
2036
  setStartingSubmit(null);
1668
2037
  }
1669
2038
  }
1670
- }, [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]);
1671
2040
  // Drain the queue once the run ends and no modal needs the user first.
1672
2041
  // The placeholder row is removed right before resubmitting — handleSubmit
1673
2042
  // renders the message again as a regular user row.
@@ -1699,7 +2068,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1699
2068
  const timer = setTimeout(drainQueuedInput, 0);
1700
2069
  return () => clearTimeout(timer);
1701
2070
  }, [isRunning, queuedCount, startingSubmitFingerprint, pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, statsPanel, drainQueuedInput]);
1702
- const currentProviderId = agent.providerId || safeRegistry.getDefault()?.id;
2071
+ const currentProviderId = externalSessionBound
2072
+ ? (grokSessionBound ? GROK_SUBSCRIPTION_PROVIDER_ID : undefined)
2073
+ : agent.providerId || safeRegistry.getDefault()?.id;
1703
2074
  const keyTarget = keyProviderId
1704
2075
  ? safeRegistry.getConfigured().find((p) => p.id === keyProviderId)
1705
2076
  : safeRegistry.getDefault();
@@ -1726,8 +2097,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1726
2097
  const showThinkingLabel = Boolean(thinkingLevel)
1727
2098
  && thinkingLevel !== "off"
1728
2099
  && (isThinkingToggle || isThinkingOnly || availableThinkingLevels.length > 2);
1729
- 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, modelId: agent.apiModel, modelLabel: agent.model ? displayModel(agent.model) : undefined, thinkingLabel: showThinkingLabel ? thinkingLevel : undefined })) : null;
1730
- const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry), [safeSkillRegistry]);
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]);
1731
2108
  const mcpReconnectItems = useMemo(() => buildMcpReconnectItems(mcpManager), [mcpManager]);
1732
2109
  // No fixed-height frame: settled rows flow into the terminal's native
1733
2110
  // scrollback via <Static>, and only the dynamic bottom stack (streaming
@@ -1735,7 +2112,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1735
2112
  // to its content keeps the composer pinned just below the latest output the
1736
2113
  // way ordinary shell programs do.
1737
2114
  const mainWidth = Math.max(40, terminalColumns);
1738
- 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, 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
1739
2123
  .filter((p) => isUserVisibleProvider(p.id))
1740
2124
  .map((p) => {
1741
2125
  const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
@@ -1745,13 +2129,21 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1745
2129
  name: `${p.name} [${configuredLabel}]`,
1746
2130
  enabled: true,
1747
2131
  };
1748
- }), 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
1749
2133
  .filter((p) => isUserVisibleProvider(p.id))
1750
- .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
1751
2138
  .filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
1752
- .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()
1753
- .filter((p) => safeRegistry.getAuthStorage().has(p.id))
1754
- .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: () => {
1755
2147
  closePicker();
1756
2148
  setKeyProviderId(null);
1757
2149
  } }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
@@ -1808,27 +2200,49 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1808
2200
  else if (result.kind === "error") {
1809
2201
  addMessage("error", `Feedback failed: ${result.message}`);
1810
2202
  }
1811
- } }) })), !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: () => {
1812
2214
  if (subagentMembers.length > 0 && !pickerMode)
1813
2215
  setSubagentEntryFocused(true);
1814
- }, 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, 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
+ }) }) }))] }) }));
1815
2225
  }
1816
- function buildCommandPaletteItems(skillRegistry) {
2226
+ function buildCommandPaletteItems(skillRegistry, grokSessionBound = false) {
1817
2227
  const items = new Map();
1818
2228
  const add = (item) => {
1819
2229
  const key = `${item.action ?? "command"}:${item.value}`;
1820
2230
  if (!items.has(key))
1821
2231
  items.set(key, item);
1822
2232
  };
1823
- for (const command of INK_LOCAL_SLASH_COMMANDS) {
1824
- add({
1825
- label: `/${command.name}`,
1826
- detail: command.description,
1827
- value: command.name,
1828
- command: `/${command.name}`,
1829
- });
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
+ }
1830
2242
  }
1831
2243
  for (const command of slashRegistry.list()) {
2244
+ if (grokSessionBound && !isGrokLocalSlashCommand(command.name))
2245
+ continue;
1832
2246
  const source = command.source === "mcp" ? " :mcp" : "";
1833
2247
  const sourceLabel = command.sourceLabel ? `[${command.sourceLabel}] ` : "";
1834
2248
  add({
@@ -1838,14 +2252,16 @@ function buildCommandPaletteItems(skillRegistry) {
1838
2252
  command: `/${command.name}`,
1839
2253
  });
1840
2254
  }
1841
- for (const skill of skillRegistry.summaries()) {
1842
- add({
1843
- label: `/${skill.name} :skill`,
1844
- detail: `[${skill.source}] ${skill.description}`,
1845
- value: skill.name,
1846
- command: `/${skill.name}`,
1847
- action: "insert-skill",
1848
- });
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
+ }
1849
2265
  }
1850
2266
  return [...items.values()];
1851
2267
  }