@runtypelabs/persona 3.9.2 → 3.10.0

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 (38) hide show
  1. package/dist/index.cjs +46 -43
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +119 -0
  4. package/dist/index.d.ts +119 -0
  5. package/dist/index.global.js +67 -64
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +46 -43
  8. package/dist/index.js.map +1 -1
  9. package/dist/theme-editor.cjs +826 -210
  10. package/dist/theme-editor.d.cts +128 -3
  11. package/dist/theme-editor.d.ts +128 -3
  12. package/dist/theme-editor.js +822 -210
  13. package/dist/theme-reference.cjs +1 -1
  14. package/dist/theme-reference.d.cts +8 -0
  15. package/dist/theme-reference.d.ts +8 -0
  16. package/dist/theme-reference.js +1 -1
  17. package/dist/widget.css +124 -0
  18. package/package.json +1 -1
  19. package/src/client.test.ts +312 -1
  20. package/src/client.ts +247 -24
  21. package/src/components/messages.ts +1 -1
  22. package/src/components/reasoning-bubble.ts +117 -28
  23. package/src/components/tool-bubble.ts +161 -27
  24. package/src/defaults.ts +12 -0
  25. package/src/styles/widget.css +124 -0
  26. package/src/theme-editor/index.ts +5 -0
  27. package/src/theme-editor/preview-utils.test.ts +58 -0
  28. package/src/theme-editor/preview-utils.ts +220 -4
  29. package/src/theme-editor/sections.test.ts +20 -0
  30. package/src/theme-editor/sections.ts +10 -0
  31. package/src/theme-reference.ts +8 -3
  32. package/src/tool-call-display-defaults.test.ts +23 -0
  33. package/src/types.ts +126 -0
  34. package/src/ui.scroll.test.ts +104 -0
  35. package/src/ui.tool-display.test.ts +204 -0
  36. package/src/ui.ts +103 -3
  37. package/src/utils/message-fingerprint.test.ts +17 -0
  38. package/src/utils/message-fingerprint.ts +13 -1
@@ -197,7 +197,195 @@ export function buildSrcdoc(
197
197
 
198
198
  export type PreviewScene = 'home' | 'conversation' | 'minimized' | 'artifact';
199
199
 
200
- export function createPreviewMessages(scene: PreviewScene): AgentWidgetMessage[] {
200
+ export type PreviewTranscriptEntryPreset =
201
+ | 'user-message'
202
+ | 'assistant-message'
203
+ | 'reasoning-streaming'
204
+ | 'reasoning-complete'
205
+ | 'tool-running'
206
+ | 'tool-complete';
207
+
208
+ const PREVIEW_TRANSCRIPT_PRESET_LABELS: Record<PreviewTranscriptEntryPreset, string> = {
209
+ 'user-message': 'User message',
210
+ 'assistant-message': 'Assistant message',
211
+ 'reasoning-streaming': 'Reasoning (streaming)',
212
+ 'reasoning-complete': 'Reasoning (complete)',
213
+ 'tool-running': 'Tool call (running)',
214
+ 'tool-complete': 'Tool call (complete)',
215
+ };
216
+
217
+ export function getPreviewTranscriptPresetLabel(preset: PreviewTranscriptEntryPreset): string {
218
+ return PREVIEW_TRANSCRIPT_PRESET_LABELS[preset];
219
+ }
220
+
221
+ export function createPreviewTranscriptEntry(
222
+ preset: PreviewTranscriptEntryPreset,
223
+ index = 0
224
+ ): AgentWidgetMessage {
225
+ const createdAt = new Date(Date.now() - Math.max(0, 60 - index) * 1000).toISOString();
226
+ const suffix = `${preset}-${index}`;
227
+
228
+ switch (preset) {
229
+ case 'user-message':
230
+ return {
231
+ id: `preview-seq-user-${suffix}`,
232
+ role: 'user',
233
+ content: 'Can you continue with the next step?',
234
+ createdAt,
235
+ };
236
+ case 'assistant-message':
237
+ return {
238
+ id: `preview-seq-assistant-${suffix}`,
239
+ role: 'assistant',
240
+ content: 'Absolutely. I can keep going and explain what happens next.',
241
+ createdAt,
242
+ };
243
+ case 'reasoning-streaming':
244
+ return {
245
+ id: `preview-seq-reasoning-stream-${suffix}`,
246
+ role: 'assistant',
247
+ content: '',
248
+ createdAt,
249
+ streaming: true,
250
+ variant: 'reasoning',
251
+ reasoning: {
252
+ id: `preview-reasoning-stream-${suffix}`,
253
+ status: 'streaming',
254
+ chunks: ['Thinking through the next step in the workflow...'],
255
+ },
256
+ };
257
+ case 'reasoning-complete':
258
+ return {
259
+ id: `preview-seq-reasoning-complete-${suffix}`,
260
+ role: 'assistant',
261
+ content: '',
262
+ createdAt,
263
+ streaming: false,
264
+ variant: 'reasoning',
265
+ reasoning: {
266
+ id: `preview-reasoning-complete-${suffix}`,
267
+ status: 'complete',
268
+ chunks: ['Reviewed the requirements and finalized the reasoning output.'],
269
+ durationMs: 1200,
270
+ },
271
+ };
272
+ case 'tool-complete':
273
+ return {
274
+ id: `preview-seq-tool-complete-${suffix}`,
275
+ role: 'assistant',
276
+ content: '',
277
+ createdAt,
278
+ streaming: false,
279
+ variant: 'tool',
280
+ toolCall: {
281
+ id: `preview-tool-complete-${suffix}`,
282
+ name: 'Create build instructions',
283
+ status: 'complete',
284
+ chunks: ['Prepared the build instructions and validated the inputs.'],
285
+ result: { ok: true },
286
+ duration: 420,
287
+ },
288
+ };
289
+ case 'tool-running':
290
+ default:
291
+ return {
292
+ id: `preview-seq-tool-running-${suffix}`,
293
+ role: 'assistant',
294
+ content: '',
295
+ createdAt,
296
+ streaming: true,
297
+ variant: 'tool',
298
+ toolCall: {
299
+ id: `preview-tool-running-${suffix}`,
300
+ name: 'Get platform documentation',
301
+ status: 'running',
302
+ chunks: ['Fetching the relevant platform documentation...'],
303
+ },
304
+ };
305
+ }
306
+ }
307
+
308
+ export function appendPreviewTranscriptEntry(
309
+ messages: AgentWidgetMessage[],
310
+ preset: PreviewTranscriptEntryPreset
311
+ ): AgentWidgetMessage[] {
312
+ return [...messages, createPreviewTranscriptEntry(preset, messages.length)];
313
+ }
314
+
315
+ const createAdvancedTranscriptPreviewMessages = (): AgentWidgetMessage[] => [
316
+ {
317
+ id: "preview-adv-1",
318
+ role: "user",
319
+ content: "Can you create the product and gather the docs?",
320
+ createdAt: new Date(Date.now() - 180000).toISOString(),
321
+ },
322
+ {
323
+ id: "preview-adv-2",
324
+ role: "assistant",
325
+ content: "",
326
+ createdAt: new Date(Date.now() - 150000).toISOString(),
327
+ streaming: true,
328
+ variant: "reasoning",
329
+ reasoning: {
330
+ id: "preview-reasoning",
331
+ status: "streaming",
332
+ chunks: [
333
+ "Now let me get the Persona embed documentation and builtin tools catalog.",
334
+ ],
335
+ },
336
+ },
337
+ {
338
+ id: "preview-adv-3",
339
+ role: "assistant",
340
+ content: "",
341
+ createdAt: new Date(Date.now() - 120000).toISOString(),
342
+ streaming: true,
343
+ variant: "tool",
344
+ toolCall: {
345
+ id: "preview-tool-1",
346
+ name: "Load tools",
347
+ status: "running",
348
+ chunks: ["Loaded tools, used Runtype integration"],
349
+ },
350
+ },
351
+ {
352
+ id: "preview-adv-4",
353
+ role: "assistant",
354
+ content: "",
355
+ createdAt: new Date(Date.now() - 90000).toISOString(),
356
+ streaming: true,
357
+ variant: "tool",
358
+ toolCall: {
359
+ id: "preview-tool-2",
360
+ name: "Get platform documentation",
361
+ status: "running",
362
+ chunks: ["Get platform documentation"],
363
+ },
364
+ },
365
+ {
366
+ id: "preview-adv-5",
367
+ role: "assistant",
368
+ content: "I loaded the tools and fetched the docs. Next I can assemble the product details.",
369
+ createdAt: new Date(Date.now() - 30000).toISOString(),
370
+ },
371
+ ];
372
+
373
+ const shouldSeedAdvancedTranscriptPreview = (
374
+ config?: Partial<AgentWidgetConfig>
375
+ ): boolean =>
376
+ Boolean(
377
+ config?.features?.toolCallDisplay?.activePreview ||
378
+ config?.features?.toolCallDisplay?.grouped ||
379
+ (config?.features?.toolCallDisplay?.collapsedMode &&
380
+ config.features.toolCallDisplay.collapsedMode !== "tool-call") ||
381
+ config?.features?.reasoningDisplay?.activePreview
382
+ );
383
+
384
+ export function createPreviewMessages(
385
+ scene: PreviewScene,
386
+ config?: Partial<AgentWidgetConfig>,
387
+ appendedMessages: AgentWidgetMessage[] = []
388
+ ): AgentWidgetMessage[] {
201
389
  if (scene === 'home') {
202
390
  return [{ id: 'preview-home-1', role: 'assistant', content: 'Hi there! How can we help today?', createdAt: new Date().toISOString() }];
203
391
  }
@@ -210,22 +398,30 @@ export function createPreviewMessages(scene: PreviewScene): AgentWidgetMessage[]
210
398
  { id: 'preview-art-2', role: 'assistant', content: 'Here\u2019s a project overview document for you.', createdAt: new Date(Date.now() - 60000).toISOString() },
211
399
  ];
212
400
  }
401
+ if (scene === 'conversation' && shouldSeedAdvancedTranscriptPreview(config)) {
402
+ return [...createAdvancedTranscriptPreviewMessages(), ...appendedMessages];
403
+ }
213
404
  return [
214
405
  { id: 'preview-conv-1', role: 'assistant', content: 'Hello! How can I help you today?', createdAt: new Date(Date.now() - 180000).toISOString() },
215
406
  { id: 'preview-conv-2', role: 'user', content: 'I want to customize the theme editor preview.', createdAt: new Date(Date.now() - 120000).toISOString() },
216
407
  { id: 'preview-conv-3', role: 'assistant', content: 'Absolutely. Check out the [getting started guide](https://example.com) to see what\u2019s possible, then adjust colors and tokens to match your brand.', createdAt: new Date(Date.now() - 60000).toISOString() },
408
+ ...appendedMessages,
217
409
  ];
218
410
  }
219
411
 
220
412
  // ─── Scene Config ───────────────────────────────────────────────
221
413
 
222
- export function applySceneConfig(base: AgentWidgetConfig, scene: PreviewScene): AgentWidgetConfig {
414
+ export function applySceneConfig(
415
+ base: AgentWidgetConfig,
416
+ scene: PreviewScene,
417
+ appendedMessages: AgentWidgetMessage[] = []
418
+ ): AgentWidgetConfig {
223
419
  const launcher = { ...base.launcher, enabled: true, autoExpand: scene !== 'minimized' };
224
420
  const config = {
225
421
  ...base,
226
422
  launcher,
227
423
  suggestionChips: scene === 'home' ? (base.suggestionChips?.length ? base.suggestionChips : HOME_SUGGESTION_CHIPS) : base.suggestionChips,
228
- initialMessages: createPreviewMessages(scene),
424
+ initialMessages: createPreviewMessages(scene, base, appendedMessages),
229
425
  storageAdapter: PREVIEW_STORAGE_ADAPTER,
230
426
  } as AgentWidgetConfig;
231
427
 
@@ -244,6 +440,7 @@ export interface PreviewConfigOptions {
244
440
  theme?: DeepPartial<PersonaTheme>;
245
441
  darkTheme?: DeepPartial<PersonaTheme>;
246
442
  scene?: PreviewScene;
443
+ appendedMessages?: AgentWidgetMessage[];
247
444
  }
248
445
 
249
446
  export function buildPreviewConfig(
@@ -261,5 +458,24 @@ export function buildPreviewConfig(
261
458
  colorScheme: shellModeOverride ?? (options.config?.colorScheme as string) ?? 'light',
262
459
  } as AgentWidgetConfig;
263
460
 
264
- return applySceneConfig(base, scene);
461
+ return applySceneConfig(base, scene, options.appendedMessages ?? []);
462
+ }
463
+
464
+ export function buildPreviewConfigWithMessages(
465
+ options: PreviewConfigOptions,
466
+ messages: AgentWidgetMessage[],
467
+ shellModeOverride?: 'light' | 'dark'
468
+ ): AgentWidgetConfig {
469
+ const theme = options.theme ? createTheme(options.theme, { validate: false }) : createTheme();
470
+ const scene = options.scene ?? 'conversation';
471
+
472
+ const base = {
473
+ ...DEFAULT_WIDGET_CONFIG,
474
+ ...options.config,
475
+ theme,
476
+ darkTheme: options.darkTheme,
477
+ colorScheme: shellModeOverride ?? (options.config?.colorScheme as string) ?? 'light',
478
+ } as AgentWidgetConfig;
479
+
480
+ return applySceneConfig(base, scene, messages);
265
481
  }
@@ -40,4 +40,24 @@ describe("theme editor scroll-to-bottom controls", () => {
40
40
  );
41
41
  expect(INTERFACE_ROLES_SECTION.fields.some((field) => field.id === "role-scroll-to-bottom")).toBe(true);
42
42
  });
43
+
44
+ it("exposes grouped and collapsed tool call preview controls", () => {
45
+ const debugSection = CONFIGURE_SECTIONS.find((section) => section.id === "debug-inspection");
46
+
47
+ expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.collapsedMode")).toBe(true);
48
+ expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.activePreview")).toBe(true);
49
+ expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.previewMaxLines")).toBe(true);
50
+ expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.activeMinHeight")).toBe(true);
51
+ expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.expandable")).toBe(true);
52
+ expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.grouped")).toBe(true);
53
+ });
54
+
55
+ it("exposes collapsed reasoning preview controls", () => {
56
+ const debugSection = CONFIGURE_SECTIONS.find((section) => section.id === "debug-inspection");
57
+
58
+ expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.expandable")).toBe(true);
59
+ expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.activePreview")).toBe(true);
60
+ expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.previewMaxLines")).toBe(true);
61
+ expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.activeMinHeight")).toBe(true);
62
+ });
43
63
  });
@@ -743,6 +743,16 @@ const debugSectionDef: SectionDef = {
743
743
  fields: [
744
744
  { id: 'dev-reasoning', label: 'Show Reasoning', description: 'Display AI reasoning steps', type: 'toggle', path: 'features.showReasoning', defaultValue: false },
745
745
  { id: 'dev-tool-calls', label: 'Show Tool Calls', description: 'Display tool call details', type: 'toggle', path: 'features.showToolCalls', defaultValue: false },
746
+ { id: 'dev-tool-collapsed-mode', label: 'Tool Call Summary', description: 'Choose what collapsed tool rows show by default', type: 'select', path: 'features.toolCallDisplay.collapsedMode', defaultValue: 'tool-call', options: [{ value: 'tool-call', label: 'Tool Call' }, { value: 'tool-name', label: 'Tool Name' }, { value: 'tool-preview', label: 'Tool Preview' }] },
747
+ { id: 'dev-tool-active-preview', label: 'Tool Preview While Active', description: 'Show a lightweight preview in collapsed active tool rows', type: 'toggle', path: 'features.toolCallDisplay.activePreview', defaultValue: false },
748
+ { id: 'dev-tool-preview-lines', label: 'Tool Preview Lines', type: 'select', path: 'features.toolCallDisplay.previewMaxLines', defaultValue: 3, options: [{ value: '1', label: '1' }, { value: '2', label: '2' }, { value: '3', label: '3' }, { value: '4', label: '4' }, { value: '5', label: '5' }], formatValue: (v: unknown) => String(v ?? 3), parseValue: (v: unknown) => Number(v) },
749
+ { id: 'dev-tool-active-min-height', label: 'Tool Active Min Height', description: 'CSS min-height for collapsed active tool rows (e.g. 5rem)', type: 'text', path: 'features.toolCallDisplay.activeMinHeight', defaultValue: '' },
750
+ { id: 'dev-tool-expandable', label: 'Tool Calls Expandable', description: 'Allow expanding tool call rows to see full details', type: 'toggle', path: 'features.toolCallDisplay.expandable', defaultValue: true },
751
+ { id: 'dev-tool-grouped', label: 'Group Sequential Tool Calls', description: 'Render consecutive tool rows inside a grouped container', type: 'toggle', path: 'features.toolCallDisplay.grouped', defaultValue: false },
752
+ { id: 'dev-reasoning-expandable', label: 'Reasoning Expandable', description: 'Allow expanding reasoning rows to see full details', type: 'toggle', path: 'features.reasoningDisplay.expandable', defaultValue: true },
753
+ { id: 'dev-reasoning-active-preview', label: 'Reasoning Preview While Active', description: 'Show a lightweight preview in collapsed active reasoning rows', type: 'toggle', path: 'features.reasoningDisplay.activePreview', defaultValue: false },
754
+ { id: 'dev-reasoning-preview-lines', label: 'Reasoning Preview Lines', type: 'select', path: 'features.reasoningDisplay.previewMaxLines', defaultValue: 3, options: [{ value: '1', label: '1' }, { value: '2', label: '2' }, { value: '3', label: '3' }, { value: '4', label: '4' }, { value: '5', label: '5' }], formatValue: (v: unknown) => String(v ?? 3), parseValue: (v: unknown) => Number(v) },
755
+ { id: 'dev-reasoning-active-min-height', label: 'Reasoning Active Min Height', description: 'CSS min-height for collapsed active reasoning rows (e.g. 5rem)', type: 'text', path: 'features.reasoningDisplay.activeMinHeight', defaultValue: '' },
746
756
  { id: 'dev-debug', label: 'Debug Mode', description: 'Show debug information', type: 'toggle', path: 'debug', defaultValue: false },
747
757
  ],
748
758
  };
@@ -210,9 +210,14 @@ export const THEME_TOKEN_DOCS = {
210
210
  'features.scrollToBottom.enabled, features.scrollToBottom.iconName, features.scrollToBottom.label (empty string renders icon-only). Defaults: enabled=true, iconName="arrow-down", label="".',
211
211
  },
212
212
  toolCall: {
213
- description: 'Tool call display styling.',
213
+ description: 'Tool call display styling and collapsed/grouped rendering hooks.',
214
214
  properties:
215
- 'shadow, backgroundColor, borderColor, borderWidth, borderRadius, headerBackgroundColor, headerTextColor, headerPaddingX, headerPaddingY, contentBackgroundColor, contentTextColor, contentPaddingX, contentPaddingY, codeBlockBackgroundColor, codeBlockBorderColor, codeBlockTextColor, toggleTextColor, labelTextColor.',
215
+ 'shadow, backgroundColor, borderColor, borderWidth, borderRadius, headerBackgroundColor, headerTextColor, headerPaddingX, headerPaddingY, contentBackgroundColor, contentTextColor, contentPaddingX, contentPaddingY, codeBlockBackgroundColor, codeBlockBorderColor, codeBlockTextColor, toggleTextColor, labelTextColor, renderCollapsedSummary, renderCollapsedPreview, renderGroupedSummary.',
216
+ },
217
+ reasoning: {
218
+ description: 'Reasoning/thinking row rendering hooks.',
219
+ properties:
220
+ 'renderCollapsedSummary, renderCollapsedPreview.',
216
221
  },
217
222
  approval: {
218
223
  description:
@@ -277,7 +282,7 @@ export const THEME_TOKEN_DOCS = {
277
282
  features: {
278
283
  description: 'Feature flags.',
279
284
  properties:
280
- 'showReasoning (AI thinking steps), showToolCalls (tool invocations), artifacts (sidebar config).',
285
+ 'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines), artifacts (sidebar config).',
281
286
  },
282
287
  },
283
288
  }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { DEFAULT_WIDGET_CONFIG } from "./defaults";
4
+
5
+ describe("tool call display defaults", () => {
6
+ it("keeps advanced tool call transcript modes disabled by default", () => {
7
+ expect(DEFAULT_WIDGET_CONFIG.features?.toolCallDisplay).toEqual({
8
+ collapsedMode: "tool-call",
9
+ activePreview: false,
10
+ grouped: false,
11
+ previewMaxLines: 3,
12
+ expandable: true,
13
+ });
14
+ });
15
+
16
+ it("keeps advanced reasoning transcript modes disabled by default", () => {
17
+ expect(DEFAULT_WIDGET_CONFIG.features?.reasoningDisplay).toEqual({
18
+ activePreview: false,
19
+ previewMaxLines: 3,
20
+ expandable: true,
21
+ });
22
+ });
23
+ });
package/src/types.ts CHANGED
@@ -572,12 +572,77 @@ export type AgentWidgetScrollToBottomFeature = {
572
572
  label?: string;
573
573
  };
574
574
 
575
+ export type AgentWidgetToolCallCollapsedMode =
576
+ | "tool-call"
577
+ | "tool-name"
578
+ | "tool-preview";
579
+
580
+ export type AgentWidgetToolCallDisplayFeature = {
581
+ /**
582
+ * Controls what collapsed tool call rows show in their header/summary area.
583
+ * @default "tool-call"
584
+ */
585
+ collapsedMode?: AgentWidgetToolCallCollapsedMode;
586
+ /**
587
+ * When true, active collapsed tool calls can render a lightweight preview block.
588
+ * @default false
589
+ */
590
+ activePreview?: boolean;
591
+ /**
592
+ * Optional CSS min-height applied to active collapsed tool call rows.
593
+ */
594
+ activeMinHeight?: string;
595
+ /**
596
+ * Maximum preview lines shown for collapsed active tool calls.
597
+ * @default 3
598
+ */
599
+ previewMaxLines?: number;
600
+ /**
601
+ * When true, consecutive tool call rows can be visually grouped.
602
+ * @default false
603
+ */
604
+ grouped?: boolean;
605
+ /**
606
+ * When false, tool call bubbles show only the collapsed summary with no
607
+ * expand/collapse toggle. Users see tool awareness without full details.
608
+ * @default true
609
+ */
610
+ expandable?: boolean;
611
+ };
612
+
613
+ export type AgentWidgetReasoningDisplayFeature = {
614
+ /**
615
+ * When true, active collapsed reasoning rows can render a lightweight preview block.
616
+ * @default false
617
+ */
618
+ activePreview?: boolean;
619
+ /**
620
+ * Optional CSS min-height applied to active collapsed reasoning rows.
621
+ */
622
+ activeMinHeight?: string;
623
+ /**
624
+ * Maximum preview lines shown for collapsed active reasoning rows.
625
+ * @default 3
626
+ */
627
+ previewMaxLines?: number;
628
+ /**
629
+ * When false, reasoning bubbles show only the collapsed summary with no
630
+ * expand/collapse toggle. Users see reasoning awareness without full details.
631
+ * @default true
632
+ */
633
+ expandable?: boolean;
634
+ };
635
+
575
636
  export type AgentWidgetFeatureFlags = {
576
637
  showReasoning?: boolean;
577
638
  showToolCalls?: boolean;
578
639
  showEventStreamToggle?: boolean;
579
640
  /** Shared transcript + event stream scroll-to-bottom affordance. */
580
641
  scrollToBottom?: AgentWidgetScrollToBottomFeature;
642
+ /** Collapsed transcript behavior for tool call rows. */
643
+ toolCallDisplay?: AgentWidgetToolCallDisplayFeature;
644
+ /** Collapsed transcript behavior for reasoning rows. */
645
+ reasoningDisplay?: AgentWidgetReasoningDisplayFeature;
581
646
  /** Configuration for the Event Stream inspector view */
582
647
  eventStream?: EventStreamConfig;
583
648
  /** Optional artifact sidebar (split pane / mobile drawer) */
@@ -1186,6 +1251,66 @@ export type AgentWidgetToolCallConfig = {
1186
1251
  codeBlockTextColor?: string;
1187
1252
  toggleTextColor?: string;
1188
1253
  labelTextColor?: string;
1254
+ /**
1255
+ * Override the collapsed summary row content for a tool call bubble.
1256
+ * Return `null` to fall back to the built-in summary for the active display mode.
1257
+ */
1258
+ renderCollapsedSummary?: (context: {
1259
+ message: AgentWidgetMessage;
1260
+ toolCall: AgentWidgetToolCall;
1261
+ defaultSummary: string;
1262
+ previewText: string;
1263
+ collapsedMode: AgentWidgetToolCallCollapsedMode;
1264
+ isActive: boolean;
1265
+ config: AgentWidgetConfig;
1266
+ }) => HTMLElement | string | null;
1267
+ /**
1268
+ * Override the lightweight collapsed preview content shown for active tool rows.
1269
+ * Return `null` to fall back to the built-in preview text.
1270
+ */
1271
+ renderCollapsedPreview?: (context: {
1272
+ message: AgentWidgetMessage;
1273
+ toolCall: AgentWidgetToolCall;
1274
+ defaultPreview: string;
1275
+ isActive: boolean;
1276
+ config: AgentWidgetConfig;
1277
+ }) => HTMLElement | string | null;
1278
+ /**
1279
+ * Override the summary content for grouped consecutive tool-call containers.
1280
+ * Return `null` to fall back to the built-in `Called [x] tools` summary.
1281
+ */
1282
+ renderGroupedSummary?: (context: {
1283
+ messages: AgentWidgetMessage[];
1284
+ toolCalls: AgentWidgetToolCall[];
1285
+ defaultSummary: string;
1286
+ config: AgentWidgetConfig;
1287
+ }) => HTMLElement | string | null;
1288
+ };
1289
+
1290
+ export type AgentWidgetReasoningConfig = {
1291
+ /**
1292
+ * Override the collapsed summary row content for a reasoning bubble.
1293
+ * Return `null` to fall back to the built-in summary.
1294
+ */
1295
+ renderCollapsedSummary?: (context: {
1296
+ message: AgentWidgetMessage;
1297
+ reasoning: AgentWidgetReasoning;
1298
+ defaultSummary: string;
1299
+ previewText: string;
1300
+ isActive: boolean;
1301
+ config: AgentWidgetConfig;
1302
+ }) => HTMLElement | string | null;
1303
+ /**
1304
+ * Override the lightweight collapsed preview content shown for active reasoning rows.
1305
+ * Return `null` to fall back to the built-in preview text.
1306
+ */
1307
+ renderCollapsedPreview?: (context: {
1308
+ message: AgentWidgetMessage;
1309
+ reasoning: AgentWidgetReasoning;
1310
+ defaultPreview: string;
1311
+ isActive: boolean;
1312
+ config: AgentWidgetConfig;
1313
+ }) => HTMLElement | string | null;
1189
1314
  };
1190
1315
 
1191
1316
  export type AgentWidgetSuggestionChipsConfig = {
@@ -2358,6 +2483,7 @@ export type AgentWidgetConfig = {
2358
2483
  */
2359
2484
  textToSpeech?: TextToSpeechConfig;
2360
2485
  toolCall?: AgentWidgetToolCallConfig;
2486
+ reasoning?: AgentWidgetReasoningConfig;
2361
2487
  /**
2362
2488
  * Configuration for tool approval bubbles.
2363
2489
  * Set to `false` to disable built-in approval handling entirely.
@@ -135,6 +135,36 @@ const emitReasoningMessage = (
135
135
  });
136
136
  };
137
137
 
138
+ const emitToolMessage = (
139
+ controller: ReturnType<typeof createAgentExperience>,
140
+ {
141
+ id = STREAM_MESSAGE_ID,
142
+ status = "running",
143
+ chunks,
144
+ }: {
145
+ id?: string;
146
+ status?: "pending" | "running" | "complete";
147
+ chunks: string[];
148
+ }
149
+ ) => {
150
+ controller.injectTestMessage({
151
+ type: "message",
152
+ message: {
153
+ id,
154
+ role: "assistant",
155
+ content: "",
156
+ createdAt: STREAM_CREATED_AT,
157
+ streaming: status !== "complete",
158
+ variant: "tool",
159
+ toolCall: {
160
+ id,
161
+ status,
162
+ chunks,
163
+ }
164
+ }
165
+ });
166
+ };
167
+
138
168
  const createCustomComposer = () => {
139
169
  const footer = document.createElement("div");
140
170
  footer.className = "persona-widget-footer";
@@ -368,6 +398,80 @@ describe("createAgentExperience streaming scroll", () => {
368
398
  controller.destroy();
369
399
  });
370
400
 
401
+ it("keeps following collapsed tool preview updates while active", () => {
402
+ const raf = installRafMock();
403
+ const mount = createMount();
404
+ const controller = createAgentExperience(mount, {
405
+ apiUrl: "https://api.example.com/chat",
406
+ launcher: { enabled: false },
407
+ features: {
408
+ toolCallDisplay: {
409
+ activePreview: true,
410
+ },
411
+ },
412
+ } as any);
413
+
414
+ const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
415
+ expect(scrollContainer).not.toBeNull();
416
+
417
+ const metrics = installScrollMetrics(scrollContainer!, {
418
+ scrollHeight: 980,
419
+ clientHeight: 400
420
+ });
421
+
422
+ emitStreamingStatus(controller);
423
+ emitToolMessage(controller, { chunks: ["Loaded tools"] });
424
+ raf.flush();
425
+
426
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
427
+
428
+ metrics.setScrollHeight(1045);
429
+ emitToolMessage(controller, {
430
+ chunks: ["Loaded tools", "\nFetched platform documentation"]
431
+ });
432
+ raf.flush();
433
+
434
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
435
+
436
+ controller.destroy();
437
+ });
438
+
439
+ it("keeps following grouped tool sequences as new tool rows arrive", () => {
440
+ const raf = installRafMock();
441
+ const mount = createMount();
442
+ const controller = createAgentExperience(mount, {
443
+ apiUrl: "https://api.example.com/chat",
444
+ launcher: { enabled: false },
445
+ features: {
446
+ toolCallDisplay: {
447
+ grouped: true,
448
+ },
449
+ },
450
+ } as any);
451
+
452
+ const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
453
+ expect(scrollContainer).not.toBeNull();
454
+
455
+ const metrics = installScrollMetrics(scrollContainer!, {
456
+ scrollHeight: 960,
457
+ clientHeight: 400
458
+ });
459
+
460
+ emitStreamingStatus(controller);
461
+ emitToolMessage(controller, { id: "tool-1", chunks: ["Loaded tools"] });
462
+ raf.flush();
463
+
464
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
465
+
466
+ metrics.setScrollHeight(1030);
467
+ emitToolMessage(controller, { id: "tool-2", chunks: ["Fetched platform documentation"] });
468
+ raf.flush();
469
+
470
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
471
+
472
+ controller.destroy();
473
+ });
474
+
371
475
  it("uses icon-only arrow-down defaults for the transcript affordance", () => {
372
476
  const raf = installRafMock();
373
477
  const mount = createMount();