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