@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/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.0",
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 = [title, countBadge, spacer, filterSelect, copyAllBtn]
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[3]; // filterSelect (after title, badge, spacer)
243
+ return getHeaderBar(element).children[1]; // filterSelect (after spacer)
247
244
  }
248
245
  function getCopyAllBtn(element: any) {
249
- return getHeaderBar(element).children[4]; // copyAllBtn
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 show 'Event Stream' title and count badge", async () => {
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(getTitle(element).textContent).toBe("Events");
302
- expect(getCountBadge(element).textContent).toBe("1");
302
+ expect(getAllEventsOptionText(element)).toBe("All events (1)");
303
303
  });
304
304
 
305
- it("should update count badge when events change", async () => {
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(getCountBadge(element).textContent).toBe("1");
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(getCountBadge(element).textContent).toBe("2");
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
- // Title
536
- const title = createElement(
537
- "span",
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 group: "Throughput 146.3 tok/s", grouped with the
550
- // Events count. Hover reveals tokens · duration · source via a custom
551
- // tooltip (shown instantly, unlike the slow native `title` delay).
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 persona-ml-1"
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
- throughputValueEl.textContent = formatThroughputValue(metric);
738
- // Detailed breakdown is revealed on hover via the custom tooltip; mirror
739
- // it into aria-label for assistive tech. When there's nothing to show,
740
- // hide the tooltip so an empty box never flashes on hover.
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
- if (meta) {
747
- throughputContainer.setAttribute("aria-label", meta);
748
- } else {
749
- throughputContainer.removeAttribute("aria-label");
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
- filterSelect.options[0].textContent = `All events`;
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
@@ -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;