@mushi-mushi/web 1.2.1 → 1.5.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
@@ -45,10 +45,20 @@ var en = {
45
45
  heading: "Tell us more",
46
46
  descriptionPlaceholder: "Describe what happened\u2026",
47
47
  screenshotButton: "Attach Screenshot",
48
- screenshotAttached: "Screenshot attached",
48
+ screenshotAttached: "Screenshot attached \u2713",
49
+ screenshotCapturing: "Taking screenshot\u2026",
50
+ screenshotFailed: "Couldn't capture \u2014 describe it instead",
49
51
  elementButton: "Select Element",
50
- elementSelected: "Element selected",
51
- optional: "(optional)"
52
+ elementSelected: "Element selected \u2713",
53
+ elementCapturing: "Click anything on the page\u2026",
54
+ elementSelectorHint: "Click any element \xB7 Esc to cancel",
55
+ optional: "(optional)",
56
+ tooShort: "A bit more detail helps us fix it faster",
57
+ examplePrompts: [
58
+ "The save button does nothing",
59
+ "Page froze for 10 seconds",
60
+ "Layout looks broken here"
61
+ ]
52
62
  }
53
63
  };
54
64
 
@@ -95,10 +105,20 @@ var ja = {
95
105
  heading: "\u8A73\u7D30\u3092\u6559\u3048\u3066\u304F\u3060\u3055\u3044",
96
106
  descriptionPlaceholder: "\u4F55\u304C\u8D77\u304D\u305F\u304B\u8AAC\u660E\u3057\u3066\u304F\u3060\u3055\u3044\u2026",
97
107
  screenshotButton: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u6DFB\u4ED8",
98
- screenshotAttached: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u6DFB\u4ED8\u6E08\u307F",
108
+ screenshotAttached: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u6DFB\u4ED8\u6E08\u307F \u2713",
109
+ screenshotCapturing: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u64AE\u5F71\u4E2D\u2026",
110
+ screenshotFailed: "\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F \u2014 \u6587\u5B57\u3067\u6559\u3048\u3066\u304F\u3060\u3055\u3044",
99
111
  elementButton: "\u8981\u7D20\u3092\u9078\u629E",
100
- elementSelected: "\u8981\u7D20\u9078\u629E\u6E08\u307F",
101
- optional: "\uFF08\u4EFB\u610F\uFF09"
112
+ elementSelected: "\u8981\u7D20\u9078\u629E\u6E08\u307F \u2713",
113
+ elementCapturing: "\u30DA\u30FC\u30B8\u4E0A\u306E\u8981\u7D20\u3092\u30AF\u30EA\u30C3\u30AF\u2026",
114
+ elementSelectorHint: "\u8981\u7D20\u3092\u30AF\u30EA\u30C3\u30AF \xB7 Esc \u3067\u30AD\u30E3\u30F3\u30BB\u30EB",
115
+ optional: "\uFF08\u4EFB\u610F\uFF09",
116
+ tooShort: "\u3082\u3046\u5C11\u3057\u8A73\u3057\u304F\u6559\u3048\u3066\u304F\u3060\u3055\u3044",
117
+ examplePrompts: [
118
+ "\u4FDD\u5B58\u30DC\u30BF\u30F3\u304C\u53CD\u5FDC\u3057\u306A\u3044",
119
+ "\u30DA\u30FC\u30B8\u304C10\u79D2\u56FA\u307E\u3063\u305F",
120
+ "\u30EC\u30A4\u30A2\u30A6\u30C8\u304C\u5D29\u308C\u3066\u3044\u308B"
121
+ ]
102
122
  }
103
123
  };
104
124
 
@@ -145,10 +165,20 @@ var th = {
145
165
  heading: "\u0E1A\u0E2D\u0E01\u0E40\u0E23\u0E32\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E40\u0E15\u0E34\u0E21",
146
166
  descriptionPlaceholder: "\u0E2D\u0E18\u0E34\u0E1A\u0E32\u0E22\u0E2A\u0E34\u0E48\u0E07\u0E17\u0E35\u0E48\u0E40\u0E01\u0E34\u0E14\u0E02\u0E36\u0E49\u0E19\u2026",
147
167
  screenshotButton: "\u0E41\u0E19\u0E1A\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15",
148
- screenshotAttached: "\u0E41\u0E19\u0E1A\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15\u0E41\u0E25\u0E49\u0E27",
168
+ screenshotAttached: "\u0E41\u0E19\u0E1A\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15\u0E41\u0E25\u0E49\u0E27 \u2713",
169
+ screenshotCapturing: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E16\u0E48\u0E32\u0E22\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15\u2026",
170
+ screenshotFailed: "\u0E44\u0E21\u0E48\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E16\u0E48\u0E32\u0E22\u0E20\u0E32\u0E1E\u0E44\u0E14\u0E49 \u2014 \u0E42\u0E1B\u0E23\u0E14\u0E2D\u0E18\u0E34\u0E1A\u0E32\u0E22\u0E41\u0E17\u0E19",
149
171
  elementButton: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A",
150
- elementSelected: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E41\u0E25\u0E49\u0E27",
151
- optional: "(\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E07\u0E04\u0E31\u0E1A)"
172
+ elementSelected: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E41\u0E25\u0E49\u0E27 \u2713",
173
+ elementCapturing: "\u0E04\u0E25\u0E34\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E1A\u0E19\u0E2B\u0E19\u0E49\u0E32\u2026",
174
+ elementSelectorHint: "\u0E04\u0E25\u0E34\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E43\u0E14\u0E01\u0E47\u0E44\u0E14\u0E49 \xB7 Esc \u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01",
175
+ optional: "(\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E07\u0E04\u0E31\u0E1A)",
176
+ tooShort: "\u0E01\u0E23\u0E38\u0E13\u0E32\u0E43\u0E2B\u0E49\u0E23\u0E32\u0E22\u0E25\u0E30\u0E40\u0E2D\u0E35\u0E22\u0E14\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E40\u0E15\u0E34\u0E21",
177
+ examplePrompts: [
178
+ "\u0E1B\u0E38\u0E48\u0E21\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E44\u0E21\u0E48\u0E17\u0E33\u0E07\u0E32\u0E19",
179
+ "\u0E2B\u0E19\u0E49\u0E32\u0E04\u0E49\u0E32\u0E07\u0E19\u0E32\u0E19 10 \u0E27\u0E34\u0E19\u0E32\u0E17\u0E35",
180
+ "\u0E40\u0E25\u0E22\u0E4C\u0E40\u0E2D\u0E32\u0E15\u0E4C\u0E1E\u0E31\u0E07"
181
+ ]
152
182
  }
153
183
  };
154
184
 
@@ -195,18 +225,29 @@ var es = {
195
225
  heading: "Cu\xE9ntanos m\xE1s",
196
226
  descriptionPlaceholder: "Describe lo que pas\xF3\u2026",
197
227
  screenshotButton: "Adjuntar captura",
198
- screenshotAttached: "Captura adjunta",
228
+ screenshotAttached: "Captura adjunta \u2713",
229
+ screenshotCapturing: "Tomando captura\u2026",
230
+ screenshotFailed: "No se pudo capturar \u2014 descr\xEDbelo en su lugar",
199
231
  elementButton: "Seleccionar elemento",
200
- elementSelected: "Elemento seleccionado",
201
- optional: "(opcional)"
232
+ elementSelected: "Elemento seleccionado \u2713",
233
+ elementCapturing: "Haz clic en cualquier elemento\u2026",
234
+ elementSelectorHint: "Clic en cualquier elemento \xB7 Esc para cancelar",
235
+ optional: "(opcional)",
236
+ tooShort: "Un poco m\xE1s de detalle nos ayuda a resolverlo",
237
+ examplePrompts: [
238
+ "El bot\xF3n guardar no responde",
239
+ "La p\xE1gina se congel\xF3 10 segundos",
240
+ "El dise\xF1o se ve roto aqu\xED"
241
+ ]
202
242
  }
203
243
  };
204
244
 
205
245
  // src/i18n/index.ts
206
246
  var locales = { en, ja, th, es };
207
247
  function getLocale(code) {
208
- if (!code) return en;
209
- const base = code.split("-")[0].toLowerCase();
248
+ const resolved = code && code !== "auto" ? code : typeof navigator !== "undefined" ? navigator.language ?? navigator.languages?.[0] : void 0;
249
+ if (!resolved) return en;
250
+ const base = resolved.split("-")[0].toLowerCase();
210
251
  return locales[base] ?? en;
211
252
  }
212
253
  function getAvailableLocales() {
@@ -239,6 +280,7 @@ function getWidgetStyles(theme) {
239
280
  -webkit-font-smoothing: antialiased;
240
281
  -moz-osx-font-smoothing: grayscale;
241
282
  font-feature-settings: 'ss01', 'cv11'; /* nicer system-ui glyphs where supported */
283
+ --mushi-ok: ${isDark ? "#4ade80" : "#16a34a"};
242
284
  }
243
285
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
244
286
  button { font-family: inherit; }
@@ -691,6 +733,51 @@ function getWidgetStyles(theme) {
691
733
  box-shadow: inset 2px 0 0 ${vermillion};
692
734
  }
693
735
 
736
+ /* Example starter chips \u2014 reduce first-report activation energy */
737
+ .mushi-example-chips {
738
+ display: flex;
739
+ flex-wrap: wrap;
740
+ gap: 6px;
741
+ margin-bottom: 10px;
742
+ }
743
+ .mushi-example-chip {
744
+ padding: 4px 10px;
745
+ border: 1px solid ${rule};
746
+ border-radius: 12px;
747
+ background: transparent;
748
+ color: ${inkMuted};
749
+ font-family: ${fontBody};
750
+ font-size: 11px;
751
+ cursor: pointer;
752
+ transition: color 150ms ${easeStamp}, border-color 150ms ${easeStamp}, background 150ms ${easeStamp};
753
+ white-space: nowrap;
754
+ }
755
+ .mushi-example-chip:hover {
756
+ color: ${ink};
757
+ border-color: ${inkMuted};
758
+ background: ${isDark ? "rgba(242,235,221,0.06)" : "rgba(14,13,11,0.04)"};
759
+ }
760
+ .mushi-example-chip:focus-visible {
761
+ outline: 2px solid ${vermillion};
762
+ outline-offset: 2px;
763
+ }
764
+
765
+ /* Textarea wrapper to position char counter */
766
+ .mushi-textarea-wrap {
767
+ position: relative;
768
+ }
769
+ .mushi-char-counter {
770
+ position: absolute;
771
+ bottom: 4px;
772
+ right: 0;
773
+ font-family: ${fontMono};
774
+ font-size: 10px;
775
+ letter-spacing: 0.04em;
776
+ color: ${inkFaint};
777
+ pointer-events: none;
778
+ transition: color 200ms ${easeStamp};
779
+ }
780
+
694
781
  .mushi-textarea {
695
782
  width: 100%;
696
783
  min-height: 96px;
@@ -753,10 +840,34 @@ function getWidgetStyles(theme) {
753
840
  border-color: ${vermillion};
754
841
  background: ${vermillionWash};
755
842
  }
843
+ .mushi-attach-btn.loading {
844
+ opacity: 0.7;
845
+ cursor: wait;
846
+ }
847
+ .mushi-attach-btn.error {
848
+ color: ${vermillion};
849
+ border-color: ${vermillionWash};
850
+ }
756
851
  .mushi-attach-btn:focus-visible {
757
852
  outline: 2px solid ${vermillion};
758
853
  outline-offset: 2px;
759
854
  }
855
+ @keyframes mushi-spin {
856
+ to { transform: rotate(360deg); }
857
+ }
858
+ @keyframes mushi-fade-in {
859
+ from { opacity: 0; transform: translateY(4px); }
860
+ to { opacity: 1; transform: translateY(0); }
861
+ }
862
+ .mushi-spinner {
863
+ display: inline-block;
864
+ width: 10px;
865
+ height: 10px;
866
+ border: 1.5px solid currentColor;
867
+ border-top-color: transparent;
868
+ border-radius: 50%;
869
+ animation: mushi-spin 0.7s linear infinite;
870
+ }
760
871
 
761
872
  .mushi-footer {
762
873
  padding: 14px 22px 16px;
@@ -1230,7 +1341,8 @@ var MushiWidget = class {
1230
1341
  draggable: config.draggable ?? false,
1231
1342
  brandFooter: config.brandFooter ?? true,
1232
1343
  outdatedBanner: config.outdatedBanner ?? "auto",
1233
- betaMode: config.betaMode ?? {}
1344
+ betaMode: config.betaMode ?? {},
1345
+ minDescriptionLength: config.minDescriptionLength ?? 20
1234
1346
  };
1235
1347
  this.callbacks = callbacks;
1236
1348
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
@@ -1249,12 +1361,22 @@ var MushiWidget = class {
1249
1361
  selectedCategory = null;
1250
1362
  selectedIntent = null;
1251
1363
  screenshotAttached = false;
1364
+ screenshotCapturing = false;
1365
+ screenshotError = false;
1252
1366
  allowScreenshotRemove = true;
1253
1367
  elementSelected = false;
1368
+ elementCapturing = false;
1254
1369
  submitting = false;
1370
+ /** Hint element injected outside the shadow DOM during element selection. */
1371
+ selectorHint = null;
1255
1372
  triggerVisible = true;
1256
1373
  triggerShrunk = false;
1257
1374
  triggerHiddenByScroll = false;
1375
+ /** Milliseconds since mount — used for the 30s first-time nudge gate. */
1376
+ mountedAt = null;
1377
+ nudgeShown = false;
1378
+ nudgeEl = null;
1379
+ nudgeTimer = null;
1258
1380
  sdkFreshness = null;
1259
1381
  reporterReports = [];
1260
1382
  reporterComments = [];
@@ -1281,6 +1403,7 @@ var MushiWidget = class {
1281
1403
  this.syncAttachedLaunchers();
1282
1404
  this.syncSmartHide();
1283
1405
  this.render();
1406
+ this.mountedAt = Date.now();
1284
1407
  }
1285
1408
  getIsMounted() {
1286
1409
  return this.host.isConnected;
@@ -1307,7 +1430,8 @@ var MushiWidget = class {
1307
1430
  ...config.draggable !== void 0 ? { draggable: config.draggable } : {},
1308
1431
  ...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
1309
1432
  ...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {},
1310
- ...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {}
1433
+ ...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {},
1434
+ ...config.minDescriptionLength !== void 0 ? { minDescriptionLength: config.minDescriptionLength } : {}
1311
1435
  };
1312
1436
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
1313
1437
  this.syncAttachedLaunchers();
@@ -1318,9 +1442,13 @@ var MushiWidget = class {
1318
1442
  if (this.isOpen) return;
1319
1443
  this.isOpen = true;
1320
1444
  this.screenshotAttached = false;
1445
+ this.screenshotCapturing = false;
1446
+ this.screenshotError = false;
1321
1447
  this.elementSelected = false;
1448
+ this.elementCapturing = false;
1322
1449
  this.submitting = false;
1323
1450
  this.submittedAt = null;
1451
+ this.removeSelectorHint();
1324
1452
  if (options?.category) {
1325
1453
  this.selectedCategory = options.category;
1326
1454
  this.selectedIntent = null;
@@ -1376,8 +1504,114 @@ var MushiWidget = class {
1376
1504
  }
1377
1505
  setElementSelected(selected) {
1378
1506
  this.elementSelected = selected;
1507
+ this.elementCapturing = false;
1508
+ this.removeSelectorHint();
1379
1509
  if (this.isOpen) this.render();
1380
1510
  }
1511
+ setScreenshotCapturing(capturing) {
1512
+ this.screenshotCapturing = capturing;
1513
+ this.screenshotError = false;
1514
+ if (this.isOpen) this.render();
1515
+ }
1516
+ setScreenshotError(failed) {
1517
+ this.screenshotError = failed;
1518
+ this.screenshotCapturing = false;
1519
+ if (this.isOpen) this.render();
1520
+ }
1521
+ setElementCapturing(capturing) {
1522
+ this.elementCapturing = capturing;
1523
+ if (capturing) {
1524
+ this.showSelectorHint();
1525
+ } else {
1526
+ this.removeSelectorHint();
1527
+ }
1528
+ if (this.isOpen) this.render();
1529
+ }
1530
+ /** Hide the widget panel (but keep the host element) during element selection
1531
+ * so the user can click any element on the page without the panel
1532
+ * intercepting the event. */
1533
+ hidePanel() {
1534
+ const panel = this.shadow.querySelector(".mushi-panel");
1535
+ if (panel) panel.style.display = "none";
1536
+ }
1537
+ showPanel() {
1538
+ const panel = this.shadow.querySelector(".mushi-panel");
1539
+ if (panel) panel.style.display = "";
1540
+ }
1541
+ showSelectorHint() {
1542
+ this.removeSelectorHint();
1543
+ const hint = document.createElement("div");
1544
+ hint.id = "mushi-selector-hint";
1545
+ hint.setAttribute("role", "status");
1546
+ hint.setAttribute("aria-live", "polite");
1547
+ hint.style.cssText = `
1548
+ position: fixed;
1549
+ bottom: 24px;
1550
+ left: 50%;
1551
+ transform: translateX(-50%);
1552
+ z-index: 2147483646;
1553
+ background: rgba(17,17,17,0.92);
1554
+ color: #fff;
1555
+ font-family: ui-monospace, SFMono-Regular, monospace;
1556
+ font-size: 12px;
1557
+ letter-spacing: 0.04em;
1558
+ padding: 8px 16px;
1559
+ border-radius: 20px;
1560
+ pointer-events: none;
1561
+ white-space: nowrap;
1562
+ backdrop-filter: blur(4px);
1563
+ box-shadow: 0 2px 12px rgba(0,0,0,0.35);
1564
+ `;
1565
+ hint.textContent = this.locale.step3.elementSelectorHint;
1566
+ document.body.appendChild(hint);
1567
+ this.selectorHint = hint;
1568
+ }
1569
+ removeSelectorHint() {
1570
+ this.selectorHint?.remove();
1571
+ this.selectorHint = null;
1572
+ document.getElementById("mushi-selector-hint")?.remove();
1573
+ }
1574
+ showNudge() {
1575
+ if (this.nudgeShown || this.nudgeEl) return;
1576
+ this.nudgeShown = true;
1577
+ const trigger = this.shadow.querySelector(".mushi-trigger");
1578
+ const rect = trigger?.getBoundingClientRect();
1579
+ const nudge = document.createElement("div");
1580
+ nudge.id = "mushi-nudge-bubble";
1581
+ nudge.setAttribute("role", "tooltip");
1582
+ const isRight = this.config.position.includes("right");
1583
+ nudge.style.cssText = `
1584
+ position: fixed;
1585
+ z-index: 2147483645;
1586
+ ${rect ? `bottom: ${window.innerHeight - rect.top + 8}px; ${isRight ? `right: ${window.innerWidth - rect.right}px;` : `left: ${rect.left}px;`}` : "bottom: 80px; right: 24px;"}
1587
+ background: rgba(17,17,17,0.92);
1588
+ color: #fff;
1589
+ font-family: ui-sans-serif, system-ui, sans-serif;
1590
+ font-size: 12px;
1591
+ line-height: 1.4;
1592
+ padding: 8px 12px;
1593
+ border-radius: 8px;
1594
+ max-width: 200px;
1595
+ pointer-events: none;
1596
+ backdrop-filter: blur(4px);
1597
+ box-shadow: 0 2px 12px rgba(0,0,0,0.35);
1598
+ animation: mushi-fade-in 0.15s ease forwards;
1599
+ `;
1600
+ nudge.textContent = this.locale.step3.tooShort.startsWith("A bit") ? "Found a bug? One sentence is enough \u{1F41B}" : "\u30D0\u30B0\u3092\u898B\u3064\u3051\u305F\uFF1F\u4E00\u884C\u3067\u5927\u4E08\u592B\u3067\u3059 \u{1F41B}";
1601
+ document.body.appendChild(nudge);
1602
+ this.nudgeEl = nudge;
1603
+ if (this.nudgeTimer !== null) clearTimeout(this.nudgeTimer);
1604
+ this.nudgeTimer = setTimeout(() => this.removeNudge(), 5e3);
1605
+ }
1606
+ removeNudge() {
1607
+ if (this.nudgeTimer !== null) {
1608
+ clearTimeout(this.nudgeTimer);
1609
+ this.nudgeTimer = null;
1610
+ }
1611
+ this.nudgeEl?.remove();
1612
+ this.nudgeEl = null;
1613
+ document.getElementById("mushi-nudge-bubble")?.remove();
1614
+ }
1381
1615
  setSdkFreshness(info) {
1382
1616
  this.sdkFreshness = info;
1383
1617
  if (this.isOpen) this.render();
@@ -1403,6 +1637,8 @@ var MushiWidget = class {
1403
1637
  this.smartHideCleanup = null;
1404
1638
  this.attachedLaunchers.forEach((cleanup) => cleanup());
1405
1639
  this.attachedLaunchers = [];
1640
+ this.removeSelectorHint();
1641
+ this.removeNudge();
1406
1642
  this.host.remove();
1407
1643
  }
1408
1644
  syncAttachedLaunchers() {
@@ -1498,9 +1734,22 @@ var MushiWidget = class {
1498
1734
  trigger.style.zIndex = String(this.config.zIndex);
1499
1735
  this.applyInsetVars(trigger);
1500
1736
  trigger.addEventListener("click", () => {
1737
+ this.removeNudge();
1501
1738
  if (this.isOpen) this.close();
1502
1739
  else this.open();
1503
1740
  });
1741
+ trigger.addEventListener("mouseenter", () => {
1742
+ const onPageMs = this.mountedAt ? Date.now() - this.mountedAt : 0;
1743
+ if (!this.nudgeShown && !this.isOpen && onPageMs >= 3e4) {
1744
+ this.showNudge();
1745
+ }
1746
+ });
1747
+ trigger.addEventListener("mouseleave", () => {
1748
+ if (this.nudgeEl) {
1749
+ if (this.nudgeTimer !== null) clearTimeout(this.nudgeTimer);
1750
+ this.nudgeTimer = setTimeout(() => this.removeNudge(), 2e3);
1751
+ }
1752
+ });
1504
1753
  this.shadow.appendChild(trigger);
1505
1754
  }
1506
1755
  const panel = document.createElement("div");
@@ -1752,25 +2001,62 @@ var MushiWidget = class {
1752
2001
  ${this.renderStepIndicator(STEP_NUMBER.intent)}
1753
2002
  `;
1754
2003
  }
2004
+ effectiveMinLength() {
2005
+ const base = this.config.minDescriptionLength ?? 20;
2006
+ const lang = this.config.locale === "auto" ? typeof navigator !== "undefined" ? navigator.language ?? "" : "" : this.config.locale ?? "";
2007
+ const isCjk = /^(ja|zh|ko)/i.test(lang);
2008
+ return isCjk ? Math.max(4, Math.floor(base / 2)) : base;
2009
+ }
1755
2010
  renderDetailsStep() {
1756
2011
  const t = this.locale;
2012
+ const minLen = this.effectiveMinLength();
2013
+ const screenshotLabel = this.screenshotCapturing ? t.step3.screenshotCapturing : this.screenshotError ? t.step3.screenshotFailed : this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton;
2014
+ const screenshotClass = [
2015
+ "mushi-attach-btn",
2016
+ this.screenshotAttached ? "active" : "",
2017
+ this.screenshotError ? "error" : "",
2018
+ this.screenshotCapturing ? "loading" : ""
2019
+ ].filter(Boolean).join(" ");
2020
+ const elementLabel = this.elementCapturing ? t.step3.elementCapturing : this.elementSelected ? t.step3.elementSelected : t.step3.elementButton;
2021
+ const elementClass = [
2022
+ "mushi-attach-btn",
2023
+ this.elementSelected ? "active" : "",
2024
+ this.elementCapturing ? "loading" : ""
2025
+ ].filter(Boolean).join(" ");
2026
+ const exampleChips = t.step3.examplePrompts.map((p) => `<button type="button" class="mushi-example-chip" data-example="${escapeHtml(p)}">${escapeHtml(p)}</button>`).join("");
1757
2027
  return `
1758
2028
  ${this.renderHeader({ title: t.step3.heading, showBack: true, step: STEP_NUMBER.details })}
1759
2029
  <div class="mushi-body">
1760
- <textarea
1761
- class="mushi-textarea"
1762
- placeholder="${t.step3.descriptionPlaceholder}"
1763
- rows="4"
1764
- aria-label="${t.step3.heading}"
1765
- autofocus
1766
- ></textarea>
2030
+ <div class="mushi-example-chips" aria-label="Example prompts">${exampleChips}</div>
2031
+ <div class="mushi-textarea-wrap">
2032
+ <textarea
2033
+ class="mushi-textarea"
2034
+ placeholder="${t.step3.descriptionPlaceholder}"
2035
+ rows="4"
2036
+ aria-label="${t.step3.heading}"
2037
+ autofocus
2038
+ ></textarea>
2039
+ <div class="mushi-char-counter" data-role="char-counter" aria-hidden="true">
2040
+ <span data-role="char-current">0</span>/<span data-role="char-min">${minLen}</span>
2041
+ </div>
2042
+ </div>
1767
2043
  <div class="mushi-attachments">
1768
- <button type="button" class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
1769
- \u{1F4F8} ${this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton}
2044
+ <button type="button" class="${screenshotClass}"
2045
+ data-action="screenshot"
2046
+ ${this.screenshotCapturing ? "disabled" : ""}
2047
+ aria-label="${escapeHtml(screenshotLabel)}"
2048
+ >
2049
+ ${this.screenshotCapturing ? '<span class="mushi-spinner" aria-hidden="true"></span>' : "\u{1F4F8}"}
2050
+ ${escapeHtml(screenshotLabel)}
1770
2051
  </button>
1771
- ${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot">\u2715 Remove screenshot</button>' : ""}
1772
- <button type="button" class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
1773
- \u{1F3AF} ${this.elementSelected ? t.step3.elementSelected : t.step3.elementButton}
2052
+ ${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot" aria-label="Remove screenshot">\u2715 Remove</button>' : ""}
2053
+ <button type="button" class="${elementClass}"
2054
+ data-action="element"
2055
+ ${this.elementCapturing ? "disabled" : ""}
2056
+ aria-label="${escapeHtml(elementLabel)}"
2057
+ >
2058
+ ${this.elementCapturing ? '<span class="mushi-spinner" aria-hidden="true"></span>' : "\u{1F3AF}"}
2059
+ ${escapeHtml(elementLabel)}
1774
2060
  </button>
1775
2061
  </div>
1776
2062
  <div class="mushi-error" style="display:none" role="alert"></div>
@@ -1940,6 +2226,30 @@ var MushiWidget = class {
1940
2226
  this.render();
1941
2227
  });
1942
2228
  });
2229
+ const textarea = panel.querySelector(".mushi-textarea");
2230
+ const charCurrentEl = panel.querySelector('[data-role="char-current"]');
2231
+ if (textarea && charCurrentEl) {
2232
+ const minLen = this.effectiveMinLength();
2233
+ const updateCounter = () => {
2234
+ const len = textarea.value.trim().length;
2235
+ charCurrentEl.textContent = String(len);
2236
+ const counterEl = panel.querySelector('[data-role="char-counter"]');
2237
+ if (counterEl) {
2238
+ counterEl.style.color = len >= minLen ? "var(--mushi-ok, #22c55e)" : "";
2239
+ }
2240
+ };
2241
+ textarea.addEventListener("input", updateCounter);
2242
+ }
2243
+ panel.querySelectorAll("[data-example]").forEach((chip) => {
2244
+ chip.addEventListener("click", () => {
2245
+ const example = chip.dataset.example ?? "";
2246
+ if (textarea) {
2247
+ textarea.value = example;
2248
+ textarea.focus();
2249
+ textarea.dispatchEvent(new Event("input"));
2250
+ }
2251
+ });
2252
+ });
1943
2253
  panel.querySelector('[data-action="screenshot"]')?.addEventListener("click", () => {
1944
2254
  this.callbacks.onScreenshotRequest();
1945
2255
  });
@@ -1950,14 +2260,16 @@ var MushiWidget = class {
1950
2260
  this.callbacks.onElementSelectorRequest?.();
1951
2261
  });
1952
2262
  const submitReport = () => {
1953
- const textarea = panel.querySelector(".mushi-textarea");
1954
- const description = textarea?.value?.trim() ?? "";
2263
+ const textarea2 = panel.querySelector(".mushi-textarea");
2264
+ const description = textarea2?.value?.trim() ?? "";
1955
2265
  const errorEl = panel.querySelector(".mushi-error");
1956
- const MIN_DESCRIPTION_LENGTH = 20;
1957
- if (description.length < MIN_DESCRIPTION_LENGTH) {
2266
+ const minLen = this.effectiveMinLength();
2267
+ if (description.length < minLen) {
1958
2268
  if (errorEl) {
1959
- errorEl.textContent = `${t.widget.error} (${description.length}/${MIN_DESCRIPTION_LENGTH})`;
2269
+ const msg = `${t.step3.tooShort} (${description.length}/${minLen})`;
2270
+ errorEl.textContent = msg;
1960
2271
  errorEl.style.display = "block";
2272
+ textarea2?.focus();
1961
2273
  }
1962
2274
  return;
1963
2275
  }
@@ -2620,50 +2932,16 @@ function truncateUrl(url) {
2620
2932
  function createScreenshotCapture(options = {}) {
2621
2933
  let activeOptions = options;
2622
2934
  async function take() {
2623
- try {
2624
- if (typeof document === "undefined") return null;
2625
- const canvas = document.createElement("canvas");
2626
- const ctx = canvas.getContext("2d");
2627
- if (!ctx) return null;
2628
- const width = window.innerWidth;
2629
- const height = window.innerHeight;
2630
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
2631
- canvas.width = width * dpr;
2632
- canvas.height = height * dpr;
2633
- ctx.scale(dpr, dpr);
2634
- const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
2635
- const svgData = `
2636
- <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
2637
- <foreignObject width="100%" height="100%">
2638
- <div xmlns="http://www.w3.org/1999/xhtml">
2639
- ${new XMLSerializer().serializeToString(safeDocument)}
2640
- </div>
2641
- </foreignObject>
2642
- </svg>
2643
- `;
2644
- const img = new Image();
2645
- const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
2646
- const url = URL.createObjectURL(blob);
2647
- return new Promise((resolve) => {
2648
- img.onload = () => {
2649
- ctx.drawImage(img, 0, 0, width, height);
2650
- URL.revokeObjectURL(url);
2651
- try {
2652
- const dataUrl = canvas.toDataURL("image/jpeg", 0.7);
2653
- resolve(dataUrl);
2654
- } catch {
2655
- resolve(null);
2656
- }
2657
- };
2658
- img.onerror = () => {
2659
- URL.revokeObjectURL(url);
2660
- resolve(null);
2661
- };
2662
- img.src = url;
2663
- });
2664
- } catch {
2665
- return null;
2935
+ if (typeof document === "undefined") {
2936
+ return { ok: false, reason: "unsupported", message: "Not in a browser context" };
2937
+ }
2938
+ const svgResult = await trySvgCapture(activeOptions.privacy);
2939
+ if (svgResult.ok) return svgResult;
2940
+ if (svgResult.reason !== "unsupported") {
2941
+ const mediaResult = await tryDisplayMediaCapture();
2942
+ if (mediaResult.ok) return mediaResult;
2666
2943
  }
2944
+ return svgResult;
2667
2945
  }
2668
2946
  return {
2669
2947
  take,
@@ -2672,8 +2950,110 @@ function createScreenshotCapture(options = {}) {
2672
2950
  }
2673
2951
  };
2674
2952
  }
2953
+ async function trySvgCapture(privacy) {
2954
+ try {
2955
+ const canvas = document.createElement("canvas");
2956
+ const ctx = canvas.getContext("2d");
2957
+ if (!ctx) return { ok: false, reason: "unsupported", message: "Canvas 2d context unavailable" };
2958
+ const width = window.innerWidth;
2959
+ const height = window.innerHeight;
2960
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
2961
+ canvas.width = width * dpr;
2962
+ canvas.height = height * dpr;
2963
+ ctx.scale(dpr, dpr);
2964
+ const safeDocument = buildPrivacySafeDocument(privacy);
2965
+ const svgData = `
2966
+ <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
2967
+ <foreignObject width="100%" height="100%">
2968
+ <div xmlns="http://www.w3.org/1999/xhtml">
2969
+ ${new XMLSerializer().serializeToString(safeDocument)}
2970
+ </div>
2971
+ </foreignObject>
2972
+ </svg>
2973
+ `;
2974
+ const img = new Image();
2975
+ const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
2976
+ const url = URL.createObjectURL(blob);
2977
+ const loadResult = await new Promise((resolve) => {
2978
+ img.onload = () => resolve("loaded");
2979
+ img.onerror = () => resolve("error");
2980
+ const timeout = setTimeout(() => resolve("error"), 5e3);
2981
+ img.onload = () => {
2982
+ clearTimeout(timeout);
2983
+ resolve("loaded");
2984
+ };
2985
+ });
2986
+ URL.revokeObjectURL(url);
2987
+ if (loadResult === "error") {
2988
+ return { ok: false, reason: "load-error", message: "SVG image load failed" };
2989
+ }
2990
+ ctx.drawImage(img, 0, 0, width, height);
2991
+ try {
2992
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.75);
2993
+ return { ok: true, dataUrl };
2994
+ } catch (err) {
2995
+ const message = err instanceof Error ? err.message : String(err);
2996
+ return { ok: false, reason: "tainted", message };
2997
+ }
2998
+ } catch (err) {
2999
+ return { ok: false, reason: "error", message: err instanceof Error ? err.message : String(err) };
3000
+ }
3001
+ }
3002
+ async function tryDisplayMediaCapture() {
3003
+ if (typeof navigator === "undefined" || !("mediaDevices" in navigator)) {
3004
+ return { ok: false, reason: "unsupported", message: "mediaDevices not available" };
3005
+ }
3006
+ const mediaDevices = navigator.mediaDevices;
3007
+ if (typeof mediaDevices.getDisplayMedia !== "function") {
3008
+ return { ok: false, reason: "unsupported", message: "getDisplayMedia not available" };
3009
+ }
3010
+ let stream = null;
3011
+ try {
3012
+ stream = await mediaDevices.getDisplayMedia({
3013
+ video: { displaySurface: "browser" },
3014
+ audio: false
3015
+ });
3016
+ const track = stream.getVideoTracks()[0];
3017
+ if (!track) return { ok: false, reason: "error", message: "No video track" };
3018
+ const imageCapture = new window.ImageCapture(track);
3019
+ const bitmap = await imageCapture.grabFrame();
3020
+ const canvas = document.createElement("canvas");
3021
+ canvas.width = bitmap.width;
3022
+ canvas.height = bitmap.height;
3023
+ const ctx = canvas.getContext("2d");
3024
+ ctx.drawImage(bitmap, 0, 0);
3025
+ bitmap.close();
3026
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
3027
+ return { ok: true, dataUrl };
3028
+ } catch (err) {
3029
+ const message = err instanceof Error ? err.message : String(err);
3030
+ if (err instanceof Error && err.name === "NotAllowedError") {
3031
+ return { ok: false, reason: "cancelled", message };
3032
+ }
3033
+ return { ok: false, reason: "error", message };
3034
+ } finally {
3035
+ stream?.getTracks().forEach((t) => t.stop());
3036
+ }
3037
+ }
2675
3038
  function buildPrivacySafeDocument(privacy) {
2676
3039
  const clone = document.documentElement.cloneNode(true);
3040
+ for (const img of Array.from(clone.querySelectorAll("img[src]"))) {
3041
+ const src = img.getAttribute("src") ?? "";
3042
+ try {
3043
+ const url = new URL(src, window.location.href);
3044
+ if (url.origin !== window.location.origin) {
3045
+ img.removeAttribute("src");
3046
+ img.removeAttribute("srcset");
3047
+ }
3048
+ } catch {
3049
+ }
3050
+ }
3051
+ for (const el of Array.from(clone.querySelectorAll("[style]"))) {
3052
+ const style = el.getAttribute("style") ?? "";
3053
+ if (/url\(["']?https?:\/\/(?!localhost)/.test(style)) {
3054
+ el.setAttribute("style", style.replace(/url\([^)]*\)/g, "none"));
3055
+ }
3056
+ }
2677
3057
  for (const selector of privacy?.blockSelectors ?? []) {
2678
3058
  for (const el of safeQueryAll(clone, selector)) {
2679
3059
  el.remove();
@@ -2708,6 +3088,15 @@ function maskElement(el) {
2708
3088
  }
2709
3089
 
2710
3090
  // src/capture/performance.ts
3091
+ var INP_DURATION_THRESHOLD_MS = 40;
3092
+ function describeElement(target) {
3093
+ if (!target || !target.tagName) return void 0;
3094
+ const el = target;
3095
+ const tag = el.tagName.toLowerCase();
3096
+ const id = el.id ? `#${el.id}` : "";
3097
+ const cls = el.classList && el.classList.length > 0 ? `.${el.classList[0]}` : "";
3098
+ return `${tag}${id}${cls}`;
3099
+ }
2711
3100
  function createPerformanceCapture() {
2712
3101
  const metrics = {};
2713
3102
  const observers = [];
@@ -2759,6 +3148,53 @@ function createPerformanceCapture() {
2759
3148
  observers.push(longTaskObserver);
2760
3149
  } catch {
2761
3150
  }
3151
+ try {
3152
+ const seenInteractions = /* @__PURE__ */ new Map();
3153
+ const inpObserver = new PerformanceObserver((list) => {
3154
+ for (const entry of list.getEntries()) {
3155
+ const interactionId = entry.interactionId;
3156
+ if (!interactionId) continue;
3157
+ const prev = seenInteractions.get(interactionId) ?? 0;
3158
+ if (entry.duration > prev) {
3159
+ seenInteractions.set(interactionId, entry.duration);
3160
+ }
3161
+ if (entry.duration > (metrics.inp ?? 0)) {
3162
+ metrics.inp = entry.duration;
3163
+ const inputDelay = entry.processingStart - entry.startTime;
3164
+ const processingDuration = entry.processingEnd - entry.processingStart;
3165
+ const presentationDelay = entry.startTime + entry.duration - entry.processingEnd;
3166
+ metrics.inpAttribution = {
3167
+ eventType: entry.name,
3168
+ targetSelector: describeElement(entry.target),
3169
+ inputDelay: Math.max(0, inputDelay),
3170
+ processingDuration: Math.max(0, processingDuration),
3171
+ presentationDelay: Math.max(0, presentationDelay)
3172
+ };
3173
+ }
3174
+ }
3175
+ });
3176
+ inpObserver.observe({
3177
+ type: "event",
3178
+ // `durationThreshold` filters out fast (< 40 ms) interactions
3179
+ // that sit below human perception. Spec-recommended floor.
3180
+ durationThreshold: INP_DURATION_THRESHOLD_MS,
3181
+ buffered: true
3182
+ });
3183
+ observers.push(inpObserver);
3184
+ } catch {
3185
+ }
3186
+ try {
3187
+ const fidObserver = new PerformanceObserver((list) => {
3188
+ for (const entry of list.getEntries()) {
3189
+ if (metrics.fid === void 0) {
3190
+ metrics.fid = entry.processingStart - entry.startTime;
3191
+ }
3192
+ }
3193
+ });
3194
+ fidObserver.observe({ type: "first-input", buffered: true });
3195
+ observers.push(fidObserver);
3196
+ } catch {
3197
+ }
2762
3198
  }
2763
3199
  if (typeof performance !== "undefined" && performance.getEntriesByType) {
2764
3200
  try {
@@ -2870,6 +3306,11 @@ function createElementSelector() {
2870
3306
  e.stopPropagation();
2871
3307
  const target = e.target;
2872
3308
  if (target === overlay) return;
3309
+ const path = e.composedPath ? e.composedPath() : [];
3310
+ const hitsMushiHost = path.some(
3311
+ (node) => node instanceof Element && node.id === "mushi-mushi-widget"
3312
+ );
3313
+ if (hitsMushiHost) return;
2873
3314
  const captured = captureElement(target);
2874
3315
  finish(captured);
2875
3316
  }
@@ -3584,7 +4025,7 @@ function createProactiveManager(config = {}) {
3584
4025
 
3585
4026
  // src/version.ts
3586
4027
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
3587
- var MUSHI_SDK_VERSION = "1.2.1" ;
4028
+ var MUSHI_SDK_VERSION = "1.5.0" ;
3588
4029
 
3589
4030
  // src/mushi.ts
3590
4031
  var instance = null;
@@ -3789,8 +4230,21 @@ function createInstance(config) {
3789
4230
  onScreenshotRequest: async () => {
3790
4231
  if (!screenshotCap || activeConfig.capture?.screenshot === "off") return;
3791
4232
  log.debug("Taking screenshot");
3792
- pendingScreenshot = await screenshotCap.take();
3793
- widget.setScreenshotAttached(pendingScreenshot !== null);
4233
+ widget.setScreenshotCapturing(true);
4234
+ const result = await screenshotCap.take();
4235
+ if (result.ok) {
4236
+ pendingScreenshot = result.dataUrl;
4237
+ widget.setScreenshotAttached(true);
4238
+ log.debug("Screenshot captured");
4239
+ } else {
4240
+ pendingScreenshot = null;
4241
+ if (result.reason !== "cancelled") {
4242
+ widget.setScreenshotError(true);
4243
+ log.debug("Screenshot failed", { reason: result.reason, message: result.message });
4244
+ } else {
4245
+ widget.setScreenshotCapturing(false);
4246
+ }
4247
+ }
3794
4248
  },
3795
4249
  onScreenshotRemove: () => {
3796
4250
  log.debug("Screenshot attachment removed");
@@ -3800,11 +4254,19 @@ function createInstance(config) {
3800
4254
  onElementSelectorRequest: async () => {
3801
4255
  if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
3802
4256
  log.debug("Element selector activated");
3803
- const el = await elementSelector.activate();
3804
- if (el) {
3805
- pendingElement = el;
3806
- widget.setElementSelected(true);
3807
- log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
4257
+ widget.setElementCapturing(true);
4258
+ widget.hidePanel();
4259
+ try {
4260
+ const el = await elementSelector.activate();
4261
+ if (el) {
4262
+ pendingElement = el;
4263
+ widget.setElementSelected(true);
4264
+ log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
4265
+ } else {
4266
+ widget.setElementCapturing(false);
4267
+ }
4268
+ } finally {
4269
+ widget.showPanel();
3808
4270
  }
3809
4271
  },
3810
4272
  async onReporterReportsRequest() {
@@ -4040,17 +4502,43 @@ function createInstance(config) {
4040
4502
  };
4041
4503
  config.integrations.custom(builder);
4042
4504
  }
4043
- emit("report:submitted", { reportId: report.id });
4505
+ let finalReport = report;
4506
+ if (config.beforeSendFeedback) {
4507
+ try {
4508
+ const hookResult = await Promise.race([
4509
+ Promise.resolve(config.beforeSendFeedback(report)),
4510
+ // 2s timeout — async hooks must not block the user's "submit"
4511
+ // for longer than the network would. Falls back to original.
4512
+ new Promise(
4513
+ (resolve) => setTimeout(() => resolve(report), 2e3)
4514
+ )
4515
+ ]);
4516
+ if (hookResult === null) {
4517
+ log.info("Report dropped by beforeSendFeedback hook", { reportId: report.id });
4518
+ return;
4519
+ }
4520
+ finalReport = hookResult;
4521
+ } catch (err) {
4522
+ log.warn("beforeSendFeedback hook threw \u2014 sending unmodified report", {
4523
+ error: err instanceof Error ? err.message : String(err)
4524
+ });
4525
+ }
4526
+ }
4527
+ emit("report:submitted", { reportId: finalReport.id });
4044
4528
  if (typeof navigator !== "undefined" && !navigator.onLine) {
4045
- await offlineQueue.enqueue(report);
4046
- log.info("Offline \u2014 report queued", { reportId: report.id });
4047
- emit("report:queued", { reportId: report.id });
4529
+ await offlineQueue.enqueue(finalReport);
4530
+ log.info("Offline \u2014 report queued", { reportId: finalReport.id });
4531
+ emit("report:queued", { reportId: finalReport.id });
4048
4532
  return;
4049
4533
  }
4050
- const result = await apiClient2.submitReport(report);
4534
+ const result = await apiClient2.submitReport(finalReport);
4051
4535
  if (result.ok) {
4052
4536
  log.info("Report sent", { reportId: result.data?.reportId });
4053
4537
  emit("report:sent", { reportId: result.data?.reportId });
4538
+ if (result.data?.cursorAgentId) {
4539
+ const d = result.data;
4540
+ emit("report:dispatched", { reportId: d.reportId, agentId: d.cursorAgentId, fixId: d.fixId });
4541
+ }
4054
4542
  breadcrumbs.add({
4055
4543
  category: "lifecycle",
4056
4544
  level: "info",
@@ -4067,13 +4555,13 @@ function createInstance(config) {
4067
4555
  } catch {
4068
4556
  }
4069
4557
  } else {
4070
- log.warn("Report failed, queuing for retry", { reportId: report.id, error: result.error });
4071
- await offlineQueue.enqueue(report);
4072
- emit("report:failed", { reportId: report.id, error: result.error });
4558
+ log.warn("Report failed, queuing for retry", { reportId: finalReport.id, error: result.error });
4559
+ await offlineQueue.enqueue(finalReport);
4560
+ emit("report:failed", { reportId: finalReport.id, error: result.error });
4073
4561
  breadcrumbs.add({
4074
4562
  category: "lifecycle",
4075
4563
  level: "warning",
4076
- message: `Mushi report queued for retry (${report.id})`
4564
+ message: `Mushi report queued for retry (${finalReport.id})`
4077
4565
  });
4078
4566
  }
4079
4567
  pendingScreenshot = null;
@@ -4335,6 +4823,35 @@ function createInstance(config) {
4335
4823
  if (typeof globalThis !== "undefined" && (bootstrapConfig.debug ?? false)) {
4336
4824
  exposeMarketingRecorder(widget);
4337
4825
  }
4826
+ if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
4827
+ const SENTINEL_KEY = "mushi:last-run";
4828
+ let crashed = null;
4829
+ try {
4830
+ const previous = localStorage.getItem(SENTINEL_KEY);
4831
+ crashed = previous === null ? null : previous === "unfinished";
4832
+ localStorage.setItem(SENTINEL_KEY, "unfinished");
4833
+ } catch {
4834
+ crashed = null;
4835
+ }
4836
+ try {
4837
+ window.addEventListener("pagehide", () => {
4838
+ try {
4839
+ localStorage.setItem(SENTINEL_KEY, "clean");
4840
+ } catch {
4841
+ }
4842
+ });
4843
+ } catch {
4844
+ }
4845
+ if (typeof bootstrapConfig.onCrashedLastRun === "function") {
4846
+ try {
4847
+ bootstrapConfig.onCrashedLastRun({ crashed });
4848
+ } catch (err) {
4849
+ log.warn("onCrashedLastRun hook threw", {
4850
+ error: err instanceof Error ? err.message : String(err)
4851
+ });
4852
+ }
4853
+ }
4854
+ }
4338
4855
  return sdk;
4339
4856
  }
4340
4857
  function mergeRuntimeConfig(config, runtime) {