@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.
- package/README.md +27 -7
- package/dist/agent/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -2
- package/dist/agent/profiles.js +25 -14
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +21 -0
- package/dist/agent/workflow/control.js +50 -8
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +82 -48
- package/dist/agent.js +360 -232
- package/dist/approval/controller.js +28 -0
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +21 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -1
- package/dist/config.d.ts +5 -3
- package/dist/config.js +10 -5
- package/dist/context/compact-llm.d.ts +2 -2
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.js +21 -0
- package/dist/feishu/agent-host/run-driver.js +19 -3
- package/dist/feishu/serve.js +1 -0
- package/dist/goal/tools.d.ts +6 -3
- package/dist/goal/tools.js +7 -19
- package/dist/main.js +114 -28
- package/dist/memory/db.d.ts +2 -0
- package/dist/memory/db.js +10 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/phase1.js +14 -2
- package/dist/memory/phase2.js +21 -2
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/storage.d.ts +1 -0
- package/dist/memory/storage.js +13 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +92 -52
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +11 -0
- package/dist/oauth/grok.d.ts +25 -0
- package/dist/oauth/grok.js +218 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/storage.d.ts +9 -0
- package/dist/oauth/storage.js +25 -0
- package/dist/orchestrator/default-hooks.js +23 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +41 -8
- package/dist/prompt/environment.js +0 -4
- package/dist/prompt/reminders.d.ts +0 -1
- package/dist/prompt/reminders.js +1 -11
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.js +1 -1
- package/dist/prompt/task-reminders.d.ts +20 -0
- package/dist/prompt/task-reminders.js +73 -1
- package/dist/provider-ai-sdk.js +2 -2
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +59 -0
- package/dist/provider-openai-codex.d.ts +17 -1
- package/dist/provider-openai-codex.js +101 -29
- package/dist/provider-registry.d.ts +72 -0
- package/dist/provider-registry.js +336 -59
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +35 -17
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +14 -0
- package/dist/session-types.d.ts +19 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +4 -0
- package/dist/slash-commands/commands.js +224 -14
- package/dist/slash-commands/types.d.ts +14 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +42 -272
- package/dist/tools/bash.js +1 -1
- package/dist/tools/child-tools.js +2 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +7 -5
- package/dist/tools/lsp.js +1 -0
- package/dist/tools/memory.d.ts +7 -2
- package/dist/tools/memory.js +28 -43
- package/dist/tools/server.js +4 -0
- package/dist/tools/tool-search.d.ts +1 -1
- package/dist/tools/tool-search.js +23 -3
- package/dist/tui/model-switch.d.ts +7 -0
- package/dist/tui/model-switch.js +15 -5
- package/dist/tui-ink/app.d.ts +3 -1
- package/dist/tui-ink/app.js +672 -243
- package/dist/tui-ink/approval/approval-dialog.js +37 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +7 -2
- package/dist/tui-ink/input-box.d.ts +36 -7
- package/dist/tui-ink/input-box.js +229 -157
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +464 -128
- package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
- package/dist/tui-ink/paste-operation-tracker.js +25 -0
- package/dist/tui-ink/plan-confirm.js +1 -1
- package/dist/tui-ink/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +34 -1
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +8 -31
- package/dist/types.js +1 -1
- package/dist/variant/thinking-level.d.ts +2 -2
- package/dist/variant/thinking-level.js +2 -1
- package/dist/variant/variant-resolver.d.ts +22 -0
- package/dist/variant/variant-resolver.js +43 -0
- package/package.json +6 -2
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
package/dist/tui-ink/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(() =>
|
|
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
|
|
786
|
-
|
|
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:
|
|
905
|
+
...(notice ? [withMessageKey({ role: "assistant", content: notice })] : []),
|
|
815
906
|
]);
|
|
816
907
|
closePicker();
|
|
817
|
-
|
|
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
|
-
//
|
|
833
|
-
//
|
|
834
|
-
const
|
|
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
|
-
? (
|
|
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
|
|
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:
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
addMessage("
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1345
|
-
if (
|
|
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
|
|
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
|
-
|
|
1401
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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 (
|
|
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 (
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
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
|
-
&& (
|
|
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,
|
|
1717
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
1740
|
-
|
|
1741
|
-
|
|
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 && (
|
|
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({
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
}
|