@bubblebrain-ai/bubble 0.0.49 → 0.0.50
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/dist/agent/abort-errors.d.ts +4 -0
- package/dist/agent/abort-errors.js +30 -0
- package/dist/agent/provider-fingerprint.d.ts +6 -0
- package/dist/agent/provider-fingerprint.js +89 -0
- package/dist/agent/resident-history.d.ts +7 -0
- package/dist/agent/resident-history.js +61 -0
- package/dist/agent/transcript-helpers.d.ts +20 -0
- package/dist/agent/transcript-helpers.js +62 -0
- package/dist/agent/update-queue.d.ts +13 -0
- package/dist/agent/update-queue.js +69 -0
- package/dist/agent.d.ts +32 -15
- package/dist/agent.js +79 -297
- package/dist/external-runtime/grok-runtime.js +1 -1
- package/dist/feishu/agent-host/run-driver.js +1 -1
- package/dist/lsp/service.js +2 -2
- package/dist/model-catalog.js +8 -6
- package/dist/permissions/settings.d.ts +0 -1
- package/dist/permissions/settings.js +0 -2
- package/dist/provider-registry.js +13 -1
- package/dist/provider-transform.js +7 -8
- package/dist/slash-commands/commands.js +13 -9
- package/dist/tools/grep.js +1 -1
- package/dist/tui-ink/app-helpers.d.ts +43 -0
- package/dist/tui-ink/app-helpers.js +150 -0
- package/dist/tui-ink/app.d.ts +4 -11
- package/dist/tui-ink/app.js +56 -645
- package/dist/tui-ink/command-palette.d.ts +32 -0
- package/dist/tui-ink/command-palette.js +158 -0
- package/dist/tui-ink/display-reconstruct.d.ts +3 -0
- package/dist/tui-ink/display-reconstruct.js +82 -0
- package/dist/tui-ink/rewind-picker.d.ts +8 -0
- package/dist/tui-ink/rewind-picker.js +102 -0
- package/dist/tui-ink/session-picker.js +1 -1
- package/dist/tui-ink/stats-panel.d.ts +11 -0
- package/dist/tui-ink/stats-panel.js +65 -0
- package/dist/tui-ink/waiting-indicator.d.ts +12 -0
- package/dist/tui-ink/waiting-indicator.js +129 -0
- package/package.json +1 -1
package/dist/tui-ink/app.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { AgentAbortError, INTERRUPTED_ASSISTANT_CONTENT } from "../agent.js";
|
|
5
|
-
import { isHiddenToolMetadata } from "../agent/tool-visibility.js";
|
|
6
5
|
import { SessionManager } from "../session.js";
|
|
7
6
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
8
7
|
import { UserConfig, maskKey } from "../config.js";
|
|
9
8
|
import { InputBox, isCtrlCInput, isCtrlLetterInput, } from "./input-box.js";
|
|
10
9
|
import { MessageList } from "./message-list.js";
|
|
11
10
|
import { isMultiplexedTerminal } from "./terminal-env.js";
|
|
12
|
-
import { appendTextPart, appendToolPart, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts,
|
|
11
|
+
import { appendTextPart, appendToolPart, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
|
|
13
12
|
import { AgentRunInputQueue } from "../agent/input-controller.js";
|
|
14
|
-
import { canvasBackgroundFor, paletteFor, ThemeProvider
|
|
15
|
-
import {
|
|
13
|
+
import { canvasBackgroundFor, paletteFor, ThemeProvider } from "./theme.js";
|
|
14
|
+
import { ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
|
|
16
15
|
import { FeishuSetupPicker } from "./feishu-setup-picker.js";
|
|
17
16
|
import { BUILTIN_PROVIDERS, ProviderRegistry, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
18
17
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
@@ -49,234 +48,25 @@ import { tokenUsageTotal } from "../goal/usage.js";
|
|
|
49
48
|
import { formatInternalContextBlock, isInternalBlockOnlyContent } from "../agent/internal-reminder-sanitizer.js";
|
|
50
49
|
import { MAX_ACTIVE_LOOPS, decideLoopFiring, formatInterval, formatLoopList, parseLoopCommand, } from "../loop/engine.js";
|
|
51
50
|
import { TaskWakeCoalescer, findDanglingTaskStarts, formatTaskWakeSummary, isPidAlive, shouldFireTaskWake, taskEligibleForWake, } from "../tasks/wake.js";
|
|
52
|
-
|
|
53
|
-
import {
|
|
51
|
+
import { STREAMING_FLUSH_INTERVAL_MS, TASK_WAKE_DEBOUNCE_MS, buildTips, formatContextUsageLabel, friendlyCwd, parsePartialArgs, sessionBasename, slashResultNoticeKind, taskRowSummary, truncate, withMessageKey, } from "./app-helpers.js";
|
|
52
|
+
import { reconstructDisplayMessages } from "./display-reconstruct.js";
|
|
53
|
+
export { reconstructDisplayMessages } from "./display-reconstruct.js";
|
|
54
|
+
import { INK_LOCAL_SLASH_COMMANDS, CommandPalette, McpReconnectPicker, buildCommandPaletteItems, buildMcpReconnectItems, } from "./command-palette.js";
|
|
55
|
+
export { INK_LOCAL_SLASH_COMMANDS } from "./command-palette.js";
|
|
56
|
+
import { RewindPicker } from "./rewind-picker.js";
|
|
57
|
+
import { StatsPanel } from "./stats-panel.js";
|
|
58
|
+
import { WaitingIndicator } from "./waiting-indicator.js";
|
|
59
|
+
import { collectUsageStatsBundle } from "../stats/usage.js";
|
|
54
60
|
import { GrokRuntimeError } from "../external-runtime/grok-errors.js";
|
|
55
|
-
import { GROK_LOCAL_SLASH_COMMANDS, classifyGrokInput,
|
|
61
|
+
import { GROK_LOCAL_SLASH_COMMANDS, classifyGrokInput, } from "../external-runtime/grok-input-policy.js";
|
|
56
62
|
import { GROK_SUBSCRIPTION_PROVIDER_ID, isGrokSubscriptionProviderId, withGrokSubscriptionProvider, } from "../external-runtime/grok-provider.js";
|
|
57
63
|
import { classifyExternalRuntimeBinding, stopExternalRuntimeForSessionSwitch, } from "../external-runtime/session-policy.js";
|
|
58
|
-
import os from "node:os";
|
|
59
64
|
import { rmSync } from "node:fs";
|
|
60
65
|
import { execFile } from "node:child_process";
|
|
61
|
-
function buildTips(agent, registry, grokActive = false) {
|
|
62
|
-
const tips = [];
|
|
63
|
-
if (grokActive) {
|
|
64
|
-
return [
|
|
65
|
-
"Grok Subscription is active with workspace tools and Bubble approvals",
|
|
66
|
-
"Use /model to choose the Grok model and reasoning effort",
|
|
67
|
-
"Use /logout grok to return to a fresh native session",
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
const hasProvider = registry.getEnabled().length > 0;
|
|
71
|
-
if (!hasProvider) {
|
|
72
|
-
tips.push("Run /login or /provider --add to configure a model");
|
|
73
|
-
}
|
|
74
|
-
else if (agent.model) {
|
|
75
|
-
tips.push(`Ready with ${displayModel(agent.model)}`);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
tips.push("Run /model to pick a model");
|
|
79
|
-
}
|
|
80
|
-
tips.push("Type @ to reference a file");
|
|
81
|
-
tips.push("Type / for commands and skills");
|
|
82
|
-
return tips;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Slash-command results arrive as plain strings; recognize the /model
|
|
86
|
-
* confirmations by shape so they render as accent-colored UI notices — the
|
|
87
|
-
* same treatment the picker path applies — instead of assistant prose.
|
|
88
|
-
*/
|
|
89
|
-
function slashResultNoticeKind(result) {
|
|
90
|
-
return /^(Grok m|M)odel switched to /.test(result) ? "ui_notice" : undefined;
|
|
91
|
-
}
|
|
92
66
|
// Canonical implementation lives in the sanitizer module so non-TUI surfaces
|
|
93
67
|
// (session pickers, desktop host) share it; re-exported here for existing users.
|
|
94
68
|
export { isInternalBlockOnlyContent };
|
|
95
|
-
|
|
96
|
-
function taskRowSummary(tasks, nowTick) {
|
|
97
|
-
const first = tasks[0];
|
|
98
|
-
const label = first.description?.trim() || first.command.replace(/\s+/g, " ").slice(0, 32);
|
|
99
|
-
const elapsed = Math.max(0, Math.round((nowTick - first.startedAt) / 1000));
|
|
100
|
-
return `${label} ${elapsed}s`;
|
|
101
|
-
}
|
|
102
|
-
function friendlyCwd(cwd) {
|
|
103
|
-
const home = os.homedir();
|
|
104
|
-
if (cwd === home)
|
|
105
|
-
return "~";
|
|
106
|
-
if (cwd.startsWith(home + "/"))
|
|
107
|
-
return "~" + cwd.slice(home.length);
|
|
108
|
-
return cwd;
|
|
109
|
-
}
|
|
110
|
-
function sessionBasename(sessionFile) {
|
|
111
|
-
if (!sessionFile)
|
|
112
|
-
return undefined;
|
|
113
|
-
const base = sessionFile.split("/").pop() ?? sessionFile;
|
|
114
|
-
return base.replace(/\.jsonl$/, "");
|
|
115
|
-
}
|
|
116
|
-
function truncate(value, max) {
|
|
117
|
-
if (value.length <= max)
|
|
118
|
-
return value;
|
|
119
|
-
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
120
|
-
}
|
|
121
|
-
function formatCompactTokens(n) {
|
|
122
|
-
if (n < 1000)
|
|
123
|
-
return `${Math.round(n)}`;
|
|
124
|
-
if (n < 1_000_000)
|
|
125
|
-
return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}K`;
|
|
126
|
-
return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`;
|
|
127
|
-
}
|
|
128
|
-
function formatContextUsageLabel(snapshot) {
|
|
129
|
-
if (!snapshot.contextWindow || snapshot.contextWindow <= 0) {
|
|
130
|
-
return `${formatCompactTokens(snapshot.usedTokens)} context`;
|
|
131
|
-
}
|
|
132
|
-
const percent = (snapshot.usedTokens / snapshot.contextWindow) * 100;
|
|
133
|
-
const label = percent >= 10
|
|
134
|
-
? `${Math.round(percent)}%`
|
|
135
|
-
: percent >= 0.05
|
|
136
|
-
? `${percent.toFixed(1)}%`
|
|
137
|
-
: "<0.1%";
|
|
138
|
-
return `${label} context`;
|
|
139
|
-
}
|
|
140
|
-
export function reconstructDisplayMessages(agentMessages) {
|
|
141
|
-
const result = [];
|
|
142
|
-
for (const m of agentMessages) {
|
|
143
|
-
if (m.role === "system" || m.role === "tool")
|
|
144
|
-
continue;
|
|
145
|
-
if (m.role === "user") {
|
|
146
|
-
if (m.isMeta)
|
|
147
|
-
continue; // <system-reminder> injections are not user-visible
|
|
148
|
-
// Harness-initiated kicks (goal continuations, task wakes) persist as
|
|
149
|
-
// user-role messages wrapped in internal blocks so the model keeps
|
|
150
|
-
// them across resume — but they were hidden live and must stay hidden
|
|
151
|
-
// when the transcript is rebuilt.
|
|
152
|
-
if (isInternalBlockOnlyContent(m.content))
|
|
153
|
-
continue;
|
|
154
|
-
result.push({
|
|
155
|
-
key: nextDisplayMessageKey("user"),
|
|
156
|
-
role: "user",
|
|
157
|
-
content: typeof m.content === "string" ? m.content : "(multimedia)",
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
else if (m.role === "assistant") {
|
|
161
|
-
const toolCalls = [];
|
|
162
|
-
if (m.toolCalls) {
|
|
163
|
-
for (const tc of m.toolCalls) {
|
|
164
|
-
let args = {};
|
|
165
|
-
try {
|
|
166
|
-
args = JSON.parse(tc.arguments || "{}");
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
args = {};
|
|
170
|
-
}
|
|
171
|
-
const toolResult = agentMessages.find((tm) => tm.role === "tool" && tm.toolCallId === tc.id);
|
|
172
|
-
if (isHiddenToolMetadata(toolResult ? toolResult.metadata : undefined))
|
|
173
|
-
continue;
|
|
174
|
-
toolCalls.push({
|
|
175
|
-
id: tc.id,
|
|
176
|
-
name: tc.name,
|
|
177
|
-
args,
|
|
178
|
-
result: toolResult ? toolResult.content : undefined,
|
|
179
|
-
isError: toolResult ? toolResult.content?.startsWith?.("Error:") : false,
|
|
180
|
-
metadata: toolResult ? toolResult.metadata : undefined,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// An aborted assistant message carries the model-facing interruption
|
|
185
|
-
// note in its content. Render only what the assistant actually said
|
|
186
|
-
// (partial streamed text, if any) plus a dedicated interrupt row —
|
|
187
|
-
// never the note itself, which reads like a leaked system prompt.
|
|
188
|
-
const interrupted = m.error?.aborted === true;
|
|
189
|
-
const content = interrupted
|
|
190
|
-
? stripInterruptedAssistantMarker(m.content, INTERRUPTED_ASSISTANT_CONTENT)
|
|
191
|
-
: m.content;
|
|
192
|
-
if (content || m.reasoning || toolCalls.length > 0 || m.systemFingerprint) {
|
|
193
|
-
result.push({
|
|
194
|
-
key: nextDisplayMessageKey("asst"),
|
|
195
|
-
role: "assistant",
|
|
196
|
-
content,
|
|
197
|
-
reasoning: m.reasoning || undefined,
|
|
198
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
199
|
-
systemFingerprint: m.systemFingerprint,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
if (interrupted) {
|
|
203
|
-
result.push({
|
|
204
|
-
key: nextDisplayMessageKey("asst"),
|
|
205
|
-
role: "assistant",
|
|
206
|
-
content: "Interrupted by user",
|
|
207
|
-
syntheticKind: "ui_interrupt",
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Streaming tool arguments arrive as an incomplete JSON buffer. We can't
|
|
216
|
-
* JSON.parse() until the closing brace lands, but the user wants to see the
|
|
217
|
-
* short identifying fields (path, command, …) as soon as the model emits
|
|
218
|
-
* them so the tool row header reflects what's happening.
|
|
219
|
-
*
|
|
220
|
-
* Intentionally limited to short, single-line fields. Long fields like
|
|
221
|
-
* `content` are *not* surfaced live: rendering thousands of partial lines
|
|
222
|
-
* per delta floods the terminal and the partial value can break around
|
|
223
|
-
* unescaped sequences. The final value lands when the tool actually
|
|
224
|
-
* executes and tool_start delivers canonical args.
|
|
225
|
-
*/
|
|
226
|
-
function parsePartialArgs(buffer, previous) {
|
|
227
|
-
// If the buffer is now valid JSON, prefer the real parse.
|
|
228
|
-
try {
|
|
229
|
-
const parsed = JSON.parse(buffer);
|
|
230
|
-
if (parsed && typeof parsed === "object")
|
|
231
|
-
return parsed;
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
// fall through to partial extraction below
|
|
235
|
-
}
|
|
236
|
-
const result = { ...previous };
|
|
237
|
-
const FIELDS = ["path", "command", "pattern", "url", "query"];
|
|
238
|
-
for (const field of FIELDS) {
|
|
239
|
-
// Match a complete-looking quoted string. Requires a closing quote so we
|
|
240
|
-
// don't surface half-typed paths that may still change as bytes arrive.
|
|
241
|
-
const match = buffer.match(new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`));
|
|
242
|
-
if (match) {
|
|
243
|
-
const raw = match[1] ?? "";
|
|
244
|
-
result[field] = raw
|
|
245
|
-
.replace(/\\n/g, "\n")
|
|
246
|
-
.replace(/\\t/g, "\t")
|
|
247
|
-
.replace(/\\"/g, '"')
|
|
248
|
-
.replace(/\\\\/g, "\\");
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return result;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Coerce a freshly-constructed DisplayMessage into one that carries a stable
|
|
255
|
-
* `key`. Centralizes the safety net so callers don't have to remember to call
|
|
256
|
-
* nextDisplayMessageKey on every push.
|
|
257
|
-
*/
|
|
258
|
-
function withMessageKey(message) {
|
|
259
|
-
if (message.key)
|
|
260
|
-
return message;
|
|
261
|
-
const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
|
|
262
|
-
return { ...message, key: nextDisplayMessageKey(prefix) };
|
|
263
|
-
}
|
|
264
|
-
// Batch streaming text deltas before committing them to React state. Without
|
|
265
|
-
// <Static>, every commit re-renders the full-screen frame; per-token commits
|
|
266
|
-
// would make Yoga re-lay-out the transcript for every few bytes of output.
|
|
267
|
-
// 40ms keeps perceived latency invisible while capping layout work at 25fps.
|
|
268
|
-
const STREAMING_FLUSH_INTERVAL_MS = 40;
|
|
269
|
-
export const INK_LOCAL_SLASH_COMMANDS = [
|
|
270
|
-
{
|
|
271
|
-
name: "goal",
|
|
272
|
-
description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
name: "loop",
|
|
276
|
-
description: "Run a prompt on a recurring interval (/loop 5m <prompt>|list|stop [id])",
|
|
277
|
-
},
|
|
278
|
-
];
|
|
279
|
-
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, processManager, tasksAutoResume, promotionChannel, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }) {
|
|
69
|
+
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, processManager, tasksAutoResume, promotionChannel, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }) {
|
|
280
70
|
const [sessionManager, setSessionManager] = useState(initialSessionManager);
|
|
281
71
|
const [externalRuntimeBinding, setExternalRuntimeBinding] = useState(() => initialSessionManager?.getMetadata().externalRuntime);
|
|
282
72
|
const sessionManagerRef = useRef(sessionManager);
|
|
@@ -372,8 +162,30 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
372
162
|
});
|
|
373
163
|
return () => { cancelled = true; };
|
|
374
164
|
}, [args.cwd]);
|
|
165
|
+
// Push-based sync: the agent notifies on every resident-context mutation
|
|
166
|
+
// (message rewrites from /clear /compact /rewind and session switches,
|
|
167
|
+
// per-message appends during runs, model/provider switches), so this single
|
|
168
|
+
// subscription replaces the per-path manual refreshes that were easy to
|
|
169
|
+
// forget (the /clear footer bug). Initial read covers agents mounted with
|
|
170
|
+
// pre-existing history.
|
|
171
|
+
//
|
|
172
|
+
// Notifications are trailing-edge debounced: a snapshot run is O(all
|
|
173
|
+
// resident messages) (tiktoken providers measured ~225ms at 400 messages),
|
|
174
|
+
// and a tool-dense turn appends 5-15 messages synchronously — recomputing
|
|
175
|
+
// per append would block the run loop 1-3s. The footer is a human-facing
|
|
176
|
+
// readout; one recomputation 150ms after the last append is indistinguishable.
|
|
375
177
|
useEffect(() => {
|
|
376
|
-
|
|
178
|
+
let timer;
|
|
179
|
+
const update = () => setContextUsage(formatContextUsageLabel(agent.getContextUsageSnapshot()));
|
|
180
|
+
update();
|
|
181
|
+
const off = agent.onContextChanged(() => {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
timer = setTimeout(update, 150);
|
|
184
|
+
});
|
|
185
|
+
return () => {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
off();
|
|
188
|
+
};
|
|
377
189
|
}, [agent]);
|
|
378
190
|
const [currentUpdateNotice, setCurrentUpdateNotice] = useState(updateNotice);
|
|
379
191
|
const [pendingPlan, setPendingPlan] = useState(null);
|
|
@@ -403,12 +215,19 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
403
215
|
// MessageList so Ink discards its already-printed rows and re-prints the
|
|
404
216
|
// rebuilt list onto a freshly-cleared screen instead of appending duplicates.
|
|
405
217
|
const [staticGeneration, setStaticGeneration] = useState(0);
|
|
218
|
+
// The clear must go through Ink's stdout hook, not a raw process.stdout
|
|
219
|
+
// write: Ink replays its last dynamic frame after any external write
|
|
220
|
+
// (restoreLastOutput), which is the only repaint guarantee when the
|
|
221
|
+
// generation bump produces no new output — with zero settled rows (fresh
|
|
222
|
+
// welcome screen) <Static> emits nothing and the dynamic frame diffs equal,
|
|
223
|
+
// so Ink skips the write and a raw clear would leave the screen blank.
|
|
224
|
+
const { write: writeThroughInk } = useStdout();
|
|
406
225
|
const reprintTranscript = useCallback(() => {
|
|
407
226
|
if (process.stdout.isTTY) {
|
|
408
|
-
|
|
227
|
+
writeThroughInk("\x1b[0m\x1b[2J\x1b[3J\x1b[H");
|
|
409
228
|
}
|
|
410
229
|
setStaticGeneration((generation) => generation + 1);
|
|
411
|
-
}, []);
|
|
230
|
+
}, [writeThroughInk]);
|
|
412
231
|
// Steer/queue while the agent runs:
|
|
413
232
|
// Enter steers the current run via the agent's input controller; Tab (or an
|
|
414
233
|
// ineligible input) queues for the next turn. Both render placeholder user
|
|
@@ -1098,6 +917,11 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1098
917
|
setThinkingLevel,
|
|
1099
918
|
sessionManager: targetSessionManager,
|
|
1100
919
|
});
|
|
920
|
+
// Persist the resolved effort alongside the model so a fresh session
|
|
921
|
+
// restores both. Without this, the picker path only remembered the model
|
|
922
|
+
// and new sessions fell back to a stale config.json defaultThinkingLevel
|
|
923
|
+
// (e.g. picking glm-5.3 + max still restored "high" on next launch).
|
|
924
|
+
userConfig.setDefaultThinkingLevel(nextThinkingLevel);
|
|
1101
925
|
// Binary thinking toggles and thinking-only models use internal level
|
|
1102
926
|
// placeholders; do not present those placeholders as real effort grades.
|
|
1103
927
|
const decodedModel = decodeModel(model);
|
|
@@ -1107,7 +931,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1107
931
|
const effortNote = nextThinkingLevel && nextThinkingLevel !== "off"
|
|
1108
932
|
? (isThinkingToggle || isThinkingOnly ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
|
|
1109
933
|
: "";
|
|
1110
|
-
setContextUsage(formatContextUsageLabel(agent.getContextUsageSnapshot()));
|
|
1111
934
|
addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}.`, "ui_notice");
|
|
1112
935
|
closePicker();
|
|
1113
936
|
return nextThinkingLevel;
|
|
@@ -1167,8 +990,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1167
990
|
transitionToNative,
|
|
1168
991
|
onExternalRuntimeChange: refreshExternalRuntimeBinding,
|
|
1169
992
|
});
|
|
1170
|
-
if (handled
|
|
1171
|
-
|
|
993
|
+
if (handled) {
|
|
994
|
+
if (result)
|
|
995
|
+
addMessage("assistant", result, slashResultNoticeKind(result));
|
|
996
|
+
}
|
|
1172
997
|
}, [
|
|
1173
998
|
addMessage,
|
|
1174
999
|
agent,
|
|
@@ -1758,7 +1583,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1758
1583
|
goalRunTokens += tokenUsageTotal(event.usage);
|
|
1759
1584
|
}
|
|
1760
1585
|
assistantSystemFingerprint = event.systemFingerprint;
|
|
1761
|
-
setContextUsage(formatContextUsageLabel(agent.getContextUsageSnapshot()));
|
|
1762
1586
|
if (event.willContinue) {
|
|
1763
1587
|
commitAssistantMessage();
|
|
1764
1588
|
clearAssistantStream();
|
|
@@ -2562,416 +2386,3 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2562
2386
|
contextUsage: externalSessionBound ? undefined : (contextUsage || undefined),
|
|
2563
2387
|
}) }) }))] }) }));
|
|
2564
2388
|
}
|
|
2565
|
-
function buildCommandPaletteItems(skillRegistry, grokSessionBound = false) {
|
|
2566
|
-
const items = new Map();
|
|
2567
|
-
const add = (item) => {
|
|
2568
|
-
const key = `${item.action ?? "command"}:${item.value}`;
|
|
2569
|
-
if (!items.has(key))
|
|
2570
|
-
items.set(key, item);
|
|
2571
|
-
};
|
|
2572
|
-
if (!grokSessionBound) {
|
|
2573
|
-
for (const command of INK_LOCAL_SLASH_COMMANDS) {
|
|
2574
|
-
add({
|
|
2575
|
-
label: `/${command.name}`,
|
|
2576
|
-
detail: command.description,
|
|
2577
|
-
value: command.name,
|
|
2578
|
-
command: `/${command.name}`,
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
for (const command of slashRegistry.list()) {
|
|
2583
|
-
if (grokSessionBound && !isGrokLocalSlashCommand(command.name))
|
|
2584
|
-
continue;
|
|
2585
|
-
const source = command.source === "mcp" ? " :mcp" : "";
|
|
2586
|
-
const sourceLabel = command.sourceLabel ? `[${command.sourceLabel}] ` : "";
|
|
2587
|
-
add({
|
|
2588
|
-
label: `/${command.name}${source}`,
|
|
2589
|
-
detail: `${sourceLabel}${command.description}`,
|
|
2590
|
-
value: command.name,
|
|
2591
|
-
command: `/${command.name}`,
|
|
2592
|
-
});
|
|
2593
|
-
}
|
|
2594
|
-
if (!grokSessionBound) {
|
|
2595
|
-
for (const skill of skillRegistry.summaries()) {
|
|
2596
|
-
add({
|
|
2597
|
-
label: `/${skill.name} :skill`,
|
|
2598
|
-
detail: `[${skill.source}] ${skill.description}`,
|
|
2599
|
-
value: skill.name,
|
|
2600
|
-
command: `/${skill.name}`,
|
|
2601
|
-
action: "insert-skill",
|
|
2602
|
-
});
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
return [...items.values()];
|
|
2606
|
-
}
|
|
2607
|
-
function buildMcpReconnectItems(mcpManager) {
|
|
2608
|
-
return (mcpManager?.getStates() ?? []).map((state) => {
|
|
2609
|
-
let detail;
|
|
2610
|
-
if (state.status.kind === "connected") {
|
|
2611
|
-
const tools = state.status.tools.length;
|
|
2612
|
-
const prompts = state.status.prompts.length;
|
|
2613
|
-
detail = `connected · ${tools} tool${tools === 1 ? "" : "s"} · ${prompts} prompt${prompts === 1 ? "" : "s"}`;
|
|
2614
|
-
}
|
|
2615
|
-
else if (state.status.kind === "failed") {
|
|
2616
|
-
detail = `failed · ${state.status.error}`;
|
|
2617
|
-
}
|
|
2618
|
-
else {
|
|
2619
|
-
detail = "disabled";
|
|
2620
|
-
}
|
|
2621
|
-
return {
|
|
2622
|
-
label: state.name,
|
|
2623
|
-
detail,
|
|
2624
|
-
value: state.name,
|
|
2625
|
-
command: `/mcp reconnect ${state.name}`,
|
|
2626
|
-
};
|
|
2627
|
-
});
|
|
2628
|
-
}
|
|
2629
|
-
function CommandPalette({ items, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
2630
|
-
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 }));
|
|
2631
|
-
}
|
|
2632
|
-
function McpReconnectPicker({ items, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
2633
|
-
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 }));
|
|
2634
|
-
}
|
|
2635
|
-
function PalettePicker({ title, hint, emptyText, items, terminalColumns, terminalRows, searchable = false, reconnectAlias = false, onSelect, onCancel, }) {
|
|
2636
|
-
const theme = useTheme();
|
|
2637
|
-
const [query, setQuery] = useState("");
|
|
2638
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
2639
|
-
const maxVisible = Math.max(5, Math.min(12, terminalRows - 10));
|
|
2640
|
-
const filtered = useMemo(() => {
|
|
2641
|
-
const needle = query.trim().toLowerCase();
|
|
2642
|
-
if (!needle)
|
|
2643
|
-
return items;
|
|
2644
|
-
return items.filter((item) => item.label.toLowerCase().includes(needle) ||
|
|
2645
|
-
item.detail.toLowerCase().includes(needle) ||
|
|
2646
|
-
item.value.toLowerCase().includes(needle));
|
|
2647
|
-
}, [items, query]);
|
|
2648
|
-
useEffect(() => {
|
|
2649
|
-
setSelectedIndex((current) => Math.min(Math.max(0, filtered.length - 1), current));
|
|
2650
|
-
}, [filtered.length]);
|
|
2651
|
-
useInput((input, key) => {
|
|
2652
|
-
if (isKeyReleaseEvent(key))
|
|
2653
|
-
return;
|
|
2654
|
-
if (key.escape) {
|
|
2655
|
-
onCancel();
|
|
2656
|
-
return;
|
|
2657
|
-
}
|
|
2658
|
-
if (key.return || (reconnectAlias && input.toLowerCase() === "r")) {
|
|
2659
|
-
const item = filtered[selectedIndex];
|
|
2660
|
-
if (item)
|
|
2661
|
-
onSelect(item);
|
|
2662
|
-
return;
|
|
2663
|
-
}
|
|
2664
|
-
if (key.upArrow) {
|
|
2665
|
-
setSelectedIndex((index) => Math.max(0, index - 1));
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
if (key.downArrow) {
|
|
2669
|
-
setSelectedIndex((index) => Math.min(Math.max(0, filtered.length - 1), index + 1));
|
|
2670
|
-
return;
|
|
2671
|
-
}
|
|
2672
|
-
if (key.pageUp) {
|
|
2673
|
-
setSelectedIndex((index) => Math.max(0, index - maxVisible));
|
|
2674
|
-
return;
|
|
2675
|
-
}
|
|
2676
|
-
if (key.pageDown) {
|
|
2677
|
-
setSelectedIndex((index) => Math.min(Math.max(0, filtered.length - 1), index + maxVisible));
|
|
2678
|
-
return;
|
|
2679
|
-
}
|
|
2680
|
-
if (!searchable)
|
|
2681
|
-
return;
|
|
2682
|
-
if (key.backspace || key.delete) {
|
|
2683
|
-
setQuery((current) => current.slice(0, -1));
|
|
2684
|
-
return;
|
|
2685
|
-
}
|
|
2686
|
-
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
2687
|
-
setQuery((current) => current + input);
|
|
2688
|
-
}
|
|
2689
|
-
});
|
|
2690
|
-
const start = clampWindowStartForIndex(filtered.length, selectedIndex, maxVisible);
|
|
2691
|
-
const visible = filtered.slice(start, start + maxVisible);
|
|
2692
|
-
const labelWidth = Math.max(18, Math.min(36, Math.floor(terminalColumns * 0.32)));
|
|
2693
|
-
const detailWidth = Math.max(20, terminalColumns - labelWidth - 10);
|
|
2694
|
-
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) => {
|
|
2695
|
-
const actualIndex = start + offset;
|
|
2696
|
-
const selected = actualIndex === selectedIndex;
|
|
2697
|
-
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}`));
|
|
2698
|
-
})] })] }));
|
|
2699
|
-
}
|
|
2700
|
-
const REWIND_SCOPE_ORDER = ["all", "chat", "code"];
|
|
2701
|
-
const REWIND_SCOPE_LABEL = {
|
|
2702
|
-
all: "chat + files",
|
|
2703
|
-
chat: "chat only",
|
|
2704
|
-
code: "files only",
|
|
2705
|
-
};
|
|
2706
|
-
function rewindCommand(turnIndex, scope) {
|
|
2707
|
-
const base = `/rewind ${turnIndex + 1}`;
|
|
2708
|
-
if (scope === "chat")
|
|
2709
|
-
return `${base} --chat`;
|
|
2710
|
-
if (scope === "code")
|
|
2711
|
-
return `${base} --code`;
|
|
2712
|
-
return base;
|
|
2713
|
-
}
|
|
2714
|
-
function cycleRewindScope(scope, direction) {
|
|
2715
|
-
const index = REWIND_SCOPE_ORDER.indexOf(scope);
|
|
2716
|
-
return REWIND_SCOPE_ORDER[(index + direction + REWIND_SCOPE_ORDER.length) % REWIND_SCOPE_ORDER.length];
|
|
2717
|
-
}
|
|
2718
|
-
function RewindPicker({ sessionManager, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
2719
|
-
const theme = useTheme();
|
|
2720
|
-
const turns = useMemo(() => sessionManager.listUserTurns(), [sessionManager]);
|
|
2721
|
-
const checkpoints = useMemo(() => sessionManager.getCheckpoints(), [sessionManager]);
|
|
2722
|
-
const fileCounts = useMemo(() => {
|
|
2723
|
-
const entries = checkpoints.listEntries();
|
|
2724
|
-
const byTurn = new Map();
|
|
2725
|
-
for (const entry of entries) {
|
|
2726
|
-
const files = byTurn.get(entry.turn);
|
|
2727
|
-
if (files)
|
|
2728
|
-
files.add(entry.path);
|
|
2729
|
-
else
|
|
2730
|
-
byTurn.set(entry.turn, new Set([entry.path]));
|
|
2731
|
-
}
|
|
2732
|
-
return new Map(turns.map((turn) => [turn.id, byTurn.get(turn.id)?.size ?? 0]));
|
|
2733
|
-
}, [checkpoints, turns]);
|
|
2734
|
-
const [selectedIndex, setSelectedIndex] = useState(() => Math.max(0, turns.length - 1));
|
|
2735
|
-
const [scope, setScope] = useState("all");
|
|
2736
|
-
const maxVisible = Math.max(4, Math.min(10, terminalRows - 10));
|
|
2737
|
-
useEffect(() => {
|
|
2738
|
-
setSelectedIndex((current) => Math.min(Math.max(0, turns.length - 1), current));
|
|
2739
|
-
}, [turns.length]);
|
|
2740
|
-
useInput((input, key) => {
|
|
2741
|
-
if (isKeyReleaseEvent(key))
|
|
2742
|
-
return;
|
|
2743
|
-
if (key.escape) {
|
|
2744
|
-
onCancel();
|
|
2745
|
-
return;
|
|
2746
|
-
}
|
|
2747
|
-
if (key.return) {
|
|
2748
|
-
if (turns[selectedIndex])
|
|
2749
|
-
onSelect(rewindCommand(selectedIndex, scope));
|
|
2750
|
-
return;
|
|
2751
|
-
}
|
|
2752
|
-
if (key.upArrow) {
|
|
2753
|
-
setSelectedIndex((index) => Math.max(0, index - 1));
|
|
2754
|
-
return;
|
|
2755
|
-
}
|
|
2756
|
-
if (key.downArrow) {
|
|
2757
|
-
setSelectedIndex((index) => Math.min(Math.max(0, turns.length - 1), index + 1));
|
|
2758
|
-
return;
|
|
2759
|
-
}
|
|
2760
|
-
if (key.pageUp) {
|
|
2761
|
-
setSelectedIndex((index) => Math.max(0, index - maxVisible));
|
|
2762
|
-
return;
|
|
2763
|
-
}
|
|
2764
|
-
if (key.pageDown) {
|
|
2765
|
-
setSelectedIndex((index) => Math.min(Math.max(0, turns.length - 1), index + maxVisible));
|
|
2766
|
-
return;
|
|
2767
|
-
}
|
|
2768
|
-
if (key.tab || key.rightArrow || input === "l") {
|
|
2769
|
-
setScope((current) => cycleRewindScope(current, 1));
|
|
2770
|
-
return;
|
|
2771
|
-
}
|
|
2772
|
-
if (key.leftArrow || input === "h") {
|
|
2773
|
-
setScope((current) => cycleRewindScope(current, -1));
|
|
2774
|
-
}
|
|
2775
|
-
});
|
|
2776
|
-
const start = clampWindowStartForIndex(turns.length, selectedIndex, maxVisible);
|
|
2777
|
-
const visibleTurns = turns.slice(start, start + maxVisible);
|
|
2778
|
-
const previewWidth = Math.max(18, Math.min(76, terminalColumns - 34));
|
|
2779
|
-
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) => {
|
|
2780
|
-
const actualIndex = start + offset;
|
|
2781
|
-
const isSelected = actualIndex === selectedIndex;
|
|
2782
|
-
const touched = fileCounts.get(turn.id) ?? 0;
|
|
2783
|
-
return (_jsx(RewindRow, { turn: turn, turnNumber: actualIndex + 1, selected: isSelected, fileCount: touched, previewWidth: previewWidth }, turn.id));
|
|
2784
|
-
})] })] }));
|
|
2785
|
-
}
|
|
2786
|
-
function RewindRow({ turn, turnNumber, selected, fileCount, previewWidth, }) {
|
|
2787
|
-
const theme = useTheme();
|
|
2788
|
-
const time = new Date(turn.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
2789
|
-
const fileNote = fileCount > 0 ? ` · ${fileCount} file${fileCount === 1 ? "" : "s"}` : "";
|
|
2790
|
-
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 })] }));
|
|
2791
|
-
}
|
|
2792
|
-
function clampWindowStartForIndex(total, selectedIndex, maxVisible) {
|
|
2793
|
-
if (total <= maxVisible)
|
|
2794
|
-
return 0;
|
|
2795
|
-
const half = Math.floor(maxVisible / 2);
|
|
2796
|
-
let start = Math.max(0, selectedIndex - half);
|
|
2797
|
-
if (start + maxVisible > total)
|
|
2798
|
-
start = total - maxVisible;
|
|
2799
|
-
return Math.max(0, start);
|
|
2800
|
-
}
|
|
2801
|
-
function StatsPanel({ panel, terminalColumns, terminalRows, onRangeChange, onCancel, }) {
|
|
2802
|
-
const theme = useTheme();
|
|
2803
|
-
const [scroll, setScroll] = useState(0);
|
|
2804
|
-
const bodyWidth = Math.max(48, Math.min(92, terminalColumns - 6));
|
|
2805
|
-
const lines = useMemo(() => formatStatsPanelBody(panel.bundle.ranges[panel.range], bodyWidth).split("\n"), [bodyWidth, panel.bundle, panel.range]);
|
|
2806
|
-
const maxVisible = Math.max(5, Math.min(16, terminalRows - 10));
|
|
2807
|
-
const maxScroll = Math.max(0, lines.length - maxVisible);
|
|
2808
|
-
useEffect(() => {
|
|
2809
|
-
setScroll(0);
|
|
2810
|
-
}, [panel.range]);
|
|
2811
|
-
useEffect(() => {
|
|
2812
|
-
setScroll((current) => Math.min(current, maxScroll));
|
|
2813
|
-
}, [maxScroll]);
|
|
2814
|
-
useInput((input, key) => {
|
|
2815
|
-
if (isKeyReleaseEvent(key))
|
|
2816
|
-
return;
|
|
2817
|
-
if (key.escape) {
|
|
2818
|
-
onCancel();
|
|
2819
|
-
return;
|
|
2820
|
-
}
|
|
2821
|
-
if (key.tab) {
|
|
2822
|
-
onRangeChange(panel.range === "30d" ? "7d" : "30d");
|
|
2823
|
-
return;
|
|
2824
|
-
}
|
|
2825
|
-
if (key.leftArrow || input === "h") {
|
|
2826
|
-
onRangeChange("7d");
|
|
2827
|
-
return;
|
|
2828
|
-
}
|
|
2829
|
-
if (key.rightArrow || input === "l") {
|
|
2830
|
-
onRangeChange("30d");
|
|
2831
|
-
return;
|
|
2832
|
-
}
|
|
2833
|
-
if (key.upArrow) {
|
|
2834
|
-
setScroll((current) => Math.max(0, current - 1));
|
|
2835
|
-
return;
|
|
2836
|
-
}
|
|
2837
|
-
if (key.downArrow) {
|
|
2838
|
-
setScroll((current) => Math.min(maxScroll, current + 1));
|
|
2839
|
-
return;
|
|
2840
|
-
}
|
|
2841
|
-
if (key.pageUp) {
|
|
2842
|
-
setScroll((current) => Math.max(0, current - maxVisible));
|
|
2843
|
-
return;
|
|
2844
|
-
}
|
|
2845
|
-
if (key.pageDown) {
|
|
2846
|
-
setScroll((current) => Math.min(maxScroll, current + maxVisible));
|
|
2847
|
-
}
|
|
2848
|
-
});
|
|
2849
|
-
const visible = lines.slice(scroll, scroll + maxVisible);
|
|
2850
|
-
const generatedAt = panel.bundle.generatedAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
2851
|
-
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) => {
|
|
2852
|
-
const key = `${scroll + index}-${line}`;
|
|
2853
|
-
const heading = line === "Activity" || line === "Model usage" || line === "Summary";
|
|
2854
|
-
return (_jsx(Text, { color: heading ? theme.accent : undefined, bold: heading, children: line || " " }, key));
|
|
2855
|
-
}) }), maxScroll > 0 && (_jsxs(Text, { color: theme.muted, children: [scroll + 1, "-", Math.min(lines.length, scroll + maxVisible), " of ", lines.length] }))] }));
|
|
2856
|
-
}
|
|
2857
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2858
|
-
const GENERIC_PHRASES = [
|
|
2859
|
-
"mapping the workspace",
|
|
2860
|
-
"reading the room",
|
|
2861
|
-
"following the threads",
|
|
2862
|
-
"connecting the pieces",
|
|
2863
|
-
"sorting the context",
|
|
2864
|
-
"scanning the structure",
|
|
2865
|
-
"shaping the next step",
|
|
2866
|
-
"gathering signal",
|
|
2867
|
-
"checking the edges",
|
|
2868
|
-
"lining up the answer",
|
|
2869
|
-
"tracing the flow",
|
|
2870
|
-
"building the picture",
|
|
2871
|
-
"walking the graph",
|
|
2872
|
-
"collecting the clues",
|
|
2873
|
-
"framing the problem",
|
|
2874
|
-
"locating the source",
|
|
2875
|
-
"resolving the shape",
|
|
2876
|
-
"untangling the state",
|
|
2877
|
-
"comparing the paths",
|
|
2878
|
-
"narrowing the target",
|
|
2879
|
-
"tracking the changes",
|
|
2880
|
-
"reading the patterns",
|
|
2881
|
-
"weighing the options",
|
|
2882
|
-
"assembling the context",
|
|
2883
|
-
"following the signal",
|
|
2884
|
-
"checking the assumptions",
|
|
2885
|
-
"aligning the details",
|
|
2886
|
-
"testing the shape",
|
|
2887
|
-
"pulling the thread",
|
|
2888
|
-
"cleaning the edges",
|
|
2889
|
-
"refining the draft",
|
|
2890
|
-
"verifying the route",
|
|
2891
|
-
"making sense of it",
|
|
2892
|
-
"looking for leverage",
|
|
2893
|
-
"stitching the answer",
|
|
2894
|
-
"holding the thread",
|
|
2895
|
-
"distilling the noise",
|
|
2896
|
-
"finding the seam",
|
|
2897
|
-
"reading between the lines",
|
|
2898
|
-
"preparing the response",
|
|
2899
|
-
];
|
|
2900
|
-
const TOOL_TARGET_PHRASES = {
|
|
2901
|
-
read: "reading files",
|
|
2902
|
-
write: "writing changes",
|
|
2903
|
-
edit: "patching files",
|
|
2904
|
-
grep: "searching the codebase",
|
|
2905
|
-
glob: "scanning paths",
|
|
2906
|
-
ls: "listing directories",
|
|
2907
|
-
bash: "running command",
|
|
2908
|
-
web_search: "searching the web",
|
|
2909
|
-
web_fetch: "fetching a page",
|
|
2910
|
-
task: "spawning subagent",
|
|
2911
|
-
};
|
|
2912
|
-
function formatTokensApprox(chars) {
|
|
2913
|
-
const tokens = Math.max(0, Math.round(chars / 4));
|
|
2914
|
-
if (tokens < 1000)
|
|
2915
|
-
return `${tokens}`;
|
|
2916
|
-
if (tokens < 10000)
|
|
2917
|
-
return `${(tokens / 1000).toFixed(1)}k`;
|
|
2918
|
-
return `${Math.round(tokens / 1000)}k`;
|
|
2919
|
-
}
|
|
2920
|
-
function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars, nowTick, pendingSteerCount = 0, queuedCount = 0, }) {
|
|
2921
|
-
void nowTick;
|
|
2922
|
-
const theme = useTheme();
|
|
2923
|
-
const [frameIndex, setFrameIndex] = useState(0);
|
|
2924
|
-
const [idlePhrase, setIdlePhrase] = useState(() => GENERIC_PHRASES[0]);
|
|
2925
|
-
// Frame timer is independent of the agent state — keeps animation smooth.
|
|
2926
|
-
useEffect(() => {
|
|
2927
|
-
const t = setInterval(() => {
|
|
2928
|
-
setFrameIndex((i) => (i + 1) % SPINNER_FRAMES.length);
|
|
2929
|
-
}, 100);
|
|
2930
|
-
return () => clearInterval(t);
|
|
2931
|
-
}, []);
|
|
2932
|
-
// Determine state: active tool > streaming text > streaming reasoning > idle
|
|
2933
|
-
const activeTool = [...tools].reverse().find((t) => !t.result);
|
|
2934
|
-
const state = activeTool
|
|
2935
|
-
? "tool"
|
|
2936
|
-
: hasStreamingText
|
|
2937
|
-
? "text"
|
|
2938
|
-
: hasStreamingReasoning
|
|
2939
|
-
? "reasoning"
|
|
2940
|
-
: "idle";
|
|
2941
|
-
// Rotate idle phrases on a slower cadence; only matters in the idle state.
|
|
2942
|
-
useEffect(() => {
|
|
2943
|
-
if (state !== "idle")
|
|
2944
|
-
return;
|
|
2945
|
-
const t = setInterval(() => {
|
|
2946
|
-
setIdlePhrase((current) => {
|
|
2947
|
-
const candidates = GENERIC_PHRASES.filter((item) => item !== current);
|
|
2948
|
-
return candidates[Math.floor(Math.random() * candidates.length)] || current;
|
|
2949
|
-
});
|
|
2950
|
-
}, 1500);
|
|
2951
|
-
return () => clearInterval(t);
|
|
2952
|
-
}, [state]);
|
|
2953
|
-
let phrase;
|
|
2954
|
-
if (state === "tool" && activeTool) {
|
|
2955
|
-
phrase =
|
|
2956
|
-
TOOL_TARGET_PHRASES[activeTool.name] || `running ${activeTool.name}`;
|
|
2957
|
-
}
|
|
2958
|
-
else if (state === "text") {
|
|
2959
|
-
phrase = "writing the response";
|
|
2960
|
-
}
|
|
2961
|
-
else if (state === "reasoning") {
|
|
2962
|
-
phrase = "working through the request";
|
|
2963
|
-
}
|
|
2964
|
-
else {
|
|
2965
|
-
phrase = idlePhrase;
|
|
2966
|
-
}
|
|
2967
|
-
const tokenText = streamedChars > 0 ? `↓${formatTokensApprox(streamedChars)} tok` : "";
|
|
2968
|
-
const hintParts = [];
|
|
2969
|
-
if (tokenText)
|
|
2970
|
-
hintParts.push(tokenText);
|
|
2971
|
-
if (pendingSteerCount > 0)
|
|
2972
|
-
hintParts.push(`${pendingSteerCount} pending steer${pendingSteerCount === 1 ? "" : "s"}`);
|
|
2973
|
-
if (queuedCount > 0)
|
|
2974
|
-
hintParts.push(`${queuedCount} queued`);
|
|
2975
|
-
hintParts.push("enter steer", "tab queue", "esc stop");
|
|
2976
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: SPINNER_FRAMES[frameIndex] }), _jsxs(Text, { color: theme.muted, children: [" ", phrase, " "] }), _jsxs(Text, { color: theme.muted, dimColor: true, children: ["(", hintParts.join(" · "), ")"] })] }));
|
|
2977
|
-
}
|