@meshagent/meshagent-tailwind 0.39.4 → 0.39.6

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
@@ -1,3 +1,12 @@
1
+ ## [0.39.6]
2
+ - Chat thread message sending now supports an “agent messages” mode: it selects participants that advertise agent-message support and sends `agent-message` payloads using `meshagent.agent.turn.start` / `meshagent.agent.turn.steer` types (including turn/thread scoping), with Promise-based sending and cancellation when recipients never materialize.
3
+ - Chat UI/logic now determines the correct outbound message type (chat vs steer) and turn context from thread status, and passes that into message sending.
4
+ - DatasetsClient now adds strongly typed `importFromStorage` and `exportToStorage` APIs with dataset storage format + import mode options, optional `namespace`/`branch` scoping, and `batch_size` support that is omitted when unset.
5
+ - Unit tests were updated to verify the new import/export request payload shapes and defaults.
6
+
7
+ ## [0.39.5]
8
+ - Stability
9
+
1
10
  ## [0.39.4]
2
11
  - Updated the Node/TS React dev package dependency graph to the newer `@meshagent/meshagent` and `@meshagent/meshagent-react` versions (`^0.38.4`) and upgraded supporting UI dependencies (including `shadcn` `^4.5.0`, `radix-ui` `^1.4.3`, and `lucide-react` `^0.525.0`).
3
12
  - Updated build tooling in the React dev package (new `build:js` / `build:types` scripts using TS configs) to match the updated packaging workflow.
package/dist/cjs/Chat.js CHANGED
@@ -152,6 +152,9 @@ function ResolvedChatView({
152
152
  showNewThreadButton = false,
153
153
  onStartNewThread
154
154
  }) {
155
+ const threadStatus = (0, import_chat_hooks.useThreadStatus)({ room, path, agentName });
156
+ const useAgentMessages = threadStatus.supportsAgentMessages;
157
+ const messageType = useAgentMessages && threadStatus.mode === "steerable" && threadStatus.turnId ? "steer" : "chat";
155
158
  const {
156
159
  document,
157
160
  messages,
@@ -162,9 +165,16 @@ function ResolvedChatView({
162
165
  onlineParticipants,
163
166
  localParticipantName,
164
167
  cancelRequest
165
- } = (0, import_chat_hooks.useChatThread)({ room, path, participants, agentName });
168
+ } = (0, import_chat_hooks.useChatThread)({
169
+ room,
170
+ path,
171
+ participants,
172
+ agentName,
173
+ useAgentMessages,
174
+ messageType,
175
+ turnId: threadStatus.turnId
176
+ });
166
177
  const { typing, thinking } = (0, import_meshagent_react.useRoomIndicators)({ room, path });
167
- const threadStatus = (0, import_chat_hooks.useThreadStatus)({ room, path, agentName });
168
178
  const [showCompletedToolCalls, setShowCompletedToolCalls] = (0, import_react.useState)(false);
169
179
  const onTextChange = (0, import_react.useCallback)(() => {
170
180
  for (const participant of onlineParticipants) {
@@ -9,11 +9,14 @@ export interface UseChatThreadProps {
9
9
  includeLocalParticipant?: boolean;
10
10
  initialMessage?: ChatMessage;
11
11
  agentName?: string;
12
+ useAgentMessages?: boolean;
13
+ messageType?: string;
14
+ turnId?: string;
12
15
  }
13
16
  export interface UseChatThreadResult {
14
17
  document: MeshDocument | null;
15
18
  messages: Element[];
16
- sendMessage: (message: ChatMessage) => void;
19
+ sendMessage: (message: ChatMessage) => Promise<void>;
17
20
  selectAttachments: (files: File[]) => void;
18
21
  attachments: FileUpload[];
19
22
  setAttachments: (attachments: FileUpload[]) => void;
@@ -70,5 +73,5 @@ export interface UseThreadStatusProps {
70
73
  previous?: ChatThreadStatusState;
71
74
  }
72
75
  export declare function formatThreadStatusText(text: string, startedAt?: Date | null): string;
73
- export declare function useChatThread({ room, path, participants, participantNames, initialMessage, includeLocalParticipant, agentName, }: UseChatThreadProps): UseChatThreadResult;
76
+ export declare function useChatThread({ room, path, participants, participantNames, initialMessage, includeLocalParticipant, agentName, useAgentMessages, messageType, turnId, }: UseChatThreadProps): UseChatThreadResult;
74
77
  export declare function useThreadStatus({ room, path, agentName }: UseThreadStatusProps): ThreadStatus;
@@ -29,7 +29,15 @@ var import_react = require("react");
29
29
  var import_meshagent = require("@meshagent/meshagent");
30
30
  var import_meshagent_react = require("@meshagent/meshagent-react");
31
31
  var import_file_attachment = require("./file-attachment");
32
+ const agentRoomMessageType = "agent-message";
33
+ const agentTurnStartType = "meshagent.agent.turn.start";
32
34
  const agentTurnSteerType = "meshagent.agent.turn.steer";
35
+ class ChatSendCancelledError extends Error {
36
+ constructor() {
37
+ super("chat send cancelled");
38
+ this.name = "ChatSendCancelledError";
39
+ }
40
+ }
33
41
  class PendingAgentMessage {
34
42
  messageId;
35
43
  messageType;
@@ -202,6 +210,175 @@ function getOnlineParticipants(roomParticipants, participantNames) {
202
210
  function supportsAgentMessages(participant) {
203
211
  return participant.getAttribute("supports_agent_messages") === true;
204
212
  }
213
+ function uniqueRemoteParticipantsById(participants) {
214
+ const seenParticipantIds = /* @__PURE__ */ new Set();
215
+ const uniqueParticipants = [];
216
+ for (const participant of participants) {
217
+ if (seenParticipantIds.has(participant.id)) {
218
+ continue;
219
+ }
220
+ seenParticipantIds.add(participant.id);
221
+ uniqueParticipants.push(participant);
222
+ }
223
+ return uniqueParticipants;
224
+ }
225
+ function getAgentParticipants({
226
+ room,
227
+ document,
228
+ participantName
229
+ }) {
230
+ const normalizedParticipantName = participantName?.trim();
231
+ return uniqueRemoteParticipantsById(
232
+ getOnlineParticipants(room.messaging.remoteParticipants, getDocumentParticipantNames(document)).filter((participant) => {
233
+ if (normalizedParticipantName && getParticipantName(participant) !== normalizedParticipantName) {
234
+ return false;
235
+ }
236
+ return supportsAgentMessages(participant);
237
+ })
238
+ );
239
+ }
240
+ function matchingRecipients({
241
+ room,
242
+ document,
243
+ useAgentMessages,
244
+ participantName
245
+ }) {
246
+ const normalizedParticipantName = participantName?.trim();
247
+ if (useAgentMessages) {
248
+ return getAgentParticipants({ room, document, participantName: normalizedParticipantName });
249
+ }
250
+ return uniqueRemoteParticipantsById(
251
+ getOnlineParticipants(room.messaging.remoteParticipants, getDocumentParticipantNames(document)).filter((participant) => {
252
+ if (!normalizedParticipantName) {
253
+ return true;
254
+ }
255
+ return getParticipantName(participant) === normalizedParticipantName;
256
+ })
257
+ );
258
+ }
259
+ function normalizeAgentAttachmentUrl(path) {
260
+ const trimmedPath = path.trim();
261
+ if (trimmedPath === "") {
262
+ return null;
263
+ }
264
+ try {
265
+ const url = new URL(trimmedPath);
266
+ if (url.protocol !== "") {
267
+ return trimmedPath;
268
+ }
269
+ } catch {
270
+ }
271
+ const roomPath = trimmedPath.startsWith("/") ? trimmedPath.slice(1) : trimmedPath;
272
+ return roomPath === "" ? null : `room:///${roomPath}`;
273
+ }
274
+ function agentInputContentFromMessage(message) {
275
+ const content = [];
276
+ if (message.text.trim() !== "") {
277
+ content.push({ type: "text", text: message.text });
278
+ }
279
+ for (const attachmentPath of message.attachments) {
280
+ const normalizedUrl = normalizeAgentAttachmentUrl(attachmentPath);
281
+ if (normalizedUrl !== null) {
282
+ content.push({ type: "file", url: normalizedUrl });
283
+ }
284
+ }
285
+ return content;
286
+ }
287
+ async function sendMessageToParticipant({
288
+ room,
289
+ participant,
290
+ path,
291
+ message,
292
+ messageType = "chat",
293
+ useAgentMessages = false,
294
+ turnId,
295
+ store = false
296
+ }) {
297
+ if (message.text.trim() === "" && message.attachments.length === 0) {
298
+ return;
299
+ }
300
+ if (useAgentMessages) {
301
+ const isSteer = messageType === "steer";
302
+ const payload = {
303
+ type: isSteer ? agentTurnSteerType : agentTurnStartType,
304
+ thread_id: path,
305
+ message_id: message.id,
306
+ content: agentInputContentFromMessage(message)
307
+ };
308
+ if (isSteer && turnId?.trim()) {
309
+ payload.turn_id = turnId.trim();
310
+ }
311
+ await room.messaging.sendMessage({
312
+ to: participant,
313
+ type: agentRoomMessageType,
314
+ message: { payload }
315
+ });
316
+ return;
317
+ }
318
+ await room.messaging.sendMessage({
319
+ to: participant,
320
+ type: messageType,
321
+ message: {
322
+ path,
323
+ text: message.text,
324
+ attachments: message.attachments.map((attachmentPath) => ({ path: attachmentPath })),
325
+ store
326
+ }
327
+ });
328
+ }
329
+ function waitForRecipients({
330
+ room,
331
+ document,
332
+ messageId,
333
+ useAgentMessages,
334
+ participantName,
335
+ pendingRecipientWaits
336
+ }) {
337
+ pendingRecipientWaits.get(messageId)?.();
338
+ return new Promise((resolve, reject) => {
339
+ let finished = false;
340
+ const eventNames = [
341
+ "participant_added",
342
+ "participant_removed",
343
+ "participant_attributes_updated",
344
+ "messaging_enabled"
345
+ ];
346
+ const finish = () => {
347
+ if (finished) {
348
+ return;
349
+ }
350
+ finished = true;
351
+ for (const eventName of eventNames) {
352
+ room.messaging.off(eventName, listener);
353
+ }
354
+ if (pendingRecipientWaits.get(messageId) === cancel) {
355
+ pendingRecipientWaits.delete(messageId);
356
+ }
357
+ };
358
+ const cancel = () => {
359
+ finish();
360
+ reject(new ChatSendCancelledError());
361
+ };
362
+ const listener = () => {
363
+ const recipients = matchingRecipients({
364
+ room,
365
+ document,
366
+ useAgentMessages,
367
+ participantName
368
+ });
369
+ if (recipients.length === 0) {
370
+ return;
371
+ }
372
+ finish();
373
+ resolve(recipients);
374
+ };
375
+ pendingRecipientWaits.set(messageId, cancel);
376
+ for (const eventName of eventNames) {
377
+ room.messaging.on(eventName, listener);
378
+ }
379
+ listener();
380
+ });
381
+ }
205
382
  function threadStatusAttributeCandidates(path, prefix) {
206
383
  if (path.startsWith("/")) {
207
384
  return [`${prefix}.${path}`, `${prefix}.${path.slice(1)}`];
@@ -387,13 +564,17 @@ function useChatThread({
387
564
  participantNames,
388
565
  initialMessage,
389
566
  includeLocalParticipant,
390
- agentName
567
+ agentName,
568
+ useAgentMessages = false,
569
+ messageType = "chat",
570
+ turnId
391
571
  }) {
392
572
  const [document, setDocument] = (0, import_react.useState)(null);
393
573
  const [messages, setMessages] = (0, import_react.useState)(() => document ? mapThreadElements(document) : []);
394
574
  const [attachments, setAttachments] = (0, import_react.useState)([]);
395
575
  const [documentParticipantNames, setDocumentParticipantNames] = (0, import_react.useState)(() => document ? getDocumentParticipantNames(document) : []);
396
576
  const initialMessageSentRef = (0, import_react.useRef)(false);
577
+ const pendingRecipientWaitsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
397
578
  const syncDocumentState = (0, import_react.useCallback)((nextDocument) => {
398
579
  const nextMessages = mapThreadElements(nextDocument);
399
580
  const nextParticipantNames = getDocumentParticipantNames(nextDocument);
@@ -442,6 +623,14 @@ function useChatThread({
442
623
  }
443
624
  };
444
625
  }, [path, room, syncDocumentState]);
626
+ (0, import_react.useEffect)(() => {
627
+ return () => {
628
+ for (const cancel of pendingRecipientWaitsRef.current.values()) {
629
+ cancel();
630
+ }
631
+ pendingRecipientWaitsRef.current.clear();
632
+ };
633
+ }, []);
445
634
  (0, import_react.useEffect)(() => {
446
635
  if (!document || !room.localParticipant) {
447
636
  return;
@@ -476,38 +665,68 @@ function useChatThread({
476
665
  () => getOnlineParticipants(roomParticipants, documentParticipantNames),
477
666
  [roomParticipants, documentParticipantNames]
478
667
  );
479
- const sendMessage = (0, import_react.useCallback)((message) => {
668
+ const sendMessage = (0, import_react.useCallback)(async (message) => {
669
+ if (message.text.trim() === "" && message.attachments.length === 0) {
670
+ return;
671
+ }
672
+ const normalizedParticipantName = agentName?.trim();
673
+ const storeLocally = !normalizedParticipantName && !useAgentMessages;
480
674
  const children = document?.root.getChildren() ?? [];
481
- const thread = children.find((child) => child.tagName === "messages");
482
- if (!thread) {
675
+ const messagesElement = children.find((child) => child.tagName === "messages");
676
+ if (!document || !messagesElement) {
483
677
  return;
484
678
  }
485
- const authorName = getParticipantName(room.localParticipant);
486
- const messageElement = thread.createChildElement("message", {
487
- id: message.id,
488
- text: message.text,
489
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
490
- author_name: authorName,
491
- author_ref: null
492
- });
493
- for (const attachmentPath of message.attachments) {
494
- messageElement.createChildElement("file", { path: attachmentPath });
679
+ if (storeLocally) {
680
+ const authorName = getParticipantName(room.localParticipant);
681
+ const messageElement = messagesElement.createChildElement("message", {
682
+ id: message.id,
683
+ text: message.text,
684
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
685
+ author_name: authorName,
686
+ author_ref: null
687
+ });
688
+ for (const attachmentPath of message.attachments) {
689
+ messageElement.createChildElement("file", { path: attachmentPath });
690
+ }
495
691
  }
496
- for (const participant of onlineParticipants) {
497
- if (!matchesParticipantName(participant, agentName)) {
498
- continue;
692
+ let recipients = matchingRecipients({
693
+ room,
694
+ document,
695
+ useAgentMessages,
696
+ participantName: normalizedParticipantName
697
+ });
698
+ if (recipients.length === 0) {
699
+ const shouldWaitForRecipient = useAgentMessages || Boolean(normalizedParticipantName);
700
+ if (!shouldWaitForRecipient) {
701
+ throw new Error(`no matching recipients are available for '${path}'`);
499
702
  }
500
- room.messaging.sendMessage({
501
- to: participant,
502
- type: "chat",
503
- message: {
504
- path,
505
- text: message.text,
506
- attachments: message.attachments.map((attachmentPath) => ({ path: attachmentPath }))
703
+ try {
704
+ recipients = await waitForRecipients({
705
+ room,
706
+ document,
707
+ messageId: message.id,
708
+ useAgentMessages,
709
+ participantName: normalizedParticipantName,
710
+ pendingRecipientWaits: pendingRecipientWaitsRef.current
711
+ });
712
+ } catch (error) {
713
+ if (error instanceof ChatSendCancelledError) {
714
+ return;
507
715
  }
508
- });
716
+ throw error;
717
+ }
509
718
  }
510
- }, [agentName, document, onlineParticipants, path, room]);
719
+ await Promise.all(recipients.map((participant) => sendMessageToParticipant({
720
+ room,
721
+ participant,
722
+ path,
723
+ message,
724
+ messageType,
725
+ useAgentMessages,
726
+ turnId,
727
+ store: Boolean(normalizedParticipantName) && getParticipantName(participant) === normalizedParticipantName
728
+ })));
729
+ }, [agentName, document, messageType, path, room, turnId, useAgentMessages]);
511
730
  (0, import_react.useEffect)(() => {
512
731
  if (!document || !initialMessage || initialMessageSentRef.current) {
513
732
  return;
package/dist/esm/Chat.js CHANGED
@@ -129,6 +129,9 @@ function ResolvedChatView({
129
129
  showNewThreadButton = false,
130
130
  onStartNewThread
131
131
  }) {
132
+ const threadStatus = useThreadStatus({ room, path, agentName });
133
+ const useAgentMessages = threadStatus.supportsAgentMessages;
134
+ const messageType = useAgentMessages && threadStatus.mode === "steerable" && threadStatus.turnId ? "steer" : "chat";
132
135
  const {
133
136
  document,
134
137
  messages,
@@ -139,9 +142,16 @@ function ResolvedChatView({
139
142
  onlineParticipants,
140
143
  localParticipantName,
141
144
  cancelRequest
142
- } = useChatThread({ room, path, participants, agentName });
145
+ } = useChatThread({
146
+ room,
147
+ path,
148
+ participants,
149
+ agentName,
150
+ useAgentMessages,
151
+ messageType,
152
+ turnId: threadStatus.turnId
153
+ });
143
154
  const { typing, thinking } = useRoomIndicators({ room, path });
144
- const threadStatus = useThreadStatus({ room, path, agentName });
145
155
  const [showCompletedToolCalls, setShowCompletedToolCalls] = useState(false);
146
156
  const onTextChange = useCallback(() => {
147
157
  for (const participant of onlineParticipants) {
@@ -9,11 +9,14 @@ export interface UseChatThreadProps {
9
9
  includeLocalParticipant?: boolean;
10
10
  initialMessage?: ChatMessage;
11
11
  agentName?: string;
12
+ useAgentMessages?: boolean;
13
+ messageType?: string;
14
+ turnId?: string;
12
15
  }
13
16
  export interface UseChatThreadResult {
14
17
  document: MeshDocument | null;
15
18
  messages: Element[];
16
- sendMessage: (message: ChatMessage) => void;
19
+ sendMessage: (message: ChatMessage) => Promise<void>;
17
20
  selectAttachments: (files: File[]) => void;
18
21
  attachments: FileUpload[];
19
22
  setAttachments: (attachments: FileUpload[]) => void;
@@ -70,5 +73,5 @@ export interface UseThreadStatusProps {
70
73
  previous?: ChatThreadStatusState;
71
74
  }
72
75
  export declare function formatThreadStatusText(text: string, startedAt?: Date | null): string;
73
- export declare function useChatThread({ room, path, participants, participantNames, initialMessage, includeLocalParticipant, agentName, }: UseChatThreadProps): UseChatThreadResult;
76
+ export declare function useChatThread({ room, path, participants, participantNames, initialMessage, includeLocalParticipant, agentName, useAgentMessages, messageType, turnId, }: UseChatThreadProps): UseChatThreadResult;
74
77
  export declare function useThreadStatus({ room, path, agentName }: UseThreadStatusProps): ThreadStatus;
@@ -4,7 +4,15 @@ import {
4
4
  } from "@meshagent/meshagent";
5
5
  import { subscribe, useDocumentChanged, useRoomParticipants } from "@meshagent/meshagent-react";
6
6
  import { MeshagentFileUpload, fileToAsyncIterable } from "./file-attachment";
7
+ const agentRoomMessageType = "agent-message";
8
+ const agentTurnStartType = "meshagent.agent.turn.start";
7
9
  const agentTurnSteerType = "meshagent.agent.turn.steer";
10
+ class ChatSendCancelledError extends Error {
11
+ constructor() {
12
+ super("chat send cancelled");
13
+ this.name = "ChatSendCancelledError";
14
+ }
15
+ }
8
16
  class PendingAgentMessage {
9
17
  messageId;
10
18
  messageType;
@@ -177,6 +185,175 @@ function getOnlineParticipants(roomParticipants, participantNames) {
177
185
  function supportsAgentMessages(participant) {
178
186
  return participant.getAttribute("supports_agent_messages") === true;
179
187
  }
188
+ function uniqueRemoteParticipantsById(participants) {
189
+ const seenParticipantIds = /* @__PURE__ */ new Set();
190
+ const uniqueParticipants = [];
191
+ for (const participant of participants) {
192
+ if (seenParticipantIds.has(participant.id)) {
193
+ continue;
194
+ }
195
+ seenParticipantIds.add(participant.id);
196
+ uniqueParticipants.push(participant);
197
+ }
198
+ return uniqueParticipants;
199
+ }
200
+ function getAgentParticipants({
201
+ room,
202
+ document,
203
+ participantName
204
+ }) {
205
+ const normalizedParticipantName = participantName?.trim();
206
+ return uniqueRemoteParticipantsById(
207
+ getOnlineParticipants(room.messaging.remoteParticipants, getDocumentParticipantNames(document)).filter((participant) => {
208
+ if (normalizedParticipantName && getParticipantName(participant) !== normalizedParticipantName) {
209
+ return false;
210
+ }
211
+ return supportsAgentMessages(participant);
212
+ })
213
+ );
214
+ }
215
+ function matchingRecipients({
216
+ room,
217
+ document,
218
+ useAgentMessages,
219
+ participantName
220
+ }) {
221
+ const normalizedParticipantName = participantName?.trim();
222
+ if (useAgentMessages) {
223
+ return getAgentParticipants({ room, document, participantName: normalizedParticipantName });
224
+ }
225
+ return uniqueRemoteParticipantsById(
226
+ getOnlineParticipants(room.messaging.remoteParticipants, getDocumentParticipantNames(document)).filter((participant) => {
227
+ if (!normalizedParticipantName) {
228
+ return true;
229
+ }
230
+ return getParticipantName(participant) === normalizedParticipantName;
231
+ })
232
+ );
233
+ }
234
+ function normalizeAgentAttachmentUrl(path) {
235
+ const trimmedPath = path.trim();
236
+ if (trimmedPath === "") {
237
+ return null;
238
+ }
239
+ try {
240
+ const url = new URL(trimmedPath);
241
+ if (url.protocol !== "") {
242
+ return trimmedPath;
243
+ }
244
+ } catch {
245
+ }
246
+ const roomPath = trimmedPath.startsWith("/") ? trimmedPath.slice(1) : trimmedPath;
247
+ return roomPath === "" ? null : `room:///${roomPath}`;
248
+ }
249
+ function agentInputContentFromMessage(message) {
250
+ const content = [];
251
+ if (message.text.trim() !== "") {
252
+ content.push({ type: "text", text: message.text });
253
+ }
254
+ for (const attachmentPath of message.attachments) {
255
+ const normalizedUrl = normalizeAgentAttachmentUrl(attachmentPath);
256
+ if (normalizedUrl !== null) {
257
+ content.push({ type: "file", url: normalizedUrl });
258
+ }
259
+ }
260
+ return content;
261
+ }
262
+ async function sendMessageToParticipant({
263
+ room,
264
+ participant,
265
+ path,
266
+ message,
267
+ messageType = "chat",
268
+ useAgentMessages = false,
269
+ turnId,
270
+ store = false
271
+ }) {
272
+ if (message.text.trim() === "" && message.attachments.length === 0) {
273
+ return;
274
+ }
275
+ if (useAgentMessages) {
276
+ const isSteer = messageType === "steer";
277
+ const payload = {
278
+ type: isSteer ? agentTurnSteerType : agentTurnStartType,
279
+ thread_id: path,
280
+ message_id: message.id,
281
+ content: agentInputContentFromMessage(message)
282
+ };
283
+ if (isSteer && turnId?.trim()) {
284
+ payload.turn_id = turnId.trim();
285
+ }
286
+ await room.messaging.sendMessage({
287
+ to: participant,
288
+ type: agentRoomMessageType,
289
+ message: { payload }
290
+ });
291
+ return;
292
+ }
293
+ await room.messaging.sendMessage({
294
+ to: participant,
295
+ type: messageType,
296
+ message: {
297
+ path,
298
+ text: message.text,
299
+ attachments: message.attachments.map((attachmentPath) => ({ path: attachmentPath })),
300
+ store
301
+ }
302
+ });
303
+ }
304
+ function waitForRecipients({
305
+ room,
306
+ document,
307
+ messageId,
308
+ useAgentMessages,
309
+ participantName,
310
+ pendingRecipientWaits
311
+ }) {
312
+ pendingRecipientWaits.get(messageId)?.();
313
+ return new Promise((resolve, reject) => {
314
+ let finished = false;
315
+ const eventNames = [
316
+ "participant_added",
317
+ "participant_removed",
318
+ "participant_attributes_updated",
319
+ "messaging_enabled"
320
+ ];
321
+ const finish = () => {
322
+ if (finished) {
323
+ return;
324
+ }
325
+ finished = true;
326
+ for (const eventName of eventNames) {
327
+ room.messaging.off(eventName, listener);
328
+ }
329
+ if (pendingRecipientWaits.get(messageId) === cancel) {
330
+ pendingRecipientWaits.delete(messageId);
331
+ }
332
+ };
333
+ const cancel = () => {
334
+ finish();
335
+ reject(new ChatSendCancelledError());
336
+ };
337
+ const listener = () => {
338
+ const recipients = matchingRecipients({
339
+ room,
340
+ document,
341
+ useAgentMessages,
342
+ participantName
343
+ });
344
+ if (recipients.length === 0) {
345
+ return;
346
+ }
347
+ finish();
348
+ resolve(recipients);
349
+ };
350
+ pendingRecipientWaits.set(messageId, cancel);
351
+ for (const eventName of eventNames) {
352
+ room.messaging.on(eventName, listener);
353
+ }
354
+ listener();
355
+ });
356
+ }
180
357
  function threadStatusAttributeCandidates(path, prefix) {
181
358
  if (path.startsWith("/")) {
182
359
  return [`${prefix}.${path}`, `${prefix}.${path.slice(1)}`];
@@ -362,13 +539,17 @@ function useChatThread({
362
539
  participantNames,
363
540
  initialMessage,
364
541
  includeLocalParticipant,
365
- agentName
542
+ agentName,
543
+ useAgentMessages = false,
544
+ messageType = "chat",
545
+ turnId
366
546
  }) {
367
547
  const [document, setDocument] = useState(null);
368
548
  const [messages, setMessages] = useState(() => document ? mapThreadElements(document) : []);
369
549
  const [attachments, setAttachments] = useState([]);
370
550
  const [documentParticipantNames, setDocumentParticipantNames] = useState(() => document ? getDocumentParticipantNames(document) : []);
371
551
  const initialMessageSentRef = useRef(false);
552
+ const pendingRecipientWaitsRef = useRef(/* @__PURE__ */ new Map());
372
553
  const syncDocumentState = useCallback((nextDocument) => {
373
554
  const nextMessages = mapThreadElements(nextDocument);
374
555
  const nextParticipantNames = getDocumentParticipantNames(nextDocument);
@@ -417,6 +598,14 @@ function useChatThread({
417
598
  }
418
599
  };
419
600
  }, [path, room, syncDocumentState]);
601
+ useEffect(() => {
602
+ return () => {
603
+ for (const cancel of pendingRecipientWaitsRef.current.values()) {
604
+ cancel();
605
+ }
606
+ pendingRecipientWaitsRef.current.clear();
607
+ };
608
+ }, []);
420
609
  useEffect(() => {
421
610
  if (!document || !room.localParticipant) {
422
611
  return;
@@ -451,38 +640,68 @@ function useChatThread({
451
640
  () => getOnlineParticipants(roomParticipants, documentParticipantNames),
452
641
  [roomParticipants, documentParticipantNames]
453
642
  );
454
- const sendMessage = useCallback((message) => {
643
+ const sendMessage = useCallback(async (message) => {
644
+ if (message.text.trim() === "" && message.attachments.length === 0) {
645
+ return;
646
+ }
647
+ const normalizedParticipantName = agentName?.trim();
648
+ const storeLocally = !normalizedParticipantName && !useAgentMessages;
455
649
  const children = document?.root.getChildren() ?? [];
456
- const thread = children.find((child) => child.tagName === "messages");
457
- if (!thread) {
650
+ const messagesElement = children.find((child) => child.tagName === "messages");
651
+ if (!document || !messagesElement) {
458
652
  return;
459
653
  }
460
- const authorName = getParticipantName(room.localParticipant);
461
- const messageElement = thread.createChildElement("message", {
462
- id: message.id,
463
- text: message.text,
464
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
465
- author_name: authorName,
466
- author_ref: null
467
- });
468
- for (const attachmentPath of message.attachments) {
469
- messageElement.createChildElement("file", { path: attachmentPath });
654
+ if (storeLocally) {
655
+ const authorName = getParticipantName(room.localParticipant);
656
+ const messageElement = messagesElement.createChildElement("message", {
657
+ id: message.id,
658
+ text: message.text,
659
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
660
+ author_name: authorName,
661
+ author_ref: null
662
+ });
663
+ for (const attachmentPath of message.attachments) {
664
+ messageElement.createChildElement("file", { path: attachmentPath });
665
+ }
470
666
  }
471
- for (const participant of onlineParticipants) {
472
- if (!matchesParticipantName(participant, agentName)) {
473
- continue;
667
+ let recipients = matchingRecipients({
668
+ room,
669
+ document,
670
+ useAgentMessages,
671
+ participantName: normalizedParticipantName
672
+ });
673
+ if (recipients.length === 0) {
674
+ const shouldWaitForRecipient = useAgentMessages || Boolean(normalizedParticipantName);
675
+ if (!shouldWaitForRecipient) {
676
+ throw new Error(`no matching recipients are available for '${path}'`);
474
677
  }
475
- room.messaging.sendMessage({
476
- to: participant,
477
- type: "chat",
478
- message: {
479
- path,
480
- text: message.text,
481
- attachments: message.attachments.map((attachmentPath) => ({ path: attachmentPath }))
678
+ try {
679
+ recipients = await waitForRecipients({
680
+ room,
681
+ document,
682
+ messageId: message.id,
683
+ useAgentMessages,
684
+ participantName: normalizedParticipantName,
685
+ pendingRecipientWaits: pendingRecipientWaitsRef.current
686
+ });
687
+ } catch (error) {
688
+ if (error instanceof ChatSendCancelledError) {
689
+ return;
482
690
  }
483
- });
691
+ throw error;
692
+ }
484
693
  }
485
- }, [agentName, document, onlineParticipants, path, room]);
694
+ await Promise.all(recipients.map((participant) => sendMessageToParticipant({
695
+ room,
696
+ participant,
697
+ path,
698
+ message,
699
+ messageType,
700
+ useAgentMessages,
701
+ turnId,
702
+ store: Boolean(normalizedParticipantName) && getParticipantName(participant) === normalizedParticipantName
703
+ })));
704
+ }, [agentName, document, messageType, path, room, turnId, useAgentMessages]);
486
705
  useEffect(() => {
487
706
  if (!document || !initialMessage || initialMessageSentRef.current) {
488
707
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshagent/meshagent-tailwind",
3
- "version": "0.39.4",
3
+ "version": "0.39.6",
4
4
  "description": "Meshagent Tailwind Components",
5
5
  "homepage": "https://github.com/meshagent/meshagent-tailwind",
6
6
  "scripts": {
@@ -32,8 +32,8 @@
32
32
  "CHANGELOG.md"
33
33
  ],
34
34
  "dependencies": {
35
- "@meshagent/meshagent": "^0.39.4",
36
- "@meshagent/meshagent-react": "^0.39.4",
35
+ "@meshagent/meshagent": "^0.39.6",
36
+ "@meshagent/meshagent-react": "^0.39.6",
37
37
  "@radix-ui/react-avatar": "^1.1.10",
38
38
  "@radix-ui/react-checkbox": "^1.3.2",
39
39
  "@radix-ui/react-dialog": "^1.1.14",