@bubblebrain-ai/bubble 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/agent.js +1 -1
- package/dist/clipboard.d.ts +14 -0
- package/dist/clipboard.js +132 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +22 -6
- package/dist/goal/format.js +34 -4
- package/dist/goal/store.d.ts +3 -0
- package/dist/goal/store.js +14 -1
- package/dist/goal/usage.d.ts +2 -0
- package/dist/goal/usage.js +3 -0
- package/dist/main.js +23 -42
- package/dist/model-catalog.d.ts +3 -1
- package/dist/model-catalog.js +17 -28
- package/dist/prompt/compose.js +1 -1
- package/dist/provider-anthropic.d.ts +4 -0
- package/dist/provider-anthropic.js +31 -0
- package/dist/provider-ark-responses.d.ts +17 -0
- package/dist/provider-ark-responses.js +462 -0
- package/dist/provider-transform.js +7 -0
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +170 -27
- package/dist/slash-commands/commands.js +22 -0
- package/dist/tools/todo.js +22 -38
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +23 -0
- package/dist/tui/image-display.d.ts +13 -0
- package/dist/tui/image-display.js +49 -0
- package/dist/tui/input-history.d.ts +37 -6
- package/dist/tui/input-history.js +194 -23
- package/dist/tui/model-switch.d.ts +42 -0
- package/dist/tui/model-switch.js +55 -0
- package/dist/tui-ink/app.d.ts +32 -2
- package/dist/tui-ink/app.js +1409 -549
- package/dist/tui-ink/approval/select.js +10 -0
- package/dist/tui-ink/detect-theme.d.ts +1 -2
- package/dist/tui-ink/detect-theme.js +1 -87
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +11 -0
- package/dist/tui-ink/feedback-dialog.js +10 -0
- package/dist/tui-ink/feishu-setup-picker.js +10 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +8 -2
- package/dist/tui-ink/input-box.d.ts +71 -9
- package/dist/tui-ink/input-box.js +359 -121
- package/dist/tui-ink/input-history.d.ts +1 -16
- package/dist/tui-ink/input-history.js +1 -79
- package/dist/tui-ink/input-queue.d.ts +12 -0
- package/dist/tui-ink/input-queue.js +17 -0
- package/dist/tui-ink/key-events.d.ts +9 -0
- package/dist/tui-ink/key-events.js +8 -0
- package/dist/tui-ink/markdown.js +1 -1
- package/dist/tui-ink/message-list.d.ts +19 -1
- package/dist/tui-ink/message-list.js +111 -32
- package/dist/tui-ink/model-picker.d.ts +25 -2
- package/dist/tui-ink/model-picker.js +237 -20
- package/dist/tui-ink/plan-confirm.js +10 -0
- package/dist/tui-ink/question-dialog.js +46 -10
- package/dist/tui-ink/run.d.ts +10 -1
- package/dist/tui-ink/run.js +27 -42
- package/dist/tui-ink/session-picker.js +3 -0
- package/dist/tui-ink/submit-dedupe.d.ts +5 -0
- package/dist/tui-ink/submit-dedupe.js +25 -0
- package/dist/tui-ink/terminal-mouse.d.ts +24 -1
- package/dist/tui-ink/terminal-mouse.js +76 -21
- package/dist/tui-ink/theme.d.ts +6 -3
- package/dist/tui-ink/theme.js +10 -4
- package/dist/tui-ink/welcome.d.ts +1 -0
- package/dist/tui-ink/welcome.js +34 -27
- package/dist/variant/variant-resolver.js +4 -1
- package/package.json +1 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -22
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -67
- package/dist/tui/run.js +0 -10166
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -135
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/types.js +0 -1
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -32
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- package/dist/tui/transcript-scroll.d.ts +0 -25
- package/dist/tui/transcript-scroll.js +0 -20
- package/dist/tui-ink/transcript-viewport-math.d.ts +0 -11
- package/dist/tui-ink/transcript-viewport-math.js +0 -17
- package/dist/tui-ink/transcript-viewport.d.ts +0 -24
- package/dist/tui-ink/transcript-viewport.js +0 -83
- package/dist/tui-opentui/app.d.ts +0 -54
- package/dist/tui-opentui/app.js +0 -1371
- package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
- package/dist/tui-opentui/approval/approval-dialog.js +0 -155
- package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
- package/dist/tui-opentui/approval/diff-view.js +0 -43
- package/dist/tui-opentui/approval/select.d.ts +0 -37
- package/dist/tui-opentui/approval/select.js +0 -91
- package/dist/tui-opentui/detect-theme.d.ts +0 -2
- package/dist/tui-opentui/detect-theme.js +0 -87
- package/dist/tui-opentui/display-history.d.ts +0 -56
- package/dist/tui-opentui/display-history.js +0 -130
- package/dist/tui-opentui/edit-diff.d.ts +0 -11
- package/dist/tui-opentui/edit-diff.js +0 -57
- package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
- package/dist/tui-opentui/feedback-dialog.js +0 -164
- package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
- package/dist/tui-opentui/feishu-setup-picker.js +0 -272
- package/dist/tui-opentui/file-mentions.d.ts +0 -29
- package/dist/tui-opentui/file-mentions.js +0 -174
- package/dist/tui-opentui/footer.d.ts +0 -26
- package/dist/tui-opentui/footer.js +0 -40
- package/dist/tui-opentui/image-paste.d.ts +0 -54
- package/dist/tui-opentui/image-paste.js +0 -288
- package/dist/tui-opentui/input-box.d.ts +0 -32
- package/dist/tui-opentui/input-box.js +0 -462
- package/dist/tui-opentui/input-history.d.ts +0 -16
- package/dist/tui-opentui/input-history.js +0 -79
- package/dist/tui-opentui/markdown.d.ts +0 -66
- package/dist/tui-opentui/markdown.js +0 -127
- package/dist/tui-opentui/message-list.d.ts +0 -31
- package/dist/tui-opentui/message-list.js +0 -131
- package/dist/tui-opentui/model-picker.d.ts +0 -63
- package/dist/tui-opentui/model-picker.js +0 -450
- package/dist/tui-opentui/plan-confirm.d.ts +0 -9
- package/dist/tui-opentui/plan-confirm.js +0 -124
- package/dist/tui-opentui/question-dialog.d.ts +0 -10
- package/dist/tui-opentui/question-dialog.js +0 -110
- package/dist/tui-opentui/recent-activity.d.ts +0 -8
- package/dist/tui-opentui/recent-activity.js +0 -71
- package/dist/tui-opentui/run-session-picker.d.ts +0 -10
- package/dist/tui-opentui/run-session-picker.js +0 -28
- package/dist/tui-opentui/run.d.ts +0 -38
- package/dist/tui-opentui/run.js +0 -48
- package/dist/tui-opentui/session-picker.d.ts +0 -12
- package/dist/tui-opentui/session-picker.js +0 -120
- package/dist/tui-opentui/theme.d.ts +0 -89
- package/dist/tui-opentui/theme.js +0 -157
- package/dist/tui-opentui/todos.d.ts +0 -9
- package/dist/tui-opentui/todos.js +0 -45
- package/dist/tui-opentui/trace-groups.d.ts +0 -27
- package/dist/tui-opentui/trace-groups.js +0 -455
- package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
- package/dist/tui-opentui/use-terminal-size.js +0 -5
- package/dist/tui-opentui/welcome.d.ts +0 -25
- package/dist/tui-opentui/welcome.js +0 -77
package/dist/tui-opentui/app.js
DELETED
|
@@ -1,1371 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
|
-
/** @jsxImportSource @opentui/react */
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
5
|
-
import { AgentAbortError } from "../agent.js";
|
|
6
|
-
import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
7
|
-
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
8
|
-
import { UserConfig, maskKey } from "../config.js";
|
|
9
|
-
import { createPastedContentMarker, InputBox, shouldCollapsePastedContent, } from "./input-box.js";
|
|
10
|
-
import { MessageList } from "./message-list.js";
|
|
11
|
-
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, nextDisplayMessageKey, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
|
|
12
|
-
import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
|
|
13
|
-
import { ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
|
|
14
|
-
import { FeishuSetupPicker } from "./feishu-setup-picker.js";
|
|
15
|
-
import { BUILTIN_PROVIDERS, ProviderRegistry, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
16
|
-
import { buildSystemPrompt } from "../system-prompt.js";
|
|
17
|
-
import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
|
|
18
|
-
import { FooterBar, buildFooterData } from "./footer.js";
|
|
19
|
-
import { SkillRegistry } from "../skills/registry.js";
|
|
20
|
-
import { parseSkillInvocation } from "../skills/invocation.js";
|
|
21
|
-
import { useTerminalSize } from "./use-terminal-size.js";
|
|
22
|
-
import { HomeSurface, WelcomeBanner, shouldShowWelcomeBanner } from "./welcome.js";
|
|
23
|
-
import { expandAtMentions } from "./file-mentions.js";
|
|
24
|
-
import { TodosPanel } from "./todos.js";
|
|
25
|
-
import { PlanConfirm } from "./plan-confirm.js";
|
|
26
|
-
import { ApprovalDialog } from "./approval/approval-dialog.js";
|
|
27
|
-
import { getNextPermissionMode } from "../permission/mode.js";
|
|
28
|
-
import { QuestionDialog } from "./question-dialog.js";
|
|
29
|
-
import { FeedbackDialog } from "./feedback-dialog.js";
|
|
30
|
-
import { collectFeedback } from "../feedback/collect.js";
|
|
31
|
-
// OpenTUI handles mouse selection natively via useSelectionHandler; no need
|
|
32
|
-
// to filter escape-sequence noise out of stdin like we did in the Ink path.
|
|
33
|
-
import os from "node:os";
|
|
34
|
-
import { existsSync } from "node:fs";
|
|
35
|
-
import { join } from "node:path";
|
|
36
|
-
function buildTips(agent, registry) {
|
|
37
|
-
const tips = [];
|
|
38
|
-
const hasProvider = registry.getEnabled().length > 0;
|
|
39
|
-
if (!hasProvider) {
|
|
40
|
-
tips.push("Run /login or /provider --add to configure a model");
|
|
41
|
-
}
|
|
42
|
-
else if (agent.model) {
|
|
43
|
-
tips.push(`Ready with ${displayModel(agent.model)}`);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
tips.push("Run /model to pick a model");
|
|
47
|
-
}
|
|
48
|
-
tips.push("Type @ to reference a file");
|
|
49
|
-
tips.push("Type / for commands and skills");
|
|
50
|
-
return tips;
|
|
51
|
-
}
|
|
52
|
-
function friendlyCwd(cwd) {
|
|
53
|
-
const home = os.homedir();
|
|
54
|
-
if (cwd === home)
|
|
55
|
-
return "~";
|
|
56
|
-
if (cwd.startsWith(home + "/"))
|
|
57
|
-
return "~" + cwd.slice(home.length);
|
|
58
|
-
return cwd;
|
|
59
|
-
}
|
|
60
|
-
function hasVisibleDisplayMessage(message) {
|
|
61
|
-
if (message.syntheticKind === "ui_summary")
|
|
62
|
-
return false;
|
|
63
|
-
if (message.syntheticKind === "ui_compact_summary")
|
|
64
|
-
return true;
|
|
65
|
-
if (message.role === "user" || message.role === "error")
|
|
66
|
-
return message.content.trim().length > 0;
|
|
67
|
-
return !!(message.content.trim() ||
|
|
68
|
-
message.reasoning?.trim() ||
|
|
69
|
-
(message.toolCalls?.length ?? 0) > 0 ||
|
|
70
|
-
(message.parts?.length ?? 0) > 0);
|
|
71
|
-
}
|
|
72
|
-
function reconstructDisplayMessages(agentMessages) {
|
|
73
|
-
const result = [];
|
|
74
|
-
for (const m of agentMessages) {
|
|
75
|
-
if (m.role === "system" || m.role === "tool")
|
|
76
|
-
continue;
|
|
77
|
-
if (m.role === "user") {
|
|
78
|
-
if (m.isMeta)
|
|
79
|
-
continue; // <system-reminder> injections are not user-visible
|
|
80
|
-
result.push({
|
|
81
|
-
key: nextDisplayMessageKey("user"),
|
|
82
|
-
role: "user",
|
|
83
|
-
content: typeof m.content === "string"
|
|
84
|
-
? (shouldCollapsePastedContent(m.content) ? createPastedContentMarker(m.content) : m.content)
|
|
85
|
-
: "(multimedia)",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
else if (m.role === "assistant") {
|
|
89
|
-
const toolCalls = [];
|
|
90
|
-
if (m.toolCalls) {
|
|
91
|
-
for (const tc of m.toolCalls) {
|
|
92
|
-
let args = {};
|
|
93
|
-
try {
|
|
94
|
-
args = JSON.parse(tc.arguments || "{}");
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
args = {};
|
|
98
|
-
}
|
|
99
|
-
const toolResult = agentMessages.find((tm) => tm.role === "tool" && tm.toolCallId === tc.id);
|
|
100
|
-
if (isHiddenToolMetadata(toolResult ? toolResult.metadata : undefined))
|
|
101
|
-
continue;
|
|
102
|
-
toolCalls.push({
|
|
103
|
-
id: tc.id,
|
|
104
|
-
name: tc.name,
|
|
105
|
-
args,
|
|
106
|
-
result: toolResult ? toolResult.content : undefined,
|
|
107
|
-
isError: toolResult ? toolResult.content?.startsWith?.("Error:") : false,
|
|
108
|
-
metadata: toolResult ? toolResult.metadata : undefined,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
result.push({
|
|
113
|
-
key: nextDisplayMessageKey("asst"),
|
|
114
|
-
role: "assistant",
|
|
115
|
-
content: m.content,
|
|
116
|
-
reasoning: m.reasoning || undefined,
|
|
117
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Streaming tool arguments arrive as an incomplete JSON buffer. We can't
|
|
125
|
-
* JSON.parse() until the closing brace lands, but the user wants to see the
|
|
126
|
-
* short identifying fields (path, command, …) as soon as the model emits
|
|
127
|
-
* them so the tool row header reflects what's happening.
|
|
128
|
-
*
|
|
129
|
-
* Intentionally limited to short, single-line fields. Long fields like
|
|
130
|
-
* `content` are *not* surfaced live: rendering thousands of partial lines
|
|
131
|
-
* per delta floods the terminal and the partial value can break around
|
|
132
|
-
* unescaped sequences. The final value lands when the tool actually
|
|
133
|
-
* executes and tool_start delivers canonical args.
|
|
134
|
-
*/
|
|
135
|
-
function parsePartialArgs(buffer, previous) {
|
|
136
|
-
// If the buffer is now valid JSON, prefer the real parse.
|
|
137
|
-
try {
|
|
138
|
-
const parsed = JSON.parse(buffer);
|
|
139
|
-
if (parsed && typeof parsed === "object")
|
|
140
|
-
return parsed;
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
// fall through to partial extraction below
|
|
144
|
-
}
|
|
145
|
-
const result = { ...previous };
|
|
146
|
-
const FIELDS = ["path", "command", "pattern", "url", "query"];
|
|
147
|
-
for (const field of FIELDS) {
|
|
148
|
-
// Match a complete-looking quoted string. Requires a closing quote so we
|
|
149
|
-
// don't surface half-typed paths that may still change as bytes arrive.
|
|
150
|
-
const match = buffer.match(new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`));
|
|
151
|
-
if (match) {
|
|
152
|
-
const raw = match[1] ?? "";
|
|
153
|
-
result[field] = raw
|
|
154
|
-
.replace(/\\n/g, "\n")
|
|
155
|
-
.replace(/\\t/g, "\t")
|
|
156
|
-
.replace(/\\"/g, '"')
|
|
157
|
-
.replace(/\\\\/g, "\\");
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
function mergeToolMetadata(current, incoming) {
|
|
163
|
-
if (!incoming)
|
|
164
|
-
return current;
|
|
165
|
-
if (current?.kind !== "subagent" || incoming.kind !== "subagent") {
|
|
166
|
-
return incoming;
|
|
167
|
-
}
|
|
168
|
-
const currentSubagents = Array.isArray(current.subagents) ? current.subagents : [];
|
|
169
|
-
const incomingSubagents = Array.isArray(incoming.subagents) ? incoming.subagents : [];
|
|
170
|
-
const byId = new Map();
|
|
171
|
-
for (const item of currentSubagents) {
|
|
172
|
-
const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
|
|
173
|
-
? String(item.subAgentId)
|
|
174
|
-
: "";
|
|
175
|
-
byId.set(subAgentId || `current:${byId.size}`, item);
|
|
176
|
-
}
|
|
177
|
-
for (const item of incomingSubagents) {
|
|
178
|
-
const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
|
|
179
|
-
? String(item.subAgentId)
|
|
180
|
-
: "";
|
|
181
|
-
byId.set(subAgentId || `incoming:${byId.size}`, item);
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
...current,
|
|
185
|
-
...incoming,
|
|
186
|
-
subagents: [...byId.values()],
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Coerce a freshly-constructed DisplayMessage into one that carries a stable
|
|
191
|
-
* `key`. Centralizes the safety net so callers don't have to remember to call
|
|
192
|
-
* nextDisplayMessageKey on every push.
|
|
193
|
-
*/
|
|
194
|
-
function withMessageKey(message) {
|
|
195
|
-
if (message.key)
|
|
196
|
-
return message;
|
|
197
|
-
const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
|
|
198
|
-
return { ...message, key: nextDisplayMessageKey(prefix) };
|
|
199
|
-
}
|
|
200
|
-
// Keep the live (non-Static) region small so non-GPU terminals (xterm.js DOM
|
|
201
|
-
// renderer, ssh into a basic terminal, tmux without GPU) don't flicker when
|
|
202
|
-
// Ink re-reconciles the streaming block on every token. Flushing earlier and
|
|
203
|
-
// in smaller chunks shifts most of the answer into terminal scrollback, where
|
|
204
|
-
// it's a one-time write that doesn't get re-rendered.
|
|
205
|
-
const STREAMING_STATIC_FLUSH_MIN_CHARS = 600;
|
|
206
|
-
const STREAMING_STATIC_FLUSH_TARGET_CHARS = 400;
|
|
207
|
-
const STREAMING_STATIC_FLUSH_MIN_TAIL = 120;
|
|
208
|
-
/**
|
|
209
|
-
* True iff `prefix` ends inside an open ```/~~~ fenced code block. Splitting
|
|
210
|
-
* the streaming buffer at such a point would let the flushed half render
|
|
211
|
-
* without its closing fence — `MarkdownContent` would then treat the body as
|
|
212
|
-
* plain prose and the trailing half would render as an isolated code block
|
|
213
|
-
* with no opener. Fence delimiters of different families don't close each
|
|
214
|
-
* other (a `~~~` inside a ``` block is just text). We use a permissive
|
|
215
|
-
* "line starts with three or more of the same char" rule, ignoring the info
|
|
216
|
-
* string — that's enough to spot when we're mid-block.
|
|
217
|
-
*/
|
|
218
|
-
function endsInsideUnclosedCodeFence(prefix) {
|
|
219
|
-
let openMarker = null;
|
|
220
|
-
for (const rawLine of prefix.split("\n")) {
|
|
221
|
-
const line = rawLine.replace(/^ {0,3}/, "");
|
|
222
|
-
if (openMarker === null) {
|
|
223
|
-
if (line.startsWith("```"))
|
|
224
|
-
openMarker = "`";
|
|
225
|
-
else if (line.startsWith("~~~"))
|
|
226
|
-
openMarker = "~";
|
|
227
|
-
}
|
|
228
|
-
else if (line.startsWith(openMarker.repeat(3))) {
|
|
229
|
-
openMarker = null;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return openMarker !== null;
|
|
233
|
-
}
|
|
234
|
-
function findStreamingStaticFlushIndex(content) {
|
|
235
|
-
if (content.length < STREAMING_STATIC_FLUSH_MIN_CHARS)
|
|
236
|
-
return -1;
|
|
237
|
-
const upper = Math.min(STREAMING_STATIC_FLUSH_TARGET_CHARS, content.length - STREAMING_STATIC_FLUSH_MIN_TAIL);
|
|
238
|
-
if (upper <= 0)
|
|
239
|
-
return -1;
|
|
240
|
-
const search = content.slice(0, upper);
|
|
241
|
-
const paragraphBreak = search.lastIndexOf("\n\n");
|
|
242
|
-
if (paragraphBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
243
|
-
const splitIndex = paragraphBreak + 2;
|
|
244
|
-
if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
|
|
245
|
-
return splitIndex;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const lineBreak = search.lastIndexOf("\n");
|
|
249
|
-
if (lineBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
250
|
-
const splitIndex = lineBreak + 1;
|
|
251
|
-
if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
|
|
252
|
-
return splitIndex;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// Inside an open code fence: hold off flushing until the closing fence
|
|
256
|
-
// arrives. The live region grows a bit, but Markdown rendering stays correct.
|
|
257
|
-
return -1;
|
|
258
|
-
}
|
|
259
|
-
function cloneDisplayPart(part) {
|
|
260
|
-
if (part.type === "text") {
|
|
261
|
-
return { type: "text", content: part.content };
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
type: "tools",
|
|
265
|
-
toolCalls: part.toolCalls.map((toolCall) => ({
|
|
266
|
-
...toolCall,
|
|
267
|
-
args: { ...toolCall.args },
|
|
268
|
-
})),
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function splitDisplayPartsAtTextOffset(parts, offset) {
|
|
272
|
-
const flushedParts = [];
|
|
273
|
-
const remainingParts = [];
|
|
274
|
-
let remainingOffset = Math.max(0, offset);
|
|
275
|
-
let reachedTail = false;
|
|
276
|
-
for (const part of parts) {
|
|
277
|
-
if (part.type === "text") {
|
|
278
|
-
if (!reachedTail && remainingOffset >= part.content.length) {
|
|
279
|
-
if (part.content)
|
|
280
|
-
flushedParts.push(cloneDisplayPart(part));
|
|
281
|
-
remainingOffset -= part.content.length;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (!reachedTail && remainingOffset > 0) {
|
|
285
|
-
const head = part.content.slice(0, remainingOffset);
|
|
286
|
-
const tail = part.content.slice(remainingOffset);
|
|
287
|
-
if (head)
|
|
288
|
-
flushedParts.push({ type: "text", content: head });
|
|
289
|
-
if (tail)
|
|
290
|
-
remainingParts.push({ type: "text", content: tail });
|
|
291
|
-
remainingOffset = 0;
|
|
292
|
-
reachedTail = true;
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
remainingParts.push(cloneDisplayPart(part));
|
|
296
|
-
reachedTail = true;
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (!reachedTail && remainingOffset > 0) {
|
|
300
|
-
flushedParts.push(cloneDisplayPart(part));
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
remainingParts.push(cloneDisplayPart(part));
|
|
304
|
-
reachedTail = true;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return { flushedParts, remainingParts };
|
|
308
|
-
}
|
|
309
|
-
export function App({ agent, args, sessionManager, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, bypassEnabled, onExit }) {
|
|
310
|
-
const [themeMode, setThemeMode] = useState(initialThemeMode ?? "auto");
|
|
311
|
-
// `detectedTheme` is captured once at startup in main.ts. We keep it in state
|
|
312
|
-
// so future re-detection (e.g. if a user runs `/theme auto` after switching
|
|
313
|
-
// their terminal) is possible without re-mounting the app. For now it never
|
|
314
|
-
// changes after first render.
|
|
315
|
-
const [autoResolved] = useState(detectedTheme ?? "dark");
|
|
316
|
-
const palette = useMemo(() => {
|
|
317
|
-
const resolved = themeMode === "auto" ? autoResolved : themeMode;
|
|
318
|
-
return paletteFor(resolved, themeOverrides);
|
|
319
|
-
}, [themeMode, autoResolved, themeOverrides]);
|
|
320
|
-
const applyThemeMode = useCallback((mode) => {
|
|
321
|
-
setThemeMode(mode);
|
|
322
|
-
onThemeModeChange?.(mode);
|
|
323
|
-
}, [onThemeModeChange]);
|
|
324
|
-
const themeResolved = themeMode === "auto" ? autoResolved : themeMode;
|
|
325
|
-
const renderer = useRenderer();
|
|
326
|
-
const exit = useCallback(() => {
|
|
327
|
-
try {
|
|
328
|
-
renderer.destroy?.();
|
|
329
|
-
}
|
|
330
|
-
catch {
|
|
331
|
-
// ignore — already torn down
|
|
332
|
-
}
|
|
333
|
-
}, [renderer]);
|
|
334
|
-
const [messages, setMessages] = useState(() => compactDisplayMessages(reconstructDisplayMessages(agent.messages)));
|
|
335
|
-
const [clearEpoch, setClearEpoch] = useState(0);
|
|
336
|
-
const [isRunning, setIsRunning] = useState(false);
|
|
337
|
-
const [streamingContent, setStreamingContent] = useState("");
|
|
338
|
-
const [streamingReasoning, setStreamingReasoning] = useState("");
|
|
339
|
-
const [streamingTools, setStreamingTools] = useState([]);
|
|
340
|
-
const [streamingParts, setStreamingParts] = useState([]);
|
|
341
|
-
const [usageTotals, setUsageTotals] = useState({ prompt: 0, completion: 0 });
|
|
342
|
-
const [thinkingLevel, setThinkingLevel] = useState(agent.thinking);
|
|
343
|
-
const [permissionMode, setPermissionMode] = useState(agent.mode);
|
|
344
|
-
const [todos, setTodos] = useState(() => agent.getTodos());
|
|
345
|
-
const [pendingPlan, setPendingPlan] = useState(null);
|
|
346
|
-
const [pendingApproval, setPendingApproval] = useState(null);
|
|
347
|
-
const [pendingQuestion, setPendingQuestion] = useState(null);
|
|
348
|
-
const [pendingFeedback, setPendingFeedback] = useState(null);
|
|
349
|
-
const [pickerMode, setPickerMode] = useState(null);
|
|
350
|
-
const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
|
|
351
|
-
const [composerDraft, setComposerDraft] = useState(null);
|
|
352
|
-
const [keyProviderId, setKeyProviderId] = useState(null);
|
|
353
|
-
const [verboseTrace, setVerboseTrace] = useState(false);
|
|
354
|
-
const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
|
|
355
|
-
const { columns: terminalColumns, rows: terminalRows } = useTerminalSize();
|
|
356
|
-
const showWelcome = shouldShowWelcomeBanner({
|
|
357
|
-
messages,
|
|
358
|
-
startedWithVisibleHistory: startedWithVisibleHistoryRef.current,
|
|
359
|
-
});
|
|
360
|
-
const activeAbortRef = useRef(null);
|
|
361
|
-
const exitRequestedRef = useRef(false);
|
|
362
|
-
const sessionStartRef = useRef(Date.now());
|
|
363
|
-
const previousTerminalColumnsRef = useRef(null);
|
|
364
|
-
useEffect(() => {
|
|
365
|
-
if (previousTerminalColumnsRef.current === null) {
|
|
366
|
-
previousTerminalColumnsRef.current = terminalColumns;
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (previousTerminalColumnsRef.current === terminalColumns)
|
|
370
|
-
return;
|
|
371
|
-
previousTerminalColumnsRef.current = terminalColumns;
|
|
372
|
-
// This follows Gemini CLI's normal terminal-buffer strategy: after a
|
|
373
|
-
// resize, the previous live Ink frame may have wrapped at the old width,
|
|
374
|
-
// so cursor-up based repaint can leave stale progress frames behind.
|
|
375
|
-
// Debounce resize storms, then clear and replay Static at the settled width.
|
|
376
|
-
const timer = setTimeout(() => {
|
|
377
|
-
if (exitRequestedRef.current)
|
|
378
|
-
return;
|
|
379
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
380
|
-
setClearEpoch((epoch) => epoch + 1);
|
|
381
|
-
}, 300);
|
|
382
|
-
return () => clearTimeout(timer);
|
|
383
|
-
}, [terminalColumns]);
|
|
384
|
-
// Set true the moment /quit is invoked so we can hide dynamic UI (composer,
|
|
385
|
-
// waiting indicator, footer) before Ink snapshots its final frame into the
|
|
386
|
-
// shell scrollback. Without this, the last visible "> " input row stays
|
|
387
|
-
// glued to the bottom of the terminal after exit.
|
|
388
|
-
const [isExiting, setIsExiting] = useState(false);
|
|
389
|
-
// 1Hz tick keeps the composer activity indicator animated while the agent is
|
|
390
|
-
// running without churning renders at idle.
|
|
391
|
-
const [nowTick, setNowTick] = useState(() => Date.now());
|
|
392
|
-
// Timestamp of when the current agent run started. Used only for the final
|
|
393
|
-
// per-task duration summary.
|
|
394
|
-
const runStartRef = useRef(null);
|
|
395
|
-
// Mark the moment the run started; flips back to null in the finally block.
|
|
396
|
-
useEffect(() => {
|
|
397
|
-
if (!isRunning)
|
|
398
|
-
return;
|
|
399
|
-
setNowTick(Date.now());
|
|
400
|
-
const t = setInterval(() => setNowTick(Date.now()), 1000);
|
|
401
|
-
return () => clearInterval(t);
|
|
402
|
-
}, [isRunning]);
|
|
403
|
-
const userConfig = new UserConfig();
|
|
404
|
-
const safeRegistry = registry ?? new ProviderRegistry(userConfig);
|
|
405
|
-
const safeSkillRegistry = skillRegistry ?? new SkillRegistry({
|
|
406
|
-
cwd: args.cwd,
|
|
407
|
-
skillPaths: userConfig.getSkillPaths(),
|
|
408
|
-
});
|
|
409
|
-
const requestExit = useCallback(() => {
|
|
410
|
-
if (exitRequestedRef.current)
|
|
411
|
-
return;
|
|
412
|
-
exitRequestedRef.current = true;
|
|
413
|
-
// Drop the composer / waiting indicator / footer from the React tree
|
|
414
|
-
// *before* we tell Ink to exit, so Ink's final log-update snapshot
|
|
415
|
-
// doesn't leave an empty "> " row behind in the shell scrollback.
|
|
416
|
-
setIsExiting(true);
|
|
417
|
-
// Cancel any in-flight agent run first so its tools / network calls
|
|
418
|
-
// don't keep emitting text after Ink unmounts and corrupt the
|
|
419
|
-
// restored shell prompt.
|
|
420
|
-
if (activeAbortRef.current) {
|
|
421
|
-
try {
|
|
422
|
-
activeAbortRef.current.abort(new AgentAbortError("Exiting Bubble."));
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
// ignore — abort is best effort during shutdown
|
|
426
|
-
}
|
|
427
|
-
activeAbortRef.current = null;
|
|
428
|
-
}
|
|
429
|
-
void (async () => {
|
|
430
|
-
// Yield once so React can commit the `isExiting=true` render
|
|
431
|
-
// (which strips the composer/footer) before we hand control to
|
|
432
|
-
// Ink's teardown. Without this, on the no-flushMemory path the
|
|
433
|
-
// exit() below races the next React commit and Ink snapshots the
|
|
434
|
-
// pre-exit frame with the composer still visible.
|
|
435
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
436
|
-
let flushError = null;
|
|
437
|
-
if (flushMemory) {
|
|
438
|
-
// Bound the flush so a stuck LLM/network call cannot trap the TUI.
|
|
439
|
-
let timer;
|
|
440
|
-
try {
|
|
441
|
-
await Promise.race([
|
|
442
|
-
flushMemory(),
|
|
443
|
-
new Promise((_, reject) => {
|
|
444
|
-
timer = setTimeout(() => reject(new Error("flushMemory timed out after 3s")), 3000);
|
|
445
|
-
}),
|
|
446
|
-
]);
|
|
447
|
-
}
|
|
448
|
-
catch (err) {
|
|
449
|
-
flushError = err;
|
|
450
|
-
}
|
|
451
|
-
finally {
|
|
452
|
-
if (timer)
|
|
453
|
-
clearTimeout(timer);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// Hand off to Ink. Ink's render instance owns TTY teardown (raw mode,
|
|
457
|
-
// cursor, alt-screen); doing it ourselves here races with that and
|
|
458
|
-
// leaves the terminal in odd states. run.tsx awaits waitUntilExit()
|
|
459
|
-
// and then main.ts handles the rest.
|
|
460
|
-
exit();
|
|
461
|
-
// Surface flush failures *after* Ink has restored the screen so the
|
|
462
|
-
// warning lands on the real shell instead of being clobbered.
|
|
463
|
-
if (flushError) {
|
|
464
|
-
const message = flushError instanceof Error ? flushError.message : String(flushError);
|
|
465
|
-
process.nextTick(() => {
|
|
466
|
-
process.stderr.write(`warning: failed to flush memory on exit: ${message}\n`);
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
onExit?.({ wallMs: Date.now() - sessionStartRef.current });
|
|
470
|
-
})();
|
|
471
|
-
}, [exit, flushMemory, onExit]);
|
|
472
|
-
useEffect(() => {
|
|
473
|
-
if (!planHandlerRef)
|
|
474
|
-
return;
|
|
475
|
-
planHandlerRef.current = (plan) => new Promise((resolve) => {
|
|
476
|
-
setPendingPlan({ plan, resolve });
|
|
477
|
-
});
|
|
478
|
-
return () => {
|
|
479
|
-
if (planHandlerRef.current) {
|
|
480
|
-
planHandlerRef.current = undefined;
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
}, [planHandlerRef]);
|
|
484
|
-
useEffect(() => {
|
|
485
|
-
if (!approvalHandlerRef)
|
|
486
|
-
return;
|
|
487
|
-
approvalHandlerRef.current = (request) => new Promise((resolve) => {
|
|
488
|
-
setPendingApproval({ request, resolve });
|
|
489
|
-
});
|
|
490
|
-
return () => {
|
|
491
|
-
if (approvalHandlerRef.current) {
|
|
492
|
-
approvalHandlerRef.current = undefined;
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
}, [approvalHandlerRef]);
|
|
496
|
-
useEffect(() => {
|
|
497
|
-
if (!questionController)
|
|
498
|
-
return;
|
|
499
|
-
const syncFirstPending = () => {
|
|
500
|
-
setPendingQuestion((current) => current ?? questionController.list()[0] ?? null);
|
|
501
|
-
};
|
|
502
|
-
const unsubscribe = questionController.subscribe((event) => {
|
|
503
|
-
if (event.type === "asked") {
|
|
504
|
-
setPendingQuestion(event.request);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
setPendingQuestion((current) => current?.id === event.request.id ? null : current);
|
|
508
|
-
setTimeout(syncFirstPending, 0);
|
|
509
|
-
});
|
|
510
|
-
syncFirstPending();
|
|
511
|
-
return unsubscribe;
|
|
512
|
-
}, [questionController]);
|
|
513
|
-
const rebuildSystemPrompt = useCallback((overrides) => {
|
|
514
|
-
const modelParts = agent.model.includes(":")
|
|
515
|
-
? agent.model.split(":")
|
|
516
|
-
: [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
|
|
517
|
-
const providerId = modelParts[0];
|
|
518
|
-
agent.setSystemPrompt(buildSystemPrompt({
|
|
519
|
-
agentName: "Bubble",
|
|
520
|
-
configuredProvider: providerId,
|
|
521
|
-
configuredModel: displayModel(agent.model),
|
|
522
|
-
configuredModelId: agent.model,
|
|
523
|
-
thinkingLevel: overrides?.thinkingLevel ?? agent.thinking,
|
|
524
|
-
mode: overrides?.mode ?? agent.mode,
|
|
525
|
-
workingDir: args.cwd,
|
|
526
|
-
...agent.getSystemPromptToolOptions(),
|
|
527
|
-
}));
|
|
528
|
-
}, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
|
|
529
|
-
useKeyboard((key) => {
|
|
530
|
-
if (key.eventType === "release")
|
|
531
|
-
return;
|
|
532
|
-
if (key.ctrl && key.name === "c") {
|
|
533
|
-
requestExit();
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback)
|
|
537
|
-
return;
|
|
538
|
-
if (key.ctrl && key.name === "o" && !pickerMode) {
|
|
539
|
-
setVerboseTrace((v) => !v);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
// Ctrl+R: cycle thinking level (formerly Shift+Tab)
|
|
543
|
-
if (key.ctrl && key.name === "r" && !pickerMode) {
|
|
544
|
-
const modelParts = agent.model.includes(":")
|
|
545
|
-
? agent.model.split(":")
|
|
546
|
-
: [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
|
|
547
|
-
const providerId = modelParts[0];
|
|
548
|
-
const modelId = modelParts.slice(1).join(":");
|
|
549
|
-
const availableLevels = getAvailableThinkingLevels(providerId, modelId);
|
|
550
|
-
const currentLevel = normalizeThinkingLevel(agent.thinking, availableLevels);
|
|
551
|
-
const currentIndex = availableLevels.indexOf(currentLevel);
|
|
552
|
-
const nextLevel = availableLevels[(currentIndex + 1) % availableLevels.length];
|
|
553
|
-
agent.thinking = nextLevel;
|
|
554
|
-
rebuildSystemPrompt({ thinkingLevel: nextLevel });
|
|
555
|
-
userConfig.setDefaultThinkingLevel(nextLevel);
|
|
556
|
-
setThinkingLevel(nextLevel);
|
|
557
|
-
sessionManager?.updateMetadata({ model: agent.model, thinkingLevel: nextLevel, reasoningEffort: nextLevel });
|
|
558
|
-
sessionManager?.appendMarker("thinking_level_switch", nextLevel);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
// Shift+Tab: cycle through permission modes (default → acceptEdits → plan
|
|
562
|
-
// → [bypassPermissions if enabled] → default). Agent.setMode injects a
|
|
563
|
-
// <system-reminder>, so we do not rebuild the cache-friendly system prompt here.
|
|
564
|
-
if (key.name === "tab" && key.shift && !pickerMode) {
|
|
565
|
-
const nextMode = getNextPermissionMode(agent.mode);
|
|
566
|
-
agent.setMode(nextMode);
|
|
567
|
-
setPermissionMode(nextMode);
|
|
568
|
-
sessionManager?.appendMarker("mode_switch", nextMode);
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
if (key.name === "escape" && !pickerMode) {
|
|
572
|
-
if (isRunning && activeAbortRef.current) {
|
|
573
|
-
activeAbortRef.current.abort(new AgentAbortError("Agent run cancelled by user."));
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
const updateDisplayMessages = useCallback((updater) => {
|
|
579
|
-
setMessages((prev) => compactDisplayMessages(updater(prev).map(withMessageKey)));
|
|
580
|
-
}, []);
|
|
581
|
-
const addMessage = useCallback((role, content) => {
|
|
582
|
-
updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
|
|
583
|
-
}, [updateDisplayMessages]);
|
|
584
|
-
const clearMessages = useCallback(() => {
|
|
585
|
-
// Static history is already written to terminal scrollback, so clearing
|
|
586
|
-
// React state alone would leave old rows visible.
|
|
587
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
588
|
-
setMessages([]);
|
|
589
|
-
setClearEpoch((epoch) => epoch + 1);
|
|
590
|
-
}, []);
|
|
591
|
-
const openPicker = useCallback((mode, providerId) => {
|
|
592
|
-
if (mode === "key") {
|
|
593
|
-
setKeyProviderId(providerId ?? null);
|
|
594
|
-
}
|
|
595
|
-
setPickerMode(mode);
|
|
596
|
-
}, []);
|
|
597
|
-
const closePicker = useCallback(() => {
|
|
598
|
-
setPickerMode(null);
|
|
599
|
-
setCursorResetEpoch((epoch) => epoch + 1);
|
|
600
|
-
}, []);
|
|
601
|
-
const fillComposer = useCallback((text) => {
|
|
602
|
-
setComposerDraft((current) => ({
|
|
603
|
-
text,
|
|
604
|
-
epoch: (current?.epoch ?? 0) + 1,
|
|
605
|
-
}));
|
|
606
|
-
}, []);
|
|
607
|
-
const clearComposerDraft = useCallback(() => {
|
|
608
|
-
setComposerDraft(null);
|
|
609
|
-
}, []);
|
|
610
|
-
const openFeedback = useCallback((initialDescription) => {
|
|
611
|
-
const base = collectFeedback(agent, { description: "" });
|
|
612
|
-
const { description: _drop, ...rest } = base;
|
|
613
|
-
setPendingFeedback({ base: rest, initialDescription });
|
|
614
|
-
}, [agent]);
|
|
615
|
-
const handleModelSelect = useCallback((model) => {
|
|
616
|
-
const run = async () => {
|
|
617
|
-
agent.model = model;
|
|
618
|
-
const decoded = model.includes(":")
|
|
619
|
-
? model.split(":")
|
|
620
|
-
: [agent.providerId || safeRegistry.getDefault()?.id || "openai", model];
|
|
621
|
-
const providerId = decoded[0];
|
|
622
|
-
await safeRegistry.prepareProvider(providerId);
|
|
623
|
-
const provider = safeRegistry.getConfigured().find((item) => item.id === providerId);
|
|
624
|
-
if (!provider?.apiKey || !createProvider) {
|
|
625
|
-
addMessage("error", `Provider ${providerId} is not configured or has no active credentials.`);
|
|
626
|
-
closePicker();
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
const modelId = model.includes(":") ? model.split(":").slice(1).join(":") : model;
|
|
630
|
-
agent.thinking = normalizeThinkingLevel(agent.thinking || getDefaultThinkingLevel(providerId, modelId), getAvailableThinkingLevels(providerId, modelId));
|
|
631
|
-
agent.setProvider(createProvider(providerId, provider.apiKey, provider.baseURL));
|
|
632
|
-
agent.providerId = providerId;
|
|
633
|
-
agent.setSystemPrompt(buildSystemPrompt({
|
|
634
|
-
agentName: "Bubble",
|
|
635
|
-
configuredProvider: providerId,
|
|
636
|
-
configuredModel: displayModel(model),
|
|
637
|
-
configuredModelId: model,
|
|
638
|
-
thinkingLevel: agent.thinking,
|
|
639
|
-
workingDir: args.cwd,
|
|
640
|
-
...agent.getSystemPromptToolOptions(),
|
|
641
|
-
}));
|
|
642
|
-
userConfig.pushRecentModel(model);
|
|
643
|
-
setThinkingLevel(agent.thinking);
|
|
644
|
-
sessionManager?.updateMetadata({ model, thinkingLevel: agent.thinking, reasoningEffort: agent.thinking });
|
|
645
|
-
sessionManager?.appendMarker("model_switch", model);
|
|
646
|
-
addMessage("assistant", `Model switched to ${displayModel(model)}.`);
|
|
647
|
-
closePicker();
|
|
648
|
-
};
|
|
649
|
-
void run();
|
|
650
|
-
}, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
|
|
651
|
-
const handleProviderSelect = useCallback(async (providerId) => {
|
|
652
|
-
await safeRegistry.prepareProvider(providerId);
|
|
653
|
-
const configured = safeRegistry.getConfigured();
|
|
654
|
-
const p = configured.find((x) => x.id === providerId);
|
|
655
|
-
const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
|
|
656
|
-
if (!p && !builtin) {
|
|
657
|
-
addMessage("error", `Provider ${providerId} not found.`);
|
|
658
|
-
closePicker();
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if (!p?.apiKey) {
|
|
662
|
-
if (!p && builtin) {
|
|
663
|
-
safeRegistry.addProvider(providerId, "");
|
|
664
|
-
}
|
|
665
|
-
safeRegistry.setDefault(providerId);
|
|
666
|
-
setKeyProviderId(providerId);
|
|
667
|
-
setPickerMode("key");
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
safeRegistry.setDefault(providerId);
|
|
671
|
-
agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
|
|
672
|
-
agent.providerId = providerId;
|
|
673
|
-
addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
|
|
674
|
-
closePicker();
|
|
675
|
-
}, [addMessage, agent, closePicker, createProvider, safeRegistry]);
|
|
676
|
-
const handleProviderAddSelect = useCallback((providerId) => {
|
|
677
|
-
const ok = safeRegistry.addProvider(providerId, "");
|
|
678
|
-
if (!ok) {
|
|
679
|
-
addMessage("error", `Provider ${providerId} could not be added.`);
|
|
680
|
-
closePicker();
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
safeRegistry.setDefault(providerId);
|
|
684
|
-
setKeyProviderId(providerId);
|
|
685
|
-
setPickerMode("key");
|
|
686
|
-
}, [addMessage, closePicker, safeRegistry]);
|
|
687
|
-
const handleLoginProviderSelect = useCallback(async (providerId) => {
|
|
688
|
-
closePicker();
|
|
689
|
-
const command = `/login ${providerId}`;
|
|
690
|
-
const { handled, result } = await slashRegistry.execute(command, {
|
|
691
|
-
agent,
|
|
692
|
-
addMessage,
|
|
693
|
-
clearMessages,
|
|
694
|
-
cwd: args.cwd,
|
|
695
|
-
exit: () => { requestExit(); },
|
|
696
|
-
sessionManager,
|
|
697
|
-
createProvider: createProvider ?? (() => {
|
|
698
|
-
throw new Error("Provider creation not available");
|
|
699
|
-
}),
|
|
700
|
-
openPicker,
|
|
701
|
-
openFeedback,
|
|
702
|
-
registry: safeRegistry,
|
|
703
|
-
skillRegistry: safeSkillRegistry,
|
|
704
|
-
bashAllowlist,
|
|
705
|
-
settingsManager,
|
|
706
|
-
lspService,
|
|
707
|
-
mcpManager,
|
|
708
|
-
flushMemory,
|
|
709
|
-
runMemoryCompaction,
|
|
710
|
-
runMemorySummary,
|
|
711
|
-
runMemoryRefresh,
|
|
712
|
-
getThemeMode: () => themeMode,
|
|
713
|
-
getResolvedTheme: () => themeResolved,
|
|
714
|
-
setThemeMode: applyThemeMode,
|
|
715
|
-
});
|
|
716
|
-
if (handled && result) {
|
|
717
|
-
addMessage("assistant", result);
|
|
718
|
-
}
|
|
719
|
-
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
720
|
-
const handleLogoutProviderSelect = useCallback(async (providerId) => {
|
|
721
|
-
closePicker();
|
|
722
|
-
const command = `/logout ${providerId}`;
|
|
723
|
-
const { handled, result } = await slashRegistry.execute(command, {
|
|
724
|
-
agent,
|
|
725
|
-
addMessage,
|
|
726
|
-
clearMessages,
|
|
727
|
-
cwd: args.cwd,
|
|
728
|
-
exit: () => { requestExit(); },
|
|
729
|
-
sessionManager,
|
|
730
|
-
createProvider: createProvider ?? (() => {
|
|
731
|
-
throw new Error("Provider creation not available");
|
|
732
|
-
}),
|
|
733
|
-
openPicker,
|
|
734
|
-
openFeedback,
|
|
735
|
-
registry: safeRegistry,
|
|
736
|
-
skillRegistry: safeSkillRegistry,
|
|
737
|
-
bashAllowlist,
|
|
738
|
-
settingsManager,
|
|
739
|
-
lspService,
|
|
740
|
-
mcpManager,
|
|
741
|
-
flushMemory,
|
|
742
|
-
runMemoryCompaction,
|
|
743
|
-
runMemorySummary,
|
|
744
|
-
runMemoryRefresh,
|
|
745
|
-
getThemeMode: () => themeMode,
|
|
746
|
-
getResolvedTheme: () => themeResolved,
|
|
747
|
-
setThemeMode: applyThemeMode,
|
|
748
|
-
});
|
|
749
|
-
if (handled && result) {
|
|
750
|
-
addMessage("assistant", result);
|
|
751
|
-
}
|
|
752
|
-
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
753
|
-
const handleKeySubmit = useCallback((key) => {
|
|
754
|
-
const targetId = keyProviderId || safeRegistry.getDefault()?.id;
|
|
755
|
-
if (!targetId) {
|
|
756
|
-
addMessage("error", "No provider selected.");
|
|
757
|
-
closePicker();
|
|
758
|
-
setKeyProviderId(null);
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
safeRegistry.updateProviderKey(targetId, key);
|
|
762
|
-
const p = safeRegistry.getConfigured().find((x) => x.id === targetId);
|
|
763
|
-
if (p && createProvider) {
|
|
764
|
-
agent.setProvider(createProvider(targetId, key, p.baseURL));
|
|
765
|
-
agent.providerId = targetId;
|
|
766
|
-
}
|
|
767
|
-
addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
|
|
768
|
-
closePicker();
|
|
769
|
-
setKeyProviderId(null);
|
|
770
|
-
}, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry]);
|
|
771
|
-
const handleSubmit = useCallback(async (payload) => {
|
|
772
|
-
const normalized = typeof payload === "string" ? { text: payload, images: [] } : payload;
|
|
773
|
-
const input = normalized.text;
|
|
774
|
-
const displayInput = normalized.displayText ?? input;
|
|
775
|
-
const images = normalized.images;
|
|
776
|
-
if (!input.trim() && images.length === 0)
|
|
777
|
-
return;
|
|
778
|
-
const runAgentInput = async (actualInput, displayInput, attachedImages = []) => {
|
|
779
|
-
const activeProviderId = agent.providerId || safeRegistry.getDefault()?.id;
|
|
780
|
-
const hasActiveProvider = !!activeProviderId && safeRegistry.getEnabled().some((provider) => provider.id === activeProviderId);
|
|
781
|
-
if (!hasActiveProvider) {
|
|
782
|
-
addMessage("error", "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.");
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
if (!agent.model) {
|
|
786
|
-
addMessage("error", "No model selected. Use /model after /login or provider setup.");
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
const displayContent = attachedImages.length > 0
|
|
790
|
-
? `${displayInput}${displayInput ? "\n" : ""}${attachedImages
|
|
791
|
-
.map((img, i) => `[image${attachedImages.length > 1 ? ` ${i + 1}` : ""}: ${img.filename ?? "clipboard"} · ${Math.max(1, Math.round(img.bytes / 1024))}KB]`)
|
|
792
|
-
.join(" ")}`
|
|
793
|
-
: displayInput;
|
|
794
|
-
updateDisplayMessages((prev) => [
|
|
795
|
-
...prev,
|
|
796
|
-
withMessageKey({ role: "user", content: displayContent }),
|
|
797
|
-
]);
|
|
798
|
-
setIsRunning(true);
|
|
799
|
-
runStartRef.current = Date.now();
|
|
800
|
-
setStreamingContent("");
|
|
801
|
-
setStreamingReasoning("");
|
|
802
|
-
setStreamingTools([]);
|
|
803
|
-
setStreamingParts([]);
|
|
804
|
-
let assistantContent = "";
|
|
805
|
-
let assistantReasoning = "";
|
|
806
|
-
const toolCalls = [];
|
|
807
|
-
const assistantParts = [];
|
|
808
|
-
const abortController = new AbortController();
|
|
809
|
-
activeAbortRef.current = abortController;
|
|
810
|
-
const syncStreamingParts = () => {
|
|
811
|
-
setStreamingParts(snapshotDisplayParts(assistantParts));
|
|
812
|
-
};
|
|
813
|
-
const hasAssistantOutput = () => (!!assistantContent ||
|
|
814
|
-
!!assistantReasoning ||
|
|
815
|
-
toolCalls.length > 0 ||
|
|
816
|
-
assistantParts.length > 0);
|
|
817
|
-
const commitAssistantMessage = (taskElapsedMs) => {
|
|
818
|
-
if (!hasAssistantOutput())
|
|
819
|
-
return;
|
|
820
|
-
const currentParts = snapshotDisplayParts(assistantParts);
|
|
821
|
-
const currentToolCalls = [...toolCalls];
|
|
822
|
-
const partContent = assistantContent || contentFromParts(currentParts);
|
|
823
|
-
const partToolCalls = currentToolCalls.length > 0
|
|
824
|
-
? currentToolCalls
|
|
825
|
-
: toolCallsFromParts(currentParts);
|
|
826
|
-
const msg = {
|
|
827
|
-
key: nextDisplayMessageKey("asst"),
|
|
828
|
-
role: "assistant",
|
|
829
|
-
content: partContent,
|
|
830
|
-
};
|
|
831
|
-
if (assistantReasoning) {
|
|
832
|
-
msg.reasoning = assistantReasoning;
|
|
833
|
-
}
|
|
834
|
-
if (partToolCalls.length > 0) {
|
|
835
|
-
msg.toolCalls = partToolCalls;
|
|
836
|
-
}
|
|
837
|
-
if (currentParts.length > 0) {
|
|
838
|
-
msg.parts = currentParts;
|
|
839
|
-
}
|
|
840
|
-
if (taskElapsedMs !== undefined && Number.isFinite(taskElapsedMs) && taskElapsedMs > 0) {
|
|
841
|
-
msg.taskElapsedMs = taskElapsedMs;
|
|
842
|
-
}
|
|
843
|
-
updateDisplayMessages((prev) => [...prev, msg]);
|
|
844
|
-
};
|
|
845
|
-
const clearAssistantStream = () => {
|
|
846
|
-
setStreamingContent("");
|
|
847
|
-
setStreamingReasoning("");
|
|
848
|
-
setStreamingTools([]);
|
|
849
|
-
setStreamingParts([]);
|
|
850
|
-
assistantContent = "";
|
|
851
|
-
assistantReasoning = "";
|
|
852
|
-
toolCalls.length = 0;
|
|
853
|
-
assistantParts.length = 0;
|
|
854
|
-
};
|
|
855
|
-
const flushAssistantStaticChunk = () => {
|
|
856
|
-
if (toolCalls.some((toolCall) => toolCall.result === undefined)) {
|
|
857
|
-
return false;
|
|
858
|
-
}
|
|
859
|
-
const splitIndex = findStreamingStaticFlushIndex(assistantContent);
|
|
860
|
-
if (splitIndex <= 0)
|
|
861
|
-
return false;
|
|
862
|
-
const { flushedParts, remainingParts } = splitDisplayPartsAtTextOffset(assistantParts, splitIndex);
|
|
863
|
-
const flushedContent = contentFromParts(flushedParts);
|
|
864
|
-
const flushedToolCalls = toolCallsFromParts(flushedParts);
|
|
865
|
-
if (!flushedContent && flushedToolCalls.length === 0)
|
|
866
|
-
return false;
|
|
867
|
-
const msg = {
|
|
868
|
-
key: nextDisplayMessageKey("asst"),
|
|
869
|
-
role: "assistant",
|
|
870
|
-
content: flushedContent,
|
|
871
|
-
};
|
|
872
|
-
if (assistantReasoning) {
|
|
873
|
-
msg.reasoning = assistantReasoning;
|
|
874
|
-
assistantReasoning = "";
|
|
875
|
-
setStreamingReasoning("");
|
|
876
|
-
}
|
|
877
|
-
if (flushedToolCalls.length > 0) {
|
|
878
|
-
msg.toolCalls = flushedToolCalls;
|
|
879
|
-
}
|
|
880
|
-
if (flushedParts.length > 0) {
|
|
881
|
-
msg.parts = flushedParts;
|
|
882
|
-
}
|
|
883
|
-
updateDisplayMessages((prev) => [...prev, msg]);
|
|
884
|
-
assistantParts.splice(0, assistantParts.length, ...remainingParts);
|
|
885
|
-
assistantContent = contentFromParts(assistantParts);
|
|
886
|
-
const remainingToolCalls = toolCallsFromParts(assistantParts);
|
|
887
|
-
toolCalls.splice(0, toolCalls.length, ...remainingToolCalls);
|
|
888
|
-
setStreamingContent(assistantContent);
|
|
889
|
-
setStreamingTools([...toolCalls]);
|
|
890
|
-
syncStreamingParts();
|
|
891
|
-
return true;
|
|
892
|
-
};
|
|
893
|
-
try {
|
|
894
|
-
for await (const event of agent.run(actualInput, args.cwd, { abortSignal: abortController.signal })) {
|
|
895
|
-
switch (event.type) {
|
|
896
|
-
case "text_delta":
|
|
897
|
-
assistantContent += event.content;
|
|
898
|
-
appendTextPart(assistantParts, event.content);
|
|
899
|
-
if (!flushAssistantStaticChunk()) {
|
|
900
|
-
setStreamingContent(assistantContent);
|
|
901
|
-
syncStreamingParts();
|
|
902
|
-
}
|
|
903
|
-
break;
|
|
904
|
-
case "reasoning_delta":
|
|
905
|
-
assistantReasoning += event.content;
|
|
906
|
-
setStreamingReasoning(assistantReasoning);
|
|
907
|
-
break;
|
|
908
|
-
case "tool_call_start": {
|
|
909
|
-
// The LLM has begun emitting this tool call. Args are still
|
|
910
|
-
// streaming — render an empty-args placeholder so the user
|
|
911
|
-
// sees the tool the moment it appears in the assistant
|
|
912
|
-
// response, not after the full arg payload finishes.
|
|
913
|
-
if (!toolCalls.some((t) => t.id === event.id)) {
|
|
914
|
-
const toolCall = {
|
|
915
|
-
id: event.id,
|
|
916
|
-
name: event.name,
|
|
917
|
-
args: {},
|
|
918
|
-
startedAt: Date.now(),
|
|
919
|
-
};
|
|
920
|
-
toolCalls.push(toolCall);
|
|
921
|
-
appendToolPart(assistantParts, toolCall);
|
|
922
|
-
setStreamingTools([...toolCalls]);
|
|
923
|
-
syncStreamingParts();
|
|
924
|
-
}
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
927
|
-
case "tool_call_delta": {
|
|
928
|
-
// Best-effort parse of the partial argument JSON to extract
|
|
929
|
-
// identifying fields (path, command, content, …). The buffer
|
|
930
|
-
// is incomplete JSON during streaming, so fall back to regex
|
|
931
|
-
// peeks on common string fields.
|
|
932
|
-
const tc = toolCalls.find((t) => t.id === event.id);
|
|
933
|
-
if (tc) {
|
|
934
|
-
tc.args = parsePartialArgs(event.arguments, tc.args);
|
|
935
|
-
setStreamingTools([...toolCalls]);
|
|
936
|
-
syncStreamingParts();
|
|
937
|
-
}
|
|
938
|
-
break;
|
|
939
|
-
}
|
|
940
|
-
case "tool_call_end": {
|
|
941
|
-
// Provider signaled args streaming is complete; agent will
|
|
942
|
-
// emit tool_start next. We don't need to do anything visual
|
|
943
|
-
// here — the placeholder is already in place and tool_start
|
|
944
|
-
// will refresh it with the canonical parsed args.
|
|
945
|
-
break;
|
|
946
|
-
}
|
|
947
|
-
case "tool_start": {
|
|
948
|
-
// Tool is about to execute. Upgrade the placeholder created
|
|
949
|
-
// by tool_call_start (or append if upstream skipped the
|
|
950
|
-
// streaming path).
|
|
951
|
-
const existing = toolCalls.find((t) => t.id === event.id);
|
|
952
|
-
if (existing) {
|
|
953
|
-
existing.args = event.args;
|
|
954
|
-
existing.startedAt = existing.startedAt ?? Date.now();
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
const toolCall = {
|
|
958
|
-
id: event.id,
|
|
959
|
-
name: event.name,
|
|
960
|
-
args: event.args,
|
|
961
|
-
startedAt: Date.now(),
|
|
962
|
-
};
|
|
963
|
-
toolCalls.push(toolCall);
|
|
964
|
-
appendToolPart(assistantParts, toolCall);
|
|
965
|
-
}
|
|
966
|
-
setStreamingTools([...toolCalls]);
|
|
967
|
-
syncStreamingParts();
|
|
968
|
-
break;
|
|
969
|
-
}
|
|
970
|
-
case "tool_end": {
|
|
971
|
-
const tc = toolCalls.find((t) => t.id === event.id);
|
|
972
|
-
if (tc) {
|
|
973
|
-
tc.result = event.result.content;
|
|
974
|
-
tc.isError = event.result.isError;
|
|
975
|
-
tc.metadata = event.result.metadata;
|
|
976
|
-
setStreamingTools([...toolCalls]);
|
|
977
|
-
syncStreamingParts();
|
|
978
|
-
}
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
|
-
case "tool_update": {
|
|
982
|
-
const tc = toolCalls.find((t) => t.id === event.id);
|
|
983
|
-
if (tc) {
|
|
984
|
-
tc.metadata = mergeToolMetadata(tc.metadata, event.update.metadata);
|
|
985
|
-
if (event.update.message) {
|
|
986
|
-
tc.result = event.update.message;
|
|
987
|
-
}
|
|
988
|
-
tc.isError = event.update.status === "failed"
|
|
989
|
-
|| event.update.status === "blocked"
|
|
990
|
-
|| event.update.status === "cancelled";
|
|
991
|
-
setStreamingTools([...toolCalls]);
|
|
992
|
-
syncStreamingParts();
|
|
993
|
-
}
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
case "todos_updated": {
|
|
997
|
-
setTodos(event.todos);
|
|
998
|
-
break;
|
|
999
|
-
}
|
|
1000
|
-
case "mode_changed": {
|
|
1001
|
-
setPermissionMode(event.mode);
|
|
1002
|
-
sessionManager?.appendMarker("mode_switch", event.mode);
|
|
1003
|
-
break;
|
|
1004
|
-
}
|
|
1005
|
-
case "turn_end": {
|
|
1006
|
-
if (event.usage) {
|
|
1007
|
-
setUsageTotals((totals) => ({
|
|
1008
|
-
prompt: totals.prompt + event.usage.promptTokens,
|
|
1009
|
-
completion: totals.completion + event.usage.completionTokens,
|
|
1010
|
-
}));
|
|
1011
|
-
}
|
|
1012
|
-
if (event.willContinue) {
|
|
1013
|
-
syncStreamingParts();
|
|
1014
|
-
break;
|
|
1015
|
-
}
|
|
1016
|
-
commitAssistantMessage(runStartRef.current ? Date.now() - runStartRef.current : undefined);
|
|
1017
|
-
clearAssistantStream();
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
catch (err) {
|
|
1024
|
-
commitAssistantMessage();
|
|
1025
|
-
if (err instanceof AgentAbortError || err?.name === "AbortError") {
|
|
1026
|
-
updateDisplayMessages((prev) => [
|
|
1027
|
-
...prev,
|
|
1028
|
-
withMessageKey({ role: "assistant", content: "Cancelled." }),
|
|
1029
|
-
]);
|
|
1030
|
-
}
|
|
1031
|
-
else {
|
|
1032
|
-
updateDisplayMessages((prev) => [
|
|
1033
|
-
...prev,
|
|
1034
|
-
withMessageKey({ role: "error", content: err.message }),
|
|
1035
|
-
]);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
finally {
|
|
1039
|
-
if (activeAbortRef.current === abortController)
|
|
1040
|
-
activeAbortRef.current = null;
|
|
1041
|
-
setIsRunning(false);
|
|
1042
|
-
runStartRef.current = null;
|
|
1043
|
-
setStreamingContent("");
|
|
1044
|
-
setStreamingReasoning("");
|
|
1045
|
-
setStreamingTools([]);
|
|
1046
|
-
setStreamingParts([]);
|
|
1047
|
-
}
|
|
1048
|
-
};
|
|
1049
|
-
// Slash commands and skill invocations drop any attached images —
|
|
1050
|
-
// they're meant for pure command routing.
|
|
1051
|
-
if (displayInput.startsWith("/")) {
|
|
1052
|
-
// Fast-path `/quit` and `/exit` before slash-registry / skill
|
|
1053
|
-
// resolution. This guarantees a literal "/quit" always exits even if
|
|
1054
|
-
// a skill or alias of the same name is later registered. The
|
|
1055
|
-
// canonical handler still lives in slash-commands/commands.ts so
|
|
1056
|
-
// `/help` and the slash menu can list it; both paths end up calling
|
|
1057
|
-
// requestExit().
|
|
1058
|
-
if (/^\/(?:quit|exit)\s*$/.test(input.trim())) {
|
|
1059
|
-
requestExit();
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
|
|
1063
|
-
if (skillInvocation) {
|
|
1064
|
-
await runAgentInput(skillInvocation.actualPrompt, displayInput);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
const { handled, result, inject } = await slashRegistry.execute(input, {
|
|
1068
|
-
agent,
|
|
1069
|
-
addMessage,
|
|
1070
|
-
clearMessages,
|
|
1071
|
-
cwd: args.cwd,
|
|
1072
|
-
exit: () => { requestExit(); },
|
|
1073
|
-
sessionManager,
|
|
1074
|
-
createProvider: createProvider ?? (() => {
|
|
1075
|
-
throw new Error("Provider creation not available");
|
|
1076
|
-
}),
|
|
1077
|
-
openPicker,
|
|
1078
|
-
openFeedback,
|
|
1079
|
-
registry: safeRegistry,
|
|
1080
|
-
skillRegistry: safeSkillRegistry,
|
|
1081
|
-
bashAllowlist,
|
|
1082
|
-
settingsManager,
|
|
1083
|
-
lspService,
|
|
1084
|
-
mcpManager,
|
|
1085
|
-
flushMemory,
|
|
1086
|
-
runMemoryCompaction,
|
|
1087
|
-
runMemorySummary,
|
|
1088
|
-
runMemoryRefresh,
|
|
1089
|
-
getThemeMode: () => themeMode,
|
|
1090
|
-
getResolvedTheme: () => themeResolved,
|
|
1091
|
-
setThemeMode: applyThemeMode,
|
|
1092
|
-
});
|
|
1093
|
-
if (handled) {
|
|
1094
|
-
if (agent.mode !== permissionMode) {
|
|
1095
|
-
setPermissionMode(agent.mode);
|
|
1096
|
-
}
|
|
1097
|
-
if (result) {
|
|
1098
|
-
// `/compact` rewrites agent.messages, so the Ink transcript needs to
|
|
1099
|
-
// be rebuilt from the new agent state before appending the summary
|
|
1100
|
-
// card; otherwise the pre-compaction history would keep rendering.
|
|
1101
|
-
if (result.startsWith("✓ Compaction complete")) {
|
|
1102
|
-
const summary = latestCompactionSummary(agent.messages);
|
|
1103
|
-
updateDisplayMessages(() => [
|
|
1104
|
-
...reconstructDisplayMessages(agent.messages),
|
|
1105
|
-
{
|
|
1106
|
-
role: "assistant",
|
|
1107
|
-
content: result,
|
|
1108
|
-
syntheticKind: "ui_compact_summary",
|
|
1109
|
-
compactionSummary: summary,
|
|
1110
|
-
},
|
|
1111
|
-
]);
|
|
1112
|
-
}
|
|
1113
|
-
else {
|
|
1114
|
-
addMessage("assistant", result);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
if (inject) {
|
|
1118
|
-
await runAgentInput(inject, displayInput);
|
|
1119
|
-
}
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
const expansion = await expandAtMentions(input, args.cwd);
|
|
1124
|
-
if (expansion.missing.length > 0) {
|
|
1125
|
-
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
1126
|
-
}
|
|
1127
|
-
for (const skip of expansion.skipped) {
|
|
1128
|
-
if (skip.reason !== "too large")
|
|
1129
|
-
addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
|
|
1130
|
-
}
|
|
1131
|
-
const agentInput = images.length > 0
|
|
1132
|
-
? [
|
|
1133
|
-
...(expansion.text ? [{ type: "text", text: expansion.text }] : []),
|
|
1134
|
-
...images.map((img) => ({
|
|
1135
|
-
type: "image_url",
|
|
1136
|
-
image_url: { url: img.dataUrl },
|
|
1137
|
-
})),
|
|
1138
|
-
]
|
|
1139
|
-
: expansion.text;
|
|
1140
|
-
await runAgentInput(agentInput, displayInput, images.map((img) => ({ filename: img.filename, bytes: img.bytes })));
|
|
1141
|
-
}, [addMessage, agent, args.cwd, openPicker, createProvider, safeRegistry, safeSkillRegistry, updateDisplayMessages]);
|
|
1142
|
-
const currentProviderId = agent.providerId || safeRegistry.getDefault()?.id;
|
|
1143
|
-
const keyTarget = keyProviderId
|
|
1144
|
-
? safeRegistry.getConfigured().find((p) => p.id === keyProviderId)
|
|
1145
|
-
: safeRegistry.getDefault();
|
|
1146
|
-
// Surface a pending approval as an inline badge on the matching tool row.
|
|
1147
|
-
// ApprovalRequest does not carry a toolCallId today; matching is loose by
|
|
1148
|
-
// type + the most identifying arg (path/command).
|
|
1149
|
-
const approvalHint = pendingApproval
|
|
1150
|
-
? (() => {
|
|
1151
|
-
const r = pendingApproval.request;
|
|
1152
|
-
if (r.type === "bash")
|
|
1153
|
-
return { toolName: "bash", command: r.command };
|
|
1154
|
-
if (r.type === "edit")
|
|
1155
|
-
return { toolName: "edit", path: r.path };
|
|
1156
|
-
if (r.type === "patch")
|
|
1157
|
-
return { toolName: "edit", path: r.paths[0] ?? r.path };
|
|
1158
|
-
if (r.type === "write")
|
|
1159
|
-
return { toolName: "write", path: r.path };
|
|
1160
|
-
return null;
|
|
1161
|
-
})()
|
|
1162
|
-
: null;
|
|
1163
|
-
const mcpStates = mcpManager?.getStates() ?? [];
|
|
1164
|
-
const mcpConnectedCount = mcpStates.filter((state) => state.status.kind === "connected").length;
|
|
1165
|
-
const hasAgentsFile = useMemo(() => existsSync(join(args.cwd, "AGENTS.md")) || existsSync(join(args.cwd, ".bubble", "AGENTS.md")), [args.cwd]);
|
|
1166
|
-
const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, modelLabel: agent.model ? displayModel(agent.model) : undefined, cwd: friendlyCwd(args.cwd), tips: buildTips(agent, safeRegistry), skillsCount: safeSkillRegistry.summaries().length, mcpConnectedCount: mcpConnectedCount, mcpTotalCount: mcpStates.length, hasAgentsFile: hasAgentsFile })) : null;
|
|
1167
|
-
const hasTranscript = messages.some(hasVisibleDisplayMessage);
|
|
1168
|
-
const hasStreamingTurn = !!(streamingContent ||
|
|
1169
|
-
streamingReasoning ||
|
|
1170
|
-
streamingTools.length > 0 ||
|
|
1171
|
-
streamingParts.length > 0);
|
|
1172
|
-
const showHomeSurface = !!(showWelcome &&
|
|
1173
|
-
!hasTranscript &&
|
|
1174
|
-
!hasStreamingTurn &&
|
|
1175
|
-
!isRunning &&
|
|
1176
|
-
!pickerMode &&
|
|
1177
|
-
!pendingPlan &&
|
|
1178
|
-
!pendingApproval &&
|
|
1179
|
-
!pendingQuestion &&
|
|
1180
|
-
!pendingFeedback &&
|
|
1181
|
-
!isExiting);
|
|
1182
|
-
const footerData = buildFooterData({
|
|
1183
|
-
cwd: args.cwd,
|
|
1184
|
-
providerId: agent.providerId || safeRegistry.getDefault()?.id || "unknown",
|
|
1185
|
-
model: displayModel(agent.model) || "no model",
|
|
1186
|
-
thinkingLevel,
|
|
1187
|
-
showThinking: getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2,
|
|
1188
|
-
mode: permissionMode,
|
|
1189
|
-
usageTotals,
|
|
1190
|
-
verboseTrace,
|
|
1191
|
-
});
|
|
1192
|
-
const composerNode = (_jsx(InputBox, { onSubmit: handleSubmit, disabled: isRunning || !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, terminalColumns: terminalColumns, cwd: args.cwd }));
|
|
1193
|
-
return (_jsx(ThemeProvider, { value: palette, children: _jsx("box", { style: { flexDirection: "column", height: terminalRows, backgroundColor: palette.background }, children: showHomeSurface ? (_jsx(HomeSurface, { terminalColumns: terminalColumns, terminalRows: terminalRows, modelLabel: agent.model ? displayModel(agent.model) : undefined, cwd: friendlyCwd(args.cwd), tips: buildTips(agent, safeRegistry), skillsCount: safeSkillRegistry.summaries().length, mcpConnectedCount: mcpConnectedCount, mcpTotalCount: mcpStates.length, hasAgentsFile: hasAgentsFile, composer: composerNode })) : (_jsxs(_Fragment, { children: [_jsxs("box", { style: { flexDirection: "column", paddingLeft: 1, paddingRight: 1, paddingTop: 1, flexShrink: 0 }, children: [_jsx(SessionHeader, { terminalColumns: terminalColumns, cwd: friendlyCwd(args.cwd), mode: permissionMode, model: displayModel(agent.model) || "no model" }), _jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: showHomeSurface ? null : welcomeBannerNode }, clearEpoch), pickerMode === "model" && (_jsx(ModelPicker, { registry: safeRegistry, current: agent.model, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker })), pickerMode === "provider" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1194
|
-
.filter((p) => isUserVisibleProvider(p.id))
|
|
1195
|
-
.map((p) => {
|
|
1196
|
-
const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
|
|
1197
|
-
const configuredLabel = configured?.apiKey ? "configured" : "needs key";
|
|
1198
|
-
return {
|
|
1199
|
-
id: p.id,
|
|
1200
|
-
name: `${p.name} [${configuredLabel}]`,
|
|
1201
|
-
enabled: true,
|
|
1202
|
-
};
|
|
1203
|
-
}), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker })), pickerMode === "provider-add" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1204
|
-
.filter((p) => isUserVisibleProvider(p.id))
|
|
1205
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" })), pickerMode === "login" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1206
|
-
.filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
|
|
1207
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" })), pickerMode === "logout" && (_jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
|
|
1208
|
-
.filter((p) => safeRegistry.getAuthStorage().has(p.id))
|
|
1209
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" })), pickerMode === "key" && keyTarget && (_jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
|
|
1210
|
-
closePicker();
|
|
1211
|
-
setKeyProviderId(null);
|
|
1212
|
-
} })), pickerMode === "skill" && (_jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
|
|
1213
|
-
fillComposer(`/${name} `);
|
|
1214
|
-
closePicker();
|
|
1215
|
-
}, onCancel: closePicker })), pickerMode === "feishu-setup" && (_jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
1216
|
-
closePicker();
|
|
1217
|
-
addMessage("assistant", summary);
|
|
1218
|
-
}, onCancel: () => {
|
|
1219
|
-
closePicker();
|
|
1220
|
-
addMessage("assistant", "已取消 Feishu setup。");
|
|
1221
|
-
} }))] }), todos.length > 0 && !pickerMode && !pendingPlan && !pendingQuestion && (_jsx("box", { style: { paddingLeft: 1, paddingRight: 1, flexShrink: 0 }, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !pendingQuestion && (_jsx("box", { style: { paddingLeft: 1, paddingRight: 1, flexShrink: 0 }, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
|
|
1222
|
-
const resolve = pendingPlan.resolve;
|
|
1223
|
-
setPendingPlan(null);
|
|
1224
|
-
resolve({ action: "approve", plan: finalPlan });
|
|
1225
|
-
}, onReject: (reason) => {
|
|
1226
|
-
const resolve = pendingPlan.resolve;
|
|
1227
|
-
setPendingPlan(null);
|
|
1228
|
-
resolve({ action: "reject", reason });
|
|
1229
|
-
} }) })), pendingApproval && !pickerMode && !pendingPlan && !pendingQuestion && (_jsx("box", { style: { paddingLeft: 1, paddingRight: 1, flexShrink: 0 }, children: _jsx(ApprovalDialog, { request: pendingApproval.request, onDecision: (decision) => {
|
|
1230
|
-
const resolve = pendingApproval.resolve;
|
|
1231
|
-
setPendingApproval(null);
|
|
1232
|
-
resolve(decision);
|
|
1233
|
-
}, onAllowBashPrefix: (prefix) => {
|
|
1234
|
-
bashAllowlist?.add(prefix);
|
|
1235
|
-
} }) })), pendingQuestion && !pickerMode && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx("box", { style: { paddingLeft: 1, paddingRight: 1, flexShrink: 0 }, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
|
|
1236
|
-
questionController?.reply(pendingQuestion.id, answers);
|
|
1237
|
-
setPendingQuestion(null);
|
|
1238
|
-
}, onCancel: () => {
|
|
1239
|
-
questionController?.reject(pendingQuestion.id);
|
|
1240
|
-
setPendingQuestion(null);
|
|
1241
|
-
} }) })), pendingFeedback && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx("box", { style: { paddingLeft: 1, paddingRight: 1, flexShrink: 0 }, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
|
|
1242
|
-
if (result.kind === "success") {
|
|
1243
|
-
addMessage("assistant", `Feedback submitted: ${result.url}`);
|
|
1244
|
-
}
|
|
1245
|
-
else if (result.kind === "error") {
|
|
1246
|
-
addMessage("error", `Feedback failed: ${result.message}`);
|
|
1247
|
-
}
|
|
1248
|
-
} }) })), !isExiting && isRunning && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx("box", { style: { paddingLeft: 1, paddingRight: 1, paddingBottom: 1, flexShrink: 0 }, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick }) })), !isExiting && !pickerMode && (_jsx("box", { style: { paddingBottom: 1, flexShrink: 0 }, children: composerNode })), !isExiting && (_jsx("box", { style: { flexShrink: 0 }, children: _jsx(FooterBar, { data: footerData }) }))] })) }) }));
|
|
1249
|
-
}
|
|
1250
|
-
function SessionHeader({ terminalColumns, cwd, mode, model, }) {
|
|
1251
|
-
const theme = useTheme();
|
|
1252
|
-
const width = Math.max(20, terminalColumns - 4);
|
|
1253
|
-
const modeLabel = mode === "plan" ? "PLAN" : mode === "bypassPermissions" ? "BYPASS" : "BUILD";
|
|
1254
|
-
const left = ` ${cwd}`;
|
|
1255
|
-
const right = `${modeLabel} · ${model} `;
|
|
1256
|
-
const filler = "─".repeat(Math.max(1, width - left.length - right.length));
|
|
1257
|
-
return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx("text", { fg: theme.textMuted, content: left }), _jsx("text", { fg: theme.textDim, content: filler }), _jsx("text", { fg: mode === "bypassPermissions" ? theme.warning : theme.accent, attributes: 1, content: right })] }));
|
|
1258
|
-
}
|
|
1259
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
1260
|
-
const GENERIC_PHRASES = [
|
|
1261
|
-
"mapping the workspace",
|
|
1262
|
-
"reading the room",
|
|
1263
|
-
"following the threads",
|
|
1264
|
-
"connecting the pieces",
|
|
1265
|
-
"sorting the context",
|
|
1266
|
-
"scanning the structure",
|
|
1267
|
-
"shaping the next step",
|
|
1268
|
-
"gathering signal",
|
|
1269
|
-
"checking the edges",
|
|
1270
|
-
"lining up the answer",
|
|
1271
|
-
"tracing the flow",
|
|
1272
|
-
"building the picture",
|
|
1273
|
-
"walking the graph",
|
|
1274
|
-
"collecting the clues",
|
|
1275
|
-
"framing the problem",
|
|
1276
|
-
"locating the source",
|
|
1277
|
-
"resolving the shape",
|
|
1278
|
-
"untangling the state",
|
|
1279
|
-
"comparing the paths",
|
|
1280
|
-
"narrowing the target",
|
|
1281
|
-
"tracking the changes",
|
|
1282
|
-
"reading the patterns",
|
|
1283
|
-
"weighing the options",
|
|
1284
|
-
"assembling the context",
|
|
1285
|
-
"following the signal",
|
|
1286
|
-
"checking the assumptions",
|
|
1287
|
-
"aligning the details",
|
|
1288
|
-
"testing the shape",
|
|
1289
|
-
"pulling the thread",
|
|
1290
|
-
"cleaning the edges",
|
|
1291
|
-
"refining the draft",
|
|
1292
|
-
"verifying the route",
|
|
1293
|
-
"making sense of it",
|
|
1294
|
-
"looking for leverage",
|
|
1295
|
-
"stitching the answer",
|
|
1296
|
-
"holding the thread",
|
|
1297
|
-
"distilling the noise",
|
|
1298
|
-
"finding the seam",
|
|
1299
|
-
"reading between the lines",
|
|
1300
|
-
"preparing the response",
|
|
1301
|
-
];
|
|
1302
|
-
const TOOL_TARGET_PHRASES = {
|
|
1303
|
-
read: "reading files",
|
|
1304
|
-
write: "writing changes",
|
|
1305
|
-
edit: "patching files",
|
|
1306
|
-
grep: "searching the codebase",
|
|
1307
|
-
glob: "scanning paths",
|
|
1308
|
-
ls: "listing directories",
|
|
1309
|
-
bash: "running command",
|
|
1310
|
-
web_search: "searching the web",
|
|
1311
|
-
web_fetch: "fetching a page",
|
|
1312
|
-
task: "spawning subagent",
|
|
1313
|
-
};
|
|
1314
|
-
function formatTokensApprox(chars) {
|
|
1315
|
-
const tokens = Math.max(0, Math.round(chars / 4));
|
|
1316
|
-
if (tokens < 1000)
|
|
1317
|
-
return `${tokens}`;
|
|
1318
|
-
if (tokens < 10000)
|
|
1319
|
-
return `${(tokens / 1000).toFixed(1)}k`;
|
|
1320
|
-
return `${Math.round(tokens / 1000)}k`;
|
|
1321
|
-
}
|
|
1322
|
-
function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars, nowTick, }) {
|
|
1323
|
-
void nowTick;
|
|
1324
|
-
const theme = useTheme();
|
|
1325
|
-
const [frameIndex, setFrameIndex] = useState(0);
|
|
1326
|
-
const [idlePhrase, setIdlePhrase] = useState(() => GENERIC_PHRASES[0]);
|
|
1327
|
-
// Frame timer is independent of the agent state — keeps animation smooth.
|
|
1328
|
-
useEffect(() => {
|
|
1329
|
-
const t = setInterval(() => {
|
|
1330
|
-
setFrameIndex((i) => (i + 1) % SPINNER_FRAMES.length);
|
|
1331
|
-
}, 100);
|
|
1332
|
-
return () => clearInterval(t);
|
|
1333
|
-
}, []);
|
|
1334
|
-
// Determine state: active tool > streaming text > streaming reasoning > idle
|
|
1335
|
-
const activeTool = [...tools].reverse().find((t) => !t.result);
|
|
1336
|
-
const state = activeTool
|
|
1337
|
-
? "tool"
|
|
1338
|
-
: hasStreamingText
|
|
1339
|
-
? "text"
|
|
1340
|
-
: hasStreamingReasoning
|
|
1341
|
-
? "reasoning"
|
|
1342
|
-
: "idle";
|
|
1343
|
-
// Rotate idle phrases on a slower cadence; only matters in the idle state.
|
|
1344
|
-
useEffect(() => {
|
|
1345
|
-
if (state !== "idle")
|
|
1346
|
-
return;
|
|
1347
|
-
const t = setInterval(() => {
|
|
1348
|
-
setIdlePhrase((current) => {
|
|
1349
|
-
const candidates = GENERIC_PHRASES.filter((item) => item !== current);
|
|
1350
|
-
return candidates[Math.floor(Math.random() * candidates.length)] || current;
|
|
1351
|
-
});
|
|
1352
|
-
}, 1500);
|
|
1353
|
-
return () => clearInterval(t);
|
|
1354
|
-
}, [state]);
|
|
1355
|
-
let phrase;
|
|
1356
|
-
if (state === "tool" && activeTool) {
|
|
1357
|
-
phrase =
|
|
1358
|
-
TOOL_TARGET_PHRASES[activeTool.name] || `running ${activeTool.name}`;
|
|
1359
|
-
}
|
|
1360
|
-
else if (state === "text") {
|
|
1361
|
-
phrase = "writing the response";
|
|
1362
|
-
}
|
|
1363
|
-
else if (state === "reasoning") {
|
|
1364
|
-
phrase = "working through the request";
|
|
1365
|
-
}
|
|
1366
|
-
else {
|
|
1367
|
-
phrase = idlePhrase;
|
|
1368
|
-
}
|
|
1369
|
-
const tokenText = streamedChars > 0 ? `↓${formatTokensApprox(streamedChars)} tok` : "";
|
|
1370
|
-
return (_jsxs("box", { children: [_jsx("text", { fg: theme.accent, children: SPINNER_FRAMES[frameIndex] }), _jsxs("text", { fg: theme.muted, children: [" ", phrase, " "] }), _jsxs("text", { fg: theme.muted, children: ["(", tokenText ? `${tokenText} · ` : "", "esc\u00B7esc to interrupt)"] })] }));
|
|
1371
|
-
}
|