@mushi-mushi/web 0.5.1 → 0.8.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/dist/index.cjs CHANGED
@@ -245,11 +245,6 @@ function getWidgetStyles(theme) {
245
245
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
246
246
  button { font-family: inherit; }
247
247
 
248
- /* \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
249
- A small "stamp card" \u2014 soft rounded square (4px radius), paper
250
- background, vermillion bottom edge that reads as the inked face
251
- of a real \u5370\u9451. A pulsing dot in the top-right hints there's a
252
- channel here without needing a notification badge. */
253
248
  .mushi-trigger {
254
249
  position: fixed;
255
250
  width: 52px;
@@ -303,10 +298,53 @@ function getWidgetStyles(theme) {
303
298
  outline: 2px solid ${vermillion};
304
299
  outline-offset: 3px;
305
300
  }
306
- .mushi-trigger.bottom-right { bottom: 24px; right: 24px; }
307
- .mushi-trigger.bottom-left { bottom: 24px; left: 24px; }
308
- .mushi-trigger.top-right { top: 24px; right: 24px; }
309
- .mushi-trigger.top-left { top: 24px; left: 24px; }
301
+ .mushi-trigger.bottom-right {
302
+ bottom: var(--mushi-bottom, calc(24px + env(safe-area-inset-bottom, 0px)));
303
+ right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
304
+ }
305
+ .mushi-trigger.bottom-left {
306
+ bottom: var(--mushi-bottom, calc(24px + env(safe-area-inset-bottom, 0px)));
307
+ left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
308
+ }
309
+ .mushi-trigger.top-right {
310
+ top: var(--mushi-top, calc(24px + env(safe-area-inset-top, 0px)));
311
+ right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
312
+ }
313
+ .mushi-trigger.top-left {
314
+ top: var(--mushi-top, calc(24px + env(safe-area-inset-top, 0px)));
315
+ left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
316
+ }
317
+ .mushi-trigger.edge-tab {
318
+ width: 32px;
319
+ height: 88px;
320
+ border-radius: 4px 0 0 4px;
321
+ writing-mode: vertical-rl;
322
+ text-orientation: upright;
323
+ font-size: 16px;
324
+ box-shadow:
325
+ 0 1px 0 ${rule},
326
+ 0 10px 24px -14px rgba(14,13,11,0.45),
327
+ inset -3px 0 0 ${vermillion};
328
+ }
329
+ .mushi-trigger.edge-tab.bottom-right,
330
+ .mushi-trigger.edge-tab.top-right {
331
+ right: var(--mushi-right, 0);
332
+ }
333
+ .mushi-trigger.edge-tab.bottom-left,
334
+ .mushi-trigger.edge-tab.top-left {
335
+ left: var(--mushi-left, 0);
336
+ border-radius: 0 4px 4px 0;
337
+ box-shadow:
338
+ 0 1px 0 ${rule},
339
+ 0 10px 24px -14px rgba(14,13,11,0.45),
340
+ inset 3px 0 0 ${vermillion};
341
+ }
342
+ .mushi-trigger.shrunk {
343
+ width: 36px;
344
+ height: 36px;
345
+ opacity: 0.82;
346
+ transform: scale(0.92);
347
+ }
310
348
 
311
349
  @keyframes mushi-pulse {
312
350
  0% { box-shadow: 0 0 0 0 ${vermillion}; opacity: 1; }
@@ -314,12 +352,6 @@ function getWidgetStyles(theme) {
314
352
  100% { box-shadow: 0 0 0 0 rgba(224,60,44,0); opacity: 1; }
315
353
  }
316
354
 
317
- /* \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
318
- Paper-card. Sharper corners (6px) than typical SaaS modals
319
- (which default to 12-16px and read as plastic). Two-layer shadow:
320
- one hairline that sells the paper edge, one diffuse that lifts
321
- the panel off the underlying app. No backdrop-filter \u2014 we want
322
- the widget to feel like it sits ON the page, not blur INTO it. */
323
355
  .mushi-panel {
324
356
  position: fixed;
325
357
  width: 384px;
@@ -339,10 +371,49 @@ function getWidgetStyles(theme) {
339
371
  }
340
372
  .mushi-panel.open { animation: mushi-stamp-in 320ms ${easeStamp} both; }
341
373
  .mushi-panel.closed { display: none; }
342
- .mushi-panel.bottom-right { bottom: 88px; right: 24px; --mushi-origin: bottom right; }
343
- .mushi-panel.bottom-left { bottom: 88px; left: 24px; --mushi-origin: bottom left; }
344
- .mushi-panel.top-right { top: 88px; right: 24px; --mushi-origin: top right; }
345
- .mushi-panel.top-left { top: 88px; left: 24px; --mushi-origin: top left; }
374
+ .mushi-panel.bottom-right {
375
+ bottom: var(--mushi-panel-bottom, calc(var(--mushi-bottom, 24px) + 64px));
376
+ right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
377
+ --mushi-origin: bottom right;
378
+ }
379
+ .mushi-panel.bottom-left {
380
+ bottom: var(--mushi-panel-bottom, calc(var(--mushi-bottom, 24px) + 64px));
381
+ left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
382
+ --mushi-origin: bottom left;
383
+ }
384
+ .mushi-panel.top-right {
385
+ top: var(--mushi-panel-top, calc(var(--mushi-top, 24px) + 64px));
386
+ right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
387
+ --mushi-origin: top right;
388
+ }
389
+ .mushi-panel.top-left {
390
+ top: var(--mushi-panel-top, calc(var(--mushi-top, 24px) + 64px));
391
+ left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
392
+ --mushi-origin: top left;
393
+ }
394
+ .mushi-outdated {
395
+ margin: 12px 14px 0;
396
+ padding: 10px 12px;
397
+ border: 1px solid ${vermillionWash};
398
+ background: ${vermillionWash};
399
+ color: ${vermillionInk};
400
+ font-family: ${fontBody};
401
+ font-size: 12px;
402
+ line-height: 1.4;
403
+ }
404
+ .mushi-outdated strong {
405
+ display: block;
406
+ font-family: ${fontMono};
407
+ font-size: 10px;
408
+ letter-spacing: 0.12em;
409
+ text-transform: uppercase;
410
+ margin-bottom: 2px;
411
+ }
412
+ .mushi-outdated span {
413
+ display: block;
414
+ margin-top: 3px;
415
+ color: ${inkMuted};
416
+ }
346
417
 
347
418
  @keyframes mushi-stamp-in {
348
419
  0% { opacity: 0; transform: scale(0.94) translateY(6px); }
@@ -350,10 +421,6 @@ function getWidgetStyles(theme) {
350
421
  100% { opacity: 1; transform: scale(1) translateY(0); }
351
422
  }
352
423
 
353
- /* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
354
- Editorial masthead: small mono eyebrow ("MUSHI / REPORT") on top,
355
- serif display headline below, mono step counter on the far right.
356
- A single hairline separates header from body \u2014 no card stacking. */
357
424
  .mushi-header {
358
425
  padding: 18px 20px 14px;
359
426
  border-bottom: 1px solid ${rule};
@@ -450,10 +517,6 @@ function getWidgetStyles(theme) {
450
517
  .mushi-body::-webkit-scrollbar { width: 6px; }
451
518
  .mushi-body::-webkit-scrollbar-thumb { background: ${inkFaint}; border-radius: 3px; }
452
519
 
453
- /* \u2500\u2500 Step 1: Categories as a contents-page list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
454
- No boxes. Hairline rules between rows. Hovering a row pulls a
455
- vermillion arrow in from the right and tints the row label \u2014
456
- reads like flipping through an index card. */
457
520
  .mushi-option-btn {
458
521
  display: grid;
459
522
  grid-template-columns: auto 1fr auto;
@@ -505,12 +568,76 @@ function getWidgetStyles(theme) {
505
568
  transform: translateX(-4px);
506
569
  transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp}, color 220ms ${easeStamp};
507
570
  }
571
+ .mushi-report-row {
572
+ width: 100%;
573
+ display: grid;
574
+ grid-template-columns: auto 1fr auto;
575
+ gap: 8px;
576
+ align-items: center;
577
+ padding: 10px 0;
578
+ border: 0;
579
+ border-bottom: 1px solid ${rule};
580
+ background: transparent;
581
+ color: ${ink};
582
+ cursor: pointer;
583
+ text-align: left;
584
+ }
585
+ .mushi-report-status {
586
+ font-family: ${fontMono};
587
+ font-size: 10px;
588
+ color: ${vermillion};
589
+ text-transform: uppercase;
590
+ }
591
+ .mushi-report-title {
592
+ font-size: 13px;
593
+ overflow: hidden;
594
+ text-overflow: ellipsis;
595
+ white-space: nowrap;
596
+ }
597
+ .mushi-thread-summary {
598
+ border-bottom: 1px solid ${rule};
599
+ padding-bottom: 10px;
600
+ margin-bottom: 10px;
601
+ }
602
+ .mushi-thread-summary span {
603
+ font-family: ${fontMono};
604
+ font-size: 10px;
605
+ color: ${vermillion};
606
+ text-transform: uppercase;
607
+ }
608
+ .mushi-thread {
609
+ display: grid;
610
+ gap: 8px;
611
+ max-height: 180px;
612
+ overflow: auto;
613
+ margin-bottom: 12px;
614
+ }
615
+ .mushi-thread-comment {
616
+ padding: 8px 10px;
617
+ border: 1px solid ${rule};
618
+ background: ${isDark ? "rgba(242,235,221,0.04)" : "rgba(14,13,11,0.03)"};
619
+ }
620
+ .mushi-thread-comment.reporter {
621
+ border-color: ${vermillionWash};
622
+ background: ${vermillionWash};
623
+ }
624
+ .mushi-thread-comment strong {
625
+ display: block;
626
+ font-family: ${fontMono};
627
+ font-size: 10px;
628
+ letter-spacing: 0.08em;
629
+ text-transform: uppercase;
630
+ margin-bottom: 3px;
631
+ }
632
+ .mushi-thread-comment p,
633
+ .mushi-muted,
634
+ .mushi-error-inline {
635
+ font-size: 12px;
636
+ color: ${inkMuted};
637
+ line-height: 1.45;
638
+ }
639
+ .mushi-error-inline { color: ${vermillion}; }
508
640
 
509
- /* \u2500\u2500 Step 2: Selected-category breadcrumb + intent text-buttons \u2500
510
- Breadcrumb is a thin chip with the kanji-stamp aesthetic carried
511
- over (vermillion left rule). Intents are inline TEXT buttons
512
- with vermillion underlines on hover \u2014 not pill-shaped chips,
513
- which is the SaaS default and not what we are. */
514
641
  .mushi-selected-category {
515
642
  display: inline-flex;
516
643
  align-items: center;
@@ -566,10 +693,6 @@ function getWidgetStyles(theme) {
566
693
  box-shadow: inset 2px 0 0 ${vermillion};
567
694
  }
568
695
 
569
- /* \u2500\u2500 Step 3: Borderless textarea + minimal attach pills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
570
- The textarea has no box around it \u2014 just a hairline underline
571
- that turns vermillion on focus. Encourages writing rather than
572
- form-filling. */
573
696
  .mushi-textarea {
574
697
  width: 100%;
575
698
  min-height: 96px;
@@ -622,15 +745,21 @@ function getWidgetStyles(theme) {
622
745
  border-color: ${vermillion};
623
746
  background: ${vermillionWash};
624
747
  }
748
+ .mushi-attach-btn.danger {
749
+ color: ${vermillionInk};
750
+ border-color: ${vermillionWash};
751
+ background: transparent;
752
+ }
753
+ .mushi-attach-btn.danger:hover {
754
+ color: ${vermillion};
755
+ border-color: ${vermillion};
756
+ background: ${vermillionWash};
757
+ }
625
758
  .mushi-attach-btn:focus-visible {
626
759
  outline: 2px solid ${vermillion};
627
760
  outline-offset: 2px;
628
761
  }
629
762
 
630
- /* \u2500\u2500 Footer + submit (vermillion stamp) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
631
- Submit button is the heaviest visual moment in the widget \u2014
632
- vermillion fill, mono-caps label, send arrow. Holds an ink-
633
- bloom pseudo-element that animates outward when pressed. */
634
763
  .mushi-footer {
635
764
  padding: 14px 22px 16px;
636
765
  border-top: 1px solid ${rule};
@@ -695,10 +824,17 @@ function getWidgetStyles(theme) {
695
824
  }
696
825
  .mushi-submit:hover .mushi-submit-arrow { transform: translateX(3px); }
697
826
 
698
- /* \u2500\u2500 Step indicator (numeral ledger) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
699
- Replaces the generic three-dots with a typographic series:
700
- "01 \u2014 02 \u2014 03". The active step uses serif numerals, the
701
- others use mono so the active one literally reads heavier. */
827
+ .mushi-brand-footer {
828
+ padding: 9px 14px 11px;
829
+ border-top: 1px solid ${rule};
830
+ color: ${inkFaint};
831
+ font-family: ${fontMono};
832
+ font-size: 9px;
833
+ letter-spacing: 0.16em;
834
+ text-align: center;
835
+ text-transform: uppercase;
836
+ }
837
+
702
838
  .mushi-step-indicator {
703
839
  display: flex;
704
840
  align-items: center;
@@ -726,10 +862,6 @@ function getWidgetStyles(theme) {
726
862
  }
727
863
  .mushi-step-sep { width: 14px; height: 1px; background: ${rule}; }
728
864
 
729
- /* \u2500\u2500 Success: \u6731\u5370 stamp animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
730
- The success state is the signature moment. A vermillion ring
731
- scribes itself, then a "RECEIVED" mono-caps label fades in at
732
- the centre, evoking a hanko being pressed onto the form. */
733
865
  .mushi-success {
734
866
  text-align: center;
735
867
  padding: 28px 16px 20px;
@@ -793,9 +925,6 @@ function getWidgetStyles(theme) {
793
925
  100% { opacity: 1; transform: rotate(-6deg) scale(1); }
794
926
  }
795
927
 
796
- /* \u2500\u2500 Error \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
797
- Inline editorial note rather than a red box. Vermillion left
798
- rule keeps the same accent language. */
799
928
  .mushi-error {
800
929
  margin-top: 10px;
801
930
  padding: 8px 0 8px 10px;
@@ -806,9 +935,6 @@ function getWidgetStyles(theme) {
806
935
  letter-spacing: 0.02em;
807
936
  }
808
937
 
809
- /* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
810
- Honour the OS preference: kill every transition + animation
811
- except the focus underline (which is critical feedback). */
812
938
  @media (prefers-reduced-motion: reduce) {
813
939
  *,
814
940
  *::before,
@@ -838,12 +964,51 @@ var TOTAL_STEPS = 3;
838
964
  var STEP_NUMBER = {
839
965
  category: 1,
840
966
  intent: 2,
841
- details: 3
842
- };
967
+ details: 3};
843
968
  function isSubmitShortcut(e) {
844
969
  return (e.metaKey || e.ctrlKey) && e.key === "Enter";
845
970
  }
971
+ function escapeHtml(value) {
972
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
973
+ }
846
974
  var MushiWidget = class {
975
+ constructor(config = {}, callbacks, sdkVersion = "0.7.0") {
976
+ this.sdkVersion = sdkVersion;
977
+ this.config = {
978
+ position: config.position ?? "bottom-right",
979
+ anchor: config.anchor ?? {},
980
+ theme: config.theme ?? "auto",
981
+ // Falsy-OR (NOT `??`) on purpose: `triggerText: ''` is semantically
982
+ // nonsense — it would render a labelless, glyphless trigger button
983
+ // that users can't see or aim at. Treat empty string the same as
984
+ // omitted so any caller that wires this to a cleared form input or
985
+ // pastes a legacy snippet that emitted `triggerText: ""` (see
986
+ // apps/admin/src/lib/sdkSnippets.ts widgetLines history) still gets
987
+ // the default 🐛 and a visible button.
988
+ triggerText: config.triggerText || "\u{1F41B}",
989
+ expandedTitle: config.expandedTitle ?? "",
990
+ mode: config.mode ?? "conversational",
991
+ locale: config.locale ?? "auto",
992
+ zIndex: config.zIndex ?? 99999,
993
+ trigger: config.trigger ?? "auto",
994
+ attachToSelector: config.attachToSelector ?? "",
995
+ inset: config.inset ?? {},
996
+ respectSafeArea: config.respectSafeArea ?? true,
997
+ hideOnSelector: config.hideOnSelector ?? "",
998
+ hideOnRoutes: config.hideOnRoutes ?? [],
999
+ environments: config.environments ?? {},
1000
+ smartHide: config.smartHide ?? false,
1001
+ draggable: config.draggable ?? false,
1002
+ brandFooter: config.brandFooter ?? true,
1003
+ outdatedBanner: config.outdatedBanner ?? "auto"
1004
+ };
1005
+ this.callbacks = callbacks;
1006
+ this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
1007
+ this.host = document.createElement("div");
1008
+ this.host.id = "mushi-mushi-widget";
1009
+ this.shadow = this.host.attachShadow({ mode: "closed" });
1010
+ }
1011
+ sdkVersion;
847
1012
  host;
848
1013
  shadow;
849
1014
  config;
@@ -854,8 +1019,21 @@ var MushiWidget = class {
854
1019
  selectedCategory = null;
855
1020
  selectedIntent = null;
856
1021
  screenshotAttached = false;
1022
+ allowScreenshotRemove = true;
857
1023
  elementSelected = false;
858
1024
  submitting = false;
1025
+ triggerVisible = true;
1026
+ triggerShrunk = false;
1027
+ triggerHiddenByScroll = false;
1028
+ sdkFreshness = null;
1029
+ reporterReports = [];
1030
+ reporterComments = [];
1031
+ selectedReportId = null;
1032
+ reporterLoading = false;
1033
+ reporterError = null;
1034
+ attachedLaunchers = [];
1035
+ smartHideCleanup = null;
1036
+ smartHideTimer = null;
859
1037
  /** Captured at the moment of submit so the success ledger metadata
860
1038
  * ("REPORT · 14:23:07 JST") doesn't drift while the success step
861
1039
  * is on screen. */
@@ -866,45 +1044,42 @@ var MushiWidget = class {
866
1044
  * root) for up to ~3.3s after destroy. */
867
1045
  successTimer = null;
868
1046
  autoCloseTimer = null;
869
- constructor(config = {}, callbacks) {
870
- this.config = {
871
- position: config.position ?? "bottom-right",
872
- theme: config.theme ?? "auto",
873
- // Falsy-OR (NOT `??`) on purpose: `triggerText: ''` is semantically
874
- // nonsense — it would render a labelless, glyphless trigger button
875
- // that users can't see or aim at. Treat empty string the same as
876
- // omitted so any caller that wires this to a cleared form input or
877
- // pastes a legacy snippet that emitted `triggerText: ""` (see
878
- // apps/admin/src/lib/sdkSnippets.ts widgetLines history) still gets
879
- // the default 🐛 and a visible button.
880
- triggerText: config.triggerText || "\u{1F41B}",
881
- expandedTitle: config.expandedTitle ?? "",
882
- mode: config.mode ?? "conversational",
883
- locale: config.locale ?? "auto",
884
- zIndex: config.zIndex ?? 99999
885
- };
886
- this.callbacks = callbacks;
887
- this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
888
- this.host = document.createElement("div");
889
- this.host.id = "mushi-mushi-widget";
890
- this.shadow = this.host.attachShadow({ mode: "closed" });
891
- }
892
1047
  mount() {
1048
+ if (this.host.isConnected) return;
893
1049
  document.body.appendChild(this.host);
1050
+ this.syncAttachedLaunchers();
1051
+ this.syncSmartHide();
894
1052
  this.render();
895
1053
  }
1054
+ getIsMounted() {
1055
+ return this.host.isConnected;
1056
+ }
896
1057
  updateConfig(config = {}) {
897
1058
  this.config = {
898
1059
  ...this.config,
899
1060
  ...config.position ? { position: config.position } : {},
1061
+ ...config.anchor !== void 0 ? { anchor: config.anchor } : {},
900
1062
  ...config.theme ? { theme: config.theme } : {},
901
1063
  ...config.triggerText !== void 0 ? { triggerText: config.triggerText || "\u{1F41B}" } : {},
902
1064
  ...config.expandedTitle !== void 0 ? { expandedTitle: config.expandedTitle } : {},
903
1065
  ...config.mode ? { mode: config.mode } : {},
904
1066
  ...config.locale ? { locale: config.locale } : {},
905
- ...config.zIndex !== void 0 ? { zIndex: config.zIndex } : {}
1067
+ ...config.zIndex !== void 0 ? { zIndex: config.zIndex } : {},
1068
+ ...config.trigger ? { trigger: config.trigger } : {},
1069
+ ...config.attachToSelector !== void 0 ? { attachToSelector: config.attachToSelector } : {},
1070
+ ...config.inset !== void 0 ? { inset: config.inset } : {},
1071
+ ...config.respectSafeArea !== void 0 ? { respectSafeArea: config.respectSafeArea } : {},
1072
+ ...config.hideOnSelector !== void 0 ? { hideOnSelector: config.hideOnSelector } : {},
1073
+ ...config.hideOnRoutes !== void 0 ? { hideOnRoutes: config.hideOnRoutes } : {},
1074
+ ...config.environments !== void 0 ? { environments: config.environments } : {},
1075
+ ...config.smartHide !== void 0 ? { smartHide: config.smartHide } : {},
1076
+ ...config.draggable !== void 0 ? { draggable: config.draggable } : {},
1077
+ ...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
1078
+ ...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {}
906
1079
  };
907
1080
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
1081
+ this.syncAttachedLaunchers();
1082
+ this.syncSmartHide();
908
1083
  this.render();
909
1084
  }
910
1085
  open(options) {
@@ -935,14 +1110,46 @@ var MushiWidget = class {
935
1110
  getIsOpen() {
936
1111
  return this.isOpen;
937
1112
  }
1113
+ showTrigger() {
1114
+ this.triggerVisible = true;
1115
+ this.render();
1116
+ }
1117
+ hideTrigger() {
1118
+ this.triggerVisible = false;
1119
+ this.render();
1120
+ }
1121
+ setTrigger(trigger) {
1122
+ this.updateConfig({ trigger });
1123
+ }
1124
+ attachTo(selectorOrElement, options = {}) {
1125
+ const elements = typeof selectorOrElement === "string" ? Array.from(document.querySelectorAll(selectorOrElement)) : [selectorOrElement];
1126
+ const cleanups = elements.map((el) => {
1127
+ const onClick = (event) => {
1128
+ event.preventDefault();
1129
+ this.updateConfig(options);
1130
+ this.open();
1131
+ };
1132
+ el.addEventListener("click", onClick);
1133
+ return () => el.removeEventListener("click", onClick);
1134
+ });
1135
+ return () => cleanups.forEach((cleanup) => cleanup());
1136
+ }
938
1137
  setScreenshotAttached(attached) {
939
1138
  this.screenshotAttached = attached;
940
1139
  if (this.isOpen) this.render();
941
1140
  }
1141
+ setAllowScreenshotRemove(allow) {
1142
+ this.allowScreenshotRemove = allow;
1143
+ if (this.isOpen) this.render();
1144
+ }
942
1145
  setElementSelected(selected) {
943
1146
  this.elementSelected = selected;
944
1147
  if (this.isOpen) this.render();
945
1148
  }
1149
+ setSdkFreshness(info) {
1150
+ this.sdkFreshness = info;
1151
+ if (this.isOpen) this.render();
1152
+ }
946
1153
  destroy() {
947
1154
  if (this.successTimer !== null) {
948
1155
  clearTimeout(this.successTimer);
@@ -952,8 +1159,83 @@ var MushiWidget = class {
952
1159
  clearTimeout(this.autoCloseTimer);
953
1160
  this.autoCloseTimer = null;
954
1161
  }
1162
+ if (this.smartHideTimer !== null) {
1163
+ clearTimeout(this.smartHideTimer);
1164
+ this.smartHideTimer = null;
1165
+ }
1166
+ this.smartHideCleanup?.();
1167
+ this.smartHideCleanup = null;
1168
+ this.attachedLaunchers.forEach((cleanup) => cleanup());
1169
+ this.attachedLaunchers = [];
955
1170
  this.host.remove();
956
1171
  }
1172
+ syncAttachedLaunchers() {
1173
+ this.attachedLaunchers.forEach((cleanup) => cleanup());
1174
+ this.attachedLaunchers = [];
1175
+ if (this.config.trigger !== "attach" || !this.config.attachToSelector) return;
1176
+ if (typeof document === "undefined") return;
1177
+ this.attachedLaunchers.push(this.attachTo(this.config.attachToSelector));
1178
+ }
1179
+ syncSmartHide() {
1180
+ this.smartHideCleanup?.();
1181
+ this.smartHideCleanup = null;
1182
+ this.triggerShrunk = false;
1183
+ this.triggerHiddenByScroll = false;
1184
+ if (!this.config.smartHide || typeof window === "undefined") return;
1185
+ const smart = this.config.smartHide === true ? { onScroll: "shrink", onIdleMs: 900 } : this.config.smartHide;
1186
+ if (!smart.onScroll) return;
1187
+ const onScroll = () => {
1188
+ if (smart.onScroll === "hide") {
1189
+ this.triggerHiddenByScroll = true;
1190
+ } else {
1191
+ this.triggerShrunk = true;
1192
+ }
1193
+ this.render();
1194
+ if (this.smartHideTimer !== null) clearTimeout(this.smartHideTimer);
1195
+ this.smartHideTimer = setTimeout(() => {
1196
+ this.triggerHiddenByScroll = false;
1197
+ this.triggerShrunk = false;
1198
+ this.render();
1199
+ }, smart.onIdleMs ?? 900);
1200
+ };
1201
+ window.addEventListener("scroll", onScroll, { passive: true });
1202
+ this.smartHideCleanup = () => window.removeEventListener("scroll", onScroll);
1203
+ }
1204
+ shouldRenderTrigger() {
1205
+ if (!this.triggerVisible) return false;
1206
+ if (this.triggerHiddenByScroll) return false;
1207
+ if (this.config.trigger === "manual" || this.config.trigger === "hidden" || this.config.trigger === "attach") {
1208
+ return false;
1209
+ }
1210
+ if (this.isMobileSmartHidden()) return false;
1211
+ if (this.isRouteHidden()) return false;
1212
+ if (this.config.hideOnSelector && document.querySelector(this.config.hideOnSelector)) return false;
1213
+ const action = this.config.environments[this.detectEnvironment()];
1214
+ return action !== "never" && action !== "manual";
1215
+ }
1216
+ effectiveTrigger() {
1217
+ if (!this.config.smartHide || typeof window === "undefined") return this.config.trigger;
1218
+ const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
1219
+ if (window.matchMedia("(max-width: 768px)").matches && smart.onMobile === "edge-tab") {
1220
+ return "edge-tab";
1221
+ }
1222
+ return this.config.trigger;
1223
+ }
1224
+ isMobileSmartHidden() {
1225
+ if (!this.config.smartHide || typeof window === "undefined") return false;
1226
+ const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
1227
+ return window.matchMedia("(max-width: 768px)").matches && smart.onMobile === "hide";
1228
+ }
1229
+ detectEnvironment() {
1230
+ const host = typeof location !== "undefined" ? location.hostname : "";
1231
+ if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "development";
1232
+ if (/\b(staging|stage|preview|dev)\b/i.test(host)) return "staging";
1233
+ return "production";
1234
+ }
1235
+ isRouteHidden() {
1236
+ if (!this.config.hideOnRoutes.length || typeof location === "undefined") return false;
1237
+ return this.config.hideOnRoutes.some((route) => location.pathname.includes(route));
1238
+ }
957
1239
  getTheme() {
958
1240
  if (this.config.theme !== "auto") return this.config.theme;
959
1241
  if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
@@ -969,31 +1251,59 @@ var MushiWidget = class {
969
1251
  const style = document.createElement("style");
970
1252
  style.textContent = getWidgetStyles(theme);
971
1253
  this.shadow.appendChild(style);
972
- const trigger = document.createElement("button");
973
- trigger.className = `mushi-trigger ${pos}`;
974
- trigger.textContent = this.config.triggerText;
975
- trigger.setAttribute("aria-label", t.widget.trigger);
976
- trigger.setAttribute("aria-haspopup", "dialog");
977
- trigger.setAttribute("aria-expanded", String(this.isOpen));
978
- trigger.style.zIndex = String(this.config.zIndex);
979
- trigger.addEventListener("click", () => {
980
- if (this.isOpen) this.close();
981
- else this.open();
982
- });
983
- this.shadow.appendChild(trigger);
1254
+ if (this.shouldRenderTrigger()) {
1255
+ const effectiveTrigger = this.effectiveTrigger();
1256
+ const trigger = document.createElement("button");
1257
+ trigger.className = `mushi-trigger ${pos}${effectiveTrigger === "edge-tab" ? " edge-tab" : ""}${this.triggerShrunk ? " shrunk" : ""}`;
1258
+ trigger.textContent = this.config.triggerText;
1259
+ trigger.setAttribute("aria-label", t.widget.trigger);
1260
+ trigger.setAttribute("aria-haspopup", "dialog");
1261
+ trigger.setAttribute("aria-expanded", String(this.isOpen));
1262
+ trigger.style.zIndex = String(this.config.zIndex);
1263
+ this.applyInsetVars(trigger);
1264
+ trigger.addEventListener("click", () => {
1265
+ if (this.isOpen) this.close();
1266
+ else this.open();
1267
+ });
1268
+ this.shadow.appendChild(trigger);
1269
+ }
984
1270
  const panel = document.createElement("div");
985
1271
  panel.className = `mushi-panel ${pos}${this.isOpen ? " open" : " closed"}`;
986
1272
  panel.setAttribute("role", "dialog");
987
1273
  panel.setAttribute("aria-modal", "true");
988
1274
  panel.setAttribute("aria-label", t.widget.title);
989
1275
  panel.style.zIndex = String(this.config.zIndex + 1);
1276
+ this.applyInsetVars(panel);
990
1277
  if (this.isOpen) {
991
- panel.innerHTML = this.renderStep();
1278
+ panel.innerHTML = `${this.renderOutdatedBanner()}${this.renderStep()}${this.renderBrandFooter()}`;
992
1279
  this.shadow.appendChild(panel);
993
1280
  this.attachHandlers(panel);
994
1281
  this.trapFocus(panel);
995
1282
  }
996
1283
  }
1284
+ applyInsetVars(el) {
1285
+ const { anchor } = this.config;
1286
+ if (anchor && Object.keys(anchor).length > 0) {
1287
+ ["top", "right", "bottom", "left"].forEach((edge) => {
1288
+ const value = anchor[edge];
1289
+ if (value !== void 0) el.style.setProperty(`--mushi-${edge}`, value);
1290
+ });
1291
+ el.style.setProperty("--mushi-safe-area", this.config.respectSafeArea ? "1" : "0");
1292
+ return;
1293
+ }
1294
+ const { inset } = this.config;
1295
+ if (!this.config.respectSafeArea) {
1296
+ ["top", "right", "bottom", "left"].forEach((edge) => {
1297
+ if (inset[edge] === void 0) el.style.setProperty(`--mushi-${edge}`, "24px");
1298
+ });
1299
+ }
1300
+ ["top", "right", "bottom", "left"].forEach((edge) => {
1301
+ const value = inset[edge];
1302
+ if (value === void 0) return;
1303
+ el.style.setProperty(`--mushi-${edge}`, value === "auto" ? "auto" : `${value}px`);
1304
+ });
1305
+ el.style.setProperty("--mushi-safe-area", this.config.respectSafeArea ? "1" : "0");
1306
+ }
997
1307
  renderStep() {
998
1308
  switch (this.step) {
999
1309
  case "category":
@@ -1004,8 +1314,29 @@ var MushiWidget = class {
1004
1314
  return this.renderDetailsStep();
1005
1315
  case "success":
1006
1316
  return this.renderSuccessStep();
1317
+ case "reports":
1318
+ return this.renderReportsStep();
1319
+ case "report-detail":
1320
+ return this.renderReportDetailStep();
1007
1321
  }
1008
1322
  }
1323
+ renderOutdatedBanner() {
1324
+ if (!this.sdkFreshness) return "";
1325
+ if (this.config.outdatedBanner === "off" || this.config.outdatedBanner === "console-only") return "";
1326
+ const { latest, current, deprecated, message } = this.sdkFreshness;
1327
+ if (!latest && !deprecated) return "";
1328
+ return `
1329
+ <div class="mushi-outdated" role="status">
1330
+ <strong>Mushi SDK ${escapeHtml(current)}</strong>
1331
+ ${latest ? `latest is ${escapeHtml(latest)}.` : "needs attention."}
1332
+ ${message ? `<span>${escapeHtml(message)}</span>` : ""}
1333
+ </div>
1334
+ `;
1335
+ }
1336
+ renderBrandFooter() {
1337
+ if (this.config.brandFooter === false) return "";
1338
+ return `<div class="mushi-brand-footer">Powered by Mushi v${escapeHtml(this.sdkVersion)}</div>`;
1339
+ }
1009
1340
  /**
1010
1341
  * Editorial masthead. Always carries:
1011
1342
  * • the brand mark (虫 kanji on vermillion, "MUSHI" in mono above)
@@ -1064,11 +1395,61 @@ var MushiWidget = class {
1064
1395
  return `
1065
1396
  ${this.renderHeader({ title: t.step1.heading, step: STEP_NUMBER.category })}
1066
1397
  <div class="mushi-body" role="radiogroup" aria-label="${t.step1.heading}">
1398
+ <button type="button" class="mushi-option-btn mushi-reports-entry" data-action="reports">
1399
+ <span class="mushi-option-icon" aria-hidden="true">\u{1F4EC}</span>
1400
+ <div class="mushi-option-text">
1401
+ <span class="mushi-option-label">Your reports${this.unreadCount() ? ` (${this.unreadCount()} new)` : ""}</span>
1402
+ <span class="mushi-option-desc">See status, developer replies, and respond</span>
1403
+ </div>
1404
+ <span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
1405
+ </button>
1067
1406
  ${categories}
1068
1407
  </div>
1069
1408
  ${this.renderStepIndicator(STEP_NUMBER.category)}
1070
1409
  `;
1071
1410
  }
1411
+ renderReportsStep() {
1412
+ const reports = this.reporterReports.map((report) => `
1413
+ <button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
1414
+ <span class="mushi-report-status">${escapeHtml(report.status)}</span>
1415
+ <span class="mushi-report-title">${escapeHtml(report.summary ?? report.description ?? `Report ${report.id.slice(0, 8)}`)}</span>
1416
+ ${report.unread_count ? `<b>${report.unread_count}</b>` : ""}
1417
+ </button>
1418
+ `).join("");
1419
+ return `
1420
+ ${this.renderHeader({ title: "Your reports", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
1421
+ <div class="mushi-body">
1422
+ ${this.reporterLoading ? '<p class="mushi-muted">Loading reports\u2026</p>' : ""}
1423
+ ${this.reporterError ? `<p class="mushi-error-inline">${escapeHtml(this.reporterError)}</p>` : ""}
1424
+ ${reports || (!this.reporterLoading ? '<p class="mushi-muted">No reports from this browser yet.</p>' : "")}
1425
+ </div>
1426
+ `;
1427
+ }
1428
+ renderReportDetailStep() {
1429
+ const report = this.reporterReports.find((r) => r.id === this.selectedReportId);
1430
+ const comments = this.reporterComments.map((comment) => `
1431
+ <div class="mushi-thread-comment ${comment.author_kind}">
1432
+ <strong>${escapeHtml(comment.author_kind === "reporter" ? "You" : comment.author_name ?? "Developer")}</strong>
1433
+ <p>${escapeHtml(comment.body)}</p>
1434
+ </div>
1435
+ `).join("");
1436
+ return `
1437
+ ${this.renderHeader({ title: "Report thread", showBack: true, eyebrow: "Mushi \xB7 Inbox" })}
1438
+ <div class="mushi-body">
1439
+ <div class="mushi-thread-summary">
1440
+ <span>${escapeHtml(report?.status ?? "unknown")}</span>
1441
+ <p>${escapeHtml(report?.summary ?? report?.description ?? "Report details")}</p>
1442
+ </div>
1443
+ <div class="mushi-thread">
1444
+ ${this.reporterLoading ? '<p class="mushi-muted">Loading thread\u2026</p>' : comments || '<p class="mushi-muted">No developer replies yet.</p>'}
1445
+ </div>
1446
+ <textarea class="mushi-textarea" data-role="reporter-reply" rows="3" placeholder="Reply to the developer\u2026"></textarea>
1447
+ <button type="button" class="mushi-submit" data-action="reporter-reply">
1448
+ <span>Reply</span><span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
1449
+ </button>
1450
+ </div>
1451
+ `;
1452
+ }
1072
1453
  renderIntentStep() {
1073
1454
  const t = this.locale;
1074
1455
  const cat = this.selectedCategory;
@@ -1108,6 +1489,7 @@ var MushiWidget = class {
1108
1489
  <button type="button" class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
1109
1490
  \u{1F4F8} ${this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton}
1110
1491
  </button>
1492
+ ${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot">\u2715 Remove screenshot</button>' : ""}
1111
1493
  <button type="button" class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
1112
1494
  \u{1F3AF} ${this.elementSelected ? t.step3.elementSelected : t.step3.elementButton}
1113
1495
  </button>
@@ -1160,9 +1542,26 @@ var MushiWidget = class {
1160
1542
  } else if (this.step === "details") {
1161
1543
  this.step = "intent";
1162
1544
  this.selectedIntent = null;
1545
+ } else if (this.step === "reports") {
1546
+ this.step = "category";
1547
+ } else if (this.step === "report-detail") {
1548
+ this.step = "reports";
1549
+ this.selectedReportId = null;
1163
1550
  }
1164
1551
  this.render();
1165
1552
  });
1553
+ panel.querySelector('[data-action="reports"]')?.addEventListener("click", () => {
1554
+ void this.loadReporterReports();
1555
+ });
1556
+ panel.querySelectorAll("[data-report-id]").forEach((btn) => {
1557
+ btn.addEventListener("click", () => {
1558
+ const reportId = btn.dataset.reportId;
1559
+ if (reportId) void this.loadReporterComments(reportId);
1560
+ });
1561
+ });
1562
+ panel.querySelector('[data-action="reporter-reply"]')?.addEventListener("click", () => {
1563
+ void this.submitReporterReply(panel);
1564
+ });
1166
1565
  panel.querySelectorAll("[data-category]").forEach((btn) => {
1167
1566
  btn.addEventListener("click", () => {
1168
1567
  this.selectedCategory = btn.dataset.category;
@@ -1180,6 +1579,9 @@ var MushiWidget = class {
1180
1579
  panel.querySelector('[data-action="screenshot"]')?.addEventListener("click", () => {
1181
1580
  this.callbacks.onScreenshotRequest();
1182
1581
  });
1582
+ panel.querySelector('[data-action="remove-screenshot"]')?.addEventListener("click", () => {
1583
+ this.callbacks.onScreenshotRemove?.();
1584
+ });
1183
1585
  panel.querySelector('[data-action="element"]')?.addEventListener("click", () => {
1184
1586
  this.callbacks.onElementSelectorRequest?.();
1185
1587
  });
@@ -1237,6 +1639,57 @@ var MushiWidget = class {
1237
1639
  if (focusable.length > 0) focusable[0].focus();
1238
1640
  });
1239
1641
  }
1642
+ unreadCount() {
1643
+ return this.reporterReports.reduce((sum, report) => sum + (report.unread_count ?? 0), 0);
1644
+ }
1645
+ async loadReporterReports() {
1646
+ this.step = "reports";
1647
+ this.reporterLoading = true;
1648
+ this.reporterError = null;
1649
+ this.render();
1650
+ try {
1651
+ this.reporterReports = await this.callbacks.onReporterReportsRequest?.() ?? [];
1652
+ } catch (err) {
1653
+ this.reporterError = err instanceof Error ? err.message : "Could not load reports.";
1654
+ } finally {
1655
+ this.reporterLoading = false;
1656
+ this.render();
1657
+ }
1658
+ }
1659
+ async loadReporterComments(reportId) {
1660
+ this.selectedReportId = reportId;
1661
+ this.step = "report-detail";
1662
+ this.reporterLoading = true;
1663
+ this.reporterError = null;
1664
+ this.render();
1665
+ try {
1666
+ this.reporterComments = await this.callbacks.onReporterCommentsRequest?.(reportId) ?? [];
1667
+ } catch (err) {
1668
+ this.reporterError = err instanceof Error ? err.message : "Could not load thread.";
1669
+ } finally {
1670
+ this.reporterLoading = false;
1671
+ this.render();
1672
+ }
1673
+ }
1674
+ async submitReporterReply(panel) {
1675
+ const reportId = this.selectedReportId;
1676
+ const textarea = panel.querySelector('[data-role="reporter-reply"]');
1677
+ const replyButton = panel.querySelector('[data-action="reporter-reply"]');
1678
+ const body = textarea?.value.trim() ?? "";
1679
+ if (!reportId || !body || this.reporterLoading) return;
1680
+ this.reporterLoading = true;
1681
+ if (replyButton) replyButton.disabled = true;
1682
+ this.render();
1683
+ try {
1684
+ await this.callbacks.onReporterReply?.(reportId, body);
1685
+ if (textarea) textarea.value = "";
1686
+ await this.loadReporterComments(reportId);
1687
+ } catch (err) {
1688
+ this.reporterError = err instanceof Error ? err.message : "Could not send reply.";
1689
+ this.reporterLoading = false;
1690
+ this.render();
1691
+ }
1692
+ }
1240
1693
  };
1241
1694
 
1242
1695
  // src/capture/console.ts
@@ -1288,35 +1741,109 @@ function createConsoleCapture() {
1288
1741
  }
1289
1742
  };
1290
1743
  }
1744
+ var DEFAULT_INTERNAL_URL_MATCHERS = [
1745
+ /\/v1\/sdk(?:\/|$)/,
1746
+ /\/v1\/reports(?:\/|$)/,
1747
+ /\/v1\/notifications(?:\/|$)/,
1748
+ /\/v1\/reputation(?:\/|$)/
1749
+ ];
1750
+ function getRequestUrl(input) {
1751
+ if (typeof input === "string") return input;
1752
+ if (input instanceof URL) return input.href;
1753
+ return input.url;
1754
+ }
1755
+ function getInternalRequestKind(input, init) {
1756
+ const marker = init?.[core.MUSHI_INTERNAL_INIT_MARKER];
1757
+ if (marker) return marker;
1758
+ const initHeader = readHeader(init?.headers, core.MUSHI_INTERNAL_HEADER);
1759
+ if (initHeader) return initHeader;
1760
+ if (typeof Request !== "undefined" && input instanceof Request) {
1761
+ const requestHeader = input.headers.get(core.MUSHI_INTERNAL_HEADER);
1762
+ if (requestHeader) return requestHeader;
1763
+ }
1764
+ return null;
1765
+ }
1766
+ function shouldIgnoreMushiUrl(url, options = {}) {
1767
+ const matchers = [...DEFAULT_INTERNAL_URL_MATCHERS, ...options.ignoreUrls ?? []];
1768
+ if (matchers.some((matcher) => matchesUrl(url, matcher))) return true;
1769
+ const endpoint = normalizeUrlPrefix(options.apiEndpoint);
1770
+ return endpoint ? normalizeComparableUrl(url).startsWith(endpoint) : false;
1771
+ }
1772
+ function matchesUrl(url, matcher) {
1773
+ if (typeof matcher === "string") {
1774
+ return normalizeComparableUrl(url).includes(matcher);
1775
+ }
1776
+ matcher.lastIndex = 0;
1777
+ return matcher.test(url);
1778
+ }
1779
+ function normalizeUrlPrefix(url) {
1780
+ if (!url) return null;
1781
+ return normalizeComparableUrl(url).replace(/\/+$/, "");
1782
+ }
1783
+ function isLocalhostEndpoint(url) {
1784
+ if (!url) return false;
1785
+ try {
1786
+ const parsed = new URL(url);
1787
+ return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" || parsed.hostname.endsWith(".localhost");
1788
+ } catch {
1789
+ return /\blocalhost\b|127\.0\.0\.1/.test(url);
1790
+ }
1791
+ }
1792
+ function normalizeComparableUrl(url) {
1793
+ try {
1794
+ return new URL(url, typeof location !== "undefined" ? location.href : "http://localhost").href;
1795
+ } catch {
1796
+ return url;
1797
+ }
1798
+ }
1799
+ function readHeader(headers, name) {
1800
+ if (!headers) return null;
1801
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
1802
+ return headers.get(name);
1803
+ }
1804
+ if (Array.isArray(headers)) {
1805
+ const found = headers.find(([key]) => key.toLowerCase() === name.toLowerCase());
1806
+ return found?.[1] ?? null;
1807
+ }
1808
+ const record = headers;
1809
+ return record[name] ?? record[name.toLowerCase()] ?? null;
1810
+ }
1291
1811
 
1292
1812
  // src/capture/network.ts
1293
1813
  var MAX_ENTRIES2 = 30;
1294
- function createNetworkCapture() {
1814
+ function createNetworkCapture(options = {}) {
1295
1815
  const entries = [];
1296
1816
  const originalFetch = globalThis.fetch;
1817
+ let activeOptions = options;
1297
1818
  globalThis.fetch = async function mushiFetchInterceptor(input, init) {
1298
1819
  const startTime = Date.now();
1299
1820
  const method = init?.method?.toUpperCase() ?? "GET";
1300
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1821
+ const url = getRequestUrl(input);
1822
+ const internalKind = getInternalRequestKind(input, init);
1823
+ const shouldRecord = !internalKind && !shouldIgnoreMushiUrl(url, activeOptions);
1301
1824
  try {
1302
1825
  const response = await originalFetch.call(globalThis, input, init);
1303
- addEntry({
1304
- method,
1305
- url: truncateUrl(url),
1306
- status: response.status,
1307
- duration: Date.now() - startTime,
1308
- timestamp: startTime
1309
- });
1826
+ if (shouldRecord) {
1827
+ addEntry({
1828
+ method,
1829
+ url: truncateUrl(url),
1830
+ status: response.status,
1831
+ duration: Date.now() - startTime,
1832
+ timestamp: startTime
1833
+ });
1834
+ }
1310
1835
  return response;
1311
1836
  } catch (error) {
1312
- addEntry({
1313
- method,
1314
- url: truncateUrl(url),
1315
- status: 0,
1316
- duration: Date.now() - startTime,
1317
- timestamp: startTime,
1318
- error: error instanceof Error ? error.message : "Network error"
1319
- });
1837
+ if (shouldRecord) {
1838
+ addEntry({
1839
+ method,
1840
+ url: truncateUrl(url),
1841
+ status: 0,
1842
+ duration: Date.now() - startTime,
1843
+ timestamp: startTime,
1844
+ error: error instanceof Error ? error.message : "Network error"
1845
+ });
1846
+ }
1320
1847
  throw error;
1321
1848
  }
1322
1849
  };
@@ -1333,6 +1860,9 @@ function createNetworkCapture() {
1333
1860
  clear() {
1334
1861
  entries.length = 0;
1335
1862
  },
1863
+ updateOptions(nextOptions) {
1864
+ activeOptions = nextOptions;
1865
+ },
1336
1866
  destroy() {
1337
1867
  globalThis.fetch = originalFetch;
1338
1868
  }
@@ -1352,7 +1882,8 @@ function truncateUrl(url) {
1352
1882
  }
1353
1883
 
1354
1884
  // src/capture/screenshot.ts
1355
- function createScreenshotCapture() {
1885
+ function createScreenshotCapture(options = {}) {
1886
+ let activeOptions = options;
1356
1887
  async function take() {
1357
1888
  try {
1358
1889
  if (typeof document === "undefined") return null;
@@ -1365,11 +1896,12 @@ function createScreenshotCapture() {
1365
1896
  canvas.width = width * dpr;
1366
1897
  canvas.height = height * dpr;
1367
1898
  ctx.scale(dpr, dpr);
1899
+ const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
1368
1900
  const svgData = `
1369
1901
  <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
1370
1902
  <foreignObject width="100%" height="100%">
1371
1903
  <div xmlns="http://www.w3.org/1999/xhtml">
1372
- ${new XMLSerializer().serializeToString(document.documentElement)}
1904
+ ${new XMLSerializer().serializeToString(safeDocument)}
1373
1905
  </div>
1374
1906
  </foreignObject>
1375
1907
  </svg>
@@ -1398,7 +1930,46 @@ function createScreenshotCapture() {
1398
1930
  return null;
1399
1931
  }
1400
1932
  }
1401
- return { take };
1933
+ return {
1934
+ take,
1935
+ updateOptions(nextOptions) {
1936
+ activeOptions = nextOptions;
1937
+ }
1938
+ };
1939
+ }
1940
+ function buildPrivacySafeDocument(privacy) {
1941
+ const clone = document.documentElement.cloneNode(true);
1942
+ for (const selector of privacy?.blockSelectors ?? []) {
1943
+ for (const el of safeQueryAll(clone, selector)) {
1944
+ el.remove();
1945
+ }
1946
+ }
1947
+ for (const selector of privacy?.maskSelectors ?? []) {
1948
+ for (const el of safeQueryAll(clone, selector)) {
1949
+ maskElement(el);
1950
+ }
1951
+ }
1952
+ return clone;
1953
+ }
1954
+ function safeQueryAll(root, selector) {
1955
+ try {
1956
+ return Array.from(root.querySelectorAll(selector));
1957
+ } catch {
1958
+ return [];
1959
+ }
1960
+ }
1961
+ function maskElement(el) {
1962
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1963
+ el.value = "";
1964
+ el.setAttribute("value", "");
1965
+ el.setAttribute("placeholder", "\u2022\u2022\u2022\u2022");
1966
+ }
1967
+ el.textContent = el.children.length === 0 ? "\u2022\u2022\u2022\u2022" : el.textContent;
1968
+ el.setAttribute(
1969
+ "style",
1970
+ `${el.getAttribute("style") ?? ""};background:#8f8f8f!important;color:transparent!important;text-shadow:none!important;`
1971
+ );
1972
+ el.setAttribute("data-mushi-masked", "true");
1402
1973
  }
1403
1974
 
1404
1975
  // src/capture/performance.ts
@@ -1587,6 +2158,108 @@ function createElementSelector() {
1587
2158
  return { activate, deactivate, isActive: () => active };
1588
2159
  }
1589
2160
 
2161
+ // src/capture/timeline.ts
2162
+ var MAX_TIMELINE_ENTRIES = 120;
2163
+ function createTimelineCapture() {
2164
+ const entries = [];
2165
+ const originalPushState = history.pushState;
2166
+ const originalReplaceState = history.replaceState;
2167
+ const handlePopState = () => recordRoute("popstate");
2168
+ const handleHashChange = () => recordRoute("hashchange");
2169
+ recordRoute("initial");
2170
+ function record(entry) {
2171
+ entries.push(entry);
2172
+ if (entries.length > MAX_TIMELINE_ENTRIES) entries.shift();
2173
+ }
2174
+ function recordRoute(source) {
2175
+ if (typeof location === "undefined") return;
2176
+ record({
2177
+ ts: Date.now(),
2178
+ kind: "route",
2179
+ payload: {
2180
+ source,
2181
+ route: `${location.pathname}${location.search}${location.hash}`,
2182
+ href: location.href
2183
+ }
2184
+ });
2185
+ }
2186
+ function handleClick(event) {
2187
+ const target = event.target instanceof Element ? event.target : null;
2188
+ if (!target) return;
2189
+ const el = target.closest('button,a,[role="button"],input,textarea,select,[data-mushi-track]') ?? target;
2190
+ record({
2191
+ ts: Date.now(),
2192
+ kind: "click",
2193
+ payload: {
2194
+ tag: el.tagName.toLowerCase(),
2195
+ id: el.id || void 0,
2196
+ text: textSnippet(el)
2197
+ }
2198
+ });
2199
+ }
2200
+ history.pushState = function mushiPushState(...args) {
2201
+ const result = originalPushState.apply(this, args);
2202
+ recordRoute("pushState");
2203
+ return result;
2204
+ };
2205
+ history.replaceState = function mushiReplaceState(...args) {
2206
+ const result = originalReplaceState.apply(this, args);
2207
+ recordRoute("replaceState");
2208
+ return result;
2209
+ };
2210
+ window.addEventListener("popstate", handlePopState);
2211
+ window.addEventListener("hashchange", handleHashChange);
2212
+ document.addEventListener("click", handleClick, true);
2213
+ return {
2214
+ setScreen(screen) {
2215
+ record({
2216
+ ts: Date.now(),
2217
+ kind: "screen",
2218
+ payload: screen
2219
+ });
2220
+ },
2221
+ getEntries(input = {}) {
2222
+ const merged = [
2223
+ ...entries,
2224
+ ...(input.consoleLogs ?? []).map((log) => ({
2225
+ ts: log.timestamp,
2226
+ kind: "log",
2227
+ payload: {
2228
+ level: log.level,
2229
+ message: log.message
2230
+ }
2231
+ })),
2232
+ ...(input.networkLogs ?? []).map((network) => ({
2233
+ ts: network.timestamp,
2234
+ kind: "request",
2235
+ payload: {
2236
+ method: network.method,
2237
+ url: network.url,
2238
+ status: network.status,
2239
+ duration: network.duration,
2240
+ error: network.error
2241
+ }
2242
+ }))
2243
+ ].sort((a, b) => a.ts - b.ts);
2244
+ return merged.slice(-MAX_TIMELINE_ENTRIES);
2245
+ },
2246
+ clear() {
2247
+ entries.length = 0;
2248
+ },
2249
+ destroy() {
2250
+ history.pushState = originalPushState;
2251
+ history.replaceState = originalReplaceState;
2252
+ window.removeEventListener("popstate", handlePopState);
2253
+ window.removeEventListener("hashchange", handleHashChange);
2254
+ document.removeEventListener("click", handleClick, true);
2255
+ }
2256
+ };
2257
+ }
2258
+ function textSnippet(el) {
2259
+ const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2260
+ return text ? text.slice(0, 80) : void 0;
2261
+ }
2262
+
1590
2263
  // src/sentry.ts
1591
2264
  function getSentryGlobal() {
1592
2265
  try {
@@ -1673,36 +2346,25 @@ function setupProactiveTriggers(callbacks, config = {}) {
1673
2346
  } catch {
1674
2347
  }
1675
2348
  }
1676
- if (config.apiCascade !== false) {
2349
+ const apiCascade = normalizeApiCascadeConfig(config.apiCascade);
2350
+ if (apiCascade.enabled) {
1677
2351
  const failedRequests = [];
1678
2352
  const origFetch = globalThis.fetch;
1679
2353
  globalThis.fetch = async function(...args) {
2354
+ const [input, init] = args;
2355
+ const url = getRequestUrl(input);
2356
+ const ignoreFailure = Boolean(getInternalRequestKind(input, init)) || shouldIgnoreMushiUrl(url, {
2357
+ apiEndpoint: config.apiEndpoint,
2358
+ ignoreUrls: apiCascade.ignoreUrls
2359
+ });
1680
2360
  try {
1681
2361
  const res = await origFetch.apply(this, args);
1682
- if (!res.ok && res.status >= 400) {
1683
- const now = Date.now();
1684
- failedRequests.push(now);
1685
- const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1686
- if (recentFailures.length >= 3) {
1687
- callbacks.onTrigger("api_cascade", {
1688
- failureCount: recentFailures.length,
1689
- windowMs: 1e4
1690
- });
1691
- failedRequests.length = 0;
1692
- }
2362
+ if (!ignoreFailure && !res.ok && res.status >= 400) {
2363
+ recordApiFailure(failedRequests, callbacks);
1693
2364
  }
1694
2365
  return res;
1695
2366
  } catch (err) {
1696
- const now = Date.now();
1697
- failedRequests.push(now);
1698
- const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1699
- if (recentFailures.length >= 3) {
1700
- callbacks.onTrigger("api_cascade", {
1701
- failureCount: recentFailures.length,
1702
- windowMs: 1e4
1703
- });
1704
- failedRequests.length = 0;
1705
- }
2367
+ if (!ignoreFailure) recordApiFailure(failedRequests, callbacks);
1706
2368
  throw err;
1707
2369
  }
1708
2370
  };
@@ -1737,6 +2399,28 @@ function setupProactiveTriggers(callbacks, config = {}) {
1737
2399
  }
1738
2400
  };
1739
2401
  }
2402
+ function normalizeApiCascadeConfig(config) {
2403
+ if (config === false) return { enabled: false, ignoreUrls: [] };
2404
+ if (config && typeof config === "object") {
2405
+ return {
2406
+ enabled: config.enabled !== false,
2407
+ ignoreUrls: config.ignoreUrls ?? []
2408
+ };
2409
+ }
2410
+ return { enabled: true, ignoreUrls: [] };
2411
+ }
2412
+ function recordApiFailure(failedRequests, callbacks) {
2413
+ const now = Date.now();
2414
+ failedRequests.push(now);
2415
+ const recentFailures = failedRequests.filter((t) => now - t < 1e4);
2416
+ if (recentFailures.length >= 3) {
2417
+ callbacks.onTrigger("api_cascade", {
2418
+ failureCount: recentFailures.length,
2419
+ windowMs: 1e4
2420
+ });
2421
+ failedRequests.length = 0;
2422
+ }
2423
+ }
1740
2424
 
1741
2425
  // src/proactive-manager.ts
1742
2426
  var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
@@ -1789,6 +2473,10 @@ function createProactiveManager(config = {}) {
1789
2473
  return { shouldShow, recordDismissal, recordSubmission, reset };
1790
2474
  }
1791
2475
 
2476
+ // src/version.ts
2477
+ var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
2478
+ var MUSHI_SDK_VERSION = "0.8.0" ;
2479
+
1792
2480
  // src/mushi.ts
1793
2481
  var instance = null;
1794
2482
  var Mushi = class {
@@ -1818,18 +2506,21 @@ var Mushi = class {
1818
2506
  instance?.destroy();
1819
2507
  instance = null;
1820
2508
  }
2509
+ static diagnose() {
2510
+ return instance?.diagnose() ?? diagnoseWithoutInstance();
2511
+ }
1821
2512
  };
1822
2513
  function createInstance(config) {
1823
- const bootstrapConfig = config;
1824
- let activeConfig = config;
2514
+ const bootstrapConfig = applyPresetConfig(config);
2515
+ let activeConfig = bootstrapConfig;
1825
2516
  const log = config.debug ?? false ? core.createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : core.noopLogger;
1826
2517
  const apiClient = core.createApiClient({
1827
- projectId: config.projectId,
1828
- apiKey: config.apiKey,
1829
- ...config.apiEndpoint ? { apiEndpoint: config.apiEndpoint } : {}
2518
+ projectId: bootstrapConfig.projectId,
2519
+ apiKey: bootstrapConfig.apiKey,
2520
+ ...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
1830
2521
  });
1831
- const preFilter = core.createPreFilter(config.preFilter);
1832
- const offlineQueue = core.createOfflineQueue(config.offline);
2522
+ const preFilter = core.createPreFilter(bootstrapConfig.preFilter);
2523
+ const offlineQueue = core.createOfflineQueue(bootstrapConfig.offline);
1833
2524
  const rateLimiter = core.createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
1834
2525
  const piiScrubber = core.createPiiScrubber();
1835
2526
  let consoleCap = null;
@@ -1837,6 +2528,8 @@ function createInstance(config) {
1837
2528
  let perfCap = null;
1838
2529
  let screenshotCap = null;
1839
2530
  let elementSelector = null;
2531
+ const timelineCap = createTimelineCapture();
2532
+ let widget;
1840
2533
  function syncCaptureModules() {
1841
2534
  if (activeConfig.capture?.console !== false) {
1842
2535
  consoleCap ??= createConsoleCapture();
@@ -1845,7 +2538,15 @@ function createInstance(config) {
1845
2538
  consoleCap = null;
1846
2539
  }
1847
2540
  if (activeConfig.capture?.network !== false) {
1848
- networkCap ??= createNetworkCapture();
2541
+ const networkOptions = {
2542
+ apiEndpoint: resolveApiEndpoint(activeConfig),
2543
+ ignoreUrls: activeConfig.capture?.ignoreUrls
2544
+ };
2545
+ if (networkCap) {
2546
+ networkCap.updateOptions(networkOptions);
2547
+ } else {
2548
+ networkCap = createNetworkCapture(networkOptions);
2549
+ }
1849
2550
  } else {
1850
2551
  networkCap?.destroy();
1851
2552
  networkCap = null;
@@ -1856,8 +2557,18 @@ function createInstance(config) {
1856
2557
  perfCap?.destroy();
1857
2558
  perfCap = null;
1858
2559
  }
1859
- screenshotCap = activeConfig.capture?.screenshot !== "off" ? screenshotCap ?? createScreenshotCapture() : null;
2560
+ if (activeConfig.capture?.screenshot !== "off") {
2561
+ const screenshotOptions = { privacy: activeConfig.privacy };
2562
+ if (screenshotCap) {
2563
+ screenshotCap.updateOptions(screenshotOptions);
2564
+ } else {
2565
+ screenshotCap = createScreenshotCapture(screenshotOptions);
2566
+ }
2567
+ } else {
2568
+ screenshotCap = null;
2569
+ }
1860
2570
  if (!screenshotCap) pendingScreenshot = null;
2571
+ widget.setAllowScreenshotRemove(activeConfig.privacy?.allowUserRemoveScreenshot !== false);
1861
2572
  if (activeConfig.capture?.elementSelector !== false) {
1862
2573
  elementSelector ??= createElementSelector();
1863
2574
  } else {
@@ -1873,10 +2584,10 @@ function createInstance(config) {
1873
2584
  let pendingScreenshot = null;
1874
2585
  let pendingElement = null;
1875
2586
  let pendingProactiveTrigger = null;
2587
+ let runtimeConfigLoaded = false;
1876
2588
  let userInfo = null;
1877
2589
  const customMetadata = {};
1878
- syncCaptureModules();
1879
- const widget = new MushiWidget(config.widget, {
2590
+ widget = new MushiWidget(bootstrapConfig.widget, {
1880
2591
  onSubmit: async ({ category, description, intent }) => {
1881
2592
  log.info("Report submitted", { category, intent });
1882
2593
  proactiveManager?.recordSubmission();
@@ -1903,6 +2614,11 @@ function createInstance(config) {
1903
2614
  pendingScreenshot = await screenshotCap.take();
1904
2615
  widget.setScreenshotAttached(pendingScreenshot !== null);
1905
2616
  },
2617
+ onScreenshotRemove: () => {
2618
+ log.debug("Screenshot attachment removed");
2619
+ pendingScreenshot = null;
2620
+ widget.setScreenshotAttached(false);
2621
+ },
1906
2622
  onElementSelectorRequest: async () => {
1907
2623
  if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
1908
2624
  log.debug("Element selector activated");
@@ -1912,8 +2628,23 @@ function createInstance(config) {
1912
2628
  widget.setElementSelected(true);
1913
2629
  log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
1914
2630
  }
2631
+ },
2632
+ async onReporterReportsRequest() {
2633
+ const result = await apiClient.listReporterReports(core.getReporterToken());
2634
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
2635
+ return result.data?.reports ?? [];
2636
+ },
2637
+ async onReporterCommentsRequest(reportId) {
2638
+ const result = await apiClient.listReporterComments(reportId, core.getReporterToken());
2639
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
2640
+ return result.data?.comments ?? [];
2641
+ },
2642
+ async onReporterReply(reportId, body) {
2643
+ const result = await apiClient.replyToReporterReport(reportId, core.getReporterToken(), body);
2644
+ if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
1915
2645
  }
1916
- });
2646
+ }, MUSHI_SDK_VERSION);
2647
+ syncCaptureModules();
1917
2648
  if (typeof document !== "undefined") {
1918
2649
  if (document.readyState === "loading") {
1919
2650
  document.addEventListener("DOMContentLoaded", () => widget.mount());
@@ -1923,7 +2654,7 @@ function createInstance(config) {
1923
2654
  }
1924
2655
  let proactiveTriggers = null;
1925
2656
  let proactiveManager = null;
1926
- const proactiveCfg = config.proactive;
2657
+ const proactiveCfg = activeConfig.proactive;
1927
2658
  const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
1928
2659
  if (hasAnyProactive && typeof document !== "undefined") {
1929
2660
  proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
@@ -1944,6 +2675,7 @@ function createInstance(config) {
1944
2675
  rageClick: proactiveCfg?.rageClick,
1945
2676
  longTask: proactiveCfg?.longTask,
1946
2677
  apiCascade: proactiveCfg?.apiCascade,
2678
+ apiEndpoint: resolveApiEndpoint(activeConfig),
1947
2679
  errorBoundary: proactiveCfg?.errorBoundary
1948
2680
  }
1949
2681
  );
@@ -1959,6 +2691,7 @@ function createInstance(config) {
1959
2691
  if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
1960
2692
  });
1961
2693
  function applyRuntimeConfig(runtime) {
2694
+ runtimeConfigLoaded = true;
1962
2695
  if (runtime.enabled === false) {
1963
2696
  activeConfig = bootstrapConfig;
1964
2697
  clearCachedRuntimeConfig(config.projectId);
@@ -1972,7 +2705,7 @@ function createInstance(config) {
1972
2705
  if (runtime.widget) widget.updateConfig(activeConfig.widget);
1973
2706
  log.debug("Applied runtime SDK config", { version: runtime.version });
1974
2707
  }
1975
- if (config.runtimeConfig !== false) {
2708
+ if (shouldUseRuntimeConfig(config)) {
1976
2709
  const cached = readCachedRuntimeConfig(config.projectId);
1977
2710
  if (cached) applyRuntimeConfig(cached);
1978
2711
  apiClient.getSdkConfig().then((result) => {
@@ -1985,8 +2718,41 @@ function createInstance(config) {
1985
2718
  }).catch((err) => {
1986
2719
  log.debug("Runtime SDK config fetch failed", { error: err instanceof Error ? err.message : String(err) });
1987
2720
  });
2721
+ } else if (config.runtimeConfig !== false && isLocalhostEndpoint(resolveApiEndpoint(config))) {
2722
+ log.debug("Runtime SDK config skipped for localhost apiEndpoint; set runtimeConfig: true to force it");
1988
2723
  }
2724
+ void checkSdkFreshness();
1989
2725
  log.info("Initialized", { projectId: config.projectId });
2726
+ async function checkSdkFreshness() {
2727
+ if (activeConfig.widget?.outdatedBanner === "off") return;
2728
+ const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
2729
+ if (cached) applySdkFreshness(cached);
2730
+ const result = await apiClient.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
2731
+ if (!result.ok || !result.data) return;
2732
+ cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
2733
+ applySdkFreshness(result.data);
2734
+ }
2735
+ function applySdkFreshness(info) {
2736
+ const latest = info.latest;
2737
+ const outdated = Boolean(latest && isVersionOlder(MUSHI_SDK_VERSION, latest));
2738
+ if (!outdated && !info.deprecated) return;
2739
+ const message = info.deprecationMessage ?? (outdated ? `Update ${MUSHI_SDK_PACKAGE} to ${latest}.` : null);
2740
+ log.warn("Mushi SDK is outdated", {
2741
+ package: MUSHI_SDK_PACKAGE,
2742
+ current: MUSHI_SDK_VERSION,
2743
+ latest,
2744
+ deprecated: info.deprecated,
2745
+ message
2746
+ });
2747
+ if (activeConfig.widget?.outdatedBanner !== "console-only") {
2748
+ widget.setSdkFreshness({
2749
+ latest,
2750
+ current: MUSHI_SDK_VERSION,
2751
+ deprecated: info.deprecated,
2752
+ message
2753
+ });
2754
+ }
2755
+ }
1990
2756
  async function submitReport(category, description, intent) {
1991
2757
  const filterResult = preFilter.check(description);
1992
2758
  if (!filterResult.passed) {
@@ -2028,6 +2794,8 @@ function createInstance(config) {
2028
2794
  const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
2029
2795
  const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
2030
2796
  const fingerprintHash = await core.getDeviceFingerprintHash().catch(() => null);
2797
+ const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
2798
+ const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
2031
2799
  const report = {
2032
2800
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2033
2801
  projectId: config.projectId,
@@ -2035,9 +2803,10 @@ function createInstance(config) {
2035
2803
  description: scrubbedDescription,
2036
2804
  userIntent: intent,
2037
2805
  environment: core.captureEnvironment(),
2038
- consoleLogs: activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries(),
2039
- networkLogs: activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries(),
2806
+ consoleLogs,
2807
+ networkLogs,
2040
2808
  performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
2809
+ timeline: timelineCap.getEntries({ consoleLogs, networkLogs }),
2041
2810
  screenshotDataUrl: pendingScreenshot ?? void 0,
2042
2811
  selectedElement: pendingElement ?? void 0,
2043
2812
  metadata: {
@@ -2049,6 +2818,8 @@ function createInstance(config) {
2049
2818
  reporterToken: core.getReporterToken(),
2050
2819
  ...fingerprintHash ? { fingerprintHash } : {},
2051
2820
  appVersion: config.integrations?.vercel?.analyticsId,
2821
+ sdkPackage: MUSHI_SDK_PACKAGE,
2822
+ sdkVersion: MUSHI_SDK_VERSION,
2052
2823
  proactiveTrigger: pendingProactiveTrigger ?? void 0,
2053
2824
  sentryEventId: sentryCtx?.eventId,
2054
2825
  sentryReplayId: sentryCtx?.replayId,
@@ -2103,18 +2874,45 @@ function createInstance(config) {
2103
2874
  setMetadata(key, value) {
2104
2875
  customMetadata[key] = value;
2105
2876
  },
2877
+ setScreen(screen) {
2878
+ timelineCap.setScreen(screen);
2879
+ },
2106
2880
  isOpen() {
2107
2881
  return widget.getIsOpen();
2108
2882
  },
2109
2883
  open() {
2110
2884
  widget.open();
2111
2885
  },
2886
+ openWith(category) {
2887
+ widget.open({ category });
2888
+ },
2889
+ show() {
2890
+ widget.showTrigger();
2891
+ },
2892
+ hide() {
2893
+ widget.hideTrigger();
2894
+ },
2895
+ attachTo(selectorOrElement, options) {
2896
+ return widget.attachTo(selectorOrElement, options);
2897
+ },
2898
+ setTrigger(trigger) {
2899
+ widget.setTrigger(trigger);
2900
+ },
2112
2901
  close() {
2113
2902
  widget.close();
2114
2903
  },
2115
2904
  updateConfig(runtimeConfig) {
2116
2905
  applyRuntimeConfig(runtimeConfig);
2117
2906
  },
2907
+ diagnose() {
2908
+ return runDiagnostics({
2909
+ apiEndpoint: resolveApiEndpoint(activeConfig),
2910
+ widgetMounted: widget.getIsMounted(),
2911
+ runtimeConfigLoaded,
2912
+ captureScreenshotAvailable: screenshotCap !== null,
2913
+ captureNetworkIntercepting: networkCap !== null
2914
+ });
2915
+ },
2118
2916
  destroy() {
2119
2917
  proactiveTriggers?.destroy();
2120
2918
  proactiveManager?.reset();
@@ -2123,6 +2921,7 @@ function createInstance(config) {
2123
2921
  networkCap?.destroy();
2124
2922
  perfCap?.destroy();
2125
2923
  elementSelector?.deactivate();
2924
+ timelineCap.destroy();
2126
2925
  offlineQueue.stopAutoSync();
2127
2926
  listeners.clear();
2128
2927
  instance = null;
@@ -2144,6 +2943,7 @@ function createInstance(config) {
2144
2943
  category,
2145
2944
  description,
2146
2945
  environment: core.captureEnvironment(),
2946
+ timeline: timelineCap.getEntries(),
2147
2947
  metadata: {
2148
2948
  ...input.metadata ?? {},
2149
2949
  ...userInfo ? { user: userInfo } : {},
@@ -2155,6 +2955,8 @@ function createInstance(config) {
2155
2955
  },
2156
2956
  sessionId: core.getSessionId(),
2157
2957
  reporterToken: core.getReporterToken(),
2958
+ sdkPackage: MUSHI_SDK_PACKAGE,
2959
+ sdkVersion: MUSHI_SDK_VERSION,
2158
2960
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2159
2961
  };
2160
2962
  emit("report:submitted", { reportId: report.id });
@@ -2184,21 +2986,140 @@ function createInstance(config) {
2184
2986
  return sdk;
2185
2987
  }
2186
2988
  function mergeRuntimeConfig(config, runtime) {
2989
+ const nativeTrigger = runtime.native?.triggerMode;
2990
+ const widgetTrigger = runtime.widget?.trigger ?? (nativeTrigger === "none" || nativeTrigger === "shake" ? "manual" : void 0);
2187
2991
  return {
2188
2992
  ...config,
2189
2993
  widget: {
2190
2994
  ...config.widget,
2191
- ...runtime.widget
2995
+ ...runtime.widget,
2996
+ ...widgetTrigger ? { trigger: widgetTrigger } : {}
2192
2997
  },
2193
2998
  capture: {
2194
2999
  ...config.capture,
2195
3000
  ...runtime.capture
3001
+ },
3002
+ privacy: {
3003
+ ...config.privacy
3004
+ }
3005
+ };
3006
+ }
3007
+ function applyPresetConfig(config) {
3008
+ if (!config.preset) return config;
3009
+ const preset = presetDefaults(config.preset);
3010
+ return {
3011
+ ...config,
3012
+ widget: {
3013
+ ...preset.widget,
3014
+ ...config.widget
3015
+ },
3016
+ capture: {
3017
+ ...preset.capture,
3018
+ ...config.capture
3019
+ },
3020
+ proactive: {
3021
+ ...preset.proactive,
3022
+ ...config.proactive,
3023
+ cooldown: {
3024
+ ...preset.proactive?.cooldown,
3025
+ ...config.proactive?.cooldown
3026
+ }
2196
3027
  }
2197
3028
  };
2198
3029
  }
3030
+ function presetDefaults(preset) {
3031
+ switch (preset) {
3032
+ case "manual-only":
3033
+ return {
3034
+ widget: { trigger: "manual", outdatedBanner: "console-only" },
3035
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
3036
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
3037
+ };
3038
+ case "beta-loud":
3039
+ return {
3040
+ widget: { trigger: "auto", outdatedBanner: "banner" },
3041
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
3042
+ proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
3043
+ };
3044
+ case "internal-debug":
3045
+ return {
3046
+ widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
3047
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
3048
+ proactive: {
3049
+ rageClick: true,
3050
+ longTask: true,
3051
+ apiCascade: true,
3052
+ errorBoundary: true,
3053
+ cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
3054
+ }
3055
+ };
3056
+ case "production-calm":
3057
+ return {
3058
+ widget: { trigger: "auto", outdatedBanner: "console-only" },
3059
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
3060
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
3061
+ };
3062
+ }
3063
+ }
3064
+ function resolveApiEndpoint(config) {
3065
+ return config.apiEndpoint ?? core.DEFAULT_API_ENDPOINT;
3066
+ }
3067
+ function shouldUseRuntimeConfig(config) {
3068
+ if (config.runtimeConfig === false) return false;
3069
+ if (config.runtimeConfig === true) return true;
3070
+ return !isLocalhostEndpoint(resolveApiEndpoint(config));
3071
+ }
3072
+ async function runDiagnostics(options) {
3073
+ const endpoint = await probeApiEndpoint(options.apiEndpoint);
3074
+ return {
3075
+ apiEndpointReachable: endpoint.reachable,
3076
+ cspAllowsEndpoint: endpoint.cspAllowed,
3077
+ widgetMounted: options.widgetMounted,
3078
+ shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
3079
+ dialogSupported: typeof HTMLDialogElement !== "undefined",
3080
+ runtimeConfigLoaded: options.runtimeConfigLoaded,
3081
+ captureScreenshotAvailable: options.captureScreenshotAvailable,
3082
+ captureNetworkIntercepting: options.captureNetworkIntercepting,
3083
+ sdkVersion: MUSHI_SDK_VERSION
3084
+ };
3085
+ }
3086
+ async function diagnoseWithoutInstance() {
3087
+ return {
3088
+ apiEndpointReachable: false,
3089
+ cspAllowsEndpoint: false,
3090
+ widgetMounted: false,
3091
+ shadowDomAvailable: typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.attachShadow === "function",
3092
+ dialogSupported: typeof HTMLDialogElement !== "undefined",
3093
+ runtimeConfigLoaded: false,
3094
+ captureScreenshotAvailable: false,
3095
+ captureNetworkIntercepting: false,
3096
+ sdkVersion: MUSHI_SDK_VERSION
3097
+ };
3098
+ }
3099
+ async function probeApiEndpoint(apiEndpoint) {
3100
+ if (typeof fetch === "undefined") return { reachable: false, cspAllowed: false };
3101
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
3102
+ const timer = controller ? setTimeout(() => controller.abort(), 3e3) : null;
3103
+ try {
3104
+ const response = await fetch(`${apiEndpoint.replace(/\/$/, "")}/health`, {
3105
+ method: "GET",
3106
+ cache: "no-store",
3107
+ ...controller ? { signal: controller.signal } : {},
3108
+ [core.MUSHI_INTERNAL_INIT_MARKER]: "diagnose"
3109
+ });
3110
+ return { reachable: response.ok, cspAllowed: true };
3111
+ } catch {
3112
+ return { reachable: false, cspAllowed: false };
3113
+ } finally {
3114
+ if (timer) clearTimeout(timer);
3115
+ }
3116
+ }
2199
3117
  function runtimeConfigCacheKey(projectId) {
2200
3118
  return `mushi:sdk-config:${projectId}`;
2201
3119
  }
3120
+ function sdkVersionCacheKey(packageName) {
3121
+ return `mushi:sdk-version:${packageName}`;
3122
+ }
2202
3123
  function readCachedRuntimeConfig(projectId) {
2203
3124
  if (typeof localStorage === "undefined") return null;
2204
3125
  try {
@@ -2227,6 +3148,42 @@ function clearCachedRuntimeConfig(projectId) {
2227
3148
  } catch {
2228
3149
  }
2229
3150
  }
3151
+ function readCachedSdkVersion(packageName) {
3152
+ if (typeof localStorage === "undefined") return null;
3153
+ try {
3154
+ const raw = localStorage.getItem(sdkVersionCacheKey(packageName));
3155
+ if (!raw) return null;
3156
+ const parsed = JSON.parse(raw);
3157
+ if (!parsed.data || !parsed.cachedAt || Date.now() - parsed.cachedAt > 864e5) return null;
3158
+ return parsed.data;
3159
+ } catch {
3160
+ return null;
3161
+ }
3162
+ }
3163
+ function cacheSdkVersion(packageName, data) {
3164
+ if (typeof localStorage === "undefined") return;
3165
+ try {
3166
+ localStorage.setItem(sdkVersionCacheKey(packageName), JSON.stringify({
3167
+ cachedAt: Date.now(),
3168
+ data
3169
+ }));
3170
+ } catch {
3171
+ }
3172
+ }
3173
+ function isVersionOlder(current, latest) {
3174
+ const currentParts = parseVersion(current);
3175
+ const latestParts = parseVersion(latest);
3176
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
3177
+ const cur = currentParts[i] ?? 0;
3178
+ const next = latestParts[i] ?? 0;
3179
+ if (cur < next) return true;
3180
+ if (cur > next) return false;
3181
+ }
3182
+ return false;
3183
+ }
3184
+ function parseVersion(version) {
3185
+ return version.split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
3186
+ }
2230
3187
  function createNoopInstance() {
2231
3188
  return {
2232
3189
  report: () => {
@@ -2237,6 +3194,8 @@ function createNoopInstance() {
2237
3194
  },
2238
3195
  setMetadata: () => {
2239
3196
  },
3197
+ setScreen: () => {
3198
+ },
2240
3199
  isOpen: () => false,
2241
3200
  open: () => {
2242
3201
  },
@@ -2244,6 +3203,17 @@ function createNoopInstance() {
2244
3203
  },
2245
3204
  updateConfig: () => {
2246
3205
  },
3206
+ diagnose: diagnoseWithoutInstance,
3207
+ openWith: () => {
3208
+ },
3209
+ show: () => {
3210
+ },
3211
+ hide: () => {
3212
+ },
3213
+ attachTo: () => () => {
3214
+ },
3215
+ setTrigger: () => {
3216
+ },
2247
3217
  destroy: () => {
2248
3218
  instance = null;
2249
3219
  },
@@ -2261,6 +3231,7 @@ exports.createNetworkCapture = createNetworkCapture;
2261
3231
  exports.createPerformanceCapture = createPerformanceCapture;
2262
3232
  exports.createProactiveManager = createProactiveManager;
2263
3233
  exports.createScreenshotCapture = createScreenshotCapture;
3234
+ exports.createTimelineCapture = createTimelineCapture;
2264
3235
  exports.getAvailableLocales = getAvailableLocales;
2265
3236
  exports.getLocale = getLocale;
2266
3237
  exports.setupProactiveTriggers = setupProactiveTriggers;