@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/README.md +55 -1
- package/dist/index.cjs +30 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +307 -1
- package/dist/index.d.ts +307 -1
- package/dist/index.global.js +71 -71
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +30 -30
- package/dist/index.js.map +1 -1
- package/dist/widget.css +185 -0
- package/package.json +2 -1
- package/src/client.test.ts +645 -0
- package/src/client.ts +373 -0
- package/src/components/event-stream-view.test.ts +1233 -0
- package/src/components/event-stream-view.ts +1179 -0
- package/src/index.ts +19 -1
- package/src/plugins/types.ts +34 -1
- package/src/runtime/init.ts +5 -0
- package/src/session.ts +104 -1
- package/src/styles/widget.css +185 -0
- package/src/types.ts +252 -0
- package/src/ui.ts +281 -4
- package/src/utils/event-stream-buffer.test.ts +268 -0
- package/src/utils/event-stream-buffer.ts +112 -0
- package/src/utils/event-stream-capture.test.ts +539 -0
- package/src/utils/event-stream-controller.test.ts +445 -0
- package/src/utils/event-stream-store.test.ts +181 -0
- package/src/utils/event-stream-store.ts +182 -0
- package/src/utils/virtual-scroller.test.ts +449 -0
- package/src/utils/virtual-scroller.ts +151 -0
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
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
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
|
// ============================================================================
|