@meshagent/meshagent-tailwind 0.38.1 → 0.38.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/Chat.d.ts +11 -3
  3. package/dist/cjs/Chat.js +376 -29
  4. package/dist/cjs/ChatBotView.d.ts +29 -0
  5. package/dist/cjs/ChatBotView.js +491 -0
  6. package/dist/cjs/ChatInput.d.ts +12 -3
  7. package/dist/cjs/ChatInput.js +143 -44
  8. package/dist/cjs/ChatThread.d.ts +17 -3
  9. package/dist/cjs/ChatThread.js +646 -90
  10. package/dist/cjs/ChatTypingIndicator.d.ts +12 -5
  11. package/dist/cjs/ChatTypingIndicator.js +104 -13
  12. package/dist/cjs/FileUploader.d.ts +3 -2
  13. package/dist/cjs/FileUploader.js +35 -11
  14. package/dist/cjs/UploadPill.d.ts +2 -2
  15. package/dist/cjs/UploadPill.js +70 -32
  16. package/dist/cjs/chat-hooks.d.ts +38 -0
  17. package/dist/cjs/chat-hooks.js +390 -0
  18. package/dist/cjs/chat-message.d.ts +11 -0
  19. package/dist/cjs/chat-message.js +33 -0
  20. package/dist/cjs/components/ui/button.d.ts +1 -1
  21. package/dist/cjs/conversation-descriptor.d.ts +59 -0
  22. package/dist/cjs/conversation-descriptor.js +300 -0
  23. package/dist/cjs/file-attachment.d.ts +45 -0
  24. package/dist/cjs/file-attachment.js +171 -0
  25. package/dist/cjs/index.d.ts +5 -0
  26. package/dist/cjs/index.js +5 -0
  27. package/dist/cjs/multi-thread-view.d.ts +18 -0
  28. package/dist/cjs/multi-thread-view.js +88 -0
  29. package/dist/cjs/tools/ui-toolkit.d.ts +1 -1
  30. package/dist/cjs/tools/ui-toolkit.js +2 -1
  31. package/dist/esm/Chat.d.ts +11 -3
  32. package/dist/esm/Chat.js +378 -31
  33. package/dist/esm/ChatBotView.d.ts +29 -0
  34. package/dist/esm/ChatBotView.js +486 -0
  35. package/dist/esm/ChatInput.d.ts +12 -3
  36. package/dist/esm/ChatInput.js +143 -34
  37. package/dist/esm/ChatThread.d.ts +17 -3
  38. package/dist/esm/ChatThread.js +648 -92
  39. package/dist/esm/ChatTypingIndicator.d.ts +12 -5
  40. package/dist/esm/ChatTypingIndicator.js +94 -13
  41. package/dist/esm/FileUploader.d.ts +3 -2
  42. package/dist/esm/FileUploader.js +26 -12
  43. package/dist/esm/UploadPill.d.ts +2 -2
  44. package/dist/esm/UploadPill.js +60 -32
  45. package/dist/esm/chat-hooks.d.ts +38 -0
  46. package/dist/esm/chat-hooks.js +372 -0
  47. package/dist/esm/chat-message.d.ts +11 -0
  48. package/dist/esm/chat-message.js +13 -0
  49. package/dist/esm/components/ui/button.d.ts +1 -1
  50. package/dist/esm/conversation-descriptor.d.ts +59 -0
  51. package/dist/esm/conversation-descriptor.js +280 -0
  52. package/dist/esm/file-attachment.d.ts +45 -0
  53. package/dist/esm/file-attachment.js +151 -0
  54. package/dist/esm/index.d.ts +5 -0
  55. package/dist/esm/index.js +5 -0
  56. package/dist/esm/multi-thread-view.d.ts +18 -0
  57. package/dist/esm/multi-thread-view.js +68 -0
  58. package/dist/esm/tools/ui-toolkit.d.ts +1 -1
  59. package/dist/esm/tools/ui-toolkit.js +2 -1
  60. package/dist/index.css +1 -1
  61. package/package.json +3 -3
@@ -1,11 +1,38 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import React, { useEffect } from "react";
3
- import { Download } from "lucide-react";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState, useEffect, useRef, useCallback } from "react";
3
+ import { Download, FileText } from "lucide-react";
4
4
  import ReactMarkdown from "react-markdown";
5
- import remarkGfm from "remark-gfm";
6
- import rehypeSanitize from "rehype-sanitize";
7
5
  import rehypeHighlight from "rehype-highlight";
6
+ import rehypeSanitize from "rehype-sanitize";
7
+ import remarkGfm from "remark-gfm";
8
+ import { Button } from "./components/ui/button";
9
+ import { Spinner } from "./components/ui/spinner";
10
+ import { ChatTypingIndicator } from "./ChatTypingIndicator";
8
11
  import { cn } from "./lib/utils";
12
+ const supportedEventKinds = /* @__PURE__ */ new Set([
13
+ "exec",
14
+ "tool",
15
+ "web",
16
+ "search",
17
+ "diff",
18
+ "image",
19
+ "approval",
20
+ "collab",
21
+ "plan",
22
+ "thread",
23
+ "file"
24
+ ]);
25
+ const stickyBottomThresholdPx = 24;
26
+ function getStringAttribute(element, name) {
27
+ const value = element.getAttribute(name);
28
+ return typeof value === "string" ? value : null;
29
+ }
30
+ function getTrimmedStringAttribute(element, name) {
31
+ return getStringAttribute(element, name)?.trim() ?? "";
32
+ }
33
+ function getElementChildren(element) {
34
+ return element.getChildren() ?? [];
35
+ }
9
36
  function formatDateTime(iso) {
10
37
  const date = new Date(iso);
11
38
  return new Intl.DateTimeFormat(void 0, {
@@ -18,118 +45,647 @@ function formatDateTime(iso) {
18
45
  timeZoneName: "short"
19
46
  }).format(date);
20
47
  }
21
- function isImageFilename(filename) {
22
- return /\.(jpe?g|png|gif|bmp|webp|avif|svg)$/i.test(filename);
23
- }
24
48
  function timeAgo(iso) {
25
- const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
26
49
  const date = new Date(iso);
50
+ if (Number.isNaN(date.getTime())) {
51
+ return "";
52
+ }
53
+ const relativeTime = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
27
54
  const now = /* @__PURE__ */ new Date();
28
- if (isNaN(date.getTime())) return "";
29
55
  const seconds = Math.round((date.getTime() - now.getTime()) / 1e3);
30
56
  const minutes = Math.round(seconds / 60);
31
57
  const hours = Math.round(minutes / 60);
32
58
  const days = Math.round(hours / 24);
33
59
  const months = Math.round(days / 30);
34
- if (Math.abs(months) >= 1) return formatDateTime(iso);
35
- if (Math.abs(days) >= 1) return rtf.format(days, "day");
36
- if (Math.abs(hours) >= 1) return rtf.format(hours, "hour");
37
- if (Math.abs(minutes) >= 1) return rtf.format(minutes, "minute");
38
- return rtf.format(seconds, "second");
39
- }
40
- function ChatThread({ room, messages, localParticipantName }) {
41
- const bottomRef = React.useRef(null);
60
+ if (Math.abs(months) >= 1) {
61
+ return formatDateTime(iso);
62
+ }
63
+ if (Math.abs(days) >= 1) {
64
+ return relativeTime.format(days, "day");
65
+ }
66
+ if (Math.abs(hours) >= 1) {
67
+ return relativeTime.format(hours, "hour");
68
+ }
69
+ if (Math.abs(minutes) >= 1) {
70
+ return relativeTime.format(minutes, "minute");
71
+ }
72
+ return relativeTime.format(seconds, "second");
73
+ }
74
+ function displayParticipantName(name) {
75
+ return name.split("@")[0]?.trim() ?? name.trim();
76
+ }
77
+ function isImagePath(path) {
78
+ return /\.(avif|bmp|gif|jpe?g|png|svg|webp)$/i.test(path);
79
+ }
80
+ function isThreadAttachmentElement(element) {
81
+ return element.tagName === "file" || element.tagName === "image";
82
+ }
83
+ function isImageAttachmentElement(element) {
84
+ if (!isThreadAttachmentElement(element)) {
85
+ return false;
86
+ }
87
+ const path = getTrimmedStringAttribute(element, "path");
88
+ return element.tagName === "image" || path !== "" && isImagePath(path);
89
+ }
90
+ function getAttachmentTrackingId(attachment) {
91
+ const path = getTrimmedStringAttribute(attachment, "path");
92
+ return attachment.id || `${attachment.tagName}:${path}`;
93
+ }
94
+ function collectImageAttachmentIds(messages) {
95
+ const ids = [];
96
+ for (const message of messages) {
97
+ if (message.tagName !== "message") {
98
+ continue;
99
+ }
100
+ for (const attachment of getElementChildren(message)) {
101
+ if (!isImageAttachmentElement(attachment)) {
102
+ continue;
103
+ }
104
+ ids.push(getAttachmentTrackingId(attachment));
105
+ }
106
+ }
107
+ return ids;
108
+ }
109
+ function parseEventDetailLines(raw) {
110
+ const value = raw.trim();
111
+ if (value === "") {
112
+ return [];
113
+ }
114
+ if (value.startsWith("[") && value.endsWith("]")) {
115
+ try {
116
+ const decoded = JSON.parse(value);
117
+ if (Array.isArray(decoded)) {
118
+ return decoded.filter((item) => typeof item === "string").map((line) => line.trim()).filter((line) => line !== "");
119
+ }
120
+ } catch {
121
+ }
122
+ }
123
+ return value.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line !== "");
124
+ }
125
+ function isCompletedToolCallEvent(message) {
126
+ if (message.tagName !== "event") {
127
+ return false;
128
+ }
129
+ const kind = getTrimmedStringAttribute(message, "kind").toLowerCase();
130
+ if (kind !== "tool") {
131
+ return false;
132
+ }
133
+ const state = (getTrimmedStringAttribute(message, "state") || "info").toLowerCase();
134
+ if (state !== "completed") {
135
+ return false;
136
+ }
137
+ const itemType = getTrimmedStringAttribute(message, "item_type").toLowerCase();
138
+ if (itemType === "tool_call") {
139
+ return true;
140
+ }
141
+ const method = getTrimmedStringAttribute(message, "method") || "agent/event";
142
+ const summary = getTrimmedStringAttribute(message, "summary") || method;
143
+ const headline = getTrimmedStringAttribute(message, "headline");
144
+ const detailLines = parseEventDetailLines(getTrimmedStringAttribute(message, "details"));
145
+ const filterHeadline = (headline !== "" ? headline : summary).trim().toLowerCase();
146
+ return filterHeadline === "called tool" && detailLines.length > 0 && detailLines.every((line) => line.trimStart().toLowerCase().startsWith("tool:"));
147
+ }
148
+ function shouldHideCompletedToolCallEvent(message, showCompletedToolCalls) {
149
+ return !showCompletedToolCalls && isCompletedToolCallEvent(message);
150
+ }
151
+ function hasRenderableStandardThreadMessageContent(message) {
152
+ if (message.tagName !== "message") {
153
+ return true;
154
+ }
155
+ const text = getTrimmedStringAttribute(message, "text");
156
+ if (text !== "") {
157
+ return true;
158
+ }
159
+ return getElementChildren(message).some(isThreadAttachmentElement);
160
+ }
161
+ function shouldRenderThreadElement(message, showCompletedToolCalls) {
162
+ if (message.tagName === "reasoning") {
163
+ return getTrimmedStringAttribute(message, "summary") !== "";
164
+ }
165
+ if (message.tagName === "message") {
166
+ return hasRenderableStandardThreadMessageContent(message);
167
+ }
168
+ if (message.tagName === "exec") {
169
+ return true;
170
+ }
171
+ if (message.tagName !== "event") {
172
+ return false;
173
+ }
174
+ const kind = getTrimmedStringAttribute(message, "kind").toLowerCase();
175
+ if (!supportedEventKinds.has(kind)) {
176
+ return false;
177
+ }
178
+ return !shouldHideCompletedToolCallEvent(message, showCompletedToolCalls);
179
+ }
180
+ function isCancellingThreadStatusText(statusText) {
181
+ const normalized = statusText?.trim().toLowerCase();
182
+ return normalized === "cancelling" || normalized === "canceling";
183
+ }
184
+ function distanceFromBottom(element) {
185
+ return Math.max(element.scrollHeight - element.clientHeight - element.scrollTop, 0);
186
+ }
187
+ function isNearBottom(element) {
188
+ return distanceFromBottom(element) <= stickyBottomThresholdPx;
189
+ }
190
+ function useDownloadUrl(room, path) {
191
+ const [url, setUrl] = useState(null);
42
192
  useEffect(() => {
43
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
44
- }, [messages]);
45
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 flex-shrink-1 basis-0 overflow-y-auto overflow-x-hidden p-4 space-y-4", children: [
46
- messages.map((message) => /* @__PURE__ */ jsx(
47
- ChatMessage,
48
- {
49
- room,
50
- message,
51
- localParticipantName
193
+ let cancelled = false;
194
+ if (path.trim() === "") {
195
+ setUrl(null);
196
+ return;
197
+ }
198
+ void room.storage.downloadUrl(path).then((nextUrl) => {
199
+ if (!cancelled) {
200
+ setUrl(nextUrl);
201
+ }
202
+ }).catch(() => {
203
+ if (!cancelled) {
204
+ setUrl(null);
205
+ }
206
+ });
207
+ return () => {
208
+ cancelled = true;
209
+ };
210
+ }, [path, room]);
211
+ return url;
212
+ }
213
+ function MarkdownBlock({ text }) {
214
+ return /* @__PURE__ */ jsx(
215
+ ReactMarkdown,
216
+ {
217
+ remarkPlugins: [remarkGfm],
218
+ rehypePlugins: [rehypeSanitize, rehypeHighlight],
219
+ components: {
220
+ pre: ({ className, children, ...props }) => /* @__PURE__ */ jsx(
221
+ "pre",
222
+ {
223
+ ...props,
224
+ className: cn("overflow-x-auto rounded-md border bg-background/80 p-3", className),
225
+ children
226
+ }
227
+ ),
228
+ p: ({ children, ...props }) => /* @__PURE__ */ jsx("p", { ...props, className: "mb-2 last:mb-0", children })
52
229
  },
53
- message.id
54
- )),
55
- /* @__PURE__ */ jsx("div", { ref: bottomRef })
56
- ] });
230
+ children: text
231
+ }
232
+ );
57
233
  }
58
- function ChatImage({ room, path, alt }) {
59
- const [url, setUrl] = React.useState("");
234
+ function ChatImage({
235
+ room,
236
+ path,
237
+ alt,
238
+ onSettled
239
+ }) {
240
+ const url = useDownloadUrl(room, path);
241
+ const imageRef = useRef(null);
242
+ const settledRef = useRef(false);
243
+ const markSettled = useCallback(() => {
244
+ if (settledRef.current) {
245
+ return;
246
+ }
247
+ settledRef.current = true;
248
+ onSettled?.();
249
+ }, [onSettled]);
60
250
  useEffect(() => {
61
- room.storage.downloadUrl(path).then(setUrl);
62
- }, [path]);
63
- return url === "" ? null : /* @__PURE__ */ jsx("img", { src: url, alt, className: "max-h-48 max-w-full rounded-lg" });
64
- }
65
- function ChatMessage({ room, message, localParticipantName }) {
66
- const mine = localParticipantName == message.getAttribute("author_name");
67
- const attachments = (message.children ?? []).filter((child) => child.tagName === "file");
68
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col max-w-prose gap-1", { "items-end self-end": mine, "items-start self-start": !mine }), children: [
69
- /* @__PURE__ */ jsxs("div", { className: "mb-0.5 text-xs text-muted-foreground", children: [
70
- "By ",
71
- message.getAttribute("author_name"),
72
- " at ",
73
- timeAgo(message.getAttribute("created_at"))
74
- ] }),
75
- /* @__PURE__ */ jsx(ChatBubble, { text: message.getAttribute("text"), mine }, message.id),
76
- attachments && attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap gap-2 mt-2", { "text-right": mine }), children: attachments.map((attachment) => {
77
- const path = attachment.getAttribute("path") || "";
78
- const isImage = isImageFilename(path);
79
- const filename = path.split("/").pop();
80
- if (isImage) {
81
- return /* @__PURE__ */ jsx(
82
- ChatImage,
83
- {
84
- room,
85
- path,
86
- alt: filename || "Image Attachment"
251
+ settledRef.current = false;
252
+ }, [path, url]);
253
+ useEffect(() => {
254
+ if (!url) {
255
+ return;
256
+ }
257
+ if (imageRef.current?.complete) {
258
+ markSettled();
259
+ }
260
+ }, [markSettled, url]);
261
+ if (!url) {
262
+ return null;
263
+ }
264
+ return /* @__PURE__ */ jsx(
265
+ "button",
266
+ {
267
+ type: "button",
268
+ className: "block overflow-hidden rounded-md bg-muted/20 shadow-xs transition-opacity hover:opacity-95",
269
+ onClick: () => {
270
+ window.open(url, "_blank", "noopener,noreferrer");
271
+ },
272
+ children: /* @__PURE__ */ jsx(
273
+ "img",
274
+ {
275
+ ref: imageRef,
276
+ src: url,
277
+ alt,
278
+ className: "max-h-[312px] w-auto max-w-full object-cover",
279
+ onLoad: () => {
280
+ markSettled();
87
281
  },
88
- attachment.id
89
- );
282
+ onError: () => {
283
+ markSettled();
284
+ }
285
+ }
286
+ )
287
+ }
288
+ );
289
+ }
290
+ function FileAttachment({ room, path }) {
291
+ const url = useDownloadUrl(room, path);
292
+ const filename = path.split("/").pop() ?? path;
293
+ return /* @__PURE__ */ jsxs(
294
+ "button",
295
+ {
296
+ type: "button",
297
+ className: "inline-flex max-w-full items-center gap-2 rounded-md bg-muted/60 px-3 py-2 text-left shadow-xs transition-colors hover:bg-muted/80",
298
+ onClick: () => {
299
+ if (url) {
300
+ window.open(url, "_blank", "noopener,noreferrer");
301
+ }
302
+ },
303
+ children: [
304
+ /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
305
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium", children: filename }),
306
+ /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 shrink-0 text-muted-foreground" })
307
+ ]
308
+ }
309
+ );
310
+ }
311
+ function ThreadAttachment({ room, attachment, onImageSettled }) {
312
+ const path = getTrimmedStringAttribute(attachment, "path");
313
+ if (path === "") {
314
+ return null;
315
+ }
316
+ const filename = path.split("/").pop() ?? "Attachment";
317
+ if (attachment.tagName === "image" || isImagePath(path)) {
318
+ const attachmentId = getAttachmentTrackingId(attachment);
319
+ return /* @__PURE__ */ jsx(
320
+ ChatImage,
321
+ {
322
+ room,
323
+ path,
324
+ alt: filename,
325
+ onSettled: () => {
326
+ onImageSettled?.(attachmentId);
327
+ }
90
328
  }
91
- return /* @__PURE__ */ jsxs(
329
+ );
330
+ }
331
+ return /* @__PURE__ */ jsx(FileAttachment, { room, path });
332
+ }
333
+ function ChatBubble({
334
+ text,
335
+ mine
336
+ }) {
337
+ if (text.trim() === "") {
338
+ return null;
339
+ }
340
+ return /* @__PURE__ */ jsx(
341
+ "div",
342
+ {
343
+ className: cn(
344
+ "w-fit max-w-[85%] rounded-md px-4 py-3 text-sm leading-6 shadow-xs sm:max-w-2xl",
345
+ mine ? "bg-secondary/85 text-foreground" : "bg-muted/70 text-foreground"
346
+ ),
347
+ children: /* @__PURE__ */ jsx(MarkdownBlock, { text })
348
+ }
349
+ );
350
+ }
351
+ function ThreadReasoning({ message }) {
352
+ const summary = getTrimmedStringAttribute(message, "summary");
353
+ if (summary === "") {
354
+ return null;
355
+ }
356
+ return /* @__PURE__ */ jsx("div", { className: "max-w-2xl border-l-2 border-primary/30 pl-4 text-sm text-muted-foreground", children: /* @__PURE__ */ jsx(MarkdownBlock, { text: summary }) });
357
+ }
358
+ function ThreadExec({ message }) {
359
+ const command = getTrimmedStringAttribute(message, "command");
360
+ const result = getTrimmedStringAttribute(message, "result");
361
+ const stdout = getTrimmedStringAttribute(message, "stdout");
362
+ const stderr = getTrimmedStringAttribute(message, "stderr");
363
+ const sections = [
364
+ command !== "" ? { label: "Command", value: command } : null,
365
+ result !== "" ? { label: "Result", value: result } : null,
366
+ stdout !== "" ? { label: "Stdout", value: stdout } : null,
367
+ stderr !== "" ? { label: "Stderr", value: stderr } : null
368
+ ].filter((section) => section !== null);
369
+ return /* @__PURE__ */ jsxs("div", { className: "max-w-3xl rounded-2xl border bg-background/70 p-4", children: [
370
+ /* @__PURE__ */ jsx("div", { className: "mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground", children: "Terminal" }),
371
+ /* @__PURE__ */ jsx("div", { className: "space-y-3 font-mono text-xs leading-6 text-foreground", children: sections.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground", children: "No command output." }) : sections.map((section) => /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
372
+ /* @__PURE__ */ jsx("div", { className: "text-[11px] uppercase tracking-[0.16em] text-muted-foreground", children: section.label }),
373
+ /* @__PURE__ */ jsx("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded-xl border bg-muted/30 p-3", children: section.value })
374
+ ] }, section.label)) })
375
+ ] });
376
+ }
377
+ function defaultEventHeadline(kind, state, eventName) {
378
+ if (kind === "approval") {
379
+ return state === "completed" ? "Approval resolved" : "Approval required";
380
+ }
381
+ if (kind === "tool") {
382
+ if (state === "failed") {
383
+ return "Tool failed";
384
+ }
385
+ if (state === "completed") {
386
+ return "Tool completed";
387
+ }
388
+ return "Calling tool";
389
+ }
390
+ if (kind === "web") {
391
+ return "Opened webpage";
392
+ }
393
+ if (kind === "search") {
394
+ return "Ran search";
395
+ }
396
+ if (kind === "diff") {
397
+ return "Updated diff";
398
+ }
399
+ if (kind === "file") {
400
+ return "File event";
401
+ }
402
+ if (kind === "thread") {
403
+ return "Thread event";
404
+ }
405
+ if (kind === "plan") {
406
+ return "Updated plan";
407
+ }
408
+ if (kind === "exec") {
409
+ return "Ran command";
410
+ }
411
+ return eventName.replace(/[._]/gu, " ");
412
+ }
413
+ function EventRow({
414
+ room,
415
+ message
416
+ }) {
417
+ const method = getTrimmedStringAttribute(message, "method") || "agent/event";
418
+ const eventName = getTrimmedStringAttribute(message, "name") || getTrimmedStringAttribute(message, "event_type") || method.replace(/\//gu, ".");
419
+ const kind = getTrimmedStringAttribute(message, "kind").toLowerCase();
420
+ const state = (getTrimmedStringAttribute(message, "state") || "info").toLowerCase();
421
+ const summary = getTrimmedStringAttribute(message, "summary");
422
+ const headline = getTrimmedStringAttribute(message, "headline") || (summary !== "" ? summary : defaultEventHeadline(kind, state, eventName));
423
+ const detailLines = parseEventDetailLines(getTrimmedStringAttribute(message, "details"));
424
+ const eventPath = getTrimmedStringAttribute(message, "path");
425
+ const inProgress = state === "in_progress" || state === "running" || state === "queued";
426
+ const textColorClass = state === "failed" ? "text-destructive" : state === "cancelled" ? "text-muted-foreground" : inProgress ? "text-primary" : "text-foreground";
427
+ if (headline === "") {
428
+ return null;
429
+ }
430
+ return /* @__PURE__ */ jsx("div", { className: "max-w-3xl rounded-2xl border bg-background/70 px-4 py-3 text-xs", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start gap-2", children: [
431
+ /* @__PURE__ */ jsx("span", { className: "rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-[0.16em] text-muted-foreground", children: kind }),
432
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [
433
+ /* @__PURE__ */ jsx("div", { className: cn("text-sm font-semibold leading-5", textColorClass), children: headline }),
434
+ summary !== "" && summary !== headline ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground", children: summary }) : null,
435
+ detailLines.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-1 text-muted-foreground", children: detailLines.map((line, index) => /* @__PURE__ */ jsx("div", { className: "leading-5", children: line }, `${line}-${index}`)) }) : null,
436
+ eventPath !== "" ? /* @__PURE__ */ jsxs(
92
437
  "button",
93
438
  {
94
439
  type: "button",
440
+ className: "inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-left text-[11px] font-medium text-foreground transition-colors hover:bg-muted",
95
441
  onClick: () => {
96
- room.storage.downloadUrl(path).then((url) => window.open(url, "_blank"));
442
+ void room.storage.downloadUrl(eventPath).then((url) => {
443
+ window.open(url, "_blank", "noopener,noreferrer");
444
+ });
97
445
  },
98
- className: "relative inline-flex max-w-full items-center border bg-muted pl-3 pr-1 py-1 gap-2 cursor-pointer hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
99
- rel: "noopener noreferrer",
100
446
  children: [
101
- /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium leading-none", children: filename }),
102
- /* @__PURE__ */ jsx(Download, { className: "inline-block mr-1" })
447
+ /* @__PURE__ */ jsx(FileText, { className: "h-3.5 w-3.5 shrink-0" }),
448
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: eventPath })
103
449
  ]
104
- },
105
- attachment.id
106
- );
107
- }) })
450
+ }
451
+ ) : null
452
+ ] })
453
+ ] }) });
454
+ }
455
+ function ThreadMessage({
456
+ room,
457
+ message,
458
+ previous,
459
+ localParticipantName,
460
+ onImageSettled
461
+ }) {
462
+ const authorName = getTrimmedStringAttribute(message, "author_name");
463
+ const mine = authorName !== "" && authorName === localParticipantName.trim();
464
+ const createdAt = getTrimmedStringAttribute(message, "created_at");
465
+ const text = getStringAttribute(message, "text") ?? "";
466
+ const attachments = getElementChildren(message).filter(isThreadAttachmentElement);
467
+ const previousAuthor = previous ? getTrimmedStringAttribute(previous, "author_name") : "";
468
+ const shouldShowHeader = previousAuthor !== authorName;
469
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
470
+ shouldShowHeader && (authorName !== "" || createdAt !== "") ? /* @__PURE__ */ jsx("div", { className: cn("flex w-full", mine ? "justify-end" : "justify-start"), children: /* @__PURE__ */ jsx("div", { className: cn("max-w-[85%] px-1 sm:max-w-2xl", mine ? "text-right" : "text-left"), children: /* @__PURE__ */ jsxs(
471
+ "div",
472
+ {
473
+ className: cn(
474
+ "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground",
475
+ mine ? "justify-end" : "justify-start"
476
+ ),
477
+ children: [
478
+ authorName !== "" ? /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: displayParticipantName(authorName) }) : null,
479
+ createdAt !== "" ? /* @__PURE__ */ jsx("span", { children: timeAgo(createdAt) }) : null
480
+ ]
481
+ }
482
+ ) }) }) : null,
483
+ text.trim() !== "" ? /* @__PURE__ */ jsx("div", { className: cn("flex w-full", mine ? "justify-end" : "justify-start"), children: /* @__PURE__ */ jsx(ChatBubble, { text, mine }) }) : null,
484
+ attachments.length > 0 ? /* @__PURE__ */ jsx("div", { className: cn("flex w-full", mine ? "justify-end" : "justify-start"), children: /* @__PURE__ */ jsx(
485
+ "div",
486
+ {
487
+ className: cn(
488
+ "flex max-w-[85%] flex-wrap gap-3 px-1 sm:max-w-2xl",
489
+ mine ? "justify-end" : "justify-start"
490
+ ),
491
+ children: attachments.map((attachment) => /* @__PURE__ */ jsx(
492
+ ThreadAttachment,
493
+ {
494
+ room,
495
+ attachment,
496
+ onImageSettled
497
+ },
498
+ attachment.id
499
+ ))
500
+ }
501
+ ) }) : null
108
502
  ] });
109
503
  }
110
- function ChatBubble({ text, mine }) {
111
- if (!text || text.trim() === "") {
112
- return /* @__PURE__ */ jsx(Fragment, {});
113
- }
114
- return /* @__PURE__ */ jsx("div", { className: cn(
115
- "rounded-lg px-4 py-2 text-sm max-w-prose whitespace-pre-wrap",
116
- {
117
- "bg-primary text-primary-foreground": mine,
118
- "bg-muted": !mine
504
+ function EmptyState({
505
+ title,
506
+ description
507
+ }) {
508
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-2xl flex-col items-center justify-center px-6 py-20 text-center", children: [
509
+ /* @__PURE__ */ jsx("h2", { className: "text-4xl font-semibold tracking-tight text-foreground sm:text-5xl", children: title }),
510
+ description?.trim() ? /* @__PURE__ */ jsx("p", { className: "mt-3 max-w-xl text-sm leading-6 text-muted-foreground sm:text-base", children: description }) : null
511
+ ] });
512
+ }
513
+ function ChatThread({
514
+ room,
515
+ messages,
516
+ isLoading = false,
517
+ localParticipantName,
518
+ path,
519
+ showCompletedToolCalls = false,
520
+ onShowCompletedToolCallsChanged,
521
+ typing = false,
522
+ thinking = false,
523
+ threadStatusText,
524
+ threadStatusStartedAt,
525
+ threadStatusMode,
526
+ onCancelRequest,
527
+ emptyStateTitle,
528
+ emptyStateDescription
529
+ }) {
530
+ const visibleMessages = useMemo(
531
+ () => messages.filter((message) => shouldRenderThreadElement(message, showCompletedToolCalls)),
532
+ [messages, showCompletedToolCalls]
533
+ );
534
+ const hiddenCompletedToolCallCount = useMemo(
535
+ () => messages.filter(isCompletedToolCallEvent).length,
536
+ [messages]
537
+ );
538
+ const [trackedInitialImageThreadKey, setTrackedInitialImageThreadKey] = useState(null);
539
+ const [pendingInitialImageIds, setPendingInitialImageIds] = useState([]);
540
+ const scrollContainerRef = useRef(null);
541
+ const contentRef = useRef(null);
542
+ const stickToBottomRef = useRef(true);
543
+ const forceBottomRef = useRef(false);
544
+ const lastVisibleMessageId = visibleMessages.length > 0 ? visibleMessages[visibleMessages.length - 1]?.id : null;
545
+ const threadKey = path?.trim() || "__default__";
546
+ const hasOverlay = threadStatusText?.trim() || thinking || typing;
547
+ const waitingForInitialImages = trackedInitialImageThreadKey === threadKey && pendingInitialImageIds.length > 0;
548
+ useEffect(() => {
549
+ forceBottomRef.current = waitingForInitialImages;
550
+ }, [waitingForInitialImages]);
551
+ useEffect(() => {
552
+ setTrackedInitialImageThreadKey(null);
553
+ setPendingInitialImageIds([]);
554
+ }, [threadKey]);
555
+ useEffect(() => {
556
+ if (trackedInitialImageThreadKey === threadKey || visibleMessages.length === 0) {
557
+ return;
119
558
  }
120
- ), children: /* @__PURE__ */ jsx(
121
- ReactMarkdown,
122
- {
123
- remarkPlugins: [remarkGfm],
124
- rehypePlugins: [rehypeSanitize, rehypeHighlight],
125
- components: {
126
- pre: ({ node, className, children, ...props }) => /* @__PURE__ */ jsx("pre", { ...props, className: cn("overflow-x-auto rounded-lg", className), children }),
127
- p: ({ node, children, ...props }) => /* @__PURE__ */ jsx("p", { ...props, className: "mb-2 last:mb-0", children }),
128
- code: ({ className, children, ...props }) => /* @__PURE__ */ jsx("code", { ...props, className, children })
129
- },
130
- children: text
559
+ setTrackedInitialImageThreadKey(threadKey);
560
+ setPendingInitialImageIds(collectImageAttachmentIds(visibleMessages));
561
+ }, [threadKey, trackedInitialImageThreadKey, visibleMessages]);
562
+ const handleImageSettled = useCallback((attachmentId) => {
563
+ setPendingInitialImageIds((currentIds) => {
564
+ if (!currentIds.includes(attachmentId)) {
565
+ return currentIds;
566
+ }
567
+ return currentIds.filter((currentId) => currentId !== attachmentId);
568
+ });
569
+ }, []);
570
+ useEffect(() => {
571
+ const container = scrollContainerRef.current;
572
+ if (!container) {
573
+ return;
131
574
  }
132
- ) });
575
+ stickToBottomRef.current = true;
576
+ container.scrollTop = container.scrollHeight;
577
+ }, [path]);
578
+ useEffect(() => {
579
+ if (!waitingForInitialImages) {
580
+ return;
581
+ }
582
+ const container = scrollContainerRef.current;
583
+ if (!container) {
584
+ return;
585
+ }
586
+ container.scrollTop = container.scrollHeight;
587
+ }, [pendingInitialImageIds, waitingForInitialImages]);
588
+ useEffect(() => {
589
+ const container = scrollContainerRef.current;
590
+ if (!container || !stickToBottomRef.current) {
591
+ return;
592
+ }
593
+ container.scrollTop = container.scrollHeight;
594
+ }, [hasOverlay, lastVisibleMessageId, threadStatusText, thinking, typing]);
595
+ useEffect(() => {
596
+ const container = scrollContainerRef.current;
597
+ const content = contentRef.current;
598
+ if (!container || !content || typeof ResizeObserver === "undefined") {
599
+ return;
600
+ }
601
+ const observer = new ResizeObserver(() => {
602
+ if (!stickToBottomRef.current && !forceBottomRef.current) {
603
+ return;
604
+ }
605
+ container.scrollTop = container.scrollHeight;
606
+ });
607
+ observer.observe(content);
608
+ return () => {
609
+ observer.disconnect();
610
+ };
611
+ }, []);
612
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [
613
+ onShowCompletedToolCallsChanged && hiddenCompletedToolCallCount > 0 ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-center px-4 pt-3", children: /* @__PURE__ */ jsx("div", { className: "pointer-events-auto flex w-full max-w-[912px] justify-end", children: /* @__PURE__ */ jsx(
614
+ Button,
615
+ {
616
+ type: "button",
617
+ variant: "ghost",
618
+ size: "sm",
619
+ className: "rounded-full border bg-background/90 backdrop-blur",
620
+ onClick: () => {
621
+ onShowCompletedToolCallsChanged(!showCompletedToolCalls);
622
+ },
623
+ children: showCompletedToolCalls ? "Hide tool calls" : "Show tool calls"
624
+ }
625
+ ) }) }) : null,
626
+ /* @__PURE__ */ jsx(
627
+ "div",
628
+ {
629
+ ref: scrollContainerRef,
630
+ className: "min-h-0 flex-1 overflow-y-auto overflow-x-hidden [overflow-anchor:none]",
631
+ onScroll: (event) => {
632
+ stickToBottomRef.current = isNearBottom(event.currentTarget);
633
+ },
634
+ children: /* @__PURE__ */ jsxs(
635
+ "div",
636
+ {
637
+ ref: contentRef,
638
+ className: cn(
639
+ "mx-auto flex min-h-full w-full max-w-[912px] flex-col gap-8 px-4 pt-6",
640
+ visibleMessages.length > 0 ? "justify-end" : null,
641
+ hasOverlay ? "pb-24" : "pb-6"
642
+ ),
643
+ children: [
644
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: /* @__PURE__ */ jsx(Spinner, { size: "lg", className: "text-muted-foreground" }) }) : visibleMessages.length === 0 && emptyStateTitle ? /* @__PURE__ */ jsx(EmptyState, { title: emptyStateTitle, description: emptyStateDescription }) : null,
645
+ visibleMessages.map((message, index) => {
646
+ const previous = index > 0 ? visibleMessages[index - 1] : null;
647
+ if (message.tagName === "message") {
648
+ return /* @__PURE__ */ jsx(
649
+ ThreadMessage,
650
+ {
651
+ room,
652
+ message,
653
+ previous,
654
+ localParticipantName,
655
+ onImageSettled: handleImageSettled
656
+ },
657
+ message.id
658
+ );
659
+ }
660
+ if (message.tagName === "reasoning") {
661
+ return /* @__PURE__ */ jsx(ThreadReasoning, { message }, message.id);
662
+ }
663
+ if (message.tagName === "exec") {
664
+ return /* @__PURE__ */ jsx(ThreadExec, { message }, message.id);
665
+ }
666
+ if (message.tagName === "event") {
667
+ return /* @__PURE__ */ jsx(EventRow, { room, message }, message.id);
668
+ }
669
+ return null;
670
+ })
671
+ ]
672
+ }
673
+ )
674
+ }
675
+ ),
676
+ hasOverlay ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 flex justify-center px-4 pb-4", children: /* @__PURE__ */ jsx("div", { className: "pointer-events-auto w-full max-w-[912px]", children: /* @__PURE__ */ jsx(
677
+ ChatTypingIndicator,
678
+ {
679
+ typing,
680
+ thinking,
681
+ statusText: threadStatusText,
682
+ startedAt: threadStatusStartedAt,
683
+ onCancel: onCancelRequest,
684
+ showCancelButton: threadStatusMode != null,
685
+ cancelEnabled: !isCancellingThreadStatusText(threadStatusText)
686
+ }
687
+ ) }) }) : null
688
+ ] });
133
689
  }
134
690
  export {
135
691
  ChatThread,