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