@firstlovecenter/ai-chat 0.9.1 → 0.9.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to `@firstlovecenter/ai-chat` are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.2] — 2026-05-09
9
+
10
+ Fixes a streaming-vs-navigation race that caused the first turn of a brand-new conversation to vanish on reload.
11
+
12
+ ### Fixed
13
+
14
+ - **Race condition on lazy-create-on-submit**. When the user sent the first message in a fresh conversation, the client used `router.push('/chat/<uid>')` to update the URL. App-Router navigation **unmounted the chat component mid-streaming**, so the in-flight SSE / Vercel-AI data stream was dropped on the floor. The newly-mounted page would then re-hydrate from `/api/chat/sessions/<uid>` *before* the assistant message had finished persisting — so reload showed an empty thread. Now the URL is updated via `window.history.replaceState` (shallow update, no remount), and the streaming connection survives. Affects both `AiChat` and `VercelChat`.
15
+
16
+ ### Added
17
+
18
+ - **Defensive `meta` event handling**. The custom-SSE route emits `event: meta` carrying the persistent `chatSessionId` as its first frame; the Vercel route emits the same as a `{ type: 'meta' }` data part. Both shells now sync `activeSessionId` and the URL from that event as a backstop, so even if the lazy-create POST silently fails the client still picks up the server-resolved session id.
19
+
8
20
  ## [0.9.1] — 2026-05-09
9
21
 
10
22
  Lets a host mount multiple chat surfaces side-by-side without yanking users between shells.
package/dist/ui/index.cjs CHANGED
@@ -874,7 +874,9 @@ function AiChat({
874
874
  const data = await create.json();
875
875
  sessionId = data.session.id;
876
876
  setActiveSessionId(sessionId);
877
- syncUrl(sessionId);
877
+ if (typeof window !== "undefined") {
878
+ window.history.replaceState(null, "", `${basePath}/${sessionId}`);
879
+ }
878
880
  setSessions((prev) => [
879
881
  { id: data.session.id, title: data.session.title, updatedAt: null },
880
882
  ...prev
@@ -936,6 +938,17 @@ function AiChat({
936
938
  const events = buffer.split("\n\n");
937
939
  buffer = events.pop() ?? "";
938
940
  for (const raw of events) {
941
+ const meta = parseMetaChatSessionId(raw);
942
+ if (meta != null) {
943
+ setActiveSessionId((prev) => prev ?? meta);
944
+ if (typeof window !== "undefined" && !window.location.pathname.endsWith(`/${meta}`)) {
945
+ window.history.replaceState(
946
+ null,
947
+ "",
948
+ `${basePath}/${meta}`
949
+ );
950
+ }
951
+ }
939
952
  handleEvent(raw, setAnswers);
940
953
  }
941
954
  }
@@ -1394,6 +1407,22 @@ function UserChip({ text }) {
1394
1407
  )
1395
1408
  ] });
1396
1409
  }
1410
+ function parseMetaChatSessionId(raw) {
1411
+ const lines = raw.split("\n");
1412
+ let event = "";
1413
+ let dataStr = "";
1414
+ for (const line of lines) {
1415
+ if (line.startsWith("event: ")) event = line.slice(7).trim();
1416
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
1417
+ }
1418
+ if (event !== "meta") return null;
1419
+ try {
1420
+ const parsed = JSON.parse(dataStr || "{}");
1421
+ return typeof parsed.chatSessionId === "string" && parsed.chatSessionId.length > 0 ? parsed.chatSessionId : null;
1422
+ } catch {
1423
+ return null;
1424
+ }
1425
+ }
1397
1426
  function handleEvent(raw, setAnswers) {
1398
1427
  const lines = raw.split("\n");
1399
1428
  let event = "";
@@ -1646,6 +1675,20 @@ function VercelChat({
1646
1675
  cancelled = true;
1647
1676
  };
1648
1677
  }, [initialSessionId, setMessages]);
1678
+ React.useEffect(() => {
1679
+ if (!Array.isArray(data)) return;
1680
+ for (const raw of data) {
1681
+ const part = asDataPart(raw);
1682
+ if (!part || part.type !== "meta") continue;
1683
+ const id = part.value.chatSessionId;
1684
+ if (!id) continue;
1685
+ setActiveSessionId((prev) => prev ?? id);
1686
+ if (typeof window !== "undefined" && !window.location.pathname.endsWith(`/${id}`)) {
1687
+ window.history.replaceState(null, "", `${basePath}/${id}`);
1688
+ }
1689
+ break;
1690
+ }
1691
+ }, [data, basePath]);
1649
1692
  const answers = React.useMemo(() => {
1650
1693
  const liveBlocks = [];
1651
1694
  const liveErrors = [];
@@ -1868,7 +1911,13 @@ function VercelChat({
1868
1911
  const json = await create.json();
1869
1912
  activeSessionIdRef.current = json.session.id;
1870
1913
  setActiveSessionId(json.session.id);
1871
- syncUrl(json.session.id);
1914
+ if (typeof window !== "undefined") {
1915
+ window.history.replaceState(
1916
+ null,
1917
+ "",
1918
+ `${basePath}/${json.session.id}`
1919
+ );
1920
+ }
1872
1921
  setSessions((prev) => [
1873
1922
  { id: json.session.id, title: json.session.title, updatedAt: null },
1874
1923
  ...prev