@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/index.mjs CHANGED
@@ -29,7 +29,7 @@ var BRAND = {
29
29
  };
30
30
 
31
31
  // src/core/version.ts
32
- var ELEMENTS_VERSION = true ? "0.50.8" : "0.0.0-dev";
32
+ var ELEMENTS_VERSION = true ? "0.51.1" : "0.0.0-dev";
33
33
  var ELEMENTS_VERSION_PARAM = "_ev";
34
34
 
35
35
  // src/i18n/strings.ts
@@ -2112,6 +2112,13 @@ var AgentTransport = class {
2112
2112
  // (visitor, conversation) pair.
2113
2113
  __publicField(this, "visitorId");
2114
2114
  __publicField(this, "conversationId");
2115
+ /**
2116
+ * Outcome of the most recent `resumeStream()` once it found NO replayable SSE — drives the App's
2117
+ * dangling-turn recovery. `live` = a stream was replayed; otherwise the server's JSON recovery status.
2118
+ * `needs-generation` means a durable user turn has no reply (e.g. a fast send→refresh aborted the
2119
+ * deferred LLM run) → the App re-sends to generate it. Reset at the start of each `resumeStream()`.
2120
+ */
2121
+ __publicField(this, "resumeStatus");
2115
2122
  // Host-asserted context (sanitised `userContext` / `pageContext`), refreshed via
2116
2123
  // `setContext` and carried as the envelope's optional `context` so the backend
2117
2124
  // always sees the visitor's latest context as they navigate.
@@ -2447,7 +2454,7 @@ var AgentTransport = class {
2447
2454
  const seenIds = /* @__PURE__ */ new Set();
2448
2455
  const iter = this.runStream(body, ctrl, seenIds);
2449
2456
  const cancel = () => this.cancelStream(ctrl, body.conversationId);
2450
- return { iter, cancel };
2457
+ return { iter, cancel, detach: () => ctrl.abort() };
2451
2458
  }
2452
2459
  /**
2453
2460
  * Re-attach to an in-flight reply on a cold page load / refresh.
@@ -2468,8 +2475,10 @@ var AgentTransport = class {
2468
2475
  resumeStream() {
2469
2476
  const ctrl = new AbortController();
2470
2477
  const seenIds = /* @__PURE__ */ new Set();
2478
+ this.resumeStatus = void 0;
2471
2479
  const iter = this.resumeStreamGen(ctrl, seenIds, true);
2472
- return { iter, cancel: () => ctrl.abort() };
2480
+ const stop = () => ctrl.abort();
2481
+ return { iter, cancel: stop, detach: stop };
2473
2482
  }
2474
2483
  // ---- Stream orchestration ---------------------------------------------
2475
2484
  async *runStream(body, ctrl, seenIds) {
@@ -2522,6 +2531,15 @@ var AgentTransport = class {
2522
2531
  log6.debug("resume \u2192 no in-flight stream", { status: res.status });
2523
2532
  return;
2524
2533
  }
2534
+ const contentType = res.headers.get("content-type") ?? "";
2535
+ if (!contentType.includes("text/event-stream")) {
2536
+ const status = await res.json().then((body) => body.status).catch(() => void 0);
2537
+ log6.debug("resume \u2192 no live stream (status response)", { status, attempt });
2538
+ if (status === "pending" && attempt < MAX_RESUME_ATTEMPTS) continue;
2539
+ this.resumeStatus = status === "needs-generation" ? "needs-generation" : "completed";
2540
+ return;
2541
+ }
2542
+ this.resumeStatus = "live";
2525
2543
  const before = seenIds.size;
2526
2544
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
2527
2545
  if (seenIds.size === before) {
@@ -2741,6 +2759,7 @@ function toReactive(m) {
2741
2759
  role: m.role,
2742
2760
  createdAt: m.createdAt,
2743
2761
  status: m.status,
2762
+ canceled: m.canceled,
2744
2763
  errorText: m.errorText,
2745
2764
  finishReason: m.finishReason,
2746
2765
  serverMessageId: m.serverMessageId,
@@ -2804,7 +2823,11 @@ function fromWireMessage(w) {
2804
2823
  // Real send time when the backend provides it; otherwise fall back to now
2805
2824
  // (older backends) so the timestamp + day divider still render.
2806
2825
  createdAt: parseWireDate(w.createdAt) ?? Date.now(),
2807
- status: "complete",
2826
+ // Map the persisted terminal status onto the bubble's render state: `failed` → an error bubble;
2827
+ // `canceled` → `complete` + the `canceled` flag (an empty canceled turn is hidden on render — see
2828
+ // `message-bubble`); everything else is a normal completed turn.
2829
+ status: w.status === "failed" ? "error" : "complete",
2830
+ canceled: w.status === "canceled",
2808
2831
  partsSig: signal(parts)
2809
2832
  };
2810
2833
  }
@@ -2814,6 +2837,7 @@ function fromReactive(m) {
2814
2837
  role: m.role,
2815
2838
  createdAt: m.createdAt,
2816
2839
  status: m.status,
2840
+ canceled: m.canceled,
2817
2841
  errorText: m.errorText,
2818
2842
  finishReason: m.finishReason,
2819
2843
  serverMessageId: m.serverMessageId,
@@ -2836,6 +2860,9 @@ function hasNoVisibleAnswer(m) {
2836
2860
  function isEmptyAssistantReply(m) {
2837
2861
  return m.role === "assistant" && m.status !== "streaming" && hasNoVisibleAnswer(m);
2838
2862
  }
2863
+ function isHiddenCanceledTurn(m) {
2864
+ return m.canceled === true && isEmptyAssistantReply(m);
2865
+ }
2839
2866
  function partToReactive(p36) {
2840
2867
  if (p36.kind === "text" || p36.kind === "reasoning") {
2841
2868
  return { kind: p36.kind, id: p36.id, textSig: signal(p36.text), doneSig: signal(p36.done) };
@@ -2995,6 +3022,8 @@ var StreamReducer = class {
2995
3022
  return;
2996
3023
  case "data-notification":
2997
3024
  return;
3025
+ case "data-conversation-rebind":
3026
+ return;
2998
3027
  default: {
2999
3028
  const _exhaustive = chunk;
3000
3029
  void _exhaustive;
@@ -5707,6 +5736,7 @@ function MessageBubble({
5707
5736
  const parts = useComputed5(() => message.partsSig.value);
5708
5737
  const partList = parts.value;
5709
5738
  const emptyReply = useComputed5(() => isEmptyAssistantReply(message));
5739
+ const hideCanceledHusk = useComputed5(() => isHiddenCanceledTurn(message));
5710
5740
  const hasAnswerText = useComputed5(
5711
5741
  () => message.partsSig.value.some((part) => part.kind === "text" && part.textSig.value.length > 0)
5712
5742
  );
@@ -5717,6 +5747,7 @@ function MessageBubble({
5717
5747
  const bufferedHold = responseMode === "buffered" && streaming;
5718
5748
  const working = streaming && !hasAnswerText.value;
5719
5749
  const showStreamDots = streaming && !bufferedHold && !(reasoningVisible.value && working);
5750
+ if (hideCanceledHusk.value) return null;
5720
5751
  const stamp = formatStamp(message.createdAt);
5721
5752
  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: [
5722
5753
  /* @__PURE__ */ jsxs15("div", { class: `${p17}-bubble`, children: [
@@ -7590,6 +7621,7 @@ function App({ options, hostElement, bus }) {
7590
7621
  const [agent, setAgent] = useState14(null);
7591
7622
  const [suggestions, setSuggestions] = useState14([]);
7592
7623
  const [activeCancel, setActiveCancel] = useState14(null);
7624
+ const [activeDetach, setActiveDetach] = useState14(null);
7593
7625
  const stringsRef = useRef9(options.strings);
7594
7626
  const [parsedSite, setParsedSite] = useState14(void 0);
7595
7627
  const [parsedBlocks, setParsedBlocks] = useState14(void 0);
@@ -7662,6 +7694,8 @@ function App({ options, hostElement, bus }) {
7662
7694
  const homeNavSeeded = useRef9(false);
7663
7695
  const resumeActiveRef = useRef9(false);
7664
7696
  const resumeBubbleRef = useRef9(null);
7697
+ const regenerateDanglingRef = useRef9(null);
7698
+ const regenInFlightRef = useRef9(false);
7665
7699
  useEffect16(() => {
7666
7700
  if (!conversationReady || homeNavSeeded.current) return;
7667
7701
  homeNavSeeded.current = true;
@@ -7731,6 +7765,7 @@ function App({ options, hostElement, bus }) {
7731
7765
  } else {
7732
7766
  messagesSig.value = withWelcomeRows([], markers);
7733
7767
  }
7768
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7734
7769
  } catch (err) {
7735
7770
  if (isStale()) return;
7736
7771
  log17.warn("loadThread failed; resetting conversationId", { err, conversationId });
@@ -7860,11 +7895,28 @@ function App({ options, hostElement, bus }) {
7860
7895
  useEffect16(() => {
7861
7896
  if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
7862
7897
  }, [effectiveLocale, activeLocale]);
7898
+ const adoptConversationRebind = useCallback6(
7899
+ (chunk) => {
7900
+ if (chunk.type !== "data-conversation-rebind") return false;
7901
+ const next = chunk.data?.conversationId;
7902
+ if (next && next !== conversationIdSig.value) {
7903
+ const previous = conversationIdSig.value;
7904
+ conversationIdSig.value = next;
7905
+ persistence.saveConversationId(next);
7906
+ transport.primeIdentity(void 0, next);
7907
+ log17.info("conversation rebound (in-stream)", { previous, current: next });
7908
+ if (previous) bus.emit("conversationRebound", { previous, current: next, reason: next });
7909
+ }
7910
+ return true;
7911
+ },
7912
+ [conversationIdSig, persistence, transport, bus]
7913
+ );
7863
7914
  const runResume = useCallback6(
7864
7915
  async (handle) => {
7865
7916
  let bubble = null;
7866
7917
  try {
7867
7918
  for await (const evt of handle.iter) {
7919
+ if (adoptConversationRebind(evt.chunk)) continue;
7868
7920
  if (!bubble) {
7869
7921
  bubble = makeAssistantMessage();
7870
7922
  resumeBubbleRef.current = bubble;
@@ -7872,6 +7924,7 @@ function App({ options, hostElement, bus }) {
7872
7924
  messagesSig.value = [...messagesSig.value, bubble];
7873
7925
  setStreaming(true);
7874
7926
  setActiveCancel(() => handle.cancel);
7927
+ setActiveDetach(() => handle.detach);
7875
7928
  resumeActiveRef.current = true;
7876
7929
  const chatTab = chatTabIdRef.current;
7877
7930
  if (chatTab) homeNav.switchTab(chatTab);
@@ -7893,24 +7946,39 @@ function App({ options, hostElement, bus }) {
7893
7946
  }
7894
7947
  } catch (err) {
7895
7948
  if (bubble) {
7896
- bubble.status = "error";
7897
- bubble.errorText = errorMessageFor(err, options.strings);
7898
- feedback.play("error");
7949
+ if (isAbortError(err)) {
7950
+ if (hasNoVisibleAnswer(bubble)) {
7951
+ messagesSig.value = messagesSig.value.filter((m) => m !== bubble);
7952
+ } else {
7953
+ bubble.status = "complete";
7954
+ }
7955
+ } else {
7956
+ bubble.status = "error";
7957
+ bubble.errorText = errorMessageFor(err, options.strings);
7958
+ feedback.play("error");
7959
+ }
7899
7960
  }
7900
- log17.debug("mount resume ended without completing", { err });
7961
+ log17.debug("resume ended without completing", { err });
7901
7962
  } finally {
7902
7963
  resumeBubbleRef.current = null;
7903
7964
  if (bubble) {
7904
7965
  reducer.detach();
7905
7966
  setStreaming(false);
7906
7967
  setActiveCancel(null);
7968
+ setActiveDetach(null);
7907
7969
  messagesSig.value = [...messagesSig.value];
7908
7970
  }
7909
7971
  }
7910
7972
  },
7911
- [reducer, messagesSig, feedback, bus, options, homeNav]
7973
+ [reducer, messagesSig, feedback, bus, options, homeNav, adoptConversationRebind]
7912
7974
  );
7913
7975
  const resumeHandleRef = useRef9(null);
7976
+ const resumeInFlight = useCallback6(async () => {
7977
+ const handle = transport.resumeStream();
7978
+ resumeHandleRef.current = handle;
7979
+ await runResume(handle);
7980
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7981
+ }, [transport, runResume]);
7914
7982
  useEffect16(() => {
7915
7983
  const persistedConversation = conversationIdSig.value;
7916
7984
  if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
@@ -7933,16 +8001,17 @@ function App({ options, hostElement, bus }) {
7933
8001
  setFormsReady(true);
7934
8002
  }
7935
8003
  })();
7936
- if (pendingThreadRef.current) {
7937
- pendingThreadRef.current = false;
7938
- const persisted = conversationIdSig.value;
7939
- if (persisted) void loadThread(persisted);
7940
- }
7941
- if (conversationIdSig.value) {
7942
- const handle = transport.resumeStream();
7943
- resumeHandleRef.current = handle;
7944
- void runResume(handle);
7945
- }
8004
+ void (async () => {
8005
+ const tasks = [];
8006
+ if (pendingThreadRef.current) {
8007
+ pendingThreadRef.current = false;
8008
+ const persisted = conversationIdSig.value;
8009
+ if (persisted) tasks.push(loadThread(persisted));
8010
+ }
8011
+ if (conversationIdSig.value) tasks.push(resumeInFlight());
8012
+ if (tasks.length === 0) return;
8013
+ await Promise.allSettled(tasks);
8014
+ })();
7946
8015
  }, [activated]);
7947
8016
  const formsLocaleRef = useRef9(null);
7948
8017
  useEffect16(() => {
@@ -8013,8 +8082,10 @@ function App({ options, hostElement, bus }) {
8013
8082
  setStreaming(true);
8014
8083
  const handle = transport.sendMessage(body);
8015
8084
  setActiveCancel(() => handle.cancel);
8085
+ setActiveDetach(() => handle.detach);
8016
8086
  try {
8017
8087
  for await (const evt of handle.iter) {
8088
+ if (adoptConversationRebind(evt.chunk)) continue;
8018
8089
  reducer.apply(evt.chunk);
8019
8090
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) {
8020
8091
  setCanSend(false);
@@ -8052,11 +8123,39 @@ function App({ options, hostElement, bus }) {
8052
8123
  reducer.detach();
8053
8124
  setStreaming(false);
8054
8125
  setActiveCancel(null);
8126
+ setActiveDetach(null);
8055
8127
  messagesSig.value = [...messagesSig.value];
8056
8128
  }
8057
8129
  },
8058
- [transport, reducer, messagesSig, conversationIdSig, options, bus, feedback, persistence, visitorId]
8130
+ [
8131
+ transport,
8132
+ reducer,
8133
+ messagesSig,
8134
+ conversationIdSig,
8135
+ options,
8136
+ bus,
8137
+ feedback,
8138
+ persistence,
8139
+ visitorId,
8140
+ adoptConversationRebind
8141
+ ]
8059
8142
  );
8143
+ const regenerateDanglingTurn = useCallback6(() => {
8144
+ if (streaming || regenInFlightRef.current || resumeBubbleRef.current) return;
8145
+ const last = messagesSig.value.filter((m) => !m.ephemeral).at(-1);
8146
+ if (last?.role !== "user") return;
8147
+ regenInFlightRef.current = true;
8148
+ log17.info("regenerating dangling user turn (needs-generation)");
8149
+ const assistantMsg = makeAssistantMessage();
8150
+ messagesSig.value = [...messagesSig.value, assistantMsg];
8151
+ reducer.attach(assistantMsg);
8152
+ void streamAssistant(assistantMsg, { continueExisting: false }).finally(() => {
8153
+ regenInFlightRef.current = false;
8154
+ });
8155
+ }, [streaming, messagesSig, reducer, streamAssistant]);
8156
+ useEffect16(() => {
8157
+ regenerateDanglingRef.current = regenerateDanglingTurn;
8158
+ }, [regenerateDanglingTurn]);
8060
8159
  const decideTool = useCallback6(
8061
8160
  (toolCallId, mutate) => {
8062
8161
  if (streaming) return;
@@ -8272,7 +8371,7 @@ function App({ options, hostElement, bus }) {
8272
8371
  }
8273
8372
  log17.info("clear \u2192 new conversation", { wasStreaming: streaming });
8274
8373
  bus.emit("clear", void 0);
8275
- if (streaming) activeCancel?.();
8374
+ if (streaming) activeDetach?.();
8276
8375
  batch(() => {
8277
8376
  messagesSig.value = [];
8278
8377
  conversationIdSig.value = void 0;
@@ -8281,7 +8380,7 @@ function App({ options, hostElement, bus }) {
8281
8380
  setCanSend(true);
8282
8381
  setFormMarkers([]);
8283
8382
  void runHandshake({ newConversation: true });
8284
- }, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus, canSend, playWelcome]);
8383
+ }, [streaming, activeDetach, messagesSig, conversationIdSig, persistence, runHandshake, bus, canSend, playWelcome]);
8285
8384
  const handleNewChat = useCallback6(() => {
8286
8385
  handleClear();
8287
8386
  setView("chat");
@@ -8358,8 +8457,9 @@ function App({ options, hostElement, bus }) {
8358
8457
  );
8359
8458
  const handleSelectHistoryConversation = useCallback6(
8360
8459
  async (targetConversationId) => {
8361
- log17.info("selectConversation", { conversationId: targetConversationId });
8460
+ log17.info("selectConversation", { conversationId: targetConversationId, wasStreaming: streaming });
8362
8461
  bus.emit("selectConversation", { conversationId: targetConversationId });
8462
+ if (streaming) activeDetach?.();
8363
8463
  try {
8364
8464
  const [res, markers] = await Promise.all([
8365
8465
  transport.loadConversation(targetConversationId),
@@ -8372,10 +8472,12 @@ function App({ options, hostElement, bus }) {
8372
8472
  });
8373
8473
  setFormMarkers(markers);
8374
8474
  persistence.saveConversationId(res.conversationId);
8475
+ transport.primeIdentity(visitorId, res.conversationId);
8375
8476
  if (res.agent) setAgent(res.agent);
8376
8477
  setCanSend(res.canContinue);
8377
8478
  setSuggestions(res.suggestions ?? []);
8378
8479
  setView("chat");
8480
+ void resumeInFlight();
8379
8481
  await transport.markRead(targetConversationId);
8380
8482
  refreshUnread();
8381
8483
  } catch (err) {
@@ -8392,7 +8494,11 @@ function App({ options, hostElement, bus }) {
8392
8494
  persistence,
8393
8495
  refreshUnread,
8394
8496
  fetchFormMarkers,
8395
- withWelcomeRows
8497
+ withWelcomeRows,
8498
+ streaming,
8499
+ activeDetach,
8500
+ visitorId,
8501
+ resumeInFlight
8396
8502
  ]
8397
8503
  );
8398
8504
  const pendingOpenFormRef = useRef9(null);
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  ],
81
81
  "type": "module",
82
82
  "types": "./index.d.ts",
83
- "version": "0.50.8"
83
+ "version": "0.51.1"
84
84
  }