@oyehello/chat 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -192,6 +192,7 @@ declare class OyehelloChatElement extends HTMLElement {
192
192
  private idleTimer;
193
193
  private idleReset;
194
194
  private uninstallSpaHooks;
195
+ private unwatchPageTheme;
195
196
  private oc;
196
197
  private bodyEl;
197
198
  private textareaEl;
@@ -209,6 +210,8 @@ declare class OyehelloChatElement extends HTMLElement {
209
210
  private applyBrandLogo;
210
211
  disconnectedCallback(): void;
211
212
  registerTools(handlers: Record<string, ChatToolHandler>): void;
213
+ /** Open the chat window, exactly as a launcher click does. No-op if already open or not rendered. */
214
+ open(): void;
212
215
  private key;
213
216
  private get attrSuggestions();
214
217
  private get chipLabels();
package/dist/index.d.ts CHANGED
@@ -192,6 +192,7 @@ declare class OyehelloChatElement extends HTMLElement {
192
192
  private idleTimer;
193
193
  private idleReset;
194
194
  private uninstallSpaHooks;
195
+ private unwatchPageTheme;
195
196
  private oc;
196
197
  private bodyEl;
197
198
  private textareaEl;
@@ -209,6 +210,8 @@ declare class OyehelloChatElement extends HTMLElement {
209
210
  private applyBrandLogo;
210
211
  disconnectedCallback(): void;
211
212
  registerTools(handlers: Record<string, ChatToolHandler>): void;
213
+ /** Open the chat window, exactly as a launcher click does. No-op if already open or not rendered. */
214
+ open(): void;
212
215
  private key;
213
216
  private get attrSuggestions();
214
217
  private get chipLabels();
@@ -61,6 +61,16 @@ var OyehelloChat = (function (exports) {
61
61
  });
62
62
  if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
63
63
  }
64
+ async function getHistory(opts) {
65
+ const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/messages`, {
66
+ method: "GET",
67
+ credentials: "omit",
68
+ headers: { Authorization: `Bearer ${opts.sessionToken}` }
69
+ });
70
+ if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
71
+ const body = await res.json();
72
+ return body.messages;
73
+ }
64
74
  async function postPageViews(opts) {
65
75
  const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/page-views`, {
66
76
  method: "POST",
@@ -308,6 +318,33 @@ var OyehelloChat = (function (exports) {
308
318
  // src/client.ts
309
319
  var seq = 0;
310
320
  var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
321
+ var isSafeRelativePath = (p) => p.startsWith("/") && !p.startsWith("//");
322
+ var isSafeHttpUrl = (u3) => {
323
+ try {
324
+ const parsed = new URL(u3, window.location.origin);
325
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
326
+ } catch {
327
+ return false;
328
+ }
329
+ };
330
+ var AFTER_RESULT = "__ohAfterResult";
331
+ var BUILTIN_TOOL_HANDLERS = {
332
+ navigate: (args) => {
333
+ const path = String(args.path ?? "");
334
+ if (!isSafeRelativePath(path)) return { ok: false, error: "unsafe navigate path" };
335
+ const here = window.location.pathname + window.location.search + window.location.hash;
336
+ const target = new URL(path, window.location.href);
337
+ if (target.pathname + target.search + target.hash === here) return { ok: true, note: "already on this page" };
338
+ return { ok: true, [AFTER_RESULT]: () => window.location.assign(path) };
339
+ },
340
+ openUrl: (args) => {
341
+ const url = String(args.url ?? "");
342
+ if (!isSafeHttpUrl(url)) return { ok: false, error: "unsafe url" };
343
+ const sameHost = new URL(url, window.location.origin).hostname === window.location.hostname;
344
+ window.open(url, sameHost ? "_self" : "_blank", "noopener,noreferrer");
345
+ return { ok: true };
346
+ }
347
+ };
311
348
  function createChatClient(options) {
312
349
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
313
350
  const ES = options.eventSource ?? globalThis.EventSource;
@@ -416,6 +453,23 @@ var OyehelloChat = (function (exports) {
416
453
  } catch {
417
454
  }
418
455
  };
456
+ const callsKey = `oyehello-chat-calls:${options.agentId}`;
457
+ const handledCallIds = () => {
458
+ try {
459
+ const raw = store2?.getItem(callsKey);
460
+ const v2 = raw ? JSON.parse(raw) : null;
461
+ return v2 && session && v2.sessionId === session.sessionId && Array.isArray(v2.ids) ? v2.ids : [];
462
+ } catch {
463
+ return [];
464
+ }
465
+ };
466
+ const rememberHandledCall = (callId) => {
467
+ if (!session) return;
468
+ try {
469
+ store2?.setItem(callsKey, JSON.stringify({ sessionId: session.sessionId, ids: [...handledCallIds(), callId].slice(-50) }));
470
+ } catch {
471
+ }
472
+ };
419
473
  const emit = (e) => {
420
474
  for (const l of listeners) l(e);
421
475
  };
@@ -464,9 +518,10 @@ var OyehelloChat = (function (exports) {
464
518
  } else if (ev.type === "error") {
465
519
  emit({ type: "error", error: new Error(ev.message) });
466
520
  } else if (ev.type === "tool_call") {
467
- if (seenCallIds.has(ev.callId)) return;
521
+ if (seenCallIds.has(ev.callId) || handledCallIds().includes(ev.callId)) return;
468
522
  seenCallIds.add(ev.callId);
469
- const handler = toolHandlers.get(ev.name);
523
+ rememberHandledCall(ev.callId);
524
+ const handler = toolHandlers.get(ev.name) ?? BUILTIN_TOOL_HANDLERS[ev.name];
470
525
  void (async () => {
471
526
  if (!session) return;
472
527
  const base = { baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, callId: ev.callId, fetchImpl };
@@ -475,8 +530,16 @@ var OyehelloChat = (function (exports) {
475
530
  return;
476
531
  }
477
532
  try {
478
- const out = await handler(ev.args);
533
+ const raw = await handler(ev.args);
534
+ let after = null;
535
+ let out = raw;
536
+ if (raw && typeof raw === "object" && typeof raw[AFTER_RESULT] === "function") {
537
+ const { [AFTER_RESULT]: run, ...rest } = raw;
538
+ after = run;
539
+ out = rest;
540
+ }
479
541
  await postToolResult({ ...base, ok: true, result: typeof out === "string" ? out : JSON.stringify(out ?? { ok: true }) });
542
+ after?.();
480
543
  } catch (err) {
481
544
  await postToolResult({ ...base, ok: false, result: err.message });
482
545
  }
@@ -558,13 +621,24 @@ var OyehelloChat = (function (exports) {
558
621
  if (resumed) {
559
622
  session = resumed;
560
623
  emit({ type: "agent", agent: resumed.agent });
624
+ try {
625
+ const history2 = await getHistory({ baseUrl: options.baseUrl, sessionId: resumed.sessionId, sessionToken: resumed.sessionToken, fetchImpl });
626
+ for (const h of history2) {
627
+ const turnId = h.role === "assistant" || h.role === "human" ? h.turnId : void 0;
628
+ messages.push({ id: uid("m"), role: h.role, text: h.text, streaming: false, turnId });
629
+ if (h.turnId) seenTurnIds.add(h.turnId);
630
+ }
631
+ emitMessages();
632
+ } catch {
633
+ }
561
634
  await openStream();
562
635
  scheduleRefresh();
563
636
  flushJourney();
564
637
  return;
565
638
  }
566
639
  if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
567
- session = await openSession({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, user: options.user, clientTools: Array.from(toolHandlers.keys()), fingerprint: deviceFingerprint ?? void 0, fetchImpl });
640
+ const advertisedTools = /* @__PURE__ */ new Set([...toolHandlers.keys(), ...Object.keys(BUILTIN_TOOL_HANDLERS)]);
641
+ session = await openSession({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, user: options.user, clientTools: Array.from(advertisedTools), fingerprint: deviceFingerprint ?? void 0, fetchImpl });
568
642
  persistSession(session);
569
643
  if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
570
644
  warnedProtocol = true;
@@ -698,6 +772,44 @@ var OyehelloChat = (function (exports) {
698
772
  };
699
773
  }
700
774
 
775
+ // src/page-theme.ts
776
+ var WATCHED = ["class", "data-theme", "data-mode", "style"];
777
+ function themeOf(node) {
778
+ for (const attr of ["data-theme", "data-mode"]) {
779
+ const v2 = node.getAttribute(attr);
780
+ if (v2 === "dark" || v2 === "light") return v2;
781
+ }
782
+ if (node.classList.contains("dark")) return "dark";
783
+ if (node.classList.contains("light")) return "light";
784
+ return null;
785
+ }
786
+ function resolvePageTheme(el) {
787
+ for (let node = el.parentElement; node; node = node.parentElement) {
788
+ const t = themeOf(node);
789
+ if (t) return t;
790
+ }
791
+ const scheme = (document.documentElement.style.colorScheme || getComputedStyle(document.documentElement).colorScheme || "").trim();
792
+ return scheme === "dark" || scheme === "light" ? scheme : null;
793
+ }
794
+ function watchPageTheme(el, onChange) {
795
+ let last;
796
+ const check = () => {
797
+ const t = resolvePageTheme(el);
798
+ if (t !== last) {
799
+ last = t;
800
+ onChange(t);
801
+ }
802
+ };
803
+ check();
804
+ if (typeof MutationObserver === "undefined") return () => {
805
+ };
806
+ const mo = new MutationObserver(check);
807
+ for (let node = el.parentElement; node; node = node.parentElement) {
808
+ mo.observe(node, { attributes: true, attributeFilter: WATCHED });
809
+ }
810
+ return () => mo.disconnect();
811
+ }
812
+
701
813
  // ../../node_modules/marked/lib/marked.esm.js
702
814
  function A() {
703
815
  return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
@@ -3659,11 +3771,15 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3659
3771
  * body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
3660
3772
  * inline with !important at mount (see element.ts) to beat an external !important rule. */
3661
3773
  display: block; visibility: visible; opacity: 1;
3774
+ /* The closed panel still takes up its full size in the layout (it fades, it is not removed), so
3775
+ * the host box covers ~384x620px of the page. The host and its wrapper must not catch clicks;
3776
+ * only the visible parts opt back in (launcher, teaser while shown, panel while open). */
3777
+ pointer-events: none;
3662
3778
  font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
3663
3779
  -webkit-font-smoothing: antialiased;
3664
3780
  }
3665
3781
  @media (prefers-color-scheme: dark) {
3666
- :host(:not([theme="light"])) {
3782
+ :host(:not([theme="light"]):not([data-oc-page-theme="light"])) {
3667
3783
  --oc-bg: #000000; --oc-bg-sunk: #000000;
3668
3784
  --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
3669
3785
  --oc-border: rgba(255,255,255,0.09);
@@ -3673,7 +3789,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3673
3789
  --oc-glass-border: rgba(255,255,255,0.12);
3674
3790
  }
3675
3791
  }
3676
- :host([theme="dark"]) {
3792
+ /* Explicit dark, or auto on a page that declares dark (data-oc-page-theme, set by page-theme.ts). The OS rule above
3793
+ * only applies when the page declares nothing; a page that declares light keeps the light base. */
3794
+ :host([theme="dark"]), :host(:not([theme="light"])[data-oc-page-theme="dark"]) {
3677
3795
  --oc-bg: #000000; --oc-bg-sunk: #000000;
3678
3796
  --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
3679
3797
  --oc-border: rgba(255,255,255,0.09);
@@ -3685,7 +3803,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3685
3803
  * { box-sizing: border-box; }
3686
3804
  button { font-family: inherit; }
3687
3805
 
3688
- .oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
3806
+ .oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; pointer-events: none; }
3689
3807
 
3690
3808
  /* \u2500\u2500 Launcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3691
3809
  .launcher {
@@ -3694,6 +3812,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3694
3812
  box-shadow: 0 8px 22px -8px color-mix(in oklab, var(--oc-ac) 70%, transparent), 0 18px 48px -18px rgba(0,0,0,0.6);
3695
3813
  color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
3696
3814
  transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
3815
+ pointer-events: auto;
3697
3816
  }
3698
3817
  .launcher:hover { transform: scale(1.06); }
3699
3818
  .launcher:active { transform: scale(0.93); }
@@ -4061,6 +4180,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4061
4180
  // Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
4062
4181
  // connectedCallback and torn down in disconnectedCallback alongside everything else.
4063
4182
  uninstallSpaHooks = null;
4183
+ unwatchPageTheme = null;
4064
4184
  // ── shadow refs ──
4065
4185
  oc;
4066
4186
  bodyEl;
@@ -4094,6 +4214,11 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4094
4214
  this.uninstallSpaHooks = null;
4095
4215
  }
4096
4216
  this.renderShell();
4217
+ this.unwatchPageTheme?.();
4218
+ this.unwatchPageTheme = watchPageTheme(this, (t) => {
4219
+ if (t) this.setAttribute("data-oc-page-theme", t);
4220
+ else this.removeAttribute("data-oc-page-theme");
4221
+ });
4097
4222
  this.style.setProperty("display", "block", "important");
4098
4223
  this.style.setProperty("visibility", "visible", "important");
4099
4224
  this.style.setProperty("opacity", "1", "important");
@@ -4204,6 +4329,8 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4204
4329
  this.teardownIdle();
4205
4330
  this.uninstallSpaHooks?.();
4206
4331
  this.uninstallSpaHooks = null;
4332
+ this.unwatchPageTheme?.();
4333
+ this.unwatchPageTheme = null;
4207
4334
  for (const t of this.timers) clearTimeout(t);
4208
4335
  for (const url of this.imagePreviewCache.values()) {
4209
4336
  if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
@@ -4214,6 +4341,11 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4214
4341
  if (this.client) this.client.registerTools(handlers);
4215
4342
  else Object.assign(this.pendingTools, handlers);
4216
4343
  }
4344
+ /** Open the chat window, exactly as a launcher click does. No-op if already open or not rendered. */
4345
+ open() {
4346
+ if (!this.oc || this.uiOpen) return;
4347
+ this.openPanel();
4348
+ }
4217
4349
  key(k2) {
4218
4350
  return `oyehello-chat:${k2}:${this.getAttribute("agent-id") ?? ""}`;
4219
4351
  }