@runtypelabs/persona 1.41.0 → 1.43.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.
package/src/types.ts CHANGED
@@ -73,6 +73,91 @@ export type AgentWidgetRequestPayload = {
73
73
  metadata?: Record<string, unknown>;
74
74
  };
75
75
 
76
+ // ============================================================================
77
+ // Agent Execution Types
78
+ // ============================================================================
79
+
80
+ /**
81
+ * Configuration for agent loop behavior.
82
+ */
83
+ export type AgentLoopConfig = {
84
+ /** Maximum number of reasoning iterations */
85
+ maxIterations: number;
86
+ /** Stop condition: 'auto' for automatic detection, or a custom JS expression */
87
+ stopCondition?: 'auto' | string;
88
+ /** Enable periodic reflection during execution */
89
+ enableReflection?: boolean;
90
+ /** Number of iterations between reflections */
91
+ reflectionInterval?: number;
92
+ };
93
+
94
+ /**
95
+ * Agent configuration for agent execution mode.
96
+ * When provided in the widget config, enables agent loop execution instead of flow dispatch.
97
+ */
98
+ export type AgentConfig = {
99
+ /** Agent display name */
100
+ name: string;
101
+ /** Model identifier (e.g., 'openai:gpt-4o-mini') */
102
+ model: string;
103
+ /** System prompt for the agent */
104
+ systemPrompt: string;
105
+ /** Temperature for model responses */
106
+ temperature?: number;
107
+ /** Loop configuration for multi-iteration execution */
108
+ loopConfig?: AgentLoopConfig;
109
+ };
110
+
111
+ /**
112
+ * Options for agent execution requests.
113
+ */
114
+ export type AgentRequestOptions = {
115
+ /** Whether to stream the response (should be true for widget usage) */
116
+ streamResponse?: boolean;
117
+ /** Record mode: 'virtual' for no persistence, 'existing'/'create' for database records */
118
+ recordMode?: 'virtual' | 'existing' | 'create';
119
+ /** Whether to store results server-side */
120
+ storeResults?: boolean;
121
+ /** Enable debug mode for additional event data */
122
+ debugMode?: boolean;
123
+ };
124
+
125
+ /**
126
+ * Request payload for agent execution mode.
127
+ */
128
+ export type AgentWidgetAgentRequestPayload = {
129
+ agent: AgentConfig;
130
+ messages: AgentWidgetRequestPayloadMessage[];
131
+ options: AgentRequestOptions;
132
+ context?: Record<string, unknown>;
133
+ metadata?: Record<string, unknown>;
134
+ };
135
+
136
+ /**
137
+ * Agent execution state tracking.
138
+ */
139
+ export type AgentExecutionState = {
140
+ executionId: string;
141
+ agentId: string;
142
+ agentName: string;
143
+ status: 'running' | 'complete' | 'error';
144
+ currentIteration: number;
145
+ maxIterations: number;
146
+ startedAt?: number;
147
+ completedAt?: number;
148
+ stopReason?: 'max_iterations' | 'complete' | 'error' | 'manual';
149
+ };
150
+
151
+ /**
152
+ * Metadata attached to messages created during agent execution.
153
+ */
154
+ export type AgentMessageMetadata = {
155
+ executionId?: string;
156
+ iteration?: number;
157
+ turnId?: string;
158
+ agentName?: string;
159
+ };
160
+
76
161
  export type AgentWidgetRequestMiddlewareContext = {
77
162
  payload: AgentWidgetRequestPayload;
78
163
  config: AgentWidgetConfig;
@@ -270,11 +355,138 @@ export type AgentWidgetControllerEventMap = {
270
355
  "widget:state": AgentWidgetStateSnapshot;
271
356
  "message:feedback": AgentWidgetMessageFeedback;
272
357
  "message:copy": AgentWidgetMessage;
358
+ "eventStream:opened": { timestamp: number };
359
+ "eventStream:closed": { timestamp: number };
273
360
  };
274
361
 
275
362
  export type AgentWidgetFeatureFlags = {
276
363
  showReasoning?: boolean;
277
364
  showToolCalls?: boolean;
365
+ showEventStreamToggle?: boolean;
366
+ /** Configuration for the Event Stream inspector view */
367
+ eventStream?: EventStreamConfig;
368
+ };
369
+
370
+ export type SSEEventRecord = {
371
+ id: string;
372
+ type: string;
373
+ timestamp: number;
374
+ payload: string;
375
+ };
376
+
377
+ // ============================================================================
378
+ // Event Stream Configuration Types
379
+ // ============================================================================
380
+
381
+ /**
382
+ * Badge color configuration for event stream event types.
383
+ */
384
+ export type EventStreamBadgeColor = {
385
+ /** Background color (CSS value) */
386
+ bg: string;
387
+ /** Text color (CSS value) */
388
+ text: string;
389
+ };
390
+
391
+ /**
392
+ * Configuration for the Event Stream inspector view.
393
+ */
394
+ export type EventStreamConfig = {
395
+ /**
396
+ * Custom badge color mappings by event type prefix or exact type.
397
+ * Keys are matched as exact match first, then prefix match (keys ending with "_").
398
+ * @example { "flow_": { bg: "#dcfce7", text: "#166534" }, "error": { bg: "#fecaca", text: "#991b1b" } }
399
+ */
400
+ badgeColors?: Record<string, EventStreamBadgeColor>;
401
+ /**
402
+ * Timestamp display format.
403
+ * - "relative": Shows time offset from first event (+0.000s, +0.361s)
404
+ * - "absolute": Shows wall-clock time (HH:MM:SS.mmm)
405
+ * @default "relative"
406
+ */
407
+ timestampFormat?: "absolute" | "relative";
408
+ /**
409
+ * Whether to show sequential event numbers (1, 2, 3...).
410
+ * @default true
411
+ */
412
+ showSequenceNumbers?: boolean;
413
+ /**
414
+ * Maximum events to keep in the ring buffer.
415
+ * @default 500
416
+ */
417
+ maxEvents?: number;
418
+ /**
419
+ * Fields to extract from event payloads for description text.
420
+ * The first matching field value is displayed after the badge.
421
+ * @default ["flowName", "stepName", "name", "tool", "toolName"]
422
+ */
423
+ descriptionFields?: string[];
424
+ /**
425
+ * Custom CSS class names to append to event stream UI elements.
426
+ * Each value is a space-separated class string appended to the element's default classes.
427
+ */
428
+ classNames?: {
429
+ /** The toggle button in the widget header (activity icon). */
430
+ toggleButton?: string;
431
+ /** Additional classes applied to the toggle button when the event stream is open. */
432
+ toggleButtonActive?: string;
433
+ /** The outer event stream panel/container. */
434
+ panel?: string;
435
+ /** The toolbar header bar (title, filter, copy all). */
436
+ headerBar?: string;
437
+ /** The search bar wrapper. */
438
+ searchBar?: string;
439
+ /** The search text input. */
440
+ searchInput?: string;
441
+ /** Each event row wrapper. */
442
+ eventRow?: string;
443
+ /** The "new events" scroll indicator pill. */
444
+ scrollIndicator?: string;
445
+ };
446
+ };
447
+
448
+ /**
449
+ * Context for the renderEventStreamView plugin hook.
450
+ */
451
+ export type EventStreamViewRenderContext = {
452
+ config: AgentWidgetConfig;
453
+ events: SSEEventRecord[];
454
+ defaultRenderer: () => HTMLElement;
455
+ onClose?: () => void;
456
+ };
457
+
458
+ /**
459
+ * Context for the renderEventStreamRow plugin hook.
460
+ */
461
+ export type EventStreamRowRenderContext = {
462
+ event: SSEEventRecord;
463
+ index: number;
464
+ config: AgentWidgetConfig;
465
+ defaultRenderer: () => HTMLElement;
466
+ isExpanded: boolean;
467
+ onToggleExpand: () => void;
468
+ };
469
+
470
+ /**
471
+ * Context for the renderEventStreamToolbar plugin hook.
472
+ */
473
+ export type EventStreamToolbarRenderContext = {
474
+ config: AgentWidgetConfig;
475
+ defaultRenderer: () => HTMLElement;
476
+ eventCount: number;
477
+ filteredCount: number;
478
+ onFilterChange: (type: string) => void;
479
+ onSearchChange: (term: string) => void;
480
+ };
481
+
482
+ /**
483
+ * Context for the renderEventStreamPayload plugin hook.
484
+ */
485
+ export type EventStreamPayloadRenderContext = {
486
+ event: SSEEventRecord;
487
+ config: AgentWidgetConfig;
488
+ defaultRenderer: () => HTMLElement;
489
+ parsedPayload: unknown;
278
490
  };
279
491
 
280
492
  export type AgentWidgetTheme = {
@@ -1399,6 +1611,41 @@ export type AgentWidgetLoadingIndicatorConfig = {
1399
1611
  export type AgentWidgetConfig = {
1400
1612
  apiUrl?: string;
1401
1613
  flowId?: string;
1614
+ /**
1615
+ * Agent configuration for agent execution mode.
1616
+ * When provided, the widget uses agent loop execution instead of flow dispatch.
1617
+ * Mutually exclusive with `flowId`.
1618
+ *
1619
+ * @example
1620
+ * ```typescript
1621
+ * config: {
1622
+ * agent: {
1623
+ * name: 'Assistant',
1624
+ * model: 'openai:gpt-4o-mini',
1625
+ * systemPrompt: 'You are a helpful assistant.',
1626
+ * loopConfig: { maxIterations: 3, stopCondition: 'auto' }
1627
+ * }
1628
+ * }
1629
+ * ```
1630
+ */
1631
+ agent?: AgentConfig;
1632
+ /**
1633
+ * Options for agent execution requests.
1634
+ * Only used when `agent` is configured.
1635
+ *
1636
+ * @default { streamResponse: true, recordMode: 'virtual' }
1637
+ */
1638
+ agentOptions?: AgentRequestOptions;
1639
+ /**
1640
+ * Controls how multiple agent iterations are displayed in the chat UI.
1641
+ * Only used when `agent` is configured.
1642
+ *
1643
+ * - `'separate'`: Each iteration creates a new assistant message bubble
1644
+ * - `'merged'`: All iterations stream into a single assistant message
1645
+ *
1646
+ * @default 'separate'
1647
+ */
1648
+ iterationDisplay?: 'separate' | 'merged';
1402
1649
  /**
1403
1650
  * Client token for direct browser-to-API communication.
1404
1651
  * When set, the widget uses /v1/client/* endpoints instead of /v1/dispatch.
@@ -1944,6 +2191,11 @@ export type AgentWidgetMessage = {
1944
2191
  * }
1945
2192
  */
1946
2193
  llmContent?: string;
2194
+ /**
2195
+ * Metadata for messages created during agent loop execution.
2196
+ * Contains execution context like iteration number and turn ID.
2197
+ */
2198
+ agentMetadata?: AgentMessageMetadata;
1947
2199
  };
1948
2200
 
1949
2201
  // ============================================================================
package/src/ui.ts CHANGED
@@ -38,6 +38,9 @@ import { createStandardBubble, createTypingIndicator } from "./components/messag
38
38
  import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
39
39
  import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
40
40
  import { createSuggestions } from "./components/suggestions";
41
+ import { EventStreamBuffer } from "./utils/event-stream-buffer";
42
+ import { EventStreamStore } from "./utils/event-stream-store";
43
+ import { createEventStreamView } from "./components/event-stream-view";
41
44
  import { enhanceWithForms } from "./components/forms";
42
45
  import { pluginRegistry } from "./plugins/registry";
43
46
  import { mergeWithDefaults } from "./defaults";
@@ -172,6 +175,10 @@ type Controller = {
172
175
  * Convenience method for injecting system messages.
173
176
  */
174
177
  injectSystemMessage: (options: InjectSystemMessageOptions) => AgentWidgetMessage;
178
+ /**
179
+ * Inject multiple messages in a single batch with one sort and one render pass.
180
+ */
181
+ injectMessageBatch: (optionsList: InjectMessageOptions[]) => AgentWidgetMessage[];
175
182
  /**
176
183
  * @deprecated Use injectMessage() instead.
177
184
  */
@@ -199,6 +206,14 @@ type Controller = {
199
206
  showNPSFeedback: (options?: Partial<NPSFeedbackOptions>) => void;
200
207
  submitCSATFeedback: (rating: number, comment?: string) => Promise<void>;
201
208
  submitNPSFeedback: (rating: number, comment?: string) => Promise<void>;
209
+ /** Push a raw event into the event stream buffer (for testing/debugging) */
210
+ __pushEventStreamEvent: (event: { type: string; payload: unknown }) => void;
211
+ /** Opens the event stream panel */
212
+ showEventStream: () => void;
213
+ /** Closes the event stream panel */
214
+ hideEventStream: () => void;
215
+ /** Returns current visibility state of the event stream panel */
216
+ isEventStreamVisible: () => boolean;
202
217
  };
203
218
 
204
219
  const buildPostprocessor = (
@@ -260,10 +275,12 @@ export const createAgentExperience = (
260
275
  initialConfig?: AgentWidgetConfig,
261
276
  runtimeOptions?: { debugTools?: boolean }
262
277
  ): Controller => {
263
- // Tailwind config uses important: "#persona-root", so ensure mount has this ID
264
- if (!mount.id || mount.id !== "persona-root") {
265
- mount.id = "persona-root";
278
+ // Preserve original mount id as data attribute for window event instance scoping,
279
+ // then set mount.id to "persona-root" (required by Tailwind important: "#persona-root")
280
+ if (mount.id && mount.id !== "persona-root" && !mount.getAttribute("data-persona-instance")) {
281
+ mount.setAttribute("data-persona-instance", mount.id);
266
282
  }
283
+ mount.id = "persona-root";
267
284
 
268
285
  let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
269
286
  // Note: applyThemeVariables is called after applyFullHeightStyles() below
@@ -404,7 +421,24 @@ export const createAgentExperience = (
404
421
  let postprocess = buildPostprocessor(config, actionManager, handleResubmitRequested);
405
422
  let showReasoning = config.features?.showReasoning ?? true;
406
423
  let showToolCalls = config.features?.showToolCalls ?? true;
407
-
424
+ let showEventStreamToggle = config.features?.showEventStreamToggle ?? false;
425
+ const persistKeyPrefix = (typeof config.persistState === 'object' ? config.persistState?.keyPrefix : undefined) ?? "persona-";
426
+ const eventStreamDbName = `${persistKeyPrefix}event-stream`;
427
+ let eventStreamStore = showEventStreamToggle ? new EventStreamStore(eventStreamDbName) : null;
428
+ const eventStreamMaxEvents = config.features?.eventStream?.maxEvents ?? 2000;
429
+ let eventStreamBuffer = showEventStreamToggle ? new EventStreamBuffer(eventStreamMaxEvents, eventStreamStore) : null;
430
+ let eventStreamView: ReturnType<typeof createEventStreamView> | null = null;
431
+ let eventStreamVisible = false;
432
+ let eventStreamRAF: number | null = null;
433
+ let eventStreamLastUpdate = 0;
434
+
435
+ // Open IndexedDB store and restore persisted events into the buffer
436
+ eventStreamStore?.open().then(() => {
437
+ return eventStreamBuffer?.restore();
438
+ }).catch(err => {
439
+ if (config.debug) console.warn('[AgentWidget] IndexedDB not available for event stream:', err);
440
+ });
441
+
408
442
  // Create message action callbacks that emit events and optionally send to API
409
443
  const messageActionCallbacks: MessageActionCallbacks = {
410
444
  onCopy: (message: AgentWidgetMessage) => {
@@ -519,6 +553,101 @@ export const createAgentExperience = (
519
553
  }
520
554
  }
521
555
 
556
+ // Event stream toggle functions (lifted to outer scope for controller access)
557
+ const toggleEventStreamOn = () => {
558
+ if (!eventStreamBuffer) return;
559
+ eventStreamVisible = true;
560
+ if (!eventStreamView && eventStreamBuffer) {
561
+ eventStreamView = createEventStreamView({
562
+ buffer: eventStreamBuffer,
563
+ getFullHistory: () => eventStreamBuffer!.getAllFromStore(),
564
+ onClose: () => toggleEventStreamOff(),
565
+ config,
566
+ plugins,
567
+ });
568
+ }
569
+ if (eventStreamView) {
570
+ body.style.display = "none";
571
+ footer.parentNode?.insertBefore(eventStreamView.element, footer);
572
+ eventStreamView.update();
573
+ }
574
+ if (eventStreamToggleBtn) {
575
+ eventStreamToggleBtn.classList.remove("tvw-text-cw-muted");
576
+ eventStreamToggleBtn.classList.add("tvw-text-cw-accent");
577
+ eventStreamToggleBtn.style.boxShadow = "inset 0 0 0 1.5px var(--cw-accent, #3b82f6)";
578
+ const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
579
+ if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.add(c));
580
+ }
581
+ // Start RAF-based update loop (throttled to ~200ms)
582
+ const rafLoop = () => {
583
+ if (!eventStreamVisible) return;
584
+ const now = Date.now();
585
+ if (now - eventStreamLastUpdate >= 200) {
586
+ eventStreamView?.update();
587
+ eventStreamLastUpdate = now;
588
+ }
589
+ eventStreamRAF = requestAnimationFrame(rafLoop);
590
+ };
591
+ eventStreamLastUpdate = 0;
592
+ eventStreamRAF = requestAnimationFrame(rafLoop);
593
+ eventBus.emit("eventStream:opened", { timestamp: Date.now() });
594
+ };
595
+
596
+ const toggleEventStreamOff = () => {
597
+ if (!eventStreamVisible) return;
598
+ eventStreamVisible = false;
599
+ if (eventStreamView) {
600
+ eventStreamView.element.remove();
601
+ }
602
+ body.style.display = "";
603
+ if (eventStreamToggleBtn) {
604
+ eventStreamToggleBtn.classList.remove("tvw-text-cw-accent");
605
+ eventStreamToggleBtn.classList.add("tvw-text-cw-muted");
606
+ eventStreamToggleBtn.style.boxShadow = "";
607
+ const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
608
+ if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.remove(c));
609
+ }
610
+ // Cancel RAF update loop
611
+ if (eventStreamRAF !== null) {
612
+ cancelAnimationFrame(eventStreamRAF);
613
+ eventStreamRAF = null;
614
+ }
615
+ eventBus.emit("eventStream:closed", { timestamp: Date.now() });
616
+ };
617
+
618
+ // Event stream toggle button
619
+ let eventStreamToggleBtn: HTMLButtonElement | null = null;
620
+ if (showEventStreamToggle) {
621
+ const esClassNames = config.features?.eventStream?.classNames;
622
+ const toggleBtnClasses = "tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none tvw-bg-transparent tvw-p-1" + (esClassNames?.toggleButton ? " " + esClassNames.toggleButton : "");
623
+ eventStreamToggleBtn = createElement("button", toggleBtnClasses) as HTMLButtonElement;
624
+ eventStreamToggleBtn.style.width = "28px";
625
+ eventStreamToggleBtn.style.height = "28px";
626
+ eventStreamToggleBtn.type = "button";
627
+ eventStreamToggleBtn.setAttribute("aria-label", "Event Stream");
628
+ eventStreamToggleBtn.title = "Event Stream";
629
+ const activityIcon = renderLucideIcon("activity", "18px", "currentColor", 1.5);
630
+ if (activityIcon) eventStreamToggleBtn.appendChild(activityIcon);
631
+
632
+ // Insert before clear chat button wrapper or close button wrapper
633
+ const clearChatWrapper = panelElements.clearChatButtonWrapper;
634
+ const closeWrapper = panelElements.closeButtonWrapper;
635
+ const insertBefore = clearChatWrapper || closeWrapper;
636
+ if (insertBefore && insertBefore.parentNode === header) {
637
+ header.insertBefore(eventStreamToggleBtn, insertBefore);
638
+ } else {
639
+ header.appendChild(eventStreamToggleBtn);
640
+ }
641
+
642
+ eventStreamToggleBtn.addEventListener("click", () => {
643
+ if (eventStreamVisible) {
644
+ toggleEventStreamOff();
645
+ } else {
646
+ toggleEventStreamOn();
647
+ }
648
+ });
649
+ }
650
+
522
651
  // Plugin hook: renderComposer - allow plugins to provide custom composer
523
652
  const composerPlugin = plugins.find(p => p.renderComposer);
524
653
  if (composerPlugin?.renderComposer) {
@@ -887,6 +1016,21 @@ export const createAgentExperience = (
887
1016
 
888
1017
  const destroyCallbacks: Array<() => void> = [];
889
1018
 
1019
+ // Event stream cleanup
1020
+ if (showEventStreamToggle) {
1021
+ destroyCallbacks.push(() => {
1022
+ if (eventStreamRAF !== null) {
1023
+ cancelAnimationFrame(eventStreamRAF);
1024
+ eventStreamRAF = null;
1025
+ }
1026
+ eventStreamView?.destroy();
1027
+ eventStreamView = null;
1028
+ eventStreamBuffer?.destroy();
1029
+ eventStreamBuffer = null;
1030
+ eventStreamStore = null;
1031
+ });
1032
+ }
1033
+
890
1034
  // Set up theme observer for auto color scheme detection
891
1035
  let cleanupThemeObserver: (() => void) | null = null;
892
1036
  const setupThemeObserver = () => {
@@ -1670,6 +1814,18 @@ export const createAgentExperience = (
1670
1814
  }
1671
1815
  });
1672
1816
 
1817
+ // Wire up event stream buffer to capture SSE events
1818
+ if (eventStreamBuffer) {
1819
+ session.setSSEEventCallback((type: string, payload: unknown) => {
1820
+ eventStreamBuffer?.push({
1821
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1822
+ type,
1823
+ timestamp: Date.now(),
1824
+ payload: JSON.stringify(payload)
1825
+ });
1826
+ });
1827
+ }
1828
+
1673
1829
  if (pendingStoredState) {
1674
1830
  pendingStoredState
1675
1831
  .then((state) => {
@@ -2254,6 +2410,10 @@ export const createAgentExperience = (
2254
2410
  }
2255
2411
  persistentMetadata = {};
2256
2412
  actionManager.syncFromMetadata();
2413
+
2414
+ // Clear event stream buffer and store
2415
+ eventStreamBuffer?.clear();
2416
+ eventStreamView?.update();
2257
2417
  });
2258
2418
  };
2259
2419
 
@@ -2304,6 +2464,66 @@ export const createAgentExperience = (
2304
2464
  autoExpand = config.launcher?.autoExpand ?? false;
2305
2465
  showReasoning = config.features?.showReasoning ?? true;
2306
2466
  showToolCalls = config.features?.showToolCalls ?? true;
2467
+ const prevShowEventStreamToggle = showEventStreamToggle;
2468
+ showEventStreamToggle = config.features?.showEventStreamToggle ?? false;
2469
+
2470
+ // Handle dynamic event stream feature flag toggling
2471
+ if (showEventStreamToggle && !prevShowEventStreamToggle) {
2472
+ // Flag changed from false to true - create buffer/store if needed
2473
+ if (!eventStreamBuffer) {
2474
+ eventStreamStore = new EventStreamStore(eventStreamDbName);
2475
+ eventStreamBuffer = new EventStreamBuffer(eventStreamMaxEvents, eventStreamStore);
2476
+ eventStreamStore.open().then(() => eventStreamBuffer?.restore()).catch(() => {});
2477
+ // Register the SSE event callback
2478
+ session.setSSEEventCallback((type: string, payload: unknown) => {
2479
+ eventStreamBuffer!.push({
2480
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2481
+ type,
2482
+ timestamp: Date.now(),
2483
+ payload: JSON.stringify(payload)
2484
+ });
2485
+ });
2486
+ }
2487
+ // Add header toggle button if not present
2488
+ if (!eventStreamToggleBtn && header) {
2489
+ const dynEsClassNames = config.features?.eventStream?.classNames;
2490
+ const dynToggleBtnClasses = "tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none tvw-bg-transparent tvw-p-1" + (dynEsClassNames?.toggleButton ? " " + dynEsClassNames.toggleButton : "");
2491
+ eventStreamToggleBtn = createElement("button", dynToggleBtnClasses) as HTMLButtonElement;
2492
+ eventStreamToggleBtn.style.width = "28px";
2493
+ eventStreamToggleBtn.style.height = "28px";
2494
+ eventStreamToggleBtn.type = "button";
2495
+ eventStreamToggleBtn.setAttribute("aria-label", "Event Stream");
2496
+ eventStreamToggleBtn.title = "Event Stream";
2497
+ const activityIcon = renderLucideIcon("activity", "18px", "currentColor", 1.5);
2498
+ if (activityIcon) eventStreamToggleBtn.appendChild(activityIcon);
2499
+ const clearChatWrapper = panelElements.clearChatButtonWrapper;
2500
+ const closeWrapper = panelElements.closeButtonWrapper;
2501
+ const insertBefore = clearChatWrapper || closeWrapper;
2502
+ if (insertBefore && insertBefore.parentNode === header) {
2503
+ header.insertBefore(eventStreamToggleBtn, insertBefore);
2504
+ } else {
2505
+ header.appendChild(eventStreamToggleBtn);
2506
+ }
2507
+ eventStreamToggleBtn.addEventListener("click", () => {
2508
+ if (eventStreamVisible) {
2509
+ toggleEventStreamOff();
2510
+ } else {
2511
+ toggleEventStreamOn();
2512
+ }
2513
+ });
2514
+ }
2515
+ } else if (!showEventStreamToggle && prevShowEventStreamToggle) {
2516
+ // Flag changed from true to false - hide and clean up
2517
+ toggleEventStreamOff();
2518
+ if (eventStreamToggleBtn) {
2519
+ eventStreamToggleBtn.remove();
2520
+ eventStreamToggleBtn = null;
2521
+ }
2522
+ eventStreamBuffer?.clear();
2523
+ eventStreamStore?.destroy();
2524
+ eventStreamBuffer = null;
2525
+ eventStreamStore = null;
2526
+ }
2307
2527
 
2308
2528
  if (config.launcher?.enabled === false && launcherButtonInstance) {
2309
2529
  launcherButtonInstance.destroy();
@@ -3450,6 +3670,10 @@ export const createAgentExperience = (
3450
3670
  }
3451
3671
  persistentMetadata = {};
3452
3672
  actionManager.syncFromMetadata();
3673
+
3674
+ // Clear event stream buffer and store
3675
+ eventStreamBuffer?.clear();
3676
+ eventStreamView?.update();
3453
3677
  },
3454
3678
  setMessage(message: string): boolean {
3455
3679
  if (!textarea) return false;
@@ -3552,6 +3776,12 @@ export const createAgentExperience = (
3552
3776
  }
3553
3777
  return session.injectSystemMessage(options);
3554
3778
  },
3779
+ injectMessageBatch(optionsList: InjectMessageOptions[]): AgentWidgetMessage[] {
3780
+ if (!open && launcherEnabled) {
3781
+ setOpenState(true, "system");
3782
+ }
3783
+ return session.injectMessageBatch(optionsList);
3784
+ },
3555
3785
  /** @deprecated Use injectMessage() instead */
3556
3786
  injectTestMessage(event: AgentWidgetEvent) {
3557
3787
  // Auto-open widget if closed and launcher is enabled
@@ -3560,6 +3790,28 @@ export const createAgentExperience = (
3560
3790
  }
3561
3791
  session.injectTestEvent(event);
3562
3792
  },
3793
+ /** Push a raw event into the event stream buffer (for testing/debugging) */
3794
+ __pushEventStreamEvent(event: { type: string; payload: unknown }): void {
3795
+ if (eventStreamBuffer) {
3796
+ eventStreamBuffer.push({
3797
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
3798
+ type: event.type,
3799
+ timestamp: Date.now(),
3800
+ payload: JSON.stringify(event.payload)
3801
+ });
3802
+ }
3803
+ },
3804
+ showEventStream(): void {
3805
+ if (!showEventStreamToggle || !eventStreamBuffer) return;
3806
+ toggleEventStreamOn();
3807
+ },
3808
+ hideEventStream(): void {
3809
+ if (!eventStreamVisible) return;
3810
+ toggleEventStreamOff();
3811
+ },
3812
+ isEventStreamVisible(): boolean {
3813
+ return eventStreamVisible;
3814
+ },
3563
3815
  getMessages() {
3564
3816
  return session.getMessages();
3565
3817
  },
@@ -3692,6 +3944,31 @@ export const createAgentExperience = (
3692
3944
  });
3693
3945
  }
3694
3946
 
3947
+ // ============================================================================
3948
+ // INSTANCE-SCOPED WINDOW EVENTS FOR PROGRAMMATIC CONTROL
3949
+ // ============================================================================
3950
+ if (showEventStreamToggle && typeof window !== "undefined") {
3951
+ const instanceId = mount.getAttribute("data-persona-instance") || mount.id || "persona-" + Math.random().toString(36).slice(2, 8);
3952
+ const handleShowEvent = (e: Event) => {
3953
+ const detail = (e as CustomEvent).detail;
3954
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
3955
+ controller.showEventStream();
3956
+ }
3957
+ };
3958
+ const handleHideEvent = (e: Event) => {
3959
+ const detail = (e as CustomEvent).detail;
3960
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
3961
+ controller.hideEventStream();
3962
+ }
3963
+ };
3964
+ window.addEventListener("persona:showEventStream", handleShowEvent);
3965
+ window.addEventListener("persona:hideEventStream", handleHideEvent);
3966
+ destroyCallbacks.push(() => {
3967
+ window.removeEventListener("persona:showEventStream", handleShowEvent);
3968
+ window.removeEventListener("persona:hideEventStream", handleHideEvent);
3969
+ });
3970
+ }
3971
+
3695
3972
  // ============================================================================
3696
3973
  // STATE PERSISTENCE ACROSS PAGE NAVIGATIONS
3697
3974
  // ============================================================================