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