@bubblebrain-ai/bubble 0.0.49 → 0.0.51
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/profiles.js +3 -4
- 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/subtask-policy.js +4 -4
- 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 +33 -26
- package/dist/agent.js +80 -334
- package/dist/debug-trace.js +0 -2
- package/dist/external-runtime/grok-runtime.js +1 -1
- package/dist/feishu/agent-host/run-driver.js +1 -8
- package/dist/feishu/card/run-state.js +0 -3
- package/dist/lsp/service.js +2 -2
- package/dist/main.js +0 -15
- package/dist/memory/phase1.js +0 -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/prompt/compose.js +0 -3
- package/dist/prompt/environment.js +0 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/provider-registry.js +13 -1
- package/dist/provider-transform.js +7 -8
- package/dist/sdk/index.d.ts +17 -2
- package/dist/sdk/index.js +37 -6
- package/dist/session-log.d.ts +2 -4
- package/dist/session-log.js +0 -23
- package/dist/session-types.d.ts +2 -6
- package/dist/session.d.ts +1 -3
- package/dist/session.js +0 -8
- package/dist/slash-commands/commands.js +13 -13
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +1 -1
- package/dist/tools/grep.js +1 -1
- package/dist/tools/index.d.ts +0 -3
- package/dist/tools/index.js +0 -3
- 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 +57 -657
- 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/message-list.js +0 -1
- 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/dist/types.d.ts +0 -9
- package/package.json +1 -1
- package/dist/tools/todo.d.ts +0 -12
- package/dist/tools/todo.js +0 -136
- package/dist/tui-ink/todos.d.ts +0 -7
- package/dist/tui-ink/todos.js +0 -46
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";
|
|
@@ -24,7 +23,6 @@ import { useTerminalSize } from "./use-terminal-size.js";
|
|
|
24
23
|
import { WelcomeBanner, shouldShowWelcomeBanner } from "./welcome.js";
|
|
25
24
|
import { BubbleCodeWordmark } from "./wordmark.js";
|
|
26
25
|
import { expandAtMentions } from "./file-mentions.js";
|
|
27
|
-
import { TodosPanel } from "./todos.js";
|
|
28
26
|
import { CompactionProgressCard } from "./compaction-progress.js";
|
|
29
27
|
import { PlanConfirm } from "./plan-confirm.js";
|
|
30
28
|
import { ApprovalDialog } from "./approval/approval-dialog.js";
|
|
@@ -49,234 +47,25 @@ import { tokenUsageTotal } from "../goal/usage.js";
|
|
|
49
47
|
import { formatInternalContextBlock, isInternalBlockOnlyContent } from "../agent/internal-reminder-sanitizer.js";
|
|
50
48
|
import { MAX_ACTIVE_LOOPS, decideLoopFiring, formatInterval, formatLoopList, parseLoopCommand, } from "../loop/engine.js";
|
|
51
49
|
import { TaskWakeCoalescer, findDanglingTaskStarts, formatTaskWakeSummary, isPidAlive, shouldFireTaskWake, taskEligibleForWake, } from "../tasks/wake.js";
|
|
52
|
-
|
|
53
|
-
import {
|
|
50
|
+
import { STREAMING_FLUSH_INTERVAL_MS, TASK_WAKE_DEBOUNCE_MS, buildTips, formatContextUsageLabel, friendlyCwd, parsePartialArgs, sessionBasename, slashResultNoticeKind, taskRowSummary, truncate, withMessageKey, } from "./app-helpers.js";
|
|
51
|
+
import { reconstructDisplayMessages } from "./display-reconstruct.js";
|
|
52
|
+
export { reconstructDisplayMessages } from "./display-reconstruct.js";
|
|
53
|
+
import { INK_LOCAL_SLASH_COMMANDS, CommandPalette, McpReconnectPicker, buildCommandPaletteItems, buildMcpReconnectItems, } from "./command-palette.js";
|
|
54
|
+
export { INK_LOCAL_SLASH_COMMANDS } from "./command-palette.js";
|
|
55
|
+
import { RewindPicker } from "./rewind-picker.js";
|
|
56
|
+
import { StatsPanel } from "./stats-panel.js";
|
|
57
|
+
import { WaitingIndicator } from "./waiting-indicator.js";
|
|
58
|
+
import { collectUsageStatsBundle } from "../stats/usage.js";
|
|
54
59
|
import { GrokRuntimeError } from "../external-runtime/grok-errors.js";
|
|
55
|
-
import { GROK_LOCAL_SLASH_COMMANDS, classifyGrokInput,
|
|
60
|
+
import { GROK_LOCAL_SLASH_COMMANDS, classifyGrokInput, } from "../external-runtime/grok-input-policy.js";
|
|
56
61
|
import { GROK_SUBSCRIPTION_PROVIDER_ID, isGrokSubscriptionProviderId, withGrokSubscriptionProvider, } from "../external-runtime/grok-provider.js";
|
|
57
62
|
import { classifyExternalRuntimeBinding, stopExternalRuntimeForSessionSwitch, } from "../external-runtime/session-policy.js";
|
|
58
|
-
import os from "node:os";
|
|
59
63
|
import { rmSync } from "node:fs";
|
|
60
64
|
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
65
|
// Canonical implementation lives in the sanitizer module so non-TUI surfaces
|
|
93
66
|
// (session pickers, desktop host) share it; re-exported here for existing users.
|
|
94
67
|
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 }) {
|
|
68
|
+
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
69
|
const [sessionManager, setSessionManager] = useState(initialSessionManager);
|
|
281
70
|
const [externalRuntimeBinding, setExternalRuntimeBinding] = useState(() => initialSessionManager?.getMetadata().externalRuntime);
|
|
282
71
|
const sessionManagerRef = useRef(sessionManager);
|
|
@@ -356,7 +145,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
356
145
|
// model ladder.
|
|
357
146
|
const [thinkingLevel, setThinkingLevel] = useState(agent.thinking);
|
|
358
147
|
const [permissionMode, setPermissionMode] = useState(agent.mode);
|
|
359
|
-
const [todos, setTodos] = useState(() => agent.getTodos());
|
|
360
148
|
const [goalLine, setGoalLine] = useState("");
|
|
361
149
|
const [branch, setBranch] = useState(undefined);
|
|
362
150
|
const [contextUsage, setContextUsage] = useState("");
|
|
@@ -372,8 +160,30 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
372
160
|
});
|
|
373
161
|
return () => { cancelled = true; };
|
|
374
162
|
}, [args.cwd]);
|
|
163
|
+
// Push-based sync: the agent notifies on every resident-context mutation
|
|
164
|
+
// (message rewrites from /clear /compact /rewind and session switches,
|
|
165
|
+
// per-message appends during runs, model/provider switches), so this single
|
|
166
|
+
// subscription replaces the per-path manual refreshes that were easy to
|
|
167
|
+
// forget (the /clear footer bug). Initial read covers agents mounted with
|
|
168
|
+
// pre-existing history.
|
|
169
|
+
//
|
|
170
|
+
// Notifications are trailing-edge debounced: a snapshot run is O(all
|
|
171
|
+
// resident messages) (tiktoken providers measured ~225ms at 400 messages),
|
|
172
|
+
// and a tool-dense turn appends 5-15 messages synchronously — recomputing
|
|
173
|
+
// per append would block the run loop 1-3s. The footer is a human-facing
|
|
174
|
+
// readout; one recomputation 150ms after the last append is indistinguishable.
|
|
375
175
|
useEffect(() => {
|
|
376
|
-
|
|
176
|
+
let timer;
|
|
177
|
+
const update = () => setContextUsage(formatContextUsageLabel(agent.getContextUsageSnapshot()));
|
|
178
|
+
update();
|
|
179
|
+
const off = agent.onContextChanged(() => {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
timer = setTimeout(update, 150);
|
|
182
|
+
});
|
|
183
|
+
return () => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
off();
|
|
186
|
+
};
|
|
377
187
|
}, [agent]);
|
|
378
188
|
const [currentUpdateNotice, setCurrentUpdateNotice] = useState(updateNotice);
|
|
379
189
|
const [pendingPlan, setPendingPlan] = useState(null);
|
|
@@ -403,12 +213,19 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
403
213
|
// MessageList so Ink discards its already-printed rows and re-prints the
|
|
404
214
|
// rebuilt list onto a freshly-cleared screen instead of appending duplicates.
|
|
405
215
|
const [staticGeneration, setStaticGeneration] = useState(0);
|
|
216
|
+
// The clear must go through Ink's stdout hook, not a raw process.stdout
|
|
217
|
+
// write: Ink replays its last dynamic frame after any external write
|
|
218
|
+
// (restoreLastOutput), which is the only repaint guarantee when the
|
|
219
|
+
// generation bump produces no new output — with zero settled rows (fresh
|
|
220
|
+
// welcome screen) <Static> emits nothing and the dynamic frame diffs equal,
|
|
221
|
+
// so Ink skips the write and a raw clear would leave the screen blank.
|
|
222
|
+
const { write: writeThroughInk } = useStdout();
|
|
406
223
|
const reprintTranscript = useCallback(() => {
|
|
407
224
|
if (process.stdout.isTTY) {
|
|
408
|
-
|
|
225
|
+
writeThroughInk("\x1b[0m\x1b[2J\x1b[3J\x1b[H");
|
|
409
226
|
}
|
|
410
227
|
setStaticGeneration((generation) => generation + 1);
|
|
411
|
-
}, []);
|
|
228
|
+
}, [writeThroughInk]);
|
|
412
229
|
// Steer/queue while the agent runs:
|
|
413
230
|
// Enter steers the current run via the agent's input controller; Tab (or an
|
|
414
231
|
// ineligible input) queues for the next turn. Both render placeholder user
|
|
@@ -825,10 +642,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
825
642
|
// <Static>), so clearing React state is not enough — resetTranscript wipes
|
|
826
643
|
// the screen + scrollback and re-prints the (now empty) transcript.
|
|
827
644
|
resetTranscript(() => []);
|
|
828
|
-
// The todos panel renders off React state, not the transcript, so wiping
|
|
829
|
-
// messages alone leaves a stale To-Do list on screen. /clear already reset
|
|
830
|
-
// the agent's todos; mirror that into the UI (same as session switch).
|
|
831
|
-
setTodos(agent.getTodos());
|
|
832
645
|
clearLiveSubagentTools();
|
|
833
646
|
}, [resetTranscript, agent, clearLiveSubagentTools]);
|
|
834
647
|
// Render a placeholder user row for input waiting to enter the run.
|
|
@@ -986,7 +799,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
986
799
|
const nextExternalBinding = result.manager.getMetadata().externalRuntime;
|
|
987
800
|
externalRuntimeBindingRef.current = nextExternalBinding;
|
|
988
801
|
setExternalRuntimeBinding(nextExternalBinding);
|
|
989
|
-
setTodos(agent.getTodos());
|
|
990
802
|
clearLiveSubagentTools();
|
|
991
803
|
resetTranscript(() => [
|
|
992
804
|
...reconstructDisplayMessages(agent.messages).filter((message) => !queuedDisplayKeys.has(message.key ?? "")),
|
|
@@ -1098,6 +910,11 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1098
910
|
setThinkingLevel,
|
|
1099
911
|
sessionManager: targetSessionManager,
|
|
1100
912
|
});
|
|
913
|
+
// Persist the resolved effort alongside the model so a fresh session
|
|
914
|
+
// restores both. Without this, the picker path only remembered the model
|
|
915
|
+
// and new sessions fell back to a stale config.json defaultThinkingLevel
|
|
916
|
+
// (e.g. picking glm-5.3 + max still restored "high" on next launch).
|
|
917
|
+
userConfig.setDefaultThinkingLevel(nextThinkingLevel);
|
|
1101
918
|
// Binary thinking toggles and thinking-only models use internal level
|
|
1102
919
|
// placeholders; do not present those placeholders as real effort grades.
|
|
1103
920
|
const decodedModel = decodeModel(model);
|
|
@@ -1107,7 +924,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1107
924
|
const effortNote = nextThinkingLevel && nextThinkingLevel !== "off"
|
|
1108
925
|
? (isThinkingToggle || isThinkingOnly ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
|
|
1109
926
|
: "";
|
|
1110
|
-
setContextUsage(formatContextUsageLabel(agent.getContextUsageSnapshot()));
|
|
1111
927
|
addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}.`, "ui_notice");
|
|
1112
928
|
closePicker();
|
|
1113
929
|
return nextThinkingLevel;
|
|
@@ -1167,8 +983,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1167
983
|
transitionToNative,
|
|
1168
984
|
onExternalRuntimeChange: refreshExternalRuntimeBinding,
|
|
1169
985
|
});
|
|
1170
|
-
if (handled
|
|
1171
|
-
|
|
986
|
+
if (handled) {
|
|
987
|
+
if (result)
|
|
988
|
+
addMessage("assistant", result, slashResultNoticeKind(result));
|
|
989
|
+
}
|
|
1172
990
|
}, [
|
|
1173
991
|
addMessage,
|
|
1174
992
|
agent,
|
|
@@ -1688,10 +1506,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1688
1506
|
}
|
|
1689
1507
|
break;
|
|
1690
1508
|
}
|
|
1691
|
-
case "todos_updated": {
|
|
1692
|
-
setTodos(event.todos);
|
|
1693
|
-
break;
|
|
1694
|
-
}
|
|
1695
1509
|
case "mode_changed": {
|
|
1696
1510
|
// Marker already persisted by onModeUpdate at setMode time.
|
|
1697
1511
|
setPermissionMode(event.mode);
|
|
@@ -1758,7 +1572,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1758
1572
|
goalRunTokens += tokenUsageTotal(event.usage);
|
|
1759
1573
|
}
|
|
1760
1574
|
assistantSystemFingerprint = event.systemFingerprint;
|
|
1761
|
-
setContextUsage(formatContextUsageLabel(agent.getContextUsageSnapshot()));
|
|
1762
1575
|
if (event.willContinue) {
|
|
1763
1576
|
commitAssistantMessage();
|
|
1764
1577
|
clearAssistantStream();
|
|
@@ -2494,7 +2307,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2494
2307
|
}, onCancel: () => {
|
|
2495
2308
|
closePicker();
|
|
2496
2309
|
addMessage("assistant", "已取消 Feishu setup。");
|
|
2497
|
-
} }) })), 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 }) })),
|
|
2310
|
+
} }) })), 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 }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
|
|
2498
2311
|
const resolve = pendingPlan.resolve;
|
|
2499
2312
|
setPendingPlan(null);
|
|
2500
2313
|
resolve({ action: "approve", plan: finalPlan });
|
|
@@ -2562,416 +2375,3 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2562
2375
|
contextUsage: externalSessionBound ? undefined : (contextUsage || undefined),
|
|
2563
2376
|
}) }) }))] }) }));
|
|
2564
2377
|
}
|
|
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
|
-
}
|