dewee-webchat 0.4.1 → 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-EVZ5K7BN.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
  }
@@ -279,6 +309,257 @@ function raceAbort(p, signal) {
279
309
  });
280
310
  }
281
311
 
312
+ // src/actions/dom-actions.ts
313
+ var DOM_ACTION_NAMES = ["page.snapshot", "page.scroll", "page.click", "page.fill"];
314
+ var INTERACTIVE = [
315
+ "a[href]",
316
+ "button",
317
+ "input",
318
+ "select",
319
+ "textarea",
320
+ "summary",
321
+ "[contenteditable]:not([contenteditable=false])",
322
+ ...["button", "link", "checkbox", "radio", "switch", "tab", "menuitem", "option", "combobox", "textbox", "searchbox"].map((r) => `[role=${r}]`)
323
+ ].join(",");
324
+ var UNFILLABLE_INPUTS = /* @__PURE__ */ new Set(["checkbox", "radio", "file", "submit", "button", "image", "reset", "hidden", "password"]);
325
+ var SENSITIVE_AUTOCOMPLETE = /\b(cc-[a-z-]+|one-time-code|current-password|new-password)\b/i;
326
+ var SENSITIVE_NAME = /(passw|passcode|\bpin\b|otp|one.?time|cvv|cvc|card.?(num|no)|cc.?num|security.?code|\bssn\b)/i;
327
+ var MAX_NAME = 80;
328
+ var MAX_VALUE = 60;
329
+ var DEFAULT_LIMIT = 40;
330
+ var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
331
+ var squash = (s) => (s ?? "").replace(/\s+/g, " ").trim();
332
+ var refSchema = { type: "string", pattern: "^e[0-9]{1,4}$" };
333
+ var genSchema = { type: "integer", minimum: 1 };
334
+ function createDomActions(o) {
335
+ let generation = 0;
336
+ let refs = /* @__PURE__ */ new Map();
337
+ const rootEl = () => {
338
+ const r = o.root();
339
+ const el2 = typeof r === "string" ? document.querySelector(r) : r;
340
+ if (r !== void 0 && !el2) throw new Error(`domActionsRoot ${JSON.stringify(r)} matches no element`);
341
+ return el2 ?? document.body;
342
+ };
343
+ const allowed = (el2, root) => el2.isConnected && root.contains(el2) && !o.exclude?.(el2) && !el2.closest("[data-dewee-ignore]") && !isSensitive(el2) && isVisible(el2);
344
+ const resolve = (args) => {
345
+ if (args.generation !== generation) {
346
+ throw new Error(`Stale ref: generation ${args.generation} is not the latest snapshot (${generation || "none"}). Call page.snapshot again.`);
347
+ }
348
+ const el2 = refs.get(args.ref);
349
+ if (!el2) throw new Error(`Unknown ref "${args.ref}". Call page.snapshot again.`);
350
+ if (!allowed(el2, rootEl())) throw new Error(`Ref "${args.ref}" is no longer available. Call page.snapshot again.`);
351
+ return el2;
352
+ };
353
+ const snapshot = {
354
+ name: "page.snapshot",
355
+ description: 'List the interactive elements visible on the page (links, buttons, form fields) as lines `ref role "name" [state]`. Use the ref together with the returned generation in page.scroll, page.click and page.fill. Every snapshot invalidates older refs. Use query to filter by visible name, offset/limit to page through long lists.',
356
+ sensitivity: "read",
357
+ parameters: {
358
+ type: "object",
359
+ properties: { query: { type: "string", maxLength: 100 }, offset: { type: "integer", minimum: 0 }, limit: { type: "integer", minimum: 1, maximum: 100 } },
360
+ additionalProperties: false
361
+ },
362
+ handler: ({ query, offset = 0, limit = DEFAULT_LIMIT }) => {
363
+ const root = rootEl();
364
+ generation++;
365
+ refs = /* @__PURE__ */ new Map();
366
+ const q = squash(query).toLowerCase();
367
+ const lines = [];
368
+ let total = 0;
369
+ for (const el2 of root.querySelectorAll(INTERACTIVE)) {
370
+ if (!allowed(el2, root)) continue;
371
+ const line = describe(el2);
372
+ if (q && !line.toLowerCase().includes(q)) continue;
373
+ total++;
374
+ if (total <= offset || lines.length >= limit) continue;
375
+ const ref = `e${total}`;
376
+ refs.set(ref, el2);
377
+ lines.push(`${ref} ${line}`);
378
+ }
379
+ return { generation, url: location.pathname + location.search, title: clip(document.title, MAX_NAME), total, offset, elements: lines, ...scrollState(root) };
380
+ }
381
+ };
382
+ const scroll = {
383
+ name: "page.scroll",
384
+ description: "Scroll the page by about one screen (direction up/down) or to the top/bottom, or scroll an element from page.snapshot into view (ref + generation).",
385
+ sensitivity: "read",
386
+ parameters: {
387
+ type: "object",
388
+ properties: { direction: { type: "string", enum: ["up", "down", "top", "bottom"] }, ref: refSchema, generation: genSchema },
389
+ additionalProperties: false
390
+ },
391
+ handler: ({ direction, ref, generation: gen }) => {
392
+ const root = rootEl();
393
+ if (ref !== void 0) {
394
+ if (gen === void 0) throw new Error("generation is required with ref");
395
+ const el2 = resolve({ ref, generation: gen });
396
+ el2.scrollIntoView?.({ block: "center", inline: "nearest" });
397
+ return { ref, ...scrollState(root) };
398
+ }
399
+ if (!direction) throw new Error("Give a direction or a ref");
400
+ const box = scroller(root);
401
+ const page = box === null ? window.innerHeight : box.clientHeight;
402
+ const current = box === null ? window.scrollY : box.scrollTop;
403
+ const max = box === null ? document.documentElement.scrollHeight : box.scrollHeight;
404
+ const top = direction === "top" ? 0 : direction === "bottom" ? max : current + (direction === "down" ? 0.8 : -0.8) * page;
405
+ if (box === null) window.scrollTo({ top });
406
+ else box.scrollTop = top;
407
+ return { direction, ...scrollState(root) };
408
+ }
409
+ };
410
+ const click = {
411
+ name: "page.click",
412
+ description: "Click an element from page.snapshot (ref + generation). The visitor approves every click.",
413
+ sensitivity: "mutate",
414
+ parameters: { type: "object", properties: { ref: refSchema, generation: genSchema }, required: ["ref", "generation"], additionalProperties: false },
415
+ handler: (args) => {
416
+ const el2 = resolve(args);
417
+ if (isDisabled(el2)) throw new Error(`Ref "${args.ref}" is disabled`);
418
+ el2.scrollIntoView?.({ block: "center", inline: "nearest" });
419
+ if (el2 instanceof HTMLElement) {
420
+ el2.focus({ preventScroll: true });
421
+ el2.click();
422
+ } else {
423
+ el2.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true }));
424
+ }
425
+ return { ref: args.ref, clicked: describe(el2) };
426
+ }
427
+ };
428
+ const fill = {
429
+ name: "page.fill",
430
+ description: "Replace the value of a text field, textarea, select (option value or label) or editable area from page.snapshot (ref + generation). The visitor approves every fill.",
431
+ sensitivity: "mutate",
432
+ parameters: {
433
+ type: "object",
434
+ properties: { ref: refSchema, generation: genSchema, text: { type: "string", maxLength: 1e4 } },
435
+ required: ["ref", "generation", "text"],
436
+ additionalProperties: false
437
+ },
438
+ handler: (args) => {
439
+ const el2 = resolve(args);
440
+ if (isDisabled(el2) || el2.readOnly) throw new Error(`Ref "${args.ref}" is not editable`);
441
+ setValue(el2, args.text);
442
+ return { ref: args.ref, value: clip(currentValue(el2), MAX_VALUE) };
443
+ }
444
+ };
445
+ return {
446
+ actions: [snapshot, scroll, click, fill],
447
+ target: (args) => {
448
+ try {
449
+ return describe(resolve(args));
450
+ } catch {
451
+ return void 0;
452
+ }
453
+ }
454
+ };
455
+ }
456
+ function setValue(el2, text) {
457
+ if (el2 instanceof HTMLInputElement) {
458
+ if (UNFILLABLE_INPUTS.has(el2.type)) throw new Error(`Inputs of type "${el2.type}" cannot be filled; use page.click`);
459
+ nativeSet(HTMLInputElement.prototype, el2, text);
460
+ } else if (el2 instanceof HTMLTextAreaElement) {
461
+ nativeSet(HTMLTextAreaElement.prototype, el2, text);
462
+ } else if (el2 instanceof HTMLSelectElement) {
463
+ const opt = [...el2.options].find((op) => op.value === text) ?? [...el2.options].find((op) => squash(op.textContent).toLowerCase() === squash(text).toLowerCase());
464
+ if (!opt) throw new Error(`No option "${clip(text, MAX_VALUE)}"`);
465
+ nativeSet(HTMLSelectElement.prototype, el2, opt.value);
466
+ } else if (el2 instanceof HTMLElement && el2.isContentEditable) {
467
+ el2.focus({ preventScroll: true });
468
+ el2.textContent = text;
469
+ el2.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true, inputType: "insertReplacementText", data: text }));
470
+ return;
471
+ } else {
472
+ throw new Error("This element cannot be filled; use page.click");
473
+ }
474
+ el2.focus({ preventScroll: true });
475
+ el2.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
476
+ el2.dispatchEvent(new Event("change", { bubbles: true }));
477
+ }
478
+ function nativeSet(proto, el2, value) {
479
+ const set = Object.getOwnPropertyDescriptor(proto, "value")?.set;
480
+ if (set) set.call(el2, value);
481
+ else el2.value = value;
482
+ }
483
+ function currentValue(el2) {
484
+ if (el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement) return el2.value;
485
+ return squash(el2.textContent);
486
+ }
487
+ function isDisabled(el2) {
488
+ return el2.disabled === true || el2.closest("fieldset[disabled]") !== null || el2.getAttribute("aria-disabled") === "true";
489
+ }
490
+ function isSensitive(el2) {
491
+ if (el2 instanceof HTMLInputElement && el2.type === "password") return true;
492
+ if (!(el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement)) return false;
493
+ if (SENSITIVE_AUTOCOMPLETE.test(el2.getAttribute("autocomplete") ?? "")) return true;
494
+ const hint = [el2.name, el2.id, el2.getAttribute("aria-label"), el2.placeholder].filter(Boolean).join(" ");
495
+ return SENSITIVE_NAME.test(hint);
496
+ }
497
+ function isVisible(el2) {
498
+ if (el2 instanceof HTMLInputElement && el2.type === "hidden") return false;
499
+ if (el2.closest("[hidden],[inert],[aria-hidden=true]")) return false;
500
+ for (let n = el2; n; n = n.parentElement) {
501
+ const cs = getComputedStyle(n);
502
+ if (cs.display === "none" || n === el2 && (cs.visibility === "hidden" || cs.visibility === "collapse")) return false;
503
+ }
504
+ return true;
505
+ }
506
+ function role(el2) {
507
+ const explicit = el2.getAttribute("role");
508
+ if (explicit) return explicit;
509
+ const tag = el2.tagName.toLowerCase();
510
+ if (tag === "a") return "link";
511
+ if (tag === "select") return "combobox";
512
+ if (tag === "textarea") return "textbox";
513
+ if (tag === "summary") return "button";
514
+ if (el2 instanceof HTMLInputElement) {
515
+ const t = el2.type;
516
+ if (t === "checkbox" || t === "radio") return t;
517
+ if (t === "submit" || t === "button" || t === "reset" || t === "image") return "button";
518
+ if (t === "search") return "searchbox";
519
+ if (t === "range") return "slider";
520
+ return t === "text" ? "textbox" : `textbox:${t}`;
521
+ }
522
+ if (el2.isContentEditable) return "textbox";
523
+ return tag;
524
+ }
525
+ function accessibleName(el2) {
526
+ const labelledBy = el2.getAttribute("aria-labelledby");
527
+ if (labelledBy) {
528
+ const text2 = labelledBy.split(/\s+/).map((id) => squash(document.getElementById(id)?.textContent)).filter(Boolean).join(" ");
529
+ if (text2) return text2;
530
+ }
531
+ const aria = squash(el2.getAttribute("aria-label"));
532
+ if (aria) return aria;
533
+ const labels = el2.labels;
534
+ if (labels?.length) {
535
+ const text2 = squash([...labels].map((l) => l.textContent).join(" "));
536
+ if (text2) return text2;
537
+ }
538
+ if (el2 instanceof HTMLInputElement && ["submit", "button", "reset"].includes(el2.type)) return squash(el2.value);
539
+ const text = el2 instanceof HTMLSelectElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLInputElement ? "" : squash(el2.textContent);
540
+ return text || squash(el2.getAttribute("placeholder")) || squash(el2.getAttribute("title")) || squash(el2.querySelector("img[alt]")?.getAttribute("alt")) || squash(el2.getAttribute("name"));
541
+ }
542
+ function describe(el2) {
543
+ const parts = [role(el2), JSON.stringify(clip(accessibleName(el2), MAX_NAME))];
544
+ if (el2 instanceof HTMLInputElement && (el2.type === "checkbox" || el2.type === "radio")) parts.push(el2.checked ? "checked" : "unchecked");
545
+ else if (el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement) {
546
+ const v = currentValue(el2);
547
+ if (v && !(el2 instanceof HTMLInputElement && ["submit", "button", "reset"].includes(el2.type))) parts.push(`value=${JSON.stringify(clip(v, MAX_VALUE))}`);
548
+ }
549
+ if (el2 instanceof HTMLAnchorElement) parts.push(`href=${JSON.stringify(clip(el2.getAttribute("href") ?? "", MAX_NAME))}`);
550
+ if (isDisabled(el2)) parts.push("disabled");
551
+ return parts.join(" ");
552
+ }
553
+ function scroller(root) {
554
+ if (root === document.body || root === document.documentElement) return null;
555
+ return root.scrollHeight > root.clientHeight ? root : null;
556
+ }
557
+ function scrollState(root) {
558
+ const box = scroller(root);
559
+ if (box === null) return { scrollY: Math.round(window.scrollY), maxScrollY: Math.max(0, Math.round(document.documentElement.scrollHeight - window.innerHeight)) };
560
+ return { scrollY: Math.round(box.scrollTop), maxScrollY: Math.max(0, Math.round(box.scrollHeight - box.clientHeight)) };
561
+ }
562
+
282
563
  // src/widget/api-client.ts
283
564
  var RENEW_CODES = /* @__PURE__ */ new Set(["LINK_EXPIRED", "LINK_REVOKED"]);
284
565
  var VERIFIED_REFRESH_MS = 6e4;
@@ -333,6 +614,8 @@ var ApiClient = class {
333
614
  session = null;
334
615
  config = null;
335
616
  sessionPromise = null;
617
+ supersededSessions = /* @__PURE__ */ new Map();
618
+ sessionGeneration = 0;
336
619
  url(action, qs) {
337
620
  const base = this.o.endpoint.replace(/\/+$/, "");
338
621
  const q = qs ? "?" + new URLSearchParams(qs).toString() : "";
@@ -367,7 +650,7 @@ var ApiClient = class {
367
650
  h[HEADER_GATEWAY_TOKEN] = this.o.gatewayToken;
368
651
  }
369
652
  if (auth) {
370
- const s = this.session && this.fresh(this.session) ? this.session : await this.ensureSession();
653
+ const s = await this.ensureSession();
371
654
  h.authorization = `Bearer ${s.token}`;
372
655
  }
373
656
  return h;
@@ -381,16 +664,24 @@ var ApiClient = class {
381
664
  }
382
665
  return body;
383
666
  }
384
- 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) {
385
675
  if (this.config && !force) return this.config;
386
- 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() }));
387
677
  return this.config;
388
678
  }
389
679
  /** When each signed-in session should be refreshed, on this browser's clock. */
390
680
  refreshAt = /* @__PURE__ */ new WeakMap();
391
681
  /**
392
682
  * Session reuse. A signed-in session follows its schedule (see `scheduleRefresh`). An anonymous
393
- * 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
394
685
  * stored token from before `obtainedAt` existed uses the 24 h margin. When the backend reports the
395
686
  * lifetime (`expiresIn`), it is counted from receipt, so a skewed browser clock does not matter.
396
687
  */
@@ -399,7 +690,7 @@ var ApiClient = class {
399
690
  const reported = typeof s.expiresIn === "number" && s.expiresIn >= 0 && s.obtainedAt;
400
691
  const end = reported ? s.obtainedAt + s.expiresIn : s.expiresAt;
401
692
  const lifetime = reported ? s.expiresIn : s.obtainedAt ? s.expiresAt - s.obtainedAt : Infinity;
402
- 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));
403
694
  return end - Date.now() > margin;
404
695
  }
405
696
  /**
@@ -417,28 +708,50 @@ var ApiClient = class {
417
708
  if (!saved) return null;
418
709
  try {
419
710
  const s = JSON.parse(saved);
420
- return s.verified ? null : s;
711
+ if (s.verified) {
712
+ store.del(this.storageKey);
713
+ return null;
714
+ }
715
+ return s;
421
716
  } catch {
422
717
  return null;
423
718
  }
424
719
  }
720
+ /** The visitor of the session in use, or null before one is held. Backend conversations are keyed by it. */
721
+ visitorId() {
722
+ return this.session?.visitorId ?? null;
723
+ }
425
724
  /** Returns a valid visitor session, creating or refreshing it when needed. */
426
725
  ensureSession() {
726
+ if (this.sessionPromise) return this.sessionPromise;
427
727
  if (this.session && this.fresh(this.session)) return Promise.resolve(this.session);
428
- 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;
429
737
  }
430
- async loadSession() {
738
+ async loadSession(generation) {
431
739
  if (!this.session) {
432
740
  const s = this.stored();
433
741
  if (s) {
434
- 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
+ }
435
747
  this.session = s;
436
748
  }
437
749
  }
438
- return this.createSession();
750
+ return this.createSession(generation);
439
751
  }
440
- async createSession() {
752
+ async createSession(generation) {
441
753
  const cfg = await this.getConfig();
754
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
442
755
  const body = {};
443
756
  const previous = this.session && !this.session.verified ? this.session : this.stored();
444
757
  if (previous?.token) body.token = previous.token;
@@ -462,9 +775,12 @@ var ApiClient = class {
462
775
  s = await post();
463
776
  }
464
777
  } catch (e) {
778
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
779
+ this.session = null;
465
780
  if (e instanceof ApiError && e.status === 401) tabStore.del(this.reuseKey);
466
781
  throw e;
467
782
  }
783
+ if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
468
784
  const now = Date.now();
469
785
  const held = { ...s, obtainedAt: now };
470
786
  this.session = held;
@@ -494,8 +810,14 @@ var ApiClient = class {
494
810
  * stored anonymous visitor id is kept, so an anonymous visitor's history survives.
495
811
  */
496
812
  async renewSession() {
813
+ const previousGeneration = this.sessionGeneration;
814
+ const previous = this.sessionPromise;
815
+ this.sessionGeneration++;
497
816
  this.session = null;
498
- return this.ensureSession();
817
+ this.sessionPromise = null;
818
+ const next = this.ensureSession();
819
+ if (previous) this.supersededSessions.set(previousGeneration, next);
820
+ return next;
499
821
  }
500
822
  /** Requests a single-use launch ticket. Does not refresh or swap the signed-in session after refusal. */
501
823
  async jobAccess(signal) {
@@ -547,8 +869,10 @@ var ApiClient = class {
547
869
  await this.ensureSession();
548
870
  let res = await this.request(this.url("chat"), { method: "POST", headers: await this.headers(true), body: JSON.stringify(input), signal });
549
871
  if (res.status === 401) {
872
+ const before = this.visitorId();
550
873
  this.forgetSession();
551
874
  await this.ensureSession();
875
+ if (input.actionsHash && this.visitorId() !== before) input = { ...input, actionsHash: void 0 };
552
876
  res = await this.request(this.url("chat"), { method: "POST", headers: await this.headers(true), body: JSON.stringify(input), signal });
553
877
  }
554
878
  if (!res.ok || !res.body) await this.parse(res);
@@ -821,7 +1145,10 @@ var en = {
821
1145
  jobAccessOpened: "Job access opened in a new window. Review and confirm any permissions there.",
822
1146
  jobAccessUnavailable: "Job access could not be opened. Sign in again or retry.",
823
1147
  jobAccessPopupBlocked: "Allow popups for this site, then choose Job access again.",
824
- 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."
825
1152
  };
826
1153
  var vi = {
827
1154
  title: "Tr\u1EE3 l\xFD",
@@ -881,7 +1208,10 @@ var vi = {
881
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.",
882
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.",
883
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.",
884
- 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."
885
1215
  };
886
1216
  function pickStrings(locale) {
887
1217
  const l = (locale || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "") || "en").toLowerCase();
@@ -901,6 +1231,40 @@ var icons = {
901
1231
  };
902
1232
 
903
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
+ }
904
1268
  function el(doc, tag, cls) {
905
1269
  const e = doc.createElement(tag);
906
1270
  if (cls) e.className = cls;
@@ -912,45 +1276,87 @@ function safeHref(url) {
912
1276
  if (/^[/#?]/.test(u) && !u.startsWith("//")) return u;
913
1277
  return null;
914
1278
  }
915
- var INLINE_SRC = /(`+)([\s\S]*?[^`])\1(?!`)|\*\*([^*]+?)\*\*|__([^_]+?)__|~~([^~]+?)~~|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/;
916
- 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 = {}) {
917
1281
  const re = new RegExp(INLINE_SRC.source, "g");
918
1282
  let last = 0;
919
1283
  for (let m; m = re.exec(text); ) {
920
1284
  if (m.index > last) parent.appendChild(doc.createTextNode(text.slice(last, m.index)));
921
1285
  last = m.index + m[0].length;
922
- 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) {
923
1289
  const c = el(doc, "code");
924
1290
  c.textContent = m[2];
925
1291
  parent.appendChild(c);
926
1292
  } else if (m[3] !== void 0 || m[4] !== void 0) {
927
1293
  const s = el(doc, "strong");
928
- renderInline(doc, m[3] ?? m[4], s);
1294
+ renderInline(doc, m[3] ?? m[4], s, options);
929
1295
  parent.appendChild(s);
930
1296
  } else if (m[5] !== void 0) {
931
1297
  const s = el(doc, "del");
932
- renderInline(doc, m[5], s);
1298
+ renderInline(doc, m[5], s, options);
933
1299
  parent.appendChild(s);
934
1300
  } else if (m[6] !== void 0 || m[7] !== void 0) {
935
1301
  const s = el(doc, "em");
936
- renderInline(doc, m[6] ?? m[7], s);
1302
+ renderInline(doc, m[6] ?? m[7], s, options);
937
1303
  parent.appendChild(s);
938
1304
  } else if (m[8] !== void 0) {
939
- 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]);
940
1346
  if (href) {
941
1347
  const a = el(doc, "a");
942
1348
  a.href = href;
943
1349
  a.target = "_blank";
944
1350
  a.rel = "noopener noreferrer nofollow";
945
- renderInline(doc, m[8], a);
1351
+ renderInline(doc, m[10], a, options);
946
1352
  parent.appendChild(a);
947
- } else parent.appendChild(doc.createTextNode(m[8]));
948
- } else if (m[10] !== void 0) {
1353
+ } else parent.appendChild(doc.createTextNode(m[10]));
1354
+ } else if (m[12] !== void 0) {
949
1355
  const a = el(doc, "a");
950
- a.href = m[10];
1356
+ a.href = m[12];
951
1357
  a.target = "_blank";
952
1358
  a.rel = "noopener noreferrer nofollow";
953
- a.textContent = m[10];
1359
+ a.textContent = m[12];
954
1360
  parent.appendChild(a);
955
1361
  }
956
1362
  }
@@ -958,7 +1364,7 @@ function renderInline(doc, text, parent) {
958
1364
  }
959
1365
  var isTableSep = (l) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(l);
960
1366
  var splitRow = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
961
- function renderMarkdown(doc, src) {
1367
+ function renderMarkdown(doc, src, options = {}) {
962
1368
  const frag = doc.createDocumentFragment();
963
1369
  const lines = src.replace(/\r\n?/g, "\n").split("\n");
964
1370
  let i = 0;
@@ -986,7 +1392,7 @@ function renderMarkdown(doc, src) {
986
1392
  if (h) {
987
1393
  const tag = `h${Math.min(6, h[1].length + 2)}`;
988
1394
  const e = el(doc, tag);
989
- renderInline(doc, h[2], e);
1395
+ renderInline(doc, h[2], e, options);
990
1396
  frag.appendChild(e);
991
1397
  i++;
992
1398
  continue;
@@ -1003,7 +1409,7 @@ function renderMarkdown(doc, src) {
1003
1409
  const tr = el(doc, "tr");
1004
1410
  for (const c of splitRow(line)) {
1005
1411
  const th = el(doc, "th");
1006
- renderInline(doc, c, th);
1412
+ renderInline(doc, c, th, options);
1007
1413
  tr.appendChild(th);
1008
1414
  }
1009
1415
  thead.appendChild(tr);
@@ -1014,7 +1420,7 @@ function renderMarkdown(doc, src) {
1014
1420
  const r = el(doc, "tr");
1015
1421
  for (const c of splitRow(lines[i])) {
1016
1422
  const td = el(doc, "td");
1017
- renderInline(doc, c, td);
1423
+ renderInline(doc, c, td, options);
1018
1424
  r.appendChild(td);
1019
1425
  }
1020
1426
  tbody.appendChild(r);
@@ -1029,7 +1435,7 @@ function renderMarkdown(doc, src) {
1029
1435
  const body = [];
1030
1436
  while (i < lines.length && /^\s*>/.test(lines[i])) body.push(lines[i++].replace(/^\s*>\s?/, ""));
1031
1437
  const q = el(doc, "blockquote");
1032
- q.appendChild(renderMarkdown(doc, body.join("\n")));
1438
+ q.appendChild(renderMarkdown(doc, body.join("\n"), options));
1033
1439
  frag.appendChild(q);
1034
1440
  continue;
1035
1441
  }
@@ -1045,11 +1451,11 @@ function renderMarkdown(doc, src) {
1045
1451
  if (m[1].length > baseIndent) {
1046
1452
  const sub = [];
1047
1453
  while (i < lines.length && (/^\s+/.test(lines[i]) && lines[i].match(/^(\s*)/)[1].length > baseIndent)) sub.push(lines[i++].slice(baseIndent + 2));
1048
- list.lastElementChild?.appendChild(renderMarkdown(doc, sub.join("\n")));
1454
+ list.lastElementChild?.appendChild(renderMarkdown(doc, sub.join("\n"), options));
1049
1455
  continue;
1050
1456
  }
1051
1457
  const item = el(doc, "li");
1052
- renderInline(doc, m[3], item);
1458
+ renderInline(doc, m[3], item, options);
1053
1459
  list.appendChild(item);
1054
1460
  i++;
1055
1461
  }
@@ -1062,7 +1468,7 @@ function renderMarkdown(doc, src) {
1062
1468
  const p = el(doc, "p");
1063
1469
  para.forEach((l, idx) => {
1064
1470
  if (idx) p.appendChild(el(doc, "br"));
1065
- renderInline(doc, l, p);
1471
+ renderInline(doc, l, p, options);
1066
1472
  });
1067
1473
  frag.appendChild(p);
1068
1474
  }
@@ -1260,6 +1666,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
1260
1666
  .md li { margin: 3px 0; }
1261
1667
  .md h3, .md h4, .md h5, .md h6 { margin: 14px 0 6px; font-size: 15px; line-height: 1.35; }
1262
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; }
1263
1670
  .md code { font-family: var(--dw-mono); font-size: .86em; background: var(--dw-surface); padding: 1.5px 5px; border-radius: 5px; }
1264
1671
  .md pre { background: #12132a; color: #e6e6f5; padding: 12px 14px; border-radius: 12px; overflow-x: auto; font-size: 12.5px; line-height: 1.55; }
1265
1672
  .md pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
@@ -1354,6 +1761,9 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
1354
1761
 
1355
1762
  /* ---------- composer ---------- */
1356
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; }
1357
1767
  .box {
1358
1768
  display: flex; align-items: flex-end; gap: 8px; border: 1px solid var(--dw-border); border-radius: 14px;
1359
1769
  padding: 6px 6px 6px 12px; background: var(--dw-bg); transition: border-color .15s, box-shadow .15s;
@@ -1386,7 +1796,7 @@ textarea::placeholder { color: var(--dw-muted); }
1386
1796
 
1387
1797
  // src/widget/element.ts
1388
1798
  var DEFAULT_COOLDOWN_MS = 5e3;
1389
- var BOOL_ATTRS = ["show-reasoning", "show-tool-calls", "allow-agent-switch", "open", "persist", "hide-branding", "launcher-avatar"];
1799
+ var BOOL_ATTRS = ["show-reasoning", "show-tool-calls", "allow-agent-switch", "open", "persist", "hide-branding", "launcher-avatar", "dom-actions"];
1390
1800
  var ATTRS = [
1391
1801
  "endpoint",
1392
1802
  "agent",
@@ -1407,6 +1817,11 @@ var ATTRS = [
1407
1817
  "width",
1408
1818
  "height",
1409
1819
  "z-index",
1820
+ "dom-actions-root",
1821
+ "image-origins",
1822
+ "page-context",
1823
+ "context-selector",
1824
+ "exclude-selectors",
1410
1825
  ...BOOL_ATTRS
1411
1826
  ];
1412
1827
  var camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -1454,17 +1869,24 @@ var DeweeChatElement = class extends ElementBase {
1454
1869
  api = null;
1455
1870
  apiKey = "";
1456
1871
  server = null;
1872
+ sharedPage;
1873
+ preparingPage = false;
1874
+ pagePending = false;
1875
+ pageGeneration = 0;
1457
1876
  agentId = "";
1458
1877
  conversationId = "";
1459
1878
  messages = [];
1460
1879
  running = null;
1461
1880
  jobAccessLaunch = null;
1462
1881
  activated = false;
1882
+ probedPage = false;
1463
1883
  strings = pickStrings();
1464
1884
  mql = null;
1465
1885
  registry = new ActionRegistry();
1466
1886
  propActionNames = /* @__PURE__ */ new Set();
1467
1887
  hooks = {};
1888
+ /** Built-in DOM actions, created on first use when `domActions` is on (keeps the snapshot refs). */
1889
+ dom = null;
1468
1890
  acks = /* @__PURE__ */ new Map();
1469
1891
  /** Pause after a RATE_LIMITED refusal; send stays disabled until it ends. */
1470
1892
  cooldown = new SendCooldown((s) => this.renderCooldown(s), () => this.endCooldown());
@@ -1482,6 +1904,8 @@ var DeweeChatElement = class extends ElementBase {
1482
1904
  if (v === null) continue;
1483
1905
  if (BOOL_ATTRS.includes(a)) c[camel(a)] = parseBool(v);
1484
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);
1485
1909
  else if (a === "z-index") c.zIndex = Number(v);
1486
1910
  else c[camel(a)] = v;
1487
1911
  }
@@ -1489,13 +1913,18 @@ var DeweeChatElement = class extends ElementBase {
1489
1913
  }
1490
1914
  /** Updates configuration at runtime (wins over attributes). */
1491
1915
  configure(partial) {
1492
- const { actions, confirm, getPageContext, headers, fetch, ...rest } = partial;
1916
+ const { actions, confirm, getPageContext, headers, fetch, domActionsRoot, contextProvider, ...rest } = partial;
1493
1917
  const has = (k) => Object.prototype.hasOwnProperty.call(partial, k);
1494
1918
  if (has("actions")) this.setPropActions(actions ?? []);
1495
1919
  if (has("confirm")) this.hooks.confirm = confirm;
1496
1920
  if (has("getPageContext")) this.hooks.getPageContext = getPageContext;
1497
1921
  if (has("headers")) this.hooks.headers = headers;
1498
1922
  if (has("fetch")) this.hooks.fetch = fetch;
1923
+ if (has("contextProvider")) this.hooks.contextProvider = contextProvider;
1924
+ if (has("domActionsRoot")) {
1925
+ if (domActionsRoot === void 0) delete this.hooks.domActionsRoot;
1926
+ else this.hooks.domActionsRoot = domActionsRoot;
1927
+ }
1499
1928
  if (!Object.keys(rest).length) return;
1500
1929
  this.overrides = { ...this.overrides, ...rest };
1501
1930
  if (this.els) this.applyConfig();
@@ -1523,9 +1952,23 @@ var DeweeChatElement = class extends ElementBase {
1523
1952
  set fetch(v) {
1524
1953
  this.hooks.fetch = v;
1525
1954
  }
1526
- /** Names of the currently registered page actions. */
1955
+ /** Names of the page actions offered to the agent (host actions, then enabled built-ins). */
1527
1956
  get actionNames() {
1528
- return this.registry.names();
1957
+ return this.actionRegistry().names();
1958
+ }
1959
+ /** Host actions plus the enabled built-in DOM actions whose names the host did not take. */
1960
+ actionRegistry() {
1961
+ if (!this.config.domActions) return this.registry;
1962
+ this.dom ??= createDomActions({
1963
+ root: () => this.hooks.domActionsRoot ?? this.getAttribute("dom-actions-root") ?? void 0,
1964
+ exclude: (el2) => el2 === this || this.contains(el2) || el2.closest("dewee-chat") !== null
1965
+ });
1966
+ const host = this.registry.names().map((n) => this.registry.get(n));
1967
+ const room = ACTION_LIMITS.maxActions - host.length;
1968
+ const builtins = this.dom.actions.filter((a) => !this.registry.get(a.name)).slice(0, Math.max(0, room));
1969
+ const all = new ActionRegistry();
1970
+ all.register([...host, ...builtins]);
1971
+ return all;
1529
1972
  }
1530
1973
  setPropActions(actions) {
1531
1974
  const next = new Set(actions.map((a) => a.name));
@@ -1554,16 +1997,49 @@ var DeweeChatElement = class extends ElementBase {
1554
1997
  /** Sends a message programmatically (opens the widget first). */
1555
1998
  async send(text) {
1556
1999
  const message = text.trim();
1557
- if (!message || this.running || this.cooldown.active) return;
2000
+ if (!message || this.running || this.preparingPage || this.cooldown.active) return;
1558
2001
  if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
1559
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
+ }
1560
2036
  this.els.input.value = "";
1561
2037
  this.autosize();
1562
2038
  let next = { message, hidden: false };
1563
2039
  for (let round = 0; next; round++) {
1564
- const reply = await this.runTurn(next.message, next.hidden);
2040
+ const reply = await this.runTurn(next.message, next.hidden, round === 0 ? attachment : void 0);
1565
2041
  next = null;
1566
- if (reply.status !== "done" || reply.stopped || !this.registry.size) break;
2042
+ if (reply.status !== "done" || reply.stopped || !this.actionRegistry().size) break;
1567
2043
  const { calls } = parseDirectives(reply.content);
1568
2044
  if (!calls.length) break;
1569
2045
  reply.actions = calls.map((c) => ({ id: c.id, name: c.name, args: c.ok ? c.args : void 0, status: "pending" }));
@@ -1580,14 +2056,15 @@ var DeweeChatElement = class extends ElementBase {
1580
2056
  }
1581
2057
  }
1582
2058
  /** One request/response with the agent. */
1583
- async runTurn(message, hidden) {
2059
+ async runTurn(message, hidden, attachment) {
1584
2060
  this.pushMessage({ role: "user", content: message, hidden });
1585
2061
  const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
1586
2062
  this.pushMessage(reply);
1587
2063
  this.setRunning(new AbortController());
1588
2064
  this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId, auto: hidden || void 0 });
1589
2065
  try {
1590
- await this.client().chat(this.chatBody(message), (e) => this.onStreamEvent(reply, e), this.running.signal);
2066
+ await this.client().ensureSession();
2067
+ await this.client().chat({ ...this.chatBody(message), ...attachment ? { page_context: attachment } : {} }, (e) => this.onStreamEvent(reply, e), this.running.signal);
1591
2068
  if (reply.status === "streaming") reply.status = "done";
1592
2069
  } catch (e) {
1593
2070
  if (e.name === "AbortError") {
@@ -1603,7 +2080,7 @@ var DeweeChatElement = class extends ElementBase {
1603
2080
  this.updateMessage(reply);
1604
2081
  this.setRunning(null);
1605
2082
  if (reply.status === "done") {
1606
- if (reply.actionsHash && !reply.stopped) this.setAck(reply.actionsHash);
2083
+ if (reply.actionsHash && reply.ackVisitor && !reply.stopped) this.setAck(reply.actionsHash, reply.ackVisitor);
1607
2084
  this.emit("dewee:reply", { content: reply.content, agent: this.agentId, conversationId: this.conversationId });
1608
2085
  if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.setAttribute("data-unread", "");
1609
2086
  }
@@ -1612,9 +2089,11 @@ var DeweeChatElement = class extends ElementBase {
1612
2089
  }
1613
2090
  chatBody(message) {
1614
2091
  const body = { message, agent: this.agentId, conversationId: this.conversationId };
1615
- if (this.registry.size) {
1616
- body.actions = this.registry.manifest();
1617
- const ack = this.getAck();
2092
+ const registry = this.actionRegistry();
2093
+ if (registry.size) {
2094
+ body.actions = registry.manifest();
2095
+ const visitor = this.client().visitorId();
2096
+ const ack = visitor ? this.getAck(visitor) : void 0;
1618
2097
  if (ack) body.actionsHash = ack;
1619
2098
  }
1620
2099
  const ctx = this.pageContext();
@@ -1637,15 +2116,21 @@ var DeweeChatElement = class extends ElementBase {
1637
2116
  return void 0;
1638
2117
  }
1639
2118
  }
1640
- ackKey() {
1641
- return `dewee-webchat:actions:${this.config.endpoint}:${this.agentId}:${this.conversationId}`;
2119
+ /**
2120
+ * The backend keys a conversation by visitor as well as by id, so a sign-in, a sign-out or another
2121
+ * user starts a conversation that has not seen the manifest, even with the same conversation id.
2122
+ */
2123
+ ackKey(visitor) {
2124
+ return `dewee-webchat:actions:${this.config.endpoint}:${this.agentId}:${visitor}:${this.conversationId}`;
1642
2125
  }
1643
- getAck() {
1644
- return this.acks.get(this.ackKey()) ?? (this.config.persist !== false ? store2.get(this.ackKey()) ?? void 0 : void 0);
2126
+ getAck(visitor) {
2127
+ const key = this.ackKey(visitor);
2128
+ return this.acks.get(key) ?? (this.config.persist !== false ? store2.get(key) ?? void 0 : void 0);
1645
2129
  }
1646
- setAck(hash) {
1647
- this.acks.set(this.ackKey(), hash);
1648
- if (this.config.persist !== false) store2.set(this.ackKey(), hash);
2130
+ setAck(hash, visitor) {
2131
+ const key = this.ackKey(visitor);
2132
+ this.acks.set(key, hash);
2133
+ if (this.config.persist !== false) store2.set(key, hash);
1649
2134
  }
1650
2135
  /** Executes the calls of one answer in order. Resolves null when the visitor stopped the chain. */
1651
2136
  async runActions(reply, calls) {
@@ -1653,15 +2138,16 @@ var DeweeChatElement = class extends ElementBase {
1653
2138
  this.setRunning(ctrl);
1654
2139
  const results = [];
1655
2140
  const base = { agent: this.agentId, conversationId: this.conversationId };
2141
+ const registry = this.actionRegistry();
1656
2142
  try {
1657
2143
  for (const [i, call] of calls.entries()) {
1658
2144
  const view = reply.actions[i];
1659
- const sensitivity = call.ok ? this.registry.get(call.name)?.sensitivity : void 0;
2145
+ const sensitivity = call.ok ? registry.get(call.name)?.sensitivity : void 0;
1660
2146
  const args = call.ok ? call.args : void 0;
1661
2147
  this.emit("dewee:action", { phase: "start", id: call.id, name: call.name, sensitivity, args, ...base });
1662
2148
  const r = await runActionCall(call, {
1663
- registry: this.registry,
1664
- confirm: (action, a) => this.confirmAction(action, a, ctrl.signal),
2149
+ registry,
2150
+ confirm: (action, a) => this.confirmAction(registry, action, a, ctrl.signal),
1665
2151
  signal: ctrl.signal,
1666
2152
  timeoutMs: this.config.actionTimeoutMs,
1667
2153
  ...base,
@@ -1680,10 +2166,13 @@ var DeweeChatElement = class extends ElementBase {
1680
2166
  }
1681
2167
  return ctrl.signal.aborted ? null : results;
1682
2168
  }
1683
- confirmAction(action, args, signal) {
1684
- if (this.hooks.confirm) return this.hooks.confirm(action, args);
2169
+ confirmAction(registry, action, args, signal) {
2170
+ const builtin = this.dom?.actions.includes(registry.get(action.name));
2171
+ const target = builtin ? this.dom.target(args) : void 0;
2172
+ const shown = target ? { ...args, target } : args;
2173
+ if (this.hooks.confirm) return this.hooks.confirm(action, shown);
1685
2174
  if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
1686
- return showConfirmDialog(this.els.panel, this.strings, action, args, signal);
2175
+ return showConfirmDialog(this.els.panel, this.strings, action, shown, signal);
1687
2176
  }
1688
2177
  showNotice(text) {
1689
2178
  const div = document.createElement("div");
@@ -1811,6 +2300,12 @@ var DeweeChatElement = class extends ElementBase {
1811
2300
  }
1812
2301
  }
1813
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;
1814
2309
  this.jobAccessLaunch?.abort();
1815
2310
  this.stop();
1816
2311
  this.mql?.removeEventListener("change", this.onScheme);
@@ -1892,6 +2387,8 @@ var DeweeChatElement = class extends ElementBase {
1892
2387
  e.jobAccess.setAttribute("aria-label", s.jobAccess);
1893
2388
  e.jobAccess.title = s.jobAccess;
1894
2389
  e.input.placeholder = c.placeholder || s.placeholder;
2390
+ e.pageLabel.textContent = s.pageShare;
2391
+ this.updatePageChoice();
1895
2392
  e.foot.hidden = !!c.hideBranding;
1896
2393
  e.foot.querySelector(".by").textContent = s.poweredBy + " ";
1897
2394
  this.renderSubtitle();
@@ -1899,11 +2396,16 @@ var DeweeChatElement = class extends ElementBase {
1899
2396
  if (this.activated && this.api && this.apiKey !== [c.endpoint, c.gatewayUrl, c.gatewayToken].join("|")) {
1900
2397
  this.activated = false;
1901
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;
1902
2403
  this.activate();
1903
2404
  } else if (this.activated && c.agent && c.agent !== this.agentId && this.server) {
1904
2405
  this.switchAgent(c.agent);
1905
2406
  } else if (this.activated) {
1906
- this.renderLog();
2407
+ if (!this.messages.length) this.renderLog();
2408
+ else for (const message of this.messages) this.updateMessage(message);
1907
2409
  }
1908
2410
  }
1909
2411
  renderShell() {
@@ -1924,6 +2426,7 @@ var DeweeChatElement = class extends ElementBase {
1924
2426
  <div class="log" part="log" role="log" aria-live="polite"></div>
1925
2427
  <div class="challenge"><slot name="challenge"></slot></div>
1926
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>
1927
2430
  <div class="cooldown" part="cooldown" aria-hidden="true" hidden></div>
1928
2431
  <div class="sr-only announcer" role="status" aria-live="polite"></div>
1929
2432
  <div class="box">
@@ -1950,7 +2453,11 @@ var DeweeChatElement = class extends ElementBase {
1950
2453
  close: $(".close"),
1951
2454
  foot: $(".foot"),
1952
2455
  cooldown: $(".cooldown"),
1953
- announcer: $(".announcer")
2456
+ announcer: $(".announcer"),
2457
+ pageChoice: $(".page-choice"),
2458
+ pageWrap: $(".page-context"),
2459
+ pageLabel: $(".page-label"),
2460
+ pagePreview: $(".page-preview")
1954
2461
  };
1955
2462
  const e = this.els;
1956
2463
  e.launcher.addEventListener("click", () => this.toggle());
@@ -1962,6 +2469,9 @@ var DeweeChatElement = class extends ElementBase {
1962
2469
  e.jobAccess.addEventListener("click", () => {
1963
2470
  void this.openJobAccess();
1964
2471
  });
2472
+ e.pageChoice.addEventListener("change", () => {
2473
+ void this.choosePage();
2474
+ });
1965
2475
  e.select.addEventListener("change", () => this.switchAgent(e.select.value));
1966
2476
  $("form").addEventListener("submit", (ev) => {
1967
2477
  ev.preventDefault();
@@ -1986,6 +2496,80 @@ var DeweeChatElement = class extends ElementBase {
1986
2496
  t.style.height = "auto";
1987
2497
  t.style.height = Math.min(t.scrollHeight, 140) + "px";
1988
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
+ }
1989
2573
  updateSendButton() {
1990
2574
  const b = this.els.send;
1991
2575
  b.innerHTML = this.running ? icons.stop : icons.send;
@@ -2036,7 +2620,8 @@ var DeweeChatElement = class extends ElementBase {
2036
2620
  this.activated = true;
2037
2621
  this.renderLog();
2038
2622
  try {
2039
- 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);
2040
2625
  this.els.input.maxLength = this.server.maxMessageLength;
2041
2626
  const wanted = this.config.agent;
2042
2627
  const known = (id) => !!id && (this.server.agents.length === 0 || this.server.agents.some((a) => a.id === id));
@@ -2048,6 +2633,7 @@ var DeweeChatElement = class extends ElementBase {
2048
2633
  this.showError(null, e);
2049
2634
  }
2050
2635
  this.renderSubtitle();
2636
+ this.updatePageChoice();
2051
2637
  this.emit("dewee:ready", { agent: this.agentId, config: this.server });
2052
2638
  }
2053
2639
  async switchAgent(id) {
@@ -2074,6 +2660,7 @@ var DeweeChatElement = class extends ElementBase {
2074
2660
  }
2075
2661
  renderLog() {
2076
2662
  const log = this.els.log;
2663
+ disposeMarkdownImages(log);
2077
2664
  log.replaceChildren();
2078
2665
  if (!this.messages.length) log.appendChild(this.renderWelcome());
2079
2666
  for (const m of this.messages) log.appendChild(this.renderMessage(m));
@@ -2173,13 +2760,18 @@ var DeweeChatElement = class extends ElementBase {
2173
2760
  v.tools.clear();
2174
2761
  }
2175
2762
  v.trace.hidden = !v.trace.childElementCount;
2176
- const shown = this.registry.size || m.actions ? stripDirectives(m.content) : m.content;
2763
+ const shown = this.actionRegistry().size || m.actions ? stripDirectives(m.content) : m.content;
2177
2764
  const typing = m.status === "streaming" && !shown;
2178
2765
  v.md.hidden = !typing && !shown && !!m.actions?.length;
2179
- if (typing) {
2180
- v.md.innerHTML = `<span class="typing" aria-label="${this.strings.thinking}"><i></i><i></i><i></i></span>`;
2181
- } else {
2182
- 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
+ }
2183
2775
  v.md.classList.toggle("cursor", m.status === "streaming");
2184
2776
  }
2185
2777
  this.renderActions(v, m.actions ?? []);
@@ -2233,7 +2825,10 @@ var DeweeChatElement = class extends ElementBase {
2233
2825
  onStreamEvent(m, e) {
2234
2826
  switch (e.type) {
2235
2827
  case "start":
2236
- if (e.actionsHash) m.actionsHash = e.actionsHash;
2828
+ if (e.actionsHash) {
2829
+ m.actionsHash = e.actionsHash;
2830
+ m.ackVisitor = this.client().visitorId() ?? void 0;
2831
+ }
2237
2832
  if (e.conversationId && e.conversationId !== this.conversationId) {
2238
2833
  this.conversationId = e.conversationId;
2239
2834
  if (this.config.persist !== false) store2.set(this.convKey(), e.conversationId);
@@ -2416,6 +3011,7 @@ function mount(config = {}, target) {
2416
3011
  defineDeweeChat();
2417
3012
 
2418
3013
  export {
3014
+ DOM_ACTION_NAMES,
2419
3015
  RESERVED_HEADERS,
2420
3016
  ApiError,
2421
3017
  ApiClient,