@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.js CHANGED
@@ -61,6 +61,16 @@ async function postMessage(opts) {
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 @@ function installSpaHooks(onNav) {
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 = (u) => {
323
+ try {
324
+ const parsed = new URL(u, 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 @@ function createChatClient(options) {
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 v = raw ? JSON.parse(raw) : null;
461
+ return v && session && v.sessionId === session.sessionId && Array.isArray(v.ids) ? v.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 @@ function createChatClient(options) {
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 @@ function createChatClient(options) {
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 @@ function createChatClient(options) {
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;
@@ -697,6 +771,44 @@ function createChatClient(options) {
697
771
  }
698
772
  };
699
773
  }
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 v = node.getAttribute(attr);
780
+ if (v === "dark" || v === "light") return v;
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
+ }
700
812
  marked.setOptions({ gfm: true, breaks: true });
701
813
  var hookInstalled = false;
702
814
  function installLinkHook() {
@@ -779,11 +891,15 @@ var STYLES = `
779
891
  * body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
780
892
  * inline with !important at mount (see element.ts) to beat an external !important rule. */
781
893
  display: block; visibility: visible; opacity: 1;
894
+ /* The closed panel still takes up its full size in the layout (it fades, it is not removed), so
895
+ * the host box covers ~384x620px of the page. The host and its wrapper must not catch clicks;
896
+ * only the visible parts opt back in (launcher, teaser while shown, panel while open). */
897
+ pointer-events: none;
782
898
  font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
783
899
  -webkit-font-smoothing: antialiased;
784
900
  }
785
901
  @media (prefers-color-scheme: dark) {
786
- :host(:not([theme="light"])) {
902
+ :host(:not([theme="light"]):not([data-oc-page-theme="light"])) {
787
903
  --oc-bg: #000000; --oc-bg-sunk: #000000;
788
904
  --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
789
905
  --oc-border: rgba(255,255,255,0.09);
@@ -793,7 +909,9 @@ var STYLES = `
793
909
  --oc-glass-border: rgba(255,255,255,0.12);
794
910
  }
795
911
  }
796
- :host([theme="dark"]) {
912
+ /* Explicit dark, or auto on a page that declares dark (data-oc-page-theme, set by page-theme.ts). The OS rule above
913
+ * only applies when the page declares nothing; a page that declares light keeps the light base. */
914
+ :host([theme="dark"]), :host(:not([theme="light"])[data-oc-page-theme="dark"]) {
797
915
  --oc-bg: #000000; --oc-bg-sunk: #000000;
798
916
  --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
799
917
  --oc-border: rgba(255,255,255,0.09);
@@ -805,7 +923,7 @@ var STYLES = `
805
923
  * { box-sizing: border-box; }
806
924
  button { font-family: inherit; }
807
925
 
808
- .oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
926
+ .oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; pointer-events: none; }
809
927
 
810
928
  /* \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 */
811
929
  .launcher {
@@ -814,6 +932,7 @@ var STYLES = `
814
932
  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);
815
933
  color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
816
934
  transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
935
+ pointer-events: auto;
817
936
  }
818
937
  .launcher:hover { transform: scale(1.06); }
819
938
  .launcher:active { transform: scale(0.93); }
@@ -1181,6 +1300,7 @@ var OyehelloChatElement = class extends HTMLElement {
1181
1300
  // Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
1182
1301
  // connectedCallback and torn down in disconnectedCallback alongside everything else.
1183
1302
  uninstallSpaHooks = null;
1303
+ unwatchPageTheme = null;
1184
1304
  // ── shadow refs ──
1185
1305
  oc;
1186
1306
  bodyEl;
@@ -1214,6 +1334,11 @@ var OyehelloChatElement = class extends HTMLElement {
1214
1334
  this.uninstallSpaHooks = null;
1215
1335
  }
1216
1336
  this.renderShell();
1337
+ this.unwatchPageTheme?.();
1338
+ this.unwatchPageTheme = watchPageTheme(this, (t) => {
1339
+ if (t) this.setAttribute("data-oc-page-theme", t);
1340
+ else this.removeAttribute("data-oc-page-theme");
1341
+ });
1217
1342
  this.style.setProperty("display", "block", "important");
1218
1343
  this.style.setProperty("visibility", "visible", "important");
1219
1344
  this.style.setProperty("opacity", "1", "important");
@@ -1324,6 +1449,8 @@ var OyehelloChatElement = class extends HTMLElement {
1324
1449
  this.teardownIdle();
1325
1450
  this.uninstallSpaHooks?.();
1326
1451
  this.uninstallSpaHooks = null;
1452
+ this.unwatchPageTheme?.();
1453
+ this.unwatchPageTheme = null;
1327
1454
  for (const t of this.timers) clearTimeout(t);
1328
1455
  for (const url of this.imagePreviewCache.values()) {
1329
1456
  if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
@@ -1334,6 +1461,11 @@ var OyehelloChatElement = class extends HTMLElement {
1334
1461
  if (this.client) this.client.registerTools(handlers);
1335
1462
  else Object.assign(this.pendingTools, handlers);
1336
1463
  }
1464
+ /** Open the chat window, exactly as a launcher click does. No-op if already open or not rendered. */
1465
+ open() {
1466
+ if (!this.oc || this.uiOpen) return;
1467
+ this.openPanel();
1468
+ }
1337
1469
  key(k) {
1338
1470
  return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
1339
1471
  }