@anakin824/prdg-chat-ui 0.3.0 → 0.3.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/dist/index.d.mts CHANGED
@@ -249,15 +249,42 @@ declare class ChatAPI {
249
249
  }>;
250
250
  listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
251
251
  listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
252
- findOrCreateDirect(peerUserId: string): Promise<Conversation>;
253
- createGroup(title: string, members: string[]): Promise<Conversation>;
252
+ findOrCreateDirect(peerUserId: string, opts?: {
253
+ peerKind?: "internal" | "external";
254
+ }): Promise<Conversation>;
255
+ createGroup(title: string, members: string[], externalMembers?: string[]): Promise<Conversation>;
254
256
  findOrCreateEntity(input: {
255
257
  title: string;
256
258
  entity_ref: string;
257
259
  entity_uuid: string;
260
+ /** prdg-chat user UUIDs */
258
261
  members?: string[];
262
+ /** Integration / HDS user ids (`ext_user_id`); resolved server-side */
263
+ external_members?: string[];
259
264
  }): Promise<Conversation>;
265
+ /**
266
+ * Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
267
+ * 404 if none exists; 403 if the user is not a member.
268
+ */
269
+ getConversationByEntity(entity_ref: string, entity_uuid: string): Promise<Conversation>;
270
+ /** Add members to a group or entity conversation (admin only). */
271
+ addConversationMembers(conversationId: string, body: {
272
+ user_ids?: string[];
273
+ external_user_ids?: string[];
274
+ role?: "member" | "admin";
275
+ }): Promise<{
276
+ added_user_ids: string[];
277
+ }>;
260
278
  listConversationMembers(conversationId: string): Promise<AppUser[]>;
279
+ /**
280
+ * Remove a member by prdg-chat user id, or by integration `ext_user_id`
281
+ * (`DELETE .../members/external/:external_user_id`).
282
+ */
283
+ removeConversationMember(conversationId: string, by: {
284
+ userId: string;
285
+ } | {
286
+ externalUserId: string;
287
+ }): Promise<void>;
261
288
  listContacts(q?: string, limit?: number): Promise<AppUser[]>;
262
289
  sendMessage(conversationId: string, body: {
263
290
  body?: string | null;
package/dist/index.d.ts CHANGED
@@ -249,15 +249,42 @@ declare class ChatAPI {
249
249
  }>;
250
250
  listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
251
251
  listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
252
- findOrCreateDirect(peerUserId: string): Promise<Conversation>;
253
- createGroup(title: string, members: string[]): Promise<Conversation>;
252
+ findOrCreateDirect(peerUserId: string, opts?: {
253
+ peerKind?: "internal" | "external";
254
+ }): Promise<Conversation>;
255
+ createGroup(title: string, members: string[], externalMembers?: string[]): Promise<Conversation>;
254
256
  findOrCreateEntity(input: {
255
257
  title: string;
256
258
  entity_ref: string;
257
259
  entity_uuid: string;
260
+ /** prdg-chat user UUIDs */
258
261
  members?: string[];
262
+ /** Integration / HDS user ids (`ext_user_id`); resolved server-side */
263
+ external_members?: string[];
259
264
  }): Promise<Conversation>;
265
+ /**
266
+ * Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
267
+ * 404 if none exists; 403 if the user is not a member.
268
+ */
269
+ getConversationByEntity(entity_ref: string, entity_uuid: string): Promise<Conversation>;
270
+ /** Add members to a group or entity conversation (admin only). */
271
+ addConversationMembers(conversationId: string, body: {
272
+ user_ids?: string[];
273
+ external_user_ids?: string[];
274
+ role?: "member" | "admin";
275
+ }): Promise<{
276
+ added_user_ids: string[];
277
+ }>;
260
278
  listConversationMembers(conversationId: string): Promise<AppUser[]>;
279
+ /**
280
+ * Remove a member by prdg-chat user id, or by integration `ext_user_id`
281
+ * (`DELETE .../members/external/:external_user_id`).
282
+ */
283
+ removeConversationMember(conversationId: string, by: {
284
+ userId: string;
285
+ } | {
286
+ externalUserId: string;
287
+ }): Promise<void>;
261
288
  listContacts(q?: string, limit?: number): Promise<AppUser[]>;
262
289
  sendMessage(conversationId: string, body: {
263
290
  body?: string | null;
package/dist/index.js CHANGED
@@ -66,16 +66,18 @@ var ChatAPI = class {
66
66
  q.set("limit", String(limit));
67
67
  return this.json(`/conversations/${conversationId}/messages?${q}`);
68
68
  }
69
- findOrCreateDirect(peerUserId) {
69
+ findOrCreateDirect(peerUserId, opts) {
70
+ const kind = opts?.peerKind ?? "internal";
71
+ const body = kind === "external" ? { external_user_uuid: peerUserId } : { peer_user_id: peerUserId };
70
72
  return this.json("/conversations/direct", {
71
73
  method: "POST",
72
- body: JSON.stringify({ peer_user_id: peerUserId })
74
+ body: JSON.stringify(body)
73
75
  });
74
76
  }
75
- createGroup(title, members) {
77
+ createGroup(title, members, externalMembers = []) {
76
78
  return this.json("/conversations/group", {
77
79
  method: "POST",
78
- body: JSON.stringify({ title, members })
80
+ body: JSON.stringify({ title, members, external_members: externalMembers })
79
81
  });
80
82
  }
81
83
  findOrCreateEntity(input) {
@@ -85,13 +87,58 @@ var ChatAPI = class {
85
87
  title: input.title,
86
88
  entity_ref: input.entity_ref,
87
89
  entity_uuid: input.entity_uuid,
88
- members: input.members ?? []
90
+ members: input.members ?? [],
91
+ external_members: input.external_members ?? []
92
+ })
93
+ });
94
+ }
95
+ /**
96
+ * Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
97
+ * 404 if none exists; 403 if the user is not a member.
98
+ */
99
+ getConversationByEntity(entity_ref, entity_uuid) {
100
+ return this.json(
101
+ `/conversations/by-entity/ref/${encodeURIComponent(entity_ref)}/entity/${encodeURIComponent(entity_uuid)}`
102
+ );
103
+ }
104
+ /** Add members to a group or entity conversation (admin only). */
105
+ addConversationMembers(conversationId, body) {
106
+ return this.json(`/conversations/${conversationId}/members`, {
107
+ method: "POST",
108
+ body: JSON.stringify({
109
+ user_ids: body.user_ids ?? [],
110
+ external_user_ids: body.external_user_ids ?? [],
111
+ role: body.role
89
112
  })
90
113
  });
91
114
  }
92
115
  listConversationMembers(conversationId) {
93
116
  return this.json(`/conversations/${conversationId}/members`);
94
117
  }
118
+ /**
119
+ * Remove a member by prdg-chat user id, or by integration `ext_user_id`
120
+ * (`DELETE .../members/external/:external_user_id`).
121
+ */
122
+ async removeConversationMember(conversationId, by) {
123
+ const path = "externalUserId" in by ? `/conversations/${conversationId}/members/external/${encodeURIComponent(by.externalUserId)}` : `/conversations/${conversationId}/members/${by.userId}`;
124
+ let res = await fetch(`${this.baseUrl}${path}`, {
125
+ method: "DELETE",
126
+ headers: this.headers()
127
+ });
128
+ if (res.status === 401 && this.onAuthError) {
129
+ const newToken = await this.onAuthError();
130
+ if (newToken) {
131
+ res = await fetch(`${this.baseUrl}${path}`, {
132
+ method: "DELETE",
133
+ headers: this.headers(newToken)
134
+ });
135
+ }
136
+ }
137
+ if (!res.ok) {
138
+ const err = await res.text();
139
+ throw new Error(err || res.statusText);
140
+ }
141
+ }
95
142
  listContacts(q, limit = 50) {
96
143
  const params = new URLSearchParams();
97
144
  params.set("limit", String(limit));
@@ -180,22 +227,200 @@ function userInboxSubject(natsTenantId, userId) {
180
227
  function entityConversationSubject(natsTenantId, conversationId) {
181
228
  return `chat.${natsTenantId}.conversation.${conversationId}`;
182
229
  }
183
- function invalidateQueriesFromNatsPayload(payload, queryClient) {
184
- if (!payload || typeof payload !== "object") return;
230
+ function parseMessage(raw) {
231
+ if (typeof raw.id !== "string" || typeof raw.conversation_id !== "string") {
232
+ return null;
233
+ }
234
+ return {
235
+ id: raw.id,
236
+ conversation_id: raw.conversation_id,
237
+ sender_id: typeof raw.sender_id === "string" ? raw.sender_id : "",
238
+ body: typeof raw.body === "string" ? raw.body : null,
239
+ created_at: typeof raw.created_at === "string" ? raw.created_at : (/* @__PURE__ */ new Date()).toISOString(),
240
+ edited_at: typeof raw.edited_at === "string" ? raw.edited_at : null,
241
+ deleted_at: typeof raw.deleted_at === "string" ? raw.deleted_at : null,
242
+ attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
243
+ mentioned_user_ids: Array.isArray(raw.mentioned_user_ids) ? raw.mentioned_user_ids : []
244
+ };
245
+ }
246
+ function getMessagePageSummary(queryClient, conversationId) {
247
+ const page = queryClient.getQueryData(chatKeys.messages(conversationId));
248
+ const lastItem = page && page.items.length > 0 ? page.items[page.items.length - 1] : null;
249
+ return {
250
+ count: page?.items.length ?? 0,
251
+ firstId: page?.items[0]?.id ?? null,
252
+ lastId: lastItem?.id ?? null
253
+ };
254
+ }
255
+ function pushMessageToCache(queryClient, msg) {
256
+ let result = "missing-cache";
257
+ queryClient.setQueryData(chatKeys.messages(msg.conversation_id), (prev) => {
258
+ if (!prev) return void 0;
259
+ if (prev.items.some((m) => m.id === msg.id)) {
260
+ result = "duplicate";
261
+ return prev;
262
+ }
263
+ result = "updated";
264
+ return { ...prev, items: [msg, ...prev.items] };
265
+ });
266
+ return result;
267
+ }
268
+ function markDeletedInCache(queryClient, conversationId, messageId, deletedAt) {
269
+ let result = "missing-cache";
270
+ queryClient.setQueryData(
271
+ chatKeys.messages(conversationId),
272
+ (prev) => {
273
+ if (!prev) return void 0;
274
+ const idx = prev.items.findIndex((m) => m.id === messageId);
275
+ if (idx === -1) {
276
+ result = "missing-message";
277
+ return prev;
278
+ }
279
+ const updated = [...prev.items];
280
+ updated[idx] = { ...updated[idx], deleted_at: deletedAt };
281
+ result = "updated";
282
+ return { ...prev, items: updated };
283
+ }
284
+ );
285
+ return result;
286
+ }
287
+ function applyEditInCache(queryClient, updated) {
288
+ let result = "missing-cache";
289
+ queryClient.setQueryData(
290
+ chatKeys.messages(updated.conversation_id),
291
+ (prev) => {
292
+ if (!prev) return void 0;
293
+ const idx = prev.items.findIndex((m) => m.id === updated.id);
294
+ if (idx === -1) {
295
+ result = "missing-message";
296
+ return prev;
297
+ }
298
+ const items = [...prev.items];
299
+ items[idx] = { ...items[idx], ...updated };
300
+ result = "updated";
301
+ return { ...prev, items };
302
+ }
303
+ );
304
+ return result;
305
+ }
306
+ function invalidateQueriesFromNatsPayload(payload, queryClient, debug = false) {
307
+ if (!payload || typeof payload !== "object") {
308
+ chatDebugWarn(debug, "NATS cache update ignored: payload was not an object", payload);
309
+ return;
310
+ }
185
311
  const p = payload;
186
- if (p.type !== "chat.message.new") return;
312
+ const eventType = p.type;
313
+ if (typeof eventType !== "string") {
314
+ chatDebugWarn(debug, "NATS cache update ignored: payload.type missing", p);
315
+ return;
316
+ }
187
317
  const ed = p.event_data;
188
- if (!ed || typeof ed !== "object") return;
189
- const convId = ed.conversation_id;
190
- if (typeof convId !== "string" || !convId.trim()) return;
191
- void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
192
- void queryClient.invalidateQueries({ queryKey: chatKeys.conversations() });
318
+ if (!ed || typeof ed !== "object") {
319
+ chatDebugWarn(debug, "NATS cache update ignored: payload.event_data missing", {
320
+ eventType,
321
+ payload: p
322
+ });
323
+ return;
324
+ }
325
+ const eventData = ed;
326
+ const conversationId = typeof eventData.conversation_id === "string" ? eventData.conversation_id : null;
327
+ chatDebugLog(debug, "NATS cache update received", {
328
+ eventType,
329
+ conversationId,
330
+ hasMessage: !!eventData.message && typeof eventData.message === "object"
331
+ });
332
+ if (eventType === "chat.message.new") {
333
+ const convId = eventData.conversation_id;
334
+ if (typeof convId !== "string" || !convId.trim()) {
335
+ chatDebugWarn(debug, "NATS message.new ignored: missing conversation_id", eventData);
336
+ return;
337
+ }
338
+ const before = getMessagePageSummary(queryClient, convId);
339
+ const msgRaw = eventData.message;
340
+ if (msgRaw && typeof msgRaw === "object") {
341
+ const raw = msgRaw;
342
+ const msg = parseMessage(raw);
343
+ if (msg) {
344
+ const mutation = pushMessageToCache(queryClient, msg);
345
+ const after = getMessagePageSummary(queryClient, convId);
346
+ chatDebugLog(debug, "NATS message.new cache mutation", {
347
+ conversationId: convId,
348
+ messageId: msg.id,
349
+ mutation,
350
+ before,
351
+ after
352
+ });
353
+ } else {
354
+ chatDebugWarn(debug, "NATS message.new ignored: event_data.message missing required ids", {
355
+ conversationId: convId,
356
+ message: raw
357
+ });
358
+ }
359
+ } else {
360
+ chatDebugWarn(debug, "NATS message.new ignored: event_data.message missing or not an object", {
361
+ conversationId: convId,
362
+ eventData
363
+ });
364
+ }
365
+ chatDebugLog(debug, "NATS message.new invalidating queries", {
366
+ conversationId: convId,
367
+ messageQueryKey: chatKeys.messages(convId),
368
+ conversationsQueryKey: chatKeys.conversations()
369
+ });
370
+ void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
371
+ void queryClient.invalidateQueries({ queryKey: chatKeys.conversations() });
372
+ return;
373
+ }
374
+ if (eventType === "chat.message.edited") {
375
+ const msgRaw = eventData.message;
376
+ if (msgRaw && typeof msgRaw === "object") {
377
+ const raw = msgRaw;
378
+ const updated = parseMessage(raw);
379
+ if (updated) {
380
+ const mutation = applyEditInCache(queryClient, updated);
381
+ chatDebugLog(debug, "NATS message.edited cache mutation", {
382
+ conversationId: updated.conversation_id,
383
+ messageId: updated.id,
384
+ mutation
385
+ });
386
+ void queryClient.invalidateQueries({ queryKey: chatKeys.messages(updated.conversation_id) });
387
+ } else {
388
+ chatDebugWarn(debug, "NATS message.edited ignored: event_data.message missing required ids", raw);
389
+ }
390
+ } else {
391
+ chatDebugWarn(debug, "NATS message.edited ignored: event_data.message missing or not an object", eventData);
392
+ }
393
+ return;
394
+ }
395
+ if (eventType === "chat.message.deleted") {
396
+ const msgRaw = eventData.message;
397
+ if (msgRaw && typeof msgRaw === "object") {
398
+ const raw = msgRaw;
399
+ const msgId = raw.id;
400
+ const convId = raw.conversation_id;
401
+ const deletedAt = typeof raw.deleted_at === "string" ? raw.deleted_at : (/* @__PURE__ */ new Date()).toISOString();
402
+ if (typeof msgId === "string" && typeof convId === "string") {
403
+ const mutation = markDeletedInCache(queryClient, convId, msgId, deletedAt);
404
+ chatDebugLog(debug, "NATS message.deleted cache mutation", {
405
+ conversationId: convId,
406
+ messageId: msgId,
407
+ mutation
408
+ });
409
+ void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
410
+ } else {
411
+ chatDebugWarn(debug, "NATS message.deleted ignored: event_data.message missing required ids", raw);
412
+ }
413
+ } else {
414
+ chatDebugWarn(debug, "NATS message.deleted ignored: event_data.message missing or not an object", eventData);
415
+ }
416
+ return;
417
+ }
418
+ chatDebugLog(debug, "NATS payload ignored: unsupported event type", { eventType, eventData });
193
419
  }
194
420
 
195
421
  // src/chat/provider/ChatNatsBridge.tsx
196
422
  function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
197
- const { natsTenantId, userId } = useChat();
198
- const queryClient = reactQuery.useQueryClient();
423
+ const { natsTenantId, userId, queryClient, debug } = useChat();
199
424
  const { data: convData } = useConversations();
200
425
  const entityConversationIds = react.useMemo(() => {
201
426
  const items = convData?.items ?? [];
@@ -208,6 +433,12 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
208
433
  const connRef = react.useRef(null);
209
434
  react.useEffect(() => {
210
435
  if (servers.length === 0) return;
436
+ chatDebugLog(debug, "ChatNatsBridge: starting subscriptions", {
437
+ userId,
438
+ natsTenantId,
439
+ servers,
440
+ entityConversationIds
441
+ });
211
442
  let cancelled = false;
212
443
  void (async () => {
213
444
  try {
@@ -226,10 +457,21 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
226
457
  }
227
458
  connRef.current = conn;
228
459
  onConnectedChangeRef.current(true);
460
+ chatDebugLog(debug, "ChatNatsBridge: NATS connected", {
461
+ userId,
462
+ natsTenantId,
463
+ servers
464
+ });
229
465
  const handleMsg = (data, subject) => {
466
+ const payloadText = new TextDecoder().decode(data);
230
467
  if (process.env.NODE_ENV === "development") {
231
468
  console.debug("[NATS] raw message received on subject:", subject, "bytes:", data.length);
232
469
  }
470
+ chatDebugLog(debug, "ChatNatsBridge: NATS payload received", {
471
+ subject,
472
+ bytes: data.length,
473
+ payloadText
474
+ });
233
475
  let payload;
234
476
  try {
235
477
  payload = jc.decode(data);
@@ -242,11 +484,12 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
242
484
  if (process.env.NODE_ENV === "development") {
243
485
  console.debug("[NATS] decoded payload:", payload);
244
486
  }
245
- invalidateQueriesFromNatsPayload(payload, queryClient);
487
+ invalidateQueriesFromNatsPayload(payload, queryClient, debug);
246
488
  if (process.env.NODE_ENV === "development") {
247
489
  if (payload && typeof payload === "object" && payload.type === "chat.message.new") {
248
490
  const p = payload;
249
491
  const ed = p.event_data;
492
+ const msgRaw = ed?.message;
250
493
  const rawTs = typeof ed?.timestamp_utc === "string" ? ed.timestamp_utc : null;
251
494
  const localTime = rawTs ? new Date(rawTs).toLocaleTimeString(void 0, {
252
495
  hour: "numeric",
@@ -261,6 +504,8 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
261
504
  `
262
505
  conversation: ${typeof ed?.conversation_id === "string" ? ed.conversation_id : "?"}`,
263
506
  `
507
+ message_id: ${typeof msgRaw?.id === "string" ? msgRaw.id : "(no id \u2014 cache push skipped)"}`,
508
+ `
264
509
  sender: ${typeof ed?.sender_id === "string" ? ed.sender_id : "?"}`,
265
510
  `
266
511
  body: ${typeof ed?.body === "string" ? ed.body.slice(0, 200) : "(no body)"}`,
@@ -277,6 +522,7 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
277
522
  }
278
523
  };
279
524
  const inbox = userInboxSubject(natsTenantId, userId);
525
+ chatDebugLog(debug, "ChatNatsBridge: subscribing inbox", { inbox });
280
526
  const subInbox = conn.subscribe(inbox);
281
527
  void (async () => {
282
528
  for await (const m of subInbox) {
@@ -286,6 +532,10 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
286
532
  })();
287
533
  for (const convId of entityConversationIds) {
288
534
  const subj = entityConversationSubject(natsTenantId, convId);
535
+ chatDebugLog(debug, "ChatNatsBridge: subscribing entity conversation", {
536
+ conversationId: convId,
537
+ subject: subj
538
+ });
289
539
  const sub = conn.subscribe(subj);
290
540
  void (async () => {
291
541
  for await (const m of sub) {
@@ -309,23 +559,26 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
309
559
  }
310
560
  await conn.closed();
311
561
  connRef.current = null;
562
+ chatDebugWarn(debug, "ChatNatsBridge: NATS connection closed");
312
563
  if (!cancelled) onConnectedChangeRef.current(false);
313
564
  } catch (err) {
314
565
  connRef.current = null;
315
566
  if (process.env.NODE_ENV === "development") {
316
567
  console.error("[NATS] WebSocket connect failed \u2014 check ws:// URL, NATS websocket block, and CORS/mixed content:", err);
317
568
  }
569
+ chatDebugWarn(debug, "ChatNatsBridge: NATS connection failed", err);
318
570
  if (!cancelled) onConnectedChangeRef.current(false);
319
571
  }
320
572
  })();
321
573
  return () => {
322
574
  cancelled = true;
575
+ chatDebugLog(debug, "ChatNatsBridge: cleaning up subscriptions");
323
576
  onConnectedChangeRef.current(false);
324
577
  const c = connRef.current;
325
578
  connRef.current = null;
326
579
  void c?.drain();
327
580
  };
328
- }, [servers, natsToken, natsTenantId, userId, entityKey, queryClient, entityConversationIds]);
581
+ }, [servers, natsToken, natsTenantId, userId, entityKey, queryClient, entityConversationIds, debug]);
329
582
  return null;
330
583
  }
331
584
 
@@ -701,16 +954,28 @@ function appendOlderMessages(current, older) {
701
954
  // src/chat/hooks/useMessages.ts
702
955
  var INITIAL_MESSAGE_PAGE_SIZE = 30;
703
956
  function useMessages(conversationId) {
704
- const { api, wsConnected, config, queryClient } = useChat();
957
+ const { api, wsConnected, config, queryClient, debug } = useChat();
705
958
  const interval = config.pollIntervalMs ?? 3e4;
706
959
  const [isFetchingOlder, setIsFetchingOlder] = react.useState(false);
707
960
  const fetchingRef = react.useRef(false);
708
961
  const query = reactQuery.useQuery({
709
962
  queryKey: chatKeys.messages(conversationId),
710
963
  queryFn: async () => {
964
+ chatDebugLog(debug, "useMessages: queryFn start", {
965
+ conversationId,
966
+ wsConnected
967
+ });
711
968
  const res = await api.listMessages(conversationId, void 0, INITIAL_MESSAGE_PAGE_SIZE);
712
969
  const prev = queryClient.getQueryData(chatKeys.messages(conversationId));
713
- return mergeMessagePages(res, prev);
970
+ const merged = mergeMessagePages(res, prev);
971
+ chatDebugLog(debug, "useMessages: queryFn success", {
972
+ conversationId,
973
+ serverCount: res.items.length,
974
+ previousCachedCount: prev?.items.length ?? 0,
975
+ mergedCount: merged.items.length,
976
+ firstMessageId: merged.items[0]?.id ?? null
977
+ });
978
+ return merged;
714
979
  },
715
980
  enabled: !!conversationId,
716
981
  refetchInterval: wsConnected ? false : interval
@@ -734,6 +999,29 @@ function useMessages(conversationId) {
734
999
  }, [conversationId, queryClient, api]);
735
1000
  const cached = queryClient.getQueryData(chatKeys.messages(conversationId));
736
1001
  const hasMoreMessages = !!cached?.next_cursor;
1002
+ react.useEffect(() => {
1003
+ if (!conversationId) return;
1004
+ chatDebugLog(debug, "useMessages: hook snapshot", {
1005
+ conversationId,
1006
+ wsConnected,
1007
+ status: query.status,
1008
+ fetchStatus: query.fetchStatus,
1009
+ dataCount: query.data?.items.length ?? 0,
1010
+ cachedCount: cached?.items.length ?? 0,
1011
+ firstDataMessageId: query.data?.items[0]?.id ?? null,
1012
+ firstCachedMessageId: cached?.items[0]?.id ?? null
1013
+ });
1014
+ }, [
1015
+ cached?.items.length,
1016
+ cached?.items[0]?.id,
1017
+ conversationId,
1018
+ debug,
1019
+ query.data?.items.length,
1020
+ query.data?.items[0]?.id,
1021
+ query.fetchStatus,
1022
+ query.status,
1023
+ wsConnected
1024
+ ]);
737
1025
  return {
738
1026
  ...query,
739
1027
  fetchOlderMessages,