@helpai/elements 0.50.8 → 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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, H as HandshakeResponse, L as Link, S as ServerConfig, b as SiteConfig, W as WidgetConfig, c as WidgetConfigPartial, d as WidgetSettings, e as WidgetSettingsPartial } from './deployment-C_jlXJAe.js';
1
+ export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, H as HandshakeResponse, L as Link, S as ServerConfig, b as SiteConfig, W as WidgetConfig, c as WidgetConfigPartial, d as WidgetSettings, e as WidgetSettingsPartial } from './deployment-oSJ2nv2G.js';
2
2
  import 'zod';
3
3
 
4
4
  /**
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.0" : "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.
@@ -2468,6 +2475,7 @@ 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
2480
  return { iter, cancel: () => ctrl.abort() };
2473
2481
  }
@@ -2522,6 +2530,15 @@ var AgentTransport = class {
2522
2530
  log6.debug("resume \u2192 no in-flight stream", { status: res.status });
2523
2531
  return;
2524
2532
  }
2533
+ const contentType = res.headers.get("content-type") ?? "";
2534
+ if (!contentType.includes("text/event-stream")) {
2535
+ const status = await res.json().then((body) => body.status).catch(() => void 0);
2536
+ log6.debug("resume \u2192 no live stream (status response)", { status, attempt });
2537
+ if (status === "pending" && attempt < MAX_RESUME_ATTEMPTS) continue;
2538
+ this.resumeStatus = status === "needs-generation" ? "needs-generation" : "completed";
2539
+ return;
2540
+ }
2541
+ this.resumeStatus = "live";
2525
2542
  const before = seenIds.size;
2526
2543
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
2527
2544
  if (seenIds.size === before) {
@@ -2741,6 +2758,7 @@ function toReactive(m) {
2741
2758
  role: m.role,
2742
2759
  createdAt: m.createdAt,
2743
2760
  status: m.status,
2761
+ canceled: m.canceled,
2744
2762
  errorText: m.errorText,
2745
2763
  finishReason: m.finishReason,
2746
2764
  serverMessageId: m.serverMessageId,
@@ -2804,7 +2822,11 @@ function fromWireMessage(w) {
2804
2822
  // Real send time when the backend provides it; otherwise fall back to now
2805
2823
  // (older backends) so the timestamp + day divider still render.
2806
2824
  createdAt: parseWireDate(w.createdAt) ?? Date.now(),
2807
- status: "complete",
2825
+ // Map the persisted terminal status onto the bubble's render state: `failed` → an error bubble;
2826
+ // `canceled` → `complete` + the `canceled` flag (an empty canceled turn is hidden on render — see
2827
+ // `message-bubble`); everything else is a normal completed turn.
2828
+ status: w.status === "failed" ? "error" : "complete",
2829
+ canceled: w.status === "canceled",
2808
2830
  partsSig: signal(parts)
2809
2831
  };
2810
2832
  }
@@ -2814,6 +2836,7 @@ function fromReactive(m) {
2814
2836
  role: m.role,
2815
2837
  createdAt: m.createdAt,
2816
2838
  status: m.status,
2839
+ canceled: m.canceled,
2817
2840
  errorText: m.errorText,
2818
2841
  finishReason: m.finishReason,
2819
2842
  serverMessageId: m.serverMessageId,
@@ -2836,6 +2859,9 @@ function hasNoVisibleAnswer(m) {
2836
2859
  function isEmptyAssistantReply(m) {
2837
2860
  return m.role === "assistant" && m.status !== "streaming" && hasNoVisibleAnswer(m);
2838
2861
  }
2862
+ function isHiddenCanceledTurn(m) {
2863
+ return m.canceled === true && isEmptyAssistantReply(m);
2864
+ }
2839
2865
  function partToReactive(p36) {
2840
2866
  if (p36.kind === "text" || p36.kind === "reasoning") {
2841
2867
  return { kind: p36.kind, id: p36.id, textSig: signal(p36.text), doneSig: signal(p36.done) };
@@ -2995,6 +3021,8 @@ var StreamReducer = class {
2995
3021
  return;
2996
3022
  case "data-notification":
2997
3023
  return;
3024
+ case "data-conversation-rebind":
3025
+ return;
2998
3026
  default: {
2999
3027
  const _exhaustive = chunk;
3000
3028
  void _exhaustive;
@@ -5707,6 +5735,7 @@ function MessageBubble({
5707
5735
  const parts = useComputed5(() => message.partsSig.value);
5708
5736
  const partList = parts.value;
5709
5737
  const emptyReply = useComputed5(() => isEmptyAssistantReply(message));
5738
+ const hideCanceledHusk = useComputed5(() => isHiddenCanceledTurn(message));
5710
5739
  const hasAnswerText = useComputed5(
5711
5740
  () => message.partsSig.value.some((part) => part.kind === "text" && part.textSig.value.length > 0)
5712
5741
  );
@@ -5717,6 +5746,7 @@ function MessageBubble({
5717
5746
  const bufferedHold = responseMode === "buffered" && streaming;
5718
5747
  const working = streaming && !hasAnswerText.value;
5719
5748
  const showStreamDots = streaming && !bufferedHold && !(reasoningVisible.value && working);
5749
+ if (hideCanceledHusk.value) return null;
5720
5750
  const stamp = formatStamp(message.createdAt);
5721
5751
  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
5752
  /* @__PURE__ */ jsxs15("div", { class: `${p17}-bubble`, children: [
@@ -7662,6 +7692,8 @@ function App({ options, hostElement, bus }) {
7662
7692
  const homeNavSeeded = useRef9(false);
7663
7693
  const resumeActiveRef = useRef9(false);
7664
7694
  const resumeBubbleRef = useRef9(null);
7695
+ const regenerateDanglingRef = useRef9(null);
7696
+ const regenInFlightRef = useRef9(false);
7665
7697
  useEffect16(() => {
7666
7698
  if (!conversationReady || homeNavSeeded.current) return;
7667
7699
  homeNavSeeded.current = true;
@@ -7731,6 +7763,7 @@ function App({ options, hostElement, bus }) {
7731
7763
  } else {
7732
7764
  messagesSig.value = withWelcomeRows([], markers);
7733
7765
  }
7766
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7734
7767
  } catch (err) {
7735
7768
  if (isStale()) return;
7736
7769
  log17.warn("loadThread failed; resetting conversationId", { err, conversationId });
@@ -7860,11 +7893,28 @@ function App({ options, hostElement, bus }) {
7860
7893
  useEffect16(() => {
7861
7894
  if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
7862
7895
  }, [effectiveLocale, activeLocale]);
7896
+ const adoptConversationRebind = useCallback6(
7897
+ (chunk) => {
7898
+ if (chunk.type !== "data-conversation-rebind") return false;
7899
+ const next = chunk.data?.conversationId;
7900
+ if (next && next !== conversationIdSig.value) {
7901
+ const previous = conversationIdSig.value;
7902
+ conversationIdSig.value = next;
7903
+ persistence.saveConversationId(next);
7904
+ transport.primeIdentity(void 0, next);
7905
+ log17.info("conversation rebound (in-stream)", { previous, current: next });
7906
+ if (previous) bus.emit("conversationRebound", { previous, current: next, reason: next });
7907
+ }
7908
+ return true;
7909
+ },
7910
+ [conversationIdSig, persistence, transport, bus]
7911
+ );
7863
7912
  const runResume = useCallback6(
7864
7913
  async (handle) => {
7865
7914
  let bubble = null;
7866
7915
  try {
7867
7916
  for await (const evt of handle.iter) {
7917
+ if (adoptConversationRebind(evt.chunk)) continue;
7868
7918
  if (!bubble) {
7869
7919
  bubble = makeAssistantMessage();
7870
7920
  resumeBubbleRef.current = bubble;
@@ -7908,7 +7958,7 @@ function App({ options, hostElement, bus }) {
7908
7958
  }
7909
7959
  }
7910
7960
  },
7911
- [reducer, messagesSig, feedback, bus, options, homeNav]
7961
+ [reducer, messagesSig, feedback, bus, options, homeNav, adoptConversationRebind]
7912
7962
  );
7913
7963
  const resumeHandleRef = useRef9(null);
7914
7964
  useEffect16(() => {
@@ -7933,16 +7983,22 @@ function App({ options, hostElement, bus }) {
7933
7983
  setFormsReady(true);
7934
7984
  }
7935
7985
  })();
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
- }
7986
+ void (async () => {
7987
+ const tasks = [];
7988
+ if (pendingThreadRef.current) {
7989
+ pendingThreadRef.current = false;
7990
+ const persisted = conversationIdSig.value;
7991
+ if (persisted) tasks.push(loadThread(persisted));
7992
+ }
7993
+ if (conversationIdSig.value) {
7994
+ const handle = transport.resumeStream();
7995
+ resumeHandleRef.current = handle;
7996
+ tasks.push(runResume(handle));
7997
+ }
7998
+ if (tasks.length === 0) return;
7999
+ await Promise.allSettled(tasks);
8000
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
8001
+ })();
7946
8002
  }, [activated]);
7947
8003
  const formsLocaleRef = useRef9(null);
7948
8004
  useEffect16(() => {
@@ -8015,6 +8071,7 @@ function App({ options, hostElement, bus }) {
8015
8071
  setActiveCancel(() => handle.cancel);
8016
8072
  try {
8017
8073
  for await (const evt of handle.iter) {
8074
+ if (adoptConversationRebind(evt.chunk)) continue;
8018
8075
  reducer.apply(evt.chunk);
8019
8076
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) {
8020
8077
  setCanSend(false);
@@ -8055,8 +8112,35 @@ function App({ options, hostElement, bus }) {
8055
8112
  messagesSig.value = [...messagesSig.value];
8056
8113
  }
8057
8114
  },
8058
- [transport, reducer, messagesSig, conversationIdSig, options, bus, feedback, persistence, visitorId]
8115
+ [
8116
+ transport,
8117
+ reducer,
8118
+ messagesSig,
8119
+ conversationIdSig,
8120
+ options,
8121
+ bus,
8122
+ feedback,
8123
+ persistence,
8124
+ visitorId,
8125
+ adoptConversationRebind
8126
+ ]
8059
8127
  );
8128
+ const regenerateDanglingTurn = useCallback6(() => {
8129
+ if (streaming || regenInFlightRef.current || resumeBubbleRef.current) return;
8130
+ const last = messagesSig.value.filter((m) => !m.ephemeral).at(-1);
8131
+ if (last?.role !== "user") return;
8132
+ regenInFlightRef.current = true;
8133
+ log17.info("regenerating dangling user turn (needs-generation)");
8134
+ const assistantMsg = makeAssistantMessage();
8135
+ messagesSig.value = [...messagesSig.value, assistantMsg];
8136
+ reducer.attach(assistantMsg);
8137
+ void streamAssistant(assistantMsg, { continueExisting: false }).finally(() => {
8138
+ regenInFlightRef.current = false;
8139
+ });
8140
+ }, [streaming, messagesSig, reducer, streamAssistant]);
8141
+ useEffect16(() => {
8142
+ regenerateDanglingRef.current = regenerateDanglingTurn;
8143
+ }, [regenerateDanglingTurn]);
8060
8144
  const decideTool = useCallback6(
8061
8145
  (toolCallId, mutate) => {
8062
8146
  if (streaming) return;
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.0"
84
84
  }
package/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, E as Endpoints, H as HandshakeResponse, L as Link, P as PAGE_AREA_SUGGESTIONS, f as PageContext, S as ServerConfig, b as SiteConfig, U as UserContext, W as WidgetConfig, c as WidgetConfigPartial, d as WidgetSettings, e as WidgetSettingsPartial, g as assetSchema, h as blocksConfigSchema, i as connectionConfigPartialSchema, j as connectionConfigSchema, k as cssColorSchema, l as cssLengthSchema, m as endpointsSchema, n as handshakeResponseSchema, o as linkSchema, p as localeSchema, q as pageContextSchema, s as serverConfigSchema, r as siteConfigSchema, u as userContextSchema, t as uuid7Schema, w as widgetConfigPartialSchema, v as widgetConfigSchema, x as widgetSettingsPartialSchema, y as widgetSettingsSchema } from './deployment-C_jlXJAe.js';
1
+ export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, E as Endpoints, H as HandshakeResponse, L as Link, P as PAGE_AREA_SUGGESTIONS, f as PageContext, S as ServerConfig, b as SiteConfig, U as UserContext, W as WidgetConfig, c as WidgetConfigPartial, d as WidgetSettings, e as WidgetSettingsPartial, g as assetSchema, h as blocksConfigSchema, i as connectionConfigPartialSchema, j as connectionConfigSchema, k as cssColorSchema, l as cssLengthSchema, m as endpointsSchema, n as handshakeResponseSchema, o as linkSchema, p as localeSchema, q as pageContextSchema, s as serverConfigSchema, r as siteConfigSchema, u as userContextSchema, t as uuid7Schema, w as widgetConfigPartialSchema, v as widgetConfigSchema, x as widgetSettingsPartialSchema, y as widgetSettingsSchema } from './deployment-oSJ2nv2G.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -56,9 +56,9 @@ declare const presentationSchema: z.ZodObject<{
56
56
  inset: z.ZodOptional<z.ZodString>;
57
57
  initialSize: z.ZodDefault<z.ZodEnum<{
58
58
  fullscreen: "fullscreen";
59
+ normal: "normal";
59
60
  expanded: "expanded";
60
61
  auto: "auto";
61
- normal: "normal";
62
62
  }>>;
63
63
  autoSizeBreakpoint: z.ZodDefault<z.ZodNumber>;
64
64
  }, z.core.$loose>>;
@@ -240,9 +240,9 @@ type LauncherOptions = z.infer<typeof launcherOptionsSchema>;
240
240
 
241
241
  declare const initialSizeSchema: z.ZodEnum<{
242
242
  fullscreen: "fullscreen";
243
+ normal: "normal";
243
244
  expanded: "expanded";
244
245
  auto: "auto";
245
- normal: "normal";
246
246
  }>;
247
247
  declare const resizeOptionsSchema: z.ZodObject<{
248
248
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -269,9 +269,9 @@ declare const sizeOptionsSchema: z.ZodObject<{
269
269
  inset: z.ZodOptional<z.ZodString>;
270
270
  initialSize: z.ZodDefault<z.ZodEnum<{
271
271
  fullscreen: "fullscreen";
272
+ normal: "normal";
272
273
  expanded: "expanded";
273
274
  auto: "auto";
274
- normal: "normal";
275
275
  }>>;
276
276
  autoSizeBreakpoint: z.ZodDefault<z.ZodNumber>;
277
277
  }, z.core.$loose>;
@@ -323,40 +323,40 @@ type FeatureFlags = z.infer<typeof featureFlagsSchema>;
323
323
  */
324
324
 
325
325
  declare const actionNameSchema: z.ZodEnum<{
326
- close: "close";
327
326
  expand: "expand";
328
327
  fullscreen: "fullscreen";
329
- clear: "clear";
330
- theme: "theme";
328
+ close: "close";
331
329
  language: "language";
330
+ theme: "theme";
332
331
  textSize: "textSize";
333
332
  history: "history";
333
+ clear: "clear";
334
334
  sound: "sound";
335
335
  }>;
336
336
  type ActionName = z.infer<typeof actionNameSchema>;
337
337
  declare const headerActionsSchema: z.ZodArray<z.ZodEnum<{
338
- close: "close";
339
338
  expand: "expand";
340
339
  fullscreen: "fullscreen";
341
- clear: "clear";
342
- theme: "theme";
340
+ close: "close";
343
341
  language: "language";
342
+ theme: "theme";
344
343
  textSize: "textSize";
345
344
  history: "history";
345
+ clear: "clear";
346
346
  sound: "sound";
347
347
  }>>;
348
348
  type HeaderActions = z.infer<typeof headerActionsSchema>;
349
349
  /** Section wrapper — `actions` list wrapped under `header` in the dashboard form. */
350
350
  declare const headerSchema: z.ZodObject<{
351
351
  actions: z.ZodOptional<z.ZodArray<z.ZodEnum<{
352
- close: "close";
353
352
  expand: "expand";
354
353
  fullscreen: "fullscreen";
355
- clear: "clear";
356
- theme: "theme";
354
+ close: "close";
357
355
  language: "language";
356
+ theme: "theme";
358
357
  textSize: "textSize";
359
358
  history: "history";
359
+ clear: "clear";
360
360
  sound: "sound";
361
361
  }>>>;
362
362
  }, z.core.$loose>;
@@ -372,33 +372,33 @@ type HeaderOptions = z.infer<typeof headerSchema>;
372
372
  */
373
373
 
374
374
  declare const feedbackEventSchema: z.ZodEnum<{
375
- voiceStart: "voiceStart";
376
- voiceStop: "voiceStop";
377
375
  error: "error";
378
376
  messageReceived: "messageReceived";
379
377
  messageSent: "messageSent";
378
+ voiceStart: "voiceStart";
379
+ voiceStop: "voiceStop";
380
380
  }>;
381
381
  type FeedbackEvent = z.infer<typeof feedbackEventSchema>;
382
382
  declare const soundOptionsSchema: z.ZodObject<{
383
383
  enabled: z.ZodDefault<z.ZodBoolean>;
384
384
  volume: z.ZodDefault<z.ZodNumber>;
385
385
  events: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
386
- voiceStart: "voiceStart";
387
- voiceStop: "voiceStop";
388
386
  error: "error";
389
387
  messageReceived: "messageReceived";
390
388
  messageSent: "messageSent";
389
+ voiceStart: "voiceStart";
390
+ voiceStop: "voiceStop";
391
391
  }> & z.core.$partial, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
392
392
  }, z.core.$loose>;
393
393
  type SoundOptions = z.infer<typeof soundOptionsSchema>;
394
394
  declare const hapticsOptionsSchema: z.ZodObject<{
395
395
  enabled: z.ZodDefault<z.ZodBoolean>;
396
396
  events: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
397
- voiceStart: "voiceStart";
398
- voiceStop: "voiceStop";
399
397
  error: "error";
400
398
  messageReceived: "messageReceived";
401
399
  messageSent: "messageSent";
400
+ voiceStart: "voiceStart";
401
+ voiceStop: "voiceStop";
402
402
  }> & z.core.$partial, z.ZodUnion<readonly [z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodNumber>]>>>;
403
403
  }, z.core.$loose>;
404
404
  type HapticsOptions = z.infer<typeof hapticsOptionsSchema>;
@@ -407,21 +407,21 @@ declare const feedbackSchema: z.ZodObject<{
407
407
  enabled: z.ZodDefault<z.ZodBoolean>;
408
408
  volume: z.ZodDefault<z.ZodNumber>;
409
409
  events: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
410
- voiceStart: "voiceStart";
411
- voiceStop: "voiceStop";
412
410
  error: "error";
413
411
  messageReceived: "messageReceived";
414
412
  messageSent: "messageSent";
413
+ voiceStart: "voiceStart";
414
+ voiceStop: "voiceStop";
415
415
  }> & z.core.$partial, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
416
416
  }, z.core.$loose>>;
417
417
  haptics: z.ZodOptional<z.ZodObject<{
418
418
  enabled: z.ZodDefault<z.ZodBoolean>;
419
419
  events: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
420
- voiceStart: "voiceStart";
421
- voiceStop: "voiceStop";
422
420
  error: "error";
423
421
  messageReceived: "messageReceived";
424
422
  messageSent: "messageSent";
423
+ voiceStart: "voiceStart";
424
+ voiceStop: "voiceStop";
425
425
  }> & z.core.$partial, z.ZodUnion<readonly [z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodNumber>]>>>;
426
426
  }, z.core.$loose>>;
427
427
  }, z.core.$loose>;
@@ -856,18 +856,18 @@ type I18nOptions = z.infer<typeof i18nSchema>;
856
856
  */
857
857
 
858
858
  declare const moduleLayoutSchema: z.ZodEnum<{
859
- home: "home";
860
859
  chat: "chat";
861
860
  help: "help";
861
+ home: "home";
862
862
  news: "news";
863
863
  }>;
864
864
  type ModuleLayout = z.infer<typeof moduleLayoutSchema>;
865
865
  declare const moduleSchema: z.ZodObject<{
866
866
  label: z.ZodString;
867
867
  layout: z.ZodEnum<{
868
- home: "home";
869
868
  chat: "chat";
870
869
  help: "help";
870
+ home: "home";
871
871
  news: "news";
872
872
  }>;
873
873
  contentTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -893,9 +893,9 @@ type ModuleOptions = z.infer<typeof moduleSchema>;
893
893
  declare const modulesSchema: z.ZodArray<z.ZodObject<{
894
894
  label: z.ZodString;
895
895
  layout: z.ZodEnum<{
896
- home: "home";
897
896
  chat: "chat";
898
897
  help: "help";
898
+ home: "home";
899
899
  news: "news";
900
900
  }>;
901
901
  contentTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
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.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,
@@ -2795,6 +2818,9 @@ function hasNoVisibleAnswer(m) {
2795
2818
  function isEmptyAssistantReply(m) {
2796
2819
  return m.role === "assistant" && m.status !== "streaming" && hasNoVisibleAnswer(m);
2797
2820
  }
2821
+ function isHiddenCanceledTurn(m) {
2822
+ return m.canceled === true && isEmptyAssistantReply(m);
2823
+ }
2798
2824
  function partToReactive(p36) {
2799
2825
  if (p36.kind === "text" || p36.kind === "reasoning") {
2800
2826
  return { kind: p36.kind, id: p36.id, textSig: signal(p36.text), doneSig: signal(p36.done) };
@@ -2954,6 +2980,8 @@ var StreamReducer = class {
2954
2980
  return;
2955
2981
  case "data-notification":
2956
2982
  return;
2983
+ case "data-conversation-rebind":
2984
+ return;
2957
2985
  default: {
2958
2986
  const _exhaustive = chunk;
2959
2987
  void _exhaustive;
@@ -5666,6 +5694,7 @@ function MessageBubble({
5666
5694
  const parts = useComputed5(() => message.partsSig.value);
5667
5695
  const partList = parts.value;
5668
5696
  const emptyReply = useComputed5(() => isEmptyAssistantReply(message));
5697
+ const hideCanceledHusk = useComputed5(() => isHiddenCanceledTurn(message));
5669
5698
  const hasAnswerText = useComputed5(
5670
5699
  () => message.partsSig.value.some((part) => part.kind === "text" && part.textSig.value.length > 0)
5671
5700
  );
@@ -5676,6 +5705,7 @@ function MessageBubble({
5676
5705
  const bufferedHold = responseMode === "buffered" && streaming;
5677
5706
  const working = streaming && !hasAnswerText.value;
5678
5707
  const showStreamDots = streaming && !bufferedHold && !(reasoningVisible.value && working);
5708
+ if (hideCanceledHusk.value) return null;
5679
5709
  const stamp = formatStamp(message.createdAt);
5680
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: [
5681
5711
  /* @__PURE__ */ jsxs15("div", { class: `${p17}-bubble`, children: [
@@ -7621,6 +7651,8 @@ function App({ options, hostElement, bus }) {
7621
7651
  const homeNavSeeded = useRef9(false);
7622
7652
  const resumeActiveRef = useRef9(false);
7623
7653
  const resumeBubbleRef = useRef9(null);
7654
+ const regenerateDanglingRef = useRef9(null);
7655
+ const regenInFlightRef = useRef9(false);
7624
7656
  useEffect16(() => {
7625
7657
  if (!conversationReady || homeNavSeeded.current) return;
7626
7658
  homeNavSeeded.current = true;
@@ -7690,6 +7722,7 @@ function App({ options, hostElement, bus }) {
7690
7722
  } else {
7691
7723
  messagesSig.value = withWelcomeRows([], markers);
7692
7724
  }
7725
+ if (transport.resumeStatus === "needs-generation") regenerateDanglingRef.current?.();
7693
7726
  } catch (err) {
7694
7727
  if (isStale()) return;
7695
7728
  log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
@@ -7819,11 +7852,28 @@ function App({ options, hostElement, bus }) {
7819
7852
  useEffect16(() => {
7820
7853
  if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
7821
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
+ );
7822
7871
  const runResume = useCallback6(
7823
7872
  async (handle) => {
7824
7873
  let bubble = null;
7825
7874
  try {
7826
7875
  for await (const evt of handle.iter) {
7876
+ if (adoptConversationRebind(evt.chunk)) continue;
7827
7877
  if (!bubble) {
7828
7878
  bubble = makeAssistantMessage();
7829
7879
  resumeBubbleRef.current = bubble;
@@ -7867,7 +7917,7 @@ function App({ options, hostElement, bus }) {
7867
7917
  }
7868
7918
  }
7869
7919
  },
7870
- [reducer, messagesSig, feedback, bus, options, homeNav]
7920
+ [reducer, messagesSig, feedback, bus, options, homeNav, adoptConversationRebind]
7871
7921
  );
7872
7922
  const resumeHandleRef = useRef9(null);
7873
7923
  useEffect16(() => {
@@ -7892,16 +7942,22 @@ function App({ options, hostElement, bus }) {
7892
7942
  setFormsReady(true);
7893
7943
  }
7894
7944
  })();
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
- }
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
+ })();
7905
7961
  }, [activated]);
7906
7962
  const formsLocaleRef = useRef9(null);
7907
7963
  useEffect16(() => {
@@ -7974,6 +8030,7 @@ function App({ options, hostElement, bus }) {
7974
8030
  setActiveCancel(() => handle.cancel);
7975
8031
  try {
7976
8032
  for await (const evt of handle.iter) {
8033
+ if (adoptConversationRebind(evt.chunk)) continue;
7977
8034
  reducer.apply(evt.chunk);
7978
8035
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) {
7979
8036
  setCanSend(false);
@@ -8014,8 +8071,35 @@ function App({ options, hostElement, bus }) {
8014
8071
  messagesSig.value = [...messagesSig.value];
8015
8072
  }
8016
8073
  },
8017
- [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
+ ]
8018
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]);
8019
8103
  const decideTool = useCallback6(
8020
8104
  (toolCallId, mutate) => {
8021
8105
  if (streaming) return;