dewee-webchat 0.4.2 → 0.5.0-beta.1

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.
@@ -9,7 +9,7 @@ import {
9
9
  isLinkErrorCode,
10
10
  parseJobAccessLaunch,
11
11
  stripPageBlock
12
- } from "./chunk-SAUDVPFH.js";
12
+ } from "./chunk-WHHQ6FKK.js";
13
13
 
14
14
  // src/actions/directives.ts
15
15
  var DIRECTIVE_LANG = "dewee-action";
@@ -79,14 +79,44 @@ function stripDirectives(content) {
79
79
  return removeBlocks(lines, findBlocks(lines, DIRECTIVE_OPEN_RE));
80
80
  }
81
81
  function isActionResultMessage(content) {
82
- return RESULT_OPEN_RE.test(content.trimStart().split("\n", 1)[0] ?? "");
82
+ return /^\s*```dewee-action-result(?:\s|$)/.test(content);
83
+ }
84
+ function compactResultBodies(content) {
85
+ const open = /```dewee-action-result\s+/g;
86
+ const bodies = [];
87
+ let match;
88
+ while (match = open.exec(content)) {
89
+ const start = open.lastIndex;
90
+ if (content[start] !== "{") continue;
91
+ let depth = 0, quoted = false, escaped = false;
92
+ for (let i = start; i < content.length; i++) {
93
+ const c = content[i];
94
+ if (quoted) {
95
+ if (escaped) escaped = false;
96
+ else if (c === "\\") escaped = true;
97
+ else if (c === '"') quoted = false;
98
+ } else if (c === '"') quoted = true;
99
+ else if (c === "{") depth++;
100
+ else if (c === "}" && --depth === 0) {
101
+ const close = /^\s*```/.exec(content.slice(i + 1));
102
+ if (close) {
103
+ bodies.push(content.slice(start, i + 1));
104
+ open.lastIndex = i + 1 + close[0].length;
105
+ }
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ return bodies;
83
111
  }
84
112
  function parseActionResults(content) {
85
113
  const lines = content.replace(/\r\n?/g, "\n").split("\n");
86
114
  const out = [];
87
- for (const b of findBlocks(lines, RESULT_OPEN_RE)) {
115
+ const blocks = findBlocks(lines, RESULT_OPEN_RE);
116
+ const bodies = blocks.length ? blocks.map((b) => b.body.join("\n")) : compactResultBodies(content);
117
+ for (const body of bodies) {
88
118
  try {
89
- const r = JSON.parse(b.body.join("\n"));
119
+ const r = JSON.parse(body);
90
120
  if (r && typeof r.id === "string" && ["ok", "denied", "error"].includes(r.status)) out.push(r);
91
121
  } catch {
92
122
  }
@@ -584,6 +614,8 @@ var ApiClient = class {
584
614
  session = null;
585
615
  config = null;
586
616
  sessionPromise = null;
617
+ supersededSessions = /* @__PURE__ */ new Map();
618
+ sessionGeneration = 0;
587
619
  url(action, qs) {
588
620
  const base = this.o.endpoint.replace(/\/+$/, "");
589
621
  const q = qs ? "?" + new URLSearchParams(qs).toString() : "";
@@ -618,7 +650,7 @@ var ApiClient = class {
618
650
  h[HEADER_GATEWAY_TOKEN] = this.o.gatewayToken;
619
651
  }
620
652
  if (auth) {
621
- const s = this.session && this.fresh(this.session) ? this.session : await this.ensureSession();
653
+ const s = await this.ensureSession();
622
654
  h.authorization = `Bearer ${s.token}`;
623
655
  }
624
656
  return h;
@@ -632,16 +664,24 @@ var ApiClient = class {
632
664
  }
633
665
  return body;
634
666
  }
635
- async getConfig(force = false) {
667
+ /** Obtain a short-lived, URL-bound image capability without exposing the visitor bearer in src. */
668
+ async imageUrl(source, signal) {
669
+ const res = await this.request(this.url("image-ticket"), { method: "POST", headers: await this.headers(true), body: JSON.stringify({ url: source }), redirect: "error", signal });
670
+ const data = await this.parse(res);
671
+ if (typeof data.ticket !== "string" || !/^[A-Za-z0-9_-]{1,8192}$/.test(data.ticket)) throw new Error("Invalid image ticket");
672
+ return this.url("image", { ticket: data.ticket });
673
+ }
674
+ async getConfig(force = false, probePageContext = false) {
636
675
  if (this.config && !force) return this.config;
637
- this.config = await this.parse(await this.request(this.url("config"), { headers: await this.headers() }));
676
+ this.config = await this.parse(await this.request(this.url("config", probePageContext ? { pageContext: "1" } : void 0), { headers: await this.headers() }));
638
677
  return this.config;
639
678
  }
640
679
  /** When each signed-in session should be refreshed, on this browser's clock. */
641
680
  refreshAt = /* @__PURE__ */ new WeakMap();
642
681
  /**
643
682
  * Session reuse. A signed-in session follows its schedule (see `scheduleRefresh`). An anonymous
644
- * token is refreshed once less than 10% of its lifetime (at most 24 h, at least 30 s) remains; a
683
+ * token is refreshed once less than 10% of its lifetime (at most 24 h, at least 30 s, and no more
684
+ * than half the lifetime) remains; a
645
685
  * stored token from before `obtainedAt` existed uses the 24 h margin. When the backend reports the
646
686
  * lifetime (`expiresIn`), it is counted from receipt, so a skewed browser clock does not matter.
647
687
  */
@@ -650,7 +690,7 @@ var ApiClient = class {
650
690
  const reported = typeof s.expiresIn === "number" && s.expiresIn >= 0 && s.obtainedAt;
651
691
  const end = reported ? s.obtainedAt + s.expiresIn : s.expiresAt;
652
692
  const lifetime = reported ? s.expiresIn : s.obtainedAt ? s.expiresAt - s.obtainedAt : Infinity;
653
- const margin = Math.min(864e5, Math.max(3e4, lifetime * 0.1));
693
+ const margin = Math.min(864e5, lifetime / 2, Math.max(3e4, lifetime * 0.1));
654
694
  return end - Date.now() > margin;
655
695
  }
656
696
  /**
@@ -668,7 +708,11 @@ var ApiClient = class {
668
708
  if (!saved) return null;
669
709
  try {
670
710
  const s = JSON.parse(saved);
671
- return s.verified ? null : s;
711
+ if (s.verified) {
712
+ store.del(this.storageKey);
713
+ return null;
714
+ }
715
+ return s;
672
716
  } catch {
673
717
  return null;
674
718
  }
@@ -679,21 +723,35 @@ var ApiClient = class {
679
723
  }
680
724
  /** Returns a valid visitor session, creating or refreshing it when needed. */
681
725
  ensureSession() {
726
+ if (this.sessionPromise) return this.sessionPromise;
682
727
  if (this.session && this.fresh(this.session)) return Promise.resolve(this.session);
683
- return this.sessionPromise ??= this.loadSession().finally(() => this.sessionPromise = null);
728
+ const generation = this.sessionGeneration;
729
+ const promise = this.loadSession(generation).catch((error) => {
730
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
731
+ throw error;
732
+ }).finally(() => {
733
+ if (this.sessionPromise === promise) this.sessionPromise = null;
734
+ this.supersededSessions.delete(generation);
735
+ });
736
+ return this.sessionPromise = promise;
684
737
  }
685
- async loadSession() {
738
+ async loadSession(generation) {
686
739
  if (!this.session) {
687
740
  const s = this.stored();
688
741
  if (s) {
689
- if (this.fresh(s) && !(await this.getConfig()).identity) return this.session = s;
742
+ if (this.fresh(s)) {
743
+ const cfg = await this.getConfig();
744
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
745
+ if (!cfg.identity) return this.session = s;
746
+ }
690
747
  this.session = s;
691
748
  }
692
749
  }
693
- return this.createSession();
750
+ return this.createSession(generation);
694
751
  }
695
- async createSession() {
752
+ async createSession(generation) {
696
753
  const cfg = await this.getConfig();
754
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
697
755
  const body = {};
698
756
  const previous = this.session && !this.session.verified ? this.session : this.stored();
699
757
  if (previous?.token) body.token = previous.token;
@@ -717,9 +775,12 @@ var ApiClient = class {
717
775
  s = await post();
718
776
  }
719
777
  } catch (e) {
778
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
779
+ this.session = null;
720
780
  if (e instanceof ApiError && e.status === 401) tabStore.del(this.reuseKey);
721
781
  throw e;
722
782
  }
783
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
723
784
  const now = Date.now();
724
785
  const held = { ...s, obtainedAt: now };
725
786
  this.session = held;
@@ -749,8 +810,14 @@ var ApiClient = class {
749
810
  * stored anonymous visitor id is kept, so an anonymous visitor's history survives.
750
811
  */
751
812
  async renewSession() {
813
+ const previousGeneration = this.sessionGeneration;
814
+ const previous = this.sessionPromise;
815
+ this.sessionGeneration++;
752
816
  this.session = null;
753
- return this.ensureSession();
817
+ this.sessionPromise = null;
818
+ const next = this.ensureSession();
819
+ if (previous) this.supersededSessions.set(previousGeneration, next);
820
+ return next;
754
821
  }
755
822
  /** Requests a single-use launch ticket. Does not refresh or swap the signed-in session after refusal. */
756
823
  async jobAccess(signal) {
@@ -1078,7 +1145,10 @@ var en = {
1078
1145
  jobAccessOpened: "Job access opened in a new window. Review and confirm any permissions there.",
1079
1146
  jobAccessUnavailable: "Job access could not be opened. Sign in again or retry.",
1080
1147
  jobAccessPopupBlocked: "Allow popups for this site, then choose Job access again.",
1081
- jobAccessPopupClosed: "The window was closed. Choose Job access again to retry."
1148
+ jobAccessPopupClosed: "The window was closed. Choose Job access again to retry.",
1149
+ pageShare: "Share this page with the assistant for this message",
1150
+ pageChanged: "The page changed. Review it and choose to share again before sending.",
1151
+ pageUnavailable: "Page details are unavailable. You can send without them."
1082
1152
  };
1083
1153
  var vi = {
1084
1154
  title: "Tr\u1EE3 l\xFD",
@@ -1138,7 +1208,10 @@ var vi = {
1138
1208
  jobAccessOpened: "\u0110\xE3 m\u1EDF c\u1EEDa s\u1ED5 quy\u1EC1n c\xF4ng vi\u1EC7c. H\xE3y xem l\u1EA1i v\xE0 x\xE1c nh\u1EADn quy\u1EC1n t\u1EA1i \u0111\xF3.",
1139
1209
  jobAccessUnavailable: "Ch\u01B0a m\u1EDF \u0111\u01B0\u1EE3c quy\u1EC1n c\xF4ng vi\u1EC7c. H\xE3y \u0111\u0103ng nh\u1EADp l\u1EA1i ho\u1EB7c th\u1EED l\u1EA1i.",
1140
1210
  jobAccessPopupBlocked: "Cho ph\xE9p c\u1EEDa s\u1ED5 b\u1EADt l\xEAn cho trang n\xE0y r\u1ED3i ch\u1ECDn Quy\u1EC1n c\xF4ng vi\u1EC7c l\u1EA7n n\u1EEFa.",
1141
- jobAccessPopupClosed: "C\u1EEDa s\u1ED5 \u0111\xE3 \u0111\xF3ng. Ch\u1ECDn Quy\u1EC1n c\xF4ng vi\u1EC7c l\u1EA7n n\u1EEFa \u0111\u1EC3 th\u1EED l\u1EA1i."
1211
+ jobAccessPopupClosed: "C\u1EEDa s\u1ED5 \u0111\xE3 \u0111\xF3ng. Ch\u1ECDn Quy\u1EC1n c\xF4ng vi\u1EC7c l\u1EA7n n\u1EEFa \u0111\u1EC3 th\u1EED l\u1EA1i.",
1212
+ pageShare: "Chia s\u1EBB trang n\xE0y v\u1EDBi tr\u1EE3 l\xFD trong tin nh\u1EAFn n\xE0y",
1213
+ pageChanged: "Trang \u0111\xE3 thay \u0111\u1ED5i. H\xE3y xem l\u1EA1i v\xE0 ch\u1ECDn chia s\u1EBB tr\u01B0\u1EDBc khi g\u1EEDi.",
1214
+ pageUnavailable: "Kh\xF4ng th\u1EC3 \u0111\u1ECDc n\u1ED9i dung trang. B\u1EA1n v\u1EABn c\xF3 th\u1EC3 g\u1EEDi tin nh\u1EAFn."
1142
1215
  };
1143
1216
  function pickStrings(locale) {
1144
1217
  const l = (locale || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "") || "en").toLowerCase();
@@ -1158,6 +1231,40 @@ var icons = {
1158
1231
  };
1159
1232
 
1160
1233
  // src/widget/markdown.ts
1234
+ var imageObservers = /* @__PURE__ */ new WeakMap();
1235
+ var imageLoads = /* @__PURE__ */ new WeakMap();
1236
+ function disposeMarkdownImages(root) {
1237
+ for (const image of root.querySelectorAll("img.md-image")) {
1238
+ imageObservers.get(image)?.disconnect();
1239
+ imageObservers.delete(image);
1240
+ imageLoads.get(image)?.abort();
1241
+ imageLoads.delete(image);
1242
+ }
1243
+ }
1244
+ function safeImageSrc(doc, raw, allowed) {
1245
+ try {
1246
+ if (!raw || /[\u0000-\u0020\u007f\\]/.test(raw) || raw.startsWith("//")) return null;
1247
+ const decoded = decodeURIComponent(raw);
1248
+ if (/[\u0000-\u0020\u007f\\]/.test(decoded) || decoded.startsWith("//")) return null;
1249
+ const page = new URL(doc.defaultView?.location.href ?? doc.baseURI);
1250
+ const url = new URL(raw, doc.baseURI);
1251
+ if (url.username || url.password || !["http:", "https:"].includes(url.protocol)) return null;
1252
+ if (/\.svg$/i.test(decodeURIComponent(url.pathname))) return null;
1253
+ if (url.origin === page.origin) return url.href;
1254
+ if (url.protocol !== "https:") return null;
1255
+ const trusted = allowed.some((item) => {
1256
+ try {
1257
+ const origin = new URL(item);
1258
+ return origin.protocol === "https:" && !origin.username && !origin.password && origin.href === origin.origin + "/" && origin.origin === url.origin;
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ });
1263
+ return trusted ? url.href : null;
1264
+ } catch {
1265
+ return null;
1266
+ }
1267
+ }
1161
1268
  function el(doc, tag, cls) {
1162
1269
  const e = doc.createElement(tag);
1163
1270
  if (cls) e.className = cls;
@@ -1169,45 +1276,87 @@ function safeHref(url) {
1169
1276
  if (/^[/#?]/.test(u) && !u.startsWith("//")) return u;
1170
1277
  return null;
1171
1278
  }
1172
- var INLINE_SRC = /(`+)([\s\S]*?[^`])\1(?!`)|\*\*([^*]+?)\*\*|__([^_]+?)__|~~([^~]+?)~~|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/;
1173
- function renderInline(doc, text, parent) {
1279
+ var INLINE_SRC = /\\!\[[^\]]*\]\([^)\s]+\)|(`+)([\s\S]*?[^`])\1(?!`)|\*\*([^*]+?)\*\*|__([^_]+?)__|~~([^~]+?)~~|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|(?<!\\)!\[([^\]]*)\]\(([^)\s]+)\)|\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/;
1280
+ function renderInline(doc, text, parent, options = {}) {
1174
1281
  const re = new RegExp(INLINE_SRC.source, "g");
1175
1282
  let last = 0;
1176
1283
  for (let m; m = re.exec(text); ) {
1177
1284
  if (m.index > last) parent.appendChild(doc.createTextNode(text.slice(last, m.index)));
1178
1285
  last = m.index + m[0].length;
1179
- if (m[2] !== void 0) {
1286
+ if (m[0].startsWith("\\!")) {
1287
+ parent.appendChild(doc.createTextNode(m[0].slice(1)));
1288
+ } else if (m[2] !== void 0) {
1180
1289
  const c = el(doc, "code");
1181
1290
  c.textContent = m[2];
1182
1291
  parent.appendChild(c);
1183
1292
  } else if (m[3] !== void 0 || m[4] !== void 0) {
1184
1293
  const s = el(doc, "strong");
1185
- renderInline(doc, m[3] ?? m[4], s);
1294
+ renderInline(doc, m[3] ?? m[4], s, options);
1186
1295
  parent.appendChild(s);
1187
1296
  } else if (m[5] !== void 0) {
1188
1297
  const s = el(doc, "del");
1189
- renderInline(doc, m[5], s);
1298
+ renderInline(doc, m[5], s, options);
1190
1299
  parent.appendChild(s);
1191
1300
  } else if (m[6] !== void 0 || m[7] !== void 0) {
1192
1301
  const s = el(doc, "em");
1193
- renderInline(doc, m[6] ?? m[7], s);
1302
+ renderInline(doc, m[6] ?? m[7], s, options);
1194
1303
  parent.appendChild(s);
1195
1304
  } else if (m[8] !== void 0) {
1196
- const href = safeHref(m[9]);
1305
+ const original = options.deferImages ? null : safeImageSrc(doc, m[9], options.imageOrigins ?? []);
1306
+ if (!original || !options.imageProxy) parent.appendChild(doc.createTextNode(m[8]));
1307
+ else {
1308
+ const img = el(doc, "img", "md-image");
1309
+ img.alt = m[8];
1310
+ img.setAttribute("loading", "lazy");
1311
+ img.setAttribute("decoding", "async");
1312
+ img.referrerPolicy = "no-referrer";
1313
+ let retried = false;
1314
+ const load = () => {
1315
+ const controller = new AbortController();
1316
+ imageLoads.set(img, controller);
1317
+ return options.imageProxy(original, controller.signal).then((src) => {
1318
+ if (!controller.signal.aborted && img.isConnected) img.src = src;
1319
+ }).catch(() => {
1320
+ if (!controller.signal.aborted && img.isConnected) img.replaceWith(doc.createTextNode(img.alt));
1321
+ }).finally(() => {
1322
+ if (imageLoads.get(img) === controller) imageLoads.delete(img);
1323
+ });
1324
+ };
1325
+ img.addEventListener("error", () => {
1326
+ if (!retried) {
1327
+ retried = true;
1328
+ void load();
1329
+ } else img.replaceWith(doc.createTextNode(img.alt));
1330
+ });
1331
+ parent.appendChild(img);
1332
+ const Observer = doc.defaultView?.IntersectionObserver;
1333
+ if (Observer) {
1334
+ const observer = new Observer((entries) => {
1335
+ if (!entries.some((entry) => entry.isIntersecting)) return;
1336
+ observer.disconnect();
1337
+ imageObservers.delete(img);
1338
+ if (img.parentNode) void load();
1339
+ }, { rootMargin: "200px" });
1340
+ imageObservers.set(img, observer);
1341
+ observer.observe(img);
1342
+ } else void load();
1343
+ }
1344
+ } else if (m[10] !== void 0) {
1345
+ const href = safeHref(m[11]);
1197
1346
  if (href) {
1198
1347
  const a = el(doc, "a");
1199
1348
  a.href = href;
1200
1349
  a.target = "_blank";
1201
1350
  a.rel = "noopener noreferrer nofollow";
1202
- renderInline(doc, m[8], a);
1351
+ renderInline(doc, m[10], a, options);
1203
1352
  parent.appendChild(a);
1204
- } else parent.appendChild(doc.createTextNode(m[8]));
1205
- } else if (m[10] !== void 0) {
1353
+ } else parent.appendChild(doc.createTextNode(m[10]));
1354
+ } else if (m[12] !== void 0) {
1206
1355
  const a = el(doc, "a");
1207
- a.href = m[10];
1356
+ a.href = m[12];
1208
1357
  a.target = "_blank";
1209
1358
  a.rel = "noopener noreferrer nofollow";
1210
- a.textContent = m[10];
1359
+ a.textContent = m[12];
1211
1360
  parent.appendChild(a);
1212
1361
  }
1213
1362
  }
@@ -1215,7 +1364,7 @@ function renderInline(doc, text, parent) {
1215
1364
  }
1216
1365
  var isTableSep = (l) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(l);
1217
1366
  var splitRow = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
1218
- function renderMarkdown(doc, src) {
1367
+ function renderMarkdown(doc, src, options = {}) {
1219
1368
  const frag = doc.createDocumentFragment();
1220
1369
  const lines = src.replace(/\r\n?/g, "\n").split("\n");
1221
1370
  let i = 0;
@@ -1243,7 +1392,7 @@ function renderMarkdown(doc, src) {
1243
1392
  if (h) {
1244
1393
  const tag = `h${Math.min(6, h[1].length + 2)}`;
1245
1394
  const e = el(doc, tag);
1246
- renderInline(doc, h[2], e);
1395
+ renderInline(doc, h[2], e, options);
1247
1396
  frag.appendChild(e);
1248
1397
  i++;
1249
1398
  continue;
@@ -1260,7 +1409,7 @@ function renderMarkdown(doc, src) {
1260
1409
  const tr = el(doc, "tr");
1261
1410
  for (const c of splitRow(line)) {
1262
1411
  const th = el(doc, "th");
1263
- renderInline(doc, c, th);
1412
+ renderInline(doc, c, th, options);
1264
1413
  tr.appendChild(th);
1265
1414
  }
1266
1415
  thead.appendChild(tr);
@@ -1271,7 +1420,7 @@ function renderMarkdown(doc, src) {
1271
1420
  const r = el(doc, "tr");
1272
1421
  for (const c of splitRow(lines[i])) {
1273
1422
  const td = el(doc, "td");
1274
- renderInline(doc, c, td);
1423
+ renderInline(doc, c, td, options);
1275
1424
  r.appendChild(td);
1276
1425
  }
1277
1426
  tbody.appendChild(r);
@@ -1286,7 +1435,7 @@ function renderMarkdown(doc, src) {
1286
1435
  const body = [];
1287
1436
  while (i < lines.length && /^\s*>/.test(lines[i])) body.push(lines[i++].replace(/^\s*>\s?/, ""));
1288
1437
  const q = el(doc, "blockquote");
1289
- q.appendChild(renderMarkdown(doc, body.join("\n")));
1438
+ q.appendChild(renderMarkdown(doc, body.join("\n"), options));
1290
1439
  frag.appendChild(q);
1291
1440
  continue;
1292
1441
  }
@@ -1302,11 +1451,11 @@ function renderMarkdown(doc, src) {
1302
1451
  if (m[1].length > baseIndent) {
1303
1452
  const sub = [];
1304
1453
  while (i < lines.length && (/^\s+/.test(lines[i]) && lines[i].match(/^(\s*)/)[1].length > baseIndent)) sub.push(lines[i++].slice(baseIndent + 2));
1305
- list.lastElementChild?.appendChild(renderMarkdown(doc, sub.join("\n")));
1454
+ list.lastElementChild?.appendChild(renderMarkdown(doc, sub.join("\n"), options));
1306
1455
  continue;
1307
1456
  }
1308
1457
  const item = el(doc, "li");
1309
- renderInline(doc, m[3], item);
1458
+ renderInline(doc, m[3], item, options);
1310
1459
  list.appendChild(item);
1311
1460
  i++;
1312
1461
  }
@@ -1319,7 +1468,7 @@ function renderMarkdown(doc, src) {
1319
1468
  const p = el(doc, "p");
1320
1469
  para.forEach((l, idx) => {
1321
1470
  if (idx) p.appendChild(el(doc, "br"));
1322
- renderInline(doc, l, p);
1471
+ renderInline(doc, l, p, options);
1323
1472
  });
1324
1473
  frag.appendChild(p);
1325
1474
  }
@@ -1517,6 +1666,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
1517
1666
  .md li { margin: 3px 0; }
1518
1667
  .md h3, .md h4, .md h5, .md h6 { margin: 14px 0 6px; font-size: 15px; line-height: 1.35; }
1519
1668
  .md a { color: var(--dw-accent); text-decoration: underline; text-underline-offset: 2px; }
1669
+ .md .md-image { display: block; max-width: 100%; max-height: 320px; min-height: 48px; width: auto; height: auto; object-fit: contain; border-radius: 8px; }
1520
1670
  .md code { font-family: var(--dw-mono); font-size: .86em; background: var(--dw-surface); padding: 1.5px 5px; border-radius: 5px; }
1521
1671
  .md pre { background: #12132a; color: #e6e6f5; padding: 12px 14px; border-radius: 12px; overflow-x: auto; font-size: 12.5px; line-height: 1.55; }
1522
1672
  .md pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
@@ -1611,6 +1761,9 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
1611
1761
 
1612
1762
  /* ---------- composer ---------- */
1613
1763
  .composer { padding: 10px 12px 12px; border-top: 1px solid var(--dw-border); background: var(--dw-bg); }
1764
+ .page-context { font-size: 12px; margin-bottom: 8px; }
1765
+ .page-context label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
1766
+ .page-preview { max-height: 96px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; padding: 6px; margin: 6px 0; border: 1px solid var(--dw-border); border-radius: 6px; font: inherit; }
1614
1767
  .box {
1615
1768
  display: flex; align-items: flex-end; gap: 8px; border: 1px solid var(--dw-border); border-radius: 14px;
1616
1769
  padding: 6px 6px 6px 12px; background: var(--dw-bg); transition: border-color .15s, box-shadow .15s;
@@ -1665,6 +1818,10 @@ var ATTRS = [
1665
1818
  "height",
1666
1819
  "z-index",
1667
1820
  "dom-actions-root",
1821
+ "image-origins",
1822
+ "page-context",
1823
+ "context-selector",
1824
+ "exclude-selectors",
1668
1825
  ...BOOL_ATTRS
1669
1826
  ];
1670
1827
  var camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -1712,12 +1869,17 @@ var DeweeChatElement = class extends ElementBase {
1712
1869
  api = null;
1713
1870
  apiKey = "";
1714
1871
  server = null;
1872
+ sharedPage;
1873
+ preparingPage = false;
1874
+ pagePending = false;
1875
+ pageGeneration = 0;
1715
1876
  agentId = "";
1716
1877
  conversationId = "";
1717
1878
  messages = [];
1718
1879
  running = null;
1719
1880
  jobAccessLaunch = null;
1720
1881
  activated = false;
1882
+ probedPage = false;
1721
1883
  strings = pickStrings();
1722
1884
  mql = null;
1723
1885
  registry = new ActionRegistry();
@@ -1742,6 +1904,8 @@ var DeweeChatElement = class extends ElementBase {
1742
1904
  if (v === null) continue;
1743
1905
  if (BOOL_ATTRS.includes(a)) c[camel(a)] = parseBool(v);
1744
1906
  else if (a === "suggestions") c.suggestions = v.trim().startsWith("[") ? safeJson(v, []) : v.split("|").map((s) => s.trim()).filter(Boolean);
1907
+ else if (a === "image-origins") c.imageOrigins = v.trim().startsWith("[") ? safeJson(v, []) : v.split(",").map((s) => s.trim()).filter(Boolean);
1908
+ else if (a === "exclude-selectors") c.excludeSelectors = v.trim().startsWith("[") ? safeJson(v, []) : v.split(",").map((s) => s.trim()).filter(Boolean);
1745
1909
  else if (a === "z-index") c.zIndex = Number(v);
1746
1910
  else c[camel(a)] = v;
1747
1911
  }
@@ -1749,13 +1913,14 @@ var DeweeChatElement = class extends ElementBase {
1749
1913
  }
1750
1914
  /** Updates configuration at runtime (wins over attributes). */
1751
1915
  configure(partial) {
1752
- const { actions, confirm, getPageContext, headers, fetch, domActionsRoot, ...rest } = partial;
1916
+ const { actions, confirm, getPageContext, headers, fetch, domActionsRoot, contextProvider, ...rest } = partial;
1753
1917
  const has = (k) => Object.prototype.hasOwnProperty.call(partial, k);
1754
1918
  if (has("actions")) this.setPropActions(actions ?? []);
1755
1919
  if (has("confirm")) this.hooks.confirm = confirm;
1756
1920
  if (has("getPageContext")) this.hooks.getPageContext = getPageContext;
1757
1921
  if (has("headers")) this.hooks.headers = headers;
1758
1922
  if (has("fetch")) this.hooks.fetch = fetch;
1923
+ if (has("contextProvider")) this.hooks.contextProvider = contextProvider;
1759
1924
  if (has("domActionsRoot")) {
1760
1925
  if (domActionsRoot === void 0) delete this.hooks.domActionsRoot;
1761
1926
  else this.hooks.domActionsRoot = domActionsRoot;
@@ -1832,14 +1997,47 @@ var DeweeChatElement = class extends ElementBase {
1832
1997
  /** Sends a message programmatically (opens the widget first). */
1833
1998
  async send(text) {
1834
1999
  const message = text.trim();
1835
- if (!message || this.running || this.cooldown.active) return;
2000
+ if (!message || this.running || this.preparingPage || this.cooldown.active) return;
1836
2001
  if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
1837
2002
  await this.activate();
2003
+ let attachment;
2004
+ if (this.els.pageChoice.checked) {
2005
+ if (this.pagePending) {
2006
+ this.showNotice(this.strings.pageUnavailable);
2007
+ return;
2008
+ }
2009
+ this.preparingPage = true;
2010
+ const generation = this.pageGeneration;
2011
+ try {
2012
+ const previous = this.sharedPage;
2013
+ const current = await this.captureCurrentPage();
2014
+ if (!this.els.pageChoice.checked || generation !== this.pageGeneration) return;
2015
+ if (!previous || !current || this.pageFingerprint(previous) !== this.pageFingerprint(current)) {
2016
+ this.sharedPage = current;
2017
+ this.renderPagePreview();
2018
+ this.els.pageChoice.checked = false;
2019
+ this.showNotice(this.strings.pageChanged);
2020
+ return;
2021
+ }
2022
+ attachment = current;
2023
+ } catch {
2024
+ this.sharedPage = void 0;
2025
+ this.els.pageChoice.checked = false;
2026
+ this.renderPagePreview();
2027
+ this.showNotice(this.strings.pageUnavailable);
2028
+ return;
2029
+ } finally {
2030
+ this.preparingPage = false;
2031
+ }
2032
+ this.els.pageChoice.checked = false;
2033
+ this.sharedPage = void 0;
2034
+ this.renderPagePreview();
2035
+ }
1838
2036
  this.els.input.value = "";
1839
2037
  this.autosize();
1840
2038
  let next = { message, hidden: false };
1841
2039
  for (let round = 0; next; round++) {
1842
- const reply = await this.runTurn(next.message, next.hidden);
2040
+ const reply = await this.runTurn(next.message, next.hidden, round === 0 ? attachment : void 0);
1843
2041
  next = null;
1844
2042
  if (reply.status !== "done" || reply.stopped || !this.actionRegistry().size) break;
1845
2043
  const { calls } = parseDirectives(reply.content);
@@ -1858,7 +2056,7 @@ var DeweeChatElement = class extends ElementBase {
1858
2056
  }
1859
2057
  }
1860
2058
  /** One request/response with the agent. */
1861
- async runTurn(message, hidden) {
2059
+ async runTurn(message, hidden, attachment) {
1862
2060
  this.pushMessage({ role: "user", content: message, hidden });
1863
2061
  const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
1864
2062
  this.pushMessage(reply);
@@ -1866,7 +2064,7 @@ var DeweeChatElement = class extends ElementBase {
1866
2064
  this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId, auto: hidden || void 0 });
1867
2065
  try {
1868
2066
  await this.client().ensureSession();
1869
- await this.client().chat(this.chatBody(message), (e) => this.onStreamEvent(reply, e), this.running.signal);
2067
+ await this.client().chat({ ...this.chatBody(message), ...attachment ? { page_context: attachment } : {} }, (e) => this.onStreamEvent(reply, e), this.running.signal);
1870
2068
  if (reply.status === "streaming") reply.status = "done";
1871
2069
  } catch (e) {
1872
2070
  if (e.name === "AbortError") {
@@ -2102,6 +2300,12 @@ var DeweeChatElement = class extends ElementBase {
2102
2300
  }
2103
2301
  }
2104
2302
  disconnectedCallback() {
2303
+ this.pageGeneration++;
2304
+ this.sharedPage = void 0;
2305
+ this.els.pageChoice.checked = false;
2306
+ this.renderPagePreview();
2307
+ disposeMarkdownImages(this.root);
2308
+ for (const message of this.messages) if (message.view) message.view.mdKey = void 0;
2105
2309
  this.jobAccessLaunch?.abort();
2106
2310
  this.stop();
2107
2311
  this.mql?.removeEventListener("change", this.onScheme);
@@ -2183,6 +2387,8 @@ var DeweeChatElement = class extends ElementBase {
2183
2387
  e.jobAccess.setAttribute("aria-label", s.jobAccess);
2184
2388
  e.jobAccess.title = s.jobAccess;
2185
2389
  e.input.placeholder = c.placeholder || s.placeholder;
2390
+ e.pageLabel.textContent = s.pageShare;
2391
+ this.updatePageChoice();
2186
2392
  e.foot.hidden = !!c.hideBranding;
2187
2393
  e.foot.querySelector(".by").textContent = s.poweredBy + " ";
2188
2394
  this.renderSubtitle();
@@ -2190,11 +2396,16 @@ var DeweeChatElement = class extends ElementBase {
2190
2396
  if (this.activated && this.api && this.apiKey !== [c.endpoint, c.gatewayUrl, c.gatewayToken].join("|")) {
2191
2397
  this.activated = false;
2192
2398
  this.server = null;
2399
+ this.probedPage = false;
2400
+ this.activate();
2401
+ } else if (this.activated && !this.probedPage && (c.pageContext === "metadata" || c.pageContext === "content")) {
2402
+ this.activated = false;
2193
2403
  this.activate();
2194
2404
  } else if (this.activated && c.agent && c.agent !== this.agentId && this.server) {
2195
2405
  this.switchAgent(c.agent);
2196
2406
  } else if (this.activated) {
2197
- this.renderLog();
2407
+ if (!this.messages.length) this.renderLog();
2408
+ else for (const message of this.messages) this.updateMessage(message);
2198
2409
  }
2199
2410
  }
2200
2411
  renderShell() {
@@ -2215,6 +2426,7 @@ var DeweeChatElement = class extends ElementBase {
2215
2426
  <div class="log" part="log" role="log" aria-live="polite"></div>
2216
2427
  <div class="challenge"><slot name="challenge"></slot></div>
2217
2428
  <form class="composer" part="composer">
2429
+ <div class="page-context" hidden><label><input type="checkbox" class="page-choice"> <span class="page-label"></span></label><pre class="page-preview" hidden></pre></div>
2218
2430
  <div class="cooldown" part="cooldown" aria-hidden="true" hidden></div>
2219
2431
  <div class="sr-only announcer" role="status" aria-live="polite"></div>
2220
2432
  <div class="box">
@@ -2241,7 +2453,11 @@ var DeweeChatElement = class extends ElementBase {
2241
2453
  close: $(".close"),
2242
2454
  foot: $(".foot"),
2243
2455
  cooldown: $(".cooldown"),
2244
- announcer: $(".announcer")
2456
+ announcer: $(".announcer"),
2457
+ pageChoice: $(".page-choice"),
2458
+ pageWrap: $(".page-context"),
2459
+ pageLabel: $(".page-label"),
2460
+ pagePreview: $(".page-preview")
2245
2461
  };
2246
2462
  const e = this.els;
2247
2463
  e.launcher.addEventListener("click", () => this.toggle());
@@ -2253,6 +2469,9 @@ var DeweeChatElement = class extends ElementBase {
2253
2469
  e.jobAccess.addEventListener("click", () => {
2254
2470
  void this.openJobAccess();
2255
2471
  });
2472
+ e.pageChoice.addEventListener("change", () => {
2473
+ void this.choosePage();
2474
+ });
2256
2475
  e.select.addEventListener("change", () => this.switchAgent(e.select.value));
2257
2476
  $("form").addEventListener("submit", (ev) => {
2258
2477
  ev.preventDefault();
@@ -2277,6 +2496,80 @@ var DeweeChatElement = class extends ElementBase {
2277
2496
  t.style.height = "auto";
2278
2497
  t.style.height = Math.min(t.scrollHeight, 140) + "px";
2279
2498
  }
2499
+ updatePageChoice() {
2500
+ const mode = this.config.pageContext;
2501
+ const available = (mode === "metadata" || mode === "content") && this.server?.pageContextVersions?.includes(1) === true;
2502
+ this.els.pageWrap.hidden = !available;
2503
+ if (!available) {
2504
+ this.pageGeneration++;
2505
+ this.pagePending = false;
2506
+ this.els.pageChoice.checked = false;
2507
+ this.sharedPage = void 0;
2508
+ this.renderPagePreview();
2509
+ }
2510
+ }
2511
+ async captureCurrentPage() {
2512
+ const c = this.config;
2513
+ if (c.pageContext !== "metadata" && c.pageContext !== "content" || !this.server?.pageContextVersions?.includes(1)) return void 0;
2514
+ const before = document.location.href;
2515
+ const { capturePageContext, providedPageContext } = await import("./page-context-H3ZROFAV.js");
2516
+ const provider = this.hooks.contextProvider;
2517
+ const configKey = JSON.stringify([c.pageContext, c.contextSelector, c.excludeSelectors]);
2518
+ const base = capturePageContext(document, { mode: provider ? "metadata" : c.pageContext, selector: c.contextSelector, excludeSelectors: c.excludeSelectors });
2519
+ let result = base;
2520
+ if (base && provider) {
2521
+ let timer;
2522
+ try {
2523
+ const supplied = await Promise.race([provider(), new Promise((_, reject) => {
2524
+ timer = setTimeout(() => reject(new Error("Page context timed out")), 5e3);
2525
+ })]);
2526
+ result = providedPageContext(supplied, base, c.pageContext);
2527
+ } finally {
2528
+ if (timer) clearTimeout(timer);
2529
+ }
2530
+ }
2531
+ const now = this.config;
2532
+ if (!this.isConnected || before !== document.location.href || provider !== this.hooks.contextProvider || configKey !== JSON.stringify([now.pageContext, now.contextSelector, now.excludeSelectors])) return void 0;
2533
+ return result;
2534
+ }
2535
+ pageFingerprint(page) {
2536
+ const { captured_at: _capturedAt, ...stable } = page;
2537
+ return JSON.stringify(stable);
2538
+ }
2539
+ renderPagePreview() {
2540
+ const preview = this.els.pagePreview;
2541
+ preview.hidden = !this.sharedPage;
2542
+ preview.textContent = this.sharedPage ? [this.sharedPage.title, this.sharedPage.url, this.sharedPage.description, this.sharedPage.selection, this.sharedPage.content?.text].filter(Boolean).join("\n\n") : "";
2543
+ }
2544
+ async choosePage() {
2545
+ const generation = ++this.pageGeneration;
2546
+ if (!this.els.pageChoice.checked) {
2547
+ this.pagePending = false;
2548
+ this.sharedPage = void 0;
2549
+ this.renderPagePreview();
2550
+ return;
2551
+ }
2552
+ this.sharedPage = void 0;
2553
+ this.renderPagePreview();
2554
+ this.pagePending = true;
2555
+ try {
2556
+ const page = await this.captureCurrentPage();
2557
+ if (generation !== this.pageGeneration || !this.els.pageChoice.checked) return;
2558
+ if (!page) throw new Error("Page unavailable");
2559
+ this.sharedPage = page;
2560
+ this.renderPagePreview();
2561
+ this.emit("dewee:context-ready", { available: true });
2562
+ } catch {
2563
+ if (generation !== this.pageGeneration) return;
2564
+ this.sharedPage = void 0;
2565
+ this.els.pageChoice.checked = false;
2566
+ this.renderPagePreview();
2567
+ this.showNotice(this.strings.pageUnavailable);
2568
+ this.emit("dewee:context-error", { code: "page_unavailable" });
2569
+ } finally {
2570
+ if (generation === this.pageGeneration) this.pagePending = false;
2571
+ }
2572
+ }
2280
2573
  updateSendButton() {
2281
2574
  const b = this.els.send;
2282
2575
  b.innerHTML = this.running ? icons.stop : icons.send;
@@ -2327,7 +2620,8 @@ var DeweeChatElement = class extends ElementBase {
2327
2620
  this.activated = true;
2328
2621
  this.renderLog();
2329
2622
  try {
2330
- this.server = await this.client().getConfig(true);
2623
+ this.probedPage = this.config.pageContext === "metadata" || this.config.pageContext === "content";
2624
+ this.server = await this.client().getConfig(true, this.probedPage);
2331
2625
  this.els.input.maxLength = this.server.maxMessageLength;
2332
2626
  const wanted = this.config.agent;
2333
2627
  const known = (id) => !!id && (this.server.agents.length === 0 || this.server.agents.some((a) => a.id === id));
@@ -2339,6 +2633,7 @@ var DeweeChatElement = class extends ElementBase {
2339
2633
  this.showError(null, e);
2340
2634
  }
2341
2635
  this.renderSubtitle();
2636
+ this.updatePageChoice();
2342
2637
  this.emit("dewee:ready", { agent: this.agentId, config: this.server });
2343
2638
  }
2344
2639
  async switchAgent(id) {
@@ -2365,6 +2660,7 @@ var DeweeChatElement = class extends ElementBase {
2365
2660
  }
2366
2661
  renderLog() {
2367
2662
  const log = this.els.log;
2663
+ disposeMarkdownImages(log);
2368
2664
  log.replaceChildren();
2369
2665
  if (!this.messages.length) log.appendChild(this.renderWelcome());
2370
2666
  for (const m of this.messages) log.appendChild(this.renderMessage(m));
@@ -2467,10 +2763,15 @@ var DeweeChatElement = class extends ElementBase {
2467
2763
  const shown = this.actionRegistry().size || m.actions ? stripDirectives(m.content) : m.content;
2468
2764
  const typing = m.status === "streaming" && !shown;
2469
2765
  v.md.hidden = !typing && !shown && !!m.actions?.length;
2470
- if (typing) {
2471
- v.md.innerHTML = `<span class="typing" aria-label="${this.strings.thinking}"><i></i><i></i><i></i></span>`;
2472
- } else {
2473
- v.md.replaceChildren(renderMarkdown(document, shown));
2766
+ const mdKey = JSON.stringify([shown, typing, m.status === "streaming", c.endpoint, c.imageOrigins, this.strings.thinking]);
2767
+ if (v.mdKey !== mdKey) {
2768
+ v.mdKey = mdKey;
2769
+ disposeMarkdownImages(v.md);
2770
+ if (typing) {
2771
+ v.md.innerHTML = `<span class="typing" aria-label="${this.strings.thinking}"><i></i><i></i><i></i></span>`;
2772
+ } else {
2773
+ v.md.replaceChildren(renderMarkdown(document, shown, { imageOrigins: Array.isArray(c.imageOrigins) ? c.imageOrigins : [], imageProxy: (source, signal) => this.client().imageUrl(source, signal), deferImages: m.status === "streaming" }));
2774
+ }
2474
2775
  v.md.classList.toggle("cursor", m.status === "streaming");
2475
2776
  }
2476
2777
  this.renderActions(v, m.actions ?? []);