@anakin824/prdg-chat-ui 0.3.1 → 0.3.3

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/dist/index.d.mts CHANGED
@@ -482,6 +482,8 @@ type Props = {
482
482
  };
483
483
  declare function TypingIndicator({ names }: Props): react.JSX.Element | null;
484
484
 
485
+ /** UUIDs are case-insensitive; normalize so NATS + UI always share one cache entry. */
486
+ declare function normalizeConversationCacheId(conversationId: string | null): string | null;
485
487
  /** Stable React Query keys for chat — use with invalidateQueries / host integrations. */
486
488
  declare const chatKeys: {
487
489
  all: readonly ["chat"];
@@ -734,4 +736,4 @@ declare function useVoiceRecorder(): {
734
736
  } | null>;
735
737
  };
736
738
 
737
- export { type AppUser, ChatAPI, type ChatAppearance, MessageInput as ChatComposer, type ChatConfig, type ChatContextValue, ChatConversationView, type ChatConversationViewProps, ChatInboxSidebar, type ChatInboxSidebarProps, ChatMainColumn, type ChatMainColumnProps, ChatPanel, type ChatPanelProps, ChatProvider, type ChatProviderProps, type ChatTheme, ChatWidget, type ChatWidgetProps, type Conversation, ConversationList, type EntityConversationContext as EntityContext, INITIAL_MESSAGE_PAGE_SIZE, type Message, type MessageAttachment, MessageBubble, MessageInput, type MessagePage, MessageThread, NewChatModal, type SidebarTab, StandaloneChatPage, type StandaloneChatPageProps, TypingIndicator, type UserIdentityMode, type WSServerEvent, chatKeys, convDisplayName, convLabel, formatConvTime, initials, mergeMessagePages, useChat, useChatActions, useChatPanelController, useConversationMembers, useConversations, useMessages, usePresignedUrl, useUpload, useVoiceRecorder };
739
+ export { type AppUser, ChatAPI, type ChatAppearance, MessageInput as ChatComposer, type ChatConfig, type ChatContextValue, ChatConversationView, type ChatConversationViewProps, ChatInboxSidebar, type ChatInboxSidebarProps, ChatMainColumn, type ChatMainColumnProps, ChatPanel, type ChatPanelProps, ChatProvider, type ChatProviderProps, type ChatTheme, ChatWidget, type ChatWidgetProps, type Conversation, ConversationList, type EntityConversationContext as EntityContext, INITIAL_MESSAGE_PAGE_SIZE, type Message, type MessageAttachment, MessageBubble, MessageInput, type MessagePage, MessageThread, NewChatModal, type SidebarTab, StandaloneChatPage, type StandaloneChatPageProps, TypingIndicator, type UserIdentityMode, type WSServerEvent, chatKeys, convDisplayName, convLabel, formatConvTime, initials, mergeMessagePages, normalizeConversationCacheId, useChat, useChatActions, useChatPanelController, useConversationMembers, useConversations, useMessages, usePresignedUrl, useUpload, useVoiceRecorder };
package/dist/index.d.ts CHANGED
@@ -482,6 +482,8 @@ type Props = {
482
482
  };
483
483
  declare function TypingIndicator({ names }: Props): react.JSX.Element | null;
484
484
 
485
+ /** UUIDs are case-insensitive; normalize so NATS + UI always share one cache entry. */
486
+ declare function normalizeConversationCacheId(conversationId: string | null): string | null;
485
487
  /** Stable React Query keys for chat — use with invalidateQueries / host integrations. */
486
488
  declare const chatKeys: {
487
489
  all: readonly ["chat"];
@@ -734,4 +736,4 @@ declare function useVoiceRecorder(): {
734
736
  } | null>;
735
737
  };
736
738
 
737
- export { type AppUser, ChatAPI, type ChatAppearance, MessageInput as ChatComposer, type ChatConfig, type ChatContextValue, ChatConversationView, type ChatConversationViewProps, ChatInboxSidebar, type ChatInboxSidebarProps, ChatMainColumn, type ChatMainColumnProps, ChatPanel, type ChatPanelProps, ChatProvider, type ChatProviderProps, type ChatTheme, ChatWidget, type ChatWidgetProps, type Conversation, ConversationList, type EntityConversationContext as EntityContext, INITIAL_MESSAGE_PAGE_SIZE, type Message, type MessageAttachment, MessageBubble, MessageInput, type MessagePage, MessageThread, NewChatModal, type SidebarTab, StandaloneChatPage, type StandaloneChatPageProps, TypingIndicator, type UserIdentityMode, type WSServerEvent, chatKeys, convDisplayName, convLabel, formatConvTime, initials, mergeMessagePages, useChat, useChatActions, useChatPanelController, useConversationMembers, useConversations, useMessages, usePresignedUrl, useUpload, useVoiceRecorder };
739
+ export { type AppUser, ChatAPI, type ChatAppearance, MessageInput as ChatComposer, type ChatConfig, type ChatContextValue, ChatConversationView, type ChatConversationViewProps, ChatInboxSidebar, type ChatInboxSidebarProps, ChatMainColumn, type ChatMainColumnProps, ChatPanel, type ChatPanelProps, ChatProvider, type ChatProviderProps, type ChatTheme, ChatWidget, type ChatWidgetProps, type Conversation, ConversationList, type EntityConversationContext as EntityContext, INITIAL_MESSAGE_PAGE_SIZE, type Message, type MessageAttachment, MessageBubble, MessageInput, type MessagePage, MessageThread, NewChatModal, type SidebarTab, StandaloneChatPage, type StandaloneChatPageProps, TypingIndicator, type UserIdentityMode, type WSServerEvent, chatKeys, convDisplayName, convLabel, formatConvTime, initials, mergeMessagePages, normalizeConversationCacheId, useChat, useChatActions, useChatPanelController, useConversationMembers, useConversations, useMessages, usePresignedUrl, useUpload, useVoiceRecorder };
package/dist/index.js CHANGED
@@ -185,10 +185,15 @@ function chatDebugWarn(debug, ...args) {
185
185
  }
186
186
 
187
187
  // src/chat/lib/queryKeys.ts
188
+ function normalizeConversationCacheId(conversationId) {
189
+ if (conversationId == null) return null;
190
+ const t = conversationId.trim();
191
+ return t ? t.toLowerCase() : null;
192
+ }
188
193
  var chatKeys = {
189
194
  all: ["chat"],
190
195
  conversations: () => [...chatKeys.all, "conversations"],
191
- messages: (conversationId) => [...chatKeys.all, "messages", conversationId],
196
+ messages: (conversationId) => [...chatKeys.all, "messages", normalizeConversationCacheId(conversationId)],
192
197
  members: (conversationId) => [...chatKeys.all, "members", conversationId],
193
198
  contacts: (q) => [...chatKeys.all, "contacts", q]
194
199
  };
@@ -227,74 +232,149 @@ function userInboxSubject(natsTenantId, userId) {
227
232
  function entityConversationSubject(natsTenantId, conversationId) {
228
233
  return `chat.${natsTenantId}.conversation.${conversationId}`;
229
234
  }
235
+ function parseMessage(raw) {
236
+ if (typeof raw.id !== "string" || typeof raw.conversation_id !== "string") {
237
+ return null;
238
+ }
239
+ return {
240
+ id: raw.id,
241
+ conversation_id: raw.conversation_id,
242
+ sender_id: typeof raw.sender_id === "string" ? raw.sender_id : "",
243
+ body: typeof raw.body === "string" ? raw.body : null,
244
+ created_at: typeof raw.created_at === "string" ? raw.created_at : (/* @__PURE__ */ new Date()).toISOString(),
245
+ edited_at: typeof raw.edited_at === "string" ? raw.edited_at : null,
246
+ deleted_at: typeof raw.deleted_at === "string" ? raw.deleted_at : null,
247
+ attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
248
+ mentioned_user_ids: Array.isArray(raw.mentioned_user_ids) ? raw.mentioned_user_ids : []
249
+ };
250
+ }
251
+ function getMessagePageSummary(queryClient, conversationId) {
252
+ const page = queryClient.getQueryData(chatKeys.messages(conversationId));
253
+ const lastItem = page && page.items.length > 0 ? page.items[page.items.length - 1] : null;
254
+ return {
255
+ count: page?.items.length ?? 0,
256
+ firstId: page?.items[0]?.id ?? null,
257
+ lastId: lastItem?.id ?? null
258
+ };
259
+ }
230
260
  function pushMessageToCache(queryClient, msg) {
231
- let inserted = false;
232
- queryClient.setQueryData(
233
- chatKeys.messages(msg.conversation_id),
234
- (prev) => {
235
- if (!prev) return void 0;
236
- if (prev.items.some((m) => m.id === msg.id)) return prev;
237
- inserted = true;
238
- return { ...prev, items: [msg, ...prev.items] };
261
+ let result = "missing-cache";
262
+ queryClient.setQueryData(chatKeys.messages(msg.conversation_id), (prev) => {
263
+ if (!prev) {
264
+ result = "seeded";
265
+ return { items: [msg], next_cursor: "" };
239
266
  }
240
- );
241
- return inserted;
267
+ if (prev.items.some((m) => m.id === msg.id)) {
268
+ result = "duplicate";
269
+ return prev;
270
+ }
271
+ result = "updated";
272
+ return { ...prev, items: [msg, ...prev.items] };
273
+ });
274
+ return result;
242
275
  }
243
276
  function markDeletedInCache(queryClient, conversationId, messageId, deletedAt) {
277
+ let result = "missing-cache";
244
278
  queryClient.setQueryData(
245
279
  chatKeys.messages(conversationId),
246
280
  (prev) => {
247
281
  if (!prev) return void 0;
248
282
  const idx = prev.items.findIndex((m) => m.id === messageId);
249
- if (idx === -1) return prev;
283
+ if (idx === -1) {
284
+ result = "missing-message";
285
+ return prev;
286
+ }
250
287
  const updated = [...prev.items];
251
288
  updated[idx] = { ...updated[idx], deleted_at: deletedAt };
289
+ result = "updated";
252
290
  return { ...prev, items: updated };
253
291
  }
254
292
  );
293
+ return result;
255
294
  }
256
295
  function applyEditInCache(queryClient, updated) {
296
+ let result = "missing-cache";
257
297
  queryClient.setQueryData(
258
298
  chatKeys.messages(updated.conversation_id),
259
299
  (prev) => {
260
300
  if (!prev) return void 0;
261
301
  const idx = prev.items.findIndex((m) => m.id === updated.id);
262
- if (idx === -1) return prev;
302
+ if (idx === -1) {
303
+ result = "missing-message";
304
+ return prev;
305
+ }
263
306
  const items = [...prev.items];
264
307
  items[idx] = { ...items[idx], ...updated };
308
+ result = "updated";
265
309
  return { ...prev, items };
266
310
  }
267
311
  );
312
+ return result;
268
313
  }
269
- function invalidateQueriesFromNatsPayload(payload, queryClient) {
270
- if (!payload || typeof payload !== "object") return;
314
+ function invalidateQueriesFromNatsPayload(payload, queryClient, debug = false) {
315
+ if (!payload || typeof payload !== "object") {
316
+ chatDebugWarn(debug, "NATS cache update ignored: payload was not an object", payload);
317
+ return;
318
+ }
271
319
  const p = payload;
272
320
  const eventType = p.type;
273
- if (typeof eventType !== "string") return;
321
+ if (typeof eventType !== "string") {
322
+ chatDebugWarn(debug, "NATS cache update ignored: payload.type missing", p);
323
+ return;
324
+ }
274
325
  const ed = p.event_data;
275
- if (!ed || typeof ed !== "object") return;
326
+ if (!ed || typeof ed !== "object") {
327
+ chatDebugWarn(debug, "NATS cache update ignored: payload.event_data missing", {
328
+ eventType,
329
+ payload: p
330
+ });
331
+ return;
332
+ }
276
333
  const eventData = ed;
334
+ const conversationId = typeof eventData.conversation_id === "string" ? eventData.conversation_id : null;
335
+ chatDebugLog(debug, "NATS cache update received", {
336
+ eventType,
337
+ conversationId,
338
+ hasMessage: !!eventData.message && typeof eventData.message === "object"
339
+ });
277
340
  if (eventType === "chat.message.new") {
278
341
  const convId = eventData.conversation_id;
279
- if (typeof convId !== "string" || !convId.trim()) return;
342
+ if (typeof convId !== "string" || !convId.trim()) {
343
+ chatDebugWarn(debug, "NATS message.new ignored: missing conversation_id", eventData);
344
+ return;
345
+ }
346
+ const before = getMessagePageSummary(queryClient, convId);
280
347
  const msgRaw = eventData.message;
281
348
  if (msgRaw && typeof msgRaw === "object") {
282
349
  const raw = msgRaw;
283
- if (typeof raw.id === "string" && typeof raw.conversation_id === "string") {
284
- const msg = {
285
- id: raw.id,
286
- conversation_id: raw.conversation_id,
287
- sender_id: typeof raw.sender_id === "string" ? raw.sender_id : "",
288
- body: typeof raw.body === "string" ? raw.body : null,
289
- created_at: typeof raw.created_at === "string" ? raw.created_at : (/* @__PURE__ */ new Date()).toISOString(),
290
- edited_at: typeof raw.edited_at === "string" ? raw.edited_at : null,
291
- deleted_at: typeof raw.deleted_at === "string" ? raw.deleted_at : null,
292
- attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
293
- mentioned_user_ids: Array.isArray(raw.mentioned_user_ids) ? raw.mentioned_user_ids : []
294
- };
295
- pushMessageToCache(queryClient, msg);
350
+ const msg = parseMessage(raw);
351
+ if (msg) {
352
+ const mutation = pushMessageToCache(queryClient, msg);
353
+ const after = getMessagePageSummary(queryClient, convId);
354
+ chatDebugLog(debug, "NATS message.new cache mutation", {
355
+ conversationId: convId,
356
+ messageId: msg.id,
357
+ mutation,
358
+ before,
359
+ after
360
+ });
361
+ } else {
362
+ chatDebugWarn(debug, "NATS message.new ignored: event_data.message missing required ids", {
363
+ conversationId: convId,
364
+ message: raw
365
+ });
296
366
  }
367
+ } else {
368
+ chatDebugWarn(debug, "NATS message.new ignored: event_data.message missing or not an object", {
369
+ conversationId: convId,
370
+ eventData
371
+ });
297
372
  }
373
+ chatDebugLog(debug, "NATS message.new invalidating queries", {
374
+ conversationId: convId,
375
+ messageQueryKey: chatKeys.messages(convId),
376
+ conversationsQueryKey: chatKeys.conversations()
377
+ });
298
378
  void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
299
379
  void queryClient.invalidateQueries({ queryKey: chatKeys.conversations() });
300
380
  return;
@@ -303,21 +383,20 @@ function invalidateQueriesFromNatsPayload(payload, queryClient) {
303
383
  const msgRaw = eventData.message;
304
384
  if (msgRaw && typeof msgRaw === "object") {
305
385
  const raw = msgRaw;
306
- if (typeof raw.id === "string" && typeof raw.conversation_id === "string") {
307
- const updated = {
308
- id: raw.id,
309
- conversation_id: raw.conversation_id,
310
- sender_id: typeof raw.sender_id === "string" ? raw.sender_id : "",
311
- body: typeof raw.body === "string" ? raw.body : null,
312
- created_at: typeof raw.created_at === "string" ? raw.created_at : (/* @__PURE__ */ new Date()).toISOString(),
313
- edited_at: typeof raw.edited_at === "string" ? raw.edited_at : null,
314
- deleted_at: typeof raw.deleted_at === "string" ? raw.deleted_at : null,
315
- attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
316
- mentioned_user_ids: Array.isArray(raw.mentioned_user_ids) ? raw.mentioned_user_ids : []
317
- };
318
- applyEditInCache(queryClient, updated);
386
+ const updated = parseMessage(raw);
387
+ if (updated) {
388
+ const mutation = applyEditInCache(queryClient, updated);
389
+ chatDebugLog(debug, "NATS message.edited cache mutation", {
390
+ conversationId: updated.conversation_id,
391
+ messageId: updated.id,
392
+ mutation
393
+ });
319
394
  void queryClient.invalidateQueries({ queryKey: chatKeys.messages(updated.conversation_id) });
395
+ } else {
396
+ chatDebugWarn(debug, "NATS message.edited ignored: event_data.message missing required ids", raw);
320
397
  }
398
+ } else {
399
+ chatDebugWarn(debug, "NATS message.edited ignored: event_data.message missing or not an object", eventData);
321
400
  }
322
401
  return;
323
402
  }
@@ -329,16 +408,27 @@ function invalidateQueriesFromNatsPayload(payload, queryClient) {
329
408
  const convId = raw.conversation_id;
330
409
  const deletedAt = typeof raw.deleted_at === "string" ? raw.deleted_at : (/* @__PURE__ */ new Date()).toISOString();
331
410
  if (typeof msgId === "string" && typeof convId === "string") {
332
- markDeletedInCache(queryClient, convId, msgId, deletedAt);
411
+ const mutation = markDeletedInCache(queryClient, convId, msgId, deletedAt);
412
+ chatDebugLog(debug, "NATS message.deleted cache mutation", {
413
+ conversationId: convId,
414
+ messageId: msgId,
415
+ mutation
416
+ });
333
417
  void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
418
+ } else {
419
+ chatDebugWarn(debug, "NATS message.deleted ignored: event_data.message missing required ids", raw);
334
420
  }
421
+ } else {
422
+ chatDebugWarn(debug, "NATS message.deleted ignored: event_data.message missing or not an object", eventData);
335
423
  }
424
+ return;
336
425
  }
426
+ chatDebugLog(debug, "NATS payload ignored: unsupported event type", { eventType, eventData });
337
427
  }
338
428
 
339
429
  // src/chat/provider/ChatNatsBridge.tsx
340
430
  function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
341
- const { natsTenantId, userId, queryClient } = useChat();
431
+ const { natsTenantId, userId, queryClient, debug } = useChat();
342
432
  const { data: convData } = useConversations();
343
433
  const entityConversationIds = react.useMemo(() => {
344
434
  const items = convData?.items ?? [];
@@ -351,6 +441,13 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
351
441
  const connRef = react.useRef(null);
352
442
  react.useEffect(() => {
353
443
  if (servers.length === 0) return;
444
+ const entityConvIds = entityKey ? entityKey.split(",").filter(Boolean) : [];
445
+ chatDebugLog(debug, "ChatNatsBridge: starting subscriptions", {
446
+ userId,
447
+ natsTenantId,
448
+ servers,
449
+ entityConversationIds: entityConvIds
450
+ });
354
451
  let cancelled = false;
355
452
  void (async () => {
356
453
  try {
@@ -369,10 +466,21 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
369
466
  }
370
467
  connRef.current = conn;
371
468
  onConnectedChangeRef.current(true);
469
+ chatDebugLog(debug, "ChatNatsBridge: NATS connected", {
470
+ userId,
471
+ natsTenantId,
472
+ servers
473
+ });
372
474
  const handleMsg = (data, subject) => {
475
+ const payloadText = new TextDecoder().decode(data);
373
476
  if (process.env.NODE_ENV === "development") {
374
477
  console.debug("[NATS] raw message received on subject:", subject, "bytes:", data.length);
375
478
  }
479
+ chatDebugLog(debug, "ChatNatsBridge: NATS payload received", {
480
+ subject,
481
+ bytes: data.length,
482
+ payloadText
483
+ });
376
484
  let payload;
377
485
  try {
378
486
  payload = jc.decode(data);
@@ -385,7 +493,7 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
385
493
  if (process.env.NODE_ENV === "development") {
386
494
  console.debug("[NATS] decoded payload:", payload);
387
495
  }
388
- invalidateQueriesFromNatsPayload(payload, queryClient);
496
+ invalidateQueriesFromNatsPayload(payload, queryClient, debug);
389
497
  if (process.env.NODE_ENV === "development") {
390
498
  if (payload && typeof payload === "object" && payload.type === "chat.message.new") {
391
499
  const p = payload;
@@ -423,6 +531,7 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
423
531
  }
424
532
  };
425
533
  const inbox = userInboxSubject(natsTenantId, userId);
534
+ chatDebugLog(debug, "ChatNatsBridge: subscribing inbox", { inbox });
426
535
  const subInbox = conn.subscribe(inbox);
427
536
  void (async () => {
428
537
  for await (const m of subInbox) {
@@ -430,8 +539,12 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
430
539
  handleMsg(m.data, m.subject);
431
540
  }
432
541
  })();
433
- for (const convId of entityConversationIds) {
542
+ for (const convId of entityConvIds) {
434
543
  const subj = entityConversationSubject(natsTenantId, convId);
544
+ chatDebugLog(debug, "ChatNatsBridge: subscribing entity conversation", {
545
+ conversationId: convId,
546
+ subject: subj
547
+ });
435
548
  const sub = conn.subscribe(subj);
436
549
  void (async () => {
437
550
  for await (const m of sub) {
@@ -443,7 +556,7 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
443
556
  if (process.env.NODE_ENV === "development") {
444
557
  const subjectList = [
445
558
  inbox,
446
- ...entityConversationIds.map((id) => entityConversationSubject(natsTenantId, id))
559
+ ...entityConvIds.map((id) => entityConversationSubject(natsTenantId, id))
447
560
  ];
448
561
  console.info("[NATS] WebSocket connected \u2014 servers:", servers, "subjects:", subjectList);
449
562
  void (async () => {
@@ -455,23 +568,26 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
455
568
  }
456
569
  await conn.closed();
457
570
  connRef.current = null;
571
+ chatDebugWarn(debug, "ChatNatsBridge: NATS connection closed");
458
572
  if (!cancelled) onConnectedChangeRef.current(false);
459
573
  } catch (err) {
460
574
  connRef.current = null;
461
575
  if (process.env.NODE_ENV === "development") {
462
576
  console.error("[NATS] WebSocket connect failed \u2014 check ws:// URL, NATS websocket block, and CORS/mixed content:", err);
463
577
  }
578
+ chatDebugWarn(debug, "ChatNatsBridge: NATS connection failed", err);
464
579
  if (!cancelled) onConnectedChangeRef.current(false);
465
580
  }
466
581
  })();
467
582
  return () => {
468
583
  cancelled = true;
584
+ chatDebugLog(debug, "ChatNatsBridge: cleaning up subscriptions");
469
585
  onConnectedChangeRef.current(false);
470
586
  const c = connRef.current;
471
587
  connRef.current = null;
472
588
  void c?.drain();
473
589
  };
474
- }, [servers, natsToken, natsTenantId, userId, entityKey, queryClient, entityConversationIds]);
590
+ }, [servers, natsToken, natsTenantId, userId, entityKey, queryClient, debug]);
475
591
  return null;
476
592
  }
477
593
 
@@ -847,16 +963,28 @@ function appendOlderMessages(current, older) {
847
963
  // src/chat/hooks/useMessages.ts
848
964
  var INITIAL_MESSAGE_PAGE_SIZE = 30;
849
965
  function useMessages(conversationId) {
850
- const { api, wsConnected, config, queryClient } = useChat();
966
+ const { api, wsConnected, config, queryClient, debug } = useChat();
851
967
  const interval = config.pollIntervalMs ?? 3e4;
852
968
  const [isFetchingOlder, setIsFetchingOlder] = react.useState(false);
853
969
  const fetchingRef = react.useRef(false);
854
970
  const query = reactQuery.useQuery({
855
971
  queryKey: chatKeys.messages(conversationId),
856
972
  queryFn: async () => {
973
+ chatDebugLog(debug, "useMessages: queryFn start", {
974
+ conversationId,
975
+ wsConnected
976
+ });
857
977
  const res = await api.listMessages(conversationId, void 0, INITIAL_MESSAGE_PAGE_SIZE);
858
978
  const prev = queryClient.getQueryData(chatKeys.messages(conversationId));
859
- return mergeMessagePages(res, prev);
979
+ const merged = mergeMessagePages(res, prev);
980
+ chatDebugLog(debug, "useMessages: queryFn success", {
981
+ conversationId,
982
+ serverCount: res.items.length,
983
+ previousCachedCount: prev?.items.length ?? 0,
984
+ mergedCount: merged.items.length,
985
+ firstMessageId: merged.items[0]?.id ?? null
986
+ });
987
+ return merged;
860
988
  },
861
989
  enabled: !!conversationId,
862
990
  refetchInterval: wsConnected ? false : interval
@@ -880,6 +1008,29 @@ function useMessages(conversationId) {
880
1008
  }, [conversationId, queryClient, api]);
881
1009
  const cached = queryClient.getQueryData(chatKeys.messages(conversationId));
882
1010
  const hasMoreMessages = !!cached?.next_cursor;
1011
+ react.useEffect(() => {
1012
+ if (!conversationId) return;
1013
+ chatDebugLog(debug, "useMessages: hook snapshot", {
1014
+ conversationId,
1015
+ wsConnected,
1016
+ status: query.status,
1017
+ fetchStatus: query.fetchStatus,
1018
+ dataCount: query.data?.items.length ?? 0,
1019
+ cachedCount: cached?.items.length ?? 0,
1020
+ firstDataMessageId: query.data?.items[0]?.id ?? null,
1021
+ firstCachedMessageId: cached?.items[0]?.id ?? null
1022
+ });
1023
+ }, [
1024
+ cached?.items.length,
1025
+ cached?.items[0]?.id,
1026
+ conversationId,
1027
+ debug,
1028
+ query.data?.items.length,
1029
+ query.data?.items[0]?.id,
1030
+ query.fetchStatus,
1031
+ query.status,
1032
+ wsConnected
1033
+ ]);
883
1034
  return {
884
1035
  ...query,
885
1036
  fetchOlderMessages,
@@ -2280,6 +2431,7 @@ exports.convLabel = convLabel;
2280
2431
  exports.formatConvTime = formatConvTime;
2281
2432
  exports.initials = initials2;
2282
2433
  exports.mergeMessagePages = mergeMessagePages;
2434
+ exports.normalizeConversationCacheId = normalizeConversationCacheId;
2283
2435
  exports.useChat = useChat;
2284
2436
  exports.useChatActions = useChatActions;
2285
2437
  exports.useChatPanelController = useChatPanelController;