glitchgrab 1.46.0 → 1.48.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
@@ -73,6 +73,37 @@ var import_react6 = require("react");
73
73
  // src/error-boundary.tsx
74
74
  var React = __toESM(require("react"));
75
75
 
76
+ // ../report-ui/dist/assist-response.mjs
77
+ var ASSIST_OFFLINE_MESSAGE = "The assistant is unavailable \u2014 write your report below and send it as normal.";
78
+ function assistOffline() {
79
+ return { conversationId: null, question: null, report: null, degraded: ASSIST_OFFLINE_MESSAGE };
80
+ }
81
+ function toAssistTurnResult(ok, body) {
82
+ var _a, _b, _c, _d, _e, _f, _g, _h;
83
+ const envelope = body != null ? body : null;
84
+ if (!ok || !(envelope == null ? void 0 : envelope.success)) {
85
+ return {
86
+ ...assistOffline(),
87
+ // The server words why (cap, rate limit, switched off). An hourly limit
88
+ // keeps the assistant button for this report.
89
+ degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : ASSIST_OFFLINE_MESSAGE,
90
+ retryable: (envelope == null ? void 0 : envelope.retryable) === true
91
+ };
92
+ }
93
+ const d = (_b = envelope.data) != null ? _b : {};
94
+ return {
95
+ conversationId: (_c = d.conversationId) != null ? _c : null,
96
+ question: (_d = d.question) != null ? _d : null,
97
+ options: Array.isArray(d.options) ? d.options.filter((o) => typeof o === "string") : [],
98
+ report: (_e = d.report) != null ? _e : null,
99
+ duplicate: (_f = d.duplicate) != null ? _f : null,
100
+ related: (_g = d.related) != null ? _g : null,
101
+ solved: (_h = d.solved) != null ? _h : null,
102
+ aboutGlitchgrab: d.aboutGlitchgrab === true,
103
+ degraded: null
104
+ };
105
+ }
106
+
76
107
  // src/redact.ts
77
108
  var SENSITIVE_KEY_RE = /(pass|pwd|secret|token|auth|session|cookie|credential|apikey|api_key|access_key|private|signature|otp|pin|cvv|card|ssn|aadhaar|pan)/i;
78
109
  var EMAIL_RE = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
@@ -747,13 +778,7 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
747
778
  }
748
779
  }
749
780
  async function assistTurn(params, token, baseUrl) {
750
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
751
- const offline = {
752
- conversationId: null,
753
- question: null,
754
- report: null,
755
- degraded: "The assistant is unavailable \u2014 write your report below and send it as normal."
756
- };
781
+ const offline = assistOffline();
757
782
  try {
758
783
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/report-chat`;
759
784
  const response = await fetch(url, {
@@ -764,27 +789,7 @@ async function assistTurn(params, token, baseUrl) {
764
789
  },
765
790
  body: JSON.stringify(params)
766
791
  });
767
- const envelope = await response.json().catch(() => null);
768
- if (!response.ok || !(envelope == null ? void 0 : envelope.success)) {
769
- return {
770
- conversationId: null,
771
- question: null,
772
- report: null,
773
- degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : offline.degraded,
774
- // An hourly limit, not a cap: the dialog keeps the assistant button.
775
- retryable: (envelope == null ? void 0 : envelope.retryable) === true
776
- };
777
- }
778
- return {
779
- conversationId: (_c = (_b = envelope.data) == null ? void 0 : _b.conversationId) != null ? _c : null,
780
- question: (_e = (_d = envelope.data) == null ? void 0 : _d.question) != null ? _e : null,
781
- options: (_g = (_f = envelope.data) == null ? void 0 : _f.options) != null ? _g : [],
782
- report: (_i = (_h = envelope.data) == null ? void 0 : _h.report) != null ? _i : null,
783
- duplicate: (_k = (_j = envelope.data) == null ? void 0 : _j.duplicate) != null ? _k : null,
784
- solved: (_m = (_l = envelope.data) == null ? void 0 : _l.solved) != null ? _m : null,
785
- aboutGlitchgrab: ((_n = envelope.data) == null ? void 0 : _n.aboutGlitchgrab) === true,
786
- degraded: null
787
- };
792
+ return toAssistTurnResult(response.ok, await response.json().catch(() => null));
788
793
  } catch (e) {
789
794
  return offline;
790
795
  }
@@ -1474,6 +1479,17 @@ var STARTERS = [
1474
1479
  "It did the wrong thing",
1475
1480
  "I wish it could\u2026"
1476
1481
  ];
1482
+ var CONFIRM_OPTION = "Yes, that's it";
1483
+ function isCheckBackQuestion(text) {
1484
+ const t2 = text.trim();
1485
+ return /^so,? you['’]?re saying\b/i.test(t2) || /\b(is that right|is that correct|did i get (that|it) right|have i got (that|it) right)\s*\??\s*$/i.test(t2);
1486
+ }
1487
+ function isAffirmativeReply(text) {
1488
+ return /^(y|ya|yah|yes|yess|yeah|yep|yup|ok|okay|sure|correct|right|exactly|confirm(ed)?|that['’]?s (it|right|correct)|yes,? that['’]?s (it|right)|haan|han|ha|ho|hmm yes|👍)[\s.!👍]*$/i.test(
1489
+ text.trim()
1490
+ );
1491
+ }
1492
+ var AUTO_FILE_SECONDS = 4;
1477
1493
  var NARROW_QUERY = "(max-width: 640px)";
1478
1494
  function useIsNarrow() {
1479
1495
  const [narrow, setNarrow] = (0, import_react3.useState)(false);
@@ -1588,12 +1604,15 @@ function AssistSheet({
1588
1604
  const [duplicate, setDuplicate] = (0, import_react3.useState)(null);
1589
1605
  const [glitchgrabOffer, setGlitchgrabOffer] = (0, import_react3.useState)(false);
1590
1606
  const [solved, setSolved] = (0, import_react3.useState)(null);
1607
+ const needsSeverity = (tile) => showSeverity && tile === "BUG" && !severity;
1591
1608
  const [phase, setPhase] = (0, import_react3.useState)(
1592
- !typePicked ? "type" : currentTile === "RATING" ? "rating" : "chat"
1609
+ () => !typePicked ? "type" : currentTile === "RATING" ? "rating" : needsSeverity(currentTile) ? "severity" : "chat"
1593
1610
  );
1611
+ const [autoFileLeft, setAutoFileLeft] = (0, import_react3.useState)(null);
1594
1612
  const scrollRef = (0, import_react3.useRef)(null);
1595
1613
  const draftRef = (0, import_react3.useRef)(null);
1596
1614
  const seededRef = (0, import_react3.useRef)(false);
1615
+ const seedPendingRef = (0, import_react3.useRef)(false);
1597
1616
  const eventsRef = (0, import_react3.useRef)([]);
1598
1617
  const aiDraftRef = (0, import_react3.useRef)(null);
1599
1618
  function logEvent(type, detail) {
@@ -1633,14 +1652,33 @@ function AssistSheet({
1633
1652
  var _a;
1634
1653
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1635
1654
  }, [phase]);
1655
+ (0, import_react3.useEffect)(() => {
1656
+ if (autoFileLeft === null) return;
1657
+ if (isSubmitting || submitted || phase !== "draft") {
1658
+ setAutoFileLeft(null);
1659
+ return;
1660
+ }
1661
+ if (autoFileLeft <= 0) {
1662
+ sendDraft(true);
1663
+ return;
1664
+ }
1665
+ const id = setTimeout(() => setAutoFileLeft((n) => n === null ? null : n - 1), 1e3);
1666
+ return () => clearTimeout(id);
1667
+ }, [autoFileLeft, isSubmitting, submitted, phase]);
1668
+ function stopAutoFile(reason) {
1669
+ if (autoFileLeft === null) return;
1670
+ setAutoFileLeft(null);
1671
+ logEvent("auto_file_stopped", reason);
1672
+ }
1636
1673
  function degradeKeepingEvents(events, message, retryable) {
1637
1674
  if (events) eventsRef.current = [...events, ...eventsRef.current];
1638
1675
  flushEvents();
1639
1676
  onDegrade(message, retryable);
1640
1677
  }
1641
- async function runTurn(history2) {
1642
- var _a, _b, _c, _d;
1678
+ async function runTurn(history2, autoFile = false) {
1679
+ var _a, _b;
1643
1680
  setBusy(true);
1681
+ setAutoFileLeft(null);
1644
1682
  let result;
1645
1683
  const events = takeEvents();
1646
1684
  try {
@@ -1683,8 +1721,9 @@ function AssistSheet({
1683
1721
  setConversationId(result.conversationId);
1684
1722
  onConversationChange == null ? void 0 : onConversationChange(result.conversationId);
1685
1723
  }
1686
- setDuplicate((_a = result.duplicate) != null ? _a : null);
1687
- onDuplicateChange == null ? void 0 : onDuplicateChange((_c = (_b = result.duplicate) == null ? void 0 : _b.number) != null ? _c : null);
1724
+ const attach = result.duplicate ? { ...result.duplicate, kind: "duplicate" } : result.related ? { ...result.related, kind: "related" } : null;
1725
+ setDuplicate(attach);
1726
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_a = attach == null ? void 0 : attach.number) != null ? _a : null, attach == null ? void 0 : attach.kind);
1688
1727
  setGlitchgrabOffer(!!result.aboutGlitchgrab && !!onReportGlitchgrabProblem && !!result.question);
1689
1728
  if (result.solved) {
1690
1729
  setSolved(result.solved);
@@ -1699,11 +1738,14 @@ function AssistSheet({
1699
1738
  setMessages([...history2, { role: "assistant", content: result.report, kind: "report" }]);
1700
1739
  setOptions([]);
1701
1740
  setPhase("draft");
1741
+ if (autoFile && !attach && !(severityAsked && !severity)) {
1742
+ setAutoFileLeft(AUTO_FILE_SECONDS);
1743
+ }
1702
1744
  return;
1703
1745
  }
1704
1746
  if (result.question) {
1705
1747
  setMessages([...history2, { role: "assistant", content: result.question }]);
1706
- setOptions((_d = result.options) != null ? _d : []);
1748
+ setOptions((_b = result.options) != null ? _b : []);
1707
1749
  return;
1708
1750
  }
1709
1751
  onDegrade("The assistant had nothing to add \u2014 write your report below and send it as normal.");
@@ -1712,13 +1754,34 @@ function AssistSheet({
1712
1754
  if (seededRef.current) return;
1713
1755
  seededRef.current = true;
1714
1756
  if (!typePicked || currentTile === "RATING") return;
1757
+ if (phase === "severity") {
1758
+ seedPendingRef.current = true;
1759
+ return;
1760
+ }
1761
+ seedFromDescription();
1762
+ }, []);
1763
+ function seedFromDescription() {
1715
1764
  const seed = description.trim();
1716
1765
  if (!seed) return;
1717
1766
  const history2 = [{ role: "user", content: seed }];
1718
1767
  setMessages(history2);
1719
1768
  void runTurn(history2);
1720
- }, []);
1721
- function send(text) {
1769
+ }
1770
+ function answerSeverity(value) {
1771
+ onSeverityChange(value);
1772
+ setPhase("chat");
1773
+ if (seedPendingRef.current) {
1774
+ seedPendingRef.current = false;
1775
+ seedFromDescription();
1776
+ }
1777
+ }
1778
+ function sendTyped(text) {
1779
+ var _a;
1780
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
1781
+ const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a : "");
1782
+ send(text, checkingBack && isAffirmativeReply(text));
1783
+ }
1784
+ function send(text, autoFile = false) {
1722
1785
  const value = text.trim();
1723
1786
  if (!value || busy) return;
1724
1787
  setOptions([]);
@@ -1733,11 +1796,11 @@ function AssistSheet({
1733
1796
  const history2 = [...base, { role: "user", content: value }];
1734
1797
  setMessages(history2);
1735
1798
  setInput("");
1736
- void runTurn(history2);
1799
+ void runTurn(history2, autoFile);
1737
1800
  }
1738
1801
  function pickType(tile) {
1739
1802
  onPickType(tile);
1740
- setPhase(tile === "RATING" ? "rating" : "chat");
1803
+ setPhase(tile === "RATING" ? "rating" : needsSeverity(tile) ? "severity" : "chat");
1741
1804
  }
1742
1805
  function withCurrentDraft(history2) {
1743
1806
  const current = description.trim();
@@ -1750,9 +1813,10 @@ function AssistSheet({
1750
1813
  }
1751
1814
  return next;
1752
1815
  }
1753
- function sendDraft() {
1816
+ function sendDraft(auto = false) {
1817
+ setAutoFileLeft(null);
1754
1818
  const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1755
- logEvent("draft_sent", edited ? "edited" : "as drafted");
1819
+ logEvent("draft_sent", auto ? "auto-filed" : edited ? "edited" : "as drafted");
1756
1820
  flushEvents();
1757
1821
  onSend();
1758
1822
  }
@@ -1776,15 +1840,15 @@ function AssistSheet({
1776
1840
  minWidth: 0
1777
1841
  },
1778
1842
  children: [
1779
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.text, fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1843
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.text, fontWeight: 600 }, children: duplicate.kind === "related" ? title ? "Your request goes onto this issue" : `Connected to #${duplicate.number}` : title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1780
1844
  title && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1781
1845
  "#",
1782
1846
  duplicate.number,
1783
1847
  " ",
1784
1848
  duplicate.title
1785
1849
  ] }),
1786
- duplicate.status && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.text, fontWeight: 600 }, children: duplicate.status }),
1787
- footer && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.textMuted }, children: footer })
1850
+ duplicate.status && duplicate.kind !== "related" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.text, fontWeight: 600 }, children: duplicate.status }),
1851
+ footer && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.textMuted }, children: duplicate.kind === "related" ? "Added as a separate request on it, not a repeat \u2014 the team sees both." : footer })
1788
1852
  ]
1789
1853
  }
1790
1854
  );
@@ -1812,6 +1876,7 @@ function AssistSheet({
1812
1876
  @keyframes gg-sheet-in{from{transform:translateX(24px);opacity:0}to{transform:translateX(0);opacity:1}}
1813
1877
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
1814
1878
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
1879
+ @keyframes gg-autofile{from{transform:scaleX(0)}to{transform:scaleX(1)}}
1815
1880
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
1816
1881
  [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible,[data-gg-assist-remove]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
1817
1882
  ` }),
@@ -1906,6 +1971,7 @@ function AssistSheet({
1906
1971
  ),
1907
1972
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { fontSize: "11.5px", color: t2.textMuted, marginTop: "3px" }, children: [
1908
1973
  phase === "type" ? null : reportTypeLabel,
1974
+ phase !== "type" && severityAsked && severity ? ` \xB7 ${SEVERITY_LABELS[severity]}` : null,
1909
1975
  projectSlot ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { marginLeft: "6px" }, children: projectSlot }) : null
1910
1976
  ] })
1911
1977
  ] }),
@@ -2024,6 +2090,62 @@ function AssistSheet({
2024
2090
  }
2025
2091
  )
2026
2092
  ] }),
2093
+ phase === "severity" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2094
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2095
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Avatar, { who: "assistant", theme: t2 }),
2096
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2097
+ "div",
2098
+ {
2099
+ style: {
2100
+ padding: "9px 12px",
2101
+ borderRadius: "12px 12px 12px 4px",
2102
+ backgroundColor: t2.bgSecondary,
2103
+ fontSize: "13px",
2104
+ lineHeight: 1.55,
2105
+ maxWidth: "88%"
2106
+ },
2107
+ children: "How bad is it?"
2108
+ }
2109
+ )
2110
+ ] }),
2111
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2112
+ "div",
2113
+ {
2114
+ role: "radiogroup",
2115
+ "aria-label": "Severity",
2116
+ style: {
2117
+ display: "flex",
2118
+ flexWrap: "wrap",
2119
+ gap: "6px",
2120
+ paddingLeft: "32px"
2121
+ },
2122
+ children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2123
+ "button",
2124
+ {
2125
+ type: "button",
2126
+ role: "radio",
2127
+ "aria-checked": severity === s,
2128
+ "data-gg-chip": "",
2129
+ "data-gg-severity": s,
2130
+ onClick: () => answerSeverity(s),
2131
+ style: {
2132
+ border: `1px solid ${t2.inputBorder}`,
2133
+ background: "transparent",
2134
+ color: t2.textMuted,
2135
+ borderRadius: "999px",
2136
+ // Same as the picker and option chips — one chip size.
2137
+ padding: "8px 12px",
2138
+ fontSize: "12px",
2139
+ fontFamily: "inherit",
2140
+ cursor: "pointer"
2141
+ },
2142
+ children: SEVERITY_LABELS[s]
2143
+ },
2144
+ s
2145
+ ))
2146
+ }
2147
+ )
2148
+ ] }),
2027
2149
  phase === "rating" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2028
2150
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2029
2151
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Avatar, { who: "assistant", theme: t2 }),
@@ -2212,7 +2334,11 @@ function AssistSheet({
2212
2334
  color: t2.text,
2213
2335
  border: live ? "none" : `1px solid ${t2.accent}66`
2214
2336
  },
2215
- children: live ? "Here's your report \u2014 have a read." : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2337
+ children: live ? (
2338
+ // #402: "have a read" beside a countdown reads as a
2339
+ // contradiction. Say what is about to happen instead.
2340
+ autoFileLeft !== null ? "Here's your report \u2014 I'll file it unless you want to change something." : "Here's your report \u2014 have a read."
2341
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2216
2342
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2217
2343
  "span",
2218
2344
  {
@@ -2379,7 +2505,7 @@ function AssistSheet({
2379
2505
  "data-gg-option": "",
2380
2506
  onClick: () => {
2381
2507
  logEvent("option_tap", option);
2382
- send(option);
2508
+ send(option, option === CONFIRM_OPTION);
2383
2509
  },
2384
2510
  style: {
2385
2511
  border: `1px solid ${t2.inputBorder}`,
@@ -2454,7 +2580,7 @@ function AssistSheet({
2454
2580
  textTransform: "uppercase",
2455
2581
  color: t2.accent
2456
2582
  },
2457
- children: duplicate ? `What we'll add to #${duplicate.number} \xB7 edit anything` : "Your report \xB7 edit anything"
2583
+ children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
2458
2584
  }
2459
2585
  ),
2460
2586
  renderKnownIssue({
@@ -2466,7 +2592,10 @@ function AssistSheet({
2466
2592
  {
2467
2593
  ref: draftRef,
2468
2594
  value: description,
2469
- onChange: (e) => onDescriptionChange(e.target.value),
2595
+ onChange: (e) => {
2596
+ stopAutoFile("edited the draft");
2597
+ onDescriptionChange(e.target.value);
2598
+ },
2470
2599
  rows: 6,
2471
2600
  style: {
2472
2601
  width: "100%",
@@ -2485,7 +2614,7 @@ function AssistSheet({
2485
2614
  }
2486
2615
  }
2487
2616
  ),
2488
- severityAsked && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
2617
+ severityAsked && autoFileLeft === null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
2489
2618
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2490
2619
  "span",
2491
2620
  {
@@ -2607,7 +2736,7 @@ function AssistSheet({
2607
2736
  )
2608
2737
  }
2609
2738
  ),
2610
- !submitted && phase !== "type" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2739
+ !submitted && phase !== "type" && phase !== "severity" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2611
2740
  "div",
2612
2741
  {
2613
2742
  style: {
@@ -2714,6 +2843,7 @@ function AssistSheet({
2714
2843
  "aria-label": `Remove image ${i + 1}`,
2715
2844
  title: "Remove this image",
2716
2845
  onClick: () => {
2846
+ stopAutoFile("changed attachments");
2717
2847
  logEvent(
2718
2848
  "image_removed",
2719
2849
  pageShot && shot === pageShot ? "page capture" : "attached"
@@ -2824,6 +2954,7 @@ function AssistSheet({
2824
2954
  "aria-label": `Remove file ${file.name}`,
2825
2955
  title: "Remove this file",
2826
2956
  onClick: () => {
2957
+ stopAutoFile("changed attachments");
2827
2958
  logEvent("file_removed", read ? "read" : "not read");
2828
2959
  onRemoveFile(i);
2829
2960
  },
@@ -2906,6 +3037,7 @@ function AssistSheet({
2906
3037
  var _a;
2907
3038
  const picked = Array.from((_a = e.target.files) != null ? _a : []);
2908
3039
  if (picked.length) {
3040
+ stopAutoFile("changed attachments");
2909
3041
  const images = picked.filter((f) => f.type.startsWith("image/")).length;
2910
3042
  if (images) logEvent("image_added", String(images));
2911
3043
  if (picked.length > images) {
@@ -2935,11 +3067,14 @@ function AssistSheet({
2935
3067
  "textarea",
2936
3068
  {
2937
3069
  value: input,
2938
- onChange: (e) => setInput(e.target.value),
3070
+ onChange: (e) => {
3071
+ stopAutoFile("typing a change");
3072
+ setInput(e.target.value);
3073
+ },
2939
3074
  onKeyDown: (e) => {
2940
3075
  if (e.key === "Enter" && !e.shiftKey) {
2941
3076
  e.preventDefault();
2942
- send(input);
3077
+ sendTyped(input);
2943
3078
  }
2944
3079
  },
2945
3080
  placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
@@ -2969,7 +3104,7 @@ function AssistSheet({
2969
3104
  {
2970
3105
  type: "button",
2971
3106
  "data-gg-sheet-send": "",
2972
- onClick: () => send(input),
3107
+ onClick: () => sendTyped(input),
2973
3108
  disabled: busy || !input.trim(),
2974
3109
  "aria-label": "Send message",
2975
3110
  "data-gg-send-message": "",
@@ -3008,12 +3143,87 @@ function AssistSheet({
3008
3143
  children: validationError
3009
3144
  }
3010
3145
  ),
3011
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3146
+ autoFileLeft !== null ? (
3147
+ // #402: "Yes, that's it" was the confirmation. The draft
3148
+ // files itself unless they stop it — the bar shows how
3149
+ // long is left, the button sends now.
3150
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px", marginTop: "8px" }, children: [
3151
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
3152
+ "button",
3153
+ {
3154
+ type: "button",
3155
+ "data-gg-sheet-send": "",
3156
+ "data-gg-auto-file": "",
3157
+ onClick: () => sendDraft(),
3158
+ style: {
3159
+ flex: 1,
3160
+ minWidth: 0,
3161
+ position: "relative",
3162
+ overflow: "hidden",
3163
+ padding: "12px",
3164
+ borderRadius: "10px",
3165
+ border: "none",
3166
+ backgroundColor: t2.accent,
3167
+ color: t2.accentText,
3168
+ fontSize: "14px",
3169
+ fontWeight: 700,
3170
+ fontFamily: "inherit",
3171
+ cursor: "pointer"
3172
+ },
3173
+ children: [
3174
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3175
+ "span",
3176
+ {
3177
+ "aria-hidden": "true",
3178
+ style: {
3179
+ position: "absolute",
3180
+ inset: 0,
3181
+ backgroundColor: "rgba(255,255,255,0.22)",
3182
+ transformOrigin: "left",
3183
+ animation: `gg-autofile ${AUTO_FILE_SECONDS}s linear forwards`
3184
+ }
3185
+ }
3186
+ ),
3187
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { position: "relative" }, children: [
3188
+ "Filing in ",
3189
+ autoFileLeft,
3190
+ "s\u2026"
3191
+ ] })
3192
+ ]
3193
+ }
3194
+ ),
3195
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3196
+ "button",
3197
+ {
3198
+ type: "button",
3199
+ "data-gg-edit-first": "",
3200
+ onClick: () => {
3201
+ var _a;
3202
+ stopAutoFile("edit first");
3203
+ (_a = draftRef.current) == null ? void 0 : _a.focus();
3204
+ },
3205
+ style: {
3206
+ flexShrink: 0,
3207
+ padding: "12px 14px",
3208
+ borderRadius: "10px",
3209
+ border: `1px solid ${t2.inputBorder}`,
3210
+ background: "transparent",
3211
+ color: t2.text,
3212
+ fontSize: "13px",
3213
+ fontWeight: 600,
3214
+ fontFamily: "inherit",
3215
+ cursor: "pointer"
3216
+ },
3217
+ children: "Edit first"
3218
+ }
3219
+ )
3220
+ ] })
3221
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3012
3222
  "button",
3013
3223
  {
3014
3224
  type: "button",
3015
3225
  "data-gg-sheet-send": "",
3016
- onClick: sendDraft,
3226
+ onClick: () => sendDraft(),
3017
3227
  disabled: isSubmitting || !description.trim(),
3018
3228
  style: {
3019
3229
  width: "100%",
@@ -3989,9 +4199,11 @@ function ReportDialog({
3989
4199
  reporter,
3990
4200
  onClose,
3991
4201
  headerSlot,
3992
- reportGlitchgrabProblem
4202
+ reportGlitchgrabProblem,
4203
+ layout = "modal"
3993
4204
  }) {
3994
4205
  var _a;
4206
+ const sheet = layout === "sheet";
3995
4207
  const [isEnhancing, setIsEnhancing] = (0, import_react.useState)(false);
3996
4208
  const [assistOpen, setAssistOpen] = (0, import_react.useState)(false);
3997
4209
  const [assistNotice, setAssistNotice] = (0, import_react.useState)(null);
@@ -4000,6 +4212,8 @@ function ReportDialog({
4000
4212
  const [selfReportPrefill, setSelfReportPrefill] = (0, import_react.useState)("");
4001
4213
  const [typeChosen, setTypeChosen] = (0, import_react.useState)(false);
4002
4214
  const [duplicateIssueNumber, setDuplicateIssueNumber] = (0, import_react.useState)(null);
4215
+ const [duplicateKind, setDuplicateKind] = (0, import_react.useState)("duplicate");
4216
+ const [addedAsRelated, setAddedAsRelated] = (0, import_react.useState)(false);
4003
4217
  const [similarIssues, setSimilarIssues] = (0, import_react.useState)(null);
4004
4218
  const [checkingSimilar, setCheckingSimilar] = (0, import_react.useState)(false);
4005
4219
  const [attachingTo, setAttachingTo] = (0, import_react.useState)(null);
@@ -4436,6 +4650,7 @@ function ReportDialog({
4436
4650
  setReportType("BUG");
4437
4651
  setTypeChosen(false);
4438
4652
  setDuplicateIssueNumber(null);
4653
+ setDuplicateKind("duplicate");
4439
4654
  setSimilarIssues(null);
4440
4655
  setPickedSimilar(null);
4441
4656
  setCheckingSimilar(false);
@@ -4628,7 +4843,7 @@ function ReportDialog({
4628
4843
  }
4629
4844
  };
4630
4845
  const handleSubmit = async (opts) => {
4631
- var _a2, _b;
4846
+ var _a2, _b, _c;
4632
4847
  try {
4633
4848
  if (isSubmitting) return;
4634
4849
  if (isRating) {
@@ -4666,6 +4881,7 @@ function ReportDialog({
4666
4881
  return;
4667
4882
  }
4668
4883
  const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
4884
+ const attachKind = (opts == null ? void 0 : opts.duplicateOf) ? (_b = opts.kind) != null ? _b : "duplicate" : duplicateKind;
4669
4885
  if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
4670
4886
  setIsSubmitting(true);
4671
4887
  setCheckingSimilar(true);
@@ -4701,6 +4917,7 @@ function ReportDialog({
4701
4917
  }
4702
4918
  if (duplicateOf) {
4703
4919
  metadata.duplicateIssueNumber = String(duplicateOf);
4920
+ if (attachKind === "related") metadata.duplicateKind = "related";
4704
4921
  }
4705
4922
  if (aiConversationId) {
4706
4923
  metadata.aiConversationId = aiConversationId;
@@ -4714,14 +4931,16 @@ function ReportDialog({
4714
4931
  );
4715
4932
  if (result) {
4716
4933
  setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
4934
+ setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
4717
4935
  const queued = result.status === "QUEUED" && !!result.message;
4718
- setQueuedMessage(queued ? (_b = result.message) != null ? _b : null : null);
4936
+ setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
4719
4937
  setSubmitted(true);
4720
4938
  setDescription("");
4721
4939
  setScreenshots([]);
4722
4940
  setAttachments([]);
4723
4941
  setAiConversationId(null);
4724
4942
  setDuplicateIssueNumber(null);
4943
+ setDuplicateKind("duplicate");
4725
4944
  setSimilarIssues(null);
4726
4945
  setPickedSimilar(null);
4727
4946
  setTimeout(() => {
@@ -4792,8 +5011,8 @@ function ReportDialog({
4792
5011
  // dialog's state IS the report, and unmounting would drop the
4793
5012
  // screenshots, attachments and step the reporter is on.
4794
5013
  display: sheetUp ? "none" : "flex",
4795
- alignItems: "center",
4796
- justifyContent: "center",
5014
+ alignItems: sheet ? "stretch" : "center",
5015
+ justifyContent: sheet ? "flex-end" : "center",
4797
5016
  backgroundColor: "rgba(0,0,0,0.5)",
4798
5017
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
4799
5018
  },
@@ -4813,14 +5032,15 @@ function ReportDialog({
4813
5032
  style: {
4814
5033
  position: "relative",
4815
5034
  zIndex: 2147483647,
4816
- width: "420px",
4817
- maxWidth: "calc(100% - 32px)",
4818
- maxHeight: "calc(100dvh - 32px)",
5035
+ width: sheet ? "440px" : "420px",
5036
+ maxWidth: sheet ? "100%" : "calc(100% - 32px)",
5037
+ maxHeight: sheet ? "100dvh" : "calc(100dvh - 32px)",
5038
+ ...sheet ? { height: "100dvh", borderLeft: `1px solid ${t2.inputBorder}` } : {},
4819
5039
  display: "flex",
4820
5040
  flexDirection: "column",
4821
5041
  backgroundColor: t2.bg,
4822
- borderRadius: "12px",
4823
- boxShadow: "0 20px 60px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.1)",
5042
+ borderRadius: sheet ? 0 : "12px",
5043
+ boxShadow: sheet ? "0 20px 60px rgba(0, 0, 0, 0.35)" : "0 20px 60px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.1)",
4824
5044
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
4825
5045
  overflow: "hidden",
4826
5046
  color: t2.text,
@@ -4854,7 +5074,7 @@ function ReportDialog({
4854
5074
  gap: "8px"
4855
5075
  },
4856
5076
  children: [
4857
- step > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
5077
+ step > 1 && availableTypes.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
4858
5078
  "button",
4859
5079
  {
4860
5080
  type: "button",
@@ -4972,7 +5192,7 @@ function ReportDialog({
4972
5192
  ]
4973
5193
  }
4974
5194
  ),
4975
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
5195
+ availableTypes.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
4976
5196
  "div",
4977
5197
  {
4978
5198
  style: {
@@ -5049,7 +5269,7 @@ function ReportDialog({
5049
5269
  },
5050
5270
  children: queuedMessage
5051
5271
  }
5052
- ) : addedToIssue ? `Added to #${addedToIssue}. Thank you!` : `${getTypeLabel(reportType)} sent. Thank you!`
5272
+ ) : addedToIssue ? `Added to #${addedToIssue}${addedAsRelated ? " as a related request" : ""}. Thank you!` : `${getTypeLabel(reportType)} sent. Thank you!`
5053
5273
  }
5054
5274
  ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
5055
5275
  step === 1 && headerSlot ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginBottom: "14px" }, children: headerSlot }) : null,
@@ -5248,6 +5468,7 @@ function ReportDialog({
5248
5468
  "button",
5249
5469
  {
5250
5470
  type: "button",
5471
+ disabled: availableTypes.length === 1,
5251
5472
  onClick: () => setStep(1),
5252
5473
  style: {
5253
5474
  display: "inline-flex",
@@ -5262,12 +5483,12 @@ function ReportDialog({
5262
5483
  fontSize: "11px",
5263
5484
  fontWeight: 600,
5264
5485
  fontFamily: "inherit",
5265
- cursor: "pointer"
5486
+ cursor: availableTypes.length === 1 ? "default" : "pointer"
5266
5487
  },
5267
5488
  children: [
5268
5489
  getTypeIcon(reportType, isRating ? "#f59e0b" : t2.accent, 13),
5269
5490
  getTypeLabel(reportType),
5270
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.textMuted, fontWeight: 400 }, children: "\xB7 change" })
5491
+ availableTypes.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.textMuted, fontWeight: 400 }, children: "\xB7 change" })
5271
5492
  ]
5272
5493
  }
5273
5494
  ),
@@ -6294,8 +6515,8 @@ function ReportDialog({
6294
6515
  },
6295
6516
  children: [
6296
6517
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6297
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this" }),
6298
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.textMuted, fontSize: "12px", lineHeight: 1.5 }, children: similarIssues.length > 1 ? "Pick the one that matches \u2014 your report is added to it, so the team sees everyone hitting it in one place." : "If it's the same problem, pick it \u2014 your report is added to it, so the team sees everyone hitting it in one place." })
6518
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
6519
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.textMuted, fontSize: "12px", lineHeight: 1.5 }, children: similarIssues.length > 1 ? "Pick the one that matches. Same problem, or a different request for the same feature \u2014 it's added to that issue, so the team has it in one place." : "Same problem, or a different request for the same feature? Pick it \u2014 it's added to that issue, so the team has it in one place." })
6299
6520
  ] }),
6300
6521
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6301
6522
  "div",
@@ -6400,7 +6621,8 @@ function ReportDialog({
6400
6621
  if (!pickedSimilar) return;
6401
6622
  setAttachingTo(pickedSimilar);
6402
6623
  setDuplicateIssueNumber(pickedSimilar);
6403
- void handleSubmit({ duplicateOf: pickedSimilar });
6624
+ setDuplicateKind("duplicate");
6625
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
6404
6626
  },
6405
6627
  style: {
6406
6628
  width: "100%",
@@ -6415,7 +6637,7 @@ function ReportDialog({
6415
6637
  cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
6416
6638
  fontFamily: "inherit"
6417
6639
  },
6418
- children: typeof attachingTo === "number" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6640
+ children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6419
6641
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6420
6642
  "Adding to #",
6421
6643
  attachingTo,
@@ -6423,6 +6645,39 @@ function ReportDialog({
6423
6645
  ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
6424
6646
  }
6425
6647
  ),
6648
+ pickedSimilar && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6649
+ "button",
6650
+ {
6651
+ type: "button",
6652
+ disabled: isSubmitting,
6653
+ onClick: () => {
6654
+ setAttachingTo(pickedSimilar);
6655
+ setDuplicateIssueNumber(pickedSimilar);
6656
+ setDuplicateKind("related");
6657
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
6658
+ },
6659
+ style: {
6660
+ width: "100%",
6661
+ minHeight: "40px",
6662
+ marginTop: "-4px",
6663
+ padding: "9px 10px",
6664
+ borderRadius: "8px",
6665
+ border: "1px solid rgba(245,158,11,0.4)",
6666
+ backgroundColor: "transparent",
6667
+ color: isSubmitting ? t2.textMuted : t2.text,
6668
+ fontSize: "13px",
6669
+ fontWeight: 600,
6670
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6671
+ fontFamily: "inherit"
6672
+ },
6673
+ children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6674
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6675
+ "Adding to #",
6676
+ attachingTo,
6677
+ "\u2026"
6678
+ ] }) : `Related request \u2014 add to #${pickedSimilar}`
6679
+ }
6680
+ ),
6426
6681
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6427
6682
  "button",
6428
6683
  {
@@ -6769,7 +7024,10 @@ function ReportDialog({
6769
7024
  setTypeChosen(true);
6770
7025
  setStep(2);
6771
7026
  },
6772
- onDuplicateChange: setDuplicateIssueNumber,
7027
+ onDuplicateChange: (issueNumber, kind) => {
7028
+ setDuplicateIssueNumber(issueNumber);
7029
+ setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7030
+ },
6773
7031
  onConversationChange: setAiConversationId,
6774
7032
  onFinish: handleClose,
6775
7033
  rating,