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.mjs CHANGED
@@ -178,7 +178,7 @@ function argsToString(args) {
178
178
  function previewJson(value, max) {
179
179
  let out = "";
180
180
  const path = /* @__PURE__ */ new WeakSet();
181
- const write = (input, inArray) => {
181
+ const write2 = (input, inArray) => {
182
182
  if (out.length > max) return;
183
183
  let v = input;
184
184
  if (v !== null && typeof v === "object" && typeof v.toJSON === "function") {
@@ -215,7 +215,7 @@ function previewJson(value, max) {
215
215
  out += "[";
216
216
  for (let i = 0; i < obj.length && out.length <= max; i++) {
217
217
  if (i > 0) out += ",";
218
- write(obj[i], true);
218
+ write2(obj[i], true);
219
219
  }
220
220
  out += "]";
221
221
  } else {
@@ -228,13 +228,13 @@ function previewJson(value, max) {
228
228
  if (child === void 0 || typeof child === "function" || typeof child === "symbol") continue;
229
229
  out += `${first ? "" : ","}${JSON.stringify(key)}:`;
230
230
  first = false;
231
- write(child, false);
231
+ write2(child, false);
232
232
  }
233
233
  out += "}";
234
234
  }
235
235
  path.delete(obj);
236
236
  };
237
- write(value, false);
237
+ write2(value, false);
238
238
  return out;
239
239
  }
240
240
  async function readErrorBody(response, url) {
@@ -711,12 +711,21 @@ function signedFields(identity) {
711
711
  return { userId: identity.userId, signature: identity.signature };
712
712
  }
713
713
  async function enhanceText(text, token, baseUrl, screenshot, context, identity) {
714
+ try {
715
+ return await enhanceTextOrThrow(text, token, baseUrl, screenshot, context, identity);
716
+ } catch (e) {
717
+ return text;
718
+ }
719
+ }
720
+ async function enhanceTextOrThrow(text, token, baseUrl, screenshot, context, identity) {
714
721
  var _a;
722
+ const trimmed = text.trim();
723
+ if (!trimmed) return text;
724
+ const unavailable = "AI enhance isn't available right now \u2014 send your report as written.";
725
+ let response;
715
726
  try {
716
- const trimmed = text.trim();
717
- if (!trimmed) return text;
718
727
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/enhance-text`;
719
- const response = await fetch(url, {
728
+ response = await fetch(url, {
720
729
  method: "POST",
721
730
  headers: {
722
731
  "Content-Type": "application/json",
@@ -729,15 +738,20 @@ async function enhanceText(text, token, baseUrl, screenshot, context, identity)
729
738
  ...signedFields(identity)
730
739
  })
731
740
  });
732
- if (!response.ok) return text;
733
- const envelope = await response.json();
734
- if ((envelope == null ? void 0 : envelope.success) && typeof ((_a = envelope.data) == null ? void 0 : _a.text) === "string") {
735
- return envelope.data.text;
736
- }
737
- return text;
738
741
  } catch (e) {
739
- return text;
742
+ throw new Error(unavailable);
743
+ }
744
+ if (response.status === 429) {
745
+ throw new Error("Too many AI enhance tries \u2014 wait a while, or send your report as written.");
740
746
  }
747
+ const envelope = await response.json().catch(() => null);
748
+ if (response.ok && (envelope == null ? void 0 : envelope.success) && typeof ((_a = envelope.data) == null ? void 0 : _a.text) === "string") {
749
+ return envelope.data.text;
750
+ }
751
+ if (response.status === 400 && typeof (envelope == null ? void 0 : envelope.error) === "string") {
752
+ throw new Error(envelope.error);
753
+ }
754
+ throw new Error(unavailable);
741
755
  }
742
756
  async function findSimilarIssues(description, token, baseUrl, identity, files) {
743
757
  var _a;
@@ -895,6 +909,292 @@ function clearDedupCache() {
895
909
  seen.clear();
896
910
  }
897
911
 
912
+ // src/hydration.ts
913
+ var HYDRATION_CODE_RE = /Minified React error #(418|419|422|423|425)\b/;
914
+ 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;
915
+ var BAILOUT_DIGEST = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
916
+ var DOM_SNIPPET_MAX = 3e3;
917
+ var STACK_FRAMES_MAX = 40;
918
+ function messageOf(error) {
919
+ if (error && typeof error === "object" && typeof error.message === "string") {
920
+ return error.message;
921
+ }
922
+ return typeof error === "string" ? error : "";
923
+ }
924
+ function hydrationCode(message) {
925
+ if (!message) return null;
926
+ const m = HYDRATION_CODE_RE.exec(message);
927
+ if (m) return m[1];
928
+ return HYDRATION_DEV_RE.test(message) ? "dev" : null;
929
+ }
930
+ function isHydrationError(error) {
931
+ if (hydrationCode(messageOf(error))) return true;
932
+ const cause = error && typeof error === "object" ? error.cause : void 0;
933
+ return cause !== void 0 && cause !== error && hydrationCode(messageOf(cause)) !== null;
934
+ }
935
+ function isBailout(error) {
936
+ const e = error;
937
+ if (!e || typeof e !== "object") return false;
938
+ if (e.digest === BAILOUT_DIGEST) return true;
939
+ const c = e.cause;
940
+ return !!c && typeof c === "object" && c.digest === BAILOUT_DIGEST;
941
+ }
942
+ function componentStackNames(stack) {
943
+ if (!stack) return [];
944
+ const names = [];
945
+ for (const raw of stack.split("\n")) {
946
+ const line = raw.trim();
947
+ if (!line) continue;
948
+ let name = "";
949
+ if (line.startsWith("at ")) {
950
+ name = line.slice(3).split(" (")[0].trim();
951
+ } else if (line.includes("@")) {
952
+ name = line.slice(0, line.indexOf("@")).trim();
953
+ } else {
954
+ name = line;
955
+ }
956
+ if (name) names.push(name);
957
+ if (names.length >= STACK_FRAMES_MAX) break;
958
+ }
959
+ return names;
960
+ }
961
+ function componentStackKey(stack) {
962
+ return componentStackNames(stack).join(">");
963
+ }
964
+ var observer = null;
965
+ var observerStarted = false;
966
+ var observerStopAt = 0;
967
+ var OBSERVE_MS = 3e4;
968
+ function startObserver() {
969
+ if (observerStarted || typeof MutationObserver === "undefined" || typeof document === "undefined") {
970
+ return;
971
+ }
972
+ observerStarted = true;
973
+ try {
974
+ observer = new MutationObserver(() => {
975
+ if (Date.now() > observerStopAt) stopObserver();
976
+ });
977
+ observer.observe(document, { childList: true, subtree: true });
978
+ observerStopAt = Date.now() + OBSERVE_MS;
979
+ setTimeout(stopObserver, OBSERVE_MS);
980
+ } catch (e) {
981
+ observer = null;
982
+ }
983
+ }
984
+ function stopObserver() {
985
+ try {
986
+ observer == null ? void 0 : observer.disconnect();
987
+ } catch (e) {
988
+ }
989
+ observer = null;
990
+ }
991
+ function describeNode(node) {
992
+ if (!node) return "";
993
+ if (node.nodeType === 9) return "#document";
994
+ const el = node;
995
+ if (!el.tagName) return node.nodeName;
996
+ const id = el.id ? `#${el.id}` : "";
997
+ const cls = typeof el.className === "string" && el.className.trim() ? "." + el.className.trim().split(/\s+/).slice(0, 3).join(".") : "";
998
+ return `${el.tagName.toLowerCase()}${id}${cls}`;
999
+ }
1000
+ function pathOf(node) {
1001
+ const parts = [];
1002
+ let cur = node;
1003
+ while (cur && parts.length < 6) {
1004
+ parts.unshift(describeNode(cur));
1005
+ if (cur.nodeType === 9) break;
1006
+ cur = cur.parentNode;
1007
+ }
1008
+ return parts.join(" > ");
1009
+ }
1010
+ var NON_CONTENT_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT", "TEMPLATE", "TITLE", "HEAD"]);
1011
+ function isInHead(node) {
1012
+ for (let cur = node; cur; cur = cur.parentNode) {
1013
+ if (cur.nodeName === "HEAD") return true;
1014
+ if (cur.nodeName === "BODY") return false;
1015
+ }
1016
+ return false;
1017
+ }
1018
+ var GSTIN_RE = /\b\d{2}[A-Z]{5}\d{4}[A-Z][A-Z0-9]Z[A-Z0-9]\b/g;
1019
+ var PAN_RE = /\b[A-Z]{5}\d{4}[A-Z]\b/g;
1020
+ function scrubHtml(html) {
1021
+ return redactString(
1022
+ 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)))
1023
+ );
1024
+ }
1025
+ function takeRemovedServerHtml() {
1026
+ var _a, _b;
1027
+ if (!observer) return { html: "", where: "" };
1028
+ try {
1029
+ const records = observer.takeRecords();
1030
+ let html = "";
1031
+ let where = "";
1032
+ for (const rec of records) {
1033
+ if (rec.type !== "childList" || rec.removedNodes.length === 0) continue;
1034
+ if (isInHead(rec.target)) continue;
1035
+ if (!where) where = pathOf(rec.target);
1036
+ for (const node of Array.from(rec.removedNodes)) {
1037
+ if (node.nodeType === 1 && NON_CONTENT_TAGS.has(node.tagName)) continue;
1038
+ if (node.nodeType === 3 && !((_a = node.textContent) != null ? _a : "").trim()) continue;
1039
+ const piece = node.nodeType === 1 ? node.outerHTML : node.nodeType === 3 ? `#text "${(_b = node.textContent) != null ? _b : ""}"` : "";
1040
+ if (!piece) continue;
1041
+ html += (html ? "\n" : "") + piece;
1042
+ if (html.length >= DOM_SNIPPET_MAX) break;
1043
+ }
1044
+ if (html.length >= DOM_SNIPPET_MAX) break;
1045
+ }
1046
+ const scrubbed = scrubHtml(html);
1047
+ return {
1048
+ html: scrubbed.length > DOM_SNIPPET_MAX ? `${scrubbed.slice(0, DOM_SNIPPET_MAX)}\u2026[truncated]` : scrubbed,
1049
+ where
1050
+ };
1051
+ } catch (e) {
1052
+ return { html: "", where: "" };
1053
+ }
1054
+ }
1055
+ function detectForeignNodes(doc = document) {
1056
+ var _a;
1057
+ const found = [];
1058
+ try {
1059
+ const html = doc.documentElement;
1060
+ if ((html == null ? void 0 : html.classList.contains("translated-ltr")) || (html == null ? void 0 : html.classList.contains("translated-rtl"))) {
1061
+ found.push("Google Translate is active (html.translated-*)");
1062
+ }
1063
+ const fonts = doc.getElementsByTagName("font").length;
1064
+ if (fonts > 0) found.push(`<font> \xD7 ${fonts} (Google Translate or a translation extension)`);
1065
+ const detectors = doc.querySelectorAll("a[x-apple-data-detectors]").length;
1066
+ if (detectors > 0) {
1067
+ found.push(`<a x-apple-data-detectors> \xD7 ${detectors} (iOS format detection turned text into links)`);
1068
+ }
1069
+ if (doc.querySelector("[data-gr-ext-installed],[data-new-gr-c-s-check-loaded],grammarly-desktop-integration")) {
1070
+ found.push("Grammarly extension attributes");
1071
+ }
1072
+ if (doc.querySelector("[data-lt-installed]")) found.push("LanguageTool extension attributes");
1073
+ if ((_a = doc.body) == null ? void 0 : _a.hasAttribute("cz-shortcut-listen")) found.push("ColorZilla extension attribute on <body>");
1074
+ const custom = /* @__PURE__ */ new Set();
1075
+ for (const parent of [html, doc.body]) {
1076
+ if (!parent) continue;
1077
+ for (const child of Array.from(parent.children)) {
1078
+ const tag = child.tagName.toLowerCase();
1079
+ if (tag.includes("-")) custom.add(tag);
1080
+ }
1081
+ }
1082
+ if (custom.size > 0) {
1083
+ found.push(`custom elements at top level: ${Array.from(custom).slice(0, 5).join(", ")} (often an extension)`);
1084
+ }
1085
+ } catch (e) {
1086
+ }
1087
+ return found;
1088
+ }
1089
+ function hydrationTiming() {
1090
+ var _a, _b, _c, _d;
1091
+ try {
1092
+ if (typeof performance === "undefined") return "";
1093
+ const at2 = Math.round(performance.now());
1094
+ const parts = [`error at ${at2}ms after navigation start`];
1095
+ const nav = (_b = (_a = performance.getEntriesByType) == null ? void 0 : _a.call(performance, "navigation")) == null ? void 0 : _b[0];
1096
+ if (nav == null ? void 0 : nav.domContentLoadedEventEnd) {
1097
+ parts.push(`DOMContentLoaded ${Math.round(nav.domContentLoadedEventEnd)}ms`);
1098
+ }
1099
+ const fetches = ((_d = (_c = performance.getEntriesByType) == null ? void 0 : _c.call(performance, "resource")) != null ? _d : []).filter((e) => {
1100
+ const t = e.initiatorType;
1101
+ return t === "fetch" || t === "xmlhttprequest";
1102
+ }).sort((a, b) => a.responseEnd - b.responseEnd).slice(0, 8).map((e) => {
1103
+ let path = e.name;
1104
+ try {
1105
+ const u = new URL(e.name);
1106
+ path = u.origin === window.location.origin ? u.pathname : `${u.host}${u.pathname}`;
1107
+ } catch (e2) {
1108
+ }
1109
+ return `${path} ${Math.round(e.responseEnd)}ms`;
1110
+ });
1111
+ if (fetches.length > 0) parts.push(`fetches finished before it: ${fetches.join(", ")}`);
1112
+ else parts.push("no fetch had finished yet");
1113
+ return parts.join("; ");
1114
+ } catch (e) {
1115
+ return "";
1116
+ }
1117
+ }
1118
+ function hydrationMetadata(error, extra) {
1119
+ var _a, _b, _c;
1120
+ const code = (_a = hydrationCode(messageOf(error))) != null ? _a : hydrationCode(messageOf(error == null ? void 0 : error.cause));
1121
+ const meta = { errorKind: "hydration" };
1122
+ if (code) meta.hydrationCode = code;
1123
+ const cause = error == null ? void 0 : error.cause;
1124
+ if (cause && cause !== error && messageOf(cause)) {
1125
+ meta.hydrationCause = messageOf(cause).slice(0, 500);
1126
+ }
1127
+ if ((_b = extra == null ? void 0 : extra.removed) == null ? void 0 : _b.html) meta.hydrationServerHtml = extra.removed.html;
1128
+ if ((_c = extra == null ? void 0 : extra.removed) == null ? void 0 : _c.where) meta.hydrationReplacedIn = extra.removed.where;
1129
+ const foreign = detectForeignNodes();
1130
+ if (foreign.length > 0) meta.hydrationForeignNodes = foreign.join("\n");
1131
+ const timing = hydrationTiming();
1132
+ if (timing) meta.hydrationTiming = timing;
1133
+ return meta;
1134
+ }
1135
+ var handled = /* @__PURE__ */ new WeakSet();
1136
+ function isHandledHydrationError(error) {
1137
+ return !!error && typeof error === "object" && handled.has(error);
1138
+ }
1139
+ function markHandled(error) {
1140
+ if (error && typeof error === "object") {
1141
+ handled.add(error);
1142
+ const cause = error.cause;
1143
+ if (cause && typeof cause === "object") handled.add(cause);
1144
+ }
1145
+ }
1146
+ var reporter = null;
1147
+ var PATCHED = "__glitchgrabHydrationPatched";
1148
+ function findFiberRoot(container) {
1149
+ if (!container || typeof container !== "object") return null;
1150
+ try {
1151
+ const key = Object.keys(container).find((k) => k.startsWith("__reactContainer$"));
1152
+ if (!key) return null;
1153
+ const hostRoot = container[key];
1154
+ const root = hostRoot == null ? void 0 : hostRoot.stateNode;
1155
+ return root && typeof root === "object" ? root : null;
1156
+ } catch (e) {
1157
+ return null;
1158
+ }
1159
+ }
1160
+ function patchRoot(root) {
1161
+ if (root[PATCHED]) return true;
1162
+ const original = root.onRecoverableError;
1163
+ if (typeof original !== "function") return false;
1164
+ root.onRecoverableError = function glitchgrabOnRecoverableError(error, info) {
1165
+ var _a;
1166
+ try {
1167
+ if (reporter && isHydrationError(error) && !isBailout(error)) {
1168
+ const removed = takeRemovedServerHtml();
1169
+ markHandled(error);
1170
+ reporter(error, (_a = info == null ? void 0 : info.componentStack) != null ? _a : "", hydrationMetadata(error, { removed }));
1171
+ }
1172
+ } catch (e) {
1173
+ }
1174
+ return original.call(this, error, info);
1175
+ };
1176
+ root[PATCHED] = true;
1177
+ return true;
1178
+ }
1179
+ function patchKnownRoots() {
1180
+ if (typeof document === "undefined") return false;
1181
+ let patched = false;
1182
+ for (const container of [document, document.getElementById("__next"), document.getElementById("root")]) {
1183
+ const root = findFiberRoot(container);
1184
+ if (root && patchRoot(root)) patched = true;
1185
+ }
1186
+ return patched;
1187
+ }
1188
+ function installHydrationCapture(report) {
1189
+ try {
1190
+ if (typeof window === "undefined") return;
1191
+ reporter = report;
1192
+ startObserver();
1193
+ patchKnownRoots();
1194
+ } catch (e) {
1195
+ }
1196
+ }
1197
+
898
1198
  // src/capture.ts
899
1199
  var config = null;
900
1200
  function registerCaptureConfig(next) {
@@ -914,7 +1214,7 @@ function sessionMetadata(session) {
914
1214
  ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
915
1215
  };
916
1216
  }
917
- function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError") {
1217
+ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError", dedupKey) {
918
1218
  var _a, _b, _c;
919
1219
  const err = error instanceof Error ? error : void 0;
920
1220
  const errorMessage = err ? err.message : String(error);
@@ -926,7 +1226,7 @@ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureErr
926
1226
  const sig = computeSignature({
927
1227
  errorMessage,
928
1228
  pageUrl: context.url,
929
- errorStack: err == null ? void 0 : err.stack,
1229
+ errorStack: dedupKey ? `at ${dedupKey}` : err == null ? void 0 : err.stack,
930
1230
  digest
931
1231
  });
932
1232
  if (shouldSkipDuplicate(sig)) return null;
@@ -966,6 +1266,25 @@ function captureError(error, options) {
966
1266
  } catch (e) {
967
1267
  }
968
1268
  }
1269
+ function reportHydrationError(error, componentStack, metadata) {
1270
+ try {
1271
+ const cfg = config;
1272
+ if (!(cfg == null ? void 0 : cfg.token)) return;
1273
+ const payload = buildCapturedErrorPayload(
1274
+ error,
1275
+ { componentStack: componentStack || void 0, metadata },
1276
+ cfg,
1277
+ "onRecoverableError",
1278
+ componentStackKey(componentStack) || void 0
1279
+ );
1280
+ if (!payload) return;
1281
+ void sendReport(payload, cfg.baseUrl).then((result) => {
1282
+ if (result && cfg.onReportSent) cfg.onReportSent(result);
1283
+ });
1284
+ if (cfg.onError && error instanceof Error) cfg.onError(error);
1285
+ } catch (e) {
1286
+ }
1287
+ }
969
1288
 
970
1289
  // src/error-boundary.tsx
971
1290
  var GlitchgrabErrorBoundary = class extends React.Component {
@@ -1892,6 +2211,79 @@ function renderBlocks(lines, theme, key) {
1892
2211
  function ChatMarkdown({ text, theme }) {
1893
2212
  return /* @__PURE__ */ jsx2("div", { style: STACK, children: renderBlocks(text.replace(/\r\n?/g, "\n").split("\n"), theme, "md") });
1894
2213
  }
2214
+ var MAX_SAVED_CHATS = 20;
2215
+ var SAVED_CHAT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2216
+ var TITLE_CHARS = 80;
2217
+ var PREFIX = "glitchgrab:ai-chats:";
2218
+ function chatHistoryKey(...parts) {
2219
+ const input = parts.map((p) => p != null ? p : "").join("\0");
2220
+ let hash = 2166136261;
2221
+ for (let i = 0; i < input.length; i++) {
2222
+ hash ^= input.charCodeAt(i);
2223
+ hash = Math.imul(hash, 16777619);
2224
+ }
2225
+ return (hash >>> 0).toString(36);
2226
+ }
2227
+ function chatTitle(messages) {
2228
+ var _a, _b;
2229
+ const first = (_b = (_a = messages.find((m) => m.role === "user")) == null ? void 0 : _a.content.trim()) != null ? _b : "";
2230
+ const line = first.replace(/\s+/g, " ");
2231
+ return line.length > TITLE_CHARS ? `${line.slice(0, TITLE_CHARS - 1)}\u2026` : line;
2232
+ }
2233
+ function storage() {
2234
+ try {
2235
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
2236
+ } catch (e) {
2237
+ return null;
2238
+ }
2239
+ }
2240
+ function isSavedChat(value) {
2241
+ if (!value || typeof value !== "object") return false;
2242
+ const c = value;
2243
+ return typeof c.id === "string" && typeof c.title === "string" && typeof c.updatedAt === "number" && (c.status === "open" || c.status === "filed") && Array.isArray(c.messages);
2244
+ }
2245
+ function loadChats(key, now = Date.now()) {
2246
+ const store = storage();
2247
+ if (!store) return [];
2248
+ try {
2249
+ const raw = store.getItem(PREFIX + key);
2250
+ if (!raw) return [];
2251
+ const parsed = JSON.parse(raw);
2252
+ if (!Array.isArray(parsed)) return [];
2253
+ 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);
2254
+ } catch (e) {
2255
+ return [];
2256
+ }
2257
+ }
2258
+ function write(key, chats) {
2259
+ const store = storage();
2260
+ if (!store) return;
2261
+ try {
2262
+ if (chats.length === 0) store.removeItem(PREFIX + key);
2263
+ else store.setItem(PREFIX + key, JSON.stringify(chats));
2264
+ } catch (e) {
2265
+ }
2266
+ }
2267
+ function saveChat(key, chat, now = Date.now()) {
2268
+ const rest = loadChats(key, now).filter((c) => c.id !== chat.id);
2269
+ const next = [chat, ...rest].slice(0, MAX_SAVED_CHATS);
2270
+ write(key, next);
2271
+ return next;
2272
+ }
2273
+ function markChatFiled(key, conversationId, label, now = Date.now()) {
2274
+ const chats = loadChats(key, now);
2275
+ const hit = chats.find((c) => c.conversationId === conversationId);
2276
+ if (!hit || hit.status === "filed") return;
2277
+ write(
2278
+ key,
2279
+ chats.map((c) => c === hit ? { ...c, status: "filed", filedLabel: label, updatedAt: now } : c)
2280
+ );
2281
+ }
2282
+ function removeChat(key, id, now = Date.now()) {
2283
+ const next = loadChats(key, now).filter((c) => c.id !== id);
2284
+ write(key, next);
2285
+ return next;
2286
+ }
1895
2287
  function pickAssistImages(all) {
1896
2288
  const picked = [];
1897
2289
  let total = 0;
@@ -1918,6 +2310,18 @@ function pickAssistFiles(files) {
1918
2310
  }
1919
2311
  return picked;
1920
2312
  }
2313
+ function newChatId() {
2314
+ return `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
2315
+ }
2316
+ function chatAge(at2, now = Date.now()) {
2317
+ const min = Math.round((now - at2) / 6e4);
2318
+ if (min < 1) return "just now";
2319
+ if (min < 60) return `${min} min ago`;
2320
+ const hours = Math.round(min / 60);
2321
+ if (hours < 24) return `${hours} h ago`;
2322
+ if (hours < 48) return "yesterday";
2323
+ return new Date(at2).toLocaleDateString(void 0, { day: "numeric", month: "short" });
2324
+ }
1921
2325
  var STARTERS = [
1922
2326
  "Something on this page is broken",
1923
2327
  "It did the wrong thing",
@@ -2000,6 +2404,33 @@ function Avatar({
2000
2404
  }
2001
2405
  );
2002
2406
  }
2407
+ var THINKING_SLOW_SECONDS = 20;
2408
+ function ThinkingClock({ color }) {
2409
+ const [seconds, setSeconds] = useState2(0);
2410
+ useEffect2(() => {
2411
+ const started = Date.now();
2412
+ const id = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1e3)), 1e3);
2413
+ return () => clearInterval(id);
2414
+ }, []);
2415
+ return (
2416
+ // Hidden from screen readers: the bubble already says "Thinking", and a
2417
+ // number changing every second would be read out every second.
2418
+ /* @__PURE__ */ jsxs3(
2419
+ "span",
2420
+ {
2421
+ "data-gg-thinking-clock": "",
2422
+ "aria-hidden": "true",
2423
+ style: { marginLeft: "6px", fontSize: "12px", color, fontVariantNumeric: "tabular-nums" },
2424
+ children: [
2425
+ "Thinking\u2026 ",
2426
+ seconds,
2427
+ "s",
2428
+ seconds >= THINKING_SLOW_SECONDS ? " \xB7 taking longer than usual, still working" : ""
2429
+ ]
2430
+ }
2431
+ )
2432
+ );
2433
+ }
2003
2434
  function AssistSheet({
2004
2435
  assist,
2005
2436
  theme: t,
@@ -2040,14 +2471,22 @@ function AssistSheet({
2040
2471
  onDegrade,
2041
2472
  onClose,
2042
2473
  onFinish,
2043
- onReportGlitchgrabProblem
2474
+ onReportGlitchgrabProblem,
2475
+ historyKey = null
2044
2476
  }) {
2477
+ var _a;
2045
2478
  const narrow = useIsNarrow();
2046
2479
  const severityAsked = showSeverity && currentTile === "BUG";
2047
2480
  const [messages, setMessages] = useState2([]);
2048
2481
  const [input, setInput] = useState2("");
2049
2482
  const [busy, setBusy] = useState2(false);
2050
2483
  const [conversationId, setConversationId] = useState2(null);
2484
+ const chatIdRef = useRef2(newChatId());
2485
+ const [savedChats, setSavedChats] = useState2(
2486
+ () => historyKey ? loadChats(historyKey) : []
2487
+ );
2488
+ const [showChats, setShowChats] = useState2(false);
2489
+ const [viewingChat, setViewingChat] = useState2(null);
2051
2490
  const [hoveredStar, setHoveredStar] = useState2(0);
2052
2491
  const [options, setOptions] = useState2([]);
2053
2492
  const [duplicate, setDuplicate] = useState2(null);
@@ -2099,8 +2538,8 @@ function AssistSheet({
2099
2538
  if (el) el.scrollTop = el.scrollHeight;
2100
2539
  }, [messages, busy, phase]);
2101
2540
  useEffect2(() => {
2102
- var _a;
2103
- if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
2541
+ var _a2;
2542
+ if (phase === "draft") (_a2 = draftRef.current) == null ? void 0 : _a2.focus();
2104
2543
  }, [phase]);
2105
2544
  useEffect2(() => {
2106
2545
  const el = composerRef.current;
@@ -2132,7 +2571,8 @@ function AssistSheet({
2132
2571
  onDegrade(message, retryable);
2133
2572
  }
2134
2573
  async function runTurn(history2, autoFile = false) {
2135
- var _a, _b, _c;
2574
+ var _a2, _b, _c;
2575
+ const turnChat = chatIdRef.current;
2136
2576
  setBusy(true);
2137
2577
  setAutoFileLeft(null);
2138
2578
  let result;
@@ -2169,6 +2609,7 @@ function AssistSheet({
2169
2609
  } finally {
2170
2610
  setBusy(false);
2171
2611
  }
2612
+ if (chatIdRef.current !== turnChat) return;
2172
2613
  if (result.degraded) {
2173
2614
  degradeKeepingEvents(events, result.degraded, result.retryable === true);
2174
2615
  return;
@@ -2179,7 +2620,7 @@ function AssistSheet({
2179
2620
  }
2180
2621
  const attach = result.duplicate ? { ...result.duplicate, kind: "duplicate" } : result.related ? { ...result.related, kind: "related" } : null;
2181
2622
  setDuplicate(attach);
2182
- onDuplicateChange == null ? void 0 : onDuplicateChange((_a = attach == null ? void 0 : attach.number) != null ? _a : null, attach == null ? void 0 : attach.kind);
2623
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_a2 = attach == null ? void 0 : attach.number) != null ? _a2 : null, attach == null ? void 0 : attach.kind);
2183
2624
  setGlitchgrabOffer(!!result.aboutGlitchgrab && !!onReportGlitchgrabProblem && !!result.question);
2184
2625
  if (result.solved) {
2185
2626
  setSolved(result.solved);
@@ -2233,9 +2674,9 @@ function AssistSheet({
2233
2674
  }
2234
2675
  }
2235
2676
  function sendTyped(text) {
2236
- var _a;
2677
+ var _a2;
2237
2678
  const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
2238
- const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a : "");
2679
+ const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a2 = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a2 : "");
2239
2680
  send(text, checkingBack && isAffirmativeReply(text));
2240
2681
  }
2241
2682
  function send(text, autoFile = false) {
@@ -2277,7 +2718,215 @@ function AssistSheet({
2277
2718
  flushEvents();
2278
2719
  onSend();
2279
2720
  }
2721
+ useEffect2(() => {
2722
+ if (!historyKey || !messages.some((m) => m.role === "user")) return;
2723
+ const done = submitted || phase === "solved";
2724
+ const kept = submitted || phase === "draft" ? withCurrentDraft(messages) : messages;
2725
+ setSavedChats(
2726
+ saveChat(historyKey, {
2727
+ id: chatIdRef.current,
2728
+ conversationId,
2729
+ title: chatTitle(kept),
2730
+ tile: currentTile,
2731
+ messages: kept,
2732
+ ...issueTitle ? { issueTitle } : {},
2733
+ status: done ? "filed" : "open",
2734
+ ...done ? {
2735
+ filedLabel: submitted ? submittedText != null ? submittedText : `${reportTypeLabel} sent` : "Answered \u2014 nothing was filed"
2736
+ } : {},
2737
+ updatedAt: Date.now()
2738
+ })
2739
+ );
2740
+ }, [
2741
+ historyKey,
2742
+ messages,
2743
+ conversationId,
2744
+ submitted,
2745
+ phase === "solved",
2746
+ issueTitle,
2747
+ phase === "draft" ? description : null
2748
+ ]);
2749
+ function resumeChat(chat) {
2750
+ var _a2;
2751
+ flushEvents();
2752
+ chatIdRef.current = chat.id;
2753
+ setMessages(chat.messages);
2754
+ setConversationId(chat.conversationId);
2755
+ onConversationChange == null ? void 0 : onConversationChange(chat.conversationId);
2756
+ setOptions([]);
2757
+ setDuplicate(null);
2758
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2759
+ setGlitchgrabOffer(false);
2760
+ setAutoFileLeft(null);
2761
+ setInput("");
2762
+ if (chat.tile !== currentTile && tiles.includes(chat.tile)) {
2763
+ onPickType(chat.tile);
2764
+ }
2765
+ const last = chat.messages[chat.messages.length - 1];
2766
+ if ((last == null ? void 0 : last.kind) === "report") {
2767
+ onDescriptionChange(last.content);
2768
+ onIssueTitleChange((_a2 = chat.issueTitle) != null ? _a2 : "");
2769
+ aiDraftRef.current = last.content;
2770
+ setPhase("draft");
2771
+ } else {
2772
+ setPhase("chat");
2773
+ }
2774
+ setShowChats(false);
2775
+ setViewingChat(null);
2776
+ }
2777
+ function startNewChat() {
2778
+ flushEvents();
2779
+ chatIdRef.current = newChatId();
2780
+ setMessages([]);
2781
+ setConversationId(null);
2782
+ onConversationChange == null ? void 0 : onConversationChange(null);
2783
+ setOptions([]);
2784
+ setDuplicate(null);
2785
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2786
+ setGlitchgrabOffer(false);
2787
+ setAutoFileLeft(null);
2788
+ setInput("");
2789
+ onDescriptionChange("");
2790
+ onIssueTitleChange("");
2791
+ aiDraftRef.current = null;
2792
+ setPhase("chat");
2793
+ setShowChats(false);
2794
+ setViewingChat(null);
2795
+ }
2796
+ function forgetChat(id) {
2797
+ if (!historyKey) return;
2798
+ setSavedChats(removeChat(historyKey, id));
2799
+ if (viewingChat === id) setViewingChat(null);
2800
+ }
2801
+ const otherChats = savedChats.filter((c) => c.id !== chatIdRef.current);
2802
+ const resumable = (_a = otherChats.find((c) => c.status === "open" && c.messages.length > 0)) != null ? _a : null;
2280
2803
  const empty = phase === "chat" && messages.length === 0 && !busy;
2804
+ function renderChatList() {
2805
+ return /* @__PURE__ */ jsxs3("div", { "data-gg-chat-list": "", style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [
2806
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
2807
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: "13px", fontWeight: 600 }, children: "Your chats" }),
2808
+ messages.length > 0 && /* @__PURE__ */ jsx3(
2809
+ "button",
2810
+ {
2811
+ type: "button",
2812
+ "data-gg-new-chat": "",
2813
+ onClick: startNewChat,
2814
+ style: {
2815
+ border: `1px solid ${t.inputBorder}`,
2816
+ borderRadius: "999px",
2817
+ background: "transparent",
2818
+ color: t.text,
2819
+ padding: "7px 12px",
2820
+ fontSize: "12px",
2821
+ fontFamily: "inherit",
2822
+ cursor: "pointer"
2823
+ },
2824
+ children: "+ New chat"
2825
+ }
2826
+ )
2827
+ ] }),
2828
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: "11.5px", color: t.textMuted, lineHeight: 1.5 }, children: "Kept in this browser only, for 30 days." }),
2829
+ otherChats.length === 0 && /* @__PURE__ */ jsx3("span", { style: { fontSize: "12.5px", color: t.textMuted, padding: "12px 0" }, children: "No other chats yet." }),
2830
+ otherChats.map((chat) => {
2831
+ var _a2;
2832
+ const filed = chat.status === "filed";
2833
+ const open = viewingChat === chat.id;
2834
+ return /* @__PURE__ */ jsxs3(
2835
+ "div",
2836
+ {
2837
+ "data-gg-saved-chat": chat.status,
2838
+ style: {
2839
+ border: `1px solid ${t.border}`,
2840
+ borderRadius: "10px",
2841
+ padding: "10px 12px",
2842
+ display: "flex",
2843
+ flexDirection: "column",
2844
+ gap: "6px",
2845
+ minWidth: 0
2846
+ },
2847
+ children: [
2848
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", alignItems: "flex-start", minWidth: 0 }, children: [
2849
+ /* @__PURE__ */ jsxs3(
2850
+ "button",
2851
+ {
2852
+ type: "button",
2853
+ onClick: () => filed ? setViewingChat(open ? null : chat.id) : resumeChat(chat),
2854
+ style: {
2855
+ flex: 1,
2856
+ minWidth: 0,
2857
+ textAlign: "left",
2858
+ border: "none",
2859
+ background: "transparent",
2860
+ color: t.text,
2861
+ padding: 0,
2862
+ fontFamily: "inherit",
2863
+ cursor: "pointer",
2864
+ display: "flex",
2865
+ flexDirection: "column",
2866
+ gap: "4px"
2867
+ },
2868
+ children: [
2869
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: "13px", lineHeight: 1.45, overflowWrap: "anywhere" }, children: chat.title || "Untitled chat" }),
2870
+ /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
2871
+ filed ? (_a2 = chat.filedLabel) != null ? _a2 : "Sent" : "Not sent yet \u2014 tap to continue",
2872
+ " \xB7",
2873
+ " ",
2874
+ chatAge(chat.updatedAt)
2875
+ ] })
2876
+ ]
2877
+ }
2878
+ ),
2879
+ /* @__PURE__ */ jsx3(
2880
+ "button",
2881
+ {
2882
+ type: "button",
2883
+ "aria-label": `Delete chat: ${chat.title}`,
2884
+ onClick: () => forgetChat(chat.id),
2885
+ style: {
2886
+ flexShrink: 0,
2887
+ border: "none",
2888
+ background: "transparent",
2889
+ color: t.textMuted,
2890
+ fontSize: "16px",
2891
+ lineHeight: 1,
2892
+ padding: "4px 6px",
2893
+ cursor: "pointer",
2894
+ fontFamily: "inherit"
2895
+ },
2896
+ children: "\xD7"
2897
+ }
2898
+ )
2899
+ ] }),
2900
+ open && /* @__PURE__ */ jsx3(
2901
+ "div",
2902
+ {
2903
+ "data-gg-saved-transcript": "",
2904
+ style: {
2905
+ display: "flex",
2906
+ flexDirection: "column",
2907
+ gap: "6px",
2908
+ borderTop: `1px solid ${t.border}`,
2909
+ paddingTop: "8px",
2910
+ fontSize: "12.5px",
2911
+ lineHeight: 1.5
2912
+ },
2913
+ children: chat.messages.map((m, i) => /* @__PURE__ */ jsxs3("div", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: [
2914
+ /* @__PURE__ */ jsxs3("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
2915
+ m.role === "user" ? "You" : m.kind === "report" ? "Report" : "Assistant",
2916
+ ":",
2917
+ " "
2918
+ ] }),
2919
+ /* @__PURE__ */ jsx3("span", { style: { whiteSpace: "pre-wrap" }, children: m.content })
2920
+ ] }, i))
2921
+ }
2922
+ )
2923
+ ]
2924
+ },
2925
+ chat.id
2926
+ );
2927
+ })
2928
+ ] });
2929
+ }
2281
2930
  function renderKnownIssue({ title, footer }) {
2282
2931
  if (!duplicate) return null;
2283
2932
  return /* @__PURE__ */ jsxs3(
@@ -2311,8 +2960,8 @@ function AssistSheet({
2311
2960
  );
2312
2961
  }
2313
2962
  const initials2 = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
2314
- var _a, _b;
2315
- return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
2963
+ var _a2, _b;
2964
+ return (_b = (_a2 = w[0]) == null ? void 0 : _a2.toUpperCase()) != null ? _b : "";
2316
2965
  }).join("");
2317
2966
  const panelStyle = narrow ? {
2318
2967
  width: "100%",
@@ -2403,7 +3052,9 @@ function AssistSheet({
2403
3052
  alignItems: "center",
2404
3053
  gap: "7px",
2405
3054
  fontSize: "14px",
2406
- fontWeight: 600
3055
+ fontWeight: 600,
3056
+ // One line even with the Chats pill beside it (#567).
3057
+ whiteSpace: "nowrap"
2407
3058
  },
2408
3059
  children: [
2409
3060
  /* @__PURE__ */ jsx3(Sparkle, { color: t.accent }),
@@ -2433,801 +3084,914 @@ function AssistSheet({
2433
3084
  projectSlot ? /* @__PURE__ */ jsx3("span", { style: { marginLeft: "6px" }, children: projectSlot }) : null
2434
3085
  ] })
2435
3086
  ] }),
2436
- /* @__PURE__ */ jsx3(
2437
- "button",
2438
- {
2439
- type: "button",
2440
- "data-gg-write-myself": "",
2441
- onClick: () => leave("write_myself", `header, ${phase}`),
2442
- "aria-label": "Write it myself instead",
2443
- style: {
2444
- flexShrink: 0,
2445
- border: `1px solid ${t.inputBorder}`,
2446
- borderRadius: "999px",
2447
- background: "transparent",
2448
- color: t.textMuted,
2449
- cursor: "pointer",
2450
- fontSize: "11.5px",
2451
- lineHeight: 1,
2452
- fontFamily: "inherit",
2453
- whiteSpace: "nowrap",
2454
- // ~32px tall either way — this is the escape hatch, not a hint.
2455
- padding: "9px 11px"
2456
- },
2457
- children: "Write it myself"
2458
- }
2459
- )
2460
- ]
2461
- }
2462
- ),
2463
- /* @__PURE__ */ jsx3(
2464
- "div",
2465
- {
2466
- ref: scrollRef,
2467
- style: {
2468
- flex: 1,
2469
- minHeight: narrow ? "180px" : 0,
2470
- overflowY: "auto",
2471
- padding: "16px",
3087
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "6px", flexShrink: 0 }, children: [
3088
+ historyKey && (showChats || otherChats.length > 0) && /* @__PURE__ */ jsx3(
3089
+ "button",
3090
+ {
3091
+ type: "button",
3092
+ "data-gg-chats-toggle": "",
3093
+ disabled: busy,
3094
+ "aria-pressed": showChats,
3095
+ onClick: () => {
3096
+ setShowChats((v) => !v);
3097
+ setViewingChat(null);
3098
+ },
3099
+ style: {
3100
+ flexShrink: 0,
3101
+ border: `1px solid ${showChats ? t.accent : t.inputBorder}`,
3102
+ borderRadius: "999px",
3103
+ background: "transparent",
3104
+ color: showChats ? t.accent : t.textMuted,
3105
+ cursor: busy ? "default" : "pointer",
3106
+ opacity: busy ? 0.5 : 1,
3107
+ fontSize: "11.5px",
3108
+ lineHeight: 1,
3109
+ fontFamily: "inherit",
3110
+ whiteSpace: "nowrap",
3111
+ padding: "9px 11px"
3112
+ },
3113
+ children: showChats ? "\u2190 Chat" : `Chats \xB7 ${otherChats.length}`
3114
+ }
3115
+ ),
3116
+ /* @__PURE__ */ jsx3(
3117
+ "button",
3118
+ {
3119
+ type: "button",
3120
+ "data-gg-write-myself": "",
3121
+ onClick: () => leave("write_myself", `header, ${phase}`),
3122
+ "aria-label": "Write it myself instead",
3123
+ style: {
3124
+ flexShrink: 0,
3125
+ border: `1px solid ${t.inputBorder}`,
3126
+ borderRadius: "999px",
3127
+ background: "transparent",
3128
+ color: t.textMuted,
3129
+ cursor: "pointer",
3130
+ fontSize: "11.5px",
3131
+ lineHeight: 1,
3132
+ fontFamily: "inherit",
3133
+ whiteSpace: "nowrap",
3134
+ // ~32px tall either way — this is the escape hatch, not a hint.
3135
+ padding: "9px 11px"
3136
+ },
3137
+ children: "Write it myself"
3138
+ }
3139
+ )
3140
+ ] })
3141
+ ]
3142
+ }
3143
+ ),
3144
+ /* @__PURE__ */ jsxs3(
3145
+ "div",
3146
+ {
3147
+ ref: scrollRef,
3148
+ style: {
3149
+ flex: 1,
3150
+ minHeight: narrow ? "180px" : 0,
3151
+ overflowY: "auto",
3152
+ padding: "16px",
2472
3153
  display: "flex",
2473
3154
  flexDirection: "column"
2474
3155
  },
2475
- children: /* @__PURE__ */ jsxs3(
2476
- "div",
2477
- {
2478
- style: {
2479
- marginTop: "auto",
2480
- display: "flex",
2481
- flexDirection: "column",
2482
- gap: "12px",
2483
- minWidth: 0
2484
- },
2485
- children: [
2486
- phase === "type" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2487
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2488
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2489
- /* @__PURE__ */ jsxs3(
3156
+ children: [
3157
+ showChats && renderChatList(),
3158
+ /* @__PURE__ */ jsx3(
3159
+ "div",
3160
+ {
3161
+ style: {
3162
+ marginTop: "auto",
3163
+ display: "flex",
3164
+ flexDirection: "column",
3165
+ gap: "12px",
3166
+ minWidth: 0
3167
+ },
3168
+ children: /* @__PURE__ */ jsxs3("div", { style: { display: showChats ? "none" : "contents" }, children: [
3169
+ phase === "type" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3170
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3171
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3172
+ /* @__PURE__ */ jsxs3(
3173
+ "div",
3174
+ {
3175
+ style: {
3176
+ padding: "9px 12px",
3177
+ borderRadius: "12px 12px 12px 4px",
3178
+ backgroundColor: t.bgSecondary,
3179
+ fontSize: "13px",
3180
+ lineHeight: 1.55,
3181
+ maxWidth: "88%"
3182
+ },
3183
+ children: [
3184
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
3185
+ "hat do you want to do? Pick one and I'll take it from there."
3186
+ ]
3187
+ }
3188
+ )
3189
+ ] }),
3190
+ /* @__PURE__ */ jsx3(
2490
3191
  "div",
2491
3192
  {
2492
3193
  style: {
2493
- padding: "9px 12px",
2494
- borderRadius: "12px 12px 12px 4px",
2495
- backgroundColor: t.bgSecondary,
2496
- fontSize: "13px",
2497
- lineHeight: 1.55,
2498
- maxWidth: "88%"
3194
+ display: "flex",
3195
+ flexWrap: "wrap",
3196
+ gap: "6px",
3197
+ paddingLeft: "32px"
2499
3198
  },
2500
- children: [
2501
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
2502
- "hat do you want to do? Pick one and I'll take it from there."
2503
- ]
3199
+ children: tiles.map((tile) => {
3200
+ const rate = tile === "RATING";
3201
+ return /* @__PURE__ */ jsx3(
3202
+ "button",
3203
+ {
3204
+ type: "button",
3205
+ "data-gg-chip": "",
3206
+ "data-gg-tile": tile,
3207
+ onClick: () => pickType(tile),
3208
+ style: {
3209
+ display: "inline-flex",
3210
+ alignItems: "center",
3211
+ gap: "5px",
3212
+ // The rating is the only tile that isn't a report, so
3213
+ // it wears the star colour the dialog's hero row uses
3214
+ // rather than hiding among the report types.
3215
+ border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
3216
+ background: rate ? "rgba(245,158,11,0.08)" : "transparent",
3217
+ color: rate ? "#f59e0b" : t.textMuted,
3218
+ borderRadius: "999px",
3219
+ // 8px vertical keeps the tap target at ~32px.
3220
+ padding: "8px 12px",
3221
+ fontSize: "12px",
3222
+ fontWeight: rate ? 600 : 400,
3223
+ fontFamily: "inherit",
3224
+ cursor: "pointer"
3225
+ },
3226
+ children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3227
+ },
3228
+ tile
3229
+ );
3230
+ })
2504
3231
  }
2505
3232
  )
2506
3233
  ] }),
2507
- /* @__PURE__ */ jsx3(
2508
- "div",
2509
- {
2510
- style: {
2511
- display: "flex",
2512
- flexWrap: "wrap",
2513
- gap: "6px",
2514
- paddingLeft: "32px"
2515
- },
2516
- children: tiles.map((tile) => {
2517
- const rate = tile === "RATING";
2518
- return /* @__PURE__ */ jsx3(
3234
+ phase === "severity" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3235
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3236
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3237
+ /* @__PURE__ */ jsx3(
3238
+ "div",
3239
+ {
3240
+ style: {
3241
+ padding: "9px 12px",
3242
+ borderRadius: "12px 12px 12px 4px",
3243
+ backgroundColor: t.bgSecondary,
3244
+ fontSize: "13px",
3245
+ lineHeight: 1.55,
3246
+ maxWidth: "88%"
3247
+ },
3248
+ children: "How bad is it?"
3249
+ }
3250
+ )
3251
+ ] }),
3252
+ /* @__PURE__ */ jsx3(
3253
+ "div",
3254
+ {
3255
+ role: "radiogroup",
3256
+ "aria-label": "Severity",
3257
+ style: {
3258
+ display: "flex",
3259
+ flexWrap: "wrap",
3260
+ gap: "6px",
3261
+ paddingLeft: "32px"
3262
+ },
3263
+ children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ jsx3(
2519
3264
  "button",
2520
3265
  {
2521
3266
  type: "button",
3267
+ role: "radio",
3268
+ "aria-checked": severity === s,
2522
3269
  "data-gg-chip": "",
2523
- "data-gg-tile": tile,
2524
- onClick: () => pickType(tile),
3270
+ "data-gg-severity": s,
3271
+ onClick: () => answerSeverity(s),
2525
3272
  style: {
2526
- display: "inline-flex",
2527
- alignItems: "center",
2528
- gap: "5px",
2529
- // The rating is the only tile that isn't a report, so
2530
- // it wears the star colour the dialog's hero row uses
2531
- // rather than hiding among the report types.
2532
- border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
2533
- background: rate ? "rgba(245,158,11,0.08)" : "transparent",
2534
- color: rate ? "#f59e0b" : t.textMuted,
3273
+ border: `1px solid ${t.inputBorder}`,
3274
+ background: "transparent",
3275
+ color: t.textMuted,
2535
3276
  borderRadius: "999px",
2536
- // 8px vertical keeps the tap target at ~32px.
3277
+ // Same as the picker and option chips — one chip size.
2537
3278
  padding: "8px 12px",
2538
3279
  fontSize: "12px",
2539
- fontWeight: rate ? 600 : 400,
2540
3280
  fontFamily: "inherit",
2541
3281
  cursor: "pointer"
2542
3282
  },
2543
- children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3283
+ children: SEVERITY_LABELS[s]
2544
3284
  },
2545
- tile
2546
- );
2547
- })
2548
- }
2549
- )
2550
- ] }),
2551
- phase === "severity" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2552
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2553
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2554
- /* @__PURE__ */ jsx3(
2555
- "div",
2556
- {
2557
- style: {
2558
- padding: "9px 12px",
2559
- borderRadius: "12px 12px 12px 4px",
2560
- backgroundColor: t.bgSecondary,
2561
- fontSize: "13px",
2562
- lineHeight: 1.55,
2563
- maxWidth: "88%"
2564
- },
2565
- children: "How bad is it?"
3285
+ s
3286
+ ))
2566
3287
  }
2567
3288
  )
2568
3289
  ] }),
2569
- /* @__PURE__ */ jsx3(
2570
- "div",
2571
- {
2572
- role: "radiogroup",
2573
- "aria-label": "Severity",
2574
- style: {
2575
- display: "flex",
2576
- flexWrap: "wrap",
2577
- gap: "6px",
2578
- paddingLeft: "32px"
2579
- },
2580
- children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ jsx3(
2581
- "button",
3290
+ phase === "rating" && !submitted && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3291
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3292
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3293
+ /* @__PURE__ */ jsx3(
3294
+ "div",
2582
3295
  {
2583
- type: "button",
2584
- role: "radio",
2585
- "aria-checked": severity === s,
2586
- "data-gg-chip": "",
2587
- "data-gg-severity": s,
2588
- onClick: () => answerSeverity(s),
2589
3296
  style: {
2590
- border: `1px solid ${t.inputBorder}`,
2591
- background: "transparent",
2592
- color: t.textMuted,
2593
- borderRadius: "999px",
2594
- // Same as the picker and option chips — one chip size.
2595
- padding: "8px 12px",
2596
- fontSize: "12px",
2597
- fontFamily: "inherit",
2598
- cursor: "pointer"
3297
+ padding: "9px 12px",
3298
+ borderRadius: "12px 12px 12px 4px",
3299
+ backgroundColor: t.bgSecondary,
3300
+ fontSize: "13px",
3301
+ lineHeight: 1.55,
3302
+ maxWidth: "88%"
2599
3303
  },
2600
- children: SEVERITY_LABELS[s]
2601
- },
2602
- s
2603
- ))
2604
- }
2605
- )
2606
- ] }),
2607
- phase === "rating" && !submitted && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2608
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2609
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2610
- /* @__PURE__ */ jsx3(
3304
+ children: "How are we doing? Tap a star \u2014 the words are optional."
3305
+ }
3306
+ )
3307
+ ] }),
3308
+ /* @__PURE__ */ jsxs3(
2611
3309
  "div",
2612
3310
  {
2613
3311
  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%"
3312
+ display: "flex",
3313
+ flexDirection: "column",
3314
+ gap: "10px",
3315
+ marginLeft: "32px",
3316
+ padding: "12px",
3317
+ borderRadius: "12px",
3318
+ border: "1px solid rgba(245,158,11,0.4)",
3319
+ backgroundColor: "rgba(245,158,11,0.06)"
2620
3320
  },
2621
- children: "How are we doing? Tap a star \u2014 the words are optional."
2622
- }
2623
- )
2624
- ] }),
2625
- /* @__PURE__ */ jsxs3(
2626
- "div",
2627
- {
2628
- style: {
2629
- display: "flex",
2630
- flexDirection: "column",
2631
- gap: "10px",
2632
- marginLeft: "32px",
2633
- padding: "12px",
2634
- borderRadius: "12px",
2635
- border: "1px solid rgba(245,158,11,0.4)",
2636
- backgroundColor: "rgba(245,158,11,0.06)"
2637
- },
2638
- children: [
2639
- /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
2640
- const filled = star <= (hoveredStar || rating);
2641
- return /* @__PURE__ */ jsx3(
3321
+ children: [
3322
+ /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
3323
+ const filled = star <= (hoveredStar || rating);
3324
+ return /* @__PURE__ */ jsx3(
3325
+ "button",
3326
+ {
3327
+ type: "button",
3328
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
3329
+ "aria-pressed": star <= rating,
3330
+ onClick: () => onRatingChange(star),
3331
+ onMouseEnter: () => setHoveredStar(star),
3332
+ onMouseLeave: () => setHoveredStar(0),
3333
+ style: {
3334
+ border: "none",
3335
+ background: "transparent",
3336
+ padding: 0,
3337
+ cursor: "pointer",
3338
+ lineHeight: 0
3339
+ },
3340
+ children: /* @__PURE__ */ jsx3("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
3341
+ "path",
3342
+ {
3343
+ 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",
3344
+ fill: filled ? "#f59e0b" : "transparent",
3345
+ stroke: filled ? "#f59e0b" : t.inputBorder,
3346
+ strokeWidth: "1.5",
3347
+ strokeLinejoin: "round"
3348
+ }
3349
+ ) })
3350
+ },
3351
+ star
3352
+ );
3353
+ }) }),
3354
+ /* @__PURE__ */ jsx3(
3355
+ "textarea",
3356
+ {
3357
+ value: description,
3358
+ onChange: (e) => onDescriptionChange(e.target.value),
3359
+ rows: 3,
3360
+ placeholder: "What made it good \u2014 or what let you down? (optional)",
3361
+ style: {
3362
+ width: "100%",
3363
+ padding: "10px",
3364
+ borderRadius: "8px",
3365
+ border: `1px solid ${t.inputBorder}`,
3366
+ backgroundColor: t.inputBg,
3367
+ color: t.text,
3368
+ fontSize: "13px",
3369
+ lineHeight: 1.5,
3370
+ fontFamily: "inherit",
3371
+ resize: "vertical",
3372
+ outline: "none",
3373
+ boxSizing: "border-box"
3374
+ }
3375
+ }
3376
+ ),
3377
+ /* @__PURE__ */ jsx3(
2642
3378
  "button",
2643
3379
  {
2644
3380
  type: "button",
2645
- "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2646
- "aria-pressed": star <= rating,
2647
- onClick: () => onRatingChange(star),
2648
- onMouseEnter: () => setHoveredStar(star),
2649
- onMouseLeave: () => setHoveredStar(0),
3381
+ onClick: () => setPhase("type"),
2650
3382
  style: {
3383
+ alignSelf: "flex-start",
2651
3384
  border: "none",
2652
3385
  background: "transparent",
2653
- padding: 0,
3386
+ color: t.textMuted,
3387
+ fontSize: "12px",
3388
+ fontFamily: "inherit",
3389
+ // A bare text link is ~14px tall — half a thumb.
3390
+ padding: "9px 0",
2654
3391
  cursor: "pointer",
2655
- lineHeight: 0
3392
+ textDecoration: "underline"
2656
3393
  },
2657
- children: /* @__PURE__ */ jsx3("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
2658
- "path",
2659
- {
2660
- 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",
2661
- fill: filled ? "#f59e0b" : "transparent",
2662
- stroke: filled ? "#f59e0b" : t.inputBorder,
2663
- strokeWidth: "1.5",
2664
- strokeLinejoin: "round"
2665
- }
2666
- ) })
2667
- },
2668
- star
2669
- );
2670
- }) }),
2671
- /* @__PURE__ */ jsx3(
2672
- "textarea",
2673
- {
2674
- value: description,
2675
- onChange: (e) => onDescriptionChange(e.target.value),
2676
- rows: 3,
2677
- placeholder: "What made it good \u2014 or what let you down? (optional)",
2678
- style: {
2679
- width: "100%",
2680
- padding: "10px",
2681
- borderRadius: "8px",
2682
- border: `1px solid ${t.inputBorder}`,
2683
- backgroundColor: t.inputBg,
2684
- color: t.text,
2685
- fontSize: "13px",
2686
- lineHeight: 1.5,
2687
- fontFamily: "inherit",
2688
- resize: "vertical",
2689
- outline: "none",
2690
- boxSizing: "border-box"
3394
+ children: "Report something instead"
2691
3395
  }
2692
- }
2693
- ),
2694
- /* @__PURE__ */ jsx3(
2695
- "button",
2696
- {
2697
- type: "button",
2698
- onClick: () => setPhase("type"),
2699
- style: {
2700
- alignSelf: "flex-start",
2701
- border: "none",
2702
- background: "transparent",
2703
- color: t.textMuted,
2704
- fontSize: "12px",
2705
- fontFamily: "inherit",
2706
- // A bare text link is ~14px tall — half a thumb.
2707
- padding: "9px 0",
2708
- cursor: "pointer",
2709
- textDecoration: "underline"
2710
- },
2711
- children: "Report something instead"
2712
- }
2713
- )
2714
- ]
2715
- }
2716
- )
2717
- ] }),
2718
- empty && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2719
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2720
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2721
- /* @__PURE__ */ jsxs3(
3396
+ )
3397
+ ]
3398
+ }
3399
+ )
3400
+ ] }),
3401
+ empty && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3402
+ resumable && /* @__PURE__ */ jsxs3(
2722
3403
  "div",
2723
3404
  {
3405
+ "data-gg-resume-chat": "",
2724
3406
  style: {
2725
- padding: "9px 12px",
2726
- borderRadius: "12px 12px 12px 4px",
3407
+ display: "flex",
3408
+ flexDirection: "column",
3409
+ gap: "8px",
3410
+ padding: "10px 12px",
3411
+ borderRadius: "10px",
3412
+ border: `1px solid ${t.accent}55`,
2727
3413
  backgroundColor: t.bgSecondary,
2728
- fontSize: "13px",
2729
- lineHeight: 1.55,
2730
- maxWidth: "88%"
3414
+ minWidth: 0
2731
3415
  },
2732
3416
  children: [
2733
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
2734
- "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
2735
- ]
2736
- }
2737
- )
2738
- ] }),
2739
- /* @__PURE__ */ jsx3(
2740
- "div",
2741
- {
2742
- style: {
2743
- display: "flex",
2744
- flexWrap: "wrap",
2745
- gap: "6px",
2746
- paddingLeft: "32px"
2747
- },
2748
- children: STARTERS.map((s) => /* @__PURE__ */ jsx3(
2749
- "button",
2750
- {
2751
- type: "button",
2752
- "data-gg-chip": "",
2753
- onClick: () => {
2754
- logEvent("starter_tap", s);
2755
- send(s);
2756
- },
2757
- style: {
2758
- border: `1px solid ${t.inputBorder}`,
2759
- background: "transparent",
2760
- color: t.textMuted,
2761
- borderRadius: "999px",
2762
- padding: "6px 11px",
2763
- fontSize: "12px",
2764
- fontFamily: "inherit",
2765
- cursor: "pointer"
2766
- },
2767
- children: s
2768
- },
2769
- s
2770
- ))
2771
- }
2772
- )
2773
- ] }),
2774
- messages.map((m, i) => {
2775
- const mine = m.role === "user";
2776
- if (m.kind === "report") {
2777
- const live = phase === "draft" && i === messages.length - 1;
2778
- return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
2779
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2780
- /* @__PURE__ */ jsx3(
2781
- "div",
2782
- {
2783
- "data-gg-draft-bubble": "",
2784
- style: {
2785
- maxWidth: "82%",
2786
- minWidth: 0,
2787
- padding: "9px 12px",
2788
- borderRadius: "12px 12px 12px 4px",
2789
- fontSize: "13px",
2790
- lineHeight: 1.55,
2791
- backgroundColor: t.bgSecondary,
2792
- color: t.text,
2793
- border: live ? "none" : `1px solid ${t.accent}66`
2794
- },
2795
- children: live ? (
2796
- // #402: "have a read" beside a countdown reads as a
2797
- // contradiction. Say what is about to happen instead.
2798
- 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."
2799
- ) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
3417
+ /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3418
+ "Pick up where you left off \xB7 ",
3419
+ chatAge(resumable.updatedAt)
3420
+ ] }),
3421
+ /* @__PURE__ */ jsx3(
3422
+ "span",
3423
+ {
3424
+ style: {
3425
+ fontSize: "13px",
3426
+ lineHeight: 1.45,
3427
+ overflowWrap: "anywhere"
3428
+ },
3429
+ children: resumable.title
3430
+ }
3431
+ ),
3432
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: [
2800
3433
  /* @__PURE__ */ jsx3(
2801
- "span",
3434
+ "button",
2802
3435
  {
3436
+ type: "button",
3437
+ "data-gg-resume-continue": "",
3438
+ onClick: () => resumeChat(resumable),
2803
3439
  style: {
2804
- display: "block",
2805
- fontSize: "10px",
2806
- fontWeight: 700,
2807
- letterSpacing: ".06em",
2808
- textTransform: "uppercase",
2809
- color: t.accent,
2810
- marginBottom: "4px"
3440
+ border: "none",
3441
+ borderRadius: "999px",
3442
+ backgroundColor: t.accent,
3443
+ color: t.accentText,
3444
+ padding: "7px 13px",
3445
+ fontSize: "12px",
3446
+ fontWeight: 600,
3447
+ fontFamily: "inherit",
3448
+ cursor: "pointer"
2811
3449
  },
2812
- children: "Draft so far"
3450
+ children: "Continue this chat"
2813
3451
  }
2814
3452
  ),
2815
3453
  /* @__PURE__ */ jsx3(
2816
- "span",
3454
+ "button",
2817
3455
  {
3456
+ type: "button",
3457
+ onClick: () => setShowChats(true),
2818
3458
  style: {
2819
- display: "block",
2820
- whiteSpace: "pre-wrap",
2821
- wordBreak: "break-word",
2822
- maxHeight: "160px",
2823
- overflowY: "auto",
2824
- color: t.textMuted
3459
+ border: `1px solid ${t.inputBorder}`,
3460
+ borderRadius: "999px",
3461
+ background: "transparent",
3462
+ color: t.textMuted,
3463
+ padding: "7px 13px",
3464
+ fontSize: "12px",
3465
+ fontFamily: "inherit",
3466
+ cursor: "pointer"
2825
3467
  },
2826
- children: m.content
3468
+ children: "All chats"
2827
3469
  }
2828
3470
  )
2829
3471
  ] })
3472
+ ]
3473
+ }
3474
+ ),
3475
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3476
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3477
+ /* @__PURE__ */ jsxs3(
3478
+ "div",
3479
+ {
3480
+ style: {
3481
+ padding: "9px 12px",
3482
+ borderRadius: "12px 12px 12px 4px",
3483
+ backgroundColor: t.bgSecondary,
3484
+ fontSize: "13px",
3485
+ lineHeight: 1.55,
3486
+ maxWidth: "88%"
3487
+ },
3488
+ children: [
3489
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
3490
+ "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
3491
+ ]
2830
3492
  }
2831
3493
  )
2832
- ] }, i);
2833
- }
2834
- return /* @__PURE__ */ jsxs3(
2835
- "div",
2836
- {
2837
- style: {
2838
- display: "flex",
2839
- gap: "8px",
2840
- minWidth: 0,
2841
- flexDirection: mine ? "row-reverse" : "row"
2842
- },
2843
- children: [
2844
- /* @__PURE__ */ jsx3(Avatar, { who: m.role, theme: t, initials: initials2 }),
2845
- /* @__PURE__ */ jsx3(
2846
- "div",
2847
- {
2848
- style: {
2849
- maxWidth: "82%",
2850
- minWidth: 0,
2851
- padding: "9px 12px",
2852
- borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
2853
- fontSize: "13px",
2854
- lineHeight: 1.55,
2855
- // Markdown owns the assistant's line breaks and lists.
2856
- whiteSpace: mine ? "pre-wrap" : "normal",
2857
- wordBreak: "break-word",
2858
- backgroundColor: mine ? t.accent : t.bgSecondary,
2859
- color: mine ? t.accentText : t.text
2860
- },
2861
- children: mine ? m.content : /* @__PURE__ */ jsx3(ChatMarkdown, { text: m.content, theme: t })
2862
- }
2863
- )
2864
- ]
2865
- },
2866
- i
2867
- );
2868
- }),
2869
- duplicate && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
2870
- glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ jsxs3(
2871
- "div",
2872
- {
2873
- "data-gg-glitchgrab-offer": "",
2874
- style: {
2875
- display: "flex",
2876
- flexDirection: "column",
2877
- gap: "8px",
2878
- padding: "10px",
2879
- borderRadius: "8px",
2880
- border: `1px solid ${t.accent}55`,
2881
- backgroundColor: `${t.accent}14`,
2882
- fontSize: "12px",
2883
- lineHeight: 1.5
2884
- },
2885
- children: [
2886
- /* @__PURE__ */ jsx3("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
2887
- /* @__PURE__ */ jsx3("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
2888
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
2889
- /* @__PURE__ */ jsx3(
3494
+ ] }),
3495
+ /* @__PURE__ */ jsx3(
3496
+ "div",
3497
+ {
3498
+ style: {
3499
+ display: "flex",
3500
+ flexWrap: "wrap",
3501
+ gap: "6px",
3502
+ paddingLeft: "32px"
3503
+ },
3504
+ children: STARTERS.map((s) => /* @__PURE__ */ jsx3(
2890
3505
  "button",
2891
3506
  {
2892
3507
  type: "button",
2893
- "data-gg-glitchgrab-offer-send": "",
3508
+ "data-gg-chip": "",
2894
3509
  onClick: () => {
2895
- logEvent("glitchgrab_offer_tap");
2896
- setGlitchgrabOffer(false);
2897
- onReportGlitchgrabProblem(
2898
- messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
2899
- );
3510
+ logEvent("starter_tap", s);
3511
+ send(s);
2900
3512
  },
2901
3513
  style: {
2902
- padding: "8px 12px",
3514
+ border: `1px solid ${t.inputBorder}`,
3515
+ background: "transparent",
3516
+ color: t.textMuted,
2903
3517
  borderRadius: "999px",
2904
- border: "none",
2905
- backgroundColor: t.accent,
2906
- color: t.accentText,
3518
+ padding: "6px 11px",
2907
3519
  fontSize: "12px",
2908
- fontWeight: 700,
2909
3520
  fontFamily: "inherit",
2910
- cursor: "pointer",
2911
- // Both stretch, so on a phone each wrapped line reads as
2912
- // a choice rather than one button left stranded.
2913
- flex: "1 1 auto"
3521
+ cursor: "pointer"
2914
3522
  },
2915
- children: "Send to the Glitchgrab team"
2916
- }
2917
- ),
3523
+ children: s
3524
+ },
3525
+ s
3526
+ ))
3527
+ }
3528
+ )
3529
+ ] }),
3530
+ messages.map((m, i) => {
3531
+ const mine = m.role === "user";
3532
+ if (m.kind === "report") {
3533
+ const live = phase === "draft" && i === messages.length - 1;
3534
+ return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
3535
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2918
3536
  /* @__PURE__ */ jsx3(
2919
- "button",
3537
+ "div",
2920
3538
  {
2921
- type: "button",
2922
- "data-gg-glitchgrab-offer-no": "",
2923
- onClick: () => {
2924
- logEvent("glitchgrab_offer_declined");
2925
- send("No \u2014 it's about this app, not the reporting tool");
2926
- },
3539
+ "data-gg-draft-bubble": "",
2927
3540
  style: {
2928
- padding: "8px 12px",
2929
- borderRadius: "999px",
2930
- border: `1px solid ${t.inputBorder}`,
2931
- background: "transparent",
2932
- color: t.textMuted,
2933
- fontSize: "12px",
2934
- fontFamily: "inherit",
2935
- cursor: "pointer",
2936
- flex: "1 1 auto"
3541
+ maxWidth: "82%",
3542
+ minWidth: 0,
3543
+ padding: "9px 12px",
3544
+ borderRadius: "12px 12px 12px 4px",
3545
+ fontSize: "13px",
3546
+ lineHeight: 1.55,
3547
+ backgroundColor: t.bgSecondary,
3548
+ color: t.text,
3549
+ border: live ? "none" : `1px solid ${t.accent}66`
2937
3550
  },
2938
- children: "No, it's about this app"
3551
+ children: live ? (
3552
+ // #402: "have a read" beside a countdown reads as a
3553
+ // contradiction. Say what is about to happen instead.
3554
+ 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."
3555
+ ) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
3556
+ /* @__PURE__ */ jsx3(
3557
+ "span",
3558
+ {
3559
+ style: {
3560
+ display: "block",
3561
+ fontSize: "10px",
3562
+ fontWeight: 700,
3563
+ letterSpacing: ".06em",
3564
+ textTransform: "uppercase",
3565
+ color: t.accent,
3566
+ marginBottom: "4px"
3567
+ },
3568
+ children: "Draft so far"
3569
+ }
3570
+ ),
3571
+ /* @__PURE__ */ jsx3(
3572
+ "span",
3573
+ {
3574
+ style: {
3575
+ display: "block",
3576
+ whiteSpace: "pre-wrap",
3577
+ wordBreak: "break-word",
3578
+ maxHeight: "160px",
3579
+ overflowY: "auto",
3580
+ color: t.textMuted
3581
+ },
3582
+ children: m.content
3583
+ }
3584
+ )
3585
+ ] })
2939
3586
  }
2940
3587
  )
2941
- ] })
2942
- ]
2943
- }
2944
- ) }),
2945
- options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ jsx3(
2946
- "div",
2947
- {
2948
- style: {
2949
- display: "flex",
2950
- // Long labels wrapped into a ragged row that always stranded
2951
- // the last chip on a line of its own. Past ~24 characters
2952
- // these stop being chips and become a choice list, so they
2953
- // stack full width and read like one.
2954
- flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
2955
- alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
2956
- flexWrap: "wrap",
2957
- gap: "6px",
2958
- paddingLeft: "32px"
2959
- },
2960
- children: options.map((option) => /* @__PURE__ */ jsx3(
2961
- "button",
3588
+ ] }, i);
3589
+ }
3590
+ return /* @__PURE__ */ jsxs3(
3591
+ "div",
2962
3592
  {
2963
- type: "button",
2964
- "data-gg-chip": "",
2965
- "data-gg-option": "",
2966
- onClick: () => {
2967
- logEvent("option_tap", option);
2968
- send(option, option === CONFIRM_OPTION);
2969
- },
2970
3593
  style: {
2971
- border: `1px solid ${t.inputBorder}`,
2972
- background: "transparent",
2973
- color: t.textMuted,
2974
- borderRadius: "999px",
2975
- // Same 8px as the picker chips — one tap target size in
2976
- // this sheet, not two.
2977
- padding: "8px 12px",
2978
- fontSize: "12px",
2979
- fontFamily: "inherit",
2980
- cursor: "pointer",
2981
- textAlign: "left"
3594
+ display: "flex",
3595
+ gap: "8px",
3596
+ minWidth: 0,
3597
+ flexDirection: mine ? "row-reverse" : "row"
2982
3598
  },
2983
- children: option
3599
+ children: [
3600
+ /* @__PURE__ */ jsx3(Avatar, { who: m.role, theme: t, initials: initials2 }),
3601
+ /* @__PURE__ */ jsx3(
3602
+ "div",
3603
+ {
3604
+ style: {
3605
+ maxWidth: "82%",
3606
+ minWidth: 0,
3607
+ padding: "9px 12px",
3608
+ borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
3609
+ fontSize: "13px",
3610
+ lineHeight: 1.55,
3611
+ // Markdown owns the assistant's line breaks and lists.
3612
+ whiteSpace: mine ? "pre-wrap" : "normal",
3613
+ wordBreak: "break-word",
3614
+ backgroundColor: mine ? t.accent : t.bgSecondary,
3615
+ color: mine ? t.accentText : t.text
3616
+ },
3617
+ children: mine ? m.content : /* @__PURE__ */ jsx3(ChatMarkdown, { text: m.content, theme: t })
3618
+ }
3619
+ )
3620
+ ]
2984
3621
  },
2985
- option
2986
- ))
2987
- }
2988
- ),
2989
- busy && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, role: "status", "aria-label": "Thinking", children: [
2990
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2991
- /* @__PURE__ */ jsx3(
2992
- "span",
3622
+ i
3623
+ );
3624
+ }),
3625
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
3626
+ glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ jsxs3(
3627
+ "div",
2993
3628
  {
3629
+ "data-gg-glitchgrab-offer": "",
2994
3630
  style: {
2995
- display: "inline-flex",
2996
- alignItems: "center",
2997
- gap: "4px",
2998
- padding: "12px",
2999
- borderRadius: "12px 12px 12px 4px",
3000
- backgroundColor: t.bgSecondary
3631
+ display: "flex",
3632
+ flexDirection: "column",
3633
+ gap: "8px",
3634
+ padding: "10px",
3635
+ borderRadius: "8px",
3636
+ border: `1px solid ${t.accent}55`,
3637
+ backgroundColor: `${t.accent}14`,
3638
+ fontSize: "12px",
3639
+ lineHeight: 1.5
3001
3640
  },
3002
- children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx3(
3003
- "span",
3004
- {
3005
- style: {
3006
- width: "5px",
3007
- height: "5px",
3008
- borderRadius: "50%",
3009
- backgroundColor: t.textMuted,
3010
- animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3011
- }
3012
- },
3013
- i
3014
- ))
3641
+ children: [
3642
+ /* @__PURE__ */ jsx3("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
3643
+ /* @__PURE__ */ jsx3("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
3644
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
3645
+ /* @__PURE__ */ jsx3(
3646
+ "button",
3647
+ {
3648
+ type: "button",
3649
+ "data-gg-glitchgrab-offer-send": "",
3650
+ onClick: () => {
3651
+ logEvent("glitchgrab_offer_tap");
3652
+ setGlitchgrabOffer(false);
3653
+ onReportGlitchgrabProblem(
3654
+ messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
3655
+ );
3656
+ },
3657
+ style: {
3658
+ padding: "8px 12px",
3659
+ borderRadius: "999px",
3660
+ border: "none",
3661
+ backgroundColor: t.accent,
3662
+ color: t.accentText,
3663
+ fontSize: "12px",
3664
+ fontWeight: 700,
3665
+ fontFamily: "inherit",
3666
+ cursor: "pointer",
3667
+ // Both stretch, so on a phone each wrapped line reads as
3668
+ // a choice rather than one button left stranded.
3669
+ flex: "1 1 auto"
3670
+ },
3671
+ children: "Send to the Glitchgrab team"
3672
+ }
3673
+ ),
3674
+ /* @__PURE__ */ jsx3(
3675
+ "button",
3676
+ {
3677
+ type: "button",
3678
+ "data-gg-glitchgrab-offer-no": "",
3679
+ onClick: () => {
3680
+ logEvent("glitchgrab_offer_declined");
3681
+ send("No \u2014 it's about this app, not the reporting tool");
3682
+ },
3683
+ style: {
3684
+ padding: "8px 12px",
3685
+ borderRadius: "999px",
3686
+ border: `1px solid ${t.inputBorder}`,
3687
+ background: "transparent",
3688
+ color: t.textMuted,
3689
+ fontSize: "12px",
3690
+ fontFamily: "inherit",
3691
+ cursor: "pointer",
3692
+ flex: "1 1 auto"
3693
+ },
3694
+ children: "No, it's about this app"
3695
+ }
3696
+ )
3697
+ ] })
3698
+ ]
3015
3699
  }
3016
- )
3017
- ] }),
3018
- phase === "draft" && !submitted && /* @__PURE__ */ jsxs3(
3019
- "div",
3020
- {
3021
- style: {
3022
- border: `1px solid ${t.accent}`,
3023
- borderRadius: "12px",
3024
- backgroundColor: t.bgSecondary,
3025
- padding: "12px",
3026
- display: "flex",
3027
- flexDirection: "column",
3028
- gap: "10px",
3029
- minWidth: 0,
3030
- boxSizing: "border-box"
3031
- },
3032
- children: [
3033
- /* @__PURE__ */ jsx3(
3034
- "span",
3035
- {
3036
- style: {
3037
- fontSize: "10px",
3038
- fontWeight: 700,
3039
- letterSpacing: ".06em",
3040
- textTransform: "uppercase",
3041
- color: t.accent
3042
- },
3043
- children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3044
- }
3045
- ),
3046
- renderKnownIssue({
3047
- title: true,
3048
- footer: "Your details will be added to it instead of opening a new issue."
3049
- }),
3050
- !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3051
- // box edge is the very thing #543 is about. One line of text,
3052
- // so Enter is swallowed.
3053
- /* @__PURE__ */ jsx3(
3054
- "textarea",
3055
- {
3056
- "aria-label": "Title",
3057
- value: issueTitle,
3058
- maxLength: 120,
3059
- rows: 2,
3060
- onKeyDown: (e) => {
3061
- if (e.key === "Enter") e.preventDefault();
3062
- },
3063
- onChange: (e) => {
3064
- stopAutoFile("edited the title");
3065
- onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3066
- },
3067
- style: {
3068
- width: "100%",
3069
- padding: "8px 10px",
3070
- lineHeight: 1.45,
3071
- resize: "none",
3072
- borderRadius: "8px",
3073
- border: `1px solid ${t.inputBorder}`,
3074
- backgroundColor: t.inputBg,
3075
- color: t.text,
3076
- fontSize: "13px",
3077
- fontWeight: 600,
3078
- fontFamily: "inherit",
3079
- outline: "none",
3080
- boxSizing: "border-box"
3081
- }
3082
- }
3083
- ),
3084
- /* @__PURE__ */ jsx3(
3085
- "textarea",
3700
+ ) }),
3701
+ options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ jsx3(
3702
+ "div",
3703
+ {
3704
+ style: {
3705
+ display: "flex",
3706
+ // Long labels wrapped into a ragged row that always stranded
3707
+ // the last chip on a line of its own. Past ~24 characters
3708
+ // these stop being chips and become a choice list, so they
3709
+ // stack full width and read like one.
3710
+ flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
3711
+ alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
3712
+ flexWrap: "wrap",
3713
+ gap: "6px",
3714
+ paddingLeft: "32px"
3715
+ },
3716
+ children: options.map((option) => /* @__PURE__ */ jsx3(
3717
+ "button",
3086
3718
  {
3087
- ref: draftRef,
3088
- value: description,
3089
- onChange: (e) => {
3090
- stopAutoFile("edited the draft");
3091
- onDescriptionChange(e.target.value);
3719
+ type: "button",
3720
+ "data-gg-chip": "",
3721
+ "data-gg-option": "",
3722
+ onClick: () => {
3723
+ logEvent("option_tap", option);
3724
+ send(option, option === CONFIRM_OPTION);
3092
3725
  },
3093
- rows: 6,
3094
3726
  style: {
3095
- width: "100%",
3096
- minHeight: "120px",
3097
- padding: "10px",
3098
- borderRadius: "8px",
3099
3727
  border: `1px solid ${t.inputBorder}`,
3100
- backgroundColor: t.inputBg,
3101
- color: t.text,
3102
- fontSize: "13px",
3103
- lineHeight: 1.55,
3728
+ background: "transparent",
3729
+ color: t.textMuted,
3730
+ borderRadius: "999px",
3731
+ // Same 8px as the picker chips — one tap target size in
3732
+ // this sheet, not two.
3733
+ padding: "8px 12px",
3734
+ fontSize: "12px",
3104
3735
  fontFamily: "inherit",
3105
- resize: "vertical",
3106
- outline: "none",
3107
- boxSizing: "border-box"
3108
- }
3109
- }
3110
- ),
3111
- severityAsked && autoFileLeft === null && /* @__PURE__ */ jsxs3("div", { children: [
3112
- /* @__PURE__ */ jsxs3(
3736
+ cursor: "pointer",
3737
+ textAlign: "left"
3738
+ },
3739
+ children: option
3740
+ },
3741
+ option
3742
+ ))
3743
+ }
3744
+ ),
3745
+ busy && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, role: "status", "aria-label": "Thinking", children: [
3746
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3747
+ /* @__PURE__ */ jsxs3(
3748
+ "span",
3749
+ {
3750
+ style: {
3751
+ display: "inline-flex",
3752
+ alignItems: "center",
3753
+ // The "still working" line must wrap on a phone, not push
3754
+ // the bubble past the sheet's edge.
3755
+ flexWrap: "wrap",
3756
+ minWidth: 0,
3757
+ gap: "4px",
3758
+ padding: "12px",
3759
+ borderRadius: "12px 12px 12px 4px",
3760
+ backgroundColor: t.bgSecondary
3761
+ },
3762
+ children: [
3763
+ [0, 1, 2].map((i) => /* @__PURE__ */ jsx3(
3764
+ "span",
3765
+ {
3766
+ style: {
3767
+ width: "5px",
3768
+ height: "5px",
3769
+ borderRadius: "50%",
3770
+ backgroundColor: t.textMuted,
3771
+ animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3772
+ }
3773
+ },
3774
+ i
3775
+ )),
3776
+ /* @__PURE__ */ jsx3(ThinkingClock, { color: t.textMuted })
3777
+ ]
3778
+ }
3779
+ )
3780
+ ] }),
3781
+ phase === "draft" && !submitted && /* @__PURE__ */ jsxs3(
3782
+ "div",
3783
+ {
3784
+ style: {
3785
+ border: `1px solid ${t.accent}`,
3786
+ borderRadius: "12px",
3787
+ backgroundColor: t.bgSecondary,
3788
+ padding: "12px",
3789
+ display: "flex",
3790
+ flexDirection: "column",
3791
+ gap: "10px",
3792
+ minWidth: 0,
3793
+ boxSizing: "border-box"
3794
+ },
3795
+ children: [
3796
+ /* @__PURE__ */ jsx3(
3113
3797
  "span",
3114
3798
  {
3115
3799
  style: {
3116
- fontSize: "11.5px",
3117
- color: t.textMuted,
3118
- display: "block",
3119
- marginBottom: "6px"
3800
+ fontSize: "10px",
3801
+ fontWeight: 700,
3802
+ letterSpacing: ".06em",
3803
+ textTransform: "uppercase",
3804
+ color: t.accent
3120
3805
  },
3121
- children: [
3122
- "How bad is it?",
3123
- !severity && /* @__PURE__ */ jsx3(
3124
- "span",
3125
- {
3126
- style: {
3127
- marginLeft: "6px",
3128
- fontSize: "11px",
3129
- color: severityRefused ? "#ef4444" : t.textMuted
3130
- },
3131
- children: "\xB7 required"
3132
- }
3133
- )
3134
- ]
3806
+ children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3135
3807
  }
3136
3808
  ),
3809
+ renderKnownIssue({
3810
+ title: true,
3811
+ footer: "Your details will be added to it instead of opening a new issue."
3812
+ }),
3813
+ !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3814
+ // box edge is the very thing #543 is about. One line of text,
3815
+ // so Enter is swallowed.
3137
3816
  /* @__PURE__ */ jsx3(
3138
- "div",
3817
+ "textarea",
3139
3818
  {
3140
- role: "radiogroup",
3141
- "aria-label": "Severity",
3142
- style: { display: "flex", gap: "6px" },
3143
- children: SEVERITY_LEVELS.map((s) => {
3144
- const on = severity === s;
3145
- return /* @__PURE__ */ jsx3(
3146
- "button",
3147
- {
3148
- type: "button",
3149
- onClick: () => onSeverityChange(s),
3150
- role: "radio",
3151
- "aria-checked": on,
3152
- style: {
3153
- flex: 1,
3154
- minWidth: 0,
3155
- padding: "8px 2px",
3156
- borderRadius: "8px",
3157
- border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3158
- backgroundColor: on ? t.accent : "transparent",
3159
- color: on ? t.accentText : t.textMuted,
3160
- fontSize: "12px",
3161
- fontWeight: 600,
3162
- fontFamily: "inherit",
3163
- whiteSpace: "nowrap",
3164
- overflow: "hidden",
3165
- textOverflow: "ellipsis",
3166
- cursor: "pointer"
3167
- },
3168
- children: SEVERITY_LABELS[s]
3169
- },
3170
- s
3171
- );
3172
- })
3819
+ "aria-label": "Title",
3820
+ value: issueTitle,
3821
+ maxLength: 120,
3822
+ rows: 2,
3823
+ onKeyDown: (e) => {
3824
+ if (e.key === "Enter") e.preventDefault();
3825
+ },
3826
+ onChange: (e) => {
3827
+ stopAutoFile("edited the title");
3828
+ onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3829
+ },
3830
+ style: {
3831
+ width: "100%",
3832
+ padding: "8px 10px",
3833
+ lineHeight: 1.45,
3834
+ resize: "none",
3835
+ borderRadius: "8px",
3836
+ border: `1px solid ${t.inputBorder}`,
3837
+ backgroundColor: t.inputBg,
3838
+ color: t.text,
3839
+ fontSize: "13px",
3840
+ fontWeight: 600,
3841
+ fontFamily: "inherit",
3842
+ outline: "none",
3843
+ boxSizing: "border-box"
3844
+ }
3173
3845
  }
3174
- )
3175
- ] }),
3176
- attachmentCount > 0 && /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3177
- attachmentCount,
3178
- " ",
3179
- attachmentCount === 1 ? "attachment" : "attachments",
3180
- " will be sent with this."
3181
- ] })
3182
- ]
3183
- }
3184
- ),
3185
- phase === "solved" && solved && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3186
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3187
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3846
+ ),
3847
+ /* @__PURE__ */ jsx3(
3848
+ "textarea",
3849
+ {
3850
+ ref: draftRef,
3851
+ value: description,
3852
+ onChange: (e) => {
3853
+ stopAutoFile("edited the draft");
3854
+ onDescriptionChange(e.target.value);
3855
+ },
3856
+ rows: 6,
3857
+ style: {
3858
+ width: "100%",
3859
+ minHeight: "120px",
3860
+ padding: "10px",
3861
+ borderRadius: "8px",
3862
+ border: `1px solid ${t.inputBorder}`,
3863
+ backgroundColor: t.inputBg,
3864
+ color: t.text,
3865
+ fontSize: "13px",
3866
+ lineHeight: 1.55,
3867
+ fontFamily: "inherit",
3868
+ resize: "vertical",
3869
+ outline: "none",
3870
+ boxSizing: "border-box"
3871
+ }
3872
+ }
3873
+ ),
3874
+ severityAsked && autoFileLeft === null && /* @__PURE__ */ jsxs3("div", { children: [
3875
+ /* @__PURE__ */ jsxs3(
3876
+ "span",
3877
+ {
3878
+ style: {
3879
+ fontSize: "11.5px",
3880
+ color: t.textMuted,
3881
+ display: "block",
3882
+ marginBottom: "6px"
3883
+ },
3884
+ children: [
3885
+ "How bad is it?",
3886
+ !severity && /* @__PURE__ */ jsx3(
3887
+ "span",
3888
+ {
3889
+ style: {
3890
+ marginLeft: "6px",
3891
+ fontSize: "11px",
3892
+ color: severityRefused ? "#ef4444" : t.textMuted
3893
+ },
3894
+ children: "\xB7 required"
3895
+ }
3896
+ )
3897
+ ]
3898
+ }
3899
+ ),
3900
+ /* @__PURE__ */ jsx3(
3901
+ "div",
3902
+ {
3903
+ role: "radiogroup",
3904
+ "aria-label": "Severity",
3905
+ style: { display: "flex", gap: "6px" },
3906
+ children: SEVERITY_LEVELS.map((s) => {
3907
+ const on = severity === s;
3908
+ return /* @__PURE__ */ jsx3(
3909
+ "button",
3910
+ {
3911
+ type: "button",
3912
+ onClick: () => onSeverityChange(s),
3913
+ role: "radio",
3914
+ "aria-checked": on,
3915
+ style: {
3916
+ flex: 1,
3917
+ minWidth: 0,
3918
+ padding: "8px 2px",
3919
+ borderRadius: "8px",
3920
+ border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3921
+ backgroundColor: on ? t.accent : "transparent",
3922
+ color: on ? t.accentText : t.textMuted,
3923
+ fontSize: "12px",
3924
+ fontWeight: 600,
3925
+ fontFamily: "inherit",
3926
+ whiteSpace: "nowrap",
3927
+ overflow: "hidden",
3928
+ textOverflow: "ellipsis",
3929
+ cursor: "pointer"
3930
+ },
3931
+ children: SEVERITY_LABELS[s]
3932
+ },
3933
+ s
3934
+ );
3935
+ })
3936
+ }
3937
+ )
3938
+ ] }),
3939
+ attachmentCount > 0 && /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3940
+ attachmentCount,
3941
+ " ",
3942
+ attachmentCount === 1 ? "attachment" : "attachments",
3943
+ " will be sent with this."
3944
+ ] })
3945
+ ]
3946
+ }
3947
+ ),
3948
+ phase === "solved" && solved && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3949
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3950
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3951
+ /* @__PURE__ */ jsx3(
3952
+ "div",
3953
+ {
3954
+ style: {
3955
+ padding: "9px 12px",
3956
+ borderRadius: "12px 12px 12px 4px",
3957
+ backgroundColor: t.bgSecondary,
3958
+ fontSize: "13px",
3959
+ lineHeight: 1.55,
3960
+ maxWidth: "88%",
3961
+ whiteSpace: "pre-wrap"
3962
+ },
3963
+ children: solved
3964
+ }
3965
+ )
3966
+ ] }),
3188
3967
  /* @__PURE__ */ jsx3(
3189
- "div",
3968
+ "span",
3190
3969
  {
3191
- style: {
3192
- padding: "9px 12px",
3193
- borderRadius: "12px 12px 12px 4px",
3194
- backgroundColor: t.bgSecondary,
3195
- fontSize: "13px",
3196
- lineHeight: 1.55,
3197
- maxWidth: "88%",
3198
- whiteSpace: "pre-wrap"
3199
- },
3200
- children: solved
3970
+ style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
3971
+ children: "Nothing was filed \u2014 you are all set."
3201
3972
  }
3202
3973
  )
3203
3974
  ] }),
3204
- /* @__PURE__ */ jsx3(
3205
- "span",
3975
+ submitted && /* @__PURE__ */ jsx3(
3976
+ "div",
3206
3977
  {
3207
- style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
3208
- children: "Nothing was filed \u2014 you are all set."
3978
+ style: {
3979
+ textAlign: "center",
3980
+ padding: "24px 0",
3981
+ color: t.accent,
3982
+ fontSize: "14px",
3983
+ fontWeight: 600
3984
+ },
3985
+ children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3209
3986
  }
3210
3987
  )
3211
- ] }),
3212
- submitted && /* @__PURE__ */ jsx3(
3213
- "div",
3214
- {
3215
- style: {
3216
- textAlign: "center",
3217
- padding: "24px 0",
3218
- color: t.accent,
3219
- fontSize: "14px",
3220
- fontWeight: 600
3221
- },
3222
- children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3223
- }
3224
- )
3225
- ]
3226
- }
3227
- )
3988
+ ] })
3989
+ }
3990
+ )
3991
+ ]
3228
3992
  }
3229
3993
  ),
3230
- !submitted && phase !== "type" && phase !== "severity" && /* @__PURE__ */ jsxs3(
3994
+ !submitted && !showChats && phase !== "type" && phase !== "severity" && /* @__PURE__ */ jsxs3(
3231
3995
  "div",
3232
3996
  {
3233
3997
  style: {
@@ -3544,8 +4308,8 @@ function AssistSheet({
3544
4308
  accept: canReadPdf ? ASSIST_ATTACHMENT_ACCEPT_WITH_PDF : ASSIST_ATTACHMENT_ACCEPT,
3545
4309
  multiple: true,
3546
4310
  onChange: (e) => {
3547
- var _a;
3548
- const picked = Array.from((_a = e.target.files) != null ? _a : []);
4311
+ var _a2;
4312
+ const picked = Array.from((_a2 = e.target.files) != null ? _a2 : []);
3549
4313
  if (picked.length) {
3550
4314
  stopAutoFile("changed attachments");
3551
4315
  const images = picked.filter((f) => f.type.startsWith("image/")).length;
@@ -4523,20 +5287,20 @@ function initialsOf(name) {
4523
5287
  }).join("");
4524
5288
  }
4525
5289
  function ReporterChip({
4526
- reporter,
5290
+ reporter: reporter2,
4527
5291
  t
4528
5292
  }) {
4529
5293
  var _a;
4530
5294
  const [avatarFailed, setAvatarFailed] = useState4(false);
4531
- const avatarUrl = (reporter == null ? void 0 : reporter.avatarUrl) || "";
5295
+ const avatarUrl = (reporter2 == null ? void 0 : reporter2.avatarUrl) || "";
4532
5296
  useEffect4(() => {
4533
5297
  setAvatarFailed(false);
4534
5298
  }, [avatarUrl]);
4535
- const name = ((_a = reporter == null ? void 0 : reporter.name) == null ? void 0 : _a.trim()) || "";
5299
+ const name = ((_a = reporter2 == null ? void 0 : reporter2.name) == null ? void 0 : _a.trim()) || "";
4536
5300
  const isAnonymous = !name;
4537
5301
  const initials2 = isAnonymous ? "" : initialsOf(name);
4538
5302
  const label = isAnonymous ? "Anonymous" : name;
4539
- 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 ");
5303
+ 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 ");
4540
5304
  const showAvatarImage = !!avatarUrl && !avatarFailed;
4541
5305
  return /* @__PURE__ */ jsxs5(
4542
5306
  "span",
@@ -4600,7 +5364,7 @@ function ReporterChip({
4600
5364
  },
4601
5365
  children: [
4602
5366
  label,
4603
- (reporter == null ? void 0 : reporter.role) ? ` \xB7 ${reporter.role}` : ""
5367
+ (reporter2 == null ? void 0 : reporter2.role) ? ` \xB7 ${reporter2.role}` : ""
4604
5368
  ]
4605
5369
  }
4606
5370
  )
@@ -4717,11 +5481,12 @@ function ReportDialog({
4717
5481
  types,
4718
5482
  showSeverity = true,
4719
5483
  captureScreenshot = captureDefaultScreenshot,
4720
- reporter,
5484
+ reporter: reporter2,
4721
5485
  onClose,
4722
5486
  headerSlot,
4723
5487
  reportGlitchgrabProblem,
4724
- layout = "modal"
5488
+ layout = "modal",
5489
+ chatHistoryKey: chatHistoryKey2 = null
4725
5490
  }) {
4726
5491
  var _a;
4727
5492
  const sheet = layout === "sheet";
@@ -4743,6 +5508,7 @@ function ReportDialog({
4743
5508
  const [aiConversationId, setAiConversationId] = useState4(null);
4744
5509
  const [issueTitle, setIssueTitle] = useState4("");
4745
5510
  const [isEnhanced, setIsEnhanced] = useState4(false);
5511
+ const [enhanceError, setEnhanceError] = useState4(null);
4746
5512
  const [originalDescription, setOriginalDescription] = useState4(
4747
5513
  null
4748
5514
  );
@@ -4826,15 +5592,15 @@ function ReportDialog({
4826
5592
  });
4827
5593
  };
4828
5594
  applyInert();
4829
- const observer = new MutationObserver((mutations) => {
5595
+ const observer2 = new MutationObserver((mutations) => {
4830
5596
  const structural = mutations.some(
4831
5597
  (m) => m.addedNodes.length > 0 || m.removedNodes.length > 0
4832
5598
  );
4833
5599
  if (structural) applyInert();
4834
5600
  });
4835
- observer.observe(document.body, { childList: true, subtree: true });
5601
+ observer2.observe(document.body, { childList: true, subtree: true });
4836
5602
  return () => {
4837
- observer.disconnect();
5603
+ observer2.disconnect();
4838
5604
  inerted.forEach((el) => el.removeAttribute("inert"));
4839
5605
  };
4840
5606
  }, [isOpen]);
@@ -5197,6 +5963,7 @@ function ReportDialog({
5197
5963
  setSelfReportFrom(null);
5198
5964
  setVoiceError(null);
5199
5965
  setIsEnhanced(false);
5966
+ setEnhanceError(null);
5200
5967
  setOriginalDescription(null);
5201
5968
  setAssistOpen(false);
5202
5969
  setAssistUsed(false);
@@ -5262,6 +6029,7 @@ function ReportDialog({
5262
6029
  const sep = base.trim() ? " " : "";
5263
6030
  setDescriptionFromVoice(base + sep + text.trim());
5264
6031
  setValidationError(null);
6032
+ setEnhanceError(null);
5265
6033
  setIsEnhanced(false);
5266
6034
  setOriginalDescription(null);
5267
6035
  }
@@ -5486,6 +6254,9 @@ function ReportDialog({
5486
6254
  setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
5487
6255
  const queued = result.status === "QUEUED" && !!result.message;
5488
6256
  setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
6257
+ if (chatHistoryKey2 && aiConversationId) {
6258
+ markChatFiled(chatHistoryKey2, aiConversationId, `${getTypeLabel(reportType)} sent`);
6259
+ }
5489
6260
  setSubmitted(true);
5490
6261
  setDescription("");
5491
6262
  setScreenshots([]);
@@ -6354,6 +7125,7 @@ function ReportDialog({
6354
7125
  notePushToTalkEdit();
6355
7126
  setDescription(e.target.value);
6356
7127
  if (validationError) setValidationError(null);
7128
+ if (enhanceError) setEnhanceError(null);
6357
7129
  if (isEnhanced) {
6358
7130
  setIsEnhanced(false);
6359
7131
  setOriginalDescription(null);
@@ -6475,6 +7247,7 @@ function ReportDialog({
6475
7247
  var _a2;
6476
7248
  if (!description.trim() || isEnhancing) return;
6477
7249
  setIsEnhancing(true);
7250
+ setEnhanceError(null);
6478
7251
  try {
6479
7252
  const polished = await enhanceText2(
6480
7253
  description,
@@ -6487,6 +7260,10 @@ function ReportDialog({
6487
7260
  if (validationError)
6488
7261
  setValidationError(null);
6489
7262
  }
7263
+ } catch (err) {
7264
+ setEnhanceError(
7265
+ err instanceof Error && err.message ? err.message : "AI enhance isn't available right now \u2014 send your report as written."
7266
+ );
6490
7267
  } finally {
6491
7268
  setIsEnhancing(false);
6492
7269
  }
@@ -6745,6 +7522,20 @@ function ReportDialog({
6745
7522
  children: voiceError
6746
7523
  }
6747
7524
  ),
7525
+ enhanceError && /* @__PURE__ */ jsx5(
7526
+ "p",
7527
+ {
7528
+ role: "alert",
7529
+ style: {
7530
+ color: "#ef4444",
7531
+ fontSize: "11px",
7532
+ lineHeight: 1.5,
7533
+ marginTop: "4px",
7534
+ marginBottom: 0
7535
+ },
7536
+ children: enhanceError
7537
+ }
7538
+ ),
6748
7539
  assistNotice && /* @__PURE__ */ jsx5(
6749
7540
  "p",
6750
7541
  {
@@ -7305,7 +8096,7 @@ function ReportDialog({
7305
8096
  gap: "12px"
7306
8097
  },
7307
8098
  children: [
7308
- /* @__PURE__ */ jsx5(ReporterChip, { reporter, t }),
8099
+ /* @__PURE__ */ jsx5(ReporterChip, { reporter: reporter2, t }),
7309
8100
  reportGlitchgrabProblem ? /* @__PURE__ */ jsx5(
7310
8101
  "button",
7311
8102
  {
@@ -7585,6 +8376,7 @@ function ReportDialog({
7585
8376
  setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7586
8377
  },
7587
8378
  onConversationChange: setAiConversationId,
8379
+ historyKey: chatHistoryKey2,
7588
8380
  onFinish: handleClose,
7589
8381
  rating,
7590
8382
  onRatingChange: (value) => {
@@ -7592,7 +8384,7 @@ function ReportDialog({
7592
8384
  if (validationError) setValidationError(null);
7593
8385
  },
7594
8386
  projectSlot: headerSlot,
7595
- reporterName: (_a = reporter == null ? void 0 : reporter.name) != null ? _a : null,
8387
+ reporterName: (_a = reporter2 == null ? void 0 : reporter2.name) != null ? _a : null,
7596
8388
  description,
7597
8389
  onDescriptionChange: (value) => {
7598
8390
  setDescription(value);
@@ -9179,6 +9971,9 @@ function GlitchgrabProviderInner({
9179
9971
  onError,
9180
9972
  onReportSent
9181
9973
  });
9974
+ if (process.env.NODE_ENV !== "development") {
9975
+ installHydrationCapture(reportHydrationError);
9976
+ }
9182
9977
  useEffect6(() => {
9183
9978
  if (enableBreadcrumbs) {
9184
9979
  initBreadcrumbs(maxBreadcrumbs2, { responseBodyOrigins: responseBodyOrigins2 });
@@ -9234,7 +10029,7 @@ function GlitchgrabProviderInner({
9234
10029
  return matchesIgnorePatterns(message, ignoreErrors);
9235
10030
  };
9236
10031
  const handleError = (event) => {
9237
- var _a, _b, _c, _d, _e, _f, _g;
10032
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
9238
10033
  try {
9239
10034
  const isOpaqueCrossOrigin = event.message === "Script error." || event.message === "Script error" || !event.error && !event.filename;
9240
10035
  if (isOpaqueCrossOrigin) {
@@ -9249,6 +10044,7 @@ function GlitchgrabProviderInner({
9249
10044
  if (event.filename && EXTENSION_ORIGIN_RE.test(event.filename) || ((_a = event.error) == null ? void 0 : _a.stack) && EXTENSION_ORIGIN_RE.test(event.error.stack)) {
9250
10045
  return;
9251
10046
  }
10047
+ if (isHandledHydrationError(event.error)) return;
9252
10048
  incrementErrorCount();
9253
10049
  const context = captureContext(visitedPagesRef.current);
9254
10050
  const sig = computeSignature({
@@ -9274,6 +10070,9 @@ function GlitchgrabProviderInner({
9274
10070
  filename: (_e = event.filename) != null ? _e : "",
9275
10071
  lineno: String((_f = event.lineno) != null ? _f : ""),
9276
10072
  colno: String((_g = event.colno) != null ? _g : ""),
10073
+ // The root patch could not attach (React internals moved, or a
10074
+ // root it does not know): still label it and send the DOM context.
10075
+ ...isHydrationError((_h = event.error) != null ? _h : event.message) ? hydrationMetadata((_i = event.error) != null ? _i : event.message) : {},
9277
10076
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
9278
10077
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
9279
10078
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -9490,21 +10289,27 @@ function GlitchgrabProviderInner({
9490
10289
  () => ({ userId: signedUserId != null ? signedUserId : null, signature }),
9491
10290
  [signedUserId, signature]
9492
10291
  );
10292
+ const enhanceForDialog = useCallback3(
10293
+ async (text, screenshot) => {
10294
+ const ctx = captureContext(visitedPagesRef.current);
10295
+ const context = {
10296
+ url: ctx.url,
10297
+ visitedPages: ctx.visitedPages.slice(-5),
10298
+ breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
10299
+ };
10300
+ return await enhanceTextOrThrow(text, token, baseUrl, screenshot, context, identity);
10301
+ },
10302
+ [token, baseUrl, identity]
10303
+ );
9493
10304
  const enhance = useCallback3(
9494
10305
  async (text, screenshot) => {
9495
10306
  try {
9496
- const ctx = captureContext(visitedPagesRef.current);
9497
- const context = {
9498
- url: ctx.url,
9499
- visitedPages: ctx.visitedPages.slice(-5),
9500
- breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
9501
- };
9502
- return await enhanceText(text, token, baseUrl, screenshot, context, identity);
10307
+ return await enhanceForDialog(text, screenshot);
9503
10308
  } catch (e) {
9504
10309
  return text;
9505
10310
  }
9506
10311
  },
9507
- [token, baseUrl, identity]
10312
+ [enhanceForDialog]
9508
10313
  );
9509
10314
  const assist = useCallback3(
9510
10315
  async (params) => {
@@ -9615,7 +10420,7 @@ function GlitchgrabProviderInner({
9615
10420
  } catch (e) {
9616
10421
  }
9617
10422
  }, [openReportDialog]);
9618
- const reporter = useMemo4(() => {
10423
+ const reporter2 = useMemo4(() => {
9619
10424
  var _a;
9620
10425
  if (!session) return null;
9621
10426
  const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
@@ -9678,7 +10483,7 @@ function GlitchgrabProviderInner({
9678
10483
  {
9679
10484
  report,
9680
10485
  sendFeedback: feedback,
9681
- enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
10486
+ enhanceText: aiAssist && !aiBlockedUnsigned ? enhanceForDialog : void 0,
9682
10487
  assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
9683
10488
  findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
9684
10489
  readFile: aiAssist && !aiBlockedUnsigned ? readFile : void 0,
@@ -9686,7 +10491,8 @@ function GlitchgrabProviderInner({
9686
10491
  transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
9687
10492
  types,
9688
10493
  showSeverity,
9689
- reporter,
10494
+ reporter: reporter2,
10495
+ chatHistoryKey: chatHistoryKey("sdk", token, session == null ? void 0 : session.userId),
9690
10496
  headerSlot: projectName ? /* @__PURE__ */ jsxs7("div", { style: { fontSize: 11, color: "#9aa0a6" }, children: [
9691
10497
  "Reporting to ",
9692
10498
  /* @__PURE__ */ jsx7("strong", { style: { color: "#e5e5e5" }, children: projectName })
@@ -10022,6 +10828,87 @@ function useGlitchgrabActions({
10022
10828
  error
10023
10829
  };
10024
10830
  }
10831
+ async function fetchGlitchgrabReportThread({
10832
+ token,
10833
+ reportId
10834
+ }) {
10835
+ var _a, _b, _c;
10836
+ const res = await fetch(`${BASE_URL}/api/v1/sdk/reports/${encodeURIComponent(reportId)}`, {
10837
+ headers: { Authorization: `Bearer ${token}` }
10838
+ });
10839
+ const data = await res.json().catch(() => ({}));
10840
+ if (!res.ok || !data.success) throw new Error((_a = data.error) != null ? _a : "Failed to fetch report");
10841
+ return { ...data.data, comments: (_c = (_b = data.data) == null ? void 0 : _b.comments) != null ? _c : [] };
10842
+ }
10843
+ async function postGlitchgrabReportComment({
10844
+ token,
10845
+ reportId,
10846
+ message,
10847
+ reporterName,
10848
+ reporterEmail
10849
+ }) {
10850
+ var _a;
10851
+ const res = await fetch(`${BASE_URL}/api/v1/sdk/reports/${encodeURIComponent(reportId)}/comments`, {
10852
+ method: "POST",
10853
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
10854
+ body: JSON.stringify({ message, reporterName, reporterEmail })
10855
+ });
10856
+ const data = await res.json().catch(() => ({}));
10857
+ if (!res.ok || !data.success) throw new Error((_a = data.error) != null ? _a : "Failed to post comment");
10858
+ return data.data;
10859
+ }
10860
+ function useGlitchgrabReportThread({
10861
+ token,
10862
+ reportId,
10863
+ reporterName,
10864
+ reporterEmail
10865
+ }) {
10866
+ var _a;
10867
+ const [report, setReport] = useState8(null);
10868
+ const [isLoading, setIsLoading] = useState8(!!reportId);
10869
+ const [isFetching, setIsFetching] = useState8(false);
10870
+ const [isPosting, setIsPosting] = useState8(false);
10871
+ const [error, setError] = useState8(null);
10872
+ const refetch = useCallback4(async () => {
10873
+ if (!token || !reportId) {
10874
+ setReport(null);
10875
+ setIsLoading(false);
10876
+ return;
10877
+ }
10878
+ setIsFetching(true);
10879
+ setError(null);
10880
+ try {
10881
+ setReport(await fetchGlitchgrabReportThread({ token, reportId }));
10882
+ } catch (e) {
10883
+ setError(e instanceof Error ? e.message : "Network error");
10884
+ } finally {
10885
+ setIsLoading(false);
10886
+ setIsFetching(false);
10887
+ }
10888
+ }, [token, reportId]);
10889
+ useEffect7(() => {
10890
+ refetch();
10891
+ }, [refetch]);
10892
+ const addComment = useCallback4(
10893
+ async (message) => {
10894
+ if (!token || !reportId || !message.trim()) return false;
10895
+ setIsPosting(true);
10896
+ setError(null);
10897
+ try {
10898
+ await postGlitchgrabReportComment({ token, reportId, message: message.trim(), reporterName, reporterEmail });
10899
+ await refetch();
10900
+ return true;
10901
+ } catch (e) {
10902
+ setError(e instanceof Error ? e.message : "Failed to post comment");
10903
+ return false;
10904
+ } finally {
10905
+ setIsPosting(false);
10906
+ }
10907
+ },
10908
+ [token, reportId, reporterName, reporterEmail, refetch]
10909
+ );
10910
+ return { report, comments: (_a = report == null ? void 0 : report.comments) != null ? _a : [], isLoading, isFetching, isPosting, error, refetch, addComment };
10911
+ }
10025
10912
 
10026
10913
  // src/use-feedback.ts
10027
10914
  import { useState as useState9, useEffect as useEffect8, useCallback as useCallback5 } from "react";
@@ -10102,12 +10989,14 @@ export {
10102
10989
  computeSignature,
10103
10990
  enhanceText,
10104
10991
  fetchGlitchgrabFeedback,
10992
+ fetchGlitchgrabReportThread,
10105
10993
  fetchGlitchgrabReports,
10106
10994
  getAppContext,
10107
10995
  getBreadcrumbs,
10108
10996
  getShortcutLabel2 as getShortcutLabel,
10109
10997
  initBreadcrumbs,
10110
10998
  matchesShortcut,
10999
+ postGlitchgrabReportComment,
10111
11000
  sanitizeUrl,
10112
11001
  sendFeedback,
10113
11002
  sendLead,
@@ -10118,6 +11007,7 @@ export {
10118
11007
  useGlitchgrab,
10119
11008
  useGlitchgrabActions,
10120
11009
  useGlitchgrabFeedback,
11010
+ useGlitchgrabReportThread,
10121
11011
  useGlitchgrabReports
10122
11012
  };
10123
11013
  //# sourceMappingURL=index.mjs.map