@runtypelabs/persona 4.4.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +27 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.global.js +25 -25
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +29 -29
- package/dist/index.js.map +1 -1
- package/dist/theme-editor-preview.cjs +28 -28
- package/dist/theme-editor-preview.js +26 -26
- package/dist/widget.css +24 -0
- package/package.json +1 -1
- package/src/components/event-stream-view.test.ts +23 -22
- package/src/components/event-stream-view.ts +25 -47
- package/src/styles/widget.css +24 -0
- package/src/ui.ts +9 -0
package/dist/widget.css
CHANGED
|
@@ -1586,6 +1586,18 @@
|
|
|
1586
1586
|
/* ============================================
|
|
1587
1587
|
Markdown Table Styles
|
|
1588
1588
|
============================================ */
|
|
1589
|
+
/* Tables span the full chat column. The assistant bubble is a flex item that
|
|
1590
|
+
sizes to its content, so a width:100% table only fills that shrunk box — and
|
|
1591
|
+
once the streaming `table-layout: fixed` lock is released the table visibly
|
|
1592
|
+
collapses to content width ("renders full-width, then snaps narrow"). When the
|
|
1593
|
+
bubble holds a table, let it grow to fill the row so the table keeps the full
|
|
1594
|
+
column width. */
|
|
1595
|
+
.persona-message-bubble:has(table) {
|
|
1596
|
+
width: 100%;
|
|
1597
|
+
max-width: 100%;
|
|
1598
|
+
flex-grow: 1;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1589
1601
|
.persona-message-bubble table {
|
|
1590
1602
|
width: 100%;
|
|
1591
1603
|
border-collapse: collapse;
|
|
@@ -4015,3 +4027,15 @@
|
|
|
4015
4027
|
animation: none !important;
|
|
4016
4028
|
}
|
|
4017
4029
|
}
|
|
4030
|
+
|
|
4031
|
+
/* Event stream toolbar: a size container so "Copy All" collapses to icon-only
|
|
4032
|
+
* in a narrow panel before the filter dropdown gets clipped. */
|
|
4033
|
+
[data-persona-root] .persona-event-toolbar {
|
|
4034
|
+
container-type: inline-size;
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
@container (max-width: 390px) {
|
|
4038
|
+
[data-persona-root] .persona-event-copy-all {
|
|
4039
|
+
display: none;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -223,7 +223,10 @@ async function loadModule() {
|
|
|
223
223
|
// Helper: navigate the new DOM structure
|
|
224
224
|
// container.children = [toolbarOuter, truncationBanner, eventsListWrapper]
|
|
225
225
|
// toolbarOuter.children = [headerBar, searchBar]
|
|
226
|
-
// headerBar.children = [
|
|
226
|
+
// headerBar.children = [spacer, filterSelect, copyAllBtn]
|
|
227
|
+
// (the total event count is shown in the "All events (N)" filter option; an
|
|
228
|
+
// optional throughput readout is prepended only when a getThroughput option
|
|
229
|
+
// is passed, which these tests don't)
|
|
227
230
|
// searchBar.children = [searchIconWrapper, searchInput, searchClearBtn]
|
|
228
231
|
// eventsListWrapper.children = [eventsList, noResultsMsg, scrollIndicator]
|
|
229
232
|
|
|
@@ -236,17 +239,15 @@ function getHeaderBar(element: any) {
|
|
|
236
239
|
function getSearchBar(element: any) {
|
|
237
240
|
return getToolbar(element).children[1]; // searchBar
|
|
238
241
|
}
|
|
239
|
-
function getTitle(element: any) {
|
|
240
|
-
return getHeaderBar(element).children[0]; // title span
|
|
241
|
-
}
|
|
242
|
-
function getCountBadge(element: any) {
|
|
243
|
-
return getHeaderBar(element).children[1]; // count badge span
|
|
244
|
-
}
|
|
245
242
|
function getFilterSelect(element: any) {
|
|
246
|
-
return getHeaderBar(element).children[
|
|
243
|
+
return getHeaderBar(element).children[1]; // filterSelect (after spacer)
|
|
247
244
|
}
|
|
248
245
|
function getCopyAllBtn(element: any) {
|
|
249
|
-
return getHeaderBar(element).children[
|
|
246
|
+
return getHeaderBar(element).children[2]; // copyAllBtn
|
|
247
|
+
}
|
|
248
|
+
// The total count is carried by the "All events (N)" option, not a badge.
|
|
249
|
+
function getAllEventsOptionText(element: any) {
|
|
250
|
+
return getFilterSelect(element).options[0].textContent;
|
|
250
251
|
}
|
|
251
252
|
function getSearchInput(element: any) {
|
|
252
253
|
return getSearchBar(element).children[1]; // searchInput (after searchIconWrapper)
|
|
@@ -290,7 +291,7 @@ describe("createEventStreamView", () => {
|
|
|
290
291
|
});
|
|
291
292
|
|
|
292
293
|
describe("header bar", () => {
|
|
293
|
-
it("should
|
|
294
|
+
it("should carry the total in the All events option (no title or count badge)", async () => {
|
|
294
295
|
const { createEventStreamView } = await loadModule();
|
|
295
296
|
const events = [makeEvent("step_chunk", 1)];
|
|
296
297
|
const buffer = createMockBuffer(events);
|
|
@@ -298,11 +299,10 @@ describe("createEventStreamView", () => {
|
|
|
298
299
|
|
|
299
300
|
update();
|
|
300
301
|
|
|
301
|
-
expect(
|
|
302
|
-
expect(getCountBadge(element).textContent).toBe("1");
|
|
302
|
+
expect(getAllEventsOptionText(element)).toBe("All events (1)");
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
-
it("should update
|
|
305
|
+
it("should update the total in the All events option when events change", async () => {
|
|
306
306
|
vi.useFakeTimers();
|
|
307
307
|
const { createEventStreamView } = await loadModule();
|
|
308
308
|
const events = [makeEvent("step_chunk", 1)];
|
|
@@ -310,13 +310,13 @@ describe("createEventStreamView", () => {
|
|
|
310
310
|
const { element, update } = createEventStreamView({ buffer: buffer as any });
|
|
311
311
|
|
|
312
312
|
update();
|
|
313
|
-
expect(
|
|
313
|
+
expect(getAllEventsOptionText(element)).toBe("All events (1)");
|
|
314
314
|
|
|
315
315
|
vi.advanceTimersByTime(150);
|
|
316
316
|
buffer.push(makeEvent("step_chunk", 2));
|
|
317
317
|
update();
|
|
318
318
|
|
|
319
|
-
expect(
|
|
319
|
+
expect(getAllEventsOptionText(element)).toBe("All events (2)");
|
|
320
320
|
vi.useRealTimers();
|
|
321
321
|
});
|
|
322
322
|
});
|
|
@@ -336,9 +336,9 @@ describe("createEventStreamView", () => {
|
|
|
336
336
|
|
|
337
337
|
const filterSelect = getFilterSelect(element);
|
|
338
338
|
|
|
339
|
-
// Should have "All events" + 2 type options
|
|
339
|
+
// Should have "All events" (with total) + 2 type options
|
|
340
340
|
expect(filterSelect.options.length).toBe(3);
|
|
341
|
-
expect(filterSelect.options[0].textContent).toBe("All events");
|
|
341
|
+
expect(filterSelect.options[0].textContent).toBe("All events (3)");
|
|
342
342
|
expect(filterSelect.options[1].textContent).toBe("flow_complete (1)");
|
|
343
343
|
expect(filterSelect.options[2].textContent).toBe("step_chunk (2)");
|
|
344
344
|
});
|
|
@@ -353,7 +353,7 @@ describe("createEventStreamView", () => {
|
|
|
353
353
|
update();
|
|
354
354
|
|
|
355
355
|
const filterSelect = getFilterSelect(element);
|
|
356
|
-
expect(filterSelect.options[0].textContent).toBe("All events");
|
|
356
|
+
expect(filterSelect.options[0].textContent).toBe("All events (1)");
|
|
357
357
|
expect(filterSelect.options[1].textContent).toBe("step_chunk (1)");
|
|
358
358
|
|
|
359
359
|
// Add another event and advance past throttle window
|
|
@@ -361,6 +361,7 @@ describe("createEventStreamView", () => {
|
|
|
361
361
|
vi.advanceTimersByTime(150);
|
|
362
362
|
update();
|
|
363
363
|
|
|
364
|
+
expect(filterSelect.options[0].textContent).toBe("All events (2)");
|
|
364
365
|
expect(filterSelect.options[1].textContent).toBe("step_chunk (2)");
|
|
365
366
|
vi.useRealTimers();
|
|
366
367
|
});
|
|
@@ -1073,15 +1074,15 @@ describe("createEventStreamView", () => {
|
|
|
1073
1074
|
update();
|
|
1074
1075
|
|
|
1075
1076
|
const filterSelect = getFilterSelect(element);
|
|
1076
|
-
expect(filterSelect.options[0].textContent).toBe("All events");
|
|
1077
|
+
expect(filterSelect.options[0].textContent).toBe("All events (2)");
|
|
1077
1078
|
|
|
1078
1079
|
// Simulate clearChat: buffer.clear() + view.update()
|
|
1079
1080
|
vi.advanceTimersByTime(150);
|
|
1080
1081
|
buffer.clear();
|
|
1081
1082
|
update();
|
|
1082
1083
|
|
|
1083
|
-
// Filter should show "All events"
|
|
1084
|
-
expect(filterSelect.options[0].textContent).toBe("All events");
|
|
1084
|
+
// Filter should show "All events" with a zero total
|
|
1085
|
+
expect(filterSelect.options[0].textContent).toBe("All events (0)");
|
|
1085
1086
|
// No type-specific options remain
|
|
1086
1087
|
expect(filterSelect.options.length).toBe(1);
|
|
1087
1088
|
vi.useRealTimers();
|
|
@@ -1102,7 +1103,7 @@ describe("createEventStreamView", () => {
|
|
|
1102
1103
|
update();
|
|
1103
1104
|
|
|
1104
1105
|
const filterSelect = getFilterSelect(element);
|
|
1105
|
-
expect(filterSelect.options[0].textContent).toBe("All events");
|
|
1106
|
+
expect(filterSelect.options[0].textContent).toBe("All events (0)");
|
|
1106
1107
|
|
|
1107
1108
|
// New events arrive in new session
|
|
1108
1109
|
vi.advanceTimersByTime(150);
|
|
@@ -507,7 +507,6 @@ export function createEventStreamView(
|
|
|
507
507
|
|
|
508
508
|
// Elements we need references to across functions
|
|
509
509
|
// These are assigned inside buildDefaultToolbar() which always runs
|
|
510
|
-
let countBadge!: HTMLElement;
|
|
511
510
|
let filterSelect!: HTMLSelectElement;
|
|
512
511
|
let copyAllBtn!: HTMLButtonElement;
|
|
513
512
|
let searchInput!: HTMLInputElement;
|
|
@@ -522,7 +521,7 @@ export function createEventStreamView(
|
|
|
522
521
|
function buildDefaultToolbar(): HTMLElement {
|
|
523
522
|
const toolbarOuter = createElement(
|
|
524
523
|
"div",
|
|
525
|
-
"persona-relative persona-flex persona-flex-col persona-flex-shrink-0"
|
|
524
|
+
"persona-event-toolbar persona-relative persona-flex persona-flex-col persona-flex-shrink-0"
|
|
526
525
|
);
|
|
527
526
|
|
|
528
527
|
// --- Header bar ---
|
|
@@ -532,36 +531,20 @@ export function createEventStreamView(
|
|
|
532
531
|
);
|
|
533
532
|
applyCustomClasses(headerBar, customClasses?.headerBar);
|
|
534
533
|
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
"persona-text-sm persona-font-medium persona-text-persona-primary persona-whitespace-nowrap"
|
|
539
|
-
);
|
|
540
|
-
title.textContent = "Events";
|
|
541
|
-
|
|
542
|
-
// Count badge
|
|
543
|
-
countBadge = createElement(
|
|
544
|
-
"span",
|
|
545
|
-
"persona-text-[11px] persona-font-mono persona-bg-persona-container persona-text-persona-muted persona-px-2 persona-py-0.5 persona-rounded persona-border persona-border-persona-border"
|
|
546
|
-
);
|
|
547
|
-
countBadge.textContent = "0";
|
|
534
|
+
// The header leads straight into the controls. Context already names
|
|
535
|
+
// itself: the "All events (N)" filter option carries the total, the search
|
|
536
|
+
// placeholder names event payloads, and every row is an event.
|
|
548
537
|
|
|
549
|
-
// Inline throughput
|
|
550
|
-
//
|
|
551
|
-
//
|
|
538
|
+
// Inline throughput value, e.g. "146.3 tok/s". The "tok/s" unit is
|
|
539
|
+
// self-describing for this developer-facing inspector, so the value stands
|
|
540
|
+
// alone; its accessible name ("Throughput: <value>") and detailed breakdown
|
|
541
|
+
// live on the container's aria-label / hover tooltip (see updateThroughputSummary).
|
|
552
542
|
if (getThroughput) {
|
|
553
543
|
throughputContainer = createElement(
|
|
554
544
|
"div",
|
|
555
|
-
"persona-relative persona-flex persona-items-center persona-gap-1.5 persona-whitespace-nowrap
|
|
545
|
+
"persona-relative persona-flex persona-items-center persona-gap-1.5 persona-whitespace-nowrap"
|
|
556
546
|
);
|
|
557
547
|
throughputContainer.style.cursor = "help";
|
|
558
|
-
// Label styled to match the "Events" title.
|
|
559
|
-
const throughputLabel = createElement(
|
|
560
|
-
"span",
|
|
561
|
-
"persona-text-sm persona-font-medium persona-text-persona-primary persona-whitespace-nowrap"
|
|
562
|
-
);
|
|
563
|
-
throughputLabel.textContent = "Throughput";
|
|
564
|
-
// Same bounding box + styling as the Events count badge.
|
|
565
548
|
throughputValueEl = createElement(
|
|
566
549
|
"span",
|
|
567
550
|
"persona-text-[11px] persona-font-mono persona-bg-persona-container persona-text-persona-muted persona-px-2 persona-py-0.5 persona-rounded persona-border persona-border-persona-border persona-tabular-nums"
|
|
@@ -594,7 +577,6 @@ export function createEventStreamView(
|
|
|
594
577
|
throughputContainer.addEventListener("mouseenter", showTooltip);
|
|
595
578
|
throughputContainer.addEventListener("mouseleave", hideTooltip);
|
|
596
579
|
|
|
597
|
-
throughputContainer.appendChild(throughputLabel);
|
|
598
580
|
throughputContainer.appendChild(throughputValueEl);
|
|
599
581
|
}
|
|
600
582
|
|
|
@@ -607,7 +589,7 @@ export function createEventStreamView(
|
|
|
607
589
|
) as HTMLSelectElement;
|
|
608
590
|
const allOption = createElement("option", "") as HTMLOptionElement;
|
|
609
591
|
allOption.value = "";
|
|
610
|
-
allOption.textContent = "All events";
|
|
592
|
+
allOption.textContent = "All events (0)";
|
|
611
593
|
filterSelect.appendChild(allOption);
|
|
612
594
|
|
|
613
595
|
// Copy All button
|
|
@@ -629,13 +611,11 @@ export function createEventStreamView(
|
|
|
629
611
|
if (copyAllIcon) copyAllBtn.appendChild(copyAllIcon);
|
|
630
612
|
const copyAllLabel = createElement(
|
|
631
613
|
"span",
|
|
632
|
-
"persona-text-xs"
|
|
614
|
+
"persona-event-copy-all persona-text-xs"
|
|
633
615
|
);
|
|
634
616
|
copyAllLabel.textContent = "Copy All";
|
|
635
617
|
copyAllBtn.appendChild(copyAllLabel);
|
|
636
618
|
|
|
637
|
-
headerBar.appendChild(title);
|
|
638
|
-
headerBar.appendChild(countBadge);
|
|
639
619
|
if (throughputContainer) headerBar.appendChild(throughputContainer);
|
|
640
620
|
headerBar.appendChild(headerSpacer);
|
|
641
621
|
headerBar.appendChild(filterSelect);
|
|
@@ -734,20 +714,21 @@ export function createEventStreamView(
|
|
|
734
714
|
function updateThroughputSummary(): void {
|
|
735
715
|
if (!getThroughput || !throughputValueEl || !throughputContainer) return;
|
|
736
716
|
const metric = getThroughput();
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
//
|
|
740
|
-
//
|
|
717
|
+
const value = formatThroughputValue(metric);
|
|
718
|
+
throughputValueEl.textContent = value;
|
|
719
|
+
// There's no visible "Throughput" label, so the accessible name carries it
|
|
720
|
+
// (plus the value). The detailed breakdown is revealed on hover via the
|
|
721
|
+
// custom tooltip and appended to the aria-label for assistive tech; when
|
|
722
|
+
// there's nothing to show, hide the tooltip so an empty box never flashes.
|
|
741
723
|
const meta = formatThroughputMeta(metric);
|
|
742
724
|
if (throughputTooltipEl) {
|
|
743
725
|
throughputTooltipEl.textContent = meta;
|
|
744
726
|
if (!meta) throughputTooltipEl.style.display = "none";
|
|
745
727
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
728
|
+
throughputContainer.setAttribute(
|
|
729
|
+
"aria-label",
|
|
730
|
+
meta ? `Throughput: ${value}, ${meta}` : `Throughput: ${value}`
|
|
731
|
+
);
|
|
751
732
|
}
|
|
752
733
|
|
|
753
734
|
// ========================================================================
|
|
@@ -838,8 +819,10 @@ export function createEventStreamView(
|
|
|
838
819
|
|
|
839
820
|
const currentValue = filterSelect.value;
|
|
840
821
|
|
|
841
|
-
// Update "All events" option
|
|
842
|
-
|
|
822
|
+
// Update "All events" option. The total is carried here (like each type
|
|
823
|
+
// option carries its own count) so a narrow panel can hide the standalone
|
|
824
|
+
// "Events" label + count badge without losing the total.
|
|
825
|
+
filterSelect.options[0].textContent = `All events (${allEvents.length})`;
|
|
843
826
|
|
|
844
827
|
if (typesChanged) {
|
|
845
828
|
while (filterSelect.options.length > 1) {
|
|
@@ -940,11 +923,6 @@ export function createEventStreamView(
|
|
|
940
923
|
const newCount = filteredEvents.length;
|
|
941
924
|
const bufferHasEvents = buffer.getSize() > 0;
|
|
942
925
|
|
|
943
|
-
// Update count badge
|
|
944
|
-
if (countBadge) {
|
|
945
|
-
countBadge.textContent = String(buffer.getSize());
|
|
946
|
-
}
|
|
947
|
-
|
|
948
926
|
// Show/hide no-results message
|
|
949
927
|
if (newCount === 0 && bufferHasEvents && hasActiveFilters()) {
|
|
950
928
|
noResultsMsg.textContent = searchTerm
|
package/src/styles/widget.css
CHANGED
|
@@ -1586,6 +1586,18 @@
|
|
|
1586
1586
|
/* ============================================
|
|
1587
1587
|
Markdown Table Styles
|
|
1588
1588
|
============================================ */
|
|
1589
|
+
/* Tables span the full chat column. The assistant bubble is a flex item that
|
|
1590
|
+
sizes to its content, so a width:100% table only fills that shrunk box — and
|
|
1591
|
+
once the streaming `table-layout: fixed` lock is released the table visibly
|
|
1592
|
+
collapses to content width ("renders full-width, then snaps narrow"). When the
|
|
1593
|
+
bubble holds a table, let it grow to fill the row so the table keeps the full
|
|
1594
|
+
column width. */
|
|
1595
|
+
.persona-message-bubble:has(table) {
|
|
1596
|
+
width: 100%;
|
|
1597
|
+
max-width: 100%;
|
|
1598
|
+
flex-grow: 1;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1589
1601
|
.persona-message-bubble table {
|
|
1590
1602
|
width: 100%;
|
|
1591
1603
|
border-collapse: collapse;
|
|
@@ -4015,3 +4027,15 @@
|
|
|
4015
4027
|
animation: none !important;
|
|
4016
4028
|
}
|
|
4017
4029
|
}
|
|
4030
|
+
|
|
4031
|
+
/* Event stream toolbar: a size container so "Copy All" collapses to icon-only
|
|
4032
|
+
* in a narrow panel before the filter dropdown gets clipped. */
|
|
4033
|
+
[data-persona-root] .persona-event-toolbar {
|
|
4034
|
+
container-type: inline-size;
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
@container (max-width: 390px) {
|
|
4038
|
+
[data-persona-root] .persona-event-copy-all {
|
|
4039
|
+
display: none;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
package/src/ui.ts
CHANGED
|
@@ -2408,6 +2408,15 @@ export const createAgentExperience = (
|
|
|
2408
2408
|
body.style.cssText = '';
|
|
2409
2409
|
footer.style.cssText = '';
|
|
2410
2410
|
|
|
2411
|
+
// Preserve the event-stream takeover across a layout-mode change. The
|
|
2412
|
+
// cssText reset above wiped the `display: none` that toggleEventStreamOn
|
|
2413
|
+
// set on the messages body, and none of the per-mode reapply branches below
|
|
2414
|
+
// touch `display` — so without this the messages would reappear and stack
|
|
2415
|
+
// above the event panel when the window crosses the fullscreen breakpoint.
|
|
2416
|
+
if (eventStreamVisible) {
|
|
2417
|
+
body.style.display = "none";
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2411
2420
|
const restoreBodyScrollTop = (): void => {
|
|
2412
2421
|
if (prevBodyScrollTop <= 0) return;
|
|
2413
2422
|
const ownerWindow = body.ownerDocument.defaultView ?? window;
|