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.mjs CHANGED
@@ -14,6 +14,37 @@ import {
14
14
  // src/error-boundary.tsx
15
15
  import * as React from "react";
16
16
 
17
+ // ../report-ui/dist/assist-response.mjs
18
+ var ASSIST_OFFLINE_MESSAGE = "The assistant is unavailable \u2014 write your report below and send it as normal.";
19
+ function assistOffline() {
20
+ return { conversationId: null, question: null, report: null, degraded: ASSIST_OFFLINE_MESSAGE };
21
+ }
22
+ function toAssistTurnResult(ok, body) {
23
+ var _a, _b, _c, _d, _e, _f, _g, _h;
24
+ const envelope = body != null ? body : null;
25
+ if (!ok || !(envelope == null ? void 0 : envelope.success)) {
26
+ return {
27
+ ...assistOffline(),
28
+ // The server words why (cap, rate limit, switched off). An hourly limit
29
+ // keeps the assistant button for this report.
30
+ degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : ASSIST_OFFLINE_MESSAGE,
31
+ retryable: (envelope == null ? void 0 : envelope.retryable) === true
32
+ };
33
+ }
34
+ const d = (_b = envelope.data) != null ? _b : {};
35
+ return {
36
+ conversationId: (_c = d.conversationId) != null ? _c : null,
37
+ question: (_d = d.question) != null ? _d : null,
38
+ options: Array.isArray(d.options) ? d.options.filter((o) => typeof o === "string") : [],
39
+ report: (_e = d.report) != null ? _e : null,
40
+ duplicate: (_f = d.duplicate) != null ? _f : null,
41
+ related: (_g = d.related) != null ? _g : null,
42
+ solved: (_h = d.solved) != null ? _h : null,
43
+ aboutGlitchgrab: d.aboutGlitchgrab === true,
44
+ degraded: null
45
+ };
46
+ }
47
+
17
48
  // src/redact.ts
18
49
  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;
19
50
  var EMAIL_RE = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
@@ -688,13 +719,7 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
688
719
  }
689
720
  }
690
721
  async function assistTurn(params, token, baseUrl) {
691
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
692
- const offline = {
693
- conversationId: null,
694
- question: null,
695
- report: null,
696
- degraded: "The assistant is unavailable \u2014 write your report below and send it as normal."
697
- };
722
+ const offline = assistOffline();
698
723
  try {
699
724
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/report-chat`;
700
725
  const response = await fetch(url, {
@@ -705,27 +730,7 @@ async function assistTurn(params, token, baseUrl) {
705
730
  },
706
731
  body: JSON.stringify(params)
707
732
  });
708
- const envelope = await response.json().catch(() => null);
709
- if (!response.ok || !(envelope == null ? void 0 : envelope.success)) {
710
- return {
711
- conversationId: null,
712
- question: null,
713
- report: null,
714
- degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : offline.degraded,
715
- // An hourly limit, not a cap: the dialog keeps the assistant button.
716
- retryable: (envelope == null ? void 0 : envelope.retryable) === true
717
- };
718
- }
719
- return {
720
- conversationId: (_c = (_b = envelope.data) == null ? void 0 : _b.conversationId) != null ? _c : null,
721
- question: (_e = (_d = envelope.data) == null ? void 0 : _d.question) != null ? _e : null,
722
- options: (_g = (_f = envelope.data) == null ? void 0 : _f.options) != null ? _g : [],
723
- report: (_i = (_h = envelope.data) == null ? void 0 : _h.report) != null ? _i : null,
724
- duplicate: (_k = (_j = envelope.data) == null ? void 0 : _j.duplicate) != null ? _k : null,
725
- solved: (_m = (_l = envelope.data) == null ? void 0 : _l.solved) != null ? _m : null,
726
- aboutGlitchgrab: ((_n = envelope.data) == null ? void 0 : _n.aboutGlitchgrab) === true,
727
- degraded: null
728
- };
733
+ return toAssistTurnResult(response.ok, await response.json().catch(() => null));
729
734
  } catch (e) {
730
735
  return offline;
731
736
  }
@@ -1420,6 +1425,17 @@ var STARTERS = [
1420
1425
  "It did the wrong thing",
1421
1426
  "I wish it could\u2026"
1422
1427
  ];
1428
+ var CONFIRM_OPTION = "Yes, that's it";
1429
+ function isCheckBackQuestion(text) {
1430
+ const t2 = text.trim();
1431
+ 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);
1432
+ }
1433
+ function isAffirmativeReply(text) {
1434
+ 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(
1435
+ text.trim()
1436
+ );
1437
+ }
1438
+ var AUTO_FILE_SECONDS = 4;
1423
1439
  var NARROW_QUERY = "(max-width: 640px)";
1424
1440
  function useIsNarrow() {
1425
1441
  const [narrow, setNarrow] = useState2(false);
@@ -1534,12 +1550,15 @@ function AssistSheet({
1534
1550
  const [duplicate, setDuplicate] = useState2(null);
1535
1551
  const [glitchgrabOffer, setGlitchgrabOffer] = useState2(false);
1536
1552
  const [solved, setSolved] = useState2(null);
1553
+ const needsSeverity = (tile) => showSeverity && tile === "BUG" && !severity;
1537
1554
  const [phase, setPhase] = useState2(
1538
- !typePicked ? "type" : currentTile === "RATING" ? "rating" : "chat"
1555
+ () => !typePicked ? "type" : currentTile === "RATING" ? "rating" : needsSeverity(currentTile) ? "severity" : "chat"
1539
1556
  );
1557
+ const [autoFileLeft, setAutoFileLeft] = useState2(null);
1540
1558
  const scrollRef = useRef2(null);
1541
1559
  const draftRef = useRef2(null);
1542
1560
  const seededRef = useRef2(false);
1561
+ const seedPendingRef = useRef2(false);
1543
1562
  const eventsRef = useRef2([]);
1544
1563
  const aiDraftRef = useRef2(null);
1545
1564
  function logEvent(type, detail) {
@@ -1579,14 +1598,33 @@ function AssistSheet({
1579
1598
  var _a;
1580
1599
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1581
1600
  }, [phase]);
1601
+ useEffect2(() => {
1602
+ if (autoFileLeft === null) return;
1603
+ if (isSubmitting || submitted || phase !== "draft") {
1604
+ setAutoFileLeft(null);
1605
+ return;
1606
+ }
1607
+ if (autoFileLeft <= 0) {
1608
+ sendDraft(true);
1609
+ return;
1610
+ }
1611
+ const id = setTimeout(() => setAutoFileLeft((n) => n === null ? null : n - 1), 1e3);
1612
+ return () => clearTimeout(id);
1613
+ }, [autoFileLeft, isSubmitting, submitted, phase]);
1614
+ function stopAutoFile(reason) {
1615
+ if (autoFileLeft === null) return;
1616
+ setAutoFileLeft(null);
1617
+ logEvent("auto_file_stopped", reason);
1618
+ }
1582
1619
  function degradeKeepingEvents(events, message, retryable) {
1583
1620
  if (events) eventsRef.current = [...events, ...eventsRef.current];
1584
1621
  flushEvents();
1585
1622
  onDegrade(message, retryable);
1586
1623
  }
1587
- async function runTurn(history2) {
1588
- var _a, _b, _c, _d;
1624
+ async function runTurn(history2, autoFile = false) {
1625
+ var _a, _b;
1589
1626
  setBusy(true);
1627
+ setAutoFileLeft(null);
1590
1628
  let result;
1591
1629
  const events = takeEvents();
1592
1630
  try {
@@ -1629,8 +1667,9 @@ function AssistSheet({
1629
1667
  setConversationId(result.conversationId);
1630
1668
  onConversationChange == null ? void 0 : onConversationChange(result.conversationId);
1631
1669
  }
1632
- setDuplicate((_a = result.duplicate) != null ? _a : null);
1633
- onDuplicateChange == null ? void 0 : onDuplicateChange((_c = (_b = result.duplicate) == null ? void 0 : _b.number) != null ? _c : null);
1670
+ const attach = result.duplicate ? { ...result.duplicate, kind: "duplicate" } : result.related ? { ...result.related, kind: "related" } : null;
1671
+ setDuplicate(attach);
1672
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_a = attach == null ? void 0 : attach.number) != null ? _a : null, attach == null ? void 0 : attach.kind);
1634
1673
  setGlitchgrabOffer(!!result.aboutGlitchgrab && !!onReportGlitchgrabProblem && !!result.question);
1635
1674
  if (result.solved) {
1636
1675
  setSolved(result.solved);
@@ -1645,11 +1684,14 @@ function AssistSheet({
1645
1684
  setMessages([...history2, { role: "assistant", content: result.report, kind: "report" }]);
1646
1685
  setOptions([]);
1647
1686
  setPhase("draft");
1687
+ if (autoFile && !attach && !(severityAsked && !severity)) {
1688
+ setAutoFileLeft(AUTO_FILE_SECONDS);
1689
+ }
1648
1690
  return;
1649
1691
  }
1650
1692
  if (result.question) {
1651
1693
  setMessages([...history2, { role: "assistant", content: result.question }]);
1652
- setOptions((_d = result.options) != null ? _d : []);
1694
+ setOptions((_b = result.options) != null ? _b : []);
1653
1695
  return;
1654
1696
  }
1655
1697
  onDegrade("The assistant had nothing to add \u2014 write your report below and send it as normal.");
@@ -1658,13 +1700,34 @@ function AssistSheet({
1658
1700
  if (seededRef.current) return;
1659
1701
  seededRef.current = true;
1660
1702
  if (!typePicked || currentTile === "RATING") return;
1703
+ if (phase === "severity") {
1704
+ seedPendingRef.current = true;
1705
+ return;
1706
+ }
1707
+ seedFromDescription();
1708
+ }, []);
1709
+ function seedFromDescription() {
1661
1710
  const seed = description.trim();
1662
1711
  if (!seed) return;
1663
1712
  const history2 = [{ role: "user", content: seed }];
1664
1713
  setMessages(history2);
1665
1714
  void runTurn(history2);
1666
- }, []);
1667
- function send(text) {
1715
+ }
1716
+ function answerSeverity(value) {
1717
+ onSeverityChange(value);
1718
+ setPhase("chat");
1719
+ if (seedPendingRef.current) {
1720
+ seedPendingRef.current = false;
1721
+ seedFromDescription();
1722
+ }
1723
+ }
1724
+ function sendTyped(text) {
1725
+ var _a;
1726
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
1727
+ const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a : "");
1728
+ send(text, checkingBack && isAffirmativeReply(text));
1729
+ }
1730
+ function send(text, autoFile = false) {
1668
1731
  const value = text.trim();
1669
1732
  if (!value || busy) return;
1670
1733
  setOptions([]);
@@ -1679,11 +1742,11 @@ function AssistSheet({
1679
1742
  const history2 = [...base, { role: "user", content: value }];
1680
1743
  setMessages(history2);
1681
1744
  setInput("");
1682
- void runTurn(history2);
1745
+ void runTurn(history2, autoFile);
1683
1746
  }
1684
1747
  function pickType(tile) {
1685
1748
  onPickType(tile);
1686
- setPhase(tile === "RATING" ? "rating" : "chat");
1749
+ setPhase(tile === "RATING" ? "rating" : needsSeverity(tile) ? "severity" : "chat");
1687
1750
  }
1688
1751
  function withCurrentDraft(history2) {
1689
1752
  const current = description.trim();
@@ -1696,9 +1759,10 @@ function AssistSheet({
1696
1759
  }
1697
1760
  return next;
1698
1761
  }
1699
- function sendDraft() {
1762
+ function sendDraft(auto = false) {
1763
+ setAutoFileLeft(null);
1700
1764
  const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1701
- logEvent("draft_sent", edited ? "edited" : "as drafted");
1765
+ logEvent("draft_sent", auto ? "auto-filed" : edited ? "edited" : "as drafted");
1702
1766
  flushEvents();
1703
1767
  onSend();
1704
1768
  }
@@ -1722,15 +1786,15 @@ function AssistSheet({
1722
1786
  minWidth: 0
1723
1787
  },
1724
1788
  children: [
1725
- /* @__PURE__ */ jsx2("span", { style: { color: t2.text, fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1789
+ /* @__PURE__ */ jsx2("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}` }),
1726
1790
  title && /* @__PURE__ */ jsxs2("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1727
1791
  "#",
1728
1792
  duplicate.number,
1729
1793
  " ",
1730
1794
  duplicate.title
1731
1795
  ] }),
1732
- duplicate.status && /* @__PURE__ */ jsx2("span", { style: { color: t2.text, fontWeight: 600 }, children: duplicate.status }),
1733
- footer && /* @__PURE__ */ jsx2("span", { style: { color: t2.textMuted }, children: footer })
1796
+ duplicate.status && duplicate.kind !== "related" && /* @__PURE__ */ jsx2("span", { style: { color: t2.text, fontWeight: 600 }, children: duplicate.status }),
1797
+ footer && /* @__PURE__ */ jsx2("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 })
1734
1798
  ]
1735
1799
  }
1736
1800
  );
@@ -1758,6 +1822,7 @@ function AssistSheet({
1758
1822
  @keyframes gg-sheet-in{from{transform:translateX(24px);opacity:0}to{transform:translateX(0);opacity:1}}
1759
1823
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
1760
1824
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
1825
+ @keyframes gg-autofile{from{transform:scaleX(0)}to{transform:scaleX(1)}}
1761
1826
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
1762
1827
  [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible,[data-gg-assist-remove]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
1763
1828
  ` }),
@@ -1852,6 +1917,7 @@ function AssistSheet({
1852
1917
  ),
1853
1918
  /* @__PURE__ */ jsxs2("div", { style: { fontSize: "11.5px", color: t2.textMuted, marginTop: "3px" }, children: [
1854
1919
  phase === "type" ? null : reportTypeLabel,
1920
+ phase !== "type" && severityAsked && severity ? ` \xB7 ${SEVERITY_LABELS[severity]}` : null,
1855
1921
  projectSlot ? /* @__PURE__ */ jsx2("span", { style: { marginLeft: "6px" }, children: projectSlot }) : null
1856
1922
  ] })
1857
1923
  ] }),
@@ -1970,6 +2036,62 @@ function AssistSheet({
1970
2036
  }
1971
2037
  )
1972
2038
  ] }),
2039
+ phase === "severity" && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2040
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "8px" }, children: [
2041
+ /* @__PURE__ */ jsx2(Avatar, { who: "assistant", theme: t2 }),
2042
+ /* @__PURE__ */ jsx2(
2043
+ "div",
2044
+ {
2045
+ style: {
2046
+ padding: "9px 12px",
2047
+ borderRadius: "12px 12px 12px 4px",
2048
+ backgroundColor: t2.bgSecondary,
2049
+ fontSize: "13px",
2050
+ lineHeight: 1.55,
2051
+ maxWidth: "88%"
2052
+ },
2053
+ children: "How bad is it?"
2054
+ }
2055
+ )
2056
+ ] }),
2057
+ /* @__PURE__ */ jsx2(
2058
+ "div",
2059
+ {
2060
+ role: "radiogroup",
2061
+ "aria-label": "Severity",
2062
+ style: {
2063
+ display: "flex",
2064
+ flexWrap: "wrap",
2065
+ gap: "6px",
2066
+ paddingLeft: "32px"
2067
+ },
2068
+ children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ jsx2(
2069
+ "button",
2070
+ {
2071
+ type: "button",
2072
+ role: "radio",
2073
+ "aria-checked": severity === s,
2074
+ "data-gg-chip": "",
2075
+ "data-gg-severity": s,
2076
+ onClick: () => answerSeverity(s),
2077
+ style: {
2078
+ border: `1px solid ${t2.inputBorder}`,
2079
+ background: "transparent",
2080
+ color: t2.textMuted,
2081
+ borderRadius: "999px",
2082
+ // Same as the picker and option chips — one chip size.
2083
+ padding: "8px 12px",
2084
+ fontSize: "12px",
2085
+ fontFamily: "inherit",
2086
+ cursor: "pointer"
2087
+ },
2088
+ children: SEVERITY_LABELS[s]
2089
+ },
2090
+ s
2091
+ ))
2092
+ }
2093
+ )
2094
+ ] }),
1973
2095
  phase === "rating" && !submitted && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
1974
2096
  /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "8px" }, children: [
1975
2097
  /* @__PURE__ */ jsx2(Avatar, { who: "assistant", theme: t2 }),
@@ -2158,7 +2280,11 @@ function AssistSheet({
2158
2280
  color: t2.text,
2159
2281
  border: live ? "none" : `1px solid ${t2.accent}66`
2160
2282
  },
2161
- children: live ? "Here's your report \u2014 have a read." : /* @__PURE__ */ jsxs2(Fragment, { children: [
2283
+ children: live ? (
2284
+ // #402: "have a read" beside a countdown reads as a
2285
+ // contradiction. Say what is about to happen instead.
2286
+ 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."
2287
+ ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
2162
2288
  /* @__PURE__ */ jsx2(
2163
2289
  "span",
2164
2290
  {
@@ -2325,7 +2451,7 @@ function AssistSheet({
2325
2451
  "data-gg-option": "",
2326
2452
  onClick: () => {
2327
2453
  logEvent("option_tap", option);
2328
- send(option);
2454
+ send(option, option === CONFIRM_OPTION);
2329
2455
  },
2330
2456
  style: {
2331
2457
  border: `1px solid ${t2.inputBorder}`,
@@ -2400,7 +2526,7 @@ function AssistSheet({
2400
2526
  textTransform: "uppercase",
2401
2527
  color: t2.accent
2402
2528
  },
2403
- children: duplicate ? `What we'll add to #${duplicate.number} \xB7 edit anything` : "Your report \xB7 edit anything"
2529
+ children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
2404
2530
  }
2405
2531
  ),
2406
2532
  renderKnownIssue({
@@ -2412,7 +2538,10 @@ function AssistSheet({
2412
2538
  {
2413
2539
  ref: draftRef,
2414
2540
  value: description,
2415
- onChange: (e) => onDescriptionChange(e.target.value),
2541
+ onChange: (e) => {
2542
+ stopAutoFile("edited the draft");
2543
+ onDescriptionChange(e.target.value);
2544
+ },
2416
2545
  rows: 6,
2417
2546
  style: {
2418
2547
  width: "100%",
@@ -2431,7 +2560,7 @@ function AssistSheet({
2431
2560
  }
2432
2561
  }
2433
2562
  ),
2434
- severityAsked && /* @__PURE__ */ jsxs2("div", { children: [
2563
+ severityAsked && autoFileLeft === null && /* @__PURE__ */ jsxs2("div", { children: [
2435
2564
  /* @__PURE__ */ jsxs2(
2436
2565
  "span",
2437
2566
  {
@@ -2553,7 +2682,7 @@ function AssistSheet({
2553
2682
  )
2554
2683
  }
2555
2684
  ),
2556
- !submitted && phase !== "type" && /* @__PURE__ */ jsxs2(
2685
+ !submitted && phase !== "type" && phase !== "severity" && /* @__PURE__ */ jsxs2(
2557
2686
  "div",
2558
2687
  {
2559
2688
  style: {
@@ -2660,6 +2789,7 @@ function AssistSheet({
2660
2789
  "aria-label": `Remove image ${i + 1}`,
2661
2790
  title: "Remove this image",
2662
2791
  onClick: () => {
2792
+ stopAutoFile("changed attachments");
2663
2793
  logEvent(
2664
2794
  "image_removed",
2665
2795
  pageShot && shot === pageShot ? "page capture" : "attached"
@@ -2770,6 +2900,7 @@ function AssistSheet({
2770
2900
  "aria-label": `Remove file ${file.name}`,
2771
2901
  title: "Remove this file",
2772
2902
  onClick: () => {
2903
+ stopAutoFile("changed attachments");
2773
2904
  logEvent("file_removed", read ? "read" : "not read");
2774
2905
  onRemoveFile(i);
2775
2906
  },
@@ -2852,6 +2983,7 @@ function AssistSheet({
2852
2983
  var _a;
2853
2984
  const picked = Array.from((_a = e.target.files) != null ? _a : []);
2854
2985
  if (picked.length) {
2986
+ stopAutoFile("changed attachments");
2855
2987
  const images = picked.filter((f) => f.type.startsWith("image/")).length;
2856
2988
  if (images) logEvent("image_added", String(images));
2857
2989
  if (picked.length > images) {
@@ -2881,11 +3013,14 @@ function AssistSheet({
2881
3013
  "textarea",
2882
3014
  {
2883
3015
  value: input,
2884
- onChange: (e) => setInput(e.target.value),
3016
+ onChange: (e) => {
3017
+ stopAutoFile("typing a change");
3018
+ setInput(e.target.value);
3019
+ },
2885
3020
  onKeyDown: (e) => {
2886
3021
  if (e.key === "Enter" && !e.shiftKey) {
2887
3022
  e.preventDefault();
2888
- send(input);
3023
+ sendTyped(input);
2889
3024
  }
2890
3025
  },
2891
3026
  placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
@@ -2915,7 +3050,7 @@ function AssistSheet({
2915
3050
  {
2916
3051
  type: "button",
2917
3052
  "data-gg-sheet-send": "",
2918
- onClick: () => send(input),
3053
+ onClick: () => sendTyped(input),
2919
3054
  disabled: busy || !input.trim(),
2920
3055
  "aria-label": "Send message",
2921
3056
  "data-gg-send-message": "",
@@ -2954,12 +3089,87 @@ function AssistSheet({
2954
3089
  children: validationError
2955
3090
  }
2956
3091
  ),
2957
- /* @__PURE__ */ jsx2(
3092
+ autoFileLeft !== null ? (
3093
+ // #402: "Yes, that's it" was the confirmation. The draft
3094
+ // files itself unless they stop it — the bar shows how
3095
+ // long is left, the button sends now.
3096
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "8px", marginTop: "8px" }, children: [
3097
+ /* @__PURE__ */ jsxs2(
3098
+ "button",
3099
+ {
3100
+ type: "button",
3101
+ "data-gg-sheet-send": "",
3102
+ "data-gg-auto-file": "",
3103
+ onClick: () => sendDraft(),
3104
+ style: {
3105
+ flex: 1,
3106
+ minWidth: 0,
3107
+ position: "relative",
3108
+ overflow: "hidden",
3109
+ padding: "12px",
3110
+ borderRadius: "10px",
3111
+ border: "none",
3112
+ backgroundColor: t2.accent,
3113
+ color: t2.accentText,
3114
+ fontSize: "14px",
3115
+ fontWeight: 700,
3116
+ fontFamily: "inherit",
3117
+ cursor: "pointer"
3118
+ },
3119
+ children: [
3120
+ /* @__PURE__ */ jsx2(
3121
+ "span",
3122
+ {
3123
+ "aria-hidden": "true",
3124
+ style: {
3125
+ position: "absolute",
3126
+ inset: 0,
3127
+ backgroundColor: "rgba(255,255,255,0.22)",
3128
+ transformOrigin: "left",
3129
+ animation: `gg-autofile ${AUTO_FILE_SECONDS}s linear forwards`
3130
+ }
3131
+ }
3132
+ ),
3133
+ /* @__PURE__ */ jsxs2("span", { style: { position: "relative" }, children: [
3134
+ "Filing in ",
3135
+ autoFileLeft,
3136
+ "s\u2026"
3137
+ ] })
3138
+ ]
3139
+ }
3140
+ ),
3141
+ /* @__PURE__ */ jsx2(
3142
+ "button",
3143
+ {
3144
+ type: "button",
3145
+ "data-gg-edit-first": "",
3146
+ onClick: () => {
3147
+ var _a;
3148
+ stopAutoFile("edit first");
3149
+ (_a = draftRef.current) == null ? void 0 : _a.focus();
3150
+ },
3151
+ style: {
3152
+ flexShrink: 0,
3153
+ padding: "12px 14px",
3154
+ borderRadius: "10px",
3155
+ border: `1px solid ${t2.inputBorder}`,
3156
+ background: "transparent",
3157
+ color: t2.text,
3158
+ fontSize: "13px",
3159
+ fontWeight: 600,
3160
+ fontFamily: "inherit",
3161
+ cursor: "pointer"
3162
+ },
3163
+ children: "Edit first"
3164
+ }
3165
+ )
3166
+ ] })
3167
+ ) : /* @__PURE__ */ jsx2(
2958
3168
  "button",
2959
3169
  {
2960
3170
  type: "button",
2961
3171
  "data-gg-sheet-send": "",
2962
- onClick: sendDraft,
3172
+ onClick: () => sendDraft(),
2963
3173
  disabled: isSubmitting || !description.trim(),
2964
3174
  style: {
2965
3175
  width: "100%",
@@ -3935,9 +4145,11 @@ function ReportDialog({
3935
4145
  reporter,
3936
4146
  onClose,
3937
4147
  headerSlot,
3938
- reportGlitchgrabProblem
4148
+ reportGlitchgrabProblem,
4149
+ layout = "modal"
3939
4150
  }) {
3940
4151
  var _a;
4152
+ const sheet = layout === "sheet";
3941
4153
  const [isEnhancing, setIsEnhancing] = useState4(false);
3942
4154
  const [assistOpen, setAssistOpen] = useState4(false);
3943
4155
  const [assistNotice, setAssistNotice] = useState4(null);
@@ -3946,6 +4158,8 @@ function ReportDialog({
3946
4158
  const [selfReportPrefill, setSelfReportPrefill] = useState4("");
3947
4159
  const [typeChosen, setTypeChosen] = useState4(false);
3948
4160
  const [duplicateIssueNumber, setDuplicateIssueNumber] = useState4(null);
4161
+ const [duplicateKind, setDuplicateKind] = useState4("duplicate");
4162
+ const [addedAsRelated, setAddedAsRelated] = useState4(false);
3949
4163
  const [similarIssues, setSimilarIssues] = useState4(null);
3950
4164
  const [checkingSimilar, setCheckingSimilar] = useState4(false);
3951
4165
  const [attachingTo, setAttachingTo] = useState4(null);
@@ -4382,6 +4596,7 @@ function ReportDialog({
4382
4596
  setReportType("BUG");
4383
4597
  setTypeChosen(false);
4384
4598
  setDuplicateIssueNumber(null);
4599
+ setDuplicateKind("duplicate");
4385
4600
  setSimilarIssues(null);
4386
4601
  setPickedSimilar(null);
4387
4602
  setCheckingSimilar(false);
@@ -4574,7 +4789,7 @@ function ReportDialog({
4574
4789
  }
4575
4790
  };
4576
4791
  const handleSubmit = async (opts) => {
4577
- var _a2, _b;
4792
+ var _a2, _b, _c;
4578
4793
  try {
4579
4794
  if (isSubmitting) return;
4580
4795
  if (isRating) {
@@ -4612,6 +4827,7 @@ function ReportDialog({
4612
4827
  return;
4613
4828
  }
4614
4829
  const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
4830
+ const attachKind = (opts == null ? void 0 : opts.duplicateOf) ? (_b = opts.kind) != null ? _b : "duplicate" : duplicateKind;
4615
4831
  if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
4616
4832
  setIsSubmitting(true);
4617
4833
  setCheckingSimilar(true);
@@ -4647,6 +4863,7 @@ function ReportDialog({
4647
4863
  }
4648
4864
  if (duplicateOf) {
4649
4865
  metadata.duplicateIssueNumber = String(duplicateOf);
4866
+ if (attachKind === "related") metadata.duplicateKind = "related";
4650
4867
  }
4651
4868
  if (aiConversationId) {
4652
4869
  metadata.aiConversationId = aiConversationId;
@@ -4660,14 +4877,16 @@ function ReportDialog({
4660
4877
  );
4661
4878
  if (result) {
4662
4879
  setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
4880
+ setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
4663
4881
  const queued = result.status === "QUEUED" && !!result.message;
4664
- setQueuedMessage(queued ? (_b = result.message) != null ? _b : null : null);
4882
+ setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
4665
4883
  setSubmitted(true);
4666
4884
  setDescription("");
4667
4885
  setScreenshots([]);
4668
4886
  setAttachments([]);
4669
4887
  setAiConversationId(null);
4670
4888
  setDuplicateIssueNumber(null);
4889
+ setDuplicateKind("duplicate");
4671
4890
  setSimilarIssues(null);
4672
4891
  setPickedSimilar(null);
4673
4892
  setTimeout(() => {
@@ -4738,8 +4957,8 @@ function ReportDialog({
4738
4957
  // dialog's state IS the report, and unmounting would drop the
4739
4958
  // screenshots, attachments and step the reporter is on.
4740
4959
  display: sheetUp ? "none" : "flex",
4741
- alignItems: "center",
4742
- justifyContent: "center",
4960
+ alignItems: sheet ? "stretch" : "center",
4961
+ justifyContent: sheet ? "flex-end" : "center",
4743
4962
  backgroundColor: "rgba(0,0,0,0.5)",
4744
4963
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
4745
4964
  },
@@ -4759,14 +4978,15 @@ function ReportDialog({
4759
4978
  style: {
4760
4979
  position: "relative",
4761
4980
  zIndex: 2147483647,
4762
- width: "420px",
4763
- maxWidth: "calc(100% - 32px)",
4764
- maxHeight: "calc(100dvh - 32px)",
4981
+ width: sheet ? "440px" : "420px",
4982
+ maxWidth: sheet ? "100%" : "calc(100% - 32px)",
4983
+ maxHeight: sheet ? "100dvh" : "calc(100dvh - 32px)",
4984
+ ...sheet ? { height: "100dvh", borderLeft: `1px solid ${t2.inputBorder}` } : {},
4765
4985
  display: "flex",
4766
4986
  flexDirection: "column",
4767
4987
  backgroundColor: t2.bg,
4768
- borderRadius: "12px",
4769
- boxShadow: "0 20px 60px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.1)",
4988
+ borderRadius: sheet ? 0 : "12px",
4989
+ 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)",
4770
4990
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
4771
4991
  overflow: "hidden",
4772
4992
  color: t2.text,
@@ -4800,7 +5020,7 @@ function ReportDialog({
4800
5020
  gap: "8px"
4801
5021
  },
4802
5022
  children: [
4803
- step > 1 && /* @__PURE__ */ jsx4(
5023
+ step > 1 && availableTypes.length > 1 && /* @__PURE__ */ jsx4(
4804
5024
  "button",
4805
5025
  {
4806
5026
  type: "button",
@@ -4918,7 +5138,7 @@ function ReportDialog({
4918
5138
  ]
4919
5139
  }
4920
5140
  ),
4921
- /* @__PURE__ */ jsx4(
5141
+ availableTypes.length > 1 && /* @__PURE__ */ jsx4(
4922
5142
  "div",
4923
5143
  {
4924
5144
  style: {
@@ -4995,7 +5215,7 @@ function ReportDialog({
4995
5215
  },
4996
5216
  children: queuedMessage
4997
5217
  }
4998
- ) : addedToIssue ? `Added to #${addedToIssue}. Thank you!` : `${getTypeLabel(reportType)} sent. Thank you!`
5218
+ ) : addedToIssue ? `Added to #${addedToIssue}${addedAsRelated ? " as a related request" : ""}. Thank you!` : `${getTypeLabel(reportType)} sent. Thank you!`
4999
5219
  }
5000
5220
  ) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
5001
5221
  step === 1 && headerSlot ? /* @__PURE__ */ jsx4("div", { style: { marginBottom: "14px" }, children: headerSlot }) : null,
@@ -5194,6 +5414,7 @@ function ReportDialog({
5194
5414
  "button",
5195
5415
  {
5196
5416
  type: "button",
5417
+ disabled: availableTypes.length === 1,
5197
5418
  onClick: () => setStep(1),
5198
5419
  style: {
5199
5420
  display: "inline-flex",
@@ -5208,12 +5429,12 @@ function ReportDialog({
5208
5429
  fontSize: "11px",
5209
5430
  fontWeight: 600,
5210
5431
  fontFamily: "inherit",
5211
- cursor: "pointer"
5432
+ cursor: availableTypes.length === 1 ? "default" : "pointer"
5212
5433
  },
5213
5434
  children: [
5214
5435
  getTypeIcon(reportType, isRating ? "#f59e0b" : t2.accent, 13),
5215
5436
  getTypeLabel(reportType),
5216
- /* @__PURE__ */ jsx4("span", { style: { color: t2.textMuted, fontWeight: 400 }, children: "\xB7 change" })
5437
+ availableTypes.length > 1 && /* @__PURE__ */ jsx4("span", { style: { color: t2.textMuted, fontWeight: 400 }, children: "\xB7 change" })
5217
5438
  ]
5218
5439
  }
5219
5440
  ),
@@ -6240,8 +6461,8 @@ function ReportDialog({
6240
6461
  },
6241
6462
  children: [
6242
6463
  /* @__PURE__ */ jsxs4("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6243
- /* @__PURE__ */ jsx4("span", { style: { color: t2.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this" }),
6244
- /* @__PURE__ */ jsx4("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." })
6464
+ /* @__PURE__ */ jsx4("span", { style: { color: t2.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
6465
+ /* @__PURE__ */ jsx4("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." })
6245
6466
  ] }),
6246
6467
  /* @__PURE__ */ jsx4(
6247
6468
  "div",
@@ -6346,7 +6567,8 @@ function ReportDialog({
6346
6567
  if (!pickedSimilar) return;
6347
6568
  setAttachingTo(pickedSimilar);
6348
6569
  setDuplicateIssueNumber(pickedSimilar);
6349
- void handleSubmit({ duplicateOf: pickedSimilar });
6570
+ setDuplicateKind("duplicate");
6571
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
6350
6572
  },
6351
6573
  style: {
6352
6574
  width: "100%",
@@ -6361,7 +6583,7 @@ function ReportDialog({
6361
6583
  cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
6362
6584
  fontFamily: "inherit"
6363
6585
  },
6364
- children: typeof attachingTo === "number" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
6586
+ children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
6365
6587
  /* @__PURE__ */ jsx4(ButtonSpinner, {}),
6366
6588
  "Adding to #",
6367
6589
  attachingTo,
@@ -6369,6 +6591,39 @@ function ReportDialog({
6369
6591
  ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
6370
6592
  }
6371
6593
  ),
6594
+ pickedSimilar && /* @__PURE__ */ jsx4(
6595
+ "button",
6596
+ {
6597
+ type: "button",
6598
+ disabled: isSubmitting,
6599
+ onClick: () => {
6600
+ setAttachingTo(pickedSimilar);
6601
+ setDuplicateIssueNumber(pickedSimilar);
6602
+ setDuplicateKind("related");
6603
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
6604
+ },
6605
+ style: {
6606
+ width: "100%",
6607
+ minHeight: "40px",
6608
+ marginTop: "-4px",
6609
+ padding: "9px 10px",
6610
+ borderRadius: "8px",
6611
+ border: "1px solid rgba(245,158,11,0.4)",
6612
+ backgroundColor: "transparent",
6613
+ color: isSubmitting ? t2.textMuted : t2.text,
6614
+ fontSize: "13px",
6615
+ fontWeight: 600,
6616
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6617
+ fontFamily: "inherit"
6618
+ },
6619
+ children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
6620
+ /* @__PURE__ */ jsx4(ButtonSpinner, {}),
6621
+ "Adding to #",
6622
+ attachingTo,
6623
+ "\u2026"
6624
+ ] }) : `Related request \u2014 add to #${pickedSimilar}`
6625
+ }
6626
+ ),
6372
6627
  /* @__PURE__ */ jsx4(
6373
6628
  "button",
6374
6629
  {
@@ -6715,7 +6970,10 @@ function ReportDialog({
6715
6970
  setTypeChosen(true);
6716
6971
  setStep(2);
6717
6972
  },
6718
- onDuplicateChange: setDuplicateIssueNumber,
6973
+ onDuplicateChange: (issueNumber, kind) => {
6974
+ setDuplicateIssueNumber(issueNumber);
6975
+ setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
6976
+ },
6719
6977
  onConversationChange: setAiConversationId,
6720
6978
  onFinish: handleClose,
6721
6979
  rating,