@helpai/elements 0.50.7 → 0.51.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/web-component.mjs CHANGED
@@ -1794,7 +1794,7 @@ function createAuth(opts) {
1794
1794
  }
1795
1795
 
1796
1796
  // src/core/version.ts
1797
- var ELEMENTS_VERSION = true ? "0.50.7" : "0.0.0-dev";
1797
+ var ELEMENTS_VERSION = true ? "0.51.0" : "0.0.0-dev";
1798
1798
  var ELEMENTS_VERSION_PARAM = "_ev";
1799
1799
 
1800
1800
  // src/stream/types.ts
@@ -2071,6 +2071,13 @@ var AgentTransport = class {
2071
2071
  // (visitor, conversation) pair.
2072
2072
  __publicField(this, "visitorId");
2073
2073
  __publicField(this, "conversationId");
2074
+ /**
2075
+ * Outcome of the most recent `resumeStream()` once it found NO replayable SSE — drives the App's
2076
+ * dangling-turn recovery. `live` = a stream was replayed; otherwise the server's JSON recovery status.
2077
+ * `needs-generation` means a durable user turn has no reply (e.g. a fast send→refresh aborted the
2078
+ * deferred LLM run) → the App re-sends to generate it. Reset at the start of each `resumeStream()`.
2079
+ */
2080
+ __publicField(this, "resumeStatus");
2074
2081
  // Host-asserted context (sanitised `userContext` / `pageContext`), refreshed via
2075
2082
  // `setContext` and carried as the envelope's optional `context` so the backend
2076
2083
  // always sees the visitor's latest context as they navigate.
@@ -2427,6 +2434,7 @@ var AgentTransport = class {
2427
2434
  resumeStream() {
2428
2435
  const ctrl = new AbortController();
2429
2436
  const seenIds = /* @__PURE__ */ new Set();
2437
+ this.resumeStatus = void 0;
2430
2438
  const iter = this.resumeStreamGen(ctrl, seenIds, true);
2431
2439
  return { iter, cancel: () => ctrl.abort() };
2432
2440
  }
@@ -2481,6 +2489,15 @@ var AgentTransport = class {
2481
2489
  log5.debug("resume \u2192 no in-flight stream", { status: res.status });
2482
2490
  return;
2483
2491
  }
2492
+ const contentType = res.headers.get("content-type") ?? "";
2493
+ if (!contentType.includes("text/event-stream")) {
2494
+ const status = await res.json().then((body) => body.status).catch(() => void 0);
2495
+ log5.debug("resume \u2192 no live stream (status response)", { status, attempt });
2496
+ if (status === "pending" && attempt < MAX_RESUME_ATTEMPTS) continue;
2497
+ this.resumeStatus = status === "needs-generation" ? "needs-generation" : "completed";
2498
+ return;
2499
+ }
2500
+ this.resumeStatus = "live";
2484
2501
  const before = seenIds.size;
2485
2502
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
2486
2503
  if (seenIds.size === before) {
@@ -2700,6 +2717,7 @@ function toReactive(m) {
2700
2717
  role: m.role,
2701
2718
  createdAt: m.createdAt,
2702
2719
  status: m.status,
2720
+ canceled: m.canceled,
2703
2721
  errorText: m.errorText,
2704
2722
  finishReason: m.finishReason,
2705
2723
  serverMessageId: m.serverMessageId,
@@ -2763,7 +2781,11 @@ function fromWireMessage(w) {
2763
2781
  // Real send time when the backend provides it; otherwise fall back to now
2764
2782
  // (older backends) so the timestamp + day divider still render.
2765
2783
  createdAt: parseWireDate(w.createdAt) ?? Date.now(),
2766
- status: "complete",
2784
+ // Map the persisted terminal status onto the bubble's render state: `failed` → an error bubble;
2785
+ // `canceled` → `complete` + the `canceled` flag (an empty canceled turn is hidden on render — see
2786
+ // `message-bubble`); everything else is a normal completed turn.
2787
+ status: w.status === "failed" ? "error" : "complete",
2788
+ canceled: w.status === "canceled",
2767
2789
  partsSig: signal(parts)
2768
2790
  };
2769
2791
  }
@@ -2773,6 +2795,7 @@ function fromReactive(m) {
2773
2795
  role: m.role,
2774
2796
  createdAt: m.createdAt,
2775
2797
  status: m.status,
2798
+ canceled: m.canceled,
2776
2799
  errorText: m.errorText,
2777
2800
  finishReason: m.finishReason,
2778
2801
  serverMessageId: m.serverMessageId,
@@ -2788,11 +2811,16 @@ function assistantText(m) {
2788
2811
  }
2789
2812
  return out;
2790
2813
  }
2791
- function isEmptyAssistantReply(m) {
2792
- if (m.role !== "assistant" || m.status === "streaming") return false;
2814
+ function hasNoVisibleAnswer(m) {
2793
2815
  if (assistantText(m).trim() !== "") return false;
2794
2816
  return !m.partsSig.value.some((p36) => p36.kind === "tool" || p36.kind === "file" || p36.kind === "source");
2795
2817
  }
2818
+ function isEmptyAssistantReply(m) {
2819
+ return m.role === "assistant" && m.status !== "streaming" && hasNoVisibleAnswer(m);
2820
+ }
2821
+ function isHiddenCanceledTurn(m) {
2822
+ return m.canceled === true && isEmptyAssistantReply(m);
2823
+ }
2796
2824
  function partToReactive(p36) {
2797
2825
  if (p36.kind === "text" || p36.kind === "reasoning") {
2798
2826
  return { kind: p36.kind, id: p36.id, textSig: signal(p36.text), doneSig: signal(p36.done) };
@@ -2952,6 +2980,8 @@ var StreamReducer = class {
2952
2980
  return;
2953
2981
  case "data-notification":
2954
2982
  return;
2983
+ case "data-conversation-rebind":
2984
+ return;
2955
2985
  default: {
2956
2986
  const _exhaustive = chunk;
2957
2987
  void _exhaustive;
@@ -5664,6 +5694,7 @@ function MessageBubble({
5664
5694
  const parts = useComputed5(() => message.partsSig.value);
5665
5695
  const partList = parts.value;
5666
5696
  const emptyReply = useComputed5(() => isEmptyAssistantReply(message));
5697
+ const hideCanceledHusk = useComputed5(() => isHiddenCanceledTurn(message));
5667
5698
  const hasAnswerText = useComputed5(
5668
5699
  () => message.partsSig.value.some((part) => part.kind === "text" && part.textSig.value.length > 0)
5669
5700
  );
@@ -5674,6 +5705,7 @@ function MessageBubble({
5674
5705
  const bufferedHold = responseMode === "buffered" && streaming;
5675
5706
  const working = streaming && !hasAnswerText.value;
5676
5707
  const showStreamDots = streaming && !bufferedHold && !(reasoningVisible.value && working);
5708
+ if (hideCanceledHusk.value) return null;
5677
5709
  const stamp = formatStamp(message.createdAt);
5678
5710
  return /* @__PURE__ */ jsx18("div", { class: `${p17}-bubble-row`, "data-role": message.role, "data-testid": tid(TID.messageBubble, message.id), children: /* @__PURE__ */ jsxs15("div", { class: `${p17}-bubble-col`, children: [
5679
5711
  /* @__PURE__ */ jsxs15("div", { class: `${p17}-bubble`, children: [
@@ -6016,6 +6048,7 @@ function MessageList({
6016
6048
  }
6017
6049
  };
6018
6050
  for (const m of messages.value) {
6051
+ if (m.role === "assistant" && m.id !== lastId && isEmptyAssistantReply(m)) continue;
6019
6052
  flushMarkersBefore(m.createdAt);
6020
6053
  const day = dayKey(m.createdAt);
6021
6054
  if (day && day !== prevDay) {
@@ -7473,6 +7506,9 @@ function allToolPartsSettled(msg) {
7473
7506
  return s === "output-available" || s === "output-error" || s === "output-denied" || s === "approval-responded";
7474
7507
  });
7475
7508
  }
7509
+ function isAbortError(error) {
7510
+ return error instanceof DOMException ? error.name === "AbortError" : error?.name === "AbortError";
7511
+ }
7476
7512
  function App({ options, hostElement, bus }) {
7477
7513
  const [persistence] = useState14(
7478
7514
  () => createPersistence(options.widgetId, options.storage, options.aiAgentDeploymentId)
@@ -7615,6 +7651,8 @@ function App({ options, hostElement, bus }) {
7615
7651
  const homeNavSeeded = useRef9(false);
7616
7652
  const resumeActiveRef = useRef9(false);
7617
7653
  const resumeBubbleRef = useRef9(null);
7654
+ const regenerateDanglingRef = useRef9(null);
7655
+ const regenInFlightRef = useRef9(false);
7618
7656
  useEffect16(() => {
7619
7657
  if (!conversationReady || homeNavSeeded.current) return;
7620
7658
  homeNavSeeded.current = true;
@@ -7684,6 +7722,7 @@ function App({ options, hostElement, bus }) {
7684
7722
  } else {
7685
7723
  messagesSig.value = withWelcomeRows([], markers);
7686
7724
  }
7725
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7687
7726
  } catch (err) {
7688
7727
  if (isStale()) return;
7689
7728
  log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
@@ -7813,11 +7852,28 @@ function App({ options, hostElement, bus }) {
7813
7852
  useEffect16(() => {
7814
7853
  if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
7815
7854
  }, [effectiveLocale, activeLocale]);
7855
+ const adoptConversationRebind = useCallback6(
7856
+ (chunk) => {
7857
+ if (chunk.type !== "data-conversation-rebind") return false;
7858
+ const next = chunk.data?.conversationId;
7859
+ if (next && next !== conversationIdSig.value) {
7860
+ const previous = conversationIdSig.value;
7861
+ conversationIdSig.value = next;
7862
+ persistence.saveConversationId(next);
7863
+ transport.primeIdentity(void 0, next);
7864
+ log16.info("conversation rebound (in-stream)", { previous, current: next });
7865
+ if (previous) bus.emit("conversationRebound", { previous, current: next, reason: next });
7866
+ }
7867
+ return true;
7868
+ },
7869
+ [conversationIdSig, persistence, transport, bus]
7870
+ );
7816
7871
  const runResume = useCallback6(
7817
7872
  async (handle) => {
7818
7873
  let bubble = null;
7819
7874
  try {
7820
7875
  for await (const evt of handle.iter) {
7876
+ if (adoptConversationRebind(evt.chunk)) continue;
7821
7877
  if (!bubble) {
7822
7878
  bubble = makeAssistantMessage();
7823
7879
  resumeBubbleRef.current = bubble;
@@ -7861,7 +7917,7 @@ function App({ options, hostElement, bus }) {
7861
7917
  }
7862
7918
  }
7863
7919
  },
7864
- [reducer, messagesSig, feedback, bus, options, homeNav]
7920
+ [reducer, messagesSig, feedback, bus, options, homeNav, adoptConversationRebind]
7865
7921
  );
7866
7922
  const resumeHandleRef = useRef9(null);
7867
7923
  useEffect16(() => {
@@ -7886,16 +7942,22 @@ function App({ options, hostElement, bus }) {
7886
7942
  setFormsReady(true);
7887
7943
  }
7888
7944
  })();
7889
- if (pendingThreadRef.current) {
7890
- pendingThreadRef.current = false;
7891
- const persisted = conversationIdSig.value;
7892
- if (persisted) void loadThread(persisted);
7893
- }
7894
- if (conversationIdSig.value) {
7895
- const handle = transport.resumeStream();
7896
- resumeHandleRef.current = handle;
7897
- void runResume(handle);
7898
- }
7945
+ void (async () => {
7946
+ const tasks = [];
7947
+ if (pendingThreadRef.current) {
7948
+ pendingThreadRef.current = false;
7949
+ const persisted = conversationIdSig.value;
7950
+ if (persisted) tasks.push(loadThread(persisted));
7951
+ }
7952
+ if (conversationIdSig.value) {
7953
+ const handle = transport.resumeStream();
7954
+ resumeHandleRef.current = handle;
7955
+ tasks.push(runResume(handle));
7956
+ }
7957
+ if (tasks.length === 0) return;
7958
+ await Promise.allSettled(tasks);
7959
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7960
+ })();
7899
7961
  }, [activated]);
7900
7962
  const formsLocaleRef = useRef9(null);
7901
7963
  useEffect16(() => {
@@ -7968,6 +8030,7 @@ function App({ options, hostElement, bus }) {
7968
8030
  setActiveCancel(() => handle.cancel);
7969
8031
  try {
7970
8032
  for await (const evt of handle.iter) {
8033
+ if (adoptConversationRebind(evt.chunk)) continue;
7971
8034
  reducer.apply(evt.chunk);
7972
8035
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) {
7973
8036
  setCanSend(false);
@@ -7988,11 +8051,19 @@ function App({ options, hostElement, bus }) {
7988
8051
  emitMessage(bus, options, "assistant", assistantText(assistantMsg));
7989
8052
  }
7990
8053
  } catch (error) {
7991
- assistantMsg.status = "error";
7992
- assistantMsg.errorText = errorMessageFor(error, options.strings);
7993
- feedback.play("error");
7994
- bus.emit("error", error);
7995
- options.onError?.(error);
8054
+ if (isAbortError(error)) {
8055
+ if (hasNoVisibleAnswer(assistantMsg)) {
8056
+ messagesSig.value = messagesSig.value.filter((m) => m !== assistantMsg);
8057
+ } else {
8058
+ assistantMsg.status = "complete";
8059
+ }
8060
+ } else {
8061
+ assistantMsg.status = "error";
8062
+ assistantMsg.errorText = errorMessageFor(error, options.strings);
8063
+ feedback.play("error");
8064
+ bus.emit("error", error);
8065
+ options.onError?.(error);
8066
+ }
7996
8067
  } finally {
7997
8068
  reducer.detach();
7998
8069
  setStreaming(false);
@@ -8000,8 +8071,35 @@ function App({ options, hostElement, bus }) {
8000
8071
  messagesSig.value = [...messagesSig.value];
8001
8072
  }
8002
8073
  },
8003
- [transport, reducer, messagesSig, conversationIdSig, options, bus, feedback, persistence, visitorId]
8074
+ [
8075
+ transport,
8076
+ reducer,
8077
+ messagesSig,
8078
+ conversationIdSig,
8079
+ options,
8080
+ bus,
8081
+ feedback,
8082
+ persistence,
8083
+ visitorId,
8084
+ adoptConversationRebind
8085
+ ]
8004
8086
  );
8087
+ const regenerateDanglingTurn = useCallback6(() => {
8088
+ if (streaming || regenInFlightRef.current || resumeBubbleRef.current) return;
8089
+ const last = messagesSig.value.filter((m) => !m.ephemeral).at(-1);
8090
+ if (last?.role !== "user") return;
8091
+ regenInFlightRef.current = true;
8092
+ log16.info("regenerating dangling user turn (needs-generation)");
8093
+ const assistantMsg = makeAssistantMessage();
8094
+ messagesSig.value = [...messagesSig.value, assistantMsg];
8095
+ reducer.attach(assistantMsg);
8096
+ void streamAssistant(assistantMsg, { continueExisting: false }).finally(() => {
8097
+ regenInFlightRef.current = false;
8098
+ });
8099
+ }, [streaming, messagesSig, reducer, streamAssistant]);
8100
+ useEffect16(() => {
8101
+ regenerateDanglingRef.current = regenerateDanglingTurn;
8102
+ }, [regenerateDanglingTurn]);
8005
8103
  const decideTool = useCallback6(
8006
8104
  (toolCallId, mutate) => {
8007
8105
  if (streaming) return;