@oyehello/chat 0.5.2 → 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",
@@ -317,17 +327,21 @@ var isSafeHttpUrl = (u) => {
317
327
  return false;
318
328
  }
319
329
  };
330
+ var AFTER_RESULT = "__ohAfterResult";
320
331
  var BUILTIN_TOOL_HANDLERS = {
321
332
  navigate: (args) => {
322
333
  const path = String(args.path ?? "");
323
334
  if (!isSafeRelativePath(path)) return { ok: false, error: "unsafe navigate path" };
324
- window.location.assign(path);
325
- return { ok: true };
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) };
326
339
  },
327
340
  openUrl: (args) => {
328
341
  const url = String(args.url ?? "");
329
342
  if (!isSafeHttpUrl(url)) return { ok: false, error: "unsafe url" };
330
- window.open(url, args.newTab === false ? "_self" : "_blank", "noopener,noreferrer");
343
+ const sameHost = new URL(url, window.location.origin).hostname === window.location.hostname;
344
+ window.open(url, sameHost ? "_self" : "_blank", "noopener,noreferrer");
331
345
  return { ok: true };
332
346
  }
333
347
  };
@@ -439,6 +453,23 @@ function createChatClient(options) {
439
453
  } catch {
440
454
  }
441
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
+ };
442
473
  const emit = (e) => {
443
474
  for (const l of listeners) l(e);
444
475
  };
@@ -487,8 +518,9 @@ function createChatClient(options) {
487
518
  } else if (ev.type === "error") {
488
519
  emit({ type: "error", error: new Error(ev.message) });
489
520
  } else if (ev.type === "tool_call") {
490
- if (seenCallIds.has(ev.callId)) return;
521
+ if (seenCallIds.has(ev.callId) || handledCallIds().includes(ev.callId)) return;
491
522
  seenCallIds.add(ev.callId);
523
+ rememberHandledCall(ev.callId);
492
524
  const handler = toolHandlers.get(ev.name) ?? BUILTIN_TOOL_HANDLERS[ev.name];
493
525
  void (async () => {
494
526
  if (!session) return;
@@ -498,8 +530,16 @@ function createChatClient(options) {
498
530
  return;
499
531
  }
500
532
  try {
501
- 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
+ }
502
541
  await postToolResult({ ...base, ok: true, result: typeof out === "string" ? out : JSON.stringify(out ?? { ok: true }) });
542
+ after?.();
503
543
  } catch (err) {
504
544
  await postToolResult({ ...base, ok: false, result: err.message });
505
545
  }
@@ -581,6 +621,16 @@ function createChatClient(options) {
581
621
  if (resumed) {
582
622
  session = resumed;
583
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
+ }
584
634
  await openStream();
585
635
  scheduleRefresh();
586
636
  flushJourney();
@@ -721,6 +771,44 @@ function createChatClient(options) {
721
771
  }
722
772
  };
723
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
+ }
724
812
  marked.setOptions({ gfm: true, breaks: true });
725
813
  var hookInstalled = false;
726
814
  function installLinkHook() {
@@ -803,11 +891,15 @@ var STYLES = `
803
891
  * body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
804
892
  * inline with !important at mount (see element.ts) to beat an external !important rule. */
805
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;
806
898
  font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
807
899
  -webkit-font-smoothing: antialiased;
808
900
  }
809
901
  @media (prefers-color-scheme: dark) {
810
- :host(:not([theme="light"])) {
902
+ :host(:not([theme="light"]):not([data-oc-page-theme="light"])) {
811
903
  --oc-bg: #000000; --oc-bg-sunk: #000000;
812
904
  --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
813
905
  --oc-border: rgba(255,255,255,0.09);
@@ -817,7 +909,9 @@ var STYLES = `
817
909
  --oc-glass-border: rgba(255,255,255,0.12);
818
910
  }
819
911
  }
820
- :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"]) {
821
915
  --oc-bg: #000000; --oc-bg-sunk: #000000;
822
916
  --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
823
917
  --oc-border: rgba(255,255,255,0.09);
@@ -829,7 +923,7 @@ var STYLES = `
829
923
  * { box-sizing: border-box; }
830
924
  button { font-family: inherit; }
831
925
 
832
- .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; }
833
927
 
834
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 */
835
929
  .launcher {
@@ -838,6 +932,7 @@ var STYLES = `
838
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);
839
933
  color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
840
934
  transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
935
+ pointer-events: auto;
841
936
  }
842
937
  .launcher:hover { transform: scale(1.06); }
843
938
  .launcher:active { transform: scale(0.93); }
@@ -1205,6 +1300,7 @@ var OyehelloChatElement = class extends HTMLElement {
1205
1300
  // Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
1206
1301
  // connectedCallback and torn down in disconnectedCallback alongside everything else.
1207
1302
  uninstallSpaHooks = null;
1303
+ unwatchPageTheme = null;
1208
1304
  // ── shadow refs ──
1209
1305
  oc;
1210
1306
  bodyEl;
@@ -1238,6 +1334,11 @@ var OyehelloChatElement = class extends HTMLElement {
1238
1334
  this.uninstallSpaHooks = null;
1239
1335
  }
1240
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
+ });
1241
1342
  this.style.setProperty("display", "block", "important");
1242
1343
  this.style.setProperty("visibility", "visible", "important");
1243
1344
  this.style.setProperty("opacity", "1", "important");
@@ -1348,6 +1449,8 @@ var OyehelloChatElement = class extends HTMLElement {
1348
1449
  this.teardownIdle();
1349
1450
  this.uninstallSpaHooks?.();
1350
1451
  this.uninstallSpaHooks = null;
1452
+ this.unwatchPageTheme?.();
1453
+ this.unwatchPageTheme = null;
1351
1454
  for (const t of this.timers) clearTimeout(t);
1352
1455
  for (const url of this.imagePreviewCache.values()) {
1353
1456
  if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
@@ -1358,6 +1461,11 @@ var OyehelloChatElement = class extends HTMLElement {
1358
1461
  if (this.client) this.client.registerTools(handlers);
1359
1462
  else Object.assign(this.pendingTools, handlers);
1360
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
+ }
1361
1469
  key(k) {
1362
1470
  return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
1363
1471
  }