@meshagent/meshagent-tailwind 0.39.5 → 0.39.7

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.
@@ -360,6 +360,8 @@ function ChatBotView({
360
360
  centerComposer = false,
361
361
  emptyStateTitle = "No threads yet",
362
362
  emptyStateDescription = "Start a new conversation to see it here.",
363
+ startNewThreadTitle = "Start a new thread",
364
+ startNewThreadDescription = "Connect with this agent and your team.",
363
365
  selectedThreadPath,
364
366
  onSelectedThreadPathChanged,
365
367
  onSelectedThreadResolved,
@@ -508,8 +510,6 @@ function ChatBotView({
508
510
  onSelectedThreadResolved: emitResolvedThread,
509
511
  newThreadResetVersion,
510
512
  centerComposer,
511
- emptyStateTitle,
512
- emptyStateDescription,
513
513
  builder: (threadPath) => /* @__PURE__ */ jsx(
514
514
  Chat,
515
515
  {
@@ -520,8 +520,8 @@ function ChatBotView({
520
520
  toolkit,
521
521
  tool,
522
522
  centerComposer,
523
- emptyStateTitle,
524
- emptyStateDescription
523
+ emptyStateTitle: startNewThreadTitle,
524
+ emptyStateDescription: startNewThreadDescription
525
525
  }
526
526
  )
527
527
  }
@@ -4,11 +4,12 @@ import { ArrowUp, LoaderCircle, X } from "lucide-react";
4
4
  import { v4 as uuidV4 } from "uuid";
5
5
  import { ChatMessage } from "./chat-message";
6
6
  import { Button } from "./components/ui/button";
7
- import { Textarea } from "./components/ui/textarea";
8
7
  import { FileUploader } from "./FileUploader";
9
8
  import { UploadPill } from "./UploadPill";
10
9
  import { UploadStatus } from "./file-attachment";
11
10
  import { cn } from "./lib/utils";
11
+ const MIN_TEXTAREA_HEIGHT = 20;
12
+ const MAX_TEXTAREA_HEIGHT = 160;
12
13
  function useAttachmentStatusVersion(attachments) {
13
14
  const [version, setVersion] = useState(0);
14
15
  useEffect(() => {
@@ -33,10 +34,13 @@ function useAutoResizingTextarea(textareaRef, value) {
33
34
  return;
34
35
  }
35
36
  if (value === "") {
36
- element.style.height = "20px";
37
+ element.style.height = `${MIN_TEXTAREA_HEIGHT}px`;
37
38
  } else {
38
39
  element.style.height = "0px";
39
- element.style.height = `${Math.max(20, Math.min(element.scrollHeight, 160))}px`;
40
+ element.style.height = `${Math.max(
41
+ MIN_TEXTAREA_HEIGHT,
42
+ Math.min(element.scrollHeight, MAX_TEXTAREA_HEIGHT)
43
+ )}px`;
40
44
  }
41
45
  }, [textareaRef, value]);
42
46
  }
@@ -162,12 +166,17 @@ function ChatInput({
162
166
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
163
167
  /* @__PURE__ */ jsx(FileUploader, { onFilesSelected, disabled }),
164
168
  /* @__PURE__ */ jsx(
165
- Textarea,
169
+ "textarea",
166
170
  {
167
171
  ref: textareaRef,
168
172
  autoFocus,
169
173
  placeholder,
170
- className: "min-h-5 max-h-40 flex-1 resize-none border-0 bg-transparent p-0 leading-5 shadow-none focus-visible:border-transparent focus-visible:ring-0",
174
+ className: cn(
175
+ "min-h-5 max-h-40",
176
+ "flex-1 resize-none border-0 bg-transparent p-0 leading-5",
177
+ "shadow-none outline-none ring-0 focus:ring-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
178
+ "md:text-sm"
179
+ ),
171
180
  readOnly: disabled,
172
181
  value,
173
182
  onChange: handleChange,
@@ -225,7 +225,47 @@ function MarkdownBlock({ text }) {
225
225
  children
226
226
  }
227
227
  ),
228
- p: ({ children, ...props }) => /* @__PURE__ */ jsx("p", { ...props, className: "mb-2 last:mb-0", children })
228
+ p: ({ children, ...props }) => /* @__PURE__ */ jsx("p", { ...props, className: "mb-2 last:mb-0", children }),
229
+ table: ({ children, ...props }) => /* @__PURE__ */ jsx("div", { className: "my-4 w-full overflow-x-auto", children: /* @__PURE__ */ jsx(
230
+ "table",
231
+ {
232
+ ...props,
233
+ className: "w-full border-collapse border-spacing-0",
234
+ children
235
+ }
236
+ ) }),
237
+ th: ({ children, ...props }) => /* @__PURE__ */ jsx(
238
+ "th",
239
+ {
240
+ ...props,
241
+ className: "border bg-muted/50 px-3 py-2 text-left text-sm font-semibold",
242
+ children
243
+ }
244
+ ),
245
+ td: ({ children, ...props }) => /* @__PURE__ */ jsx(
246
+ "td",
247
+ {
248
+ ...props,
249
+ className: "border px-3 py-2 align-top text-sm bg-background",
250
+ children
251
+ }
252
+ ),
253
+ ul: ({ children, ...props }) => /* @__PURE__ */ jsx(
254
+ "ul",
255
+ {
256
+ ...props,
257
+ className: "mb-2 ml-6 list-disc last:mb-0",
258
+ children
259
+ }
260
+ ),
261
+ ol: ({ children, ...props }) => /* @__PURE__ */ jsx(
262
+ "ol",
263
+ {
264
+ ...props,
265
+ className: "mb-2 ml-6 list-decimal last:mb-0",
266
+ children
267
+ }
268
+ )
229
269
  },
230
270
  children: text
231
271
  }
@@ -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;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
- import { Button } from "@/components/ui/button";
4
+ import { Button } from "./ui/button";
5
5
  import {
6
6
  Sheet,
7
7
  SheetContent,
@@ -9,8 +9,8 @@ import {
9
9
  SheetTitle,
10
10
  SheetDescription,
11
11
  SheetFooter
12
- } from "@/components/ui/sheet";
13
- import { registerPane, closePane } from "@/lib/pane-service";
12
+ } from "./ui/sheet";
13
+ import { registerPane, closePane } from "../lib/pane-service";
14
14
  function PaneExample() {
15
15
  const [open, setOpen] = useState(false);
16
16
  useEffect(() => {
@@ -1,3 +1,3 @@
1
- import { ToasterProps } from "sonner";
1
+ import type { ToasterProps } from "sonner";
2
2
  declare const Toaster: ({ ...props }: ToasterProps) => import("react/jsx-runtime").JSX.Element;
3
3
  export { Toaster };
@@ -8,3 +8,4 @@ export * from './chat-message';
8
8
  export * from './conversation-descriptor';
9
9
  export * from './file-attachment';
10
10
  export * from './multi-thread-view';
11
+ export * from './chat-hooks';
package/dist/esm/index.js CHANGED
@@ -8,3 +8,4 @@ export * from "./chat-message";
8
8
  export * from "./conversation-descriptor";
9
9
  export * from "./file-attachment";
10
10
  export * from "./multi-thread-view";
11
+ export * from "./chat-hooks";
@@ -16,8 +16,8 @@ function MultiThreadView({
16
16
  onSelectedThreadResolved,
17
17
  newThreadResetVersion = 0,
18
18
  centerComposer = true,
19
- emptyStateTitle,
20
- emptyStateDescription
19
+ emptyStateTitle = "Start a new thread",
20
+ emptyStateDescription = "Connect with this agent and your team"
21
21
  }) {
22
22
  const controlledSelectedThreadPath = selectedThreadPath !== void 0 ? normalizeSelectedThreadPath(selectedThreadPath) : void 0;
23
23
  const [internalSelectedThreadPath, setInternalSelectedThreadPath] = useState(() => controlledSelectedThreadPath ?? null);
@@ -49,8 +49,6 @@ function MultiThreadView({
49
49
  toolkit,
50
50
  tool,
51
51
  centerComposer,
52
- emptyStateTitle,
53
- emptyStateDescription,
54
52
  onThreadResolved: (path, displayName) => {
55
53
  const normalizedPath = normalizeSelectedThreadPath(path);
56
54
  if (controlledSelectedThreadPath === void 0) {
@@ -58,7 +56,9 @@ function MultiThreadView({
58
56
  }
59
57
  onSelectedThreadPathChanged?.(normalizedPath);
60
58
  onSelectedThreadResolved?.(normalizedPath, displayName);
61
- }
59
+ },
60
+ emptyStateTitle,
61
+ emptyStateDescription
62
62
  },
63
63
  composerKey
64
64
  );