@bubblebrain-ai/bubble 0.0.10 → 0.0.11
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.d.ts +1 -0
- package/dist/agent.js +5 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +31 -3
- package/dist/feedback/collect.d.ts +7 -0
- package/dist/feedback/collect.js +119 -0
- package/dist/feedback/config.d.ts +14 -0
- package/dist/feedback/config.js +16 -0
- package/dist/feedback/redact.d.ts +1 -0
- package/dist/feedback/redact.js +25 -0
- package/dist/feedback/submit.d.ts +6 -0
- package/dist/feedback/submit.js +43 -0
- package/dist/feedback/types.d.ts +22 -0
- package/dist/feishu/agent-host/approval-card.d.ts +11 -0
- package/dist/feishu/agent-host/approval-card.js +46 -0
- package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
- package/dist/feishu/agent-host/approval-ui.js +214 -0
- package/dist/feishu/agent-host/run-driver.d.ts +51 -0
- package/dist/feishu/agent-host/run-driver.js +295 -0
- package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
- package/dist/feishu/agent-host/runtime-deps.js +8 -0
- package/dist/feishu/card/budget.d.ts +40 -0
- package/dist/feishu/card/budget.js +134 -0
- package/dist/feishu/card/renderer.d.ts +29 -0
- package/dist/feishu/card/renderer.js +245 -0
- package/dist/feishu/card/run-state-types.d.ts +49 -0
- package/dist/feishu/card/run-state-types.js +15 -0
- package/dist/feishu/card/run-state.d.ts +21 -0
- package/dist/feishu/card/run-state.js +217 -0
- package/dist/feishu/channel/channel.d.ts +52 -0
- package/dist/feishu/channel/channel.js +74 -0
- package/dist/feishu/config.d.ts +24 -0
- package/dist/feishu/config.js +97 -0
- package/dist/feishu/format.d.ts +6 -0
- package/dist/feishu/format.js +14 -0
- package/dist/feishu/index.d.ts +4 -0
- package/dist/feishu/index.js +4 -0
- package/dist/feishu/logger.d.ts +31 -0
- package/dist/feishu/logger.js +62 -0
- package/dist/feishu/paths.d.ts +12 -0
- package/dist/feishu/paths.js +38 -0
- package/dist/feishu/process-registry.d.ts +29 -0
- package/dist/feishu/process-registry.js +90 -0
- package/dist/feishu/router/commands.d.ts +38 -0
- package/dist/feishu/router/commands.js +285 -0
- package/dist/feishu/router/event-router.d.ts +40 -0
- package/dist/feishu/router/event-router.js +208 -0
- package/dist/feishu/router/whitelist.d.ts +23 -0
- package/dist/feishu/router/whitelist.js +20 -0
- package/dist/feishu/runtime/active-runs.d.ts +32 -0
- package/dist/feishu/runtime/active-runs.js +84 -0
- package/dist/feishu/runtime/pending-queue.d.ts +36 -0
- package/dist/feishu/runtime/pending-queue.js +98 -0
- package/dist/feishu/runtime/process-pool.d.ts +29 -0
- package/dist/feishu/runtime/process-pool.js +49 -0
- package/dist/feishu/schema.d.ts +17 -0
- package/dist/feishu/schema.js +252 -0
- package/dist/feishu/scope/scope-registry.d.ts +39 -0
- package/dist/feishu/scope/scope-registry.js +148 -0
- package/dist/feishu/scope/session-binder.d.ts +44 -0
- package/dist/feishu/scope/session-binder.js +100 -0
- package/dist/feishu/scope/session-store.d.ts +24 -0
- package/dist/feishu/scope/session-store.js +73 -0
- package/dist/feishu/secrets.d.ts +37 -0
- package/dist/feishu/secrets.js +129 -0
- package/dist/feishu/serve.d.ts +12 -0
- package/dist/feishu/serve.js +288 -0
- package/dist/feishu/types.d.ts +75 -0
- package/dist/feishu/types.js +23 -0
- package/dist/feishu/wizard.d.ts +24 -0
- package/dist/feishu/wizard.js +121 -0
- package/dist/main.js +78 -29
- package/dist/model-catalog.js +3 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.js +88 -2
- package/dist/slash-commands/commands.js +13 -0
- package/dist/slash-commands/feishu.d.ts +17 -0
- package/dist/slash-commands/feishu.js +400 -0
- package/dist/slash-commands/types.d.ts +3 -1
- package/dist/tui-ink/app.js +218 -60
- package/dist/tui-ink/code-highlight.js +2 -3
- package/dist/tui-ink/detect-theme.d.ts +1 -18
- package/dist/tui-ink/detect-theme.js +1 -37
- package/dist/tui-ink/display-history.d.ts +20 -3
- package/dist/tui-ink/display-history.js +26 -27
- package/dist/tui-ink/feedback-dialog.d.ts +19 -0
- package/dist/tui-ink/feedback-dialog.js +123 -0
- package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
- package/dist/tui-ink/feishu-setup-picker.js +261 -0
- package/dist/tui-ink/input-box.d.ts +3 -0
- package/dist/tui-ink/input-box.js +27 -0
- package/dist/tui-ink/input-history.js +3 -5
- package/dist/tui-ink/markdown.d.ts +32 -0
- package/dist/tui-ink/markdown.js +111 -4
- package/dist/tui-ink/message-list.d.ts +1 -6
- package/dist/tui-ink/message-list.js +85 -34
- package/dist/tui-ink/model-picker.js +1 -4
- package/dist/tui-ink/run-session-picker.d.ts +10 -0
- package/dist/tui-ink/run-session-picker.js +22 -0
- package/dist/tui-ink/run.js +7 -2
- package/dist/tui-ink/session-picker.d.ts +10 -0
- package/dist/tui-ink/session-picker.js +112 -0
- package/dist/tui-ink/terminal-mouse.d.ts +4 -0
- package/dist/tui-ink/terminal-mouse.js +23 -0
- package/dist/tui-ink/trace-groups.js +25 -2
- package/dist/tui-ink/welcome.js +2 -4
- package/package.json +4 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/display-history.d.ts +0 -44
- package/dist/tui/display-history.js +0 -243
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/file-mentions.d.ts +0 -29
- package/dist/tui/file-mentions.js +0 -174
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/image-paste.d.ts +0 -95
- package/dist/tui/image-paste.js +0 -505
- 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 -21
- 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/recent-activity.d.ts +0 -8
- package/dist/tui/recent-activity.js +0 -71
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -38
- package/dist/tui/run.js +0 -6996
- 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 -114
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -30
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
package/dist/tui-ink/app.js
CHANGED
|
@@ -4,11 +4,12 @@ import { Box, Text, useApp, useInput } from "ink";
|
|
|
4
4
|
import { AgentAbortError } from "../agent.js";
|
|
5
5
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
6
6
|
import { UserConfig, maskKey } from "../config.js";
|
|
7
|
-
import { InputBox } from "./input-box.js";
|
|
7
|
+
import { InputBox, isCtrlCInput } from "./input-box.js";
|
|
8
8
|
import { MessageList } from "./message-list.js";
|
|
9
|
-
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, nextDisplayMessageKey, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
|
|
9
|
+
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, nextDisplayMessageKey, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
|
|
10
10
|
import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
|
|
11
11
|
import { ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
|
|
12
|
+
import { FeishuSetupPicker } from "./feishu-setup-picker.js";
|
|
12
13
|
import { BUILTIN_PROVIDERS, ProviderRegistry, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
13
14
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
14
15
|
import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
|
|
@@ -23,6 +24,9 @@ import { PlanConfirm } from "./plan-confirm.js";
|
|
|
23
24
|
import { ApprovalDialog } from "./approval/approval-dialog.js";
|
|
24
25
|
import { getNextPermissionMode } from "../permission/mode.js";
|
|
25
26
|
import { QuestionDialog } from "./question-dialog.js";
|
|
27
|
+
import { FeedbackDialog } from "./feedback-dialog.js";
|
|
28
|
+
import { collectFeedback } from "../feedback/collect.js";
|
|
29
|
+
import { hasTerminalMouseSequence } from "./terminal-mouse.js";
|
|
26
30
|
import os from "node:os";
|
|
27
31
|
import { existsSync } from "node:fs";
|
|
28
32
|
import { join } from "node:path";
|
|
@@ -174,6 +178,76 @@ function withMessageKey(message) {
|
|
|
174
178
|
const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
|
|
175
179
|
return { ...message, key: nextDisplayMessageKey(prefix) };
|
|
176
180
|
}
|
|
181
|
+
const STREAMING_STATIC_FLUSH_MIN_CHARS = 5000;
|
|
182
|
+
const STREAMING_STATIC_FLUSH_TARGET_CHARS = 3600;
|
|
183
|
+
const STREAMING_STATIC_FLUSH_MIN_TAIL = 700;
|
|
184
|
+
function findStreamingStaticFlushIndex(content) {
|
|
185
|
+
if (content.length < STREAMING_STATIC_FLUSH_MIN_CHARS)
|
|
186
|
+
return -1;
|
|
187
|
+
const upper = Math.min(STREAMING_STATIC_FLUSH_TARGET_CHARS, content.length - STREAMING_STATIC_FLUSH_MIN_TAIL);
|
|
188
|
+
if (upper <= 0)
|
|
189
|
+
return -1;
|
|
190
|
+
const search = content.slice(0, upper);
|
|
191
|
+
const paragraphBreak = search.lastIndexOf("\n\n");
|
|
192
|
+
if (paragraphBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
193
|
+
return paragraphBreak + 2;
|
|
194
|
+
}
|
|
195
|
+
const lineBreak = search.lastIndexOf("\n");
|
|
196
|
+
if (lineBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
197
|
+
return lineBreak + 1;
|
|
198
|
+
}
|
|
199
|
+
return -1;
|
|
200
|
+
}
|
|
201
|
+
function cloneDisplayPart(part) {
|
|
202
|
+
if (part.type === "text") {
|
|
203
|
+
return { type: "text", content: part.content };
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
type: "tools",
|
|
207
|
+
toolCalls: part.toolCalls.map((toolCall) => ({
|
|
208
|
+
...toolCall,
|
|
209
|
+
args: { ...toolCall.args },
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function splitDisplayPartsAtTextOffset(parts, offset) {
|
|
214
|
+
const flushedParts = [];
|
|
215
|
+
const remainingParts = [];
|
|
216
|
+
let remainingOffset = Math.max(0, offset);
|
|
217
|
+
let reachedTail = false;
|
|
218
|
+
for (const part of parts) {
|
|
219
|
+
if (part.type === "text") {
|
|
220
|
+
if (!reachedTail && remainingOffset >= part.content.length) {
|
|
221
|
+
if (part.content)
|
|
222
|
+
flushedParts.push(cloneDisplayPart(part));
|
|
223
|
+
remainingOffset -= part.content.length;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!reachedTail && remainingOffset > 0) {
|
|
227
|
+
const head = part.content.slice(0, remainingOffset);
|
|
228
|
+
const tail = part.content.slice(remainingOffset);
|
|
229
|
+
if (head)
|
|
230
|
+
flushedParts.push({ type: "text", content: head });
|
|
231
|
+
if (tail)
|
|
232
|
+
remainingParts.push({ type: "text", content: tail });
|
|
233
|
+
remainingOffset = 0;
|
|
234
|
+
reachedTail = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
remainingParts.push(cloneDisplayPart(part));
|
|
238
|
+
reachedTail = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!reachedTail && remainingOffset > 0) {
|
|
242
|
+
flushedParts.push(cloneDisplayPart(part));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
remainingParts.push(cloneDisplayPart(part));
|
|
246
|
+
reachedTail = true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { flushedParts, remainingParts };
|
|
250
|
+
}
|
|
177
251
|
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 }) {
|
|
178
252
|
const [themeMode, setThemeMode] = useState(initialThemeMode ?? "auto");
|
|
179
253
|
// `detectedTheme` is captured once at startup in main.ts. We keep it in state
|
|
@@ -205,45 +279,50 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
205
279
|
const [pendingPlan, setPendingPlan] = useState(null);
|
|
206
280
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
207
281
|
const [pendingQuestion, setPendingQuestion] = useState(null);
|
|
282
|
+
const [pendingFeedback, setPendingFeedback] = useState(null);
|
|
208
283
|
const [pickerMode, setPickerMode] = useState(null);
|
|
209
284
|
const [keyProviderId, setKeyProviderId] = useState(null);
|
|
210
285
|
const [verboseTrace, setVerboseTrace] = useState(false);
|
|
211
286
|
const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
|
|
212
287
|
const { columns: terminalColumns } = useTerminalSize();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const previousColumnsRef = useRef(null);
|
|
288
|
+
const showWelcome = shouldShowWelcomeBanner({
|
|
289
|
+
messages,
|
|
290
|
+
startedWithVisibleHistory: startedWithVisibleHistoryRef.current,
|
|
291
|
+
});
|
|
292
|
+
const activeAbortRef = useRef(null);
|
|
293
|
+
const exitRequestedRef = useRef(false);
|
|
294
|
+
const sessionStartRef = useRef(Date.now());
|
|
295
|
+
const previousTerminalColumnsRef = useRef(null);
|
|
222
296
|
useEffect(() => {
|
|
223
|
-
if (
|
|
224
|
-
|
|
297
|
+
if (previousTerminalColumnsRef.current === null) {
|
|
298
|
+
previousTerminalColumnsRef.current = terminalColumns;
|
|
225
299
|
return;
|
|
226
300
|
}
|
|
227
|
-
if (
|
|
301
|
+
if (previousTerminalColumnsRef.current === terminalColumns)
|
|
228
302
|
return;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
303
|
+
previousTerminalColumnsRef.current = terminalColumns;
|
|
304
|
+
// This follows Gemini CLI's normal terminal-buffer strategy: after a
|
|
305
|
+
// resize, the previous live Ink frame may have wrapped at the old width,
|
|
306
|
+
// so cursor-up based repaint can leave stale progress frames behind.
|
|
307
|
+
// Debounce resize storms, then clear and replay Static at the settled width.
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
if (exitRequestedRef.current)
|
|
310
|
+
return;
|
|
311
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
312
|
+
setClearEpoch((epoch) => epoch + 1);
|
|
313
|
+
}, 300);
|
|
314
|
+
return () => clearTimeout(timer);
|
|
232
315
|
}, [terminalColumns]);
|
|
233
|
-
const activeAbortRef = useRef(null);
|
|
234
|
-
const exitRequestedRef = useRef(false);
|
|
235
|
-
const sessionStartRef = useRef(Date.now());
|
|
236
316
|
// Set true the moment /quit is invoked so we can hide dynamic UI (composer,
|
|
237
317
|
// waiting indicator, footer) before Ink snapshots its final frame into the
|
|
238
318
|
// shell scrollback. Without this, the last visible "> " input row stays
|
|
239
319
|
// glued to the bottom of the terminal after exit.
|
|
240
320
|
const [isExiting, setIsExiting] = useState(false);
|
|
241
|
-
// 1Hz tick
|
|
242
|
-
//
|
|
243
|
-
// don't churn renders at idle.
|
|
321
|
+
// 1Hz tick keeps the composer activity indicator animated while the agent is
|
|
322
|
+
// running without churning renders at idle.
|
|
244
323
|
const [nowTick, setNowTick] = useState(() => Date.now());
|
|
245
|
-
// Timestamp of when the current agent run started
|
|
246
|
-
//
|
|
324
|
+
// Timestamp of when the current agent run started. Used only for the final
|
|
325
|
+
// per-task duration summary.
|
|
247
326
|
const runStartRef = useRef(null);
|
|
248
327
|
// Mark the moment the run started; flips back to null in the finally block.
|
|
249
328
|
useEffect(() => {
|
|
@@ -380,7 +459,13 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
380
459
|
}));
|
|
381
460
|
}, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
|
|
382
461
|
useInput((input, key) => {
|
|
383
|
-
if (
|
|
462
|
+
if (isCtrlCInput(input, key)) {
|
|
463
|
+
requestExit();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback)
|
|
467
|
+
return;
|
|
468
|
+
if (hasTerminalMouseSequence(input))
|
|
384
469
|
return;
|
|
385
470
|
if (key.ctrl && input === "o" && !pickerMode) {
|
|
386
471
|
setVerboseTrace((v) => !v);
|
|
@@ -429,13 +514,11 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
429
514
|
updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
|
|
430
515
|
}, [updateDisplayMessages]);
|
|
431
516
|
const clearMessages = useCallback(() => {
|
|
432
|
-
|
|
433
|
-
//
|
|
434
|
-
// them — emptying the React state alone leaves the old output visible.
|
|
435
|
-
// Wipe screen + scrollback (xterm \x1b[3J) and bump the epoch below so
|
|
436
|
-
// Static remounts with a fresh internal cursor.
|
|
517
|
+
// Static history is already written to terminal scrollback, so clearing
|
|
518
|
+
// React state alone would leave old rows visible.
|
|
437
519
|
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
438
|
-
|
|
520
|
+
setMessages([]);
|
|
521
|
+
setClearEpoch((epoch) => epoch + 1);
|
|
439
522
|
}, []);
|
|
440
523
|
const openPicker = useCallback((mode, providerId) => {
|
|
441
524
|
if (mode === "key") {
|
|
@@ -443,6 +526,11 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
443
526
|
}
|
|
444
527
|
setPickerMode(mode);
|
|
445
528
|
}, []);
|
|
529
|
+
const openFeedback = useCallback((initialDescription) => {
|
|
530
|
+
const base = collectFeedback(agent, { description: "" });
|
|
531
|
+
const { description: _drop, ...rest } = base;
|
|
532
|
+
setPendingFeedback({ base: rest, initialDescription });
|
|
533
|
+
}, [agent]);
|
|
446
534
|
const handleModelSelect = useCallback((model) => {
|
|
447
535
|
const run = async () => {
|
|
448
536
|
agent.model = model;
|
|
@@ -529,6 +617,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
529
617
|
throw new Error("Provider creation not available");
|
|
530
618
|
}),
|
|
531
619
|
openPicker,
|
|
620
|
+
openFeedback,
|
|
532
621
|
registry: safeRegistry,
|
|
533
622
|
skillRegistry: safeSkillRegistry,
|
|
534
623
|
bashAllowlist,
|
|
@@ -561,6 +650,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
561
650
|
throw new Error("Provider creation not available");
|
|
562
651
|
}),
|
|
563
652
|
openPicker,
|
|
653
|
+
openFeedback,
|
|
564
654
|
registry: safeRegistry,
|
|
565
655
|
skillRegistry: safeSkillRegistry,
|
|
566
656
|
bashAllowlist,
|
|
@@ -642,7 +732,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
642
732
|
!!assistantReasoning ||
|
|
643
733
|
toolCalls.length > 0 ||
|
|
644
734
|
assistantParts.length > 0);
|
|
645
|
-
const commitAssistantMessage = () => {
|
|
735
|
+
const commitAssistantMessage = (taskElapsedMs) => {
|
|
646
736
|
if (!hasAssistantOutput())
|
|
647
737
|
return;
|
|
648
738
|
const currentParts = snapshotDisplayParts(assistantParts);
|
|
@@ -665,6 +755,9 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
665
755
|
if (currentParts.length > 0) {
|
|
666
756
|
msg.parts = currentParts;
|
|
667
757
|
}
|
|
758
|
+
if (taskElapsedMs !== undefined && Number.isFinite(taskElapsedMs) && taskElapsedMs > 0) {
|
|
759
|
+
msg.taskElapsedMs = taskElapsedMs;
|
|
760
|
+
}
|
|
668
761
|
updateDisplayMessages((prev) => [...prev, msg]);
|
|
669
762
|
};
|
|
670
763
|
const clearAssistantStream = () => {
|
|
@@ -677,14 +770,54 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
677
770
|
toolCalls.length = 0;
|
|
678
771
|
assistantParts.length = 0;
|
|
679
772
|
};
|
|
773
|
+
const flushAssistantStaticChunk = () => {
|
|
774
|
+
if (toolCalls.some((toolCall) => toolCall.result === undefined)) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
const splitIndex = findStreamingStaticFlushIndex(assistantContent);
|
|
778
|
+
if (splitIndex <= 0)
|
|
779
|
+
return false;
|
|
780
|
+
const { flushedParts, remainingParts } = splitDisplayPartsAtTextOffset(assistantParts, splitIndex);
|
|
781
|
+
const flushedContent = contentFromParts(flushedParts);
|
|
782
|
+
const flushedToolCalls = toolCallsFromParts(flushedParts);
|
|
783
|
+
if (!flushedContent && flushedToolCalls.length === 0)
|
|
784
|
+
return false;
|
|
785
|
+
const msg = {
|
|
786
|
+
key: nextDisplayMessageKey("asst"),
|
|
787
|
+
role: "assistant",
|
|
788
|
+
content: flushedContent,
|
|
789
|
+
};
|
|
790
|
+
if (assistantReasoning) {
|
|
791
|
+
msg.reasoning = assistantReasoning;
|
|
792
|
+
assistantReasoning = "";
|
|
793
|
+
setStreamingReasoning("");
|
|
794
|
+
}
|
|
795
|
+
if (flushedToolCalls.length > 0) {
|
|
796
|
+
msg.toolCalls = flushedToolCalls;
|
|
797
|
+
}
|
|
798
|
+
if (flushedParts.length > 0) {
|
|
799
|
+
msg.parts = flushedParts;
|
|
800
|
+
}
|
|
801
|
+
updateDisplayMessages((prev) => [...prev, msg]);
|
|
802
|
+
assistantParts.splice(0, assistantParts.length, ...remainingParts);
|
|
803
|
+
assistantContent = contentFromParts(assistantParts);
|
|
804
|
+
const remainingToolCalls = toolCallsFromParts(assistantParts);
|
|
805
|
+
toolCalls.splice(0, toolCalls.length, ...remainingToolCalls);
|
|
806
|
+
setStreamingContent(assistantContent);
|
|
807
|
+
setStreamingTools([...toolCalls]);
|
|
808
|
+
syncStreamingParts();
|
|
809
|
+
return true;
|
|
810
|
+
};
|
|
680
811
|
try {
|
|
681
812
|
for await (const event of agent.run(actualInput, args.cwd, { abortSignal: abortController.signal })) {
|
|
682
813
|
switch (event.type) {
|
|
683
814
|
case "text_delta":
|
|
684
815
|
assistantContent += event.content;
|
|
685
816
|
appendTextPart(assistantParts, event.content);
|
|
686
|
-
|
|
687
|
-
|
|
817
|
+
if (!flushAssistantStaticChunk()) {
|
|
818
|
+
setStreamingContent(assistantContent);
|
|
819
|
+
syncStreamingParts();
|
|
820
|
+
}
|
|
688
821
|
break;
|
|
689
822
|
case "reasoning_delta":
|
|
690
823
|
assistantReasoning += event.content;
|
|
@@ -798,7 +931,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
798
931
|
syncStreamingParts();
|
|
799
932
|
break;
|
|
800
933
|
}
|
|
801
|
-
commitAssistantMessage();
|
|
934
|
+
commitAssistantMessage(runStartRef.current ? Date.now() - runStartRef.current : undefined);
|
|
802
935
|
clearAssistantStream();
|
|
803
936
|
break;
|
|
804
937
|
}
|
|
@@ -860,6 +993,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
860
993
|
throw new Error("Provider creation not available");
|
|
861
994
|
}),
|
|
862
995
|
openPicker,
|
|
996
|
+
openFeedback,
|
|
863
997
|
registry: safeRegistry,
|
|
864
998
|
skillRegistry: safeSkillRegistry,
|
|
865
999
|
bashAllowlist,
|
|
@@ -879,7 +1013,24 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
879
1013
|
setPermissionMode(agent.mode);
|
|
880
1014
|
}
|
|
881
1015
|
if (result) {
|
|
882
|
-
|
|
1016
|
+
// `/compact` rewrites agent.messages, so the Ink transcript needs to
|
|
1017
|
+
// be rebuilt from the new agent state before appending the summary
|
|
1018
|
+
// card; otherwise the pre-compaction history would keep rendering.
|
|
1019
|
+
if (result.startsWith("✓ Compaction complete")) {
|
|
1020
|
+
const summary = latestCompactionSummary(agent.messages);
|
|
1021
|
+
updateDisplayMessages(() => [
|
|
1022
|
+
...reconstructDisplayMessages(agent.messages),
|
|
1023
|
+
{
|
|
1024
|
+
role: "assistant",
|
|
1025
|
+
content: result,
|
|
1026
|
+
syntheticKind: "ui_compact_summary",
|
|
1027
|
+
compactionSummary: summary,
|
|
1028
|
+
},
|
|
1029
|
+
]);
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
addMessage("assistant", result);
|
|
1033
|
+
}
|
|
883
1034
|
}
|
|
884
1035
|
if (inject) {
|
|
885
1036
|
await runAgentInput(inject, input);
|
|
@@ -924,15 +1075,11 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
924
1075
|
return null;
|
|
925
1076
|
})()
|
|
926
1077
|
: null;
|
|
927
|
-
const showWelcome = shouldShowWelcomeBanner({
|
|
928
|
-
messages,
|
|
929
|
-
startedWithVisibleHistory: startedWithVisibleHistoryRef.current,
|
|
930
|
-
});
|
|
931
1078
|
const mcpStates = mcpManager?.getStates() ?? [];
|
|
932
1079
|
const mcpConnectedCount = mcpStates.filter((state) => state.status.kind === "connected").length;
|
|
933
1080
|
const hasAgentsFile = useMemo(() => existsSync(join(args.cwd, "AGENTS.md")) || existsSync(join(args.cwd, ".bubble", "AGENTS.md")), [args.cwd]);
|
|
934
1081
|
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;
|
|
935
|
-
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column",
|
|
1082
|
+
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, flexShrink: 0, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode }, clearEpoch), pickerMode === "model" && (_jsx(ModelPicker, { registry: safeRegistry, current: agent.model, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: () => setPickerMode(null) })), pickerMode === "provider" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
936
1083
|
.filter((p) => isUserVisibleProvider(p.id))
|
|
937
1084
|
.map((p) => {
|
|
938
1085
|
const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
|
|
@@ -964,6 +1111,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
964
1111
|
throw new Error("Provider creation not available");
|
|
965
1112
|
}),
|
|
966
1113
|
openPicker,
|
|
1114
|
+
openFeedback,
|
|
967
1115
|
registry: safeRegistry,
|
|
968
1116
|
skillRegistry: safeSkillRegistry,
|
|
969
1117
|
bashAllowlist,
|
|
@@ -980,7 +1128,13 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
980
1128
|
});
|
|
981
1129
|
if (handled && result)
|
|
982
1130
|
addMessage("assistant", result);
|
|
983
|
-
}, onCancel: () => setPickerMode(null) }))
|
|
1131
|
+
}, onCancel: () => setPickerMode(null) })), pickerMode === "feishu-setup" && (_jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
1132
|
+
setPickerMode(null);
|
|
1133
|
+
addMessage("assistant", summary);
|
|
1134
|
+
}, onCancel: () => {
|
|
1135
|
+
setPickerMode(null);
|
|
1136
|
+
addMessage("assistant", "已取消 Feishu setup。");
|
|
1137
|
+
} }))] }), todos.length > 0 && !pickerMode && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
|
|
984
1138
|
const resolve = pendingPlan.resolve;
|
|
985
1139
|
setPendingPlan(null);
|
|
986
1140
|
resolve({ action: "approve", plan: finalPlan });
|
|
@@ -994,22 +1148,29 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
994
1148
|
resolve(decision);
|
|
995
1149
|
}, onAllowBashPrefix: (prefix) => {
|
|
996
1150
|
bashAllowlist?.add(prefix);
|
|
997
|
-
} }) })), pendingQuestion && !pickerMode && !pendingPlan && !pendingApproval && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
|
|
1151
|
+
} }) })), pendingQuestion && !pickerMode && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
|
|
998
1152
|
questionController?.reply(pendingQuestion.id, answers);
|
|
999
1153
|
setPendingQuestion(null);
|
|
1000
1154
|
}, onCancel: () => {
|
|
1001
1155
|
questionController?.reject(pendingQuestion.id);
|
|
1002
1156
|
setPendingQuestion(null);
|
|
1003
|
-
} }) })),
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1157
|
+
} }) })), pendingFeedback && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
|
|
1158
|
+
if (result.kind === "success") {
|
|
1159
|
+
addMessage("assistant", `Feedback submitted: ${result.url}`);
|
|
1160
|
+
}
|
|
1161
|
+
else if (result.kind === "error") {
|
|
1162
|
+
addMessage("error", `Feedback failed: ${result.message}`);
|
|
1163
|
+
}
|
|
1164
|
+
} }) })), !isExiting && isRunning && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 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, { paddingBottom: 1, flexShrink: 0, children: _jsx(InputBox, { onSubmit: handleSubmit, disabled: isRunning || !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback, skillRegistry: safeSkillRegistry, terminalColumns: terminalColumns, cwd: args.cwd }) })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
|
|
1165
|
+
cwd: args.cwd,
|
|
1166
|
+
providerId: agent.providerId || safeRegistry.getDefault()?.id || "unknown",
|
|
1167
|
+
model: displayModel(agent.model) || "no model",
|
|
1168
|
+
thinkingLevel,
|
|
1169
|
+
showThinking: getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2,
|
|
1170
|
+
mode: permissionMode,
|
|
1171
|
+
usageTotals,
|
|
1172
|
+
verboseTrace,
|
|
1173
|
+
}) }) }))] }) }));
|
|
1013
1174
|
}
|
|
1014
1175
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
1015
1176
|
const GENERIC_PHRASES = [
|
|
@@ -1074,7 +1235,8 @@ function formatTokensApprox(chars) {
|
|
|
1074
1235
|
return `${(tokens / 1000).toFixed(1)}k`;
|
|
1075
1236
|
return `${Math.round(tokens / 1000)}k`;
|
|
1076
1237
|
}
|
|
1077
|
-
function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars,
|
|
1238
|
+
function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars, nowTick, }) {
|
|
1239
|
+
void nowTick;
|
|
1078
1240
|
const theme = useTheme();
|
|
1079
1241
|
const [frameIndex, setFrameIndex] = useState(0);
|
|
1080
1242
|
const [idlePhrase, setIdlePhrase] = useState(() => GENERIC_PHRASES[0]);
|
|
@@ -1120,10 +1282,6 @@ function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, stre
|
|
|
1120
1282
|
else {
|
|
1121
1283
|
phrase = idlePhrase;
|
|
1122
1284
|
}
|
|
1123
|
-
const elapsedSec = runStartedAt
|
|
1124
|
-
? Math.max(0, Math.floor((nowTick - runStartedAt) / 1000))
|
|
1125
|
-
: 0;
|
|
1126
|
-
const elapsedText = elapsedSec > 0 ? `${elapsedSec}s` : "0s";
|
|
1127
1285
|
const tokenText = streamedChars > 0 ? `↓${formatTokensApprox(streamedChars)} tok` : "";
|
|
1128
|
-
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: ["(",
|
|
1286
|
+
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: ["(", tokenText ? `${tokenText} · ` : "", "esc\u00B7esc to interrupt)"] })] }));
|
|
1129
1287
|
}
|
|
@@ -71,9 +71,8 @@ export async function highlightCode(code, lang) {
|
|
|
71
71
|
const h = await getHighlighter();
|
|
72
72
|
return runHighlight(h, code, lang);
|
|
73
73
|
}
|
|
74
|
-
// Synchronous variant that returns null when shiki hasn't finished loading yet
|
|
75
|
-
//
|
|
76
|
-
// so the first frame can already carry highlighted output.
|
|
74
|
+
// Synchronous variant that returns null when shiki hasn't finished loading yet,
|
|
75
|
+
// so the first transcript frame can already carry highlighted output when warm.
|
|
77
76
|
export function highlightCodeSync(code, lang) {
|
|
78
77
|
if (!highlighterReady) {
|
|
79
78
|
// Ensure warmup is in flight for future renders.
|
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Detect whether the host terminal is using a light or dark background so we
|
|
3
|
-
* can pick a sensible default palette when the user has theme set to "auto".
|
|
4
|
-
*
|
|
5
|
-
* Resolution order:
|
|
6
|
-
* 1. `COLORFGBG` env var — synchronous, set by VTE-family terminals (GNOME
|
|
7
|
-
* Terminal, Konsole) and iTerm2 (when enabled). Format is "fg;bg" or
|
|
8
|
-
* "fg;aux;bg" with each value being an ANSI color index 0–15.
|
|
9
|
-
* 2. OSC 11 query — write `ESC ] 11 ; ? BEL`, listen on stdin for a reply
|
|
10
|
-
* shaped like `ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL`. Capped at ~150 ms so
|
|
11
|
-
* we don't stall startup on terminals that swallow the query.
|
|
12
|
-
* 3. Fallback to "dark" — most coding terminals are dark, so this is the
|
|
13
|
-
* least surprising default when detection fails.
|
|
14
|
-
*
|
|
15
|
-
* Must run BEFORE Ink's `render()` takes over stdin. Ink puts stdin into raw
|
|
16
|
-
* mode and consumes input itself, so the OSC 11 reply would never reach us.
|
|
17
|
-
*/
|
|
18
|
-
import type { ResolvedTheme } from "./theme.js";
|
|
1
|
+
export type ResolvedTheme = "light" | "dark";
|
|
19
2
|
export declare function detectTerminalTheme(timeoutMs?: number): Promise<ResolvedTheme>;
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detect whether the host terminal is using a light or dark background so we
|
|
3
|
-
* can pick a sensible default palette when the user has theme set to "auto".
|
|
4
|
-
*
|
|
5
|
-
* Resolution order:
|
|
6
|
-
* 1. `COLORFGBG` env var — synchronous, set by VTE-family terminals (GNOME
|
|
7
|
-
* Terminal, Konsole) and iTerm2 (when enabled). Format is "fg;bg" or
|
|
8
|
-
* "fg;aux;bg" with each value being an ANSI color index 0–15.
|
|
9
|
-
* 2. OSC 11 query — write `ESC ] 11 ; ? BEL`, listen on stdin for a reply
|
|
10
|
-
* shaped like `ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL`. Capped at ~150 ms so
|
|
11
|
-
* we don't stall startup on terminals that swallow the query.
|
|
12
|
-
* 3. Fallback to "dark" — most coding terminals are dark, so this is the
|
|
13
|
-
* least surprising default when detection fails.
|
|
14
|
-
*
|
|
15
|
-
* Must run BEFORE Ink's `render()` takes over stdin. Ink puts stdin into raw
|
|
16
|
-
* mode and consumes input itself, so the OSC 11 reply would never reach us.
|
|
17
|
-
*/
|
|
18
1
|
export async function detectTerminalTheme(timeoutMs = 150) {
|
|
19
2
|
const fromEnv = parseColorFgBg(process.env.COLORFGBG);
|
|
20
3
|
if (fromEnv)
|
|
@@ -26,16 +9,6 @@ export async function detectTerminalTheme(timeoutMs = 150) {
|
|
|
26
9
|
}
|
|
27
10
|
return "dark";
|
|
28
11
|
}
|
|
29
|
-
/**
|
|
30
|
-
* COLORFGBG examples:
|
|
31
|
-
* "15;0" → bright-white fg on black bg → dark
|
|
32
|
-
* "0;15" → black fg on bright-white bg → light
|
|
33
|
-
* "15;default;0" → some terminals add a default-bg sentinel in the middle.
|
|
34
|
-
*
|
|
35
|
-
* ANSI indices 0–6 are typically dark (black, red, green, yellow, blue,
|
|
36
|
-
* magenta, cyan); 7–15 are typically light (gray-to-white-ish). 7 itself
|
|
37
|
-
* (white) is ambiguous on some terminals but more often points to light.
|
|
38
|
-
*/
|
|
39
12
|
function parseColorFgBg(value) {
|
|
40
13
|
if (!value)
|
|
41
14
|
return null;
|
|
@@ -65,7 +38,7 @@ function queryOsc11(timeoutMs) {
|
|
|
65
38
|
stdin.setRawMode(originalRaw);
|
|
66
39
|
}
|
|
67
40
|
catch {
|
|
68
|
-
// ignore
|
|
41
|
+
// ignore - terminal may have already restored
|
|
69
42
|
}
|
|
70
43
|
stdin.pause();
|
|
71
44
|
};
|
|
@@ -79,8 +52,6 @@ function queryOsc11(timeoutMs) {
|
|
|
79
52
|
};
|
|
80
53
|
const onData = (chunk) => {
|
|
81
54
|
buffer += chunk.toString("utf8");
|
|
82
|
-
// Match `ESC ] 11 ; rgb:RRRR/GGGG/BBBB ST` where ST is BEL (\x07) or
|
|
83
|
-
// ESC \\. Some terminals reply with shorter hex (rgb:rr/gg/bb).
|
|
84
55
|
const match = buffer.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x07|\x1b\\)/);
|
|
85
56
|
if (!match)
|
|
86
57
|
return;
|
|
@@ -106,17 +77,10 @@ function queryOsc11(timeoutMs) {
|
|
|
106
77
|
}
|
|
107
78
|
});
|
|
108
79
|
}
|
|
109
|
-
/** Normalize a hex channel string of arbitrary length to a 0–1 float. */
|
|
110
80
|
function parseHexChannel(hex) {
|
|
111
81
|
const max = (1 << (hex.length * 4)) - 1;
|
|
112
82
|
return parseInt(hex, 16) / max;
|
|
113
83
|
}
|
|
114
|
-
/**
|
|
115
|
-
* sRGB relative luminance per WCAG 2.x. Output range is 0 (black) to 1 (white).
|
|
116
|
-
* We treat ≥ 0.5 as "light"; the actual threshold is forgiving because real
|
|
117
|
-
* terminal backgrounds tend to be near-pure black (≈0.0) or near-pure white
|
|
118
|
-
* (≈1.0).
|
|
119
|
-
*/
|
|
120
84
|
function relativeLuminance(r, g, b) {
|
|
121
85
|
const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
122
86
|
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import type { ToolResultMetadata } from "../types.js";
|
|
1
|
+
import type { Message, ToolResultMetadata } from "../types.js";
|
|
2
2
|
export interface DisplayMessage {
|
|
3
|
-
/** Stable identity, used as
|
|
3
|
+
/** Stable identity, used as the transcript list key. Generated by the UI layer. */
|
|
4
4
|
key?: string;
|
|
5
5
|
role: "user" | "assistant" | "error";
|
|
6
6
|
content: string;
|
|
7
7
|
reasoning?: string;
|
|
8
8
|
toolCalls?: DisplayToolCall[];
|
|
9
9
|
parts?: DisplayMessagePart[];
|
|
10
|
-
syntheticKind?: "ui_summary";
|
|
10
|
+
syntheticKind?: "ui_summary" | "ui_compact_summary";
|
|
11
|
+
/** Markdown body shown inside a `ui_compact_summary` card. */
|
|
12
|
+
compactionSummary?: string;
|
|
11
13
|
hiddenCount?: number;
|
|
14
|
+
taskElapsedMs?: number;
|
|
12
15
|
}
|
|
13
16
|
export type DisplayMessagePart = DisplayTextPart | DisplayToolsPart;
|
|
14
17
|
export interface DisplayTextPart {
|
|
@@ -23,6 +26,12 @@ export interface DisplayToolCall {
|
|
|
23
26
|
id: string;
|
|
24
27
|
name: string;
|
|
25
28
|
args: Record<string, any>;
|
|
29
|
+
/**
|
|
30
|
+
* Unparsed JSON string for tool arguments, populated during partial-streaming
|
|
31
|
+
* before `args` resolves. Used as a fallback by trace-groups when extracting
|
|
32
|
+
* a command preview.
|
|
33
|
+
*/
|
|
34
|
+
rawArguments?: string;
|
|
26
35
|
result?: string;
|
|
27
36
|
isError?: boolean;
|
|
28
37
|
metadata?: ToolResultMetadata;
|
|
@@ -36,3 +45,11 @@ export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): Displ
|
|
|
36
45
|
export declare function contentFromParts(parts: DisplayMessagePart[]): string;
|
|
37
46
|
export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
|
|
38
47
|
export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
|
|
48
|
+
/**
|
|
49
|
+
* Find the most recent compaction summary embedded in the agent's system
|
|
50
|
+
* messages. Bubble's compaction step rewrites the system transcript so that
|
|
51
|
+
* the long-form summary lives in either a "Previous conversation summary:"
|
|
52
|
+
* block or an "Earlier in this turn" block; we walk from newest to oldest and
|
|
53
|
+
* return the first match so the UI can show the freshest summary.
|
|
54
|
+
*/
|
|
55
|
+
export declare function latestCompactionSummary(agentMessages: Message[]): string | undefined;
|