glitchgrab 1.58.2 → 1.59.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
@@ -49,12 +49,14 @@ __export(index_exports, {
49
49
  computeSignature: () => computeSignature,
50
50
  enhanceText: () => enhanceText,
51
51
  fetchGlitchgrabFeedback: () => fetchGlitchgrabFeedback,
52
+ fetchGlitchgrabReportThread: () => fetchGlitchgrabReportThread,
52
53
  fetchGlitchgrabReports: () => fetchGlitchgrabReports,
53
54
  getAppContext: () => getAppContext,
54
55
  getBreadcrumbs: () => getBreadcrumbs,
55
56
  getShortcutLabel: () => getShortcutLabel2,
56
57
  initBreadcrumbs: () => initBreadcrumbs,
57
58
  matchesShortcut: () => matchesShortcut,
59
+ postGlitchgrabReportComment: () => postGlitchgrabReportComment,
58
60
  sanitizeUrl: () => sanitizeUrl,
59
61
  sendFeedback: () => sendFeedback,
60
62
  sendLead: () => sendLead,
@@ -65,6 +67,7 @@ __export(index_exports, {
65
67
  useGlitchgrab: () => useGlitchgrab,
66
68
  useGlitchgrabActions: () => useGlitchgrabActions,
67
69
  useGlitchgrabFeedback: () => useGlitchgrabFeedback,
70
+ useGlitchgrabReportThread: () => useGlitchgrabReportThread,
68
71
  useGlitchgrabReports: () => useGlitchgrabReports
69
72
  });
70
73
  module.exports = __toCommonJS(index_exports);
@@ -239,7 +242,7 @@ function argsToString(args) {
239
242
  function previewJson(value, max) {
240
243
  let out = "";
241
244
  const path = /* @__PURE__ */ new WeakSet();
242
- const write = (input, inArray) => {
245
+ const write2 = (input, inArray) => {
243
246
  if (out.length > max) return;
244
247
  let v = input;
245
248
  if (v !== null && typeof v === "object" && typeof v.toJSON === "function") {
@@ -276,7 +279,7 @@ function previewJson(value, max) {
276
279
  out += "[";
277
280
  for (let i = 0; i < obj.length && out.length <= max; i++) {
278
281
  if (i > 0) out += ",";
279
- write(obj[i], true);
282
+ write2(obj[i], true);
280
283
  }
281
284
  out += "]";
282
285
  } else {
@@ -289,13 +292,13 @@ function previewJson(value, max) {
289
292
  if (child === void 0 || typeof child === "function" || typeof child === "symbol") continue;
290
293
  out += `${first ? "" : ","}${JSON.stringify(key)}:`;
291
294
  first = false;
292
- write(child, false);
295
+ write2(child, false);
293
296
  }
294
297
  out += "}";
295
298
  }
296
299
  path.delete(obj);
297
300
  };
298
- write(value, false);
301
+ write2(value, false);
299
302
  return out;
300
303
  }
301
304
  async function readErrorBody(response, url) {
@@ -772,12 +775,21 @@ function signedFields(identity) {
772
775
  return { userId: identity.userId, signature: identity.signature };
773
776
  }
774
777
  async function enhanceText(text, token, baseUrl, screenshot, context, identity) {
778
+ try {
779
+ return await enhanceTextOrThrow(text, token, baseUrl, screenshot, context, identity);
780
+ } catch (e) {
781
+ return text;
782
+ }
783
+ }
784
+ async function enhanceTextOrThrow(text, token, baseUrl, screenshot, context, identity) {
775
785
  var _a;
786
+ const trimmed = text.trim();
787
+ if (!trimmed) return text;
788
+ const unavailable = "AI enhance isn't available right now \u2014 send your report as written.";
789
+ let response;
776
790
  try {
777
- const trimmed = text.trim();
778
- if (!trimmed) return text;
779
791
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/enhance-text`;
780
- const response = await fetch(url, {
792
+ response = await fetch(url, {
781
793
  method: "POST",
782
794
  headers: {
783
795
  "Content-Type": "application/json",
@@ -790,15 +802,20 @@ async function enhanceText(text, token, baseUrl, screenshot, context, identity)
790
802
  ...signedFields(identity)
791
803
  })
792
804
  });
793
- if (!response.ok) return text;
794
- const envelope = await response.json();
795
- if ((envelope == null ? void 0 : envelope.success) && typeof ((_a = envelope.data) == null ? void 0 : _a.text) === "string") {
796
- return envelope.data.text;
797
- }
798
- return text;
799
805
  } catch (e) {
800
- return text;
806
+ throw new Error(unavailable);
807
+ }
808
+ if (response.status === 429) {
809
+ throw new Error("Too many AI enhance tries \u2014 wait a while, or send your report as written.");
801
810
  }
811
+ const envelope = await response.json().catch(() => null);
812
+ if (response.ok && (envelope == null ? void 0 : envelope.success) && typeof ((_a = envelope.data) == null ? void 0 : _a.text) === "string") {
813
+ return envelope.data.text;
814
+ }
815
+ if (response.status === 400 && typeof (envelope == null ? void 0 : envelope.error) === "string") {
816
+ throw new Error(envelope.error);
817
+ }
818
+ throw new Error(unavailable);
802
819
  }
803
820
  async function findSimilarIssues(description, token, baseUrl, identity, files) {
804
821
  var _a;
@@ -956,6 +973,292 @@ function clearDedupCache() {
956
973
  seen.clear();
957
974
  }
958
975
 
976
+ // src/hydration.ts
977
+ var HYDRATION_CODE_RE = /Minified React error #(418|419|422|423|425)\b/;
978
+ var HYDRATION_DEV_RE = /Hydration failed|hydration mismatch|Text content does not match|did not match\. Server|There was an error while hydrating|error occurred during hydration|server rendered HTML didn't match/i;
979
+ var BAILOUT_DIGEST = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
980
+ var DOM_SNIPPET_MAX = 3e3;
981
+ var STACK_FRAMES_MAX = 40;
982
+ function messageOf(error) {
983
+ if (error && typeof error === "object" && typeof error.message === "string") {
984
+ return error.message;
985
+ }
986
+ return typeof error === "string" ? error : "";
987
+ }
988
+ function hydrationCode(message) {
989
+ if (!message) return null;
990
+ const m = HYDRATION_CODE_RE.exec(message);
991
+ if (m) return m[1];
992
+ return HYDRATION_DEV_RE.test(message) ? "dev" : null;
993
+ }
994
+ function isHydrationError(error) {
995
+ if (hydrationCode(messageOf(error))) return true;
996
+ const cause = error && typeof error === "object" ? error.cause : void 0;
997
+ return cause !== void 0 && cause !== error && hydrationCode(messageOf(cause)) !== null;
998
+ }
999
+ function isBailout(error) {
1000
+ const e = error;
1001
+ if (!e || typeof e !== "object") return false;
1002
+ if (e.digest === BAILOUT_DIGEST) return true;
1003
+ const c = e.cause;
1004
+ return !!c && typeof c === "object" && c.digest === BAILOUT_DIGEST;
1005
+ }
1006
+ function componentStackNames(stack) {
1007
+ if (!stack) return [];
1008
+ const names = [];
1009
+ for (const raw of stack.split("\n")) {
1010
+ const line = raw.trim();
1011
+ if (!line) continue;
1012
+ let name = "";
1013
+ if (line.startsWith("at ")) {
1014
+ name = line.slice(3).split(" (")[0].trim();
1015
+ } else if (line.includes("@")) {
1016
+ name = line.slice(0, line.indexOf("@")).trim();
1017
+ } else {
1018
+ name = line;
1019
+ }
1020
+ if (name) names.push(name);
1021
+ if (names.length >= STACK_FRAMES_MAX) break;
1022
+ }
1023
+ return names;
1024
+ }
1025
+ function componentStackKey(stack) {
1026
+ return componentStackNames(stack).join(">");
1027
+ }
1028
+ var observer = null;
1029
+ var observerStarted = false;
1030
+ var observerStopAt = 0;
1031
+ var OBSERVE_MS = 3e4;
1032
+ function startObserver() {
1033
+ if (observerStarted || typeof MutationObserver === "undefined" || typeof document === "undefined") {
1034
+ return;
1035
+ }
1036
+ observerStarted = true;
1037
+ try {
1038
+ observer = new MutationObserver(() => {
1039
+ if (Date.now() > observerStopAt) stopObserver();
1040
+ });
1041
+ observer.observe(document, { childList: true, subtree: true });
1042
+ observerStopAt = Date.now() + OBSERVE_MS;
1043
+ setTimeout(stopObserver, OBSERVE_MS);
1044
+ } catch (e) {
1045
+ observer = null;
1046
+ }
1047
+ }
1048
+ function stopObserver() {
1049
+ try {
1050
+ observer == null ? void 0 : observer.disconnect();
1051
+ } catch (e) {
1052
+ }
1053
+ observer = null;
1054
+ }
1055
+ function describeNode(node) {
1056
+ if (!node) return "";
1057
+ if (node.nodeType === 9) return "#document";
1058
+ const el = node;
1059
+ if (!el.tagName) return node.nodeName;
1060
+ const id = el.id ? `#${el.id}` : "";
1061
+ const cls = typeof el.className === "string" && el.className.trim() ? "." + el.className.trim().split(/\s+/).slice(0, 3).join(".") : "";
1062
+ return `${el.tagName.toLowerCase()}${id}${cls}`;
1063
+ }
1064
+ function pathOf(node) {
1065
+ const parts = [];
1066
+ let cur = node;
1067
+ while (cur && parts.length < 6) {
1068
+ parts.unshift(describeNode(cur));
1069
+ if (cur.nodeType === 9) break;
1070
+ cur = cur.parentNode;
1071
+ }
1072
+ return parts.join(" > ");
1073
+ }
1074
+ var NON_CONTENT_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT", "TEMPLATE", "TITLE", "HEAD"]);
1075
+ function isInHead(node) {
1076
+ for (let cur = node; cur; cur = cur.parentNode) {
1077
+ if (cur.nodeName === "HEAD") return true;
1078
+ if (cur.nodeName === "BODY") return false;
1079
+ }
1080
+ return false;
1081
+ }
1082
+ var GSTIN_RE = /\b\d{2}[A-Z]{5}\d{4}[A-Z][A-Z0-9]Z[A-Z0-9]\b/g;
1083
+ var PAN_RE = /\b[A-Z]{5}\d{4}[A-Z]\b/g;
1084
+ function scrubHtml(html) {
1085
+ return redactString(
1086
+ html.replace(/\svalue="[^"]*"/gi, ' value="[REDACTED]"').replace(GSTIN_RE, "[REDACTED_GSTIN]").replace(PAN_RE, "[REDACTED_PAN]").replace(/\d{6,}/g, (m) => "#".repeat(Math.min(m.length, 6)))
1087
+ );
1088
+ }
1089
+ function takeRemovedServerHtml() {
1090
+ var _a, _b;
1091
+ if (!observer) return { html: "", where: "" };
1092
+ try {
1093
+ const records = observer.takeRecords();
1094
+ let html = "";
1095
+ let where = "";
1096
+ for (const rec of records) {
1097
+ if (rec.type !== "childList" || rec.removedNodes.length === 0) continue;
1098
+ if (isInHead(rec.target)) continue;
1099
+ if (!where) where = pathOf(rec.target);
1100
+ for (const node of Array.from(rec.removedNodes)) {
1101
+ if (node.nodeType === 1 && NON_CONTENT_TAGS.has(node.tagName)) continue;
1102
+ if (node.nodeType === 3 && !((_a = node.textContent) != null ? _a : "").trim()) continue;
1103
+ const piece = node.nodeType === 1 ? node.outerHTML : node.nodeType === 3 ? `#text "${(_b = node.textContent) != null ? _b : ""}"` : "";
1104
+ if (!piece) continue;
1105
+ html += (html ? "\n" : "") + piece;
1106
+ if (html.length >= DOM_SNIPPET_MAX) break;
1107
+ }
1108
+ if (html.length >= DOM_SNIPPET_MAX) break;
1109
+ }
1110
+ const scrubbed = scrubHtml(html);
1111
+ return {
1112
+ html: scrubbed.length > DOM_SNIPPET_MAX ? `${scrubbed.slice(0, DOM_SNIPPET_MAX)}\u2026[truncated]` : scrubbed,
1113
+ where
1114
+ };
1115
+ } catch (e) {
1116
+ return { html: "", where: "" };
1117
+ }
1118
+ }
1119
+ function detectForeignNodes(doc = document) {
1120
+ var _a;
1121
+ const found = [];
1122
+ try {
1123
+ const html = doc.documentElement;
1124
+ if ((html == null ? void 0 : html.classList.contains("translated-ltr")) || (html == null ? void 0 : html.classList.contains("translated-rtl"))) {
1125
+ found.push("Google Translate is active (html.translated-*)");
1126
+ }
1127
+ const fonts = doc.getElementsByTagName("font").length;
1128
+ if (fonts > 0) found.push(`<font> \xD7 ${fonts} (Google Translate or a translation extension)`);
1129
+ const detectors = doc.querySelectorAll("a[x-apple-data-detectors]").length;
1130
+ if (detectors > 0) {
1131
+ found.push(`<a x-apple-data-detectors> \xD7 ${detectors} (iOS format detection turned text into links)`);
1132
+ }
1133
+ if (doc.querySelector("[data-gr-ext-installed],[data-new-gr-c-s-check-loaded],grammarly-desktop-integration")) {
1134
+ found.push("Grammarly extension attributes");
1135
+ }
1136
+ if (doc.querySelector("[data-lt-installed]")) found.push("LanguageTool extension attributes");
1137
+ if ((_a = doc.body) == null ? void 0 : _a.hasAttribute("cz-shortcut-listen")) found.push("ColorZilla extension attribute on <body>");
1138
+ const custom = /* @__PURE__ */ new Set();
1139
+ for (const parent of [html, doc.body]) {
1140
+ if (!parent) continue;
1141
+ for (const child of Array.from(parent.children)) {
1142
+ const tag = child.tagName.toLowerCase();
1143
+ if (tag.includes("-")) custom.add(tag);
1144
+ }
1145
+ }
1146
+ if (custom.size > 0) {
1147
+ found.push(`custom elements at top level: ${Array.from(custom).slice(0, 5).join(", ")} (often an extension)`);
1148
+ }
1149
+ } catch (e) {
1150
+ }
1151
+ return found;
1152
+ }
1153
+ function hydrationTiming() {
1154
+ var _a, _b, _c, _d;
1155
+ try {
1156
+ if (typeof performance === "undefined") return "";
1157
+ const at2 = Math.round(performance.now());
1158
+ const parts = [`error at ${at2}ms after navigation start`];
1159
+ const nav = (_b = (_a = performance.getEntriesByType) == null ? void 0 : _a.call(performance, "navigation")) == null ? void 0 : _b[0];
1160
+ if (nav == null ? void 0 : nav.domContentLoadedEventEnd) {
1161
+ parts.push(`DOMContentLoaded ${Math.round(nav.domContentLoadedEventEnd)}ms`);
1162
+ }
1163
+ const fetches = ((_d = (_c = performance.getEntriesByType) == null ? void 0 : _c.call(performance, "resource")) != null ? _d : []).filter((e) => {
1164
+ const t = e.initiatorType;
1165
+ return t === "fetch" || t === "xmlhttprequest";
1166
+ }).sort((a, b) => a.responseEnd - b.responseEnd).slice(0, 8).map((e) => {
1167
+ let path = e.name;
1168
+ try {
1169
+ const u = new URL(e.name);
1170
+ path = u.origin === window.location.origin ? u.pathname : `${u.host}${u.pathname}`;
1171
+ } catch (e2) {
1172
+ }
1173
+ return `${path} ${Math.round(e.responseEnd)}ms`;
1174
+ });
1175
+ if (fetches.length > 0) parts.push(`fetches finished before it: ${fetches.join(", ")}`);
1176
+ else parts.push("no fetch had finished yet");
1177
+ return parts.join("; ");
1178
+ } catch (e) {
1179
+ return "";
1180
+ }
1181
+ }
1182
+ function hydrationMetadata(error, extra) {
1183
+ var _a, _b, _c;
1184
+ const code = (_a = hydrationCode(messageOf(error))) != null ? _a : hydrationCode(messageOf(error == null ? void 0 : error.cause));
1185
+ const meta = { errorKind: "hydration" };
1186
+ if (code) meta.hydrationCode = code;
1187
+ const cause = error == null ? void 0 : error.cause;
1188
+ if (cause && cause !== error && messageOf(cause)) {
1189
+ meta.hydrationCause = messageOf(cause).slice(0, 500);
1190
+ }
1191
+ if ((_b = extra == null ? void 0 : extra.removed) == null ? void 0 : _b.html) meta.hydrationServerHtml = extra.removed.html;
1192
+ if ((_c = extra == null ? void 0 : extra.removed) == null ? void 0 : _c.where) meta.hydrationReplacedIn = extra.removed.where;
1193
+ const foreign = detectForeignNodes();
1194
+ if (foreign.length > 0) meta.hydrationForeignNodes = foreign.join("\n");
1195
+ const timing = hydrationTiming();
1196
+ if (timing) meta.hydrationTiming = timing;
1197
+ return meta;
1198
+ }
1199
+ var handled = /* @__PURE__ */ new WeakSet();
1200
+ function isHandledHydrationError(error) {
1201
+ return !!error && typeof error === "object" && handled.has(error);
1202
+ }
1203
+ function markHandled(error) {
1204
+ if (error && typeof error === "object") {
1205
+ handled.add(error);
1206
+ const cause = error.cause;
1207
+ if (cause && typeof cause === "object") handled.add(cause);
1208
+ }
1209
+ }
1210
+ var reporter = null;
1211
+ var PATCHED = "__glitchgrabHydrationPatched";
1212
+ function findFiberRoot(container) {
1213
+ if (!container || typeof container !== "object") return null;
1214
+ try {
1215
+ const key = Object.keys(container).find((k) => k.startsWith("__reactContainer$"));
1216
+ if (!key) return null;
1217
+ const hostRoot = container[key];
1218
+ const root = hostRoot == null ? void 0 : hostRoot.stateNode;
1219
+ return root && typeof root === "object" ? root : null;
1220
+ } catch (e) {
1221
+ return null;
1222
+ }
1223
+ }
1224
+ function patchRoot(root) {
1225
+ if (root[PATCHED]) return true;
1226
+ const original = root.onRecoverableError;
1227
+ if (typeof original !== "function") return false;
1228
+ root.onRecoverableError = function glitchgrabOnRecoverableError(error, info) {
1229
+ var _a;
1230
+ try {
1231
+ if (reporter && isHydrationError(error) && !isBailout(error)) {
1232
+ const removed = takeRemovedServerHtml();
1233
+ markHandled(error);
1234
+ reporter(error, (_a = info == null ? void 0 : info.componentStack) != null ? _a : "", hydrationMetadata(error, { removed }));
1235
+ }
1236
+ } catch (e) {
1237
+ }
1238
+ return original.call(this, error, info);
1239
+ };
1240
+ root[PATCHED] = true;
1241
+ return true;
1242
+ }
1243
+ function patchKnownRoots() {
1244
+ if (typeof document === "undefined") return false;
1245
+ let patched = false;
1246
+ for (const container of [document, document.getElementById("__next"), document.getElementById("root")]) {
1247
+ const root = findFiberRoot(container);
1248
+ if (root && patchRoot(root)) patched = true;
1249
+ }
1250
+ return patched;
1251
+ }
1252
+ function installHydrationCapture(report) {
1253
+ try {
1254
+ if (typeof window === "undefined") return;
1255
+ reporter = report;
1256
+ startObserver();
1257
+ patchKnownRoots();
1258
+ } catch (e) {
1259
+ }
1260
+ }
1261
+
959
1262
  // src/capture.ts
960
1263
  var config = null;
961
1264
  function registerCaptureConfig(next) {
@@ -975,7 +1278,7 @@ function sessionMetadata(session) {
975
1278
  ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
976
1279
  };
977
1280
  }
978
- function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError") {
1281
+ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError", dedupKey) {
979
1282
  var _a, _b, _c;
980
1283
  const err = error instanceof Error ? error : void 0;
981
1284
  const errorMessage = err ? err.message : String(error);
@@ -987,7 +1290,7 @@ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureErr
987
1290
  const sig = computeSignature({
988
1291
  errorMessage,
989
1292
  pageUrl: context.url,
990
- errorStack: err == null ? void 0 : err.stack,
1293
+ errorStack: dedupKey ? `at ${dedupKey}` : err == null ? void 0 : err.stack,
991
1294
  digest
992
1295
  });
993
1296
  if (shouldSkipDuplicate(sig)) return null;
@@ -1027,6 +1330,25 @@ function captureError(error, options) {
1027
1330
  } catch (e) {
1028
1331
  }
1029
1332
  }
1333
+ function reportHydrationError(error, componentStack, metadata) {
1334
+ try {
1335
+ const cfg = config;
1336
+ if (!(cfg == null ? void 0 : cfg.token)) return;
1337
+ const payload = buildCapturedErrorPayload(
1338
+ error,
1339
+ { componentStack: componentStack || void 0, metadata },
1340
+ cfg,
1341
+ "onRecoverableError",
1342
+ componentStackKey(componentStack) || void 0
1343
+ );
1344
+ if (!payload) return;
1345
+ void sendReport(payload, cfg.baseUrl).then((result) => {
1346
+ if (result && cfg.onReportSent) cfg.onReportSent(result);
1347
+ });
1348
+ if (cfg.onError && error instanceof Error) cfg.onError(error);
1349
+ } catch (e) {
1350
+ }
1351
+ }
1030
1352
 
1031
1353
  // src/error-boundary.tsx
1032
1354
  var GlitchgrabErrorBoundary = class extends React.Component {
@@ -1948,6 +2270,79 @@ function renderBlocks(lines, theme, key) {
1948
2270
  function ChatMarkdown({ text, theme }) {
1949
2271
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: STACK, children: renderBlocks(text.replace(/\r\n?/g, "\n").split("\n"), theme, "md") });
1950
2272
  }
2273
+ var MAX_SAVED_CHATS = 20;
2274
+ var SAVED_CHAT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2275
+ var TITLE_CHARS = 80;
2276
+ var PREFIX = "glitchgrab:ai-chats:";
2277
+ function chatHistoryKey(...parts) {
2278
+ const input = parts.map((p) => p != null ? p : "").join("\0");
2279
+ let hash = 2166136261;
2280
+ for (let i = 0; i < input.length; i++) {
2281
+ hash ^= input.charCodeAt(i);
2282
+ hash = Math.imul(hash, 16777619);
2283
+ }
2284
+ return (hash >>> 0).toString(36);
2285
+ }
2286
+ function chatTitle(messages) {
2287
+ var _a, _b;
2288
+ const first = (_b = (_a = messages.find((m) => m.role === "user")) == null ? void 0 : _a.content.trim()) != null ? _b : "";
2289
+ const line = first.replace(/\s+/g, " ");
2290
+ return line.length > TITLE_CHARS ? `${line.slice(0, TITLE_CHARS - 1)}\u2026` : line;
2291
+ }
2292
+ function storage() {
2293
+ try {
2294
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
2295
+ } catch (e) {
2296
+ return null;
2297
+ }
2298
+ }
2299
+ function isSavedChat(value) {
2300
+ if (!value || typeof value !== "object") return false;
2301
+ const c = value;
2302
+ return typeof c.id === "string" && typeof c.title === "string" && typeof c.updatedAt === "number" && (c.status === "open" || c.status === "filed") && Array.isArray(c.messages);
2303
+ }
2304
+ function loadChats(key, now = Date.now()) {
2305
+ const store = storage();
2306
+ if (!store) return [];
2307
+ try {
2308
+ const raw = store.getItem(PREFIX + key);
2309
+ if (!raw) return [];
2310
+ const parsed = JSON.parse(raw);
2311
+ if (!Array.isArray(parsed)) return [];
2312
+ return parsed.filter(isSavedChat).filter((c) => now - c.updatedAt < SAVED_CHAT_MAX_AGE_MS).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SAVED_CHATS);
2313
+ } catch (e) {
2314
+ return [];
2315
+ }
2316
+ }
2317
+ function write(key, chats) {
2318
+ const store = storage();
2319
+ if (!store) return;
2320
+ try {
2321
+ if (chats.length === 0) store.removeItem(PREFIX + key);
2322
+ else store.setItem(PREFIX + key, JSON.stringify(chats));
2323
+ } catch (e) {
2324
+ }
2325
+ }
2326
+ function saveChat(key, chat, now = Date.now()) {
2327
+ const rest = loadChats(key, now).filter((c) => c.id !== chat.id);
2328
+ const next = [chat, ...rest].slice(0, MAX_SAVED_CHATS);
2329
+ write(key, next);
2330
+ return next;
2331
+ }
2332
+ function markChatFiled(key, conversationId, label, now = Date.now()) {
2333
+ const chats = loadChats(key, now);
2334
+ const hit = chats.find((c) => c.conversationId === conversationId);
2335
+ if (!hit || hit.status === "filed") return;
2336
+ write(
2337
+ key,
2338
+ chats.map((c) => c === hit ? { ...c, status: "filed", filedLabel: label, updatedAt: now } : c)
2339
+ );
2340
+ }
2341
+ function removeChat(key, id, now = Date.now()) {
2342
+ const next = loadChats(key, now).filter((c) => c.id !== id);
2343
+ write(key, next);
2344
+ return next;
2345
+ }
1951
2346
  function pickAssistImages(all) {
1952
2347
  const picked = [];
1953
2348
  let total = 0;
@@ -1974,6 +2369,18 @@ function pickAssistFiles(files) {
1974
2369
  }
1975
2370
  return picked;
1976
2371
  }
2372
+ function newChatId() {
2373
+ return `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
2374
+ }
2375
+ function chatAge(at2, now = Date.now()) {
2376
+ const min = Math.round((now - at2) / 6e4);
2377
+ if (min < 1) return "just now";
2378
+ if (min < 60) return `${min} min ago`;
2379
+ const hours = Math.round(min / 60);
2380
+ if (hours < 24) return `${hours} h ago`;
2381
+ if (hours < 48) return "yesterday";
2382
+ return new Date(at2).toLocaleDateString(void 0, { day: "numeric", month: "short" });
2383
+ }
1977
2384
  var STARTERS = [
1978
2385
  "Something on this page is broken",
1979
2386
  "It did the wrong thing",
@@ -2056,6 +2463,33 @@ function Avatar({
2056
2463
  }
2057
2464
  );
2058
2465
  }
2466
+ var THINKING_SLOW_SECONDS = 20;
2467
+ function ThinkingClock({ color }) {
2468
+ const [seconds, setSeconds] = (0, import_react3.useState)(0);
2469
+ (0, import_react3.useEffect)(() => {
2470
+ const started = Date.now();
2471
+ const id = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1e3)), 1e3);
2472
+ return () => clearInterval(id);
2473
+ }, []);
2474
+ return (
2475
+ // Hidden from screen readers: the bubble already says "Thinking", and a
2476
+ // number changing every second would be read out every second.
2477
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2478
+ "span",
2479
+ {
2480
+ "data-gg-thinking-clock": "",
2481
+ "aria-hidden": "true",
2482
+ style: { marginLeft: "6px", fontSize: "12px", color, fontVariantNumeric: "tabular-nums" },
2483
+ children: [
2484
+ "Thinking\u2026 ",
2485
+ seconds,
2486
+ "s",
2487
+ seconds >= THINKING_SLOW_SECONDS ? " \xB7 taking longer than usual, still working" : ""
2488
+ ]
2489
+ }
2490
+ )
2491
+ );
2492
+ }
2059
2493
  function AssistSheet({
2060
2494
  assist,
2061
2495
  theme: t,
@@ -2096,14 +2530,22 @@ function AssistSheet({
2096
2530
  onDegrade,
2097
2531
  onClose,
2098
2532
  onFinish,
2099
- onReportGlitchgrabProblem
2533
+ onReportGlitchgrabProblem,
2534
+ historyKey = null
2100
2535
  }) {
2536
+ var _a;
2101
2537
  const narrow = useIsNarrow();
2102
2538
  const severityAsked = showSeverity && currentTile === "BUG";
2103
2539
  const [messages, setMessages] = (0, import_react3.useState)([]);
2104
2540
  const [input, setInput] = (0, import_react3.useState)("");
2105
2541
  const [busy, setBusy] = (0, import_react3.useState)(false);
2106
2542
  const [conversationId, setConversationId] = (0, import_react3.useState)(null);
2543
+ const chatIdRef = (0, import_react3.useRef)(newChatId());
2544
+ const [savedChats, setSavedChats] = (0, import_react3.useState)(
2545
+ () => historyKey ? loadChats(historyKey) : []
2546
+ );
2547
+ const [showChats, setShowChats] = (0, import_react3.useState)(false);
2548
+ const [viewingChat, setViewingChat] = (0, import_react3.useState)(null);
2107
2549
  const [hoveredStar, setHoveredStar] = (0, import_react3.useState)(0);
2108
2550
  const [options, setOptions] = (0, import_react3.useState)([]);
2109
2551
  const [duplicate, setDuplicate] = (0, import_react3.useState)(null);
@@ -2155,8 +2597,8 @@ function AssistSheet({
2155
2597
  if (el) el.scrollTop = el.scrollHeight;
2156
2598
  }, [messages, busy, phase]);
2157
2599
  (0, import_react3.useEffect)(() => {
2158
- var _a;
2159
- if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
2600
+ var _a2;
2601
+ if (phase === "draft") (_a2 = draftRef.current) == null ? void 0 : _a2.focus();
2160
2602
  }, [phase]);
2161
2603
  (0, import_react3.useEffect)(() => {
2162
2604
  const el = composerRef.current;
@@ -2188,7 +2630,8 @@ function AssistSheet({
2188
2630
  onDegrade(message, retryable);
2189
2631
  }
2190
2632
  async function runTurn(history2, autoFile = false) {
2191
- var _a, _b, _c;
2633
+ var _a2, _b, _c;
2634
+ const turnChat = chatIdRef.current;
2192
2635
  setBusy(true);
2193
2636
  setAutoFileLeft(null);
2194
2637
  let result;
@@ -2225,6 +2668,7 @@ function AssistSheet({
2225
2668
  } finally {
2226
2669
  setBusy(false);
2227
2670
  }
2671
+ if (chatIdRef.current !== turnChat) return;
2228
2672
  if (result.degraded) {
2229
2673
  degradeKeepingEvents(events, result.degraded, result.retryable === true);
2230
2674
  return;
@@ -2235,7 +2679,7 @@ function AssistSheet({
2235
2679
  }
2236
2680
  const attach = result.duplicate ? { ...result.duplicate, kind: "duplicate" } : result.related ? { ...result.related, kind: "related" } : null;
2237
2681
  setDuplicate(attach);
2238
- onDuplicateChange == null ? void 0 : onDuplicateChange((_a = attach == null ? void 0 : attach.number) != null ? _a : null, attach == null ? void 0 : attach.kind);
2682
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_a2 = attach == null ? void 0 : attach.number) != null ? _a2 : null, attach == null ? void 0 : attach.kind);
2239
2683
  setGlitchgrabOffer(!!result.aboutGlitchgrab && !!onReportGlitchgrabProblem && !!result.question);
2240
2684
  if (result.solved) {
2241
2685
  setSolved(result.solved);
@@ -2289,9 +2733,9 @@ function AssistSheet({
2289
2733
  }
2290
2734
  }
2291
2735
  function sendTyped(text) {
2292
- var _a;
2736
+ var _a2;
2293
2737
  const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
2294
- const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a : "");
2738
+ const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a2 = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a2 : "");
2295
2739
  send(text, checkingBack && isAffirmativeReply(text));
2296
2740
  }
2297
2741
  function send(text, autoFile = false) {
@@ -2333,7 +2777,215 @@ function AssistSheet({
2333
2777
  flushEvents();
2334
2778
  onSend();
2335
2779
  }
2780
+ (0, import_react3.useEffect)(() => {
2781
+ if (!historyKey || !messages.some((m) => m.role === "user")) return;
2782
+ const done = submitted || phase === "solved";
2783
+ const kept = submitted || phase === "draft" ? withCurrentDraft(messages) : messages;
2784
+ setSavedChats(
2785
+ saveChat(historyKey, {
2786
+ id: chatIdRef.current,
2787
+ conversationId,
2788
+ title: chatTitle(kept),
2789
+ tile: currentTile,
2790
+ messages: kept,
2791
+ ...issueTitle ? { issueTitle } : {},
2792
+ status: done ? "filed" : "open",
2793
+ ...done ? {
2794
+ filedLabel: submitted ? submittedText != null ? submittedText : `${reportTypeLabel} sent` : "Answered \u2014 nothing was filed"
2795
+ } : {},
2796
+ updatedAt: Date.now()
2797
+ })
2798
+ );
2799
+ }, [
2800
+ historyKey,
2801
+ messages,
2802
+ conversationId,
2803
+ submitted,
2804
+ phase === "solved",
2805
+ issueTitle,
2806
+ phase === "draft" ? description : null
2807
+ ]);
2808
+ function resumeChat(chat) {
2809
+ var _a2;
2810
+ flushEvents();
2811
+ chatIdRef.current = chat.id;
2812
+ setMessages(chat.messages);
2813
+ setConversationId(chat.conversationId);
2814
+ onConversationChange == null ? void 0 : onConversationChange(chat.conversationId);
2815
+ setOptions([]);
2816
+ setDuplicate(null);
2817
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2818
+ setGlitchgrabOffer(false);
2819
+ setAutoFileLeft(null);
2820
+ setInput("");
2821
+ if (chat.tile !== currentTile && tiles.includes(chat.tile)) {
2822
+ onPickType(chat.tile);
2823
+ }
2824
+ const last = chat.messages[chat.messages.length - 1];
2825
+ if ((last == null ? void 0 : last.kind) === "report") {
2826
+ onDescriptionChange(last.content);
2827
+ onIssueTitleChange((_a2 = chat.issueTitle) != null ? _a2 : "");
2828
+ aiDraftRef.current = last.content;
2829
+ setPhase("draft");
2830
+ } else {
2831
+ setPhase("chat");
2832
+ }
2833
+ setShowChats(false);
2834
+ setViewingChat(null);
2835
+ }
2836
+ function startNewChat() {
2837
+ flushEvents();
2838
+ chatIdRef.current = newChatId();
2839
+ setMessages([]);
2840
+ setConversationId(null);
2841
+ onConversationChange == null ? void 0 : onConversationChange(null);
2842
+ setOptions([]);
2843
+ setDuplicate(null);
2844
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2845
+ setGlitchgrabOffer(false);
2846
+ setAutoFileLeft(null);
2847
+ setInput("");
2848
+ onDescriptionChange("");
2849
+ onIssueTitleChange("");
2850
+ aiDraftRef.current = null;
2851
+ setPhase("chat");
2852
+ setShowChats(false);
2853
+ setViewingChat(null);
2854
+ }
2855
+ function forgetChat(id) {
2856
+ if (!historyKey) return;
2857
+ setSavedChats(removeChat(historyKey, id));
2858
+ if (viewingChat === id) setViewingChat(null);
2859
+ }
2860
+ const otherChats = savedChats.filter((c) => c.id !== chatIdRef.current);
2861
+ const resumable = (_a = otherChats.find((c) => c.status === "open" && c.messages.length > 0)) != null ? _a : null;
2336
2862
  const empty = phase === "chat" && messages.length === 0 && !busy;
2863
+ function renderChatList() {
2864
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { "data-gg-chat-list": "", style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [
2865
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
2866
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "13px", fontWeight: 600 }, children: "Your chats" }),
2867
+ messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2868
+ "button",
2869
+ {
2870
+ type: "button",
2871
+ "data-gg-new-chat": "",
2872
+ onClick: startNewChat,
2873
+ style: {
2874
+ border: `1px solid ${t.inputBorder}`,
2875
+ borderRadius: "999px",
2876
+ background: "transparent",
2877
+ color: t.text,
2878
+ padding: "7px 12px",
2879
+ fontSize: "12px",
2880
+ fontFamily: "inherit",
2881
+ cursor: "pointer"
2882
+ },
2883
+ children: "+ New chat"
2884
+ }
2885
+ )
2886
+ ] }),
2887
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "11.5px", color: t.textMuted, lineHeight: 1.5 }, children: "Kept in this browser only, for 30 days." }),
2888
+ otherChats.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "12.5px", color: t.textMuted, padding: "12px 0" }, children: "No other chats yet." }),
2889
+ otherChats.map((chat) => {
2890
+ var _a2;
2891
+ const filed = chat.status === "filed";
2892
+ const open = viewingChat === chat.id;
2893
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2894
+ "div",
2895
+ {
2896
+ "data-gg-saved-chat": chat.status,
2897
+ style: {
2898
+ border: `1px solid ${t.border}`,
2899
+ borderRadius: "10px",
2900
+ padding: "10px 12px",
2901
+ display: "flex",
2902
+ flexDirection: "column",
2903
+ gap: "6px",
2904
+ minWidth: 0
2905
+ },
2906
+ children: [
2907
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-start", minWidth: 0 }, children: [
2908
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2909
+ "button",
2910
+ {
2911
+ type: "button",
2912
+ onClick: () => filed ? setViewingChat(open ? null : chat.id) : resumeChat(chat),
2913
+ style: {
2914
+ flex: 1,
2915
+ minWidth: 0,
2916
+ textAlign: "left",
2917
+ border: "none",
2918
+ background: "transparent",
2919
+ color: t.text,
2920
+ padding: 0,
2921
+ fontFamily: "inherit",
2922
+ cursor: "pointer",
2923
+ display: "flex",
2924
+ flexDirection: "column",
2925
+ gap: "4px"
2926
+ },
2927
+ children: [
2928
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "13px", lineHeight: 1.45, overflowWrap: "anywhere" }, children: chat.title || "Untitled chat" }),
2929
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
2930
+ filed ? (_a2 = chat.filedLabel) != null ? _a2 : "Sent" : "Not sent yet \u2014 tap to continue",
2931
+ " \xB7",
2932
+ " ",
2933
+ chatAge(chat.updatedAt)
2934
+ ] })
2935
+ ]
2936
+ }
2937
+ ),
2938
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2939
+ "button",
2940
+ {
2941
+ type: "button",
2942
+ "aria-label": `Delete chat: ${chat.title}`,
2943
+ onClick: () => forgetChat(chat.id),
2944
+ style: {
2945
+ flexShrink: 0,
2946
+ border: "none",
2947
+ background: "transparent",
2948
+ color: t.textMuted,
2949
+ fontSize: "16px",
2950
+ lineHeight: 1,
2951
+ padding: "4px 6px",
2952
+ cursor: "pointer",
2953
+ fontFamily: "inherit"
2954
+ },
2955
+ children: "\xD7"
2956
+ }
2957
+ )
2958
+ ] }),
2959
+ open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2960
+ "div",
2961
+ {
2962
+ "data-gg-saved-transcript": "",
2963
+ style: {
2964
+ display: "flex",
2965
+ flexDirection: "column",
2966
+ gap: "6px",
2967
+ borderTop: `1px solid ${t.border}`,
2968
+ paddingTop: "8px",
2969
+ fontSize: "12.5px",
2970
+ lineHeight: 1.5
2971
+ },
2972
+ children: chat.messages.map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: [
2973
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
2974
+ m.role === "user" ? "You" : m.kind === "report" ? "Report" : "Assistant",
2975
+ ":",
2976
+ " "
2977
+ ] }),
2978
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { whiteSpace: "pre-wrap" }, children: m.content })
2979
+ ] }, i))
2980
+ }
2981
+ )
2982
+ ]
2983
+ },
2984
+ chat.id
2985
+ );
2986
+ })
2987
+ ] });
2988
+ }
2337
2989
  function renderKnownIssue({ title, footer }) {
2338
2990
  if (!duplicate) return null;
2339
2991
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -2367,8 +3019,8 @@ function AssistSheet({
2367
3019
  );
2368
3020
  }
2369
3021
  const initials2 = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
2370
- var _a, _b;
2371
- return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
3022
+ var _a2, _b;
3023
+ return (_b = (_a2 = w[0]) == null ? void 0 : _a2.toUpperCase()) != null ? _b : "";
2372
3024
  }).join("");
2373
3025
  const panelStyle = narrow ? {
2374
3026
  width: "100%",
@@ -2459,7 +3111,9 @@ function AssistSheet({
2459
3111
  alignItems: "center",
2460
3112
  gap: "7px",
2461
3113
  fontSize: "14px",
2462
- fontWeight: 600
3114
+ fontWeight: 600,
3115
+ // One line even with the Chats pill beside it (#567).
3116
+ whiteSpace: "nowrap"
2463
3117
  },
2464
3118
  children: [
2465
3119
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Sparkle, { color: t.accent }),
@@ -2489,801 +3143,914 @@ function AssistSheet({
2489
3143
  projectSlot ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { marginLeft: "6px" }, children: projectSlot }) : null
2490
3144
  ] })
2491
3145
  ] }),
2492
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2493
- "button",
2494
- {
2495
- type: "button",
2496
- "data-gg-write-myself": "",
2497
- onClick: () => leave("write_myself", `header, ${phase}`),
2498
- "aria-label": "Write it myself instead",
2499
- style: {
2500
- flexShrink: 0,
2501
- border: `1px solid ${t.inputBorder}`,
2502
- borderRadius: "999px",
2503
- background: "transparent",
2504
- color: t.textMuted,
2505
- cursor: "pointer",
2506
- fontSize: "11.5px",
2507
- lineHeight: 1,
2508
- fontFamily: "inherit",
2509
- whiteSpace: "nowrap",
2510
- // ~32px tall either way — this is the escape hatch, not a hint.
2511
- padding: "9px 11px"
2512
- },
2513
- children: "Write it myself"
2514
- }
2515
- )
2516
- ]
2517
- }
2518
- ),
2519
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2520
- "div",
2521
- {
2522
- ref: scrollRef,
2523
- style: {
2524
- flex: 1,
2525
- minHeight: narrow ? "180px" : 0,
2526
- overflowY: "auto",
2527
- padding: "16px",
3146
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "6px", flexShrink: 0 }, children: [
3147
+ historyKey && (showChats || otherChats.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3148
+ "button",
3149
+ {
3150
+ type: "button",
3151
+ "data-gg-chats-toggle": "",
3152
+ disabled: busy,
3153
+ "aria-pressed": showChats,
3154
+ onClick: () => {
3155
+ setShowChats((v) => !v);
3156
+ setViewingChat(null);
3157
+ },
3158
+ style: {
3159
+ flexShrink: 0,
3160
+ border: `1px solid ${showChats ? t.accent : t.inputBorder}`,
3161
+ borderRadius: "999px",
3162
+ background: "transparent",
3163
+ color: showChats ? t.accent : t.textMuted,
3164
+ cursor: busy ? "default" : "pointer",
3165
+ opacity: busy ? 0.5 : 1,
3166
+ fontSize: "11.5px",
3167
+ lineHeight: 1,
3168
+ fontFamily: "inherit",
3169
+ whiteSpace: "nowrap",
3170
+ padding: "9px 11px"
3171
+ },
3172
+ children: showChats ? "\u2190 Chat" : `Chats \xB7 ${otherChats.length}`
3173
+ }
3174
+ ),
3175
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3176
+ "button",
3177
+ {
3178
+ type: "button",
3179
+ "data-gg-write-myself": "",
3180
+ onClick: () => leave("write_myself", `header, ${phase}`),
3181
+ "aria-label": "Write it myself instead",
3182
+ style: {
3183
+ flexShrink: 0,
3184
+ border: `1px solid ${t.inputBorder}`,
3185
+ borderRadius: "999px",
3186
+ background: "transparent",
3187
+ color: t.textMuted,
3188
+ cursor: "pointer",
3189
+ fontSize: "11.5px",
3190
+ lineHeight: 1,
3191
+ fontFamily: "inherit",
3192
+ whiteSpace: "nowrap",
3193
+ // ~32px tall either way — this is the escape hatch, not a hint.
3194
+ padding: "9px 11px"
3195
+ },
3196
+ children: "Write it myself"
3197
+ }
3198
+ )
3199
+ ] })
3200
+ ]
3201
+ }
3202
+ ),
3203
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3204
+ "div",
3205
+ {
3206
+ ref: scrollRef,
3207
+ style: {
3208
+ flex: 1,
3209
+ minHeight: narrow ? "180px" : 0,
3210
+ overflowY: "auto",
3211
+ padding: "16px",
2528
3212
  display: "flex",
2529
3213
  flexDirection: "column"
2530
3214
  },
2531
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2532
- "div",
2533
- {
2534
- style: {
2535
- marginTop: "auto",
2536
- display: "flex",
2537
- flexDirection: "column",
2538
- gap: "12px",
2539
- minWidth: 0
2540
- },
2541
- children: [
2542
- phase === "type" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2543
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2544
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2545
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3215
+ children: [
3216
+ showChats && renderChatList(),
3217
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3218
+ "div",
3219
+ {
3220
+ style: {
3221
+ marginTop: "auto",
3222
+ display: "flex",
3223
+ flexDirection: "column",
3224
+ gap: "12px",
3225
+ minWidth: 0
3226
+ },
3227
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: showChats ? "none" : "contents" }, children: [
3228
+ phase === "type" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3229
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3230
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3231
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3232
+ "div",
3233
+ {
3234
+ style: {
3235
+ padding: "9px 12px",
3236
+ borderRadius: "12px 12px 12px 4px",
3237
+ backgroundColor: t.bgSecondary,
3238
+ fontSize: "13px",
3239
+ lineHeight: 1.55,
3240
+ maxWidth: "88%"
3241
+ },
3242
+ children: [
3243
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
3244
+ "hat do you want to do? Pick one and I'll take it from there."
3245
+ ]
3246
+ }
3247
+ )
3248
+ ] }),
3249
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2546
3250
  "div",
2547
3251
  {
2548
3252
  style: {
2549
- padding: "9px 12px",
2550
- borderRadius: "12px 12px 12px 4px",
2551
- backgroundColor: t.bgSecondary,
2552
- fontSize: "13px",
2553
- lineHeight: 1.55,
2554
- maxWidth: "88%"
3253
+ display: "flex",
3254
+ flexWrap: "wrap",
3255
+ gap: "6px",
3256
+ paddingLeft: "32px"
2555
3257
  },
2556
- children: [
2557
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
2558
- "hat do you want to do? Pick one and I'll take it from there."
2559
- ]
3258
+ children: tiles.map((tile) => {
3259
+ const rate = tile === "RATING";
3260
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3261
+ "button",
3262
+ {
3263
+ type: "button",
3264
+ "data-gg-chip": "",
3265
+ "data-gg-tile": tile,
3266
+ onClick: () => pickType(tile),
3267
+ style: {
3268
+ display: "inline-flex",
3269
+ alignItems: "center",
3270
+ gap: "5px",
3271
+ // The rating is the only tile that isn't a report, so
3272
+ // it wears the star colour the dialog's hero row uses
3273
+ // rather than hiding among the report types.
3274
+ border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
3275
+ background: rate ? "rgba(245,158,11,0.08)" : "transparent",
3276
+ color: rate ? "#f59e0b" : t.textMuted,
3277
+ borderRadius: "999px",
3278
+ // 8px vertical keeps the tap target at ~32px.
3279
+ padding: "8px 12px",
3280
+ fontSize: "12px",
3281
+ fontWeight: rate ? 600 : 400,
3282
+ fontFamily: "inherit",
3283
+ cursor: "pointer"
3284
+ },
3285
+ children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3286
+ },
3287
+ tile
3288
+ );
3289
+ })
2560
3290
  }
2561
3291
  )
2562
3292
  ] }),
2563
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2564
- "div",
2565
- {
2566
- style: {
2567
- display: "flex",
2568
- flexWrap: "wrap",
2569
- gap: "6px",
2570
- paddingLeft: "32px"
2571
- },
2572
- children: tiles.map((tile) => {
2573
- const rate = tile === "RATING";
2574
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3293
+ phase === "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3295
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3296
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3297
+ "div",
3298
+ {
3299
+ style: {
3300
+ padding: "9px 12px",
3301
+ borderRadius: "12px 12px 12px 4px",
3302
+ backgroundColor: t.bgSecondary,
3303
+ fontSize: "13px",
3304
+ lineHeight: 1.55,
3305
+ maxWidth: "88%"
3306
+ },
3307
+ children: "How bad is it?"
3308
+ }
3309
+ )
3310
+ ] }),
3311
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3312
+ "div",
3313
+ {
3314
+ role: "radiogroup",
3315
+ "aria-label": "Severity",
3316
+ style: {
3317
+ display: "flex",
3318
+ flexWrap: "wrap",
3319
+ gap: "6px",
3320
+ paddingLeft: "32px"
3321
+ },
3322
+ children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2575
3323
  "button",
2576
3324
  {
2577
3325
  type: "button",
3326
+ role: "radio",
3327
+ "aria-checked": severity === s,
2578
3328
  "data-gg-chip": "",
2579
- "data-gg-tile": tile,
2580
- onClick: () => pickType(tile),
3329
+ "data-gg-severity": s,
3330
+ onClick: () => answerSeverity(s),
2581
3331
  style: {
2582
- display: "inline-flex",
2583
- alignItems: "center",
2584
- gap: "5px",
2585
- // The rating is the only tile that isn't a report, so
2586
- // it wears the star colour the dialog's hero row uses
2587
- // rather than hiding among the report types.
2588
- border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
2589
- background: rate ? "rgba(245,158,11,0.08)" : "transparent",
2590
- color: rate ? "#f59e0b" : t.textMuted,
3332
+ border: `1px solid ${t.inputBorder}`,
3333
+ background: "transparent",
3334
+ color: t.textMuted,
2591
3335
  borderRadius: "999px",
2592
- // 8px vertical keeps the tap target at ~32px.
3336
+ // Same as the picker and option chips — one chip size.
2593
3337
  padding: "8px 12px",
2594
3338
  fontSize: "12px",
2595
- fontWeight: rate ? 600 : 400,
2596
3339
  fontFamily: "inherit",
2597
3340
  cursor: "pointer"
2598
3341
  },
2599
- children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3342
+ children: SEVERITY_LABELS[s]
2600
3343
  },
2601
- tile
2602
- );
2603
- })
2604
- }
2605
- )
2606
- ] }),
2607
- phase === "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2608
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2609
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2610
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2611
- "div",
2612
- {
2613
- style: {
2614
- padding: "9px 12px",
2615
- borderRadius: "12px 12px 12px 4px",
2616
- backgroundColor: t.bgSecondary,
2617
- fontSize: "13px",
2618
- lineHeight: 1.55,
2619
- maxWidth: "88%"
2620
- },
2621
- children: "How bad is it?"
3344
+ s
3345
+ ))
2622
3346
  }
2623
3347
  )
2624
3348
  ] }),
2625
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2626
- "div",
2627
- {
2628
- role: "radiogroup",
2629
- "aria-label": "Severity",
2630
- style: {
2631
- display: "flex",
2632
- flexWrap: "wrap",
2633
- gap: "6px",
2634
- paddingLeft: "32px"
2635
- },
2636
- children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2637
- "button",
3349
+ phase === "rating" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3350
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3351
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3352
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3353
+ "div",
2638
3354
  {
2639
- type: "button",
2640
- role: "radio",
2641
- "aria-checked": severity === s,
2642
- "data-gg-chip": "",
2643
- "data-gg-severity": s,
2644
- onClick: () => answerSeverity(s),
2645
3355
  style: {
2646
- border: `1px solid ${t.inputBorder}`,
2647
- background: "transparent",
2648
- color: t.textMuted,
2649
- borderRadius: "999px",
2650
- // Same as the picker and option chips — one chip size.
2651
- padding: "8px 12px",
2652
- fontSize: "12px",
2653
- fontFamily: "inherit",
2654
- cursor: "pointer"
3356
+ padding: "9px 12px",
3357
+ borderRadius: "12px 12px 12px 4px",
3358
+ backgroundColor: t.bgSecondary,
3359
+ fontSize: "13px",
3360
+ lineHeight: 1.55,
3361
+ maxWidth: "88%"
2655
3362
  },
2656
- children: SEVERITY_LABELS[s]
2657
- },
2658
- s
2659
- ))
2660
- }
2661
- )
2662
- ] }),
2663
- phase === "rating" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2664
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2665
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2666
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3363
+ children: "How are we doing? Tap a star \u2014 the words are optional."
3364
+ }
3365
+ )
3366
+ ] }),
3367
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2667
3368
  "div",
2668
3369
  {
2669
3370
  style: {
2670
- padding: "9px 12px",
2671
- borderRadius: "12px 12px 12px 4px",
2672
- backgroundColor: t.bgSecondary,
2673
- fontSize: "13px",
2674
- lineHeight: 1.55,
2675
- maxWidth: "88%"
3371
+ display: "flex",
3372
+ flexDirection: "column",
3373
+ gap: "10px",
3374
+ marginLeft: "32px",
3375
+ padding: "12px",
3376
+ borderRadius: "12px",
3377
+ border: "1px solid rgba(245,158,11,0.4)",
3378
+ backgroundColor: "rgba(245,158,11,0.06)"
2676
3379
  },
2677
- children: "How are we doing? Tap a star \u2014 the words are optional."
2678
- }
2679
- )
2680
- ] }),
2681
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2682
- "div",
2683
- {
2684
- style: {
2685
- display: "flex",
2686
- flexDirection: "column",
2687
- gap: "10px",
2688
- marginLeft: "32px",
2689
- padding: "12px",
2690
- borderRadius: "12px",
2691
- border: "1px solid rgba(245,158,11,0.4)",
2692
- backgroundColor: "rgba(245,158,11,0.06)"
2693
- },
2694
- children: [
2695
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
2696
- const filled = star <= (hoveredStar || rating);
2697
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3380
+ children: [
3381
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
3382
+ const filled = star <= (hoveredStar || rating);
3383
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3384
+ "button",
3385
+ {
3386
+ type: "button",
3387
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
3388
+ "aria-pressed": star <= rating,
3389
+ onClick: () => onRatingChange(star),
3390
+ onMouseEnter: () => setHoveredStar(star),
3391
+ onMouseLeave: () => setHoveredStar(0),
3392
+ style: {
3393
+ border: "none",
3394
+ background: "transparent",
3395
+ padding: 0,
3396
+ cursor: "pointer",
3397
+ lineHeight: 0
3398
+ },
3399
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3400
+ "path",
3401
+ {
3402
+ d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
3403
+ fill: filled ? "#f59e0b" : "transparent",
3404
+ stroke: filled ? "#f59e0b" : t.inputBorder,
3405
+ strokeWidth: "1.5",
3406
+ strokeLinejoin: "round"
3407
+ }
3408
+ ) })
3409
+ },
3410
+ star
3411
+ );
3412
+ }) }),
3413
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3414
+ "textarea",
3415
+ {
3416
+ value: description,
3417
+ onChange: (e) => onDescriptionChange(e.target.value),
3418
+ rows: 3,
3419
+ placeholder: "What made it good \u2014 or what let you down? (optional)",
3420
+ style: {
3421
+ width: "100%",
3422
+ padding: "10px",
3423
+ borderRadius: "8px",
3424
+ border: `1px solid ${t.inputBorder}`,
3425
+ backgroundColor: t.inputBg,
3426
+ color: t.text,
3427
+ fontSize: "13px",
3428
+ lineHeight: 1.5,
3429
+ fontFamily: "inherit",
3430
+ resize: "vertical",
3431
+ outline: "none",
3432
+ boxSizing: "border-box"
3433
+ }
3434
+ }
3435
+ ),
3436
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2698
3437
  "button",
2699
3438
  {
2700
3439
  type: "button",
2701
- "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2702
- "aria-pressed": star <= rating,
2703
- onClick: () => onRatingChange(star),
2704
- onMouseEnter: () => setHoveredStar(star),
2705
- onMouseLeave: () => setHoveredStar(0),
3440
+ onClick: () => setPhase("type"),
2706
3441
  style: {
3442
+ alignSelf: "flex-start",
2707
3443
  border: "none",
2708
3444
  background: "transparent",
2709
- padding: 0,
3445
+ color: t.textMuted,
3446
+ fontSize: "12px",
3447
+ fontFamily: "inherit",
3448
+ // A bare text link is ~14px tall — half a thumb.
3449
+ padding: "9px 0",
2710
3450
  cursor: "pointer",
2711
- lineHeight: 0
3451
+ textDecoration: "underline"
2712
3452
  },
2713
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2714
- "path",
2715
- {
2716
- d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
2717
- fill: filled ? "#f59e0b" : "transparent",
2718
- stroke: filled ? "#f59e0b" : t.inputBorder,
2719
- strokeWidth: "1.5",
2720
- strokeLinejoin: "round"
2721
- }
2722
- ) })
2723
- },
2724
- star
2725
- );
2726
- }) }),
2727
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2728
- "textarea",
2729
- {
2730
- value: description,
2731
- onChange: (e) => onDescriptionChange(e.target.value),
2732
- rows: 3,
2733
- placeholder: "What made it good \u2014 or what let you down? (optional)",
2734
- style: {
2735
- width: "100%",
2736
- padding: "10px",
2737
- borderRadius: "8px",
2738
- border: `1px solid ${t.inputBorder}`,
2739
- backgroundColor: t.inputBg,
2740
- color: t.text,
2741
- fontSize: "13px",
2742
- lineHeight: 1.5,
2743
- fontFamily: "inherit",
2744
- resize: "vertical",
2745
- outline: "none",
2746
- boxSizing: "border-box"
3453
+ children: "Report something instead"
2747
3454
  }
2748
- }
2749
- ),
2750
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2751
- "button",
2752
- {
2753
- type: "button",
2754
- onClick: () => setPhase("type"),
2755
- style: {
2756
- alignSelf: "flex-start",
2757
- border: "none",
2758
- background: "transparent",
2759
- color: t.textMuted,
2760
- fontSize: "12px",
2761
- fontFamily: "inherit",
2762
- // A bare text link is ~14px tall — half a thumb.
2763
- padding: "9px 0",
2764
- cursor: "pointer",
2765
- textDecoration: "underline"
2766
- },
2767
- children: "Report something instead"
2768
- }
2769
- )
2770
- ]
2771
- }
2772
- )
2773
- ] }),
2774
- empty && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2775
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2776
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2777
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3455
+ )
3456
+ ]
3457
+ }
3458
+ )
3459
+ ] }),
3460
+ empty && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3461
+ resumable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2778
3462
  "div",
2779
3463
  {
3464
+ "data-gg-resume-chat": "",
2780
3465
  style: {
2781
- padding: "9px 12px",
2782
- borderRadius: "12px 12px 12px 4px",
3466
+ display: "flex",
3467
+ flexDirection: "column",
3468
+ gap: "8px",
3469
+ padding: "10px 12px",
3470
+ borderRadius: "10px",
3471
+ border: `1px solid ${t.accent}55`,
2783
3472
  backgroundColor: t.bgSecondary,
2784
- fontSize: "13px",
2785
- lineHeight: 1.55,
2786
- maxWidth: "88%"
3473
+ minWidth: 0
2787
3474
  },
2788
3475
  children: [
2789
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
2790
- "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
2791
- ]
2792
- }
2793
- )
2794
- ] }),
2795
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2796
- "div",
2797
- {
2798
- style: {
2799
- display: "flex",
2800
- flexWrap: "wrap",
2801
- gap: "6px",
2802
- paddingLeft: "32px"
2803
- },
2804
- children: STARTERS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2805
- "button",
2806
- {
2807
- type: "button",
2808
- "data-gg-chip": "",
2809
- onClick: () => {
2810
- logEvent("starter_tap", s);
2811
- send(s);
2812
- },
2813
- style: {
2814
- border: `1px solid ${t.inputBorder}`,
2815
- background: "transparent",
2816
- color: t.textMuted,
2817
- borderRadius: "999px",
2818
- padding: "6px 11px",
2819
- fontSize: "12px",
2820
- fontFamily: "inherit",
2821
- cursor: "pointer"
2822
- },
2823
- children: s
2824
- },
2825
- s
2826
- ))
2827
- }
2828
- )
2829
- ] }),
2830
- messages.map((m, i) => {
2831
- const mine = m.role === "user";
2832
- if (m.kind === "report") {
2833
- const live = phase === "draft" && i === messages.length - 1;
2834
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
2835
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2836
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2837
- "div",
2838
- {
2839
- "data-gg-draft-bubble": "",
2840
- style: {
2841
- maxWidth: "82%",
2842
- minWidth: 0,
2843
- padding: "9px 12px",
2844
- borderRadius: "12px 12px 12px 4px",
2845
- fontSize: "13px",
2846
- lineHeight: 1.55,
2847
- backgroundColor: t.bgSecondary,
2848
- color: t.text,
2849
- border: live ? "none" : `1px solid ${t.accent}66`
2850
- },
2851
- children: live ? (
2852
- // #402: "have a read" beside a countdown reads as a
2853
- // contradiction. Say what is about to happen instead.
2854
- 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."
2855
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3476
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3477
+ "Pick up where you left off \xB7 ",
3478
+ chatAge(resumable.updatedAt)
3479
+ ] }),
3480
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3481
+ "span",
3482
+ {
3483
+ style: {
3484
+ fontSize: "13px",
3485
+ lineHeight: 1.45,
3486
+ overflowWrap: "anywhere"
3487
+ },
3488
+ children: resumable.title
3489
+ }
3490
+ ),
3491
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: [
2856
3492
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2857
- "span",
3493
+ "button",
2858
3494
  {
3495
+ type: "button",
3496
+ "data-gg-resume-continue": "",
3497
+ onClick: () => resumeChat(resumable),
2859
3498
  style: {
2860
- display: "block",
2861
- fontSize: "10px",
2862
- fontWeight: 700,
2863
- letterSpacing: ".06em",
2864
- textTransform: "uppercase",
2865
- color: t.accent,
2866
- marginBottom: "4px"
3499
+ border: "none",
3500
+ borderRadius: "999px",
3501
+ backgroundColor: t.accent,
3502
+ color: t.accentText,
3503
+ padding: "7px 13px",
3504
+ fontSize: "12px",
3505
+ fontWeight: 600,
3506
+ fontFamily: "inherit",
3507
+ cursor: "pointer"
2867
3508
  },
2868
- children: "Draft so far"
3509
+ children: "Continue this chat"
2869
3510
  }
2870
3511
  ),
2871
3512
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2872
- "span",
3513
+ "button",
2873
3514
  {
3515
+ type: "button",
3516
+ onClick: () => setShowChats(true),
2874
3517
  style: {
2875
- display: "block",
2876
- whiteSpace: "pre-wrap",
2877
- wordBreak: "break-word",
2878
- maxHeight: "160px",
2879
- overflowY: "auto",
2880
- color: t.textMuted
3518
+ border: `1px solid ${t.inputBorder}`,
3519
+ borderRadius: "999px",
3520
+ background: "transparent",
3521
+ color: t.textMuted,
3522
+ padding: "7px 13px",
3523
+ fontSize: "12px",
3524
+ fontFamily: "inherit",
3525
+ cursor: "pointer"
2881
3526
  },
2882
- children: m.content
3527
+ children: "All chats"
2883
3528
  }
2884
3529
  )
2885
3530
  ] })
3531
+ ]
3532
+ }
3533
+ ),
3534
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3535
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3536
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3537
+ "div",
3538
+ {
3539
+ style: {
3540
+ padding: "9px 12px",
3541
+ borderRadius: "12px 12px 12px 4px",
3542
+ backgroundColor: t.bgSecondary,
3543
+ fontSize: "13px",
3544
+ lineHeight: 1.55,
3545
+ maxWidth: "88%"
3546
+ },
3547
+ children: [
3548
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
3549
+ "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
3550
+ ]
2886
3551
  }
2887
3552
  )
2888
- ] }, i);
2889
- }
2890
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2891
- "div",
2892
- {
2893
- style: {
2894
- display: "flex",
2895
- gap: "8px",
2896
- minWidth: 0,
2897
- flexDirection: mine ? "row-reverse" : "row"
2898
- },
2899
- children: [
2900
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: m.role, theme: t, initials: initials2 }),
2901
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2902
- "div",
2903
- {
2904
- style: {
2905
- maxWidth: "82%",
2906
- minWidth: 0,
2907
- padding: "9px 12px",
2908
- borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
2909
- fontSize: "13px",
2910
- lineHeight: 1.55,
2911
- // Markdown owns the assistant's line breaks and lists.
2912
- whiteSpace: mine ? "pre-wrap" : "normal",
2913
- wordBreak: "break-word",
2914
- backgroundColor: mine ? t.accent : t.bgSecondary,
2915
- color: mine ? t.accentText : t.text
2916
- },
2917
- children: mine ? m.content : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatMarkdown, { text: m.content, theme: t })
2918
- }
2919
- )
2920
- ]
2921
- },
2922
- i
2923
- );
2924
- }),
2925
- duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
2926
- glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2927
- "div",
2928
- {
2929
- "data-gg-glitchgrab-offer": "",
2930
- style: {
2931
- display: "flex",
2932
- flexDirection: "column",
2933
- gap: "8px",
2934
- padding: "10px",
2935
- borderRadius: "8px",
2936
- border: `1px solid ${t.accent}55`,
2937
- backgroundColor: `${t.accent}14`,
2938
- fontSize: "12px",
2939
- lineHeight: 1.5
2940
- },
2941
- children: [
2942
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
2943
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
2944
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
2945
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3553
+ ] }),
3554
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3555
+ "div",
3556
+ {
3557
+ style: {
3558
+ display: "flex",
3559
+ flexWrap: "wrap",
3560
+ gap: "6px",
3561
+ paddingLeft: "32px"
3562
+ },
3563
+ children: STARTERS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2946
3564
  "button",
2947
3565
  {
2948
3566
  type: "button",
2949
- "data-gg-glitchgrab-offer-send": "",
3567
+ "data-gg-chip": "",
2950
3568
  onClick: () => {
2951
- logEvent("glitchgrab_offer_tap");
2952
- setGlitchgrabOffer(false);
2953
- onReportGlitchgrabProblem(
2954
- messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
2955
- );
3569
+ logEvent("starter_tap", s);
3570
+ send(s);
2956
3571
  },
2957
3572
  style: {
2958
- padding: "8px 12px",
3573
+ border: `1px solid ${t.inputBorder}`,
3574
+ background: "transparent",
3575
+ color: t.textMuted,
2959
3576
  borderRadius: "999px",
2960
- border: "none",
2961
- backgroundColor: t.accent,
2962
- color: t.accentText,
3577
+ padding: "6px 11px",
2963
3578
  fontSize: "12px",
2964
- fontWeight: 700,
2965
3579
  fontFamily: "inherit",
2966
- cursor: "pointer",
2967
- // Both stretch, so on a phone each wrapped line reads as
2968
- // a choice rather than one button left stranded.
2969
- flex: "1 1 auto"
3580
+ cursor: "pointer"
2970
3581
  },
2971
- children: "Send to the Glitchgrab team"
2972
- }
2973
- ),
3582
+ children: s
3583
+ },
3584
+ s
3585
+ ))
3586
+ }
3587
+ )
3588
+ ] }),
3589
+ messages.map((m, i) => {
3590
+ const mine = m.role === "user";
3591
+ if (m.kind === "report") {
3592
+ const live = phase === "draft" && i === messages.length - 1;
3593
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
3594
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2974
3595
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2975
- "button",
3596
+ "div",
2976
3597
  {
2977
- type: "button",
2978
- "data-gg-glitchgrab-offer-no": "",
2979
- onClick: () => {
2980
- logEvent("glitchgrab_offer_declined");
2981
- send("No \u2014 it's about this app, not the reporting tool");
2982
- },
3598
+ "data-gg-draft-bubble": "",
2983
3599
  style: {
2984
- padding: "8px 12px",
2985
- borderRadius: "999px",
2986
- border: `1px solid ${t.inputBorder}`,
2987
- background: "transparent",
2988
- color: t.textMuted,
2989
- fontSize: "12px",
2990
- fontFamily: "inherit",
2991
- cursor: "pointer",
2992
- flex: "1 1 auto"
3600
+ maxWidth: "82%",
3601
+ minWidth: 0,
3602
+ padding: "9px 12px",
3603
+ borderRadius: "12px 12px 12px 4px",
3604
+ fontSize: "13px",
3605
+ lineHeight: 1.55,
3606
+ backgroundColor: t.bgSecondary,
3607
+ color: t.text,
3608
+ border: live ? "none" : `1px solid ${t.accent}66`
2993
3609
  },
2994
- children: "No, it's about this app"
3610
+ children: live ? (
3611
+ // #402: "have a read" beside a countdown reads as a
3612
+ // contradiction. Say what is about to happen instead.
3613
+ 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."
3614
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3615
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3616
+ "span",
3617
+ {
3618
+ style: {
3619
+ display: "block",
3620
+ fontSize: "10px",
3621
+ fontWeight: 700,
3622
+ letterSpacing: ".06em",
3623
+ textTransform: "uppercase",
3624
+ color: t.accent,
3625
+ marginBottom: "4px"
3626
+ },
3627
+ children: "Draft so far"
3628
+ }
3629
+ ),
3630
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3631
+ "span",
3632
+ {
3633
+ style: {
3634
+ display: "block",
3635
+ whiteSpace: "pre-wrap",
3636
+ wordBreak: "break-word",
3637
+ maxHeight: "160px",
3638
+ overflowY: "auto",
3639
+ color: t.textMuted
3640
+ },
3641
+ children: m.content
3642
+ }
3643
+ )
3644
+ ] })
2995
3645
  }
2996
3646
  )
2997
- ] })
2998
- ]
2999
- }
3000
- ) }),
3001
- options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3002
- "div",
3003
- {
3004
- style: {
3005
- display: "flex",
3006
- // Long labels wrapped into a ragged row that always stranded
3007
- // the last chip on a line of its own. Past ~24 characters
3008
- // these stop being chips and become a choice list, so they
3009
- // stack full width and read like one.
3010
- flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
3011
- alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
3012
- flexWrap: "wrap",
3013
- gap: "6px",
3014
- paddingLeft: "32px"
3015
- },
3016
- children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3017
- "button",
3647
+ ] }, i);
3648
+ }
3649
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3650
+ "div",
3018
3651
  {
3019
- type: "button",
3020
- "data-gg-chip": "",
3021
- "data-gg-option": "",
3022
- onClick: () => {
3023
- logEvent("option_tap", option);
3024
- send(option, option === CONFIRM_OPTION);
3025
- },
3026
3652
  style: {
3027
- border: `1px solid ${t.inputBorder}`,
3028
- background: "transparent",
3029
- color: t.textMuted,
3030
- borderRadius: "999px",
3031
- // Same 8px as the picker chips — one tap target size in
3032
- // this sheet, not two.
3033
- padding: "8px 12px",
3034
- fontSize: "12px",
3035
- fontFamily: "inherit",
3036
- cursor: "pointer",
3037
- textAlign: "left"
3653
+ display: "flex",
3654
+ gap: "8px",
3655
+ minWidth: 0,
3656
+ flexDirection: mine ? "row-reverse" : "row"
3038
3657
  },
3039
- children: option
3658
+ children: [
3659
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: m.role, theme: t, initials: initials2 }),
3660
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3661
+ "div",
3662
+ {
3663
+ style: {
3664
+ maxWidth: "82%",
3665
+ minWidth: 0,
3666
+ padding: "9px 12px",
3667
+ borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
3668
+ fontSize: "13px",
3669
+ lineHeight: 1.55,
3670
+ // Markdown owns the assistant's line breaks and lists.
3671
+ whiteSpace: mine ? "pre-wrap" : "normal",
3672
+ wordBreak: "break-word",
3673
+ backgroundColor: mine ? t.accent : t.bgSecondary,
3674
+ color: mine ? t.accentText : t.text
3675
+ },
3676
+ children: mine ? m.content : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatMarkdown, { text: m.content, theme: t })
3677
+ }
3678
+ )
3679
+ ]
3040
3680
  },
3041
- option
3042
- ))
3043
- }
3044
- ),
3045
- busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, role: "status", "aria-label": "Thinking", children: [
3046
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3047
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3048
- "span",
3681
+ i
3682
+ );
3683
+ }),
3684
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
3685
+ glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3686
+ "div",
3049
3687
  {
3688
+ "data-gg-glitchgrab-offer": "",
3050
3689
  style: {
3051
- display: "inline-flex",
3052
- alignItems: "center",
3053
- gap: "4px",
3054
- padding: "12px",
3055
- borderRadius: "12px 12px 12px 4px",
3056
- backgroundColor: t.bgSecondary
3690
+ display: "flex",
3691
+ flexDirection: "column",
3692
+ gap: "8px",
3693
+ padding: "10px",
3694
+ borderRadius: "8px",
3695
+ border: `1px solid ${t.accent}55`,
3696
+ backgroundColor: `${t.accent}14`,
3697
+ fontSize: "12px",
3698
+ lineHeight: 1.5
3057
3699
  },
3058
- children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3059
- "span",
3060
- {
3061
- style: {
3062
- width: "5px",
3063
- height: "5px",
3064
- borderRadius: "50%",
3065
- backgroundColor: t.textMuted,
3066
- animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3067
- }
3068
- },
3069
- i
3070
- ))
3700
+ children: [
3701
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
3702
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
3703
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
3704
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3705
+ "button",
3706
+ {
3707
+ type: "button",
3708
+ "data-gg-glitchgrab-offer-send": "",
3709
+ onClick: () => {
3710
+ logEvent("glitchgrab_offer_tap");
3711
+ setGlitchgrabOffer(false);
3712
+ onReportGlitchgrabProblem(
3713
+ messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
3714
+ );
3715
+ },
3716
+ style: {
3717
+ padding: "8px 12px",
3718
+ borderRadius: "999px",
3719
+ border: "none",
3720
+ backgroundColor: t.accent,
3721
+ color: t.accentText,
3722
+ fontSize: "12px",
3723
+ fontWeight: 700,
3724
+ fontFamily: "inherit",
3725
+ cursor: "pointer",
3726
+ // Both stretch, so on a phone each wrapped line reads as
3727
+ // a choice rather than one button left stranded.
3728
+ flex: "1 1 auto"
3729
+ },
3730
+ children: "Send to the Glitchgrab team"
3731
+ }
3732
+ ),
3733
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3734
+ "button",
3735
+ {
3736
+ type: "button",
3737
+ "data-gg-glitchgrab-offer-no": "",
3738
+ onClick: () => {
3739
+ logEvent("glitchgrab_offer_declined");
3740
+ send("No \u2014 it's about this app, not the reporting tool");
3741
+ },
3742
+ style: {
3743
+ padding: "8px 12px",
3744
+ borderRadius: "999px",
3745
+ border: `1px solid ${t.inputBorder}`,
3746
+ background: "transparent",
3747
+ color: t.textMuted,
3748
+ fontSize: "12px",
3749
+ fontFamily: "inherit",
3750
+ cursor: "pointer",
3751
+ flex: "1 1 auto"
3752
+ },
3753
+ children: "No, it's about this app"
3754
+ }
3755
+ )
3756
+ ] })
3757
+ ]
3071
3758
  }
3072
- )
3073
- ] }),
3074
- phase === "draft" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3075
- "div",
3076
- {
3077
- style: {
3078
- border: `1px solid ${t.accent}`,
3079
- borderRadius: "12px",
3080
- backgroundColor: t.bgSecondary,
3081
- padding: "12px",
3082
- display: "flex",
3083
- flexDirection: "column",
3084
- gap: "10px",
3085
- minWidth: 0,
3086
- boxSizing: "border-box"
3087
- },
3088
- children: [
3089
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3090
- "span",
3091
- {
3092
- style: {
3093
- fontSize: "10px",
3094
- fontWeight: 700,
3095
- letterSpacing: ".06em",
3096
- textTransform: "uppercase",
3097
- color: t.accent
3098
- },
3099
- children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3100
- }
3101
- ),
3102
- renderKnownIssue({
3103
- title: true,
3104
- footer: "Your details will be added to it instead of opening a new issue."
3105
- }),
3106
- !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3107
- // box edge is the very thing #543 is about. One line of text,
3108
- // so Enter is swallowed.
3109
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3110
- "textarea",
3111
- {
3112
- "aria-label": "Title",
3113
- value: issueTitle,
3114
- maxLength: 120,
3115
- rows: 2,
3116
- onKeyDown: (e) => {
3117
- if (e.key === "Enter") e.preventDefault();
3118
- },
3119
- onChange: (e) => {
3120
- stopAutoFile("edited the title");
3121
- onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3122
- },
3123
- style: {
3124
- width: "100%",
3125
- padding: "8px 10px",
3126
- lineHeight: 1.45,
3127
- resize: "none",
3128
- borderRadius: "8px",
3129
- border: `1px solid ${t.inputBorder}`,
3130
- backgroundColor: t.inputBg,
3131
- color: t.text,
3132
- fontSize: "13px",
3133
- fontWeight: 600,
3134
- fontFamily: "inherit",
3135
- outline: "none",
3136
- boxSizing: "border-box"
3137
- }
3138
- }
3139
- ),
3140
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3141
- "textarea",
3759
+ ) }),
3760
+ options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3761
+ "div",
3762
+ {
3763
+ style: {
3764
+ display: "flex",
3765
+ // Long labels wrapped into a ragged row that always stranded
3766
+ // the last chip on a line of its own. Past ~24 characters
3767
+ // these stop being chips and become a choice list, so they
3768
+ // stack full width and read like one.
3769
+ flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
3770
+ alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
3771
+ flexWrap: "wrap",
3772
+ gap: "6px",
3773
+ paddingLeft: "32px"
3774
+ },
3775
+ children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3776
+ "button",
3142
3777
  {
3143
- ref: draftRef,
3144
- value: description,
3145
- onChange: (e) => {
3146
- stopAutoFile("edited the draft");
3147
- onDescriptionChange(e.target.value);
3778
+ type: "button",
3779
+ "data-gg-chip": "",
3780
+ "data-gg-option": "",
3781
+ onClick: () => {
3782
+ logEvent("option_tap", option);
3783
+ send(option, option === CONFIRM_OPTION);
3148
3784
  },
3149
- rows: 6,
3150
3785
  style: {
3151
- width: "100%",
3152
- minHeight: "120px",
3153
- padding: "10px",
3154
- borderRadius: "8px",
3155
3786
  border: `1px solid ${t.inputBorder}`,
3156
- backgroundColor: t.inputBg,
3157
- color: t.text,
3158
- fontSize: "13px",
3159
- lineHeight: 1.55,
3787
+ background: "transparent",
3788
+ color: t.textMuted,
3789
+ borderRadius: "999px",
3790
+ // Same 8px as the picker chips — one tap target size in
3791
+ // this sheet, not two.
3792
+ padding: "8px 12px",
3793
+ fontSize: "12px",
3160
3794
  fontFamily: "inherit",
3161
- resize: "vertical",
3162
- outline: "none",
3163
- boxSizing: "border-box"
3164
- }
3165
- }
3166
- ),
3167
- severityAsked && autoFileLeft === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
3168
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3795
+ cursor: "pointer",
3796
+ textAlign: "left"
3797
+ },
3798
+ children: option
3799
+ },
3800
+ option
3801
+ ))
3802
+ }
3803
+ ),
3804
+ busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, role: "status", "aria-label": "Thinking", children: [
3805
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3806
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3807
+ "span",
3808
+ {
3809
+ style: {
3810
+ display: "inline-flex",
3811
+ alignItems: "center",
3812
+ // The "still working" line must wrap on a phone, not push
3813
+ // the bubble past the sheet's edge.
3814
+ flexWrap: "wrap",
3815
+ minWidth: 0,
3816
+ gap: "4px",
3817
+ padding: "12px",
3818
+ borderRadius: "12px 12px 12px 4px",
3819
+ backgroundColor: t.bgSecondary
3820
+ },
3821
+ children: [
3822
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3823
+ "span",
3824
+ {
3825
+ style: {
3826
+ width: "5px",
3827
+ height: "5px",
3828
+ borderRadius: "50%",
3829
+ backgroundColor: t.textMuted,
3830
+ animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3831
+ }
3832
+ },
3833
+ i
3834
+ )),
3835
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThinkingClock, { color: t.textMuted })
3836
+ ]
3837
+ }
3838
+ )
3839
+ ] }),
3840
+ phase === "draft" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3841
+ "div",
3842
+ {
3843
+ style: {
3844
+ border: `1px solid ${t.accent}`,
3845
+ borderRadius: "12px",
3846
+ backgroundColor: t.bgSecondary,
3847
+ padding: "12px",
3848
+ display: "flex",
3849
+ flexDirection: "column",
3850
+ gap: "10px",
3851
+ minWidth: 0,
3852
+ boxSizing: "border-box"
3853
+ },
3854
+ children: [
3855
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3169
3856
  "span",
3170
3857
  {
3171
3858
  style: {
3172
- fontSize: "11.5px",
3173
- color: t.textMuted,
3174
- display: "block",
3175
- marginBottom: "6px"
3859
+ fontSize: "10px",
3860
+ fontWeight: 700,
3861
+ letterSpacing: ".06em",
3862
+ textTransform: "uppercase",
3863
+ color: t.accent
3176
3864
  },
3177
- children: [
3178
- "How bad is it?",
3179
- !severity && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3180
- "span",
3181
- {
3182
- style: {
3183
- marginLeft: "6px",
3184
- fontSize: "11px",
3185
- color: severityRefused ? "#ef4444" : t.textMuted
3186
- },
3187
- children: "\xB7 required"
3188
- }
3189
- )
3190
- ]
3865
+ children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3191
3866
  }
3192
3867
  ),
3868
+ renderKnownIssue({
3869
+ title: true,
3870
+ footer: "Your details will be added to it instead of opening a new issue."
3871
+ }),
3872
+ !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3873
+ // box edge is the very thing #543 is about. One line of text,
3874
+ // so Enter is swallowed.
3193
3875
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3194
- "div",
3876
+ "textarea",
3195
3877
  {
3196
- role: "radiogroup",
3197
- "aria-label": "Severity",
3198
- style: { display: "flex", gap: "6px" },
3199
- children: SEVERITY_LEVELS.map((s) => {
3200
- const on = severity === s;
3201
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3202
- "button",
3203
- {
3204
- type: "button",
3205
- onClick: () => onSeverityChange(s),
3206
- role: "radio",
3207
- "aria-checked": on,
3208
- style: {
3209
- flex: 1,
3210
- minWidth: 0,
3211
- padding: "8px 2px",
3212
- borderRadius: "8px",
3213
- border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3214
- backgroundColor: on ? t.accent : "transparent",
3215
- color: on ? t.accentText : t.textMuted,
3216
- fontSize: "12px",
3217
- fontWeight: 600,
3218
- fontFamily: "inherit",
3219
- whiteSpace: "nowrap",
3220
- overflow: "hidden",
3221
- textOverflow: "ellipsis",
3222
- cursor: "pointer"
3223
- },
3224
- children: SEVERITY_LABELS[s]
3225
- },
3226
- s
3227
- );
3228
- })
3878
+ "aria-label": "Title",
3879
+ value: issueTitle,
3880
+ maxLength: 120,
3881
+ rows: 2,
3882
+ onKeyDown: (e) => {
3883
+ if (e.key === "Enter") e.preventDefault();
3884
+ },
3885
+ onChange: (e) => {
3886
+ stopAutoFile("edited the title");
3887
+ onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3888
+ },
3889
+ style: {
3890
+ width: "100%",
3891
+ padding: "8px 10px",
3892
+ lineHeight: 1.45,
3893
+ resize: "none",
3894
+ borderRadius: "8px",
3895
+ border: `1px solid ${t.inputBorder}`,
3896
+ backgroundColor: t.inputBg,
3897
+ color: t.text,
3898
+ fontSize: "13px",
3899
+ fontWeight: 600,
3900
+ fontFamily: "inherit",
3901
+ outline: "none",
3902
+ boxSizing: "border-box"
3903
+ }
3229
3904
  }
3230
- )
3231
- ] }),
3232
- attachmentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3233
- attachmentCount,
3234
- " ",
3235
- attachmentCount === 1 ? "attachment" : "attachments",
3236
- " will be sent with this."
3237
- ] })
3238
- ]
3239
- }
3240
- ),
3241
- phase === "solved" && solved && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3242
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3243
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3905
+ ),
3906
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3907
+ "textarea",
3908
+ {
3909
+ ref: draftRef,
3910
+ value: description,
3911
+ onChange: (e) => {
3912
+ stopAutoFile("edited the draft");
3913
+ onDescriptionChange(e.target.value);
3914
+ },
3915
+ rows: 6,
3916
+ style: {
3917
+ width: "100%",
3918
+ minHeight: "120px",
3919
+ padding: "10px",
3920
+ borderRadius: "8px",
3921
+ border: `1px solid ${t.inputBorder}`,
3922
+ backgroundColor: t.inputBg,
3923
+ color: t.text,
3924
+ fontSize: "13px",
3925
+ lineHeight: 1.55,
3926
+ fontFamily: "inherit",
3927
+ resize: "vertical",
3928
+ outline: "none",
3929
+ boxSizing: "border-box"
3930
+ }
3931
+ }
3932
+ ),
3933
+ severityAsked && autoFileLeft === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
3934
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3935
+ "span",
3936
+ {
3937
+ style: {
3938
+ fontSize: "11.5px",
3939
+ color: t.textMuted,
3940
+ display: "block",
3941
+ marginBottom: "6px"
3942
+ },
3943
+ children: [
3944
+ "How bad is it?",
3945
+ !severity && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3946
+ "span",
3947
+ {
3948
+ style: {
3949
+ marginLeft: "6px",
3950
+ fontSize: "11px",
3951
+ color: severityRefused ? "#ef4444" : t.textMuted
3952
+ },
3953
+ children: "\xB7 required"
3954
+ }
3955
+ )
3956
+ ]
3957
+ }
3958
+ ),
3959
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3960
+ "div",
3961
+ {
3962
+ role: "radiogroup",
3963
+ "aria-label": "Severity",
3964
+ style: { display: "flex", gap: "6px" },
3965
+ children: SEVERITY_LEVELS.map((s) => {
3966
+ const on = severity === s;
3967
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3968
+ "button",
3969
+ {
3970
+ type: "button",
3971
+ onClick: () => onSeverityChange(s),
3972
+ role: "radio",
3973
+ "aria-checked": on,
3974
+ style: {
3975
+ flex: 1,
3976
+ minWidth: 0,
3977
+ padding: "8px 2px",
3978
+ borderRadius: "8px",
3979
+ border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3980
+ backgroundColor: on ? t.accent : "transparent",
3981
+ color: on ? t.accentText : t.textMuted,
3982
+ fontSize: "12px",
3983
+ fontWeight: 600,
3984
+ fontFamily: "inherit",
3985
+ whiteSpace: "nowrap",
3986
+ overflow: "hidden",
3987
+ textOverflow: "ellipsis",
3988
+ cursor: "pointer"
3989
+ },
3990
+ children: SEVERITY_LABELS[s]
3991
+ },
3992
+ s
3993
+ );
3994
+ })
3995
+ }
3996
+ )
3997
+ ] }),
3998
+ attachmentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3999
+ attachmentCount,
4000
+ " ",
4001
+ attachmentCount === 1 ? "attachment" : "attachments",
4002
+ " will be sent with this."
4003
+ ] })
4004
+ ]
4005
+ }
4006
+ ),
4007
+ phase === "solved" && solved && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
4008
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
4009
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
4010
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4011
+ "div",
4012
+ {
4013
+ style: {
4014
+ padding: "9px 12px",
4015
+ borderRadius: "12px 12px 12px 4px",
4016
+ backgroundColor: t.bgSecondary,
4017
+ fontSize: "13px",
4018
+ lineHeight: 1.55,
4019
+ maxWidth: "88%",
4020
+ whiteSpace: "pre-wrap"
4021
+ },
4022
+ children: solved
4023
+ }
4024
+ )
4025
+ ] }),
3244
4026
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3245
- "div",
4027
+ "span",
3246
4028
  {
3247
- style: {
3248
- padding: "9px 12px",
3249
- borderRadius: "12px 12px 12px 4px",
3250
- backgroundColor: t.bgSecondary,
3251
- fontSize: "13px",
3252
- lineHeight: 1.55,
3253
- maxWidth: "88%",
3254
- whiteSpace: "pre-wrap"
3255
- },
3256
- children: solved
4029
+ style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
4030
+ children: "Nothing was filed \u2014 you are all set."
3257
4031
  }
3258
4032
  )
3259
4033
  ] }),
3260
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3261
- "span",
4034
+ submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4035
+ "div",
3262
4036
  {
3263
- style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
3264
- children: "Nothing was filed \u2014 you are all set."
4037
+ style: {
4038
+ textAlign: "center",
4039
+ padding: "24px 0",
4040
+ color: t.accent,
4041
+ fontSize: "14px",
4042
+ fontWeight: 600
4043
+ },
4044
+ children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3265
4045
  }
3266
4046
  )
3267
- ] }),
3268
- submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3269
- "div",
3270
- {
3271
- style: {
3272
- textAlign: "center",
3273
- padding: "24px 0",
3274
- color: t.accent,
3275
- fontSize: "14px",
3276
- fontWeight: 600
3277
- },
3278
- children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3279
- }
3280
- )
3281
- ]
3282
- }
3283
- )
4047
+ ] })
4048
+ }
4049
+ )
4050
+ ]
3284
4051
  }
3285
4052
  ),
3286
- !submitted && phase !== "type" && phase !== "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
4053
+ !submitted && !showChats && phase !== "type" && phase !== "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3287
4054
  "div",
3288
4055
  {
3289
4056
  style: {
@@ -3600,8 +4367,8 @@ function AssistSheet({
3600
4367
  accept: canReadPdf ? ASSIST_ATTACHMENT_ACCEPT_WITH_PDF : ASSIST_ATTACHMENT_ACCEPT,
3601
4368
  multiple: true,
3602
4369
  onChange: (e) => {
3603
- var _a;
3604
- const picked = Array.from((_a = e.target.files) != null ? _a : []);
4370
+ var _a2;
4371
+ const picked = Array.from((_a2 = e.target.files) != null ? _a2 : []);
3605
4372
  if (picked.length) {
3606
4373
  stopAutoFile("changed attachments");
3607
4374
  const images = picked.filter((f) => f.type.startsWith("image/")).length;
@@ -4579,20 +5346,20 @@ function initialsOf(name) {
4579
5346
  }).join("");
4580
5347
  }
4581
5348
  function ReporterChip({
4582
- reporter,
5349
+ reporter: reporter2,
4583
5350
  t
4584
5351
  }) {
4585
5352
  var _a;
4586
5353
  const [avatarFailed, setAvatarFailed] = (0, import_react.useState)(false);
4587
- const avatarUrl = (reporter == null ? void 0 : reporter.avatarUrl) || "";
5354
+ const avatarUrl = (reporter2 == null ? void 0 : reporter2.avatarUrl) || "";
4588
5355
  (0, import_react.useEffect)(() => {
4589
5356
  setAvatarFailed(false);
4590
5357
  }, [avatarUrl]);
4591
- const name = ((_a = reporter == null ? void 0 : reporter.name) == null ? void 0 : _a.trim()) || "";
5358
+ const name = ((_a = reporter2 == null ? void 0 : reporter2.name) == null ? void 0 : _a.trim()) || "";
4592
5359
  const isAnonymous = !name;
4593
5360
  const initials2 = isAnonymous ? "" : initialsOf(name);
4594
5361
  const label = isAnonymous ? "Anonymous" : name;
4595
- const title = isAnonymous ? "No reporter identity was passed to Glitchgrab \u2014 this report won't be attributed to anyone" : [name, reporter == null ? void 0 : reporter.email, reporter == null ? void 0 : reporter.role].filter(Boolean).join(" \xB7 ");
5362
+ const title = isAnonymous ? "No reporter identity was passed to Glitchgrab \u2014 this report won't be attributed to anyone" : [name, reporter2 == null ? void 0 : reporter2.email, reporter2 == null ? void 0 : reporter2.role].filter(Boolean).join(" \xB7 ");
4596
5363
  const showAvatarImage = !!avatarUrl && !avatarFailed;
4597
5364
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
4598
5365
  "span",
@@ -4656,7 +5423,7 @@ function ReporterChip({
4656
5423
  },
4657
5424
  children: [
4658
5425
  label,
4659
- (reporter == null ? void 0 : reporter.role) ? ` \xB7 ${reporter.role}` : ""
5426
+ (reporter2 == null ? void 0 : reporter2.role) ? ` \xB7 ${reporter2.role}` : ""
4660
5427
  ]
4661
5428
  }
4662
5429
  )
@@ -4773,11 +5540,12 @@ function ReportDialog({
4773
5540
  types,
4774
5541
  showSeverity = true,
4775
5542
  captureScreenshot = captureDefaultScreenshot,
4776
- reporter,
5543
+ reporter: reporter2,
4777
5544
  onClose,
4778
5545
  headerSlot,
4779
5546
  reportGlitchgrabProblem,
4780
- layout = "modal"
5547
+ layout = "modal",
5548
+ chatHistoryKey: chatHistoryKey2 = null
4781
5549
  }) {
4782
5550
  var _a;
4783
5551
  const sheet = layout === "sheet";
@@ -4799,6 +5567,7 @@ function ReportDialog({
4799
5567
  const [aiConversationId, setAiConversationId] = (0, import_react.useState)(null);
4800
5568
  const [issueTitle, setIssueTitle] = (0, import_react.useState)("");
4801
5569
  const [isEnhanced, setIsEnhanced] = (0, import_react.useState)(false);
5570
+ const [enhanceError, setEnhanceError] = (0, import_react.useState)(null);
4802
5571
  const [originalDescription, setOriginalDescription] = (0, import_react.useState)(
4803
5572
  null
4804
5573
  );
@@ -4882,15 +5651,15 @@ function ReportDialog({
4882
5651
  });
4883
5652
  };
4884
5653
  applyInert();
4885
- const observer = new MutationObserver((mutations) => {
5654
+ const observer2 = new MutationObserver((mutations) => {
4886
5655
  const structural = mutations.some(
4887
5656
  (m) => m.addedNodes.length > 0 || m.removedNodes.length > 0
4888
5657
  );
4889
5658
  if (structural) applyInert();
4890
5659
  });
4891
- observer.observe(document.body, { childList: true, subtree: true });
5660
+ observer2.observe(document.body, { childList: true, subtree: true });
4892
5661
  return () => {
4893
- observer.disconnect();
5662
+ observer2.disconnect();
4894
5663
  inerted.forEach((el) => el.removeAttribute("inert"));
4895
5664
  };
4896
5665
  }, [isOpen]);
@@ -5253,6 +6022,7 @@ function ReportDialog({
5253
6022
  setSelfReportFrom(null);
5254
6023
  setVoiceError(null);
5255
6024
  setIsEnhanced(false);
6025
+ setEnhanceError(null);
5256
6026
  setOriginalDescription(null);
5257
6027
  setAssistOpen(false);
5258
6028
  setAssistUsed(false);
@@ -5318,6 +6088,7 @@ function ReportDialog({
5318
6088
  const sep = base.trim() ? " " : "";
5319
6089
  setDescriptionFromVoice(base + sep + text.trim());
5320
6090
  setValidationError(null);
6091
+ setEnhanceError(null);
5321
6092
  setIsEnhanced(false);
5322
6093
  setOriginalDescription(null);
5323
6094
  }
@@ -5542,6 +6313,9 @@ function ReportDialog({
5542
6313
  setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
5543
6314
  const queued = result.status === "QUEUED" && !!result.message;
5544
6315
  setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
6316
+ if (chatHistoryKey2 && aiConversationId) {
6317
+ markChatFiled(chatHistoryKey2, aiConversationId, `${getTypeLabel(reportType)} sent`);
6318
+ }
5545
6319
  setSubmitted(true);
5546
6320
  setDescription("");
5547
6321
  setScreenshots([]);
@@ -6410,6 +7184,7 @@ function ReportDialog({
6410
7184
  notePushToTalkEdit();
6411
7185
  setDescription(e.target.value);
6412
7186
  if (validationError) setValidationError(null);
7187
+ if (enhanceError) setEnhanceError(null);
6413
7188
  if (isEnhanced) {
6414
7189
  setIsEnhanced(false);
6415
7190
  setOriginalDescription(null);
@@ -6531,6 +7306,7 @@ function ReportDialog({
6531
7306
  var _a2;
6532
7307
  if (!description.trim() || isEnhancing) return;
6533
7308
  setIsEnhancing(true);
7309
+ setEnhanceError(null);
6534
7310
  try {
6535
7311
  const polished = await enhanceText2(
6536
7312
  description,
@@ -6543,6 +7319,10 @@ function ReportDialog({
6543
7319
  if (validationError)
6544
7320
  setValidationError(null);
6545
7321
  }
7322
+ } catch (err) {
7323
+ setEnhanceError(
7324
+ err instanceof Error && err.message ? err.message : "AI enhance isn't available right now \u2014 send your report as written."
7325
+ );
6546
7326
  } finally {
6547
7327
  setIsEnhancing(false);
6548
7328
  }
@@ -6801,6 +7581,20 @@ function ReportDialog({
6801
7581
  children: voiceError
6802
7582
  }
6803
7583
  ),
7584
+ enhanceError && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7585
+ "p",
7586
+ {
7587
+ role: "alert",
7588
+ style: {
7589
+ color: "#ef4444",
7590
+ fontSize: "11px",
7591
+ lineHeight: 1.5,
7592
+ marginTop: "4px",
7593
+ marginBottom: 0
7594
+ },
7595
+ children: enhanceError
7596
+ }
7597
+ ),
6804
7598
  assistNotice && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
6805
7599
  "p",
6806
7600
  {
@@ -7361,7 +8155,7 @@ function ReportDialog({
7361
8155
  gap: "12px"
7362
8156
  },
7363
8157
  children: [
7364
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ReporterChip, { reporter, t }),
8158
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ReporterChip, { reporter: reporter2, t }),
7365
8159
  reportGlitchgrabProblem ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7366
8160
  "button",
7367
8161
  {
@@ -7641,6 +8435,7 @@ function ReportDialog({
7641
8435
  setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7642
8436
  },
7643
8437
  onConversationChange: setAiConversationId,
8438
+ historyKey: chatHistoryKey2,
7644
8439
  onFinish: handleClose,
7645
8440
  rating,
7646
8441
  onRatingChange: (value) => {
@@ -7648,7 +8443,7 @@ function ReportDialog({
7648
8443
  if (validationError) setValidationError(null);
7649
8444
  },
7650
8445
  projectSlot: headerSlot,
7651
- reporterName: (_a = reporter == null ? void 0 : reporter.name) != null ? _a : null,
8446
+ reporterName: (_a = reporter2 == null ? void 0 : reporter2.name) != null ? _a : null,
7652
8447
  description,
7653
8448
  onDescriptionChange: (value) => {
7654
8449
  setDescription(value);
@@ -9235,6 +10030,9 @@ function GlitchgrabProviderInner({
9235
10030
  onError,
9236
10031
  onReportSent
9237
10032
  });
10033
+ if (process.env.NODE_ENV !== "development") {
10034
+ installHydrationCapture(reportHydrationError);
10035
+ }
9238
10036
  (0, import_react6.useEffect)(() => {
9239
10037
  if (enableBreadcrumbs) {
9240
10038
  initBreadcrumbs(maxBreadcrumbs2, { responseBodyOrigins: responseBodyOrigins2 });
@@ -9290,7 +10088,7 @@ function GlitchgrabProviderInner({
9290
10088
  return matchesIgnorePatterns(message, ignoreErrors);
9291
10089
  };
9292
10090
  const handleError = (event) => {
9293
- var _a, _b, _c, _d, _e, _f, _g;
10091
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
9294
10092
  try {
9295
10093
  const isOpaqueCrossOrigin = event.message === "Script error." || event.message === "Script error" || !event.error && !event.filename;
9296
10094
  if (isOpaqueCrossOrigin) {
@@ -9305,6 +10103,7 @@ function GlitchgrabProviderInner({
9305
10103
  if (event.filename && EXTENSION_ORIGIN_RE.test(event.filename) || ((_a = event.error) == null ? void 0 : _a.stack) && EXTENSION_ORIGIN_RE.test(event.error.stack)) {
9306
10104
  return;
9307
10105
  }
10106
+ if (isHandledHydrationError(event.error)) return;
9308
10107
  incrementErrorCount();
9309
10108
  const context = captureContext(visitedPagesRef.current);
9310
10109
  const sig = computeSignature({
@@ -9330,6 +10129,9 @@ function GlitchgrabProviderInner({
9330
10129
  filename: (_e = event.filename) != null ? _e : "",
9331
10130
  lineno: String((_f = event.lineno) != null ? _f : ""),
9332
10131
  colno: String((_g = event.colno) != null ? _g : ""),
10132
+ // The root patch could not attach (React internals moved, or a
10133
+ // root it does not know): still label it and send the DOM context.
10134
+ ...isHydrationError((_h = event.error) != null ? _h : event.message) ? hydrationMetadata((_i = event.error) != null ? _i : event.message) : {},
9333
10135
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
9334
10136
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
9335
10137
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -9546,21 +10348,27 @@ function GlitchgrabProviderInner({
9546
10348
  () => ({ userId: signedUserId != null ? signedUserId : null, signature }),
9547
10349
  [signedUserId, signature]
9548
10350
  );
10351
+ const enhanceForDialog = (0, import_react6.useCallback)(
10352
+ async (text, screenshot) => {
10353
+ const ctx = captureContext(visitedPagesRef.current);
10354
+ const context = {
10355
+ url: ctx.url,
10356
+ visitedPages: ctx.visitedPages.slice(-5),
10357
+ breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
10358
+ };
10359
+ return await enhanceTextOrThrow(text, token, baseUrl, screenshot, context, identity);
10360
+ },
10361
+ [token, baseUrl, identity]
10362
+ );
9549
10363
  const enhance = (0, import_react6.useCallback)(
9550
10364
  async (text, screenshot) => {
9551
10365
  try {
9552
- const ctx = captureContext(visitedPagesRef.current);
9553
- const context = {
9554
- url: ctx.url,
9555
- visitedPages: ctx.visitedPages.slice(-5),
9556
- breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
9557
- };
9558
- return await enhanceText(text, token, baseUrl, screenshot, context, identity);
10366
+ return await enhanceForDialog(text, screenshot);
9559
10367
  } catch (e) {
9560
10368
  return text;
9561
10369
  }
9562
10370
  },
9563
- [token, baseUrl, identity]
10371
+ [enhanceForDialog]
9564
10372
  );
9565
10373
  const assist = (0, import_react6.useCallback)(
9566
10374
  async (params) => {
@@ -9671,7 +10479,7 @@ function GlitchgrabProviderInner({
9671
10479
  } catch (e) {
9672
10480
  }
9673
10481
  }, [openReportDialog]);
9674
- const reporter = (0, import_react6.useMemo)(() => {
10482
+ const reporter2 = (0, import_react6.useMemo)(() => {
9675
10483
  var _a;
9676
10484
  if (!session) return null;
9677
10485
  const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
@@ -9734,7 +10542,7 @@ function GlitchgrabProviderInner({
9734
10542
  {
9735
10543
  report,
9736
10544
  sendFeedback: feedback,
9737
- enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
10545
+ enhanceText: aiAssist && !aiBlockedUnsigned ? enhanceForDialog : void 0,
9738
10546
  assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
9739
10547
  findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
9740
10548
  readFile: aiAssist && !aiBlockedUnsigned ? readFile : void 0,
@@ -9742,7 +10550,8 @@ function GlitchgrabProviderInner({
9742
10550
  transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
9743
10551
  types,
9744
10552
  showSeverity,
9745
- reporter,
10553
+ reporter: reporter2,
10554
+ chatHistoryKey: chatHistoryKey("sdk", token, session == null ? void 0 : session.userId),
9746
10555
  headerSlot: projectName ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: 11, color: "#9aa0a6" }, children: [
9747
10556
  "Reporting to ",
9748
10557
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { style: { color: "#e5e5e5" }, children: projectName })
@@ -10078,6 +10887,87 @@ function useGlitchgrabActions({
10078
10887
  error
10079
10888
  };
10080
10889
  }
10890
+ async function fetchGlitchgrabReportThread({
10891
+ token,
10892
+ reportId
10893
+ }) {
10894
+ var _a, _b, _c;
10895
+ const res = await fetch(`${BASE_URL}/api/v1/sdk/reports/${encodeURIComponent(reportId)}`, {
10896
+ headers: { Authorization: `Bearer ${token}` }
10897
+ });
10898
+ const data = await res.json().catch(() => ({}));
10899
+ if (!res.ok || !data.success) throw new Error((_a = data.error) != null ? _a : "Failed to fetch report");
10900
+ return { ...data.data, comments: (_c = (_b = data.data) == null ? void 0 : _b.comments) != null ? _c : [] };
10901
+ }
10902
+ async function postGlitchgrabReportComment({
10903
+ token,
10904
+ reportId,
10905
+ message,
10906
+ reporterName,
10907
+ reporterEmail
10908
+ }) {
10909
+ var _a;
10910
+ const res = await fetch(`${BASE_URL}/api/v1/sdk/reports/${encodeURIComponent(reportId)}/comments`, {
10911
+ method: "POST",
10912
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
10913
+ body: JSON.stringify({ message, reporterName, reporterEmail })
10914
+ });
10915
+ const data = await res.json().catch(() => ({}));
10916
+ if (!res.ok || !data.success) throw new Error((_a = data.error) != null ? _a : "Failed to post comment");
10917
+ return data.data;
10918
+ }
10919
+ function useGlitchgrabReportThread({
10920
+ token,
10921
+ reportId,
10922
+ reporterName,
10923
+ reporterEmail
10924
+ }) {
10925
+ var _a;
10926
+ const [report, setReport] = (0, import_react8.useState)(null);
10927
+ const [isLoading, setIsLoading] = (0, import_react8.useState)(!!reportId);
10928
+ const [isFetching, setIsFetching] = (0, import_react8.useState)(false);
10929
+ const [isPosting, setIsPosting] = (0, import_react8.useState)(false);
10930
+ const [error, setError] = (0, import_react8.useState)(null);
10931
+ const refetch = (0, import_react8.useCallback)(async () => {
10932
+ if (!token || !reportId) {
10933
+ setReport(null);
10934
+ setIsLoading(false);
10935
+ return;
10936
+ }
10937
+ setIsFetching(true);
10938
+ setError(null);
10939
+ try {
10940
+ setReport(await fetchGlitchgrabReportThread({ token, reportId }));
10941
+ } catch (e) {
10942
+ setError(e instanceof Error ? e.message : "Network error");
10943
+ } finally {
10944
+ setIsLoading(false);
10945
+ setIsFetching(false);
10946
+ }
10947
+ }, [token, reportId]);
10948
+ (0, import_react8.useEffect)(() => {
10949
+ refetch();
10950
+ }, [refetch]);
10951
+ const addComment = (0, import_react8.useCallback)(
10952
+ async (message) => {
10953
+ if (!token || !reportId || !message.trim()) return false;
10954
+ setIsPosting(true);
10955
+ setError(null);
10956
+ try {
10957
+ await postGlitchgrabReportComment({ token, reportId, message: message.trim(), reporterName, reporterEmail });
10958
+ await refetch();
10959
+ return true;
10960
+ } catch (e) {
10961
+ setError(e instanceof Error ? e.message : "Failed to post comment");
10962
+ return false;
10963
+ } finally {
10964
+ setIsPosting(false);
10965
+ }
10966
+ },
10967
+ [token, reportId, reporterName, reporterEmail, refetch]
10968
+ );
10969
+ return { report, comments: (_a = report == null ? void 0 : report.comments) != null ? _a : [], isLoading, isFetching, isPosting, error, refetch, addComment };
10970
+ }
10081
10971
 
10082
10972
  // src/use-feedback.ts
10083
10973
  var import_react9 = require("react");
@@ -10159,12 +11049,14 @@ function useGlitchgrabFeedback(query) {
10159
11049
  computeSignature,
10160
11050
  enhanceText,
10161
11051
  fetchGlitchgrabFeedback,
11052
+ fetchGlitchgrabReportThread,
10162
11053
  fetchGlitchgrabReports,
10163
11054
  getAppContext,
10164
11055
  getBreadcrumbs,
10165
11056
  getShortcutLabel,
10166
11057
  initBreadcrumbs,
10167
11058
  matchesShortcut,
11059
+ postGlitchgrabReportComment,
10168
11060
  sanitizeUrl,
10169
11061
  sendFeedback,
10170
11062
  sendLead,
@@ -10175,6 +11067,7 @@ function useGlitchgrabFeedback(query) {
10175
11067
  useGlitchgrab,
10176
11068
  useGlitchgrabActions,
10177
11069
  useGlitchgrabFeedback,
11070
+ useGlitchgrabReportThread,
10178
11071
  useGlitchgrabReports
10179
11072
  });
10180
11073
  //# sourceMappingURL=index.js.map