@helpai/elements 0.50.8 → 0.51.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.
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.8" : "0.0.0-dev";
1797
+ var ELEMENTS_VERSION = true ? "0.51.1" : "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.
@@ -2406,7 +2413,7 @@ var AgentTransport = class {
2406
2413
  const seenIds = /* @__PURE__ */ new Set();
2407
2414
  const iter = this.runStream(body, ctrl, seenIds);
2408
2415
  const cancel = () => this.cancelStream(ctrl, body.conversationId);
2409
- return { iter, cancel };
2416
+ return { iter, cancel, detach: () => ctrl.abort() };
2410
2417
  }
2411
2418
  /**
2412
2419
  * Re-attach to an in-flight reply on a cold page load / refresh.
@@ -2427,8 +2434,10 @@ 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
- return { iter, cancel: () => ctrl.abort() };
2439
+ const stop = () => ctrl.abort();
2440
+ return { iter, cancel: stop, detach: stop };
2432
2441
  }
2433
2442
  // ---- Stream orchestration ---------------------------------------------
2434
2443
  async *runStream(body, ctrl, seenIds) {
@@ -2481,6 +2490,15 @@ var AgentTransport = class {
2481
2490
  log5.debug("resume \u2192 no in-flight stream", { status: res.status });
2482
2491
  return;
2483
2492
  }
2493
+ const contentType = res.headers.get("content-type") ?? "";
2494
+ if (!contentType.includes("text/event-stream")) {
2495
+ const status = await res.json().then((body) => body.status).catch(() => void 0);
2496
+ log5.debug("resume \u2192 no live stream (status response)", { status, attempt });
2497
+ if (status === "pending" && attempt < MAX_RESUME_ATTEMPTS) continue;
2498
+ this.resumeStatus = status === "needs-generation" ? "needs-generation" : "completed";
2499
+ return;
2500
+ }
2501
+ this.resumeStatus = "live";
2484
2502
  const before = seenIds.size;
2485
2503
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
2486
2504
  if (seenIds.size === before) {
@@ -2700,6 +2718,7 @@ function toReactive(m) {
2700
2718
  role: m.role,
2701
2719
  createdAt: m.createdAt,
2702
2720
  status: m.status,
2721
+ canceled: m.canceled,
2703
2722
  errorText: m.errorText,
2704
2723
  finishReason: m.finishReason,
2705
2724
  serverMessageId: m.serverMessageId,
@@ -2763,7 +2782,11 @@ function fromWireMessage(w) {
2763
2782
  // Real send time when the backend provides it; otherwise fall back to now
2764
2783
  // (older backends) so the timestamp + day divider still render.
2765
2784
  createdAt: parseWireDate(w.createdAt) ?? Date.now(),
2766
- status: "complete",
2785
+ // Map the persisted terminal status onto the bubble's render state: `failed` → an error bubble;
2786
+ // `canceled` → `complete` + the `canceled` flag (an empty canceled turn is hidden on render — see
2787
+ // `message-bubble`); everything else is a normal completed turn.
2788
+ status: w.status === "failed" ? "error" : "complete",
2789
+ canceled: w.status === "canceled",
2767
2790
  partsSig: signal(parts)
2768
2791
  };
2769
2792
  }
@@ -2773,6 +2796,7 @@ function fromReactive(m) {
2773
2796
  role: m.role,
2774
2797
  createdAt: m.createdAt,
2775
2798
  status: m.status,
2799
+ canceled: m.canceled,
2776
2800
  errorText: m.errorText,
2777
2801
  finishReason: m.finishReason,
2778
2802
  serverMessageId: m.serverMessageId,
@@ -2795,6 +2819,9 @@ function hasNoVisibleAnswer(m) {
2795
2819
  function isEmptyAssistantReply(m) {
2796
2820
  return m.role === "assistant" && m.status !== "streaming" && hasNoVisibleAnswer(m);
2797
2821
  }
2822
+ function isHiddenCanceledTurn(m) {
2823
+ return m.canceled === true && isEmptyAssistantReply(m);
2824
+ }
2798
2825
  function partToReactive(p36) {
2799
2826
  if (p36.kind === "text" || p36.kind === "reasoning") {
2800
2827
  return { kind: p36.kind, id: p36.id, textSig: signal(p36.text), doneSig: signal(p36.done) };
@@ -2954,6 +2981,8 @@ var StreamReducer = class {
2954
2981
  return;
2955
2982
  case "data-notification":
2956
2983
  return;
2984
+ case "data-conversation-rebind":
2985
+ return;
2957
2986
  default: {
2958
2987
  const _exhaustive = chunk;
2959
2988
  void _exhaustive;
@@ -5666,6 +5695,7 @@ function MessageBubble({
5666
5695
  const parts = useComputed5(() => message.partsSig.value);
5667
5696
  const partList = parts.value;
5668
5697
  const emptyReply = useComputed5(() => isEmptyAssistantReply(message));
5698
+ const hideCanceledHusk = useComputed5(() => isHiddenCanceledTurn(message));
5669
5699
  const hasAnswerText = useComputed5(
5670
5700
  () => message.partsSig.value.some((part) => part.kind === "text" && part.textSig.value.length > 0)
5671
5701
  );
@@ -5676,6 +5706,7 @@ function MessageBubble({
5676
5706
  const bufferedHold = responseMode === "buffered" && streaming;
5677
5707
  const working = streaming && !hasAnswerText.value;
5678
5708
  const showStreamDots = streaming && !bufferedHold && !(reasoningVisible.value && working);
5709
+ if (hideCanceledHusk.value) return null;
5679
5710
  const stamp = formatStamp(message.createdAt);
5680
5711
  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: [
5681
5712
  /* @__PURE__ */ jsxs15("div", { class: `${p17}-bubble`, children: [
@@ -7549,6 +7580,7 @@ function App({ options, hostElement, bus }) {
7549
7580
  const [agent, setAgent] = useState14(null);
7550
7581
  const [suggestions, setSuggestions] = useState14([]);
7551
7582
  const [activeCancel, setActiveCancel] = useState14(null);
7583
+ const [activeDetach, setActiveDetach] = useState14(null);
7552
7584
  const stringsRef = useRef9(options.strings);
7553
7585
  const [parsedSite, setParsedSite] = useState14(void 0);
7554
7586
  const [parsedBlocks, setParsedBlocks] = useState14(void 0);
@@ -7621,6 +7653,8 @@ function App({ options, hostElement, bus }) {
7621
7653
  const homeNavSeeded = useRef9(false);
7622
7654
  const resumeActiveRef = useRef9(false);
7623
7655
  const resumeBubbleRef = useRef9(null);
7656
+ const regenerateDanglingRef = useRef9(null);
7657
+ const regenInFlightRef = useRef9(false);
7624
7658
  useEffect16(() => {
7625
7659
  if (!conversationReady || homeNavSeeded.current) return;
7626
7660
  homeNavSeeded.current = true;
@@ -7690,6 +7724,7 @@ function App({ options, hostElement, bus }) {
7690
7724
  } else {
7691
7725
  messagesSig.value = withWelcomeRows([], markers);
7692
7726
  }
7727
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7693
7728
  } catch (err) {
7694
7729
  if (isStale()) return;
7695
7730
  log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
@@ -7819,11 +7854,28 @@ function App({ options, hostElement, bus }) {
7819
7854
  useEffect16(() => {
7820
7855
  if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
7821
7856
  }, [effectiveLocale, activeLocale]);
7857
+ const adoptConversationRebind = useCallback6(
7858
+ (chunk) => {
7859
+ if (chunk.type !== "data-conversation-rebind") return false;
7860
+ const next = chunk.data?.conversationId;
7861
+ if (next && next !== conversationIdSig.value) {
7862
+ const previous = conversationIdSig.value;
7863
+ conversationIdSig.value = next;
7864
+ persistence.saveConversationId(next);
7865
+ transport.primeIdentity(void 0, next);
7866
+ log16.info("conversation rebound (in-stream)", { previous, current: next });
7867
+ if (previous) bus.emit("conversationRebound", { previous, current: next, reason: next });
7868
+ }
7869
+ return true;
7870
+ },
7871
+ [conversationIdSig, persistence, transport, bus]
7872
+ );
7822
7873
  const runResume = useCallback6(
7823
7874
  async (handle) => {
7824
7875
  let bubble = null;
7825
7876
  try {
7826
7877
  for await (const evt of handle.iter) {
7878
+ if (adoptConversationRebind(evt.chunk)) continue;
7827
7879
  if (!bubble) {
7828
7880
  bubble = makeAssistantMessage();
7829
7881
  resumeBubbleRef.current = bubble;
@@ -7831,6 +7883,7 @@ function App({ options, hostElement, bus }) {
7831
7883
  messagesSig.value = [...messagesSig.value, bubble];
7832
7884
  setStreaming(true);
7833
7885
  setActiveCancel(() => handle.cancel);
7886
+ setActiveDetach(() => handle.detach);
7834
7887
  resumeActiveRef.current = true;
7835
7888
  const chatTab = chatTabIdRef.current;
7836
7889
  if (chatTab) homeNav.switchTab(chatTab);
@@ -7852,24 +7905,39 @@ function App({ options, hostElement, bus }) {
7852
7905
  }
7853
7906
  } catch (err) {
7854
7907
  if (bubble) {
7855
- bubble.status = "error";
7856
- bubble.errorText = errorMessageFor(err, options.strings);
7857
- feedback.play("error");
7908
+ if (isAbortError(err)) {
7909
+ if (hasNoVisibleAnswer(bubble)) {
7910
+ messagesSig.value = messagesSig.value.filter((m) => m !== bubble);
7911
+ } else {
7912
+ bubble.status = "complete";
7913
+ }
7914
+ } else {
7915
+ bubble.status = "error";
7916
+ bubble.errorText = errorMessageFor(err, options.strings);
7917
+ feedback.play("error");
7918
+ }
7858
7919
  }
7859
- log16.debug("mount resume ended without completing", { err });
7920
+ log16.debug("resume ended without completing", { err });
7860
7921
  } finally {
7861
7922
  resumeBubbleRef.current = null;
7862
7923
  if (bubble) {
7863
7924
  reducer.detach();
7864
7925
  setStreaming(false);
7865
7926
  setActiveCancel(null);
7927
+ setActiveDetach(null);
7866
7928
  messagesSig.value = [...messagesSig.value];
7867
7929
  }
7868
7930
  }
7869
7931
  },
7870
- [reducer, messagesSig, feedback, bus, options, homeNav]
7932
+ [reducer, messagesSig, feedback, bus, options, homeNav, adoptConversationRebind]
7871
7933
  );
7872
7934
  const resumeHandleRef = useRef9(null);
7935
+ const resumeInFlight = useCallback6(async () => {
7936
+ const handle = transport.resumeStream();
7937
+ resumeHandleRef.current = handle;
7938
+ await runResume(handle);
7939
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7940
+ }, [transport, runResume]);
7873
7941
  useEffect16(() => {
7874
7942
  const persistedConversation = conversationIdSig.value;
7875
7943
  if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
@@ -7892,16 +7960,17 @@ function App({ options, hostElement, bus }) {
7892
7960
  setFormsReady(true);
7893
7961
  }
7894
7962
  })();
7895
- if (pendingThreadRef.current) {
7896
- pendingThreadRef.current = false;
7897
- const persisted = conversationIdSig.value;
7898
- if (persisted) void loadThread(persisted);
7899
- }
7900
- if (conversationIdSig.value) {
7901
- const handle = transport.resumeStream();
7902
- resumeHandleRef.current = handle;
7903
- void runResume(handle);
7904
- }
7963
+ void (async () => {
7964
+ const tasks = [];
7965
+ if (pendingThreadRef.current) {
7966
+ pendingThreadRef.current = false;
7967
+ const persisted = conversationIdSig.value;
7968
+ if (persisted) tasks.push(loadThread(persisted));
7969
+ }
7970
+ if (conversationIdSig.value) tasks.push(resumeInFlight());
7971
+ if (tasks.length === 0) return;
7972
+ await Promise.allSettled(tasks);
7973
+ })();
7905
7974
  }, [activated]);
7906
7975
  const formsLocaleRef = useRef9(null);
7907
7976
  useEffect16(() => {
@@ -7972,8 +8041,10 @@ function App({ options, hostElement, bus }) {
7972
8041
  setStreaming(true);
7973
8042
  const handle = transport.sendMessage(body);
7974
8043
  setActiveCancel(() => handle.cancel);
8044
+ setActiveDetach(() => handle.detach);
7975
8045
  try {
7976
8046
  for await (const evt of handle.iter) {
8047
+ if (adoptConversationRebind(evt.chunk)) continue;
7977
8048
  reducer.apply(evt.chunk);
7978
8049
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) {
7979
8050
  setCanSend(false);
@@ -8011,11 +8082,39 @@ function App({ options, hostElement, bus }) {
8011
8082
  reducer.detach();
8012
8083
  setStreaming(false);
8013
8084
  setActiveCancel(null);
8085
+ setActiveDetach(null);
8014
8086
  messagesSig.value = [...messagesSig.value];
8015
8087
  }
8016
8088
  },
8017
- [transport, reducer, messagesSig, conversationIdSig, options, bus, feedback, persistence, visitorId]
8089
+ [
8090
+ transport,
8091
+ reducer,
8092
+ messagesSig,
8093
+ conversationIdSig,
8094
+ options,
8095
+ bus,
8096
+ feedback,
8097
+ persistence,
8098
+ visitorId,
8099
+ adoptConversationRebind
8100
+ ]
8018
8101
  );
8102
+ const regenerateDanglingTurn = useCallback6(() => {
8103
+ if (streaming || regenInFlightRef.current || resumeBubbleRef.current) return;
8104
+ const last = messagesSig.value.filter((m) => !m.ephemeral).at(-1);
8105
+ if (last?.role !== "user") return;
8106
+ regenInFlightRef.current = true;
8107
+ log16.info("regenerating dangling user turn (needs-generation)");
8108
+ const assistantMsg = makeAssistantMessage();
8109
+ messagesSig.value = [...messagesSig.value, assistantMsg];
8110
+ reducer.attach(assistantMsg);
8111
+ void streamAssistant(assistantMsg, { continueExisting: false }).finally(() => {
8112
+ regenInFlightRef.current = false;
8113
+ });
8114
+ }, [streaming, messagesSig, reducer, streamAssistant]);
8115
+ useEffect16(() => {
8116
+ regenerateDanglingRef.current = regenerateDanglingTurn;
8117
+ }, [regenerateDanglingTurn]);
8019
8118
  const decideTool = useCallback6(
8020
8119
  (toolCallId, mutate) => {
8021
8120
  if (streaming) return;
@@ -8231,7 +8330,7 @@ function App({ options, hostElement, bus }) {
8231
8330
  }
8232
8331
  log16.info("clear \u2192 new conversation", { wasStreaming: streaming });
8233
8332
  bus.emit("clear", void 0);
8234
- if (streaming) activeCancel?.();
8333
+ if (streaming) activeDetach?.();
8235
8334
  batch(() => {
8236
8335
  messagesSig.value = [];
8237
8336
  conversationIdSig.value = void 0;
@@ -8240,7 +8339,7 @@ function App({ options, hostElement, bus }) {
8240
8339
  setCanSend(true);
8241
8340
  setFormMarkers([]);
8242
8341
  void runHandshake({ newConversation: true });
8243
- }, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus, canSend, playWelcome]);
8342
+ }, [streaming, activeDetach, messagesSig, conversationIdSig, persistence, runHandshake, bus, canSend, playWelcome]);
8244
8343
  const handleNewChat = useCallback6(() => {
8245
8344
  handleClear();
8246
8345
  setView("chat");
@@ -8317,8 +8416,9 @@ function App({ options, hostElement, bus }) {
8317
8416
  );
8318
8417
  const handleSelectHistoryConversation = useCallback6(
8319
8418
  async (targetConversationId) => {
8320
- log16.info("selectConversation", { conversationId: targetConversationId });
8419
+ log16.info("selectConversation", { conversationId: targetConversationId, wasStreaming: streaming });
8321
8420
  bus.emit("selectConversation", { conversationId: targetConversationId });
8421
+ if (streaming) activeDetach?.();
8322
8422
  try {
8323
8423
  const [res, markers] = await Promise.all([
8324
8424
  transport.loadConversation(targetConversationId),
@@ -8331,10 +8431,12 @@ function App({ options, hostElement, bus }) {
8331
8431
  });
8332
8432
  setFormMarkers(markers);
8333
8433
  persistence.saveConversationId(res.conversationId);
8434
+ transport.primeIdentity(visitorId, res.conversationId);
8334
8435
  if (res.agent) setAgent(res.agent);
8335
8436
  setCanSend(res.canContinue);
8336
8437
  setSuggestions(res.suggestions ?? []);
8337
8438
  setView("chat");
8439
+ void resumeInFlight();
8338
8440
  await transport.markRead(targetConversationId);
8339
8441
  refreshUnread();
8340
8442
  } catch (err) {
@@ -8351,7 +8453,11 @@ function App({ options, hostElement, bus }) {
8351
8453
  persistence,
8352
8454
  refreshUnread,
8353
8455
  fetchFormMarkers,
8354
- withWelcomeRows
8456
+ withWelcomeRows,
8457
+ streaming,
8458
+ activeDetach,
8459
+ visitorId,
8460
+ resumeInFlight
8355
8461
  ]
8356
8462
  );
8357
8463
  const pendingOpenFormRef = useRef9(null);