glitchgrab 1.58.2 → 1.58.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -239,7 +239,7 @@ function argsToString(args) {
239
239
  function previewJson(value, max) {
240
240
  let out = "";
241
241
  const path = /* @__PURE__ */ new WeakSet();
242
- const write = (input, inArray) => {
242
+ const write2 = (input, inArray) => {
243
243
  if (out.length > max) return;
244
244
  let v = input;
245
245
  if (v !== null && typeof v === "object" && typeof v.toJSON === "function") {
@@ -276,7 +276,7 @@ function previewJson(value, max) {
276
276
  out += "[";
277
277
  for (let i = 0; i < obj.length && out.length <= max; i++) {
278
278
  if (i > 0) out += ",";
279
- write(obj[i], true);
279
+ write2(obj[i], true);
280
280
  }
281
281
  out += "]";
282
282
  } else {
@@ -289,13 +289,13 @@ function previewJson(value, max) {
289
289
  if (child === void 0 || typeof child === "function" || typeof child === "symbol") continue;
290
290
  out += `${first ? "" : ","}${JSON.stringify(key)}:`;
291
291
  first = false;
292
- write(child, false);
292
+ write2(child, false);
293
293
  }
294
294
  out += "}";
295
295
  }
296
296
  path.delete(obj);
297
297
  };
298
- write(value, false);
298
+ write2(value, false);
299
299
  return out;
300
300
  }
301
301
  async function readErrorBody(response, url) {
@@ -956,6 +956,292 @@ function clearDedupCache() {
956
956
  seen.clear();
957
957
  }
958
958
 
959
+ // src/hydration.ts
960
+ var HYDRATION_CODE_RE = /Minified React error #(418|419|422|423|425)\b/;
961
+ 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;
962
+ var BAILOUT_DIGEST = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
963
+ var DOM_SNIPPET_MAX = 3e3;
964
+ var STACK_FRAMES_MAX = 40;
965
+ function messageOf(error) {
966
+ if (error && typeof error === "object" && typeof error.message === "string") {
967
+ return error.message;
968
+ }
969
+ return typeof error === "string" ? error : "";
970
+ }
971
+ function hydrationCode(message) {
972
+ if (!message) return null;
973
+ const m = HYDRATION_CODE_RE.exec(message);
974
+ if (m) return m[1];
975
+ return HYDRATION_DEV_RE.test(message) ? "dev" : null;
976
+ }
977
+ function isHydrationError(error) {
978
+ if (hydrationCode(messageOf(error))) return true;
979
+ const cause = error && typeof error === "object" ? error.cause : void 0;
980
+ return cause !== void 0 && cause !== error && hydrationCode(messageOf(cause)) !== null;
981
+ }
982
+ function isBailout(error) {
983
+ const e = error;
984
+ if (!e || typeof e !== "object") return false;
985
+ if (e.digest === BAILOUT_DIGEST) return true;
986
+ const c = e.cause;
987
+ return !!c && typeof c === "object" && c.digest === BAILOUT_DIGEST;
988
+ }
989
+ function componentStackNames(stack) {
990
+ if (!stack) return [];
991
+ const names = [];
992
+ for (const raw of stack.split("\n")) {
993
+ const line = raw.trim();
994
+ if (!line) continue;
995
+ let name = "";
996
+ if (line.startsWith("at ")) {
997
+ name = line.slice(3).split(" (")[0].trim();
998
+ } else if (line.includes("@")) {
999
+ name = line.slice(0, line.indexOf("@")).trim();
1000
+ } else {
1001
+ name = line;
1002
+ }
1003
+ if (name) names.push(name);
1004
+ if (names.length >= STACK_FRAMES_MAX) break;
1005
+ }
1006
+ return names;
1007
+ }
1008
+ function componentStackKey(stack) {
1009
+ return componentStackNames(stack).join(">");
1010
+ }
1011
+ var observer = null;
1012
+ var observerStarted = false;
1013
+ var observerStopAt = 0;
1014
+ var OBSERVE_MS = 3e4;
1015
+ function startObserver() {
1016
+ if (observerStarted || typeof MutationObserver === "undefined" || typeof document === "undefined") {
1017
+ return;
1018
+ }
1019
+ observerStarted = true;
1020
+ try {
1021
+ observer = new MutationObserver(() => {
1022
+ if (Date.now() > observerStopAt) stopObserver();
1023
+ });
1024
+ observer.observe(document, { childList: true, subtree: true });
1025
+ observerStopAt = Date.now() + OBSERVE_MS;
1026
+ setTimeout(stopObserver, OBSERVE_MS);
1027
+ } catch (e) {
1028
+ observer = null;
1029
+ }
1030
+ }
1031
+ function stopObserver() {
1032
+ try {
1033
+ observer == null ? void 0 : observer.disconnect();
1034
+ } catch (e) {
1035
+ }
1036
+ observer = null;
1037
+ }
1038
+ function describeNode(node) {
1039
+ if (!node) return "";
1040
+ if (node.nodeType === 9) return "#document";
1041
+ const el = node;
1042
+ if (!el.tagName) return node.nodeName;
1043
+ const id = el.id ? `#${el.id}` : "";
1044
+ const cls = typeof el.className === "string" && el.className.trim() ? "." + el.className.trim().split(/\s+/).slice(0, 3).join(".") : "";
1045
+ return `${el.tagName.toLowerCase()}${id}${cls}`;
1046
+ }
1047
+ function pathOf(node) {
1048
+ const parts = [];
1049
+ let cur = node;
1050
+ while (cur && parts.length < 6) {
1051
+ parts.unshift(describeNode(cur));
1052
+ if (cur.nodeType === 9) break;
1053
+ cur = cur.parentNode;
1054
+ }
1055
+ return parts.join(" > ");
1056
+ }
1057
+ var NON_CONTENT_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT", "TEMPLATE", "TITLE", "HEAD"]);
1058
+ function isInHead(node) {
1059
+ for (let cur = node; cur; cur = cur.parentNode) {
1060
+ if (cur.nodeName === "HEAD") return true;
1061
+ if (cur.nodeName === "BODY") return false;
1062
+ }
1063
+ return false;
1064
+ }
1065
+ var GSTIN_RE = /\b\d{2}[A-Z]{5}\d{4}[A-Z][A-Z0-9]Z[A-Z0-9]\b/g;
1066
+ var PAN_RE = /\b[A-Z]{5}\d{4}[A-Z]\b/g;
1067
+ function scrubHtml(html) {
1068
+ return redactString(
1069
+ 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)))
1070
+ );
1071
+ }
1072
+ function takeRemovedServerHtml() {
1073
+ var _a, _b;
1074
+ if (!observer) return { html: "", where: "" };
1075
+ try {
1076
+ const records = observer.takeRecords();
1077
+ let html = "";
1078
+ let where = "";
1079
+ for (const rec of records) {
1080
+ if (rec.type !== "childList" || rec.removedNodes.length === 0) continue;
1081
+ if (isInHead(rec.target)) continue;
1082
+ if (!where) where = pathOf(rec.target);
1083
+ for (const node of Array.from(rec.removedNodes)) {
1084
+ if (node.nodeType === 1 && NON_CONTENT_TAGS.has(node.tagName)) continue;
1085
+ if (node.nodeType === 3 && !((_a = node.textContent) != null ? _a : "").trim()) continue;
1086
+ const piece = node.nodeType === 1 ? node.outerHTML : node.nodeType === 3 ? `#text "${(_b = node.textContent) != null ? _b : ""}"` : "";
1087
+ if (!piece) continue;
1088
+ html += (html ? "\n" : "") + piece;
1089
+ if (html.length >= DOM_SNIPPET_MAX) break;
1090
+ }
1091
+ if (html.length >= DOM_SNIPPET_MAX) break;
1092
+ }
1093
+ const scrubbed = scrubHtml(html);
1094
+ return {
1095
+ html: scrubbed.length > DOM_SNIPPET_MAX ? `${scrubbed.slice(0, DOM_SNIPPET_MAX)}\u2026[truncated]` : scrubbed,
1096
+ where
1097
+ };
1098
+ } catch (e) {
1099
+ return { html: "", where: "" };
1100
+ }
1101
+ }
1102
+ function detectForeignNodes(doc = document) {
1103
+ var _a;
1104
+ const found = [];
1105
+ try {
1106
+ const html = doc.documentElement;
1107
+ if ((html == null ? void 0 : html.classList.contains("translated-ltr")) || (html == null ? void 0 : html.classList.contains("translated-rtl"))) {
1108
+ found.push("Google Translate is active (html.translated-*)");
1109
+ }
1110
+ const fonts = doc.getElementsByTagName("font").length;
1111
+ if (fonts > 0) found.push(`<font> \xD7 ${fonts} (Google Translate or a translation extension)`);
1112
+ const detectors = doc.querySelectorAll("a[x-apple-data-detectors]").length;
1113
+ if (detectors > 0) {
1114
+ found.push(`<a x-apple-data-detectors> \xD7 ${detectors} (iOS format detection turned text into links)`);
1115
+ }
1116
+ if (doc.querySelector("[data-gr-ext-installed],[data-new-gr-c-s-check-loaded],grammarly-desktop-integration")) {
1117
+ found.push("Grammarly extension attributes");
1118
+ }
1119
+ if (doc.querySelector("[data-lt-installed]")) found.push("LanguageTool extension attributes");
1120
+ if ((_a = doc.body) == null ? void 0 : _a.hasAttribute("cz-shortcut-listen")) found.push("ColorZilla extension attribute on <body>");
1121
+ const custom = /* @__PURE__ */ new Set();
1122
+ for (const parent of [html, doc.body]) {
1123
+ if (!parent) continue;
1124
+ for (const child of Array.from(parent.children)) {
1125
+ const tag = child.tagName.toLowerCase();
1126
+ if (tag.includes("-")) custom.add(tag);
1127
+ }
1128
+ }
1129
+ if (custom.size > 0) {
1130
+ found.push(`custom elements at top level: ${Array.from(custom).slice(0, 5).join(", ")} (often an extension)`);
1131
+ }
1132
+ } catch (e) {
1133
+ }
1134
+ return found;
1135
+ }
1136
+ function hydrationTiming() {
1137
+ var _a, _b, _c, _d;
1138
+ try {
1139
+ if (typeof performance === "undefined") return "";
1140
+ const at2 = Math.round(performance.now());
1141
+ const parts = [`error at ${at2}ms after navigation start`];
1142
+ const nav = (_b = (_a = performance.getEntriesByType) == null ? void 0 : _a.call(performance, "navigation")) == null ? void 0 : _b[0];
1143
+ if (nav == null ? void 0 : nav.domContentLoadedEventEnd) {
1144
+ parts.push(`DOMContentLoaded ${Math.round(nav.domContentLoadedEventEnd)}ms`);
1145
+ }
1146
+ const fetches = ((_d = (_c = performance.getEntriesByType) == null ? void 0 : _c.call(performance, "resource")) != null ? _d : []).filter((e) => {
1147
+ const t = e.initiatorType;
1148
+ return t === "fetch" || t === "xmlhttprequest";
1149
+ }).sort((a, b) => a.responseEnd - b.responseEnd).slice(0, 8).map((e) => {
1150
+ let path = e.name;
1151
+ try {
1152
+ const u = new URL(e.name);
1153
+ path = u.origin === window.location.origin ? u.pathname : `${u.host}${u.pathname}`;
1154
+ } catch (e2) {
1155
+ }
1156
+ return `${path} ${Math.round(e.responseEnd)}ms`;
1157
+ });
1158
+ if (fetches.length > 0) parts.push(`fetches finished before it: ${fetches.join(", ")}`);
1159
+ else parts.push("no fetch had finished yet");
1160
+ return parts.join("; ");
1161
+ } catch (e) {
1162
+ return "";
1163
+ }
1164
+ }
1165
+ function hydrationMetadata(error, extra) {
1166
+ var _a, _b, _c;
1167
+ const code = (_a = hydrationCode(messageOf(error))) != null ? _a : hydrationCode(messageOf(error == null ? void 0 : error.cause));
1168
+ const meta = { errorKind: "hydration" };
1169
+ if (code) meta.hydrationCode = code;
1170
+ const cause = error == null ? void 0 : error.cause;
1171
+ if (cause && cause !== error && messageOf(cause)) {
1172
+ meta.hydrationCause = messageOf(cause).slice(0, 500);
1173
+ }
1174
+ if ((_b = extra == null ? void 0 : extra.removed) == null ? void 0 : _b.html) meta.hydrationServerHtml = extra.removed.html;
1175
+ if ((_c = extra == null ? void 0 : extra.removed) == null ? void 0 : _c.where) meta.hydrationReplacedIn = extra.removed.where;
1176
+ const foreign = detectForeignNodes();
1177
+ if (foreign.length > 0) meta.hydrationForeignNodes = foreign.join("\n");
1178
+ const timing = hydrationTiming();
1179
+ if (timing) meta.hydrationTiming = timing;
1180
+ return meta;
1181
+ }
1182
+ var handled = /* @__PURE__ */ new WeakSet();
1183
+ function isHandledHydrationError(error) {
1184
+ return !!error && typeof error === "object" && handled.has(error);
1185
+ }
1186
+ function markHandled(error) {
1187
+ if (error && typeof error === "object") {
1188
+ handled.add(error);
1189
+ const cause = error.cause;
1190
+ if (cause && typeof cause === "object") handled.add(cause);
1191
+ }
1192
+ }
1193
+ var reporter = null;
1194
+ var PATCHED = "__glitchgrabHydrationPatched";
1195
+ function findFiberRoot(container) {
1196
+ if (!container || typeof container !== "object") return null;
1197
+ try {
1198
+ const key = Object.keys(container).find((k) => k.startsWith("__reactContainer$"));
1199
+ if (!key) return null;
1200
+ const hostRoot = container[key];
1201
+ const root = hostRoot == null ? void 0 : hostRoot.stateNode;
1202
+ return root && typeof root === "object" ? root : null;
1203
+ } catch (e) {
1204
+ return null;
1205
+ }
1206
+ }
1207
+ function patchRoot(root) {
1208
+ if (root[PATCHED]) return true;
1209
+ const original = root.onRecoverableError;
1210
+ if (typeof original !== "function") return false;
1211
+ root.onRecoverableError = function glitchgrabOnRecoverableError(error, info) {
1212
+ var _a;
1213
+ try {
1214
+ if (reporter && isHydrationError(error) && !isBailout(error)) {
1215
+ const removed = takeRemovedServerHtml();
1216
+ markHandled(error);
1217
+ reporter(error, (_a = info == null ? void 0 : info.componentStack) != null ? _a : "", hydrationMetadata(error, { removed }));
1218
+ }
1219
+ } catch (e) {
1220
+ }
1221
+ return original.call(this, error, info);
1222
+ };
1223
+ root[PATCHED] = true;
1224
+ return true;
1225
+ }
1226
+ function patchKnownRoots() {
1227
+ if (typeof document === "undefined") return false;
1228
+ let patched = false;
1229
+ for (const container of [document, document.getElementById("__next"), document.getElementById("root")]) {
1230
+ const root = findFiberRoot(container);
1231
+ if (root && patchRoot(root)) patched = true;
1232
+ }
1233
+ return patched;
1234
+ }
1235
+ function installHydrationCapture(report) {
1236
+ try {
1237
+ if (typeof window === "undefined") return;
1238
+ reporter = report;
1239
+ startObserver();
1240
+ patchKnownRoots();
1241
+ } catch (e) {
1242
+ }
1243
+ }
1244
+
959
1245
  // src/capture.ts
960
1246
  var config = null;
961
1247
  function registerCaptureConfig(next) {
@@ -975,7 +1261,7 @@ function sessionMetadata(session) {
975
1261
  ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
976
1262
  };
977
1263
  }
978
- function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError") {
1264
+ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError", dedupKey) {
979
1265
  var _a, _b, _c;
980
1266
  const err = error instanceof Error ? error : void 0;
981
1267
  const errorMessage = err ? err.message : String(error);
@@ -987,7 +1273,7 @@ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureErr
987
1273
  const sig = computeSignature({
988
1274
  errorMessage,
989
1275
  pageUrl: context.url,
990
- errorStack: err == null ? void 0 : err.stack,
1276
+ errorStack: dedupKey ? `at ${dedupKey}` : err == null ? void 0 : err.stack,
991
1277
  digest
992
1278
  });
993
1279
  if (shouldSkipDuplicate(sig)) return null;
@@ -1027,6 +1313,25 @@ function captureError(error, options) {
1027
1313
  } catch (e) {
1028
1314
  }
1029
1315
  }
1316
+ function reportHydrationError(error, componentStack, metadata) {
1317
+ try {
1318
+ const cfg = config;
1319
+ if (!(cfg == null ? void 0 : cfg.token)) return;
1320
+ const payload = buildCapturedErrorPayload(
1321
+ error,
1322
+ { componentStack: componentStack || void 0, metadata },
1323
+ cfg,
1324
+ "onRecoverableError",
1325
+ componentStackKey(componentStack) || void 0
1326
+ );
1327
+ if (!payload) return;
1328
+ void sendReport(payload, cfg.baseUrl).then((result) => {
1329
+ if (result && cfg.onReportSent) cfg.onReportSent(result);
1330
+ });
1331
+ if (cfg.onError && error instanceof Error) cfg.onError(error);
1332
+ } catch (e) {
1333
+ }
1334
+ }
1030
1335
 
1031
1336
  // src/error-boundary.tsx
1032
1337
  var GlitchgrabErrorBoundary = class extends React.Component {
@@ -1948,6 +2253,79 @@ function renderBlocks(lines, theme, key) {
1948
2253
  function ChatMarkdown({ text, theme }) {
1949
2254
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: STACK, children: renderBlocks(text.replace(/\r\n?/g, "\n").split("\n"), theme, "md") });
1950
2255
  }
2256
+ var MAX_SAVED_CHATS = 20;
2257
+ var SAVED_CHAT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2258
+ var TITLE_CHARS = 80;
2259
+ var PREFIX = "glitchgrab:ai-chats:";
2260
+ function chatHistoryKey(...parts) {
2261
+ const input = parts.map((p) => p != null ? p : "").join("\0");
2262
+ let hash = 2166136261;
2263
+ for (let i = 0; i < input.length; i++) {
2264
+ hash ^= input.charCodeAt(i);
2265
+ hash = Math.imul(hash, 16777619);
2266
+ }
2267
+ return (hash >>> 0).toString(36);
2268
+ }
2269
+ function chatTitle(messages) {
2270
+ var _a, _b;
2271
+ const first = (_b = (_a = messages.find((m) => m.role === "user")) == null ? void 0 : _a.content.trim()) != null ? _b : "";
2272
+ const line = first.replace(/\s+/g, " ");
2273
+ return line.length > TITLE_CHARS ? `${line.slice(0, TITLE_CHARS - 1)}\u2026` : line;
2274
+ }
2275
+ function storage() {
2276
+ try {
2277
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
2278
+ } catch (e) {
2279
+ return null;
2280
+ }
2281
+ }
2282
+ function isSavedChat(value) {
2283
+ if (!value || typeof value !== "object") return false;
2284
+ const c = value;
2285
+ return typeof c.id === "string" && typeof c.title === "string" && typeof c.updatedAt === "number" && (c.status === "open" || c.status === "filed") && Array.isArray(c.messages);
2286
+ }
2287
+ function loadChats(key, now = Date.now()) {
2288
+ const store = storage();
2289
+ if (!store) return [];
2290
+ try {
2291
+ const raw = store.getItem(PREFIX + key);
2292
+ if (!raw) return [];
2293
+ const parsed = JSON.parse(raw);
2294
+ if (!Array.isArray(parsed)) return [];
2295
+ 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);
2296
+ } catch (e) {
2297
+ return [];
2298
+ }
2299
+ }
2300
+ function write(key, chats) {
2301
+ const store = storage();
2302
+ if (!store) return;
2303
+ try {
2304
+ if (chats.length === 0) store.removeItem(PREFIX + key);
2305
+ else store.setItem(PREFIX + key, JSON.stringify(chats));
2306
+ } catch (e) {
2307
+ }
2308
+ }
2309
+ function saveChat(key, chat, now = Date.now()) {
2310
+ const rest = loadChats(key, now).filter((c) => c.id !== chat.id);
2311
+ const next = [chat, ...rest].slice(0, MAX_SAVED_CHATS);
2312
+ write(key, next);
2313
+ return next;
2314
+ }
2315
+ function markChatFiled(key, conversationId, label, now = Date.now()) {
2316
+ const chats = loadChats(key, now);
2317
+ const hit = chats.find((c) => c.conversationId === conversationId);
2318
+ if (!hit || hit.status === "filed") return;
2319
+ write(
2320
+ key,
2321
+ chats.map((c) => c === hit ? { ...c, status: "filed", filedLabel: label, updatedAt: now } : c)
2322
+ );
2323
+ }
2324
+ function removeChat(key, id, now = Date.now()) {
2325
+ const next = loadChats(key, now).filter((c) => c.id !== id);
2326
+ write(key, next);
2327
+ return next;
2328
+ }
1951
2329
  function pickAssistImages(all) {
1952
2330
  const picked = [];
1953
2331
  let total = 0;
@@ -1974,6 +2352,18 @@ function pickAssistFiles(files) {
1974
2352
  }
1975
2353
  return picked;
1976
2354
  }
2355
+ function newChatId() {
2356
+ return `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
2357
+ }
2358
+ function chatAge(at2, now = Date.now()) {
2359
+ const min = Math.round((now - at2) / 6e4);
2360
+ if (min < 1) return "just now";
2361
+ if (min < 60) return `${min} min ago`;
2362
+ const hours = Math.round(min / 60);
2363
+ if (hours < 24) return `${hours} h ago`;
2364
+ if (hours < 48) return "yesterday";
2365
+ return new Date(at2).toLocaleDateString(void 0, { day: "numeric", month: "short" });
2366
+ }
1977
2367
  var STARTERS = [
1978
2368
  "Something on this page is broken",
1979
2369
  "It did the wrong thing",
@@ -2056,6 +2446,33 @@ function Avatar({
2056
2446
  }
2057
2447
  );
2058
2448
  }
2449
+ var THINKING_SLOW_SECONDS = 20;
2450
+ function ThinkingClock({ color }) {
2451
+ const [seconds, setSeconds] = (0, import_react3.useState)(0);
2452
+ (0, import_react3.useEffect)(() => {
2453
+ const started = Date.now();
2454
+ const id = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1e3)), 1e3);
2455
+ return () => clearInterval(id);
2456
+ }, []);
2457
+ return (
2458
+ // Hidden from screen readers: the bubble already says "Thinking", and a
2459
+ // number changing every second would be read out every second.
2460
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2461
+ "span",
2462
+ {
2463
+ "data-gg-thinking-clock": "",
2464
+ "aria-hidden": "true",
2465
+ style: { marginLeft: "6px", fontSize: "12px", color, fontVariantNumeric: "tabular-nums" },
2466
+ children: [
2467
+ "Thinking\u2026 ",
2468
+ seconds,
2469
+ "s",
2470
+ seconds >= THINKING_SLOW_SECONDS ? " \xB7 taking longer than usual, still working" : ""
2471
+ ]
2472
+ }
2473
+ )
2474
+ );
2475
+ }
2059
2476
  function AssistSheet({
2060
2477
  assist,
2061
2478
  theme: t,
@@ -2096,14 +2513,22 @@ function AssistSheet({
2096
2513
  onDegrade,
2097
2514
  onClose,
2098
2515
  onFinish,
2099
- onReportGlitchgrabProblem
2516
+ onReportGlitchgrabProblem,
2517
+ historyKey = null
2100
2518
  }) {
2519
+ var _a;
2101
2520
  const narrow = useIsNarrow();
2102
2521
  const severityAsked = showSeverity && currentTile === "BUG";
2103
2522
  const [messages, setMessages] = (0, import_react3.useState)([]);
2104
2523
  const [input, setInput] = (0, import_react3.useState)("");
2105
2524
  const [busy, setBusy] = (0, import_react3.useState)(false);
2106
2525
  const [conversationId, setConversationId] = (0, import_react3.useState)(null);
2526
+ const chatIdRef = (0, import_react3.useRef)(newChatId());
2527
+ const [savedChats, setSavedChats] = (0, import_react3.useState)(
2528
+ () => historyKey ? loadChats(historyKey) : []
2529
+ );
2530
+ const [showChats, setShowChats] = (0, import_react3.useState)(false);
2531
+ const [viewingChat, setViewingChat] = (0, import_react3.useState)(null);
2107
2532
  const [hoveredStar, setHoveredStar] = (0, import_react3.useState)(0);
2108
2533
  const [options, setOptions] = (0, import_react3.useState)([]);
2109
2534
  const [duplicate, setDuplicate] = (0, import_react3.useState)(null);
@@ -2155,8 +2580,8 @@ function AssistSheet({
2155
2580
  if (el) el.scrollTop = el.scrollHeight;
2156
2581
  }, [messages, busy, phase]);
2157
2582
  (0, import_react3.useEffect)(() => {
2158
- var _a;
2159
- if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
2583
+ var _a2;
2584
+ if (phase === "draft") (_a2 = draftRef.current) == null ? void 0 : _a2.focus();
2160
2585
  }, [phase]);
2161
2586
  (0, import_react3.useEffect)(() => {
2162
2587
  const el = composerRef.current;
@@ -2188,7 +2613,8 @@ function AssistSheet({
2188
2613
  onDegrade(message, retryable);
2189
2614
  }
2190
2615
  async function runTurn(history2, autoFile = false) {
2191
- var _a, _b, _c;
2616
+ var _a2, _b, _c;
2617
+ const turnChat = chatIdRef.current;
2192
2618
  setBusy(true);
2193
2619
  setAutoFileLeft(null);
2194
2620
  let result;
@@ -2225,6 +2651,7 @@ function AssistSheet({
2225
2651
  } finally {
2226
2652
  setBusy(false);
2227
2653
  }
2654
+ if (chatIdRef.current !== turnChat) return;
2228
2655
  if (result.degraded) {
2229
2656
  degradeKeepingEvents(events, result.degraded, result.retryable === true);
2230
2657
  return;
@@ -2235,7 +2662,7 @@ function AssistSheet({
2235
2662
  }
2236
2663
  const attach = result.duplicate ? { ...result.duplicate, kind: "duplicate" } : result.related ? { ...result.related, kind: "related" } : null;
2237
2664
  setDuplicate(attach);
2238
- onDuplicateChange == null ? void 0 : onDuplicateChange((_a = attach == null ? void 0 : attach.number) != null ? _a : null, attach == null ? void 0 : attach.kind);
2665
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_a2 = attach == null ? void 0 : attach.number) != null ? _a2 : null, attach == null ? void 0 : attach.kind);
2239
2666
  setGlitchgrabOffer(!!result.aboutGlitchgrab && !!onReportGlitchgrabProblem && !!result.question);
2240
2667
  if (result.solved) {
2241
2668
  setSolved(result.solved);
@@ -2289,9 +2716,9 @@ function AssistSheet({
2289
2716
  }
2290
2717
  }
2291
2718
  function sendTyped(text) {
2292
- var _a;
2719
+ var _a2;
2293
2720
  const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
2294
- const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a : "");
2721
+ const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a2 = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a2 : "");
2295
2722
  send(text, checkingBack && isAffirmativeReply(text));
2296
2723
  }
2297
2724
  function send(text, autoFile = false) {
@@ -2333,7 +2760,215 @@ function AssistSheet({
2333
2760
  flushEvents();
2334
2761
  onSend();
2335
2762
  }
2763
+ (0, import_react3.useEffect)(() => {
2764
+ if (!historyKey || !messages.some((m) => m.role === "user")) return;
2765
+ const done = submitted || phase === "solved";
2766
+ const kept = submitted || phase === "draft" ? withCurrentDraft(messages) : messages;
2767
+ setSavedChats(
2768
+ saveChat(historyKey, {
2769
+ id: chatIdRef.current,
2770
+ conversationId,
2771
+ title: chatTitle(kept),
2772
+ tile: currentTile,
2773
+ messages: kept,
2774
+ ...issueTitle ? { issueTitle } : {},
2775
+ status: done ? "filed" : "open",
2776
+ ...done ? {
2777
+ filedLabel: submitted ? submittedText != null ? submittedText : `${reportTypeLabel} sent` : "Answered \u2014 nothing was filed"
2778
+ } : {},
2779
+ updatedAt: Date.now()
2780
+ })
2781
+ );
2782
+ }, [
2783
+ historyKey,
2784
+ messages,
2785
+ conversationId,
2786
+ submitted,
2787
+ phase === "solved",
2788
+ issueTitle,
2789
+ phase === "draft" ? description : null
2790
+ ]);
2791
+ function resumeChat(chat) {
2792
+ var _a2;
2793
+ flushEvents();
2794
+ chatIdRef.current = chat.id;
2795
+ setMessages(chat.messages);
2796
+ setConversationId(chat.conversationId);
2797
+ onConversationChange == null ? void 0 : onConversationChange(chat.conversationId);
2798
+ setOptions([]);
2799
+ setDuplicate(null);
2800
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2801
+ setGlitchgrabOffer(false);
2802
+ setAutoFileLeft(null);
2803
+ setInput("");
2804
+ if (chat.tile !== currentTile && tiles.includes(chat.tile)) {
2805
+ onPickType(chat.tile);
2806
+ }
2807
+ const last = chat.messages[chat.messages.length - 1];
2808
+ if ((last == null ? void 0 : last.kind) === "report") {
2809
+ onDescriptionChange(last.content);
2810
+ onIssueTitleChange((_a2 = chat.issueTitle) != null ? _a2 : "");
2811
+ aiDraftRef.current = last.content;
2812
+ setPhase("draft");
2813
+ } else {
2814
+ setPhase("chat");
2815
+ }
2816
+ setShowChats(false);
2817
+ setViewingChat(null);
2818
+ }
2819
+ function startNewChat() {
2820
+ flushEvents();
2821
+ chatIdRef.current = newChatId();
2822
+ setMessages([]);
2823
+ setConversationId(null);
2824
+ onConversationChange == null ? void 0 : onConversationChange(null);
2825
+ setOptions([]);
2826
+ setDuplicate(null);
2827
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2828
+ setGlitchgrabOffer(false);
2829
+ setAutoFileLeft(null);
2830
+ setInput("");
2831
+ onDescriptionChange("");
2832
+ onIssueTitleChange("");
2833
+ aiDraftRef.current = null;
2834
+ setPhase("chat");
2835
+ setShowChats(false);
2836
+ setViewingChat(null);
2837
+ }
2838
+ function forgetChat(id) {
2839
+ if (!historyKey) return;
2840
+ setSavedChats(removeChat(historyKey, id));
2841
+ if (viewingChat === id) setViewingChat(null);
2842
+ }
2843
+ const otherChats = savedChats.filter((c) => c.id !== chatIdRef.current);
2844
+ const resumable = (_a = otherChats.find((c) => c.status === "open" && c.messages.length > 0)) != null ? _a : null;
2336
2845
  const empty = phase === "chat" && messages.length === 0 && !busy;
2846
+ function renderChatList() {
2847
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { "data-gg-chat-list": "", style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [
2848
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
2849
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "13px", fontWeight: 600 }, children: "Your chats" }),
2850
+ messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2851
+ "button",
2852
+ {
2853
+ type: "button",
2854
+ "data-gg-new-chat": "",
2855
+ onClick: startNewChat,
2856
+ style: {
2857
+ border: `1px solid ${t.inputBorder}`,
2858
+ borderRadius: "999px",
2859
+ background: "transparent",
2860
+ color: t.text,
2861
+ padding: "7px 12px",
2862
+ fontSize: "12px",
2863
+ fontFamily: "inherit",
2864
+ cursor: "pointer"
2865
+ },
2866
+ children: "+ New chat"
2867
+ }
2868
+ )
2869
+ ] }),
2870
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "11.5px", color: t.textMuted, lineHeight: 1.5 }, children: "Kept in this browser only, for 30 days." }),
2871
+ otherChats.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "12.5px", color: t.textMuted, padding: "12px 0" }, children: "No other chats yet." }),
2872
+ otherChats.map((chat) => {
2873
+ var _a2;
2874
+ const filed = chat.status === "filed";
2875
+ const open = viewingChat === chat.id;
2876
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2877
+ "div",
2878
+ {
2879
+ "data-gg-saved-chat": chat.status,
2880
+ style: {
2881
+ border: `1px solid ${t.border}`,
2882
+ borderRadius: "10px",
2883
+ padding: "10px 12px",
2884
+ display: "flex",
2885
+ flexDirection: "column",
2886
+ gap: "6px",
2887
+ minWidth: 0
2888
+ },
2889
+ children: [
2890
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-start", minWidth: 0 }, children: [
2891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2892
+ "button",
2893
+ {
2894
+ type: "button",
2895
+ onClick: () => filed ? setViewingChat(open ? null : chat.id) : resumeChat(chat),
2896
+ style: {
2897
+ flex: 1,
2898
+ minWidth: 0,
2899
+ textAlign: "left",
2900
+ border: "none",
2901
+ background: "transparent",
2902
+ color: t.text,
2903
+ padding: 0,
2904
+ fontFamily: "inherit",
2905
+ cursor: "pointer",
2906
+ display: "flex",
2907
+ flexDirection: "column",
2908
+ gap: "4px"
2909
+ },
2910
+ children: [
2911
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "13px", lineHeight: 1.45, overflowWrap: "anywhere" }, children: chat.title || "Untitled chat" }),
2912
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
2913
+ filed ? (_a2 = chat.filedLabel) != null ? _a2 : "Sent" : "Not sent yet \u2014 tap to continue",
2914
+ " \xB7",
2915
+ " ",
2916
+ chatAge(chat.updatedAt)
2917
+ ] })
2918
+ ]
2919
+ }
2920
+ ),
2921
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2922
+ "button",
2923
+ {
2924
+ type: "button",
2925
+ "aria-label": `Delete chat: ${chat.title}`,
2926
+ onClick: () => forgetChat(chat.id),
2927
+ style: {
2928
+ flexShrink: 0,
2929
+ border: "none",
2930
+ background: "transparent",
2931
+ color: t.textMuted,
2932
+ fontSize: "16px",
2933
+ lineHeight: 1,
2934
+ padding: "4px 6px",
2935
+ cursor: "pointer",
2936
+ fontFamily: "inherit"
2937
+ },
2938
+ children: "\xD7"
2939
+ }
2940
+ )
2941
+ ] }),
2942
+ open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2943
+ "div",
2944
+ {
2945
+ "data-gg-saved-transcript": "",
2946
+ style: {
2947
+ display: "flex",
2948
+ flexDirection: "column",
2949
+ gap: "6px",
2950
+ borderTop: `1px solid ${t.border}`,
2951
+ paddingTop: "8px",
2952
+ fontSize: "12.5px",
2953
+ lineHeight: 1.5
2954
+ },
2955
+ children: chat.messages.map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: [
2956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
2957
+ m.role === "user" ? "You" : m.kind === "report" ? "Report" : "Assistant",
2958
+ ":",
2959
+ " "
2960
+ ] }),
2961
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { whiteSpace: "pre-wrap" }, children: m.content })
2962
+ ] }, i))
2963
+ }
2964
+ )
2965
+ ]
2966
+ },
2967
+ chat.id
2968
+ );
2969
+ })
2970
+ ] });
2971
+ }
2337
2972
  function renderKnownIssue({ title, footer }) {
2338
2973
  if (!duplicate) return null;
2339
2974
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -2367,8 +3002,8 @@ function AssistSheet({
2367
3002
  );
2368
3003
  }
2369
3004
  const initials2 = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
2370
- var _a, _b;
2371
- return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
3005
+ var _a2, _b;
3006
+ return (_b = (_a2 = w[0]) == null ? void 0 : _a2.toUpperCase()) != null ? _b : "";
2372
3007
  }).join("");
2373
3008
  const panelStyle = narrow ? {
2374
3009
  width: "100%",
@@ -2459,7 +3094,9 @@ function AssistSheet({
2459
3094
  alignItems: "center",
2460
3095
  gap: "7px",
2461
3096
  fontSize: "14px",
2462
- fontWeight: 600
3097
+ fontWeight: 600,
3098
+ // One line even with the Chats pill beside it (#567).
3099
+ whiteSpace: "nowrap"
2463
3100
  },
2464
3101
  children: [
2465
3102
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Sparkle, { color: t.accent }),
@@ -2489,34 +3126,64 @@ function AssistSheet({
2489
3126
  projectSlot ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { marginLeft: "6px" }, children: projectSlot }) : null
2490
3127
  ] })
2491
3128
  ] }),
2492
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2493
- "button",
2494
- {
2495
- type: "button",
2496
- "data-gg-write-myself": "",
2497
- onClick: () => leave("write_myself", `header, ${phase}`),
2498
- "aria-label": "Write it myself instead",
2499
- style: {
2500
- flexShrink: 0,
2501
- border: `1px solid ${t.inputBorder}`,
2502
- borderRadius: "999px",
2503
- background: "transparent",
2504
- color: t.textMuted,
2505
- cursor: "pointer",
2506
- fontSize: "11.5px",
2507
- lineHeight: 1,
2508
- fontFamily: "inherit",
2509
- whiteSpace: "nowrap",
2510
- // ~32px tall either way — this is the escape hatch, not a hint.
2511
- padding: "9px 11px"
2512
- },
2513
- children: "Write it myself"
2514
- }
2515
- )
3129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "6px", flexShrink: 0 }, children: [
3130
+ historyKey && (showChats || otherChats.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3131
+ "button",
3132
+ {
3133
+ type: "button",
3134
+ "data-gg-chats-toggle": "",
3135
+ disabled: busy,
3136
+ "aria-pressed": showChats,
3137
+ onClick: () => {
3138
+ setShowChats((v) => !v);
3139
+ setViewingChat(null);
3140
+ },
3141
+ style: {
3142
+ flexShrink: 0,
3143
+ border: `1px solid ${showChats ? t.accent : t.inputBorder}`,
3144
+ borderRadius: "999px",
3145
+ background: "transparent",
3146
+ color: showChats ? t.accent : t.textMuted,
3147
+ cursor: busy ? "default" : "pointer",
3148
+ opacity: busy ? 0.5 : 1,
3149
+ fontSize: "11.5px",
3150
+ lineHeight: 1,
3151
+ fontFamily: "inherit",
3152
+ whiteSpace: "nowrap",
3153
+ padding: "9px 11px"
3154
+ },
3155
+ children: showChats ? "\u2190 Chat" : `Chats \xB7 ${otherChats.length}`
3156
+ }
3157
+ ),
3158
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3159
+ "button",
3160
+ {
3161
+ type: "button",
3162
+ "data-gg-write-myself": "",
3163
+ onClick: () => leave("write_myself", `header, ${phase}`),
3164
+ "aria-label": "Write it myself instead",
3165
+ style: {
3166
+ flexShrink: 0,
3167
+ border: `1px solid ${t.inputBorder}`,
3168
+ borderRadius: "999px",
3169
+ background: "transparent",
3170
+ color: t.textMuted,
3171
+ cursor: "pointer",
3172
+ fontSize: "11.5px",
3173
+ lineHeight: 1,
3174
+ fontFamily: "inherit",
3175
+ whiteSpace: "nowrap",
3176
+ // ~32px tall either way — this is the escape hatch, not a hint.
3177
+ padding: "9px 11px"
3178
+ },
3179
+ children: "Write it myself"
3180
+ }
3181
+ )
3182
+ ] })
2516
3183
  ]
2517
3184
  }
2518
3185
  ),
2519
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3186
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2520
3187
  "div",
2521
3188
  {
2522
3189
  ref: scrollRef,
@@ -2528,762 +3195,845 @@ function AssistSheet({
2528
3195
  display: "flex",
2529
3196
  flexDirection: "column"
2530
3197
  },
2531
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2532
- "div",
2533
- {
2534
- style: {
2535
- marginTop: "auto",
2536
- display: "flex",
2537
- flexDirection: "column",
2538
- gap: "12px",
2539
- minWidth: 0
2540
- },
2541
- children: [
2542
- phase === "type" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2543
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2544
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2545
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3198
+ children: [
3199
+ showChats && renderChatList(),
3200
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3201
+ "div",
3202
+ {
3203
+ style: {
3204
+ marginTop: "auto",
3205
+ display: "flex",
3206
+ flexDirection: "column",
3207
+ gap: "12px",
3208
+ minWidth: 0
3209
+ },
3210
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: showChats ? "none" : "contents" }, children: [
3211
+ phase === "type" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3212
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3213
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3214
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3215
+ "div",
3216
+ {
3217
+ style: {
3218
+ padding: "9px 12px",
3219
+ borderRadius: "12px 12px 12px 4px",
3220
+ backgroundColor: t.bgSecondary,
3221
+ fontSize: "13px",
3222
+ lineHeight: 1.55,
3223
+ maxWidth: "88%"
3224
+ },
3225
+ children: [
3226
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
3227
+ "hat do you want to do? Pick one and I'll take it from there."
3228
+ ]
3229
+ }
3230
+ )
3231
+ ] }),
3232
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2546
3233
  "div",
2547
3234
  {
2548
3235
  style: {
2549
- padding: "9px 12px",
2550
- borderRadius: "12px 12px 12px 4px",
2551
- backgroundColor: t.bgSecondary,
2552
- fontSize: "13px",
2553
- lineHeight: 1.55,
2554
- maxWidth: "88%"
3236
+ display: "flex",
3237
+ flexWrap: "wrap",
3238
+ gap: "6px",
3239
+ paddingLeft: "32px"
2555
3240
  },
2556
- children: [
2557
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
2558
- "hat do you want to do? Pick one and I'll take it from there."
2559
- ]
3241
+ children: tiles.map((tile) => {
3242
+ const rate = tile === "RATING";
3243
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3244
+ "button",
3245
+ {
3246
+ type: "button",
3247
+ "data-gg-chip": "",
3248
+ "data-gg-tile": tile,
3249
+ onClick: () => pickType(tile),
3250
+ style: {
3251
+ display: "inline-flex",
3252
+ alignItems: "center",
3253
+ gap: "5px",
3254
+ // The rating is the only tile that isn't a report, so
3255
+ // it wears the star colour the dialog's hero row uses
3256
+ // rather than hiding among the report types.
3257
+ border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
3258
+ background: rate ? "rgba(245,158,11,0.08)" : "transparent",
3259
+ color: rate ? "#f59e0b" : t.textMuted,
3260
+ borderRadius: "999px",
3261
+ // 8px vertical keeps the tap target at ~32px.
3262
+ padding: "8px 12px",
3263
+ fontSize: "12px",
3264
+ fontWeight: rate ? 600 : 400,
3265
+ fontFamily: "inherit",
3266
+ cursor: "pointer"
3267
+ },
3268
+ children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3269
+ },
3270
+ tile
3271
+ );
3272
+ })
2560
3273
  }
2561
3274
  )
2562
3275
  ] }),
2563
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2564
- "div",
2565
- {
2566
- style: {
2567
- display: "flex",
2568
- flexWrap: "wrap",
2569
- gap: "6px",
2570
- paddingLeft: "32px"
2571
- },
2572
- children: tiles.map((tile) => {
2573
- const rate = tile === "RATING";
2574
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3276
+ phase === "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3277
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3278
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3279
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3280
+ "div",
3281
+ {
3282
+ style: {
3283
+ padding: "9px 12px",
3284
+ borderRadius: "12px 12px 12px 4px",
3285
+ backgroundColor: t.bgSecondary,
3286
+ fontSize: "13px",
3287
+ lineHeight: 1.55,
3288
+ maxWidth: "88%"
3289
+ },
3290
+ children: "How bad is it?"
3291
+ }
3292
+ )
3293
+ ] }),
3294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3295
+ "div",
3296
+ {
3297
+ role: "radiogroup",
3298
+ "aria-label": "Severity",
3299
+ style: {
3300
+ display: "flex",
3301
+ flexWrap: "wrap",
3302
+ gap: "6px",
3303
+ paddingLeft: "32px"
3304
+ },
3305
+ children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2575
3306
  "button",
2576
3307
  {
2577
3308
  type: "button",
3309
+ role: "radio",
3310
+ "aria-checked": severity === s,
2578
3311
  "data-gg-chip": "",
2579
- "data-gg-tile": tile,
2580
- onClick: () => pickType(tile),
3312
+ "data-gg-severity": s,
3313
+ onClick: () => answerSeverity(s),
2581
3314
  style: {
2582
- display: "inline-flex",
2583
- alignItems: "center",
2584
- gap: "5px",
2585
- // The rating is the only tile that isn't a report, so
2586
- // it wears the star colour the dialog's hero row uses
2587
- // rather than hiding among the report types.
2588
- border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
2589
- background: rate ? "rgba(245,158,11,0.08)" : "transparent",
2590
- color: rate ? "#f59e0b" : t.textMuted,
3315
+ border: `1px solid ${t.inputBorder}`,
3316
+ background: "transparent",
3317
+ color: t.textMuted,
2591
3318
  borderRadius: "999px",
2592
- // 8px vertical keeps the tap target at ~32px.
3319
+ // Same as the picker and option chips — one chip size.
2593
3320
  padding: "8px 12px",
2594
3321
  fontSize: "12px",
2595
- fontWeight: rate ? 600 : 400,
2596
3322
  fontFamily: "inherit",
2597
3323
  cursor: "pointer"
2598
3324
  },
2599
- children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3325
+ children: SEVERITY_LABELS[s]
2600
3326
  },
2601
- tile
2602
- );
2603
- })
2604
- }
2605
- )
2606
- ] }),
2607
- phase === "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2608
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2609
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2610
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2611
- "div",
2612
- {
2613
- style: {
2614
- padding: "9px 12px",
2615
- borderRadius: "12px 12px 12px 4px",
2616
- backgroundColor: t.bgSecondary,
2617
- fontSize: "13px",
2618
- lineHeight: 1.55,
2619
- maxWidth: "88%"
2620
- },
2621
- children: "How bad is it?"
3327
+ s
3328
+ ))
2622
3329
  }
2623
3330
  )
2624
3331
  ] }),
2625
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2626
- "div",
2627
- {
2628
- role: "radiogroup",
2629
- "aria-label": "Severity",
2630
- style: {
2631
- display: "flex",
2632
- flexWrap: "wrap",
2633
- gap: "6px",
2634
- paddingLeft: "32px"
2635
- },
2636
- children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2637
- "button",
3332
+ phase === "rating" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3333
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3334
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3335
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3336
+ "div",
2638
3337
  {
2639
- type: "button",
2640
- role: "radio",
2641
- "aria-checked": severity === s,
2642
- "data-gg-chip": "",
2643
- "data-gg-severity": s,
2644
- onClick: () => answerSeverity(s),
2645
3338
  style: {
2646
- border: `1px solid ${t.inputBorder}`,
2647
- background: "transparent",
2648
- color: t.textMuted,
2649
- borderRadius: "999px",
2650
- // Same as the picker and option chips — one chip size.
2651
- padding: "8px 12px",
2652
- fontSize: "12px",
2653
- fontFamily: "inherit",
2654
- cursor: "pointer"
3339
+ padding: "9px 12px",
3340
+ borderRadius: "12px 12px 12px 4px",
3341
+ backgroundColor: t.bgSecondary,
3342
+ fontSize: "13px",
3343
+ lineHeight: 1.55,
3344
+ maxWidth: "88%"
2655
3345
  },
2656
- children: SEVERITY_LABELS[s]
2657
- },
2658
- s
2659
- ))
2660
- }
2661
- )
2662
- ] }),
2663
- phase === "rating" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2664
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2665
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2666
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3346
+ children: "How are we doing? Tap a star \u2014 the words are optional."
3347
+ }
3348
+ )
3349
+ ] }),
3350
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2667
3351
  "div",
2668
3352
  {
2669
3353
  style: {
2670
- padding: "9px 12px",
2671
- borderRadius: "12px 12px 12px 4px",
2672
- backgroundColor: t.bgSecondary,
2673
- fontSize: "13px",
2674
- lineHeight: 1.55,
2675
- maxWidth: "88%"
3354
+ display: "flex",
3355
+ flexDirection: "column",
3356
+ gap: "10px",
3357
+ marginLeft: "32px",
3358
+ padding: "12px",
3359
+ borderRadius: "12px",
3360
+ border: "1px solid rgba(245,158,11,0.4)",
3361
+ backgroundColor: "rgba(245,158,11,0.06)"
2676
3362
  },
2677
- children: "How are we doing? Tap a star \u2014 the words are optional."
2678
- }
2679
- )
2680
- ] }),
2681
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2682
- "div",
2683
- {
2684
- style: {
2685
- display: "flex",
2686
- flexDirection: "column",
2687
- gap: "10px",
2688
- marginLeft: "32px",
2689
- padding: "12px",
2690
- borderRadius: "12px",
2691
- border: "1px solid rgba(245,158,11,0.4)",
2692
- backgroundColor: "rgba(245,158,11,0.06)"
2693
- },
2694
- children: [
2695
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
2696
- const filled = star <= (hoveredStar || rating);
2697
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3363
+ children: [
3364
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
3365
+ const filled = star <= (hoveredStar || rating);
3366
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3367
+ "button",
3368
+ {
3369
+ type: "button",
3370
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
3371
+ "aria-pressed": star <= rating,
3372
+ onClick: () => onRatingChange(star),
3373
+ onMouseEnter: () => setHoveredStar(star),
3374
+ onMouseLeave: () => setHoveredStar(0),
3375
+ style: {
3376
+ border: "none",
3377
+ background: "transparent",
3378
+ padding: 0,
3379
+ cursor: "pointer",
3380
+ lineHeight: 0
3381
+ },
3382
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3383
+ "path",
3384
+ {
3385
+ 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",
3386
+ fill: filled ? "#f59e0b" : "transparent",
3387
+ stroke: filled ? "#f59e0b" : t.inputBorder,
3388
+ strokeWidth: "1.5",
3389
+ strokeLinejoin: "round"
3390
+ }
3391
+ ) })
3392
+ },
3393
+ star
3394
+ );
3395
+ }) }),
3396
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3397
+ "textarea",
3398
+ {
3399
+ value: description,
3400
+ onChange: (e) => onDescriptionChange(e.target.value),
3401
+ rows: 3,
3402
+ placeholder: "What made it good \u2014 or what let you down? (optional)",
3403
+ style: {
3404
+ width: "100%",
3405
+ padding: "10px",
3406
+ borderRadius: "8px",
3407
+ border: `1px solid ${t.inputBorder}`,
3408
+ backgroundColor: t.inputBg,
3409
+ color: t.text,
3410
+ fontSize: "13px",
3411
+ lineHeight: 1.5,
3412
+ fontFamily: "inherit",
3413
+ resize: "vertical",
3414
+ outline: "none",
3415
+ boxSizing: "border-box"
3416
+ }
3417
+ }
3418
+ ),
3419
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2698
3420
  "button",
2699
3421
  {
2700
3422
  type: "button",
2701
- "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2702
- "aria-pressed": star <= rating,
2703
- onClick: () => onRatingChange(star),
2704
- onMouseEnter: () => setHoveredStar(star),
2705
- onMouseLeave: () => setHoveredStar(0),
3423
+ onClick: () => setPhase("type"),
2706
3424
  style: {
3425
+ alignSelf: "flex-start",
2707
3426
  border: "none",
2708
3427
  background: "transparent",
2709
- padding: 0,
3428
+ color: t.textMuted,
3429
+ fontSize: "12px",
3430
+ fontFamily: "inherit",
3431
+ // A bare text link is ~14px tall — half a thumb.
3432
+ padding: "9px 0",
2710
3433
  cursor: "pointer",
2711
- lineHeight: 0
3434
+ textDecoration: "underline"
2712
3435
  },
2713
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2714
- "path",
2715
- {
2716
- d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
2717
- fill: filled ? "#f59e0b" : "transparent",
2718
- stroke: filled ? "#f59e0b" : t.inputBorder,
2719
- strokeWidth: "1.5",
2720
- strokeLinejoin: "round"
2721
- }
2722
- ) })
2723
- },
2724
- star
2725
- );
2726
- }) }),
2727
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2728
- "textarea",
2729
- {
2730
- value: description,
2731
- onChange: (e) => onDescriptionChange(e.target.value),
2732
- rows: 3,
2733
- placeholder: "What made it good \u2014 or what let you down? (optional)",
2734
- style: {
2735
- width: "100%",
2736
- padding: "10px",
2737
- borderRadius: "8px",
2738
- border: `1px solid ${t.inputBorder}`,
2739
- backgroundColor: t.inputBg,
2740
- color: t.text,
2741
- fontSize: "13px",
2742
- lineHeight: 1.5,
2743
- fontFamily: "inherit",
2744
- resize: "vertical",
2745
- outline: "none",
2746
- boxSizing: "border-box"
3436
+ children: "Report something instead"
2747
3437
  }
2748
- }
2749
- ),
2750
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3438
+ )
3439
+ ]
3440
+ }
3441
+ )
3442
+ ] }),
3443
+ empty && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3444
+ resumable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3445
+ "div",
3446
+ {
3447
+ "data-gg-resume-chat": "",
3448
+ style: {
3449
+ display: "flex",
3450
+ flexDirection: "column",
3451
+ gap: "8px",
3452
+ padding: "10px 12px",
3453
+ borderRadius: "10px",
3454
+ border: `1px solid ${t.accent}55`,
3455
+ backgroundColor: t.bgSecondary,
3456
+ minWidth: 0
3457
+ },
3458
+ children: [
3459
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3460
+ "Pick up where you left off \xB7 ",
3461
+ chatAge(resumable.updatedAt)
3462
+ ] }),
3463
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3464
+ "span",
3465
+ {
3466
+ style: {
3467
+ fontSize: "13px",
3468
+ lineHeight: 1.45,
3469
+ overflowWrap: "anywhere"
3470
+ },
3471
+ children: resumable.title
3472
+ }
3473
+ ),
3474
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: [
3475
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3476
+ "button",
3477
+ {
3478
+ type: "button",
3479
+ "data-gg-resume-continue": "",
3480
+ onClick: () => resumeChat(resumable),
3481
+ style: {
3482
+ border: "none",
3483
+ borderRadius: "999px",
3484
+ backgroundColor: t.accent,
3485
+ color: t.accentText,
3486
+ padding: "7px 13px",
3487
+ fontSize: "12px",
3488
+ fontWeight: 600,
3489
+ fontFamily: "inherit",
3490
+ cursor: "pointer"
3491
+ },
3492
+ children: "Continue this chat"
3493
+ }
3494
+ ),
3495
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3496
+ "button",
3497
+ {
3498
+ type: "button",
3499
+ onClick: () => setShowChats(true),
3500
+ style: {
3501
+ border: `1px solid ${t.inputBorder}`,
3502
+ borderRadius: "999px",
3503
+ background: "transparent",
3504
+ color: t.textMuted,
3505
+ padding: "7px 13px",
3506
+ fontSize: "12px",
3507
+ fontFamily: "inherit",
3508
+ cursor: "pointer"
3509
+ },
3510
+ children: "All chats"
3511
+ }
3512
+ )
3513
+ ] })
3514
+ ]
3515
+ }
3516
+ ),
3517
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3518
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3519
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3520
+ "div",
3521
+ {
3522
+ style: {
3523
+ padding: "9px 12px",
3524
+ borderRadius: "12px 12px 12px 4px",
3525
+ backgroundColor: t.bgSecondary,
3526
+ fontSize: "13px",
3527
+ lineHeight: 1.55,
3528
+ maxWidth: "88%"
3529
+ },
3530
+ children: [
3531
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
3532
+ "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
3533
+ ]
3534
+ }
3535
+ )
3536
+ ] }),
3537
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3538
+ "div",
3539
+ {
3540
+ style: {
3541
+ display: "flex",
3542
+ flexWrap: "wrap",
3543
+ gap: "6px",
3544
+ paddingLeft: "32px"
3545
+ },
3546
+ children: STARTERS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2751
3547
  "button",
2752
3548
  {
2753
3549
  type: "button",
2754
- onClick: () => setPhase("type"),
3550
+ "data-gg-chip": "",
3551
+ onClick: () => {
3552
+ logEvent("starter_tap", s);
3553
+ send(s);
3554
+ },
2755
3555
  style: {
2756
- alignSelf: "flex-start",
2757
- border: "none",
3556
+ border: `1px solid ${t.inputBorder}`,
2758
3557
  background: "transparent",
2759
3558
  color: t.textMuted,
3559
+ borderRadius: "999px",
3560
+ padding: "6px 11px",
2760
3561
  fontSize: "12px",
2761
3562
  fontFamily: "inherit",
2762
- // A bare text link is ~14px tall — half a thumb.
2763
- padding: "9px 0",
2764
- cursor: "pointer",
2765
- textDecoration: "underline"
3563
+ cursor: "pointer"
2766
3564
  },
2767
- children: "Report something instead"
3565
+ children: s
3566
+ },
3567
+ s
3568
+ ))
3569
+ }
3570
+ )
3571
+ ] }),
3572
+ messages.map((m, i) => {
3573
+ const mine = m.role === "user";
3574
+ if (m.kind === "report") {
3575
+ const live = phase === "draft" && i === messages.length - 1;
3576
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
3577
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3578
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3579
+ "div",
3580
+ {
3581
+ "data-gg-draft-bubble": "",
3582
+ style: {
3583
+ maxWidth: "82%",
3584
+ minWidth: 0,
3585
+ padding: "9px 12px",
3586
+ borderRadius: "12px 12px 12px 4px",
3587
+ fontSize: "13px",
3588
+ lineHeight: 1.55,
3589
+ backgroundColor: t.bgSecondary,
3590
+ color: t.text,
3591
+ border: live ? "none" : `1px solid ${t.accent}66`
3592
+ },
3593
+ children: live ? (
3594
+ // #402: "have a read" beside a countdown reads as a
3595
+ // contradiction. Say what is about to happen instead.
3596
+ 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."
3597
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3598
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3599
+ "span",
3600
+ {
3601
+ style: {
3602
+ display: "block",
3603
+ fontSize: "10px",
3604
+ fontWeight: 700,
3605
+ letterSpacing: ".06em",
3606
+ textTransform: "uppercase",
3607
+ color: t.accent,
3608
+ marginBottom: "4px"
3609
+ },
3610
+ children: "Draft so far"
3611
+ }
3612
+ ),
3613
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3614
+ "span",
3615
+ {
3616
+ style: {
3617
+ display: "block",
3618
+ whiteSpace: "pre-wrap",
3619
+ wordBreak: "break-word",
3620
+ maxHeight: "160px",
3621
+ overflowY: "auto",
3622
+ color: t.textMuted
3623
+ },
3624
+ children: m.content
3625
+ }
3626
+ )
3627
+ ] })
2768
3628
  }
2769
3629
  )
2770
- ]
3630
+ ] }, i);
2771
3631
  }
2772
- )
2773
- ] }),
2774
- empty && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2775
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2776
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2777
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3632
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2778
3633
  "div",
2779
3634
  {
2780
3635
  style: {
2781
- padding: "9px 12px",
2782
- borderRadius: "12px 12px 12px 4px",
2783
- backgroundColor: t.bgSecondary,
2784
- fontSize: "13px",
2785
- lineHeight: 1.55,
2786
- maxWidth: "88%"
3636
+ display: "flex",
3637
+ gap: "8px",
3638
+ minWidth: 0,
3639
+ flexDirection: mine ? "row-reverse" : "row"
2787
3640
  },
2788
3641
  children: [
2789
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
2790
- "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
3642
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: m.role, theme: t, initials: initials2 }),
3643
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3644
+ "div",
3645
+ {
3646
+ style: {
3647
+ maxWidth: "82%",
3648
+ minWidth: 0,
3649
+ padding: "9px 12px",
3650
+ borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
3651
+ fontSize: "13px",
3652
+ lineHeight: 1.55,
3653
+ // Markdown owns the assistant's line breaks and lists.
3654
+ whiteSpace: mine ? "pre-wrap" : "normal",
3655
+ wordBreak: "break-word",
3656
+ backgroundColor: mine ? t.accent : t.bgSecondary,
3657
+ color: mine ? t.accentText : t.text
3658
+ },
3659
+ children: mine ? m.content : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatMarkdown, { text: m.content, theme: t })
3660
+ }
3661
+ )
2791
3662
  ]
2792
- }
2793
- )
2794
- ] }),
2795
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3663
+ },
3664
+ i
3665
+ );
3666
+ }),
3667
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
3668
+ glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2796
3669
  "div",
2797
3670
  {
3671
+ "data-gg-glitchgrab-offer": "",
2798
3672
  style: {
2799
3673
  display: "flex",
3674
+ flexDirection: "column",
3675
+ gap: "8px",
3676
+ padding: "10px",
3677
+ borderRadius: "8px",
3678
+ border: `1px solid ${t.accent}55`,
3679
+ backgroundColor: `${t.accent}14`,
3680
+ fontSize: "12px",
3681
+ lineHeight: 1.5
3682
+ },
3683
+ children: [
3684
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
3685
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
3686
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
3687
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3688
+ "button",
3689
+ {
3690
+ type: "button",
3691
+ "data-gg-glitchgrab-offer-send": "",
3692
+ onClick: () => {
3693
+ logEvent("glitchgrab_offer_tap");
3694
+ setGlitchgrabOffer(false);
3695
+ onReportGlitchgrabProblem(
3696
+ messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
3697
+ );
3698
+ },
3699
+ style: {
3700
+ padding: "8px 12px",
3701
+ borderRadius: "999px",
3702
+ border: "none",
3703
+ backgroundColor: t.accent,
3704
+ color: t.accentText,
3705
+ fontSize: "12px",
3706
+ fontWeight: 700,
3707
+ fontFamily: "inherit",
3708
+ cursor: "pointer",
3709
+ // Both stretch, so on a phone each wrapped line reads as
3710
+ // a choice rather than one button left stranded.
3711
+ flex: "1 1 auto"
3712
+ },
3713
+ children: "Send to the Glitchgrab team"
3714
+ }
3715
+ ),
3716
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3717
+ "button",
3718
+ {
3719
+ type: "button",
3720
+ "data-gg-glitchgrab-offer-no": "",
3721
+ onClick: () => {
3722
+ logEvent("glitchgrab_offer_declined");
3723
+ send("No \u2014 it's about this app, not the reporting tool");
3724
+ },
3725
+ style: {
3726
+ padding: "8px 12px",
3727
+ borderRadius: "999px",
3728
+ border: `1px solid ${t.inputBorder}`,
3729
+ background: "transparent",
3730
+ color: t.textMuted,
3731
+ fontSize: "12px",
3732
+ fontFamily: "inherit",
3733
+ cursor: "pointer",
3734
+ flex: "1 1 auto"
3735
+ },
3736
+ children: "No, it's about this app"
3737
+ }
3738
+ )
3739
+ ] })
3740
+ ]
3741
+ }
3742
+ ) }),
3743
+ options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3744
+ "div",
3745
+ {
3746
+ style: {
3747
+ display: "flex",
3748
+ // Long labels wrapped into a ragged row that always stranded
3749
+ // the last chip on a line of its own. Past ~24 characters
3750
+ // these stop being chips and become a choice list, so they
3751
+ // stack full width and read like one.
3752
+ flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
3753
+ alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
2800
3754
  flexWrap: "wrap",
2801
3755
  gap: "6px",
2802
3756
  paddingLeft: "32px"
2803
3757
  },
2804
- children: STARTERS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3758
+ children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2805
3759
  "button",
2806
3760
  {
2807
3761
  type: "button",
2808
3762
  "data-gg-chip": "",
3763
+ "data-gg-option": "",
2809
3764
  onClick: () => {
2810
- logEvent("starter_tap", s);
2811
- send(s);
3765
+ logEvent("option_tap", option);
3766
+ send(option, option === CONFIRM_OPTION);
2812
3767
  },
2813
3768
  style: {
2814
3769
  border: `1px solid ${t.inputBorder}`,
2815
3770
  background: "transparent",
2816
3771
  color: t.textMuted,
2817
3772
  borderRadius: "999px",
2818
- padding: "6px 11px",
3773
+ // Same 8px as the picker chips — one tap target size in
3774
+ // this sheet, not two.
3775
+ padding: "8px 12px",
2819
3776
  fontSize: "12px",
2820
3777
  fontFamily: "inherit",
2821
- cursor: "pointer"
3778
+ cursor: "pointer",
3779
+ textAlign: "left"
2822
3780
  },
2823
- children: s
3781
+ children: option
2824
3782
  },
2825
- s
3783
+ option
2826
3784
  ))
2827
3785
  }
2828
- )
2829
- ] }),
2830
- messages.map((m, i) => {
2831
- const mine = m.role === "user";
2832
- if (m.kind === "report") {
2833
- const live = phase === "draft" && i === messages.length - 1;
2834
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
2835
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2836
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2837
- "div",
2838
- {
2839
- "data-gg-draft-bubble": "",
2840
- style: {
2841
- maxWidth: "82%",
2842
- minWidth: 0,
2843
- padding: "9px 12px",
2844
- borderRadius: "12px 12px 12px 4px",
2845
- fontSize: "13px",
2846
- lineHeight: 1.55,
2847
- backgroundColor: t.bgSecondary,
2848
- color: t.text,
2849
- border: live ? "none" : `1px solid ${t.accent}66`
2850
- },
2851
- children: live ? (
2852
- // #402: "have a read" beside a countdown reads as a
2853
- // contradiction. Say what is about to happen instead.
2854
- autoFileLeft !== null ? "Here's your report \u2014 I'll file it unless you want to change something." : "Here's your report \u2014 have a read."
2855
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2856
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2857
- "span",
2858
- {
2859
- style: {
2860
- display: "block",
2861
- fontSize: "10px",
2862
- fontWeight: 700,
2863
- letterSpacing: ".06em",
2864
- textTransform: "uppercase",
2865
- color: t.accent,
2866
- marginBottom: "4px"
2867
- },
2868
- children: "Draft so far"
2869
- }
2870
- ),
2871
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2872
- "span",
2873
- {
2874
- style: {
2875
- display: "block",
2876
- whiteSpace: "pre-wrap",
2877
- wordBreak: "break-word",
2878
- maxHeight: "160px",
2879
- overflowY: "auto",
2880
- color: t.textMuted
2881
- },
2882
- children: m.content
2883
- }
2884
- )
2885
- ] })
2886
- }
2887
- )
2888
- ] }, i);
2889
- }
2890
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3786
+ ),
3787
+ busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, role: "status", "aria-label": "Thinking", children: [
3788
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3789
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3790
+ "span",
3791
+ {
3792
+ style: {
3793
+ display: "inline-flex",
3794
+ alignItems: "center",
3795
+ // The "still working" line must wrap on a phone, not push
3796
+ // the bubble past the sheet's edge.
3797
+ flexWrap: "wrap",
3798
+ minWidth: 0,
3799
+ gap: "4px",
3800
+ padding: "12px",
3801
+ borderRadius: "12px 12px 12px 4px",
3802
+ backgroundColor: t.bgSecondary
3803
+ },
3804
+ children: [
3805
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3806
+ "span",
3807
+ {
3808
+ style: {
3809
+ width: "5px",
3810
+ height: "5px",
3811
+ borderRadius: "50%",
3812
+ backgroundColor: t.textMuted,
3813
+ animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3814
+ }
3815
+ },
3816
+ i
3817
+ )),
3818
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThinkingClock, { color: t.textMuted })
3819
+ ]
3820
+ }
3821
+ )
3822
+ ] }),
3823
+ phase === "draft" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2891
3824
  "div",
2892
3825
  {
2893
3826
  style: {
3827
+ border: `1px solid ${t.accent}`,
3828
+ borderRadius: "12px",
3829
+ backgroundColor: t.bgSecondary,
3830
+ padding: "12px",
2894
3831
  display: "flex",
2895
- gap: "8px",
3832
+ flexDirection: "column",
3833
+ gap: "10px",
2896
3834
  minWidth: 0,
2897
- flexDirection: mine ? "row-reverse" : "row"
3835
+ boxSizing: "border-box"
2898
3836
  },
2899
3837
  children: [
2900
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: m.role, theme: t, initials: initials2 }),
2901
3838
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2902
- "div",
3839
+ "span",
2903
3840
  {
2904
3841
  style: {
2905
- maxWidth: "82%",
2906
- minWidth: 0,
2907
- padding: "9px 12px",
2908
- borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
2909
- fontSize: "13px",
2910
- lineHeight: 1.55,
2911
- // Markdown owns the assistant's line breaks and lists.
2912
- whiteSpace: mine ? "pre-wrap" : "normal",
2913
- wordBreak: "break-word",
2914
- backgroundColor: mine ? t.accent : t.bgSecondary,
2915
- color: mine ? t.accentText : t.text
3842
+ fontSize: "10px",
3843
+ fontWeight: 700,
3844
+ letterSpacing: ".06em",
3845
+ textTransform: "uppercase",
3846
+ color: t.accent
2916
3847
  },
2917
- children: mine ? m.content : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatMarkdown, { text: m.content, theme: t })
3848
+ children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
2918
3849
  }
2919
- )
2920
- ]
2921
- },
2922
- i
2923
- );
2924
- }),
2925
- duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
2926
- glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2927
- "div",
2928
- {
2929
- "data-gg-glitchgrab-offer": "",
2930
- style: {
2931
- display: "flex",
2932
- flexDirection: "column",
2933
- gap: "8px",
2934
- padding: "10px",
2935
- borderRadius: "8px",
2936
- border: `1px solid ${t.accent}55`,
2937
- backgroundColor: `${t.accent}14`,
2938
- fontSize: "12px",
2939
- lineHeight: 1.5
2940
- },
2941
- children: [
2942
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
2943
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
2944
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
3850
+ ),
3851
+ renderKnownIssue({
3852
+ title: true,
3853
+ footer: "Your details will be added to it instead of opening a new issue."
3854
+ }),
3855
+ !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3856
+ // box edge is the very thing #543 is about. One line of text,
3857
+ // so Enter is swallowed.
2945
3858
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2946
- "button",
3859
+ "textarea",
2947
3860
  {
2948
- type: "button",
2949
- "data-gg-glitchgrab-offer-send": "",
2950
- onClick: () => {
2951
- logEvent("glitchgrab_offer_tap");
2952
- setGlitchgrabOffer(false);
2953
- onReportGlitchgrabProblem(
2954
- messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
2955
- );
3861
+ "aria-label": "Title",
3862
+ value: issueTitle,
3863
+ maxLength: 120,
3864
+ rows: 2,
3865
+ onKeyDown: (e) => {
3866
+ if (e.key === "Enter") e.preventDefault();
3867
+ },
3868
+ onChange: (e) => {
3869
+ stopAutoFile("edited the title");
3870
+ onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
2956
3871
  },
2957
3872
  style: {
2958
- padding: "8px 12px",
2959
- borderRadius: "999px",
2960
- border: "none",
2961
- backgroundColor: t.accent,
2962
- color: t.accentText,
2963
- fontSize: "12px",
2964
- fontWeight: 700,
3873
+ width: "100%",
3874
+ padding: "8px 10px",
3875
+ lineHeight: 1.45,
3876
+ resize: "none",
3877
+ borderRadius: "8px",
3878
+ border: `1px solid ${t.inputBorder}`,
3879
+ backgroundColor: t.inputBg,
3880
+ color: t.text,
3881
+ fontSize: "13px",
3882
+ fontWeight: 600,
2965
3883
  fontFamily: "inherit",
2966
- cursor: "pointer",
2967
- // Both stretch, so on a phone each wrapped line reads as
2968
- // a choice rather than one button left stranded.
2969
- flex: "1 1 auto"
2970
- },
2971
- children: "Send to the Glitchgrab team"
3884
+ outline: "none",
3885
+ boxSizing: "border-box"
3886
+ }
2972
3887
  }
2973
3888
  ),
2974
3889
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2975
- "button",
3890
+ "textarea",
2976
3891
  {
2977
- type: "button",
2978
- "data-gg-glitchgrab-offer-no": "",
2979
- onClick: () => {
2980
- logEvent("glitchgrab_offer_declined");
2981
- send("No \u2014 it's about this app, not the reporting tool");
3892
+ ref: draftRef,
3893
+ value: description,
3894
+ onChange: (e) => {
3895
+ stopAutoFile("edited the draft");
3896
+ onDescriptionChange(e.target.value);
2982
3897
  },
3898
+ rows: 6,
2983
3899
  style: {
2984
- padding: "8px 12px",
2985
- borderRadius: "999px",
3900
+ width: "100%",
3901
+ minHeight: "120px",
3902
+ padding: "10px",
3903
+ borderRadius: "8px",
2986
3904
  border: `1px solid ${t.inputBorder}`,
2987
- background: "transparent",
2988
- color: t.textMuted,
2989
- fontSize: "12px",
3905
+ backgroundColor: t.inputBg,
3906
+ color: t.text,
3907
+ fontSize: "13px",
3908
+ lineHeight: 1.55,
2990
3909
  fontFamily: "inherit",
2991
- cursor: "pointer",
2992
- flex: "1 1 auto"
2993
- },
2994
- children: "No, it's about this app"
2995
- }
2996
- )
2997
- ] })
2998
- ]
2999
- }
3000
- ) }),
3001
- options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3002
- "div",
3003
- {
3004
- style: {
3005
- display: "flex",
3006
- // Long labels wrapped into a ragged row that always stranded
3007
- // the last chip on a line of its own. Past ~24 characters
3008
- // these stop being chips and become a choice list, so they
3009
- // stack full width and read like one.
3010
- flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
3011
- alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
3012
- flexWrap: "wrap",
3013
- gap: "6px",
3014
- paddingLeft: "32px"
3015
- },
3016
- children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3017
- "button",
3018
- {
3019
- type: "button",
3020
- "data-gg-chip": "",
3021
- "data-gg-option": "",
3022
- onClick: () => {
3023
- logEvent("option_tap", option);
3024
- send(option, option === CONFIRM_OPTION);
3025
- },
3026
- style: {
3027
- border: `1px solid ${t.inputBorder}`,
3028
- background: "transparent",
3029
- color: t.textMuted,
3030
- borderRadius: "999px",
3031
- // Same 8px as the picker chips — one tap target size in
3032
- // this sheet, not two.
3033
- padding: "8px 12px",
3034
- fontSize: "12px",
3035
- fontFamily: "inherit",
3036
- cursor: "pointer",
3037
- textAlign: "left"
3038
- },
3039
- children: option
3040
- },
3041
- option
3042
- ))
3043
- }
3044
- ),
3045
- busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, role: "status", "aria-label": "Thinking", children: [
3046
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3047
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3048
- "span",
3049
- {
3050
- style: {
3051
- display: "inline-flex",
3052
- alignItems: "center",
3053
- gap: "4px",
3054
- padding: "12px",
3055
- borderRadius: "12px 12px 12px 4px",
3056
- backgroundColor: t.bgSecondary
3057
- },
3058
- children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3059
- "span",
3060
- {
3061
- style: {
3062
- width: "5px",
3063
- height: "5px",
3064
- borderRadius: "50%",
3065
- backgroundColor: t.textMuted,
3066
- animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3910
+ resize: "vertical",
3911
+ outline: "none",
3912
+ boxSizing: "border-box"
3913
+ }
3067
3914
  }
3068
- },
3069
- i
3070
- ))
3915
+ ),
3916
+ severityAsked && autoFileLeft === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
3917
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3918
+ "span",
3919
+ {
3920
+ style: {
3921
+ fontSize: "11.5px",
3922
+ color: t.textMuted,
3923
+ display: "block",
3924
+ marginBottom: "6px"
3925
+ },
3926
+ children: [
3927
+ "How bad is it?",
3928
+ !severity && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3929
+ "span",
3930
+ {
3931
+ style: {
3932
+ marginLeft: "6px",
3933
+ fontSize: "11px",
3934
+ color: severityRefused ? "#ef4444" : t.textMuted
3935
+ },
3936
+ children: "\xB7 required"
3937
+ }
3938
+ )
3939
+ ]
3940
+ }
3941
+ ),
3942
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3943
+ "div",
3944
+ {
3945
+ role: "radiogroup",
3946
+ "aria-label": "Severity",
3947
+ style: { display: "flex", gap: "6px" },
3948
+ children: SEVERITY_LEVELS.map((s) => {
3949
+ const on = severity === s;
3950
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3951
+ "button",
3952
+ {
3953
+ type: "button",
3954
+ onClick: () => onSeverityChange(s),
3955
+ role: "radio",
3956
+ "aria-checked": on,
3957
+ style: {
3958
+ flex: 1,
3959
+ minWidth: 0,
3960
+ padding: "8px 2px",
3961
+ borderRadius: "8px",
3962
+ border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3963
+ backgroundColor: on ? t.accent : "transparent",
3964
+ color: on ? t.accentText : t.textMuted,
3965
+ fontSize: "12px",
3966
+ fontWeight: 600,
3967
+ fontFamily: "inherit",
3968
+ whiteSpace: "nowrap",
3969
+ overflow: "hidden",
3970
+ textOverflow: "ellipsis",
3971
+ cursor: "pointer"
3972
+ },
3973
+ children: SEVERITY_LABELS[s]
3974
+ },
3975
+ s
3976
+ );
3977
+ })
3978
+ }
3979
+ )
3980
+ ] }),
3981
+ attachmentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3982
+ attachmentCount,
3983
+ " ",
3984
+ attachmentCount === 1 ? "attachment" : "attachments",
3985
+ " will be sent with this."
3986
+ ] })
3987
+ ]
3071
3988
  }
3072
- )
3073
- ] }),
3074
- phase === "draft" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3075
- "div",
3076
- {
3077
- style: {
3078
- border: `1px solid ${t.accent}`,
3079
- borderRadius: "12px",
3080
- backgroundColor: t.bgSecondary,
3081
- padding: "12px",
3082
- display: "flex",
3083
- flexDirection: "column",
3084
- gap: "10px",
3085
- minWidth: 0,
3086
- boxSizing: "border-box"
3087
- },
3088
- children: [
3089
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3090
- "span",
3091
- {
3092
- style: {
3093
- fontSize: "10px",
3094
- fontWeight: 700,
3095
- letterSpacing: ".06em",
3096
- textTransform: "uppercase",
3097
- color: t.accent
3098
- },
3099
- children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3100
- }
3101
- ),
3102
- renderKnownIssue({
3103
- title: true,
3104
- footer: "Your details will be added to it instead of opening a new issue."
3105
- }),
3106
- !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3107
- // box edge is the very thing #543 is about. One line of text,
3108
- // so Enter is swallowed.
3109
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3110
- "textarea",
3111
- {
3112
- "aria-label": "Title",
3113
- value: issueTitle,
3114
- maxLength: 120,
3115
- rows: 2,
3116
- onKeyDown: (e) => {
3117
- if (e.key === "Enter") e.preventDefault();
3118
- },
3119
- onChange: (e) => {
3120
- stopAutoFile("edited the title");
3121
- onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3122
- },
3123
- style: {
3124
- width: "100%",
3125
- padding: "8px 10px",
3126
- lineHeight: 1.45,
3127
- resize: "none",
3128
- borderRadius: "8px",
3129
- border: `1px solid ${t.inputBorder}`,
3130
- backgroundColor: t.inputBg,
3131
- color: t.text,
3132
- fontSize: "13px",
3133
- fontWeight: 600,
3134
- fontFamily: "inherit",
3135
- outline: "none",
3136
- boxSizing: "border-box"
3137
- }
3138
- }
3139
- ),
3989
+ ),
3990
+ phase === "solved" && solved && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3991
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3992
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3140
3993
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3141
- "textarea",
3994
+ "div",
3142
3995
  {
3143
- ref: draftRef,
3144
- value: description,
3145
- onChange: (e) => {
3146
- stopAutoFile("edited the draft");
3147
- onDescriptionChange(e.target.value);
3148
- },
3149
- rows: 6,
3150
3996
  style: {
3151
- width: "100%",
3152
- minHeight: "120px",
3153
- padding: "10px",
3154
- borderRadius: "8px",
3155
- border: `1px solid ${t.inputBorder}`,
3156
- backgroundColor: t.inputBg,
3157
- color: t.text,
3997
+ padding: "9px 12px",
3998
+ borderRadius: "12px 12px 12px 4px",
3999
+ backgroundColor: t.bgSecondary,
3158
4000
  fontSize: "13px",
3159
4001
  lineHeight: 1.55,
3160
- fontFamily: "inherit",
3161
- resize: "vertical",
3162
- outline: "none",
3163
- boxSizing: "border-box"
3164
- }
4002
+ maxWidth: "88%",
4003
+ whiteSpace: "pre-wrap"
4004
+ },
4005
+ children: solved
3165
4006
  }
3166
- ),
3167
- severityAsked && autoFileLeft === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
3168
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3169
- "span",
3170
- {
3171
- style: {
3172
- fontSize: "11.5px",
3173
- color: t.textMuted,
3174
- display: "block",
3175
- marginBottom: "6px"
3176
- },
3177
- children: [
3178
- "How bad is it?",
3179
- !severity && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3180
- "span",
3181
- {
3182
- style: {
3183
- marginLeft: "6px",
3184
- fontSize: "11px",
3185
- color: severityRefused ? "#ef4444" : t.textMuted
3186
- },
3187
- children: "\xB7 required"
3188
- }
3189
- )
3190
- ]
3191
- }
3192
- ),
3193
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3194
- "div",
3195
- {
3196
- role: "radiogroup",
3197
- "aria-label": "Severity",
3198
- style: { display: "flex", gap: "6px" },
3199
- children: SEVERITY_LEVELS.map((s) => {
3200
- const on = severity === s;
3201
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3202
- "button",
3203
- {
3204
- type: "button",
3205
- onClick: () => onSeverityChange(s),
3206
- role: "radio",
3207
- "aria-checked": on,
3208
- style: {
3209
- flex: 1,
3210
- minWidth: 0,
3211
- padding: "8px 2px",
3212
- borderRadius: "8px",
3213
- border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3214
- backgroundColor: on ? t.accent : "transparent",
3215
- color: on ? t.accentText : t.textMuted,
3216
- fontSize: "12px",
3217
- fontWeight: 600,
3218
- fontFamily: "inherit",
3219
- whiteSpace: "nowrap",
3220
- overflow: "hidden",
3221
- textOverflow: "ellipsis",
3222
- cursor: "pointer"
3223
- },
3224
- children: SEVERITY_LABELS[s]
3225
- },
3226
- s
3227
- );
3228
- })
3229
- }
3230
- )
3231
- ] }),
3232
- attachmentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3233
- attachmentCount,
3234
- " ",
3235
- attachmentCount === 1 ? "attachment" : "attachments",
3236
- " will be sent with this."
3237
- ] })
3238
- ]
3239
- }
3240
- ),
3241
- phase === "solved" && solved && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3242
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3243
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
4007
+ )
4008
+ ] }),
3244
4009
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3245
- "div",
4010
+ "span",
3246
4011
  {
3247
- style: {
3248
- padding: "9px 12px",
3249
- borderRadius: "12px 12px 12px 4px",
3250
- backgroundColor: t.bgSecondary,
3251
- fontSize: "13px",
3252
- lineHeight: 1.55,
3253
- maxWidth: "88%",
3254
- whiteSpace: "pre-wrap"
3255
- },
3256
- children: solved
4012
+ style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
4013
+ children: "Nothing was filed \u2014 you are all set."
3257
4014
  }
3258
4015
  )
3259
4016
  ] }),
3260
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3261
- "span",
4017
+ submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4018
+ "div",
3262
4019
  {
3263
- style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
3264
- children: "Nothing was filed \u2014 you are all set."
4020
+ style: {
4021
+ textAlign: "center",
4022
+ padding: "24px 0",
4023
+ color: t.accent,
4024
+ fontSize: "14px",
4025
+ fontWeight: 600
4026
+ },
4027
+ children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3265
4028
  }
3266
4029
  )
3267
- ] }),
3268
- submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3269
- "div",
3270
- {
3271
- style: {
3272
- textAlign: "center",
3273
- padding: "24px 0",
3274
- color: t.accent,
3275
- fontSize: "14px",
3276
- fontWeight: 600
3277
- },
3278
- children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3279
- }
3280
- )
3281
- ]
3282
- }
3283
- )
4030
+ ] })
4031
+ }
4032
+ )
4033
+ ]
3284
4034
  }
3285
4035
  ),
3286
- !submitted && phase !== "type" && phase !== "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
4036
+ !submitted && !showChats && phase !== "type" && phase !== "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3287
4037
  "div",
3288
4038
  {
3289
4039
  style: {
@@ -3600,8 +4350,8 @@ function AssistSheet({
3600
4350
  accept: canReadPdf ? ASSIST_ATTACHMENT_ACCEPT_WITH_PDF : ASSIST_ATTACHMENT_ACCEPT,
3601
4351
  multiple: true,
3602
4352
  onChange: (e) => {
3603
- var _a;
3604
- const picked = Array.from((_a = e.target.files) != null ? _a : []);
4353
+ var _a2;
4354
+ const picked = Array.from((_a2 = e.target.files) != null ? _a2 : []);
3605
4355
  if (picked.length) {
3606
4356
  stopAutoFile("changed attachments");
3607
4357
  const images = picked.filter((f) => f.type.startsWith("image/")).length;
@@ -4579,20 +5329,20 @@ function initialsOf(name) {
4579
5329
  }).join("");
4580
5330
  }
4581
5331
  function ReporterChip({
4582
- reporter,
5332
+ reporter: reporter2,
4583
5333
  t
4584
5334
  }) {
4585
5335
  var _a;
4586
5336
  const [avatarFailed, setAvatarFailed] = (0, import_react.useState)(false);
4587
- const avatarUrl = (reporter == null ? void 0 : reporter.avatarUrl) || "";
5337
+ const avatarUrl = (reporter2 == null ? void 0 : reporter2.avatarUrl) || "";
4588
5338
  (0, import_react.useEffect)(() => {
4589
5339
  setAvatarFailed(false);
4590
5340
  }, [avatarUrl]);
4591
- const name = ((_a = reporter == null ? void 0 : reporter.name) == null ? void 0 : _a.trim()) || "";
5341
+ const name = ((_a = reporter2 == null ? void 0 : reporter2.name) == null ? void 0 : _a.trim()) || "";
4592
5342
  const isAnonymous = !name;
4593
5343
  const initials2 = isAnonymous ? "" : initialsOf(name);
4594
5344
  const label = isAnonymous ? "Anonymous" : name;
4595
- const title = isAnonymous ? "No reporter identity was passed to Glitchgrab \u2014 this report won't be attributed to anyone" : [name, reporter == null ? void 0 : reporter.email, reporter == null ? void 0 : reporter.role].filter(Boolean).join(" \xB7 ");
5345
+ const title = isAnonymous ? "No reporter identity was passed to Glitchgrab \u2014 this report won't be attributed to anyone" : [name, reporter2 == null ? void 0 : reporter2.email, reporter2 == null ? void 0 : reporter2.role].filter(Boolean).join(" \xB7 ");
4596
5346
  const showAvatarImage = !!avatarUrl && !avatarFailed;
4597
5347
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
4598
5348
  "span",
@@ -4656,7 +5406,7 @@ function ReporterChip({
4656
5406
  },
4657
5407
  children: [
4658
5408
  label,
4659
- (reporter == null ? void 0 : reporter.role) ? ` \xB7 ${reporter.role}` : ""
5409
+ (reporter2 == null ? void 0 : reporter2.role) ? ` \xB7 ${reporter2.role}` : ""
4660
5410
  ]
4661
5411
  }
4662
5412
  )
@@ -4773,11 +5523,12 @@ function ReportDialog({
4773
5523
  types,
4774
5524
  showSeverity = true,
4775
5525
  captureScreenshot = captureDefaultScreenshot,
4776
- reporter,
5526
+ reporter: reporter2,
4777
5527
  onClose,
4778
5528
  headerSlot,
4779
5529
  reportGlitchgrabProblem,
4780
- layout = "modal"
5530
+ layout = "modal",
5531
+ chatHistoryKey: chatHistoryKey2 = null
4781
5532
  }) {
4782
5533
  var _a;
4783
5534
  const sheet = layout === "sheet";
@@ -4882,15 +5633,15 @@ function ReportDialog({
4882
5633
  });
4883
5634
  };
4884
5635
  applyInert();
4885
- const observer = new MutationObserver((mutations) => {
5636
+ const observer2 = new MutationObserver((mutations) => {
4886
5637
  const structural = mutations.some(
4887
5638
  (m) => m.addedNodes.length > 0 || m.removedNodes.length > 0
4888
5639
  );
4889
5640
  if (structural) applyInert();
4890
5641
  });
4891
- observer.observe(document.body, { childList: true, subtree: true });
5642
+ observer2.observe(document.body, { childList: true, subtree: true });
4892
5643
  return () => {
4893
- observer.disconnect();
5644
+ observer2.disconnect();
4894
5645
  inerted.forEach((el) => el.removeAttribute("inert"));
4895
5646
  };
4896
5647
  }, [isOpen]);
@@ -5542,6 +6293,9 @@ function ReportDialog({
5542
6293
  setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
5543
6294
  const queued = result.status === "QUEUED" && !!result.message;
5544
6295
  setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
6296
+ if (chatHistoryKey2 && aiConversationId) {
6297
+ markChatFiled(chatHistoryKey2, aiConversationId, `${getTypeLabel(reportType)} sent`);
6298
+ }
5545
6299
  setSubmitted(true);
5546
6300
  setDescription("");
5547
6301
  setScreenshots([]);
@@ -7361,7 +8115,7 @@ function ReportDialog({
7361
8115
  gap: "12px"
7362
8116
  },
7363
8117
  children: [
7364
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ReporterChip, { reporter, t }),
8118
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ReporterChip, { reporter: reporter2, t }),
7365
8119
  reportGlitchgrabProblem ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7366
8120
  "button",
7367
8121
  {
@@ -7641,6 +8395,7 @@ function ReportDialog({
7641
8395
  setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7642
8396
  },
7643
8397
  onConversationChange: setAiConversationId,
8398
+ historyKey: chatHistoryKey2,
7644
8399
  onFinish: handleClose,
7645
8400
  rating,
7646
8401
  onRatingChange: (value) => {
@@ -7648,7 +8403,7 @@ function ReportDialog({
7648
8403
  if (validationError) setValidationError(null);
7649
8404
  },
7650
8405
  projectSlot: headerSlot,
7651
- reporterName: (_a = reporter == null ? void 0 : reporter.name) != null ? _a : null,
8406
+ reporterName: (_a = reporter2 == null ? void 0 : reporter2.name) != null ? _a : null,
7652
8407
  description,
7653
8408
  onDescriptionChange: (value) => {
7654
8409
  setDescription(value);
@@ -9235,6 +9990,9 @@ function GlitchgrabProviderInner({
9235
9990
  onError,
9236
9991
  onReportSent
9237
9992
  });
9993
+ if (process.env.NODE_ENV !== "development") {
9994
+ installHydrationCapture(reportHydrationError);
9995
+ }
9238
9996
  (0, import_react6.useEffect)(() => {
9239
9997
  if (enableBreadcrumbs) {
9240
9998
  initBreadcrumbs(maxBreadcrumbs2, { responseBodyOrigins: responseBodyOrigins2 });
@@ -9290,7 +10048,7 @@ function GlitchgrabProviderInner({
9290
10048
  return matchesIgnorePatterns(message, ignoreErrors);
9291
10049
  };
9292
10050
  const handleError = (event) => {
9293
- var _a, _b, _c, _d, _e, _f, _g;
10051
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
9294
10052
  try {
9295
10053
  const isOpaqueCrossOrigin = event.message === "Script error." || event.message === "Script error" || !event.error && !event.filename;
9296
10054
  if (isOpaqueCrossOrigin) {
@@ -9305,6 +10063,7 @@ function GlitchgrabProviderInner({
9305
10063
  if (event.filename && EXTENSION_ORIGIN_RE.test(event.filename) || ((_a = event.error) == null ? void 0 : _a.stack) && EXTENSION_ORIGIN_RE.test(event.error.stack)) {
9306
10064
  return;
9307
10065
  }
10066
+ if (isHandledHydrationError(event.error)) return;
9308
10067
  incrementErrorCount();
9309
10068
  const context = captureContext(visitedPagesRef.current);
9310
10069
  const sig = computeSignature({
@@ -9330,6 +10089,9 @@ function GlitchgrabProviderInner({
9330
10089
  filename: (_e = event.filename) != null ? _e : "",
9331
10090
  lineno: String((_f = event.lineno) != null ? _f : ""),
9332
10091
  colno: String((_g = event.colno) != null ? _g : ""),
10092
+ // The root patch could not attach (React internals moved, or a
10093
+ // root it does not know): still label it and send the DOM context.
10094
+ ...isHydrationError((_h = event.error) != null ? _h : event.message) ? hydrationMetadata((_i = event.error) != null ? _i : event.message) : {},
9333
10095
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
9334
10096
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
9335
10097
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -9671,7 +10433,7 @@ function GlitchgrabProviderInner({
9671
10433
  } catch (e) {
9672
10434
  }
9673
10435
  }, [openReportDialog]);
9674
- const reporter = (0, import_react6.useMemo)(() => {
10436
+ const reporter2 = (0, import_react6.useMemo)(() => {
9675
10437
  var _a;
9676
10438
  if (!session) return null;
9677
10439
  const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
@@ -9742,7 +10504,8 @@ function GlitchgrabProviderInner({
9742
10504
  transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
9743
10505
  types,
9744
10506
  showSeverity,
9745
- reporter,
10507
+ reporter: reporter2,
10508
+ chatHistoryKey: chatHistoryKey("sdk", token, session == null ? void 0 : session.userId),
9746
10509
  headerSlot: projectName ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: 11, color: "#9aa0a6" }, children: [
9747
10510
  "Reporting to ",
9748
10511
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { style: { color: "#e5e5e5" }, children: projectName })