@bubblebrain-ai/bubble 0.0.9 → 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.
Files changed (153) hide show
  1. package/dist/agent.d.ts +1 -0
  2. package/dist/agent.js +5 -0
  3. package/dist/cli.d.ts +10 -0
  4. package/dist/cli.js +31 -3
  5. package/dist/feedback/collect.d.ts +7 -0
  6. package/dist/feedback/collect.js +119 -0
  7. package/dist/feedback/config.d.ts +14 -0
  8. package/dist/feedback/config.js +16 -0
  9. package/dist/feedback/redact.d.ts +1 -0
  10. package/dist/feedback/redact.js +25 -0
  11. package/dist/feedback/submit.d.ts +6 -0
  12. package/dist/feedback/submit.js +43 -0
  13. package/dist/feedback/types.d.ts +22 -0
  14. package/dist/feishu/agent-host/approval-card.d.ts +11 -0
  15. package/dist/feishu/agent-host/approval-card.js +46 -0
  16. package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
  17. package/dist/feishu/agent-host/approval-ui.js +214 -0
  18. package/dist/feishu/agent-host/run-driver.d.ts +51 -0
  19. package/dist/feishu/agent-host/run-driver.js +295 -0
  20. package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
  21. package/dist/feishu/agent-host/runtime-deps.js +8 -0
  22. package/dist/feishu/card/budget.d.ts +40 -0
  23. package/dist/feishu/card/budget.js +134 -0
  24. package/dist/feishu/card/renderer.d.ts +29 -0
  25. package/dist/feishu/card/renderer.js +245 -0
  26. package/dist/feishu/card/run-state-types.d.ts +49 -0
  27. package/dist/feishu/card/run-state-types.js +15 -0
  28. package/dist/feishu/card/run-state.d.ts +21 -0
  29. package/dist/feishu/card/run-state.js +217 -0
  30. package/dist/feishu/channel/channel.d.ts +52 -0
  31. package/dist/feishu/channel/channel.js +74 -0
  32. package/dist/feishu/config.d.ts +24 -0
  33. package/dist/feishu/config.js +97 -0
  34. package/dist/feishu/format.d.ts +6 -0
  35. package/dist/feishu/format.js +14 -0
  36. package/dist/feishu/index.d.ts +4 -0
  37. package/dist/feishu/index.js +4 -0
  38. package/dist/feishu/logger.d.ts +31 -0
  39. package/dist/feishu/logger.js +62 -0
  40. package/dist/feishu/paths.d.ts +12 -0
  41. package/dist/feishu/paths.js +38 -0
  42. package/dist/feishu/process-registry.d.ts +29 -0
  43. package/dist/feishu/process-registry.js +90 -0
  44. package/dist/feishu/router/commands.d.ts +38 -0
  45. package/dist/feishu/router/commands.js +285 -0
  46. package/dist/feishu/router/event-router.d.ts +40 -0
  47. package/dist/feishu/router/event-router.js +208 -0
  48. package/dist/feishu/router/whitelist.d.ts +23 -0
  49. package/dist/feishu/router/whitelist.js +20 -0
  50. package/dist/feishu/runtime/active-runs.d.ts +32 -0
  51. package/dist/feishu/runtime/active-runs.js +84 -0
  52. package/dist/feishu/runtime/pending-queue.d.ts +36 -0
  53. package/dist/feishu/runtime/pending-queue.js +98 -0
  54. package/dist/feishu/runtime/process-pool.d.ts +29 -0
  55. package/dist/feishu/runtime/process-pool.js +49 -0
  56. package/dist/feishu/schema.d.ts +17 -0
  57. package/dist/feishu/schema.js +252 -0
  58. package/dist/feishu/scope/scope-registry.d.ts +39 -0
  59. package/dist/feishu/scope/scope-registry.js +148 -0
  60. package/dist/feishu/scope/session-binder.d.ts +44 -0
  61. package/dist/feishu/scope/session-binder.js +100 -0
  62. package/dist/feishu/scope/session-store.d.ts +24 -0
  63. package/dist/feishu/scope/session-store.js +73 -0
  64. package/dist/feishu/secrets.d.ts +37 -0
  65. package/dist/feishu/secrets.js +129 -0
  66. package/dist/feishu/serve.d.ts +12 -0
  67. package/dist/feishu/serve.js +288 -0
  68. package/dist/feishu/types.d.ts +75 -0
  69. package/dist/feishu/types.js +23 -0
  70. package/dist/feishu/wizard.d.ts +24 -0
  71. package/dist/feishu/wizard.js +121 -0
  72. package/dist/main.js +78 -29
  73. package/dist/model-catalog.js +3 -0
  74. package/dist/session.d.ts +11 -0
  75. package/dist/session.js +88 -2
  76. package/dist/slash-commands/commands.js +13 -0
  77. package/dist/slash-commands/feishu.d.ts +17 -0
  78. package/dist/slash-commands/feishu.js +400 -0
  79. package/dist/slash-commands/types.d.ts +3 -1
  80. package/dist/tui-ink/app.js +218 -60
  81. package/dist/tui-ink/code-highlight.js +2 -3
  82. package/dist/tui-ink/detect-theme.d.ts +1 -18
  83. package/dist/tui-ink/detect-theme.js +1 -37
  84. package/dist/tui-ink/display-history.d.ts +20 -3
  85. package/dist/tui-ink/display-history.js +26 -27
  86. package/dist/tui-ink/feedback-dialog.d.ts +19 -0
  87. package/dist/tui-ink/feedback-dialog.js +123 -0
  88. package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
  89. package/dist/tui-ink/feishu-setup-picker.js +261 -0
  90. package/dist/tui-ink/input-box.d.ts +3 -0
  91. package/dist/tui-ink/input-box.js +27 -0
  92. package/dist/tui-ink/input-history.js +3 -5
  93. package/dist/tui-ink/markdown.d.ts +32 -0
  94. package/dist/tui-ink/markdown.js +111 -4
  95. package/dist/tui-ink/message-list.d.ts +1 -6
  96. package/dist/tui-ink/message-list.js +85 -34
  97. package/dist/tui-ink/model-picker.js +1 -4
  98. package/dist/tui-ink/run-session-picker.d.ts +10 -0
  99. package/dist/tui-ink/run-session-picker.js +22 -0
  100. package/dist/tui-ink/run.js +7 -2
  101. package/dist/tui-ink/session-picker.d.ts +10 -0
  102. package/dist/tui-ink/session-picker.js +112 -0
  103. package/dist/tui-ink/terminal-mouse.d.ts +4 -0
  104. package/dist/tui-ink/terminal-mouse.js +23 -0
  105. package/dist/tui-ink/trace-groups.js +25 -2
  106. package/dist/tui-ink/welcome.js +2 -4
  107. package/package.json +4 -5
  108. package/dist/tui/clipboard.d.ts +0 -1
  109. package/dist/tui/clipboard.js +0 -53
  110. package/dist/tui/display-history.d.ts +0 -44
  111. package/dist/tui/display-history.js +0 -243
  112. package/dist/tui/escape-confirmation.d.ts +0 -15
  113. package/dist/tui/escape-confirmation.js +0 -30
  114. package/dist/tui/file-mentions.d.ts +0 -29
  115. package/dist/tui/file-mentions.js +0 -174
  116. package/dist/tui/global-key-router.d.ts +0 -3
  117. package/dist/tui/global-key-router.js +0 -87
  118. package/dist/tui/image-paste.d.ts +0 -95
  119. package/dist/tui/image-paste.js +0 -505
  120. package/dist/tui/markdown-inline.d.ts +0 -22
  121. package/dist/tui/markdown-inline.js +0 -68
  122. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  123. package/dist/tui/markdown-theme-rules.js +0 -164
  124. package/dist/tui/markdown-theme.d.ts +0 -5
  125. package/dist/tui/markdown-theme.js +0 -27
  126. package/dist/tui/opencode-spinner.d.ts +0 -21
  127. package/dist/tui/opencode-spinner.js +0 -216
  128. package/dist/tui/prompt-keybindings.d.ts +0 -42
  129. package/dist/tui/prompt-keybindings.js +0 -35
  130. package/dist/tui/recent-activity.d.ts +0 -8
  131. package/dist/tui/recent-activity.js +0 -71
  132. package/dist/tui/render-signature.d.ts +0 -1
  133. package/dist/tui/render-signature.js +0 -7
  134. package/dist/tui/run.d.ts +0 -38
  135. package/dist/tui/run.js +0 -6996
  136. package/dist/tui/sidebar-mcp.d.ts +0 -31
  137. package/dist/tui/sidebar-mcp.js +0 -62
  138. package/dist/tui/sidebar-state.d.ts +0 -12
  139. package/dist/tui/sidebar-state.js +0 -69
  140. package/dist/tui/streaming-tool-args.d.ts +0 -15
  141. package/dist/tui/streaming-tool-args.js +0 -30
  142. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  143. package/dist/tui/tool-renderers/fallback.js +0 -75
  144. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  145. package/dist/tui/tool-renderers/registry.js +0 -11
  146. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  147. package/dist/tui/tool-renderers/subagent.js +0 -114
  148. package/dist/tui/tool-renderers/types.d.ts +0 -36
  149. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  150. package/dist/tui/tool-renderers/write-preview.js +0 -30
  151. package/dist/tui/tool-renderers/write.d.ts +0 -6
  152. package/dist/tui/tool-renderers/write.js +0 -88
  153. /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
@@ -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
- // When the terminal width changes mid-session (e.g. the user toggles an IDE
214
- // side-panel), every full-width ANSI bg run already written into scrollback
215
- // by <Static> stays at the old width. The terminal then wraps those rows on
216
- // the new width and leaves residual coloured stripes underneath. Ink can't
217
- // reach scrollback to repaint. So on width change, we wipe screen +
218
- // scrollback and bump `clearEpoch` so <Static> remounts and replays every
219
- // committed message at the new width. Cost: a single flicker per resize and
220
- // any pre-session shell scrollback is also cleared. Skip the initial mount.
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 (previousColumnsRef.current === null) {
224
- previousColumnsRef.current = terminalColumns;
297
+ if (previousTerminalColumnsRef.current === null) {
298
+ previousTerminalColumnsRef.current = terminalColumns;
225
299
  return;
226
300
  }
227
- if (previousColumnsRef.current === terminalColumns)
301
+ if (previousTerminalColumnsRef.current === terminalColumns)
228
302
  return;
229
- previousColumnsRef.current = terminalColumns;
230
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
231
- setClearEpoch((n) => n + 1);
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 used to refresh elapsed counters on in-progress tool rows and
242
- // on the WaitingIndicator. Only ticks while the agent is running so we
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 drives elapsed display
246
- // on the WaitingIndicator.
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 (pendingPlan || pendingApproval || pendingQuestion)
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
- setMessages([]);
433
- // Ink's <Static> writes items into terminal scrollback and never removes
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
- setClearEpoch((n) => n + 1);
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
- setStreamingContent(assistantContent);
687
- syncStreamingParts();
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
- addMessage("assistant", result);
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", padding: 1, 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
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) }))] }), 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) => {
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
- } }) })), !isExiting && isRunning && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && (_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, runStartedAt: runStartRef.current ?? undefined, nowTick: nowTick }) })), !isExiting && !pickerMode && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, children: _jsx(InputBox, { onSubmit: handleSubmit, disabled: isRunning || !!pendingPlan || !!pendingApproval || !!pendingQuestion, skillRegistry: safeSkillRegistry, terminalColumns: terminalColumns, cwd: args.cwd }) })), !isExiting && (_jsx(FooterBar, { data: buildFooterData({
1004
- cwd: args.cwd,
1005
- providerId: agent.providerId || safeRegistry.getDefault()?.id || "unknown",
1006
- model: displayModel(agent.model) || "no model",
1007
- thinkingLevel,
1008
- showThinking: getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2,
1009
- mode: permissionMode,
1010
- usageTotals,
1011
- verboseTrace,
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, runStartedAt, nowTick, }) {
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: ["(", elapsedText, tokenText ? ` · ${tokenText}` : "", " \u00B7 esc\u00B7esc to interrupt)"] })] }));
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
- // Used by code paths that render into Ink's <Static> (which only paints once)
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 terminal may have already restored
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 Static/list key. Generated by the UI layer. */
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;