@bubblebrain-ai/bubble 0.0.24 → 0.0.25
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 +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +22 -6
- package/dist/goal/format.js +34 -4
- package/dist/goal/store.d.ts +3 -0
- package/dist/goal/store.js +14 -1
- package/dist/goal/usage.d.ts +2 -0
- package/dist/goal/usage.js +3 -0
- package/dist/main.js +23 -42
- package/dist/provider.js +20 -5
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +23 -0
- package/dist/tui/image-display.d.ts +13 -0
- package/dist/tui/image-display.js +49 -0
- package/dist/tui/input-history.d.ts +37 -6
- package/dist/tui/input-history.js +194 -23
- package/dist/tui/model-switch.d.ts +42 -0
- package/dist/tui/model-switch.js +55 -0
- package/dist/tui-ink/app.d.ts +32 -2
- package/dist/tui-ink/app.js +1360 -522
- package/dist/tui-ink/approval/select.js +10 -0
- package/dist/tui-ink/detect-theme.d.ts +1 -2
- package/dist/tui-ink/detect-theme.js +1 -87
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +11 -0
- package/dist/tui-ink/feedback-dialog.js +10 -0
- package/dist/tui-ink/feishu-setup-picker.js +10 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +8 -2
- package/dist/tui-ink/input-box.d.ts +70 -9
- package/dist/tui-ink/input-box.js +354 -120
- package/dist/tui-ink/input-history.d.ts +1 -16
- package/dist/tui-ink/input-history.js +1 -79
- package/dist/tui-ink/input-queue.d.ts +12 -0
- package/dist/tui-ink/input-queue.js +17 -0
- package/dist/tui-ink/key-events.d.ts +9 -0
- package/dist/tui-ink/key-events.js +8 -0
- package/dist/tui-ink/markdown.js +1 -1
- package/dist/tui-ink/message-list.d.ts +3 -1
- package/dist/tui-ink/message-list.js +42 -24
- package/dist/tui-ink/model-picker.d.ts +24 -2
- package/dist/tui-ink/model-picker.js +224 -20
- package/dist/tui-ink/plan-confirm.js +10 -0
- package/dist/tui-ink/question-dialog.js +10 -0
- package/dist/tui-ink/run.d.ts +10 -1
- package/dist/tui-ink/run.js +21 -28
- package/dist/tui-ink/session-picker.js +3 -0
- package/dist/tui-ink/submit-dedupe.d.ts +5 -0
- package/dist/tui-ink/submit-dedupe.js +25 -0
- package/dist/tui-ink/terminal-mouse.d.ts +13 -1
- package/dist/tui-ink/terminal-mouse.js +63 -21
- package/dist/tui-ink/theme.d.ts +6 -3
- package/dist/tui-ink/theme.js +10 -4
- package/dist/tui-ink/transcript-input.d.ts +8 -0
- package/dist/tui-ink/transcript-input.js +9 -0
- package/dist/tui-ink/transcript-viewport-math.d.ts +1 -2
- package/dist/tui-ink/transcript-viewport-math.js +1 -2
- package/dist/tui-ink/welcome.d.ts +1 -0
- package/dist/tui-ink/welcome.js +25 -28
- package/package.json +1 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -22
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -67
- package/dist/tui/run.js +0 -10166
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -135
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/types.js +0 -1
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -32
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- package/dist/tui-opentui/app.d.ts +0 -54
- package/dist/tui-opentui/app.js +0 -1371
- package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
- package/dist/tui-opentui/approval/approval-dialog.js +0 -155
- package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
- package/dist/tui-opentui/approval/diff-view.js +0 -43
- package/dist/tui-opentui/approval/select.d.ts +0 -37
- package/dist/tui-opentui/approval/select.js +0 -91
- package/dist/tui-opentui/detect-theme.d.ts +0 -2
- package/dist/tui-opentui/detect-theme.js +0 -87
- package/dist/tui-opentui/display-history.d.ts +0 -56
- package/dist/tui-opentui/display-history.js +0 -130
- package/dist/tui-opentui/edit-diff.d.ts +0 -11
- package/dist/tui-opentui/edit-diff.js +0 -57
- package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
- package/dist/tui-opentui/feedback-dialog.js +0 -164
- package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
- package/dist/tui-opentui/feishu-setup-picker.js +0 -272
- package/dist/tui-opentui/file-mentions.d.ts +0 -29
- package/dist/tui-opentui/file-mentions.js +0 -174
- package/dist/tui-opentui/footer.d.ts +0 -26
- package/dist/tui-opentui/footer.js +0 -40
- package/dist/tui-opentui/image-paste.d.ts +0 -54
- package/dist/tui-opentui/image-paste.js +0 -288
- package/dist/tui-opentui/input-box.d.ts +0 -32
- package/dist/tui-opentui/input-box.js +0 -462
- package/dist/tui-opentui/input-history.d.ts +0 -16
- package/dist/tui-opentui/input-history.js +0 -79
- package/dist/tui-opentui/markdown.d.ts +0 -66
- package/dist/tui-opentui/markdown.js +0 -127
- package/dist/tui-opentui/message-list.d.ts +0 -31
- package/dist/tui-opentui/message-list.js +0 -131
- package/dist/tui-opentui/model-picker.d.ts +0 -63
- package/dist/tui-opentui/model-picker.js +0 -450
- package/dist/tui-opentui/plan-confirm.d.ts +0 -9
- package/dist/tui-opentui/plan-confirm.js +0 -124
- package/dist/tui-opentui/question-dialog.d.ts +0 -10
- package/dist/tui-opentui/question-dialog.js +0 -110
- package/dist/tui-opentui/recent-activity.d.ts +0 -8
- package/dist/tui-opentui/recent-activity.js +0 -71
- package/dist/tui-opentui/run-session-picker.d.ts +0 -10
- package/dist/tui-opentui/run-session-picker.js +0 -28
- package/dist/tui-opentui/run.d.ts +0 -38
- package/dist/tui-opentui/run.js +0 -48
- package/dist/tui-opentui/session-picker.d.ts +0 -12
- package/dist/tui-opentui/session-picker.js +0 -120
- package/dist/tui-opentui/theme.d.ts +0 -89
- package/dist/tui-opentui/theme.js +0 -157
- package/dist/tui-opentui/todos.d.ts +0 -9
- package/dist/tui-opentui/todos.js +0 -45
- package/dist/tui-opentui/trace-groups.d.ts +0 -27
- package/dist/tui-opentui/trace-groups.js +0 -455
- package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
- package/dist/tui-opentui/use-terminal-size.js +0 -5
- package/dist/tui-opentui/welcome.d.ts +0 -25
- package/dist/tui-opentui/welcome.js +0 -77
package/dist/tui-ink/app.js
CHANGED
|
@@ -3,18 +3,19 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
3
3
|
import { Box, Text, useApp, useInput } from "ink";
|
|
4
4
|
import { AgentAbortError, INTERRUPTED_ASSISTANT_CONTENT } from "../agent.js";
|
|
5
5
|
import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
6
|
+
import { SessionManager } from "../session.js";
|
|
6
7
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
7
8
|
import { UserConfig, maskKey } from "../config.js";
|
|
8
9
|
import { createPastedContentMarker, InputBox, isCtrlCInput, shouldCollapsePastedContent, } from "./input-box.js";
|
|
9
10
|
import { MessageList } from "./message-list.js";
|
|
10
|
-
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
|
|
11
|
+
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
|
|
11
12
|
import { AgentRunInputQueue } from "../agent/input-controller.js";
|
|
12
13
|
import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
|
|
13
|
-
import { ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
|
|
14
|
+
import { isPrintablePickerInput, ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
|
|
14
15
|
import { FeishuSetupPicker } from "./feishu-setup-picker.js";
|
|
15
16
|
import { BUILTIN_PROVIDERS, ProviderRegistry, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
16
17
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
17
|
-
import { getAvailableThinkingLevels,
|
|
18
|
+
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
|
|
18
19
|
import { FooterBar, buildFooterData } from "./footer.js";
|
|
19
20
|
import { SkillRegistry } from "../skills/registry.js";
|
|
20
21
|
import { parseSkillInvocation } from "../skills/invocation.js";
|
|
@@ -28,8 +29,23 @@ import { getNextPermissionMode } from "../permission/mode.js";
|
|
|
28
29
|
import { QuestionDialog } from "./question-dialog.js";
|
|
29
30
|
import { FeedbackDialog } from "./feedback-dialog.js";
|
|
30
31
|
import { collectFeedback } from "../feedback/collect.js";
|
|
31
|
-
import {
|
|
32
|
+
import { isKeyReleaseEvent } from "./key-events.js";
|
|
33
|
+
import { errorMessage, formatModelSwitchError, switchAgentModel } from "../tui/model-switch.js";
|
|
34
|
+
import { formatImageUserDisplayText, nextImageDisplayLabelStart } from "../tui/image-display.js";
|
|
35
|
+
import { sanitizeTerminalMouseInput, transcriptScrollLinesFromMouseInput } from "./terminal-mouse.js";
|
|
36
|
+
import { transcriptPageScrollDirection } from "./transcript-input.js";
|
|
37
|
+
import { decideStartingSubmitFingerprint, submitPayloadFingerprint } from "./submit-dedupe.js";
|
|
38
|
+
import { isQueuedInputForCurrentSession, queuedAndPendingDisplayKeys, } from "./input-queue.js";
|
|
32
39
|
import { TranscriptViewport } from "./transcript-viewport.js";
|
|
40
|
+
import { SessionPicker } from "./session-picker.js";
|
|
41
|
+
import { sessionDisplayName } from "../tui/session-display.js";
|
|
42
|
+
import { parseGoalCommand } from "../goal/command.js";
|
|
43
|
+
import { continuationPrompt, initialPrompt } from "../goal/prompts.js";
|
|
44
|
+
import { shouldContinueGoal, stopReasonNotice } from "../goal/engine.js";
|
|
45
|
+
import { goalCompleteNotice, goalIndicatorLine, goalSummaryText } from "../goal/format.js";
|
|
46
|
+
import { tokenUsageTotal } from "../goal/usage.js";
|
|
47
|
+
import { formatInternalContextBlock } from "../agent/internal-reminder-sanitizer.js";
|
|
48
|
+
import { collectUsageStatsBundle, formatStatsPanelBody, rangeLabel } from "../stats/usage.js";
|
|
33
49
|
import os from "node:os";
|
|
34
50
|
function buildTips(agent, registry) {
|
|
35
51
|
const tips = [];
|
|
@@ -55,6 +71,11 @@ function friendlyCwd(cwd) {
|
|
|
55
71
|
return "~" + cwd.slice(home.length);
|
|
56
72
|
return cwd;
|
|
57
73
|
}
|
|
74
|
+
function truncate(value, max) {
|
|
75
|
+
if (value.length <= max)
|
|
76
|
+
return value;
|
|
77
|
+
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
78
|
+
}
|
|
58
79
|
function reconstructDisplayMessages(agentMessages) {
|
|
59
80
|
const result = [];
|
|
60
81
|
for (const m of agentMessages) {
|
|
@@ -206,7 +227,38 @@ function withMessageKey(message) {
|
|
|
206
227
|
// would make Yoga re-lay-out the transcript for every few bytes of output.
|
|
207
228
|
// 40ms keeps perceived latency invisible while capping layout work at 25fps.
|
|
208
229
|
const STREAMING_FLUSH_INTERVAL_MS = 40;
|
|
209
|
-
export
|
|
230
|
+
export const INK_LOCAL_SLASH_COMMANDS = [
|
|
231
|
+
{
|
|
232
|
+
name: "thinking",
|
|
233
|
+
description: "Toggle thinking block visibility",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "toggle-thinking",
|
|
237
|
+
description: "Toggle thinking block visibility",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "goal",
|
|
241
|
+
description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "trace",
|
|
245
|
+
description: "Toggle verbose trace output",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "verbose",
|
|
249
|
+
description: "Toggle verbose trace output",
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "debug",
|
|
253
|
+
description: "Toggle verbose trace output",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "write-previews",
|
|
257
|
+
description: "Toggle write preview expansion",
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
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 }) {
|
|
261
|
+
const [sessionManager, setSessionManager] = useState(initialSessionManager);
|
|
210
262
|
const [themeMode, setThemeMode] = useState(initialThemeMode ?? "auto");
|
|
211
263
|
// `detectedTheme` is captured once at startup in main.ts. We keep it in state
|
|
212
264
|
// so future re-detection (e.g. if a user runs `/theme auto` after switching
|
|
@@ -224,6 +276,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
224
276
|
const themeResolved = themeMode === "auto" ? autoResolved : themeMode;
|
|
225
277
|
const { exit } = useApp();
|
|
226
278
|
const [messages, setMessages] = useState(() => compactDisplayMessages(reconstructDisplayMessages(agent.messages)));
|
|
279
|
+
const nextImageDisplayLabelStartRef = useRef(nextImageDisplayLabelStart(messages));
|
|
227
280
|
const [isRunning, setIsRunning] = useState(false);
|
|
228
281
|
const [streamingContent, setStreamingContent] = useState("");
|
|
229
282
|
const [streamingReasoning, setStreamingReasoning] = useState("");
|
|
@@ -232,26 +285,33 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
232
285
|
const [thinkingLevel, setThinkingLevel] = useState(agent.thinking);
|
|
233
286
|
const [permissionMode, setPermissionMode] = useState(agent.mode);
|
|
234
287
|
const [todos, setTodos] = useState(() => agent.getTodos());
|
|
288
|
+
const [goalLine, setGoalLine] = useState("");
|
|
289
|
+
const [currentUpdateNotice, setCurrentUpdateNotice] = useState(updateNotice);
|
|
235
290
|
const [pendingPlan, setPendingPlan] = useState(null);
|
|
236
291
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
237
292
|
const [pendingQuestion, setPendingQuestion] = useState(null);
|
|
238
293
|
const [pendingFeedback, setPendingFeedback] = useState(null);
|
|
239
294
|
const [pickerMode, setPickerMode] = useState(null);
|
|
295
|
+
const [statsPanel, setStatsPanel] = useState(null);
|
|
240
296
|
const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
|
|
241
297
|
const [composerDraft, setComposerDraft] = useState(null);
|
|
242
298
|
const [keyProviderId, setKeyProviderId] = useState(null);
|
|
299
|
+
const [showThinking, setShowThinking] = useState(false);
|
|
300
|
+
const [expandedToolOutput, setExpandedToolOutput] = useState(false);
|
|
243
301
|
const [verboseTrace, setVerboseTrace] = useState(false);
|
|
302
|
+
const [sidebarMode, setSidebarMode] = useState("collapsed");
|
|
244
303
|
const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
|
|
245
304
|
const { columns: terminalColumns, rows: terminalRows } = useTerminalSize();
|
|
246
305
|
const showWelcome = shouldShowWelcomeBanner({
|
|
247
306
|
messages,
|
|
248
307
|
startedWithVisibleHistory: startedWithVisibleHistoryRef.current,
|
|
249
308
|
});
|
|
309
|
+
const showWelcomeRef = useRef(showWelcome);
|
|
250
310
|
const activeAbortRef = useRef(null);
|
|
251
311
|
const exitRequestedRef = useRef(false);
|
|
252
312
|
const sessionStartRef = useRef(Date.now());
|
|
253
313
|
const viewportRef = useRef(null);
|
|
254
|
-
// Steer/queue while the agent runs
|
|
314
|
+
// Steer/queue while the agent runs:
|
|
255
315
|
// Enter steers the current run via the agent's input controller; Tab (or an
|
|
256
316
|
// ineligible input) queues for the next turn. Both render placeholder user
|
|
257
317
|
// rows whose badge tracks the input's lifecycle.
|
|
@@ -260,6 +320,8 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
260
320
|
const queuedInputsRef = useRef([]);
|
|
261
321
|
const [pendingSteerCount, setPendingSteerCount] = useState(0);
|
|
262
322
|
const [queuedCount, setQueuedCount] = useState(0);
|
|
323
|
+
const startingSubmitFingerprintRef = useRef(null);
|
|
324
|
+
const [startingSubmitFingerprint, setStartingSubmitFingerprint] = useState(null);
|
|
263
325
|
const nextRunIdRef = useRef(0);
|
|
264
326
|
// Set true the moment /quit is invoked so we can hide dynamic UI (composer,
|
|
265
327
|
// waiting indicator, footer) before Ink snapshots its final frame into the
|
|
@@ -286,6 +348,46 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
286
348
|
cwd: args.cwd,
|
|
287
349
|
skillPaths: userConfig.getSkillPaths(),
|
|
288
350
|
});
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
setCurrentUpdateNotice(updateNotice);
|
|
353
|
+
}, [updateNotice]);
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
showWelcomeRef.current = showWelcome;
|
|
356
|
+
}, [showWelcome]);
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (!goalStore)
|
|
359
|
+
return;
|
|
360
|
+
let persistSuspended = false;
|
|
361
|
+
const persistGoal = (goal) => {
|
|
362
|
+
if (!sessionManager)
|
|
363
|
+
return;
|
|
364
|
+
try {
|
|
365
|
+
const metadata = sessionManager.getMetadata();
|
|
366
|
+
sessionManager.setMetadata({ ...metadata, goal: goal ?? undefined });
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Goal persistence is best-effort; never break the run loop over it.
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const unsubscribe = goalStore.onChange((goal) => {
|
|
373
|
+
setGoalLine(goal ? goalIndicatorLine(goal) : "");
|
|
374
|
+
if (!persistSuspended)
|
|
375
|
+
persistGoal(goal);
|
|
376
|
+
});
|
|
377
|
+
const persisted = sessionManager?.getMetadata().goal;
|
|
378
|
+
if (persisted) {
|
|
379
|
+
persistSuspended = true;
|
|
380
|
+
goalStore.loadFrom(persisted.status === "active" ? { ...persisted, status: "paused" } : persisted);
|
|
381
|
+
persistSuspended = false;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
persistSuspended = true;
|
|
385
|
+
goalStore.loadFrom(undefined);
|
|
386
|
+
persistSuspended = false;
|
|
387
|
+
setGoalLine("");
|
|
388
|
+
}
|
|
389
|
+
return unsubscribe;
|
|
390
|
+
}, [goalStore, sessionManager]);
|
|
289
391
|
const requestExit = useCallback(() => {
|
|
290
392
|
if (exitRequestedRef.current)
|
|
291
393
|
return;
|
|
@@ -415,26 +517,54 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
415
517
|
}));
|
|
416
518
|
}, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
|
|
417
519
|
useInput((input, key) => {
|
|
520
|
+
if (isKeyReleaseEvent(key))
|
|
521
|
+
return;
|
|
418
522
|
if (isCtrlCInput(input, key)) {
|
|
419
523
|
requestExit();
|
|
420
524
|
return;
|
|
421
525
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
526
|
+
const overlayActive = !!(pickerMode || pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || statsPanel);
|
|
527
|
+
const mouseInput = sanitizeTerminalMouseInput(input);
|
|
528
|
+
if (mouseInput.wheelDirections.length > 0) {
|
|
529
|
+
for (const lines of transcriptScrollLinesFromMouseInput(mouseInput, { overlayActive })) {
|
|
530
|
+
viewportRef.current?.scrollBy(lines);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (mouseInput.hasMouse) {
|
|
534
|
+
if (!mouseInput.strippedInput)
|
|
535
|
+
return;
|
|
536
|
+
input = mouseInput.strippedInput;
|
|
537
|
+
}
|
|
538
|
+
const pageScrollDirection = transcriptPageScrollDirection(key, { overlayActive });
|
|
539
|
+
if (pageScrollDirection) {
|
|
540
|
+
viewportRef.current?.scrollPage(pageScrollDirection);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || statsPanel)
|
|
427
544
|
return;
|
|
428
|
-
if (!pickerMode &&
|
|
429
|
-
|
|
545
|
+
if (key.ctrl && input.toLowerCase() === "p" && !pickerMode && !activeAbortRef.current) {
|
|
546
|
+
setStatsPanel(null);
|
|
547
|
+
setPickerMode("slash");
|
|
430
548
|
return;
|
|
431
549
|
}
|
|
432
|
-
if (
|
|
433
|
-
|
|
550
|
+
if (key.ctrl && key.shift && input.toLowerCase() === "m" && !pickerMode) {
|
|
551
|
+
if (!mcpManager || mcpManager.getStates().length === 0) {
|
|
552
|
+
addMessage("assistant", "No MCP servers configured.");
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
setStatsPanel(null);
|
|
556
|
+
setPickerMode("mcp-reconnect");
|
|
557
|
+
}
|
|
434
558
|
return;
|
|
435
559
|
}
|
|
436
|
-
if (
|
|
560
|
+
if (key.ctrl && input.toLowerCase() === "t" && !pickerMode) {
|
|
561
|
+
setShowThinking((current) => {
|
|
562
|
+
const next = !current;
|
|
563
|
+
addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
|
|
564
|
+
return next;
|
|
565
|
+
});
|
|
437
566
|
return;
|
|
567
|
+
}
|
|
438
568
|
if (key.ctrl && input === "o" && !pickerMode) {
|
|
439
569
|
setVerboseTrace((v) => !v);
|
|
440
570
|
return;
|
|
@@ -481,6 +611,23 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
481
611
|
const addMessage = useCallback((role, content) => {
|
|
482
612
|
updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
|
|
483
613
|
}, [updateDisplayMessages]);
|
|
614
|
+
useEffect(() => {
|
|
615
|
+
if (!updateNoticeRefresh)
|
|
616
|
+
return;
|
|
617
|
+
let cancelled = false;
|
|
618
|
+
updateNoticeRefresh.then((notice) => {
|
|
619
|
+
if (cancelled || !notice)
|
|
620
|
+
return;
|
|
621
|
+
setCurrentUpdateNotice(notice);
|
|
622
|
+
if (!showWelcomeRef.current)
|
|
623
|
+
addMessage("assistant", notice);
|
|
624
|
+
}).catch(() => {
|
|
625
|
+
// Best-effort update checks should never disturb the session.
|
|
626
|
+
});
|
|
627
|
+
return () => {
|
|
628
|
+
cancelled = true;
|
|
629
|
+
};
|
|
630
|
+
}, [addMessage, updateNoticeRefresh]);
|
|
484
631
|
const clearMessages = useCallback(() => {
|
|
485
632
|
// The transcript lives entirely in React state now (alt-screen viewport,
|
|
486
633
|
// no terminal scrollback) — clearing state clears the screen. Writing
|
|
@@ -494,26 +641,42 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
494
641
|
viewportRef.current?.forceScrollToBottom();
|
|
495
642
|
return key;
|
|
496
643
|
}, [updateDisplayMessages]);
|
|
644
|
+
const prepareSubmitDisplay = useCallback((payload) => {
|
|
645
|
+
if (payload.images.length === 0)
|
|
646
|
+
return payload;
|
|
647
|
+
if (payload.imageDisplayStart !== undefined) {
|
|
648
|
+
nextImageDisplayLabelStartRef.current = Math.max(nextImageDisplayLabelStartRef.current, payload.imageDisplayStart + payload.images.length);
|
|
649
|
+
return payload;
|
|
650
|
+
}
|
|
651
|
+
const imageDisplayStart = nextImageDisplayLabelStartRef.current;
|
|
652
|
+
nextImageDisplayLabelStartRef.current += payload.images.length;
|
|
653
|
+
return { ...payload, imageDisplayStart };
|
|
654
|
+
}, []);
|
|
655
|
+
const submitDisplayText = useCallback((payload) => (formatImageUserDisplayText(payload.displayText ?? payload.text, payload.images.length, payload.imageDisplayStart)), []);
|
|
656
|
+
const currentSessionFile = useCallback(() => sessionManager?.getSessionFile(), [sessionManager]);
|
|
497
657
|
const queueInput = useCallback((payload) => {
|
|
498
|
-
const
|
|
499
|
-
|
|
658
|
+
const preparedPayload = prepareSubmitDisplay(payload);
|
|
659
|
+
const displayKey = addStatusUserMessage(submitDisplayText(preparedPayload), "queued");
|
|
660
|
+
queuedInputsRef.current.push({ payload: preparedPayload, displayKey, sessionFile: currentSessionFile() });
|
|
500
661
|
setQueuedCount(queuedInputsRef.current.length);
|
|
501
|
-
}, [addStatusUserMessage]);
|
|
662
|
+
}, [addStatusUserMessage, currentSessionFile, prepareSubmitDisplay, submitDisplayText]);
|
|
502
663
|
const submitSteer = useCallback((payload) => {
|
|
503
664
|
const controller = inputControllerRef.current;
|
|
504
665
|
if (!controller) {
|
|
505
666
|
queueInput(payload);
|
|
506
667
|
return;
|
|
507
668
|
}
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
|
|
669
|
+
const preparedPayload = prepareSubmitDisplay(payload);
|
|
670
|
+
const displayKey = addStatusUserMessage(submitDisplayText(preparedPayload), "pending_steer");
|
|
671
|
+
const pending = controller.enqueue(preparedPayload.text);
|
|
672
|
+
pendingSteersRef.current.set(pending.id, { displayKey, sessionFile: currentSessionFile() });
|
|
511
673
|
setPendingSteerCount(pendingSteersRef.current.size);
|
|
512
|
-
}, [addStatusUserMessage, queueInput]);
|
|
674
|
+
}, [addStatusUserMessage, currentSessionFile, prepareSubmitDisplay, queueInput, submitDisplayText]);
|
|
513
675
|
const openPicker = useCallback((mode, providerId) => {
|
|
514
676
|
if (mode === "key") {
|
|
515
677
|
setKeyProviderId(providerId ?? null);
|
|
516
678
|
}
|
|
679
|
+
setStatsPanel(null);
|
|
517
680
|
setPickerMode(mode);
|
|
518
681
|
}, []);
|
|
519
682
|
const closePicker = useCallback(() => {
|
|
@@ -529,71 +692,151 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
529
692
|
const clearComposerDraft = useCallback(() => {
|
|
530
693
|
setComposerDraft(null);
|
|
531
694
|
}, []);
|
|
695
|
+
const setStartingSubmit = useCallback((fingerprint) => {
|
|
696
|
+
startingSubmitFingerprintRef.current = fingerprint;
|
|
697
|
+
setStartingSubmitFingerprint(fingerprint);
|
|
698
|
+
}, []);
|
|
532
699
|
const openFeedback = useCallback((initialDescription) => {
|
|
533
700
|
const base = collectFeedback(agent, { description: "" });
|
|
534
701
|
const { description: _drop, ...rest } = base;
|
|
535
702
|
setPendingFeedback({ base: rest, initialDescription });
|
|
536
703
|
}, [agent]);
|
|
537
|
-
const
|
|
704
|
+
const sidebarFits = terminalColumns > 120;
|
|
705
|
+
const sidebarVisible = sidebarMode === "expanded" ? sidebarFits : sidebarMode === "auto" && sidebarFits;
|
|
706
|
+
const currentSidebarCommandState = useCallback((mode = sidebarMode) => {
|
|
707
|
+
const visible = mode === "expanded" ? sidebarFits : mode === "auto" && sidebarFits;
|
|
708
|
+
return { mode, visible, active: visible };
|
|
709
|
+
}, [sidebarFits, sidebarMode]);
|
|
710
|
+
const toggleSidebar = useCallback(() => {
|
|
711
|
+
const next = sidebarVisible ? "collapsed" : "expanded";
|
|
712
|
+
setSidebarMode(next);
|
|
713
|
+
return currentSidebarCommandState(next);
|
|
714
|
+
}, [currentSidebarCommandState, sidebarVisible]);
|
|
715
|
+
const applySidebarMode = useCallback((mode) => {
|
|
716
|
+
setSidebarMode(mode);
|
|
717
|
+
return currentSidebarCommandState(mode);
|
|
718
|
+
}, [currentSidebarCommandState]);
|
|
719
|
+
const openSessionPicker = useCallback(() => {
|
|
720
|
+
if (activeAbortRef.current) {
|
|
721
|
+
addMessage("error", "Stop the current run before switching sessions.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
setStatsPanel(null);
|
|
725
|
+
setPickerMode("session");
|
|
726
|
+
}, [addMessage]);
|
|
727
|
+
const openRewindPicker = useCallback(() => {
|
|
728
|
+
if (!sessionManager) {
|
|
729
|
+
addMessage("error", "Rewind requires an active session.");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (activeAbortRef.current) {
|
|
733
|
+
addMessage("error", "Stop the current run before rewinding.");
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
setStatsPanel(null);
|
|
737
|
+
setPickerMode("rewind");
|
|
738
|
+
}, [addMessage, sessionManager]);
|
|
739
|
+
const openStatsPanel = useCallback(() => {
|
|
740
|
+
setPickerMode(null);
|
|
741
|
+
setStatsPanel({
|
|
742
|
+
range: "30d",
|
|
743
|
+
bundle: collectUsageStatsBundle(),
|
|
744
|
+
});
|
|
745
|
+
}, []);
|
|
746
|
+
const closeStatsPanel = useCallback(() => {
|
|
747
|
+
setStatsPanel(null);
|
|
748
|
+
setCursorResetEpoch((epoch) => epoch + 1);
|
|
749
|
+
}, []);
|
|
750
|
+
const handleSessionSelect = useCallback((sessionFile) => {
|
|
751
|
+
if (!switchSession) {
|
|
752
|
+
addMessage("error", "Session switching is not available in this mode.");
|
|
753
|
+
closePicker();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (activeAbortRef.current) {
|
|
757
|
+
addMessage("error", "Stop the current run before switching sessions.");
|
|
758
|
+
closePicker();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const result = switchSession(sessionFile);
|
|
762
|
+
if ("error" in result) {
|
|
763
|
+
addMessage("error", `Failed to switch session: ${result.error}`);
|
|
764
|
+
closePicker();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const queuedDisplayKeys = queuedAndPendingDisplayKeys(queuedInputsRef.current, pendingSteersRef.current.values());
|
|
768
|
+
queuedInputsRef.current = [];
|
|
769
|
+
pendingSteersRef.current.clear();
|
|
770
|
+
inputControllerRef.current = null;
|
|
771
|
+
setQueuedCount(0);
|
|
772
|
+
setPendingSteerCount(0);
|
|
773
|
+
setStartingSubmit(null);
|
|
774
|
+
clearComposerDraft();
|
|
775
|
+
setSessionManager(result.manager);
|
|
776
|
+
setTodos(agent.getTodos());
|
|
777
|
+
updateDisplayMessages(() => [
|
|
778
|
+
...reconstructDisplayMessages(agent.messages).filter((message) => !queuedDisplayKeys.has(message.key ?? "")),
|
|
779
|
+
withMessageKey({ role: "assistant", content: `⤷ Resumed session: ${sessionDisplayName(result.manager)}` }),
|
|
780
|
+
]);
|
|
781
|
+
viewportRef.current?.forceScrollToBottom();
|
|
782
|
+
closePicker();
|
|
783
|
+
}, [addMessage, agent, clearComposerDraft, closePicker, setStartingSubmit, switchSession, updateDisplayMessages]);
|
|
784
|
+
const handleModelSelect = useCallback((model, selectedThinkingLevel) => {
|
|
785
|
+
const run = async () => {
|
|
786
|
+
const nextThinkingLevel = await switchAgentModel({
|
|
787
|
+
model,
|
|
788
|
+
agent,
|
|
789
|
+
registry: safeRegistry,
|
|
790
|
+
createProvider,
|
|
791
|
+
workingDir: args.cwd,
|
|
792
|
+
systemPromptOptions: agent.getSystemPromptToolOptions(),
|
|
793
|
+
thinkingLevel: selectedThinkingLevel,
|
|
794
|
+
rememberModel: (nextModel) => userConfig.pushRecentModel(nextModel),
|
|
795
|
+
setThinkingLevel,
|
|
796
|
+
sessionManager,
|
|
797
|
+
});
|
|
798
|
+
const effortNote = nextThinkingLevel && nextThinkingLevel !== "off"
|
|
799
|
+
? ` with ${nextThinkingLevel} effort`
|
|
800
|
+
: "";
|
|
801
|
+
addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}.`);
|
|
802
|
+
closePicker();
|
|
803
|
+
return nextThinkingLevel;
|
|
804
|
+
};
|
|
805
|
+
void run().catch((error) => {
|
|
806
|
+
addMessage("error", formatModelSwitchError(model, error));
|
|
807
|
+
closePicker();
|
|
808
|
+
});
|
|
809
|
+
}, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
|
|
810
|
+
const handleProviderSelect = useCallback((providerId) => {
|
|
538
811
|
const run = async () => {
|
|
539
|
-
agent.model = model;
|
|
540
|
-
const decoded = model.includes(":")
|
|
541
|
-
? model.split(":")
|
|
542
|
-
: [agent.providerId || safeRegistry.getDefault()?.id || "openai", model];
|
|
543
|
-
const providerId = decoded[0];
|
|
544
812
|
await safeRegistry.prepareProvider(providerId);
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
813
|
+
const configured = safeRegistry.getConfigured();
|
|
814
|
+
const p = configured.find((x) => x.id === providerId);
|
|
815
|
+
const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
|
|
816
|
+
if (!p && !builtin) {
|
|
817
|
+
addMessage("error", `Provider ${providerId} not found.`);
|
|
548
818
|
closePicker();
|
|
549
819
|
return;
|
|
550
820
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
821
|
+
if (!p?.apiKey) {
|
|
822
|
+
if (!p && builtin) {
|
|
823
|
+
safeRegistry.addProvider(providerId, "");
|
|
824
|
+
}
|
|
825
|
+
safeRegistry.setDefault(providerId);
|
|
826
|
+
setKeyProviderId(providerId);
|
|
827
|
+
setPickerMode("key");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
safeRegistry.setDefault(providerId);
|
|
831
|
+
agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
|
|
554
832
|
agent.providerId = providerId;
|
|
555
|
-
|
|
556
|
-
agentName: "Bubble",
|
|
557
|
-
configuredProvider: providerId,
|
|
558
|
-
configuredModel: displayModel(model),
|
|
559
|
-
configuredModelId: model,
|
|
560
|
-
thinkingLevel: agent.thinking,
|
|
561
|
-
workingDir: args.cwd,
|
|
562
|
-
...agent.getSystemPromptToolOptions(),
|
|
563
|
-
}));
|
|
564
|
-
userConfig.pushRecentModel(model);
|
|
565
|
-
setThinkingLevel(agent.thinking);
|
|
566
|
-
sessionManager?.updateMetadata({ model, thinkingLevel: agent.thinking, reasoningEffort: agent.thinking });
|
|
567
|
-
sessionManager?.appendMarker("model_switch", model);
|
|
568
|
-
addMessage("assistant", `Model switched to ${displayModel(model)}.`);
|
|
833
|
+
addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
|
|
569
834
|
closePicker();
|
|
570
835
|
};
|
|
571
|
-
void run()
|
|
572
|
-
|
|
573
|
-
const handleProviderSelect = useCallback(async (providerId) => {
|
|
574
|
-
await safeRegistry.prepareProvider(providerId);
|
|
575
|
-
const configured = safeRegistry.getConfigured();
|
|
576
|
-
const p = configured.find((x) => x.id === providerId);
|
|
577
|
-
const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
|
|
578
|
-
if (!p && !builtin) {
|
|
579
|
-
addMessage("error", `Provider ${providerId} not found.`);
|
|
836
|
+
void run().catch((error) => {
|
|
837
|
+
addMessage("error", `Failed to switch provider ${providerId}: ${errorMessage(error)}`);
|
|
580
838
|
closePicker();
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
if (!p?.apiKey) {
|
|
584
|
-
if (!p && builtin) {
|
|
585
|
-
safeRegistry.addProvider(providerId, "");
|
|
586
|
-
}
|
|
587
|
-
safeRegistry.setDefault(providerId);
|
|
588
|
-
setKeyProviderId(providerId);
|
|
589
|
-
setPickerMode("key");
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
safeRegistry.setDefault(providerId);
|
|
593
|
-
agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
|
|
594
|
-
agent.providerId = providerId;
|
|
595
|
-
addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
|
|
596
|
-
closePicker();
|
|
839
|
+
});
|
|
597
840
|
}, [addMessage, agent, closePicker, createProvider, safeRegistry]);
|
|
598
841
|
const handleProviderAddSelect = useCallback((providerId) => {
|
|
599
842
|
const ok = safeRegistry.addProvider(providerId, "");
|
|
@@ -620,7 +863,10 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
620
863
|
throw new Error("Provider creation not available");
|
|
621
864
|
}),
|
|
622
865
|
openPicker,
|
|
866
|
+
openSessionPicker,
|
|
867
|
+
openRewindPicker,
|
|
623
868
|
openFeedback,
|
|
869
|
+
fillComposer,
|
|
624
870
|
registry: safeRegistry,
|
|
625
871
|
skillRegistry: safeSkillRegistry,
|
|
626
872
|
bashAllowlist,
|
|
@@ -635,11 +881,12 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
635
881
|
getThemeMode: () => themeMode,
|
|
636
882
|
getResolvedTheme: () => themeResolved,
|
|
637
883
|
setThemeMode: applyThemeMode,
|
|
884
|
+
openStats: openStatsPanel,
|
|
638
885
|
});
|
|
639
886
|
if (handled && result) {
|
|
640
887
|
addMessage("assistant", result);
|
|
641
888
|
}
|
|
642
|
-
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
889
|
+
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, fillComposer, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, safeRegistry, sessionManager]);
|
|
643
890
|
const handleLogoutProviderSelect = useCallback(async (providerId) => {
|
|
644
891
|
closePicker();
|
|
645
892
|
const command = `/logout ${providerId}`;
|
|
@@ -654,7 +901,10 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
654
901
|
throw new Error("Provider creation not available");
|
|
655
902
|
}),
|
|
656
903
|
openPicker,
|
|
904
|
+
openSessionPicker,
|
|
905
|
+
openRewindPicker,
|
|
657
906
|
openFeedback,
|
|
907
|
+
fillComposer,
|
|
658
908
|
registry: safeRegistry,
|
|
659
909
|
skillRegistry: safeSkillRegistry,
|
|
660
910
|
bashAllowlist,
|
|
@@ -669,11 +919,12 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
669
919
|
getThemeMode: () => themeMode,
|
|
670
920
|
getResolvedTheme: () => themeResolved,
|
|
671
921
|
setThemeMode: applyThemeMode,
|
|
922
|
+
openStats: openStatsPanel,
|
|
672
923
|
});
|
|
673
924
|
if (handled && result) {
|
|
674
925
|
addMessage("assistant", result);
|
|
675
926
|
}
|
|
676
|
-
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
927
|
+
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, fillComposer, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, safeRegistry, sessionManager]);
|
|
677
928
|
const handleKeySubmit = useCallback((key) => {
|
|
678
929
|
const targetId = keyProviderId || safeRegistry.getDefault()?.id;
|
|
679
930
|
if (!targetId) {
|
|
@@ -693,16 +944,15 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
693
944
|
setKeyProviderId(null);
|
|
694
945
|
}, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry]);
|
|
695
946
|
const handleSubmit = useCallback(async (payload) => {
|
|
696
|
-
const
|
|
697
|
-
const input =
|
|
698
|
-
const displayInput =
|
|
699
|
-
const images =
|
|
947
|
+
const initialPayload = typeof payload === "string" ? { text: payload, images: [] } : payload;
|
|
948
|
+
const input = initialPayload.text;
|
|
949
|
+
const displayInput = initialPayload.displayText ?? input;
|
|
950
|
+
const images = initialPayload.images;
|
|
700
951
|
if (!input.trim() && images.length === 0)
|
|
701
952
|
return;
|
|
702
953
|
// Agent already running: route the submit into the live run instead of
|
|
703
954
|
// starting a new one. Plain prose steers the current turn; slash
|
|
704
|
-
// commands, @-mentions and image payloads queue for the next turn
|
|
705
|
-
// (mirrors the OpenTUI boundary-steer eligibility rules).
|
|
955
|
+
// commands, @-mentions and image payloads queue for the next turn.
|
|
706
956
|
if (activeAbortRef.current) {
|
|
707
957
|
if (/^\/(?:quit|exit)\s*$/.test(input.trim())) {
|
|
708
958
|
requestExit();
|
|
@@ -712,435 +962,641 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
712
962
|
!input.includes("@") &&
|
|
713
963
|
images.length === 0;
|
|
714
964
|
if (steerEligible) {
|
|
715
|
-
submitSteer(
|
|
965
|
+
submitSteer(initialPayload);
|
|
716
966
|
}
|
|
717
967
|
else {
|
|
718
|
-
queueInput(
|
|
968
|
+
queueInput(initialPayload);
|
|
719
969
|
}
|
|
720
970
|
return;
|
|
721
971
|
}
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
// transcript back to the bottom even if the user had scrolled up.
|
|
744
|
-
viewportRef.current?.forceScrollToBottom();
|
|
745
|
-
setIsRunning(true);
|
|
746
|
-
runStartRef.current = Date.now();
|
|
747
|
-
setStreamingContent("");
|
|
748
|
-
setStreamingReasoning("");
|
|
749
|
-
setStreamingTools([]);
|
|
750
|
-
setStreamingParts([]);
|
|
751
|
-
let assistantContent = "";
|
|
752
|
-
let assistantReasoning = "";
|
|
753
|
-
const toolCalls = [];
|
|
754
|
-
const assistantParts = [];
|
|
755
|
-
const abortController = new AbortController();
|
|
756
|
-
activeAbortRef.current = abortController;
|
|
757
|
-
const inputController = new AgentRunInputQueue(`run-${++nextRunIdRef.current}`);
|
|
758
|
-
inputControllerRef.current = inputController;
|
|
759
|
-
const syncStreamingParts = () => {
|
|
760
|
-
setStreamingParts(snapshotDisplayParts(assistantParts));
|
|
761
|
-
};
|
|
762
|
-
// Text/reasoning deltas arrive far faster than the screen needs to
|
|
763
|
-
// update; batch them so the full-frame re-render runs at most every
|
|
764
|
-
// STREAMING_FLUSH_INTERVAL_MS. Tool events stay immediate.
|
|
765
|
-
let streamingFlushTimer = null;
|
|
766
|
-
const cancelStreamingFlush = () => {
|
|
767
|
-
if (streamingFlushTimer !== null) {
|
|
768
|
-
clearTimeout(streamingFlushTimer);
|
|
769
|
-
streamingFlushTimer = null;
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
const scheduleStreamingFlush = () => {
|
|
773
|
-
if (streamingFlushTimer !== null)
|
|
774
|
-
return;
|
|
775
|
-
streamingFlushTimer = setTimeout(() => {
|
|
776
|
-
streamingFlushTimer = null;
|
|
777
|
-
setStreamingContent(assistantContent);
|
|
778
|
-
setStreamingReasoning(assistantReasoning);
|
|
779
|
-
syncStreamingParts();
|
|
780
|
-
}, STREAMING_FLUSH_INTERVAL_MS);
|
|
781
|
-
};
|
|
782
|
-
const hasAssistantOutput = () => (!!assistantContent ||
|
|
783
|
-
!!assistantReasoning ||
|
|
784
|
-
toolCalls.length > 0 ||
|
|
785
|
-
assistantParts.length > 0);
|
|
786
|
-
const commitAssistantMessage = (taskElapsedMs) => {
|
|
787
|
-
if (!hasAssistantOutput())
|
|
972
|
+
const submitFingerprint = submitPayloadFingerprint(initialPayload);
|
|
973
|
+
const startingDecision = decideStartingSubmitFingerprint(startingSubmitFingerprintRef.current, submitFingerprint);
|
|
974
|
+
if (startingDecision === "ignore")
|
|
975
|
+
return;
|
|
976
|
+
if (startingDecision === "queue") {
|
|
977
|
+
queueInput(initialPayload);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const normalized = prepareSubmitDisplay(initialPayload);
|
|
981
|
+
setStartingSubmit(submitFingerprint);
|
|
982
|
+
try {
|
|
983
|
+
const runAgentInput = async (actualInput, displayInput, attachedImages = [], runOptions = {}) => {
|
|
984
|
+
const runSessionFile = currentSessionFile();
|
|
985
|
+
const activeProviderId = agent.providerId || safeRegistry.getDefault()?.id;
|
|
986
|
+
const hasActiveProvider = !!activeProviderId && safeRegistry.getEnabled().some((provider) => provider.id === activeProviderId);
|
|
987
|
+
if (!hasActiveProvider) {
|
|
988
|
+
addMessage("error", "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.");
|
|
989
|
+
if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
|
|
990
|
+
goalStore.pause();
|
|
991
|
+
addMessage("assistant", stopReasonNotice("error"));
|
|
992
|
+
}
|
|
788
993
|
return;
|
|
789
|
-
const currentParts = snapshotDisplayParts(assistantParts);
|
|
790
|
-
const currentToolCalls = [...toolCalls];
|
|
791
|
-
const partContent = assistantContent || contentFromParts(currentParts);
|
|
792
|
-
const partToolCalls = currentToolCalls.length > 0
|
|
793
|
-
? currentToolCalls
|
|
794
|
-
: toolCallsFromParts(currentParts);
|
|
795
|
-
const msg = {
|
|
796
|
-
key: nextDisplayMessageKey("asst"),
|
|
797
|
-
role: "assistant",
|
|
798
|
-
content: partContent,
|
|
799
|
-
};
|
|
800
|
-
if (assistantReasoning) {
|
|
801
|
-
msg.reasoning = assistantReasoning;
|
|
802
994
|
}
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
995
|
+
if (!agent.model) {
|
|
996
|
+
addMessage("error", "No model selected. Use /model after /login or provider setup.");
|
|
997
|
+
if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
|
|
998
|
+
goalStore.pause();
|
|
999
|
+
addMessage("assistant", stopReasonNotice("error"));
|
|
1000
|
+
}
|
|
1001
|
+
return;
|
|
808
1002
|
}
|
|
809
|
-
|
|
810
|
-
|
|
1003
|
+
const displayContent = formatImageUserDisplayText(displayInput, attachedImages.length, runOptions.imageDisplayStart);
|
|
1004
|
+
if (!runOptions.hidden) {
|
|
1005
|
+
updateDisplayMessages((prev) => [
|
|
1006
|
+
...prev,
|
|
1007
|
+
withMessageKey({ role: "user", content: displayContent }),
|
|
1008
|
+
]);
|
|
1009
|
+
// Sending is an explicit "watch the newest turn" intent: snap the
|
|
1010
|
+
// transcript back to the bottom even if the user had scrolled up.
|
|
1011
|
+
viewportRef.current?.forceScrollToBottom();
|
|
811
1012
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const clearAssistantStream = () => {
|
|
815
|
-
// A timer firing after this reset would resurrect the just-committed
|
|
816
|
-
// text as a phantom streaming block — cancel before clearing.
|
|
817
|
-
cancelStreamingFlush();
|
|
1013
|
+
setIsRunning(true);
|
|
1014
|
+
runStartRef.current = Date.now();
|
|
818
1015
|
setStreamingContent("");
|
|
819
1016
|
setStreamingReasoning("");
|
|
820
1017
|
setStreamingTools([]);
|
|
821
1018
|
setStreamingParts([]);
|
|
822
|
-
assistantContent = "";
|
|
823
|
-
assistantReasoning = "";
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1019
|
+
let assistantContent = "";
|
|
1020
|
+
let assistantReasoning = "";
|
|
1021
|
+
let goalRunTokens = 0;
|
|
1022
|
+
let goalRunUsageReported = false;
|
|
1023
|
+
let runCancelled = false;
|
|
1024
|
+
let runErrored = false;
|
|
1025
|
+
const toolCalls = [];
|
|
1026
|
+
const assistantParts = [];
|
|
1027
|
+
const abortController = new AbortController();
|
|
1028
|
+
activeAbortRef.current = abortController;
|
|
1029
|
+
setStartingSubmit(null);
|
|
1030
|
+
const inputController = new AgentRunInputQueue(`run-${++nextRunIdRef.current}`);
|
|
1031
|
+
inputControllerRef.current = inputController;
|
|
1032
|
+
const syncStreamingParts = () => {
|
|
1033
|
+
setStreamingParts(snapshotDisplayParts(assistantParts));
|
|
1034
|
+
};
|
|
1035
|
+
// Text/reasoning deltas arrive far faster than the screen needs to
|
|
1036
|
+
// update; batch them so the full-frame re-render runs at most every
|
|
1037
|
+
// STREAMING_FLUSH_INTERVAL_MS. Tool events stay immediate.
|
|
1038
|
+
let streamingFlushTimer = null;
|
|
1039
|
+
const cancelStreamingFlush = () => {
|
|
1040
|
+
if (streamingFlushTimer !== null) {
|
|
1041
|
+
clearTimeout(streamingFlushTimer);
|
|
1042
|
+
streamingFlushTimer = null;
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
const scheduleStreamingFlush = () => {
|
|
1046
|
+
if (streamingFlushTimer !== null)
|
|
1047
|
+
return;
|
|
1048
|
+
streamingFlushTimer = setTimeout(() => {
|
|
1049
|
+
streamingFlushTimer = null;
|
|
1050
|
+
setStreamingContent(assistantContent);
|
|
1051
|
+
setStreamingReasoning(assistantReasoning);
|
|
1052
|
+
syncStreamingParts();
|
|
1053
|
+
}, STREAMING_FLUSH_INTERVAL_MS);
|
|
1054
|
+
};
|
|
1055
|
+
const hasAssistantOutput = () => (!!assistantContent ||
|
|
1056
|
+
!!assistantReasoning ||
|
|
1057
|
+
toolCalls.length > 0 ||
|
|
1058
|
+
assistantParts.length > 0);
|
|
1059
|
+
const commitAssistantMessage = (taskElapsedMs) => {
|
|
1060
|
+
if (!hasAssistantOutput())
|
|
1061
|
+
return;
|
|
1062
|
+
const currentParts = snapshotDisplayParts(assistantParts);
|
|
1063
|
+
const currentToolCalls = [...toolCalls];
|
|
1064
|
+
const partContent = assistantContent || contentFromParts(currentParts);
|
|
1065
|
+
const partToolCalls = currentToolCalls.length > 0
|
|
1066
|
+
? currentToolCalls
|
|
1067
|
+
: toolCallsFromParts(currentParts);
|
|
1068
|
+
const msg = {
|
|
1069
|
+
key: nextDisplayMessageKey("asst"),
|
|
1070
|
+
role: "assistant",
|
|
1071
|
+
content: partContent,
|
|
1072
|
+
};
|
|
1073
|
+
if (assistantReasoning) {
|
|
1074
|
+
msg.reasoning = assistantReasoning;
|
|
1075
|
+
}
|
|
1076
|
+
if (partToolCalls.length > 0) {
|
|
1077
|
+
msg.toolCalls = partToolCalls;
|
|
1078
|
+
}
|
|
1079
|
+
if (currentParts.length > 0) {
|
|
1080
|
+
msg.parts = currentParts;
|
|
1081
|
+
}
|
|
1082
|
+
if (taskElapsedMs !== undefined && Number.isFinite(taskElapsedMs) && taskElapsedMs > 0) {
|
|
1083
|
+
msg.taskElapsedMs = taskElapsedMs;
|
|
1084
|
+
}
|
|
1085
|
+
updateDisplayMessages((prev) => [...prev, msg]);
|
|
1086
|
+
};
|
|
1087
|
+
const clearAssistantStream = () => {
|
|
1088
|
+
// A timer firing after this reset would resurrect the just-committed
|
|
1089
|
+
// text as a phantom streaming block — cancel before clearing.
|
|
1090
|
+
cancelStreamingFlush();
|
|
1091
|
+
setStreamingContent("");
|
|
1092
|
+
setStreamingReasoning("");
|
|
1093
|
+
setStreamingTools([]);
|
|
1094
|
+
setStreamingParts([]);
|
|
1095
|
+
assistantContent = "";
|
|
1096
|
+
assistantReasoning = "";
|
|
1097
|
+
toolCalls.length = 0;
|
|
1098
|
+
assistantParts.length = 0;
|
|
1099
|
+
};
|
|
1100
|
+
try {
|
|
1101
|
+
for await (const event of agent.run(actualInput, args.cwd, {
|
|
1102
|
+
abortSignal: abortController.signal,
|
|
1103
|
+
inputController,
|
|
1104
|
+
})) {
|
|
1105
|
+
switch (event.type) {
|
|
1106
|
+
case "text_delta":
|
|
1107
|
+
assistantContent += event.content;
|
|
1108
|
+
appendTextPart(assistantParts, event.content);
|
|
1109
|
+
scheduleStreamingFlush();
|
|
1110
|
+
break;
|
|
1111
|
+
case "reasoning_delta":
|
|
1112
|
+
assistantReasoning += event.content;
|
|
1113
|
+
scheduleStreamingFlush();
|
|
1114
|
+
break;
|
|
1115
|
+
case "tool_call_start": {
|
|
1116
|
+
// The LLM has begun emitting this tool call. Args are still
|
|
1117
|
+
// streaming — render an empty-args placeholder so the user
|
|
1118
|
+
// sees the tool the moment it appears in the assistant
|
|
1119
|
+
// response, not after the full arg payload finishes.
|
|
1120
|
+
if (!toolCalls.some((t) => t.id === event.id)) {
|
|
1121
|
+
const toolCall = {
|
|
1122
|
+
id: event.id,
|
|
1123
|
+
name: event.name,
|
|
1124
|
+
args: {},
|
|
1125
|
+
startedAt: Date.now(),
|
|
1126
|
+
};
|
|
1127
|
+
toolCalls.push(toolCall);
|
|
1128
|
+
appendToolPart(assistantParts, toolCall);
|
|
1129
|
+
setStreamingTools([...toolCalls]);
|
|
1130
|
+
syncStreamingParts();
|
|
1131
|
+
}
|
|
1132
|
+
break;
|
|
858
1133
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1134
|
+
case "tool_call_delta": {
|
|
1135
|
+
// Best-effort parse of the partial argument JSON to extract
|
|
1136
|
+
// identifying fields (path, command, content, …). The buffer
|
|
1137
|
+
// is incomplete JSON during streaming, so fall back to regex
|
|
1138
|
+
// peeks on common string fields.
|
|
1139
|
+
const tc = toolCalls.find((t) => t.id === event.id);
|
|
1140
|
+
if (tc) {
|
|
1141
|
+
tc.args = parsePartialArgs(event.arguments, tc.args);
|
|
1142
|
+
setStreamingTools([...toolCalls]);
|
|
1143
|
+
syncStreamingParts();
|
|
1144
|
+
}
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
case "tool_call_end": {
|
|
1148
|
+
// Provider signaled args streaming is complete; agent will
|
|
1149
|
+
// emit tool_start next. We don't need to do anything visual
|
|
1150
|
+
// here — the placeholder is already in place and tool_start
|
|
1151
|
+
// will refresh it with the canonical parsed args.
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
case "tool_start": {
|
|
1155
|
+
// Tool is about to execute. Upgrade the placeholder created
|
|
1156
|
+
// by tool_call_start (or append if upstream skipped the
|
|
1157
|
+
// streaming path).
|
|
1158
|
+
const existing = toolCalls.find((t) => t.id === event.id);
|
|
1159
|
+
if (existing) {
|
|
1160
|
+
existing.args = event.args;
|
|
1161
|
+
existing.startedAt = existing.startedAt ?? Date.now();
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
const toolCall = {
|
|
1165
|
+
id: event.id,
|
|
1166
|
+
name: event.name,
|
|
1167
|
+
args: event.args,
|
|
1168
|
+
startedAt: Date.now(),
|
|
1169
|
+
};
|
|
1170
|
+
toolCalls.push(toolCall);
|
|
1171
|
+
appendToolPart(assistantParts, toolCall);
|
|
1172
|
+
}
|
|
869
1173
|
setStreamingTools([...toolCalls]);
|
|
870
1174
|
syncStreamingParts();
|
|
1175
|
+
break;
|
|
871
1176
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
// Tool is about to execute. Upgrade the placeholder created
|
|
883
|
-
// by tool_call_start (or append if upstream skipped the
|
|
884
|
-
// streaming path).
|
|
885
|
-
const existing = toolCalls.find((t) => t.id === event.id);
|
|
886
|
-
if (existing) {
|
|
887
|
-
existing.args = event.args;
|
|
888
|
-
existing.startedAt = existing.startedAt ?? Date.now();
|
|
1177
|
+
case "tool_end": {
|
|
1178
|
+
const tc = toolCalls.find((t) => t.id === event.id);
|
|
1179
|
+
if (tc) {
|
|
1180
|
+
tc.result = event.result.content;
|
|
1181
|
+
tc.isError = event.result.isError;
|
|
1182
|
+
tc.metadata = event.result.metadata;
|
|
1183
|
+
setStreamingTools([...toolCalls]);
|
|
1184
|
+
syncStreamingParts();
|
|
1185
|
+
}
|
|
1186
|
+
break;
|
|
889
1187
|
}
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1188
|
+
case "tool_update": {
|
|
1189
|
+
const tc = toolCalls.find((t) => t.id === event.id);
|
|
1190
|
+
if (tc) {
|
|
1191
|
+
tc.metadata = mergeToolMetadata(tc.metadata, event.update.metadata);
|
|
1192
|
+
if (event.update.message) {
|
|
1193
|
+
tc.result = event.update.message;
|
|
1194
|
+
}
|
|
1195
|
+
tc.isError = event.update.status === "failed"
|
|
1196
|
+
|| event.update.status === "blocked"
|
|
1197
|
+
|| event.update.status === "cancelled";
|
|
1198
|
+
setStreamingTools([...toolCalls]);
|
|
1199
|
+
syncStreamingParts();
|
|
1200
|
+
}
|
|
1201
|
+
break;
|
|
899
1202
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
case "tool_end": {
|
|
905
|
-
const tc = toolCalls.find((t) => t.id === event.id);
|
|
906
|
-
if (tc) {
|
|
907
|
-
tc.result = event.result.content;
|
|
908
|
-
tc.isError = event.result.isError;
|
|
909
|
-
tc.metadata = event.result.metadata;
|
|
910
|
-
setStreamingTools([...toolCalls]);
|
|
911
|
-
syncStreamingParts();
|
|
1203
|
+
case "todos_updated": {
|
|
1204
|
+
setTodos(event.todos);
|
|
1205
|
+
break;
|
|
912
1206
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1207
|
+
case "mode_changed": {
|
|
1208
|
+
setPermissionMode(event.mode);
|
|
1209
|
+
sessionManager?.appendMarker("mode_switch", event.mode);
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
case "input_applied": {
|
|
1213
|
+
// The steer joined the current turn at the next model-call
|
|
1214
|
+
// boundary. Move it after the just-finished tool/assistant
|
|
1215
|
+
// turn instead of clearing the badge in its original
|
|
1216
|
+
// placeholder position.
|
|
1217
|
+
const steer = pendingSteersRef.current.get(event.id);
|
|
1218
|
+
if (steer) {
|
|
1219
|
+
pendingSteersRef.current.delete(event.id);
|
|
1220
|
+
setPendingSteerCount(pendingSteersRef.current.size);
|
|
1221
|
+
updateDisplayMessages((prev) => moveStatusMessageToEnd(prev, steer.displayKey));
|
|
921
1222
|
}
|
|
922
|
-
|
|
923
|
-
|| event.update.status === "blocked"
|
|
924
|
-
|| event.update.status === "cancelled";
|
|
925
|
-
setStreamingTools([...toolCalls]);
|
|
926
|
-
syncStreamingParts();
|
|
1223
|
+
break;
|
|
927
1224
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
pendingSteersRef.current.delete(event.id);
|
|
945
|
-
setPendingSteerCount(pendingSteersRef.current.size);
|
|
946
|
-
updateDisplayMessages((prev) => prev.map((message) => message.key === steer.displayKey ? setUserInputStatus(message) : message));
|
|
1225
|
+
case "input_rejected": {
|
|
1226
|
+
// No model continuation left in this run: the steer moves to
|
|
1227
|
+
// the next turn's queue, badge flips to QUEUED.
|
|
1228
|
+
const steer = pendingSteersRef.current.get(event.id);
|
|
1229
|
+
if (steer) {
|
|
1230
|
+
pendingSteersRef.current.delete(event.id);
|
|
1231
|
+
setPendingSteerCount(pendingSteersRef.current.size);
|
|
1232
|
+
updateDisplayMessages((prev) => prev.map((message) => message.key === steer.displayKey ? setUserInputStatus(message, "queued") : message));
|
|
1233
|
+
queuedInputsRef.current.push({
|
|
1234
|
+
payload: { text: event.content, images: [] },
|
|
1235
|
+
displayKey: steer.displayKey,
|
|
1236
|
+
sessionFile: steer.sessionFile ?? runSessionFile,
|
|
1237
|
+
});
|
|
1238
|
+
setQueuedCount(queuedInputsRef.current.length);
|
|
1239
|
+
}
|
|
1240
|
+
break;
|
|
947
1241
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if (steer) {
|
|
955
|
-
pendingSteersRef.current.delete(event.id);
|
|
956
|
-
setPendingSteerCount(pendingSteersRef.current.size);
|
|
957
|
-
updateDisplayMessages((prev) => prev.map((message) => message.key === steer.displayKey ? setUserInputStatus(message, "queued") : message));
|
|
958
|
-
queuedInputsRef.current.push({
|
|
959
|
-
payload: { text: event.content, images: [] },
|
|
960
|
-
displayKey: steer.displayKey,
|
|
961
|
-
});
|
|
962
|
-
setQueuedCount(queuedInputsRef.current.length);
|
|
1242
|
+
case "input_pending_changed": {
|
|
1243
|
+
if (event.pending === 0 && pendingSteersRef.current.size > 0) {
|
|
1244
|
+
pendingSteersRef.current.clear();
|
|
1245
|
+
}
|
|
1246
|
+
setPendingSteerCount(event.pending === 0 ? 0 : event.pending);
|
|
1247
|
+
break;
|
|
963
1248
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1249
|
+
case "turn_end": {
|
|
1250
|
+
if (event.usage) {
|
|
1251
|
+
goalRunUsageReported = true;
|
|
1252
|
+
goalRunTokens += tokenUsageTotal(event.usage);
|
|
1253
|
+
}
|
|
1254
|
+
if (event.willContinue) {
|
|
1255
|
+
commitAssistantMessage();
|
|
1256
|
+
clearAssistantStream();
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
commitAssistantMessage(runStartRef.current ? Date.now() - runStartRef.current : undefined);
|
|
1260
|
+
clearAssistantStream();
|
|
1261
|
+
break;
|
|
969
1262
|
}
|
|
970
|
-
setPendingSteerCount(event.pending === 0 ? 0 : event.pending);
|
|
971
|
-
break;
|
|
972
1263
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
catch (err) {
|
|
1267
|
+
commitAssistantMessage();
|
|
1268
|
+
if (err instanceof AgentAbortError || err?.name === "AbortError") {
|
|
1269
|
+
runCancelled = true;
|
|
1270
|
+
updateDisplayMessages(() => reconstructDisplayMessages(agent.messages));
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
runErrored = true;
|
|
1274
|
+
updateDisplayMessages((prev) => [
|
|
1275
|
+
...prev,
|
|
1276
|
+
withMessageKey({ role: "error", content: err.message }),
|
|
1277
|
+
]);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
cancelStreamingFlush();
|
|
1282
|
+
// Leftover steers that never reached a model-call boundary: drop
|
|
1283
|
+
// them on cancel (the user asked the run to stop); requeue them for
|
|
1284
|
+
// the next turn on a normal end.
|
|
1285
|
+
const cancelled = abortController.signal.aborted;
|
|
1286
|
+
if (cancelled)
|
|
1287
|
+
runCancelled = true;
|
|
1288
|
+
for (const leftover of inputController.clear()) {
|
|
1289
|
+
const steer = pendingSteersRef.current.get(leftover.id);
|
|
1290
|
+
pendingSteersRef.current.delete(leftover.id);
|
|
1291
|
+
if (cancelled) {
|
|
1292
|
+
if (steer) {
|
|
1293
|
+
updateDisplayMessages((prev) => prev.filter((message) => message.key !== steer.displayKey));
|
|
977
1294
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (steer) {
|
|
1298
|
+
updateDisplayMessages((prev) => prev.map((message) => message.key === steer.displayKey ? setUserInputStatus(message, "queued") : message));
|
|
981
1299
|
}
|
|
1300
|
+
queuedInputsRef.current.push({
|
|
1301
|
+
payload: { text: leftover.content, images: [] },
|
|
1302
|
+
displayKey: steer?.displayKey,
|
|
1303
|
+
sessionFile: steer?.sessionFile ?? runSessionFile,
|
|
1304
|
+
});
|
|
982
1305
|
}
|
|
1306
|
+
setPendingSteerCount(0);
|
|
1307
|
+
setQueuedCount(queuedInputsRef.current.length);
|
|
1308
|
+
if (inputControllerRef.current === inputController)
|
|
1309
|
+
inputControllerRef.current = null;
|
|
1310
|
+
if (activeAbortRef.current === abortController)
|
|
1311
|
+
activeAbortRef.current = null;
|
|
1312
|
+
setIsRunning(false);
|
|
1313
|
+
runStartRef.current = null;
|
|
1314
|
+
setStreamingContent("");
|
|
1315
|
+
setStreamingReasoning("");
|
|
1316
|
+
setStreamingTools([]);
|
|
1317
|
+
setStreamingParts([]);
|
|
1318
|
+
maybeContinueGoal({
|
|
1319
|
+
runCancelled,
|
|
1320
|
+
runErrored,
|
|
1321
|
+
isGoalRun: !!runOptions.goalRun,
|
|
1322
|
+
runTokens: goalRunTokens,
|
|
1323
|
+
usageReported: goalRunUsageReported,
|
|
1324
|
+
});
|
|
983
1325
|
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1326
|
+
};
|
|
1327
|
+
const kickGoalTurn = (prompt, visibleInput) => {
|
|
1328
|
+
if (activeAbortRef.current)
|
|
1329
|
+
return;
|
|
1330
|
+
queueMicrotask(() => {
|
|
1331
|
+
void runAgentInput(prompt, visibleInput ?? "", [], {
|
|
1332
|
+
hidden: visibleInput === undefined,
|
|
1333
|
+
goalRun: true,
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
};
|
|
1337
|
+
function maybeContinueGoal(input) {
|
|
1338
|
+
if (!goalStore || exitRequestedRef.current)
|
|
1339
|
+
return;
|
|
1340
|
+
const current = goalStore.snapshot();
|
|
1341
|
+
if (!current)
|
|
1342
|
+
return;
|
|
1343
|
+
if (input.runCancelled || input.runErrored) {
|
|
1344
|
+
if (current.status === "active") {
|
|
1345
|
+
goalStore.pause();
|
|
1346
|
+
addMessage("assistant", stopReasonNotice(input.runErrored ? "error" : "cancelled"));
|
|
1347
|
+
}
|
|
1348
|
+
return;
|
|
989
1349
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1350
|
+
if (input.isGoalRun) {
|
|
1351
|
+
if (input.usageReported) {
|
|
1352
|
+
if (input.runTokens > 0)
|
|
1353
|
+
goalStore.addTokens(input.runTokens);
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
goalStore.markTokenUsageUnavailable();
|
|
1357
|
+
}
|
|
1358
|
+
goalStore.incrementTurn();
|
|
1359
|
+
}
|
|
1360
|
+
const goal = goalStore.snapshot();
|
|
1361
|
+
const decision = shouldContinueGoal({
|
|
1362
|
+
goal,
|
|
1363
|
+
queuedInputs: queuedInputsRef.current.length,
|
|
1364
|
+
});
|
|
1365
|
+
if (decision.continue) {
|
|
1366
|
+
kickGoalTurn(formatInternalContextBlock("goal", continuationPrompt(goal)));
|
|
1367
|
+
return;
|
|
995
1368
|
}
|
|
1369
|
+
if (decision.reason === "budget" && goal.status === "active") {
|
|
1370
|
+
goalStore.markBudgetLimited();
|
|
1371
|
+
}
|
|
1372
|
+
if (decision.reason === "complete") {
|
|
1373
|
+
addMessage("assistant", goalCompleteNotice(goal));
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const note = stopReasonNotice(decision.reason);
|
|
1377
|
+
if (note)
|
|
1378
|
+
addMessage("assistant", note);
|
|
996
1379
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1380
|
+
const handleGoalCommand = async (goalInput) => {
|
|
1381
|
+
if (!goalStore) {
|
|
1382
|
+
addMessage("error", "Goals are not available in this session.");
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const command = parseGoalCommand(goalInput);
|
|
1386
|
+
if (command.error) {
|
|
1387
|
+
addMessage("error", command.error);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const existing = goalStore.snapshot();
|
|
1391
|
+
switch (command.kind) {
|
|
1392
|
+
case "show": {
|
|
1393
|
+
addMessage("assistant", existing ? goalSummaryText(existing) : "No active goal. Set one with /goal <objective>");
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
case "clear": {
|
|
1397
|
+
if (!existing) {
|
|
1398
|
+
addMessage("assistant", "No active goal to clear");
|
|
1399
|
+
return;
|
|
1009
1400
|
}
|
|
1010
|
-
|
|
1401
|
+
goalStore.clear();
|
|
1402
|
+
addMessage("assistant", "Goal cleared");
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
case "pause": {
|
|
1406
|
+
if (!existing) {
|
|
1407
|
+
addMessage("assistant", "No active goal to pause");
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
goalStore.pause();
|
|
1411
|
+
addMessage("assistant", "Goal paused — /goal resume to continue");
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
case "resume": {
|
|
1415
|
+
if (!existing) {
|
|
1416
|
+
addMessage("assistant", "No goal to resume. Set one with /goal <objective>");
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const resumed = goalStore.resume();
|
|
1420
|
+
if (resumed?.status === "active") {
|
|
1421
|
+
addMessage("assistant", "Goal resumed");
|
|
1422
|
+
kickGoalTurn(formatInternalContextBlock("goal", continuationPrompt(resumed)));
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
addMessage("assistant", "Goal cannot be resumed (already complete)");
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
case "edit": {
|
|
1430
|
+
if (!existing) {
|
|
1431
|
+
addMessage("assistant", "No active goal to edit. Set one with /goal <objective>");
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
goalStore.edit(command.objective);
|
|
1435
|
+
if (command.tokenBudget !== undefined)
|
|
1436
|
+
goalStore.setBudget(command.tokenBudget);
|
|
1437
|
+
addMessage("assistant", `Goal updated: ${truncate(goalStore.snapshot().objective, 60)}`);
|
|
1438
|
+
return;
|
|
1011
1439
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1440
|
+
case "set": {
|
|
1441
|
+
const goal = goalStore.set(command.objective, { tokenBudget: command.tokenBudget });
|
|
1442
|
+
const budgetNote = goal.tokenBudget !== undefined ? ` (budget ${goal.tokenBudget} tok)` : "";
|
|
1443
|
+
addMessage("assistant", `Goal set${budgetNote} — working autonomously. /goal pause to stop.`);
|
|
1444
|
+
kickGoalTurn(formatInternalContextBlock("goal", initialPrompt(goal)), goalInput.trim());
|
|
1445
|
+
return;
|
|
1014
1446
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
// Slash commands and skill invocations drop any attached images —
|
|
1450
|
+
// they're meant for pure command routing.
|
|
1451
|
+
if (displayInput.startsWith("/")) {
|
|
1452
|
+
// Fast-path `/quit` and `/exit` before slash-registry / skill
|
|
1453
|
+
// resolution. This guarantees a literal "/quit" always exits even if
|
|
1454
|
+
// a skill or alias of the same name is later registered. The
|
|
1455
|
+
// canonical handler still lives in slash-commands/commands.ts so
|
|
1456
|
+
// `/help` and the slash menu can list it; both paths end up calling
|
|
1457
|
+
// requestExit().
|
|
1458
|
+
if (/^\/(?:quit|exit)\s*$/.test(input.trim())) {
|
|
1459
|
+
requestExit();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
|
|
1463
|
+
setShowThinking((current) => {
|
|
1464
|
+
const next = !current;
|
|
1465
|
+
addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
|
|
1466
|
+
return next;
|
|
1018
1467
|
});
|
|
1468
|
+
return;
|
|
1019
1469
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
runStartRef.current = null;
|
|
1028
|
-
setStreamingContent("");
|
|
1029
|
-
setStreamingReasoning("");
|
|
1030
|
-
setStreamingTools([]);
|
|
1031
|
-
setStreamingParts([]);
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
// Slash commands and skill invocations drop any attached images —
|
|
1035
|
-
// they're meant for pure command routing.
|
|
1036
|
-
if (displayInput.startsWith("/")) {
|
|
1037
|
-
// Fast-path `/quit` and `/exit` before slash-registry / skill
|
|
1038
|
-
// resolution. This guarantees a literal "/quit" always exits even if
|
|
1039
|
-
// a skill or alias of the same name is later registered. The
|
|
1040
|
-
// canonical handler still lives in slash-commands/commands.ts so
|
|
1041
|
-
// `/help` and the slash menu can list it; both paths end up calling
|
|
1042
|
-
// requestExit().
|
|
1043
|
-
if (/^\/(?:quit|exit)\s*$/.test(input.trim())) {
|
|
1044
|
-
requestExit();
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
|
|
1048
|
-
if (skillInvocation) {
|
|
1049
|
-
await runAgentInput(skillInvocation.actualPrompt, displayInput);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
const { handled, result, inject } = await slashRegistry.execute(input, {
|
|
1053
|
-
agent,
|
|
1054
|
-
addMessage,
|
|
1055
|
-
clearMessages,
|
|
1056
|
-
cwd: args.cwd,
|
|
1057
|
-
exit: () => { requestExit(); },
|
|
1058
|
-
sessionManager,
|
|
1059
|
-
createProvider: createProvider ?? (() => {
|
|
1060
|
-
throw new Error("Provider creation not available");
|
|
1061
|
-
}),
|
|
1062
|
-
openPicker,
|
|
1063
|
-
openFeedback,
|
|
1064
|
-
fillComposer,
|
|
1065
|
-
registry: safeRegistry,
|
|
1066
|
-
skillRegistry: safeSkillRegistry,
|
|
1067
|
-
bashAllowlist,
|
|
1068
|
-
settingsManager,
|
|
1069
|
-
lspService,
|
|
1070
|
-
mcpManager,
|
|
1071
|
-
hookController,
|
|
1072
|
-
flushMemory,
|
|
1073
|
-
runMemoryCompaction,
|
|
1074
|
-
runMemorySummary,
|
|
1075
|
-
runMemoryRefresh,
|
|
1076
|
-
getThemeMode: () => themeMode,
|
|
1077
|
-
getResolvedTheme: () => themeResolved,
|
|
1078
|
-
setThemeMode: applyThemeMode,
|
|
1079
|
-
});
|
|
1080
|
-
if (handled) {
|
|
1081
|
-
if (agent.mode !== permissionMode) {
|
|
1082
|
-
setPermissionMode(agent.mode);
|
|
1470
|
+
if (/^\/(?:trace|verbose|debug)(?:\s|$)/.test(input.trim())) {
|
|
1471
|
+
setVerboseTrace((current) => {
|
|
1472
|
+
const next = !current;
|
|
1473
|
+
addMessage("assistant", next ? "Verbose trace visible" : "Compact trace visible");
|
|
1474
|
+
return next;
|
|
1475
|
+
});
|
|
1476
|
+
return;
|
|
1083
1477
|
}
|
|
1084
|
-
if (
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1478
|
+
if (/^\/write-previews(?:\s|$)/.test(input.trim())) {
|
|
1479
|
+
setExpandedToolOutput((current) => {
|
|
1480
|
+
const next = !current;
|
|
1481
|
+
addMessage("assistant", next ? "Write previews expanded" : "Write previews collapsed");
|
|
1482
|
+
return next;
|
|
1483
|
+
});
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (/^\/goal(?:\s|$)/.test(input.trim())) {
|
|
1487
|
+
await handleGoalCommand(input);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
|
|
1491
|
+
if (skillInvocation) {
|
|
1492
|
+
await runAgentInput(skillInvocation.actualPrompt, displayInput);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const { handled, result, inject } = await slashRegistry.execute(input, {
|
|
1496
|
+
agent,
|
|
1497
|
+
addMessage,
|
|
1498
|
+
clearMessages,
|
|
1499
|
+
cwd: args.cwd,
|
|
1500
|
+
exit: () => { requestExit(); },
|
|
1501
|
+
sessionManager,
|
|
1502
|
+
createProvider: createProvider ?? (() => {
|
|
1503
|
+
throw new Error("Provider creation not available");
|
|
1504
|
+
}),
|
|
1505
|
+
openPicker,
|
|
1506
|
+
openSessionPicker,
|
|
1507
|
+
openRewindPicker,
|
|
1508
|
+
openFeedback,
|
|
1509
|
+
fillComposer,
|
|
1510
|
+
registry: safeRegistry,
|
|
1511
|
+
skillRegistry: safeSkillRegistry,
|
|
1512
|
+
bashAllowlist,
|
|
1513
|
+
settingsManager,
|
|
1514
|
+
lspService,
|
|
1515
|
+
mcpManager,
|
|
1516
|
+
hookController,
|
|
1517
|
+
flushMemory,
|
|
1518
|
+
runMemoryCompaction,
|
|
1519
|
+
runMemorySummary,
|
|
1520
|
+
runMemoryRefresh,
|
|
1521
|
+
getThemeMode: () => themeMode,
|
|
1522
|
+
getResolvedTheme: () => themeResolved,
|
|
1523
|
+
setThemeMode: applyThemeMode,
|
|
1524
|
+
toggleSidebar,
|
|
1525
|
+
setSidebarMode: applySidebarMode,
|
|
1526
|
+
openStats: openStatsPanel,
|
|
1527
|
+
});
|
|
1528
|
+
if (handled) {
|
|
1529
|
+
if (agent.mode !== permissionMode) {
|
|
1530
|
+
setPermissionMode(agent.mode);
|
|
1099
1531
|
}
|
|
1100
|
-
|
|
1101
|
-
//
|
|
1102
|
-
// the
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1532
|
+
if (result) {
|
|
1533
|
+
// `/compact` rewrites agent.messages, so the Ink transcript needs to
|
|
1534
|
+
// be rebuilt from the new agent state before appending the summary
|
|
1535
|
+
// card; otherwise the pre-compaction history would keep rendering.
|
|
1536
|
+
if (result.startsWith("✓ Compaction complete")) {
|
|
1537
|
+
const summary = latestCompactionSummary(agent.messages);
|
|
1538
|
+
updateDisplayMessages(() => [
|
|
1539
|
+
...reconstructDisplayMessages(agent.messages),
|
|
1540
|
+
{
|
|
1541
|
+
role: "assistant",
|
|
1542
|
+
content: result,
|
|
1543
|
+
syntheticKind: "ui_compact_summary",
|
|
1544
|
+
compactionSummary: summary,
|
|
1545
|
+
},
|
|
1546
|
+
]);
|
|
1547
|
+
}
|
|
1548
|
+
else if (result.startsWith("⏪")) {
|
|
1549
|
+
// /rewind truncated agent.messages — rebuild the transcript from
|
|
1550
|
+
// the rewound state before appending the summary.
|
|
1551
|
+
updateDisplayMessages(() => [
|
|
1552
|
+
...reconstructDisplayMessages(agent.messages),
|
|
1553
|
+
{ role: "assistant", content: result },
|
|
1554
|
+
]);
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
addMessage("assistant", result);
|
|
1558
|
+
}
|
|
1107
1559
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1560
|
+
if (inject) {
|
|
1561
|
+
await runAgentInput(inject, displayInput);
|
|
1110
1562
|
}
|
|
1563
|
+
return;
|
|
1111
1564
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1565
|
+
}
|
|
1566
|
+
const expansion = await expandAtMentions(input, args.cwd);
|
|
1567
|
+
if (expansion.missing.length > 0) {
|
|
1568
|
+
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
1569
|
+
}
|
|
1570
|
+
for (const skip of expansion.skipped) {
|
|
1571
|
+
if (skip.reason !== "too large")
|
|
1572
|
+
addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
|
|
1573
|
+
}
|
|
1574
|
+
const agentInput = images.length > 0
|
|
1575
|
+
? [
|
|
1576
|
+
...(expansion.text ? [{ type: "text", text: expansion.text }] : []),
|
|
1577
|
+
...images.map((img) => ({
|
|
1578
|
+
type: "image_url",
|
|
1579
|
+
image_url: { url: img.dataUrl },
|
|
1580
|
+
})),
|
|
1581
|
+
]
|
|
1582
|
+
: expansion.text;
|
|
1583
|
+
await runAgentInput(agentInput, displayInput, images.map((img) => ({ filename: img.filename, bytes: img.bytes })), { imageDisplayStart: normalized.imageDisplayStart });
|
|
1584
|
+
}
|
|
1585
|
+
finally {
|
|
1586
|
+
if (startingSubmitFingerprintRef.current === submitFingerprint) {
|
|
1587
|
+
setStartingSubmit(null);
|
|
1116
1588
|
}
|
|
1117
1589
|
}
|
|
1118
|
-
|
|
1119
|
-
if (expansion.missing.length > 0) {
|
|
1120
|
-
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
1121
|
-
}
|
|
1122
|
-
for (const skip of expansion.skipped) {
|
|
1123
|
-
if (skip.reason !== "too large")
|
|
1124
|
-
addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
|
|
1125
|
-
}
|
|
1126
|
-
const agentInput = images.length > 0
|
|
1127
|
-
? [
|
|
1128
|
-
...(expansion.text ? [{ type: "text", text: expansion.text }] : []),
|
|
1129
|
-
...images.map((img) => ({
|
|
1130
|
-
type: "image_url",
|
|
1131
|
-
image_url: { url: img.dataUrl },
|
|
1132
|
-
})),
|
|
1133
|
-
]
|
|
1134
|
-
: expansion.text;
|
|
1135
|
-
await runAgentInput(agentInput, displayInput, images.map((img) => ({ filename: img.filename, bytes: img.bytes })));
|
|
1136
|
-
}, [addMessage, agent, args.cwd, openPicker, createProvider, fillComposer, safeRegistry, safeSkillRegistry, updateDisplayMessages, queueInput, submitSteer, requestExit]);
|
|
1590
|
+
}, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, fillComposer, prepareSubmitDisplay, safeRegistry, safeSkillRegistry, updateDisplayMessages, queueInput, submitSteer, requestExit, toggleSidebar, applySidebarMode, setStartingSubmit]);
|
|
1137
1591
|
// Drain the queue once the run ends and no modal needs the user first.
|
|
1138
1592
|
// The placeholder row is removed right before resubmitting — handleSubmit
|
|
1139
1593
|
// renders the message again as a regular user row.
|
|
1140
1594
|
const drainQueuedInput = useCallback(() => {
|
|
1141
1595
|
if (activeAbortRef.current)
|
|
1142
1596
|
return;
|
|
1143
|
-
if (
|
|
1597
|
+
if (startingSubmitFingerprintRef.current)
|
|
1598
|
+
return;
|
|
1599
|
+
if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || pickerMode || statsPanel)
|
|
1144
1600
|
return;
|
|
1145
1601
|
const next = queuedInputsRef.current.shift();
|
|
1146
1602
|
if (!next)
|
|
@@ -1149,16 +1605,20 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1149
1605
|
if (next.displayKey) {
|
|
1150
1606
|
updateDisplayMessages((prev) => prev.filter((message) => message.key !== next.displayKey));
|
|
1151
1607
|
}
|
|
1608
|
+
if (!isQueuedInputForCurrentSession(next, currentSessionFile()))
|
|
1609
|
+
return;
|
|
1152
1610
|
void handleSubmit(next.payload);
|
|
1153
|
-
}, [pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, updateDisplayMessages, handleSubmit]);
|
|
1611
|
+
}, [pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, statsPanel, currentSessionFile, updateDisplayMessages, handleSubmit]);
|
|
1154
1612
|
useEffect(() => {
|
|
1155
1613
|
if (isRunning || queuedCount === 0)
|
|
1156
1614
|
return;
|
|
1157
|
-
if (
|
|
1615
|
+
if (startingSubmitFingerprint)
|
|
1616
|
+
return;
|
|
1617
|
+
if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || pickerMode || statsPanel)
|
|
1158
1618
|
return;
|
|
1159
1619
|
const timer = setTimeout(drainQueuedInput, 0);
|
|
1160
1620
|
return () => clearTimeout(timer);
|
|
1161
|
-
}, [isRunning, queuedCount, pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, drainQueuedInput]);
|
|
1621
|
+
}, [isRunning, queuedCount, startingSubmitFingerprint, pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, statsPanel, drainQueuedInput]);
|
|
1162
1622
|
const currentProviderId = agent.providerId || safeRegistry.getDefault()?.id;
|
|
1163
1623
|
const keyTarget = keyProviderId
|
|
1164
1624
|
? safeRegistry.getConfigured().find((p) => p.id === keyProviderId)
|
|
@@ -1181,7 +1641,9 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1181
1641
|
const showThinkingLabel = getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2
|
|
1182
1642
|
&& thinkingLevel
|
|
1183
1643
|
&& thinkingLevel !== "off";
|
|
1184
|
-
const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry), updateNotice:
|
|
1644
|
+
const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry), updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), providerId: agent.providerId || safeRegistry.getDefault()?.id, modelLabel: agent.model ? displayModel(agent.model) : undefined, thinkingLabel: showThinkingLabel ? thinkingLevel : undefined })) : null;
|
|
1645
|
+
const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry), [safeSkillRegistry]);
|
|
1646
|
+
const mcpReconnectItems = useMemo(() => buildMcpReconnectItems(mcpManager), [mcpManager]);
|
|
1185
1647
|
// One row shorter than the terminal on purpose. A frame that exactly fills
|
|
1186
1648
|
// the screen makes Ink omit the trailing newline ("fullscreen" mode), and
|
|
1187
1649
|
// Ink's cursor-only repositioning (buildReturnToBottom) miscalculates by one
|
|
@@ -1190,64 +1652,440 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1190
1652
|
// stdout write). Keeping every frame below viewport height keeps all of
|
|
1191
1653
|
// Ink's cursor paths on the consistent trailing-newline math.
|
|
1192
1654
|
const frameRows = Math.max(4, terminalRows - 1);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1655
|
+
const sidebarWidth = sidebarVisible ? Math.min(42, Math.max(28, Math.floor(terminalColumns * 0.34))) : 0;
|
|
1656
|
+
const mainWidth = Math.max(40, terminalColumns - sidebarWidth);
|
|
1657
|
+
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "row", width: terminalColumns, height: frameRows, backgroundColor: palette.background, children: [_jsxs(Box, { flexDirection: "column", width: mainWidth, height: frameRows, backgroundColor: palette.background, children: [_jsx(TranscriptViewport, { ref: viewportRef, children: _jsx(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, flexShrink: 0, children: _jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode }) }) }), 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
|
|
1658
|
+
.filter((p) => isUserVisibleProvider(p.id))
|
|
1659
|
+
.map((p) => {
|
|
1660
|
+
const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
|
|
1661
|
+
const configuredLabel = configured?.apiKey ? "configured" : "needs key";
|
|
1662
|
+
return {
|
|
1663
|
+
id: p.id,
|
|
1664
|
+
name: `${p.name} [${configuredLabel}]`,
|
|
1665
|
+
enabled: true,
|
|
1666
|
+
};
|
|
1667
|
+
}), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1668
|
+
.filter((p) => isUserVisibleProvider(p.id))
|
|
1669
|
+
.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
|
|
1670
|
+
.filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
|
|
1671
|
+
.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()
|
|
1672
|
+
.filter((p) => safeRegistry.getAuthStorage().has(p.id))
|
|
1673
|
+
.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: () => {
|
|
1674
|
+
closePicker();
|
|
1675
|
+
setKeyProviderId(null);
|
|
1676
|
+
} }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
|
|
1677
|
+
fillComposer(`/${name} `);
|
|
1678
|
+
closePicker();
|
|
1679
|
+
}, onCancel: closePicker }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
|
|
1680
|
+
closePicker();
|
|
1681
|
+
if (item.action === "insert-skill") {
|
|
1682
|
+
fillComposer(`/${item.value} `);
|
|
1683
|
+
}
|
|
1684
|
+
else {
|
|
1685
|
+
void handleSubmit(item.command);
|
|
1686
|
+
}
|
|
1687
|
+
}, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
|
|
1688
|
+
closePicker();
|
|
1689
|
+
void handleSubmit(item.command);
|
|
1690
|
+
}, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
|
|
1691
|
+
closePicker();
|
|
1692
|
+
void handleSubmit(command);
|
|
1693
|
+
}, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
1694
|
+
closePicker();
|
|
1695
|
+
addMessage("assistant", summary);
|
|
1696
|
+
}, onCancel: () => {
|
|
1697
|
+
closePicker();
|
|
1698
|
+
addMessage("assistant", "已取消 Feishu setup。");
|
|
1699
|
+
} }) })), statsPanel && !pickerMode && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(StatsPanel, { panel: statsPanel, terminalColumns: mainWidth, terminalRows: terminalRows, onRangeChange: (range) => setStatsPanel((current) => current ? { ...current, range } : current), onCancel: closeStatsPanel }) })), todos.length > 0 && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
|
|
1700
|
+
const resolve = pendingPlan.resolve;
|
|
1701
|
+
setPendingPlan(null);
|
|
1702
|
+
resolve({ action: "approve", plan: finalPlan });
|
|
1703
|
+
}, onReject: (reason) => {
|
|
1704
|
+
const resolve = pendingPlan.resolve;
|
|
1705
|
+
setPendingPlan(null);
|
|
1706
|
+
resolve({ action: "reject", reason });
|
|
1707
|
+
} }) })), pendingApproval && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ApprovalDialog, { request: pendingApproval.request, onDecision: (decision) => {
|
|
1708
|
+
const resolve = pendingApproval.resolve;
|
|
1709
|
+
setPendingApproval(null);
|
|
1710
|
+
resolve(decision);
|
|
1711
|
+
}, onAllowBashPrefix: (prefix) => {
|
|
1712
|
+
bashAllowlist?.add(prefix);
|
|
1713
|
+
} }) })), pendingQuestion && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
|
|
1714
|
+
questionController?.reply(pendingQuestion.id, answers);
|
|
1715
|
+
setPendingQuestion(null);
|
|
1716
|
+
}, onCancel: () => {
|
|
1717
|
+
questionController?.reject(pendingQuestion.id);
|
|
1718
|
+
setPendingQuestion(null);
|
|
1719
|
+
} }) })), pendingFeedback && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
|
|
1720
|
+
if (result.kind === "success") {
|
|
1721
|
+
addMessage("assistant", `Feedback submitted: ${result.url}`);
|
|
1722
|
+
}
|
|
1723
|
+
else if (result.kind === "error") {
|
|
1724
|
+
addMessage("error", `Feedback failed: ${result.message}`);
|
|
1725
|
+
}
|
|
1726
|
+
} }) })), !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, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel, 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 && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({ mode: permissionMode, goalLine }) }) }))] }), sidebarVisible && (_jsx(InkSidebar, { width: sidebarWidth, agent: agent, sessionManager: sessionManager, cwd: args.cwd, mode: permissionMode, goalLine: goalLine, todos: todos, mcpManager: mcpManager, lspService: lspService }))] }) }));
|
|
1727
|
+
}
|
|
1728
|
+
function buildCommandPaletteItems(skillRegistry) {
|
|
1729
|
+
const items = new Map();
|
|
1730
|
+
const add = (item) => {
|
|
1731
|
+
const key = `${item.action ?? "command"}:${item.value}`;
|
|
1732
|
+
if (!items.has(key))
|
|
1733
|
+
items.set(key, item);
|
|
1734
|
+
};
|
|
1735
|
+
for (const command of INK_LOCAL_SLASH_COMMANDS) {
|
|
1736
|
+
add({
|
|
1737
|
+
label: `/${command.name}`,
|
|
1738
|
+
detail: command.description,
|
|
1739
|
+
value: command.name,
|
|
1740
|
+
command: `/${command.name}`,
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
for (const command of slashRegistry.list()) {
|
|
1744
|
+
const source = command.source === "mcp" ? " :mcp" : "";
|
|
1745
|
+
const sourceLabel = command.sourceLabel ? `[${command.sourceLabel}] ` : "";
|
|
1746
|
+
add({
|
|
1747
|
+
label: `/${command.name}${source}`,
|
|
1748
|
+
detail: `${sourceLabel}${command.description}`,
|
|
1749
|
+
value: command.name,
|
|
1750
|
+
command: `/${command.name}`,
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
for (const skill of skillRegistry.summaries()) {
|
|
1754
|
+
add({
|
|
1755
|
+
label: `/${skill.name} :skill`,
|
|
1756
|
+
detail: `[${skill.source}] ${skill.description}`,
|
|
1757
|
+
value: skill.name,
|
|
1758
|
+
command: `/${skill.name}`,
|
|
1759
|
+
action: "insert-skill",
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
return [...items.values()];
|
|
1763
|
+
}
|
|
1764
|
+
function buildMcpReconnectItems(mcpManager) {
|
|
1765
|
+
return (mcpManager?.getStates() ?? []).map((state) => {
|
|
1766
|
+
let detail;
|
|
1767
|
+
if (state.status.kind === "connected") {
|
|
1768
|
+
const tools = state.status.tools.length;
|
|
1769
|
+
const prompts = state.status.prompts.length;
|
|
1770
|
+
detail = `connected · ${tools} tool${tools === 1 ? "" : "s"} · ${prompts} prompt${prompts === 1 ? "" : "s"}`;
|
|
1771
|
+
}
|
|
1772
|
+
else if (state.status.kind === "failed") {
|
|
1773
|
+
detail = `failed · ${state.status.error}`;
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1776
|
+
detail = "disabled";
|
|
1777
|
+
}
|
|
1778
|
+
return {
|
|
1779
|
+
label: state.name,
|
|
1780
|
+
detail,
|
|
1781
|
+
value: state.name,
|
|
1782
|
+
command: `/mcp reconnect ${state.name}`,
|
|
1783
|
+
};
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
function CommandPalette({ items, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
1787
|
+
return (_jsx(PalettePicker, { title: "Commands", hint: "Type to filter \u00B7 Up/Down choose \u00B7 Enter run \u00B7 Esc cancel", emptyText: "No commands found.", items: items, terminalColumns: terminalColumns, terminalRows: terminalRows, searchable: true, onSelect: onSelect, onCancel: onCancel }));
|
|
1788
|
+
}
|
|
1789
|
+
function McpReconnectPicker({ items, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
1790
|
+
return (_jsx(PalettePicker, { title: "MCP servers", hint: "Up/Down choose \u00B7 Enter or r reconnect \u00B7 Esc cancel", emptyText: "No MCP servers configured.", items: items, terminalColumns: terminalColumns, terminalRows: terminalRows, reconnectAlias: true, onSelect: onSelect, onCancel: onCancel }));
|
|
1791
|
+
}
|
|
1792
|
+
function PalettePicker({ title, hint, emptyText, items, terminalColumns, terminalRows, searchable = false, reconnectAlias = false, onSelect, onCancel, }) {
|
|
1793
|
+
const theme = useTheme();
|
|
1794
|
+
const [query, setQuery] = useState("");
|
|
1795
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
1796
|
+
const maxVisible = Math.max(5, Math.min(12, terminalRows - 10));
|
|
1797
|
+
const filtered = useMemo(() => {
|
|
1798
|
+
const needle = query.trim().toLowerCase();
|
|
1799
|
+
if (!needle)
|
|
1800
|
+
return items;
|
|
1801
|
+
return items.filter((item) => item.label.toLowerCase().includes(needle) ||
|
|
1802
|
+
item.detail.toLowerCase().includes(needle) ||
|
|
1803
|
+
item.value.toLowerCase().includes(needle));
|
|
1804
|
+
}, [items, query]);
|
|
1805
|
+
useEffect(() => {
|
|
1806
|
+
setSelectedIndex((current) => Math.min(Math.max(0, filtered.length - 1), current));
|
|
1807
|
+
}, [filtered.length]);
|
|
1808
|
+
useInput((input, key) => {
|
|
1809
|
+
if (isKeyReleaseEvent(key))
|
|
1810
|
+
return;
|
|
1811
|
+
if (key.escape) {
|
|
1812
|
+
onCancel();
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
if (key.return || (reconnectAlias && input.toLowerCase() === "r")) {
|
|
1816
|
+
const item = filtered[selectedIndex];
|
|
1817
|
+
if (item)
|
|
1818
|
+
onSelect(item);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (key.upArrow) {
|
|
1822
|
+
setSelectedIndex((index) => Math.max(0, index - 1));
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (key.downArrow) {
|
|
1826
|
+
setSelectedIndex((index) => Math.min(Math.max(0, filtered.length - 1), index + 1));
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (key.pageUp) {
|
|
1830
|
+
setSelectedIndex((index) => Math.max(0, index - maxVisible));
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (key.pageDown) {
|
|
1834
|
+
setSelectedIndex((index) => Math.min(Math.max(0, filtered.length - 1), index + maxVisible));
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (!searchable)
|
|
1838
|
+
return;
|
|
1839
|
+
if (key.backspace || key.delete) {
|
|
1840
|
+
setQuery((current) => current.slice(0, -1));
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
1844
|
+
setQuery((current) => current + input);
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
const start = clampWindowStartForIndex(filtered.length, selectedIndex, maxVisible);
|
|
1848
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
1849
|
+
const labelWidth = Math.max(18, Math.min(36, Math.floor(terminalColumns * 0.32)));
|
|
1850
|
+
const detailWidth = Math.max(20, terminalColumns - labelWidth - 10);
|
|
1851
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: title }), searchable && (_jsxs(Text, { color: theme.muted, children: ["Filter: ", _jsx(Text, { color: theme.userMessageText, children: query || " " })] })), _jsx(Text, { color: theme.muted, children: hint }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [filtered.length === 0 && _jsx(Text, { color: theme.muted, children: emptyText }), visible.map((item, offset) => {
|
|
1852
|
+
const actualIndex = start + offset;
|
|
1853
|
+
const selected = actualIndex === selectedIndex;
|
|
1854
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: selected ? theme.accent : undefined, children: [selected ? "> " : " ", truncate(item.label, labelWidth)] }), _jsxs(Text, { color: theme.muted, children: [" ", truncate(item.detail, detailWidth)] })] }, `${item.action ?? "command"}-${item.value}`));
|
|
1855
|
+
})] })] }));
|
|
1856
|
+
}
|
|
1857
|
+
const REWIND_SCOPE_ORDER = ["all", "chat", "code"];
|
|
1858
|
+
const REWIND_SCOPE_LABEL = {
|
|
1859
|
+
all: "chat + files",
|
|
1860
|
+
chat: "chat only",
|
|
1861
|
+
code: "files only",
|
|
1862
|
+
};
|
|
1863
|
+
function rewindCommand(turnIndex, scope) {
|
|
1864
|
+
const base = `/rewind ${turnIndex + 1}`;
|
|
1865
|
+
if (scope === "chat")
|
|
1866
|
+
return `${base} --chat`;
|
|
1867
|
+
if (scope === "code")
|
|
1868
|
+
return `${base} --code`;
|
|
1869
|
+
return base;
|
|
1870
|
+
}
|
|
1871
|
+
function cycleRewindScope(scope, direction) {
|
|
1872
|
+
const index = REWIND_SCOPE_ORDER.indexOf(scope);
|
|
1873
|
+
return REWIND_SCOPE_ORDER[(index + direction + REWIND_SCOPE_ORDER.length) % REWIND_SCOPE_ORDER.length];
|
|
1874
|
+
}
|
|
1875
|
+
function RewindPicker({ sessionManager, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
1876
|
+
const theme = useTheme();
|
|
1877
|
+
const turns = useMemo(() => sessionManager.listUserTurns(), [sessionManager]);
|
|
1878
|
+
const checkpoints = useMemo(() => sessionManager.getCheckpoints(), [sessionManager]);
|
|
1879
|
+
const fileCounts = useMemo(() => {
|
|
1880
|
+
const entries = checkpoints.listEntries();
|
|
1881
|
+
const byTurn = new Map();
|
|
1882
|
+
for (const entry of entries) {
|
|
1883
|
+
const files = byTurn.get(entry.turn);
|
|
1884
|
+
if (files)
|
|
1885
|
+
files.add(entry.path);
|
|
1886
|
+
else
|
|
1887
|
+
byTurn.set(entry.turn, new Set([entry.path]));
|
|
1888
|
+
}
|
|
1889
|
+
return new Map(turns.map((turn) => [turn.id, byTurn.get(turn.id)?.size ?? 0]));
|
|
1890
|
+
}, [checkpoints, turns]);
|
|
1891
|
+
const [selectedIndex, setSelectedIndex] = useState(() => Math.max(0, turns.length - 1));
|
|
1892
|
+
const [scope, setScope] = useState("all");
|
|
1893
|
+
const maxVisible = Math.max(4, Math.min(10, terminalRows - 10));
|
|
1894
|
+
useEffect(() => {
|
|
1895
|
+
setSelectedIndex((current) => Math.min(Math.max(0, turns.length - 1), current));
|
|
1896
|
+
}, [turns.length]);
|
|
1897
|
+
useInput((input, key) => {
|
|
1898
|
+
if (isKeyReleaseEvent(key))
|
|
1899
|
+
return;
|
|
1900
|
+
if (key.escape) {
|
|
1901
|
+
onCancel();
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (key.return) {
|
|
1905
|
+
if (turns[selectedIndex])
|
|
1906
|
+
onSelect(rewindCommand(selectedIndex, scope));
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (key.upArrow) {
|
|
1910
|
+
setSelectedIndex((index) => Math.max(0, index - 1));
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (key.downArrow) {
|
|
1914
|
+
setSelectedIndex((index) => Math.min(Math.max(0, turns.length - 1), index + 1));
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (key.pageUp) {
|
|
1918
|
+
setSelectedIndex((index) => Math.max(0, index - maxVisible));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (key.pageDown) {
|
|
1922
|
+
setSelectedIndex((index) => Math.min(Math.max(0, turns.length - 1), index + maxVisible));
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (key.tab || key.rightArrow || input === "l") {
|
|
1926
|
+
setScope((current) => cycleRewindScope(current, 1));
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (key.leftArrow || input === "h") {
|
|
1930
|
+
setScope((current) => cycleRewindScope(current, -1));
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
const start = clampWindowStartForIndex(turns.length, selectedIndex, maxVisible);
|
|
1934
|
+
const visibleTurns = turns.slice(start, start + maxVisible);
|
|
1935
|
+
const previewWidth = Math.max(18, Math.min(76, terminalColumns - 34));
|
|
1936
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Rewind" }), _jsxs(Text, { color: theme.muted, children: ["Restore: ", _jsx(Text, { color: theme.accent, children: REWIND_SCOPE_LABEL[scope] }), " · ", turns.length, " point", turns.length === 1 ? "" : "s"] }), _jsx(Text, { color: theme.muted, children: "Up/Down choose \u00B7 Left/Right scope \u00B7 Enter rewind \u00B7 Esc cancel" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [turns.length === 0 && _jsx(Text, { color: theme.muted, children: "Nothing to rewind: no user messages in this session." }), visibleTurns.map((turn, offset) => {
|
|
1937
|
+
const actualIndex = start + offset;
|
|
1938
|
+
const isSelected = actualIndex === selectedIndex;
|
|
1939
|
+
const touched = fileCounts.get(turn.id) ?? 0;
|
|
1940
|
+
return (_jsx(RewindRow, { turn: turn, turnNumber: actualIndex + 1, selected: isSelected, fileCount: touched, previewWidth: previewWidth }, turn.id));
|
|
1941
|
+
})] })] }));
|
|
1942
|
+
}
|
|
1943
|
+
function RewindRow({ turn, turnNumber, selected, fileCount, previewWidth, }) {
|
|
1944
|
+
const theme = useTheme();
|
|
1945
|
+
const time = new Date(turn.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1946
|
+
const fileNote = fileCount > 0 ? ` · ${fileCount} file${fileCount === 1 ? "" : "s"}` : "";
|
|
1947
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: selected ? theme.accent : undefined, children: [selected ? "> " : " ", String(turnNumber).padStart(2, " "), " ", time, " ", truncate(turn.preview, previewWidth)] }), _jsx(Text, { color: theme.muted, children: fileNote })] }));
|
|
1948
|
+
}
|
|
1949
|
+
function clampWindowStartForIndex(total, selectedIndex, maxVisible) {
|
|
1950
|
+
if (total <= maxVisible)
|
|
1951
|
+
return 0;
|
|
1952
|
+
const half = Math.floor(maxVisible / 2);
|
|
1953
|
+
let start = Math.max(0, selectedIndex - half);
|
|
1954
|
+
if (start + maxVisible > total)
|
|
1955
|
+
start = total - maxVisible;
|
|
1956
|
+
return Math.max(0, start);
|
|
1957
|
+
}
|
|
1958
|
+
function StatsPanel({ panel, terminalColumns, terminalRows, onRangeChange, onCancel, }) {
|
|
1959
|
+
const theme = useTheme();
|
|
1960
|
+
const [scroll, setScroll] = useState(0);
|
|
1961
|
+
const bodyWidth = Math.max(48, Math.min(92, terminalColumns - 6));
|
|
1962
|
+
const lines = useMemo(() => formatStatsPanelBody(panel.bundle.ranges[panel.range], bodyWidth).split("\n"), [bodyWidth, panel.bundle, panel.range]);
|
|
1963
|
+
const maxVisible = Math.max(5, Math.min(16, terminalRows - 10));
|
|
1964
|
+
const maxScroll = Math.max(0, lines.length - maxVisible);
|
|
1965
|
+
useEffect(() => {
|
|
1966
|
+
setScroll(0);
|
|
1967
|
+
}, [panel.range]);
|
|
1968
|
+
useEffect(() => {
|
|
1969
|
+
setScroll((current) => Math.min(current, maxScroll));
|
|
1970
|
+
}, [maxScroll]);
|
|
1971
|
+
useInput((input, key) => {
|
|
1972
|
+
if (isKeyReleaseEvent(key))
|
|
1973
|
+
return;
|
|
1974
|
+
if (key.escape) {
|
|
1975
|
+
onCancel();
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (key.tab) {
|
|
1979
|
+
onRangeChange(panel.range === "30d" ? "7d" : "30d");
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
if (key.leftArrow || input === "h") {
|
|
1983
|
+
onRangeChange("7d");
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (key.rightArrow || input === "l") {
|
|
1987
|
+
onRangeChange("30d");
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
if (key.upArrow) {
|
|
1991
|
+
setScroll((current) => Math.max(0, current - 1));
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (key.downArrow) {
|
|
1995
|
+
setScroll((current) => Math.min(maxScroll, current + 1));
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (key.pageUp) {
|
|
1999
|
+
setScroll((current) => Math.max(0, current - maxVisible));
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (key.pageDown) {
|
|
2003
|
+
setScroll((current) => Math.min(maxScroll, current + maxVisible));
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
const visible = lines.slice(scroll, scroll + maxVisible);
|
|
2007
|
+
const generatedAt = panel.bundle.generatedAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
2008
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Stats" }), _jsxs(Text, { color: theme.muted, children: [rangeLabel(panel.range), " \u00B7 generated ", generatedAt] }), _jsx(Text, { color: theme.muted, children: "Left/Right range \u00B7 Up/Down scroll \u00B7 Tab toggle \u00B7 Esc close" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((line, index) => {
|
|
2009
|
+
const key = `${scroll + index}-${line}`;
|
|
2010
|
+
const heading = line === "Activity" || line === "Model usage" || line === "Summary";
|
|
2011
|
+
return (_jsx(Text, { color: heading ? theme.accent : undefined, bold: heading, children: line || " " }, key));
|
|
2012
|
+
}) }), maxScroll > 0 && (_jsxs(Text, { color: theme.muted, children: [scroll + 1, "-", Math.min(lines.length, scroll + maxVisible), " of ", lines.length] }))] }));
|
|
2013
|
+
}
|
|
2014
|
+
function summarizeMcpStates(states) {
|
|
2015
|
+
const summary = { connected: 0, starting: 0, failed: 0, disabled: 0, tools: 0 };
|
|
2016
|
+
for (const state of states) {
|
|
2017
|
+
if (state.status.kind === "connected") {
|
|
2018
|
+
summary.connected += 1;
|
|
2019
|
+
summary.tools += state.status.tools.length;
|
|
2020
|
+
}
|
|
2021
|
+
else if (state.status.kind === "failed") {
|
|
2022
|
+
summary.failed += 1;
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
summary.disabled += 1;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return summary;
|
|
2029
|
+
}
|
|
2030
|
+
function summarizeLspStatuses(statuses) {
|
|
2031
|
+
const summary = { connected: 0, starting: 0, failed: 0, disabled: 0 };
|
|
2032
|
+
for (const status of statuses) {
|
|
2033
|
+
if (status.status === "connected")
|
|
2034
|
+
summary.connected += 1;
|
|
2035
|
+
else if (status.status === "starting")
|
|
2036
|
+
summary.starting += 1;
|
|
2037
|
+
else
|
|
2038
|
+
summary.failed += 1;
|
|
2039
|
+
}
|
|
2040
|
+
return summary;
|
|
2041
|
+
}
|
|
2042
|
+
function formatStatusCount(summary) {
|
|
2043
|
+
const parts = [];
|
|
2044
|
+
if (summary.connected > 0)
|
|
2045
|
+
parts.push(`${summary.connected} up`);
|
|
2046
|
+
if (summary.starting > 0)
|
|
2047
|
+
parts.push(`${summary.starting} starting`);
|
|
2048
|
+
if (summary.failed > 0)
|
|
2049
|
+
parts.push(`${summary.failed} failed`);
|
|
2050
|
+
if (summary.disabled > 0)
|
|
2051
|
+
parts.push(`${summary.disabled} disabled`);
|
|
2052
|
+
return parts.join(" · ") || "none";
|
|
2053
|
+
}
|
|
2054
|
+
function SidebarSection({ title, children }) {
|
|
2055
|
+
const theme = useTheme();
|
|
2056
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), children] }));
|
|
2057
|
+
}
|
|
2058
|
+
function SidebarRow({ label, value, color, }) {
|
|
2059
|
+
const theme = useTheme();
|
|
2060
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [label, ": "] }), _jsx(Text, { color: color ?? theme.userMessageText, children: value })] }));
|
|
2061
|
+
}
|
|
2062
|
+
function InkSidebar({ width, agent, sessionManager, cwd, mode, goalLine, todos, mcpManager, lspService, }) {
|
|
2063
|
+
const theme = useTheme();
|
|
2064
|
+
const innerWidth = Math.max(12, width - 4);
|
|
2065
|
+
const todoCounts = todos.reduce((acc, todo) => {
|
|
2066
|
+
acc[todo.status] = (acc[todo.status] ?? 0) + 1;
|
|
2067
|
+
return acc;
|
|
2068
|
+
}, {});
|
|
2069
|
+
const todoSummary = todos.length === 0
|
|
2070
|
+
? "none"
|
|
2071
|
+
: [
|
|
2072
|
+
todoCounts.in_progress ? `${todoCounts.in_progress} active` : "",
|
|
2073
|
+
todoCounts.pending ? `${todoCounts.pending} pending` : "",
|
|
2074
|
+
todoCounts.completed ? `${todoCounts.completed} done` : "",
|
|
2075
|
+
].filter(Boolean).join(" · ");
|
|
2076
|
+
const mcpStates = mcpManager?.getStates() ?? [];
|
|
2077
|
+
const mcpSummary = summarizeMcpStates(mcpStates);
|
|
2078
|
+
const lspSummary = lspService?.isDisabled()
|
|
2079
|
+
? { connected: 0, starting: 0, failed: 0, disabled: 1 }
|
|
2080
|
+
: summarizeLspStatuses(lspService?.status() ?? []);
|
|
2081
|
+
const latestMcpFailure = mcpStates.find((state) => state.status.kind === "failed");
|
|
2082
|
+
const latestLspFailure = lspService?.status().find((status) => status.status === "error");
|
|
2083
|
+
const sessionTitle = truncate(sessionDisplayName(sessionManager), innerWidth);
|
|
2084
|
+
const modelLabel = agent.model ? displayModel(agent.model) : "not selected";
|
|
2085
|
+
const route = agent.providerId
|
|
2086
|
+
? `${agent.providerId}/${modelLabel}`
|
|
2087
|
+
: modelLabel;
|
|
2088
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: "100%", borderStyle: "single", borderColor: theme.border, paddingX: 1, paddingY: 1, flexShrink: 0, children: [_jsx(Text, { color: theme.borderActive, bold: true, children: "Session" }), _jsx(Text, { color: theme.userMessageText, children: sessionTitle }), _jsx(Text, { color: theme.muted, children: truncate(friendlyCwd(cwd), innerWidth) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(SidebarSection, { title: "Runtime", children: [_jsx(SidebarRow, { label: "model", value: truncate(route, innerWidth - 7) }), _jsx(SidebarRow, { label: "mode", value: mode, color: mode === "bypassPermissions" ? theme.warning : theme.userMessageText }), _jsx(SidebarRow, { label: "thinking", value: agent.thinking || "off" })] }), goalLine && (_jsx(SidebarSection, { title: "Goal", children: _jsx(Text, { color: theme.userMessageText, children: truncate(goalLine, innerWidth) }) })), _jsx(SidebarSection, { title: "Todos", children: _jsx(Text, { color: todos.length > 0 ? theme.userMessageText : theme.muted, children: truncate(todoSummary, innerWidth) }) }), _jsxs(SidebarSection, { title: "MCP", children: [_jsx(Text, { color: mcpSummary.failed > 0 ? theme.warning : theme.userMessageText, children: truncate(`${formatStatusCount(mcpSummary)}${mcpSummary.tools > 0 ? ` · ${mcpSummary.tools} tools` : ""}`, innerWidth) }), latestMcpFailure?.status.kind === "failed" && (_jsx(Text, { color: theme.muted, children: truncate(latestMcpFailure.status.error, innerWidth) }))] }), _jsxs(SidebarSection, { title: "LSP", children: [_jsx(Text, { color: lspSummary.failed > 0 ? theme.warning : theme.userMessageText, children: truncate(formatStatusCount(lspSummary), innerWidth) }), latestLspFailure?.message && (_jsx(Text, { color: theme.muted, children: truncate(latestLspFailure.message, innerWidth) }))] })] })] }));
|
|
1251
2089
|
}
|
|
1252
2090
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
1253
2091
|
const GENERIC_PHRASES = [
|