camox 0.29.0 → 0.31.0

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.
@@ -0,0 +1,594 @@
1
+ import { previewStore } from "../../preview/previewStore.js";
2
+ import { getAuthCookieHeader } from "../../../lib/auth.js";
3
+ import { getApiClient, getApiUrl, getEnvironmentName } from "../../../lib/api-client.js";
4
+ import { cn } from "../../../lib/utils.js";
5
+ import { buildAgentChatToolLabelContext, getToolCallLabel } from "../agent-chat-labels.js";
6
+ import { AgentToolCallCard } from "./AgentToolCallCard.js";
7
+ import { c } from "react/compiler-runtime";
8
+ import { useNavigate } from "@tanstack/react-router";
9
+ import * as React from "react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { Button } from "@camox/ui/button";
12
+ import { ArrowUp, Brain, Loader2, Square } from "lucide-react";
13
+ import { Alert, AlertDescription, AlertTitle } from "@camox/ui/alert";
14
+ import { Textarea } from "@camox/ui/textarea";
15
+ import { fetchServerSentEvents, useChat } from "@tanstack/ai-react";
16
+ import { Streamdown } from "streamdown";
17
+
18
+ //#region src/features/agent-chat/components/AgentChatThread.tsx
19
+ function createTextMessage(id, role, content) {
20
+ return {
21
+ id,
22
+ role,
23
+ parts: [{
24
+ type: "text",
25
+ content
26
+ }]
27
+ };
28
+ }
29
+ function createPageScaffoldMessages({ nickname, fullPath }) {
30
+ return [createTextMessage(`page-scaffold-user:${fullPath}`, "user", `I just created an empty _${nickname}_ page at \`${fullPath}\`.`), createTextMessage(`page-scaffold-assistant:${fullPath}`, "assistant", "I can create a draft of the page structure and content.<br/>**What should this page be about?**")];
31
+ }
32
+ function getTextPartContent(part) {
33
+ if (part.type !== "text") return null;
34
+ return part.content;
35
+ }
36
+ function hasVisibleAssistantOutput(message) {
37
+ return message.parts.some((part) => {
38
+ if (part.type === "text") return part.content.trim().length > 0;
39
+ if (part.type === "thinking") return true;
40
+ if (part.type === "tool-call") return true;
41
+ return false;
42
+ });
43
+ }
44
+ function isAssistantResponseOutputPart(part) {
45
+ if (part.type === "text") return part.content.trim().length > 0;
46
+ if (part.type === "tool-call") return true;
47
+ return false;
48
+ }
49
+ function getAssistantResponseOutputKey(message) {
50
+ const outputParts = message.parts.flatMap((part) => {
51
+ if (part.type === "text" && isAssistantResponseOutputPart(part)) return [`text:${part.content.length}`];
52
+ if (part.type === "tool-call") return [`tool:${part.id}:${part.state}`];
53
+ return [];
54
+ });
55
+ return outputParts.length > 0 ? outputParts.join("|") : null;
56
+ }
57
+ function hasLaterAssistantResponseOutput(parts, partIndex) {
58
+ return parts.slice(partIndex + 1).some(isAssistantResponseOutputPart);
59
+ }
60
+ function hasThinkingPart(message) {
61
+ return message.parts.some((part) => part.type === "thinking");
62
+ }
63
+ function getThinkingPartDurationKey(message, part, partIndex) {
64
+ const stepId = part.stepId;
65
+ return `${message.id}:${typeof stepId === "string" ? stepId : partIndex}`;
66
+ }
67
+ function formatThinkingDuration(seconds) {
68
+ return Math.max(1, Math.round(seconds)).toString();
69
+ }
70
+ function hasRequiresApprovalMetadata(part) {
71
+ return part.metadata?.risk === "requiresApproval";
72
+ }
73
+ function findToolResult(parts, toolCallId) {
74
+ return parts.find((part) => part.type === "tool-result" && part.toolCallId === toolCallId);
75
+ }
76
+ const markdownComponents = {
77
+ p: ({ children }) => /* @__PURE__ */ jsx("p", {
78
+ className: "leading-relaxed",
79
+ children
80
+ }),
81
+ a: ({ children, ...props }) => /* @__PURE__ */ jsx("a", {
82
+ ...props,
83
+ className: "wrap-break-words underline underline-offset-2",
84
+ children
85
+ }),
86
+ ul: ({ children }) => /* @__PURE__ */ jsx("ul", {
87
+ className: "my-2 list-disc space-y-1 pl-5",
88
+ children
89
+ }),
90
+ ol: ({ children }) => /* @__PURE__ */ jsx("ol", {
91
+ className: "my-2 list-decimal space-y-1 pl-5",
92
+ children
93
+ }),
94
+ blockquote: ({ children }) => /* @__PURE__ */ jsx("blockquote", {
95
+ className: "my-2 border-l-2 border-current/30 pl-3",
96
+ children
97
+ }),
98
+ code: ({ children }) => /* @__PURE__ */ jsx("code", {
99
+ className: "bg-background/50 rounded px-1 py-0.5 font-mono text-[0.9em]",
100
+ children
101
+ }),
102
+ pre: ({ children }) => /* @__PURE__ */ jsx("pre", {
103
+ className: "bg-background/50 my-2 max-w-full overflow-x-auto rounded-md p-3 text-xs",
104
+ children
105
+ })
106
+ };
107
+ const MessageBubble = (t0) => {
108
+ const $ = c(34);
109
+ if ($[0] !== "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82") {
110
+ for (let $i = 0; $i < 34; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
111
+ $[0] = "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82";
112
+ }
113
+ const { message, source, isThinkingActive, activeThinkingDurationKeys, thinkingDurations, toolLabelContext, onApprovalResponse } = t0;
114
+ const isUser = message.role === "user";
115
+ const fallbackThinkingDurationSeconds = thinkingDurations[message.id];
116
+ let t1;
117
+ if ($[1] !== fallbackThinkingDurationSeconds || $[2] !== isThinkingActive || $[3] !== isUser || $[4] !== message) {
118
+ t1 = !isUser && !hasThinkingPart(message) && (isThinkingActive || fallbackThinkingDurationSeconds !== void 0);
119
+ $[1] = fallbackThinkingDurationSeconds;
120
+ $[2] = isThinkingActive;
121
+ $[3] = isUser;
122
+ $[4] = message;
123
+ $[5] = t1;
124
+ } else t1 = $[5];
125
+ const shouldShowMessageThinkingIndicator = t1;
126
+ const t2 = isUser && "justify-end";
127
+ let t3;
128
+ if ($[6] !== t2) {
129
+ t3 = cn("flex gap-3", t2);
130
+ $[6] = t2;
131
+ $[7] = t3;
132
+ } else t3 = $[7];
133
+ const t4 = isUser ? "max-w-[85%]" : "max-w-full";
134
+ let t5;
135
+ if ($[8] !== t4) {
136
+ t5 = cn("space-y-1", t4);
137
+ $[8] = t4;
138
+ $[9] = t5;
139
+ } else t5 = $[9];
140
+ const t6 = isUser ? "bg-primary text-primary-foreground rounded-lg px-3 py-2" : "text-foreground space-y-3";
141
+ let t7;
142
+ if ($[10] !== t6) {
143
+ t7 = cn("text-sm", t6);
144
+ $[10] = t6;
145
+ $[11] = t7;
146
+ } else t7 = $[11];
147
+ let t8;
148
+ if ($[12] !== fallbackThinkingDurationSeconds || $[13] !== shouldShowMessageThinkingIndicator) {
149
+ t8 = shouldShowMessageThinkingIndicator && /* @__PURE__ */ jsx(AgentThinkingIndicator, { durationSeconds: fallbackThinkingDurationSeconds });
150
+ $[12] = fallbackThinkingDurationSeconds;
151
+ $[13] = shouldShowMessageThinkingIndicator;
152
+ $[14] = t8;
153
+ } else t8 = $[14];
154
+ let t9;
155
+ if ($[15] !== activeThinkingDurationKeys || $[16] !== fallbackThinkingDurationSeconds || $[17] !== isUser || $[18] !== message || $[19] !== onApprovalResponse || $[20] !== source || $[21] !== thinkingDurations || $[22] !== toolLabelContext) {
156
+ t9 = message.parts.map((part, index) => {
157
+ const text = getTextPartContent(part);
158
+ if (text != null) {
159
+ if (text.trim().length === 0) return null;
160
+ return /* @__PURE__ */ jsx(Streamdown, {
161
+ className: cn("wrap-break-words space-y-2", !isUser && "pl-6"),
162
+ components: markdownComponents,
163
+ controls: false,
164
+ isAnimating: !isUser,
165
+ children: text
166
+ }, index);
167
+ }
168
+ if (part.type === "thinking") {
169
+ const durationKey = getThinkingPartDurationKey(message, part, index);
170
+ const durationSeconds = thinkingDurations[durationKey] ?? (hasLaterAssistantResponseOutput(message.parts, index) ? fallbackThinkingDurationSeconds : void 0);
171
+ if (durationSeconds !== void 0 || activeThinkingDurationKeys.has(durationKey)) return /* @__PURE__ */ jsx(AgentThinkingIndicator, { durationSeconds }, durationKey);
172
+ return null;
173
+ }
174
+ if (part.type === "tool-call") return /* @__PURE__ */ jsx(AgentToolCallCard, {
175
+ part,
176
+ label: getToolCallLabel(part, toolLabelContext),
177
+ result: findToolResult(message.parts, part.id),
178
+ requiresApprovalFallback: source === "draft" && hasRequiresApprovalMetadata(part),
179
+ onApprovalResponse: (approved) => onApprovalResponse(part, approved)
180
+ }, part.id);
181
+ if (part.type === "tool-result") return null;
182
+ return null;
183
+ });
184
+ $[15] = activeThinkingDurationKeys;
185
+ $[16] = fallbackThinkingDurationSeconds;
186
+ $[17] = isUser;
187
+ $[18] = message;
188
+ $[19] = onApprovalResponse;
189
+ $[20] = source;
190
+ $[21] = thinkingDurations;
191
+ $[22] = toolLabelContext;
192
+ $[23] = t9;
193
+ } else t9 = $[23];
194
+ let t10;
195
+ if ($[24] !== t7 || $[25] !== t8 || $[26] !== t9) {
196
+ t10 = /* @__PURE__ */ jsxs("div", {
197
+ className: t7,
198
+ children: [t8, t9]
199
+ });
200
+ $[24] = t7;
201
+ $[25] = t8;
202
+ $[26] = t9;
203
+ $[27] = t10;
204
+ } else t10 = $[27];
205
+ let t11;
206
+ if ($[28] !== t10 || $[29] !== t5) {
207
+ t11 = /* @__PURE__ */ jsx("div", {
208
+ className: t5,
209
+ children: t10
210
+ });
211
+ $[28] = t10;
212
+ $[29] = t5;
213
+ $[30] = t11;
214
+ } else t11 = $[30];
215
+ let t12;
216
+ if ($[31] !== t11 || $[32] !== t3) {
217
+ t12 = /* @__PURE__ */ jsx("div", {
218
+ className: t3,
219
+ children: t11
220
+ });
221
+ $[31] = t11;
222
+ $[32] = t3;
223
+ $[33] = t12;
224
+ } else t12 = $[33];
225
+ return t12;
226
+ };
227
+ const AgentThinkingIndicator = (t0) => {
228
+ const $ = c(7);
229
+ if ($[0] !== "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82") {
230
+ for (let $i = 0; $i < 7; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
231
+ $[0] = "ac29c5bab5ba745d35d55b8522bbfdd5d2f187fdcb71b67d53addf6539f9af82";
232
+ }
233
+ const { durationSeconds } = t0;
234
+ if (durationSeconds !== void 0) {
235
+ let t1;
236
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
237
+ t1 = /* @__PURE__ */ jsx(Brain, { className: "size-3 justify-self-center" });
238
+ $[1] = t1;
239
+ } else t1 = $[1];
240
+ let t2;
241
+ if ($[2] !== durationSeconds) {
242
+ t2 = formatThinkingDuration(durationSeconds);
243
+ $[2] = durationSeconds;
244
+ $[3] = t2;
245
+ } else t2 = $[3];
246
+ let t3;
247
+ if ($[4] !== t2) {
248
+ t3 = /* @__PURE__ */ jsxs("div", {
249
+ className: "text-muted-foreground grid grid-cols-[1rem_minmax(0,1fr)] items-center gap-2 text-sm",
250
+ children: [t1, /* @__PURE__ */ jsxs("span", { children: [
251
+ "Thought for ",
252
+ t2,
253
+ "s"
254
+ ] })]
255
+ });
256
+ $[4] = t2;
257
+ $[5] = t3;
258
+ } else t3 = $[5];
259
+ return t3;
260
+ }
261
+ let t1;
262
+ if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
263
+ t1 = /* @__PURE__ */ jsxs("div", {
264
+ className: "text-muted-foreground grid grid-cols-[1rem_minmax(0,1fr)] items-center gap-2 text-sm",
265
+ children: [/* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin justify-self-center" }), /* @__PURE__ */ jsx("span", { children: "Thinking..." })]
266
+ });
267
+ $[6] = t1;
268
+ } else t1 = $[6];
269
+ return t1;
270
+ };
271
+ const AgentChatThread = ({ projectId, currentPath, source, disabled, focusKey = 0, pageScaffoldContext }) => {
272
+ const [input, setInput] = React.useState("");
273
+ const inputRef = React.useRef(null);
274
+ const messagesScrollRef = React.useRef(null);
275
+ const thinkingStartedAtRef = React.useRef(null);
276
+ const thinkingSegmentStartsRef = React.useRef(/* @__PURE__ */ new Map());
277
+ const toolCallContextByIdRef = React.useRef(/* @__PURE__ */ new Map());
278
+ const [thinkingDurations, setThinkingDurations] = React.useState({});
279
+ const [activeThinkingDurationKeys, setActiveThinkingDurationKeys] = React.useState(() => /* @__PURE__ */ new Set());
280
+ const navigate = useNavigate();
281
+ const initialMessages = React.useMemo(() => {
282
+ if (!pageScaffoldContext) return [];
283
+ return createPageScaffoldMessages(pageScaffoldContext);
284
+ }, [pageScaffoldContext]);
285
+ const connection = React.useMemo(() => fetchServerSentEvents(`${getApiUrl()}/agent/chat`, () => {
286
+ const headers = {
287
+ "Better-Auth-Cookie": getAuthCookieHeader(),
288
+ "x-camox-client": "studio"
289
+ };
290
+ const environmentName = getEnvironmentName();
291
+ if (environmentName) headers["x-environment-name"] = environmentName;
292
+ if (__CAMOX_TELEMETRY_DISABLED__) headers["x-camox-telemetry-disabled"] = "1";
293
+ return {
294
+ headers,
295
+ credentials: "omit",
296
+ body: {
297
+ projectId,
298
+ currentPath,
299
+ source
300
+ }
301
+ };
302
+ }), [
303
+ currentPath,
304
+ projectId,
305
+ source
306
+ ]);
307
+ const handleCustomEvent = React.useCallback((eventType, data) => {
308
+ if (eventType !== "agent-chat-ui-action") return;
309
+ if (!data || typeof data !== "object") return;
310
+ const action = data.action;
311
+ if (action === "navigate") {
312
+ const fullPath = data.fullPath;
313
+ if (typeof fullPath !== "string" || !fullPath.startsWith("/")) return;
314
+ if (fullPath === currentPath) return;
315
+ navigate({ to: fullPath });
316
+ return;
317
+ }
318
+ if (action !== "focusBlock") return;
319
+ const blockId = data.blockId;
320
+ if (typeof blockId !== "number") return;
321
+ previewStore.send({
322
+ type: "focusAgentBlock",
323
+ blockId
324
+ });
325
+ }, [currentPath, navigate]);
326
+ const { messages, sendMessage, isLoading, error, stop, addToolApprovalResponse, addToolResult } = useChat({
327
+ connection,
328
+ body: {
329
+ projectId,
330
+ currentPath,
331
+ source
332
+ },
333
+ initialMessages,
334
+ onCustomEvent: handleCustomEvent
335
+ });
336
+ const lastMessage = messages[messages.length - 1];
337
+ const lastAssistantMessage = lastMessage?.role === "assistant" ? lastMessage : [...messages].reverse().find((message) => message.role === "assistant");
338
+ const activeThinkingMessageId = isLoading && thinkingStartedAtRef.current !== null && lastMessage?.role === "assistant" ? lastMessage.id : void 0;
339
+ const lastMessageResponseOutputKey = lastMessage?.role === "assistant" ? getAssistantResponseOutputKey(lastMessage) : null;
340
+ const shouldShowThinkingFallback = isLoading && thinkingStartedAtRef.current !== null && !error && (!lastMessage || lastMessage.role === "user" || lastMessage.role === "assistant" && !hasVisibleAssistantOutput(lastMessage));
341
+ const toolCallContextById = React.useMemo(() => {
342
+ for (const message_0 of messages) for (const part of message_0.parts) {
343
+ if (part.type !== "tool-call") continue;
344
+ if (toolCallContextByIdRef.current.has(part.id)) continue;
345
+ toolCallContextByIdRef.current.set(part.id, {
346
+ currentPath,
347
+ source
348
+ });
349
+ }
350
+ return new Map(toolCallContextByIdRef.current);
351
+ }, [
352
+ currentPath,
353
+ messages,
354
+ source
355
+ ]);
356
+ const toolLabelContext = React.useMemo(() => buildAgentChatToolLabelContext({
357
+ messages,
358
+ currentPath,
359
+ source,
360
+ toolCallContextById
361
+ }), [
362
+ currentPath,
363
+ messages,
364
+ source,
365
+ toolCallContextById
366
+ ]);
367
+ React.useEffect(() => {
368
+ const now = Date.now();
369
+ const activeDurationKeys = /* @__PURE__ */ new Set();
370
+ setThinkingDurations((durations) => {
371
+ let nextDurations = durations;
372
+ for (const message_1 of messages) {
373
+ if (message_1.role !== "assistant") continue;
374
+ message_1.parts.forEach((part_0, index) => {
375
+ if (part_0.type !== "thinking") return;
376
+ const durationKey = getThinkingPartDurationKey(message_1, part_0, index);
377
+ if (durations[durationKey] !== void 0) return;
378
+ const startedAt = thinkingSegmentStartsRef.current.get(durationKey) ?? now;
379
+ thinkingSegmentStartsRef.current.set(durationKey, startedAt);
380
+ if (!hasLaterAssistantResponseOutput(message_1.parts, index)) {
381
+ activeDurationKeys.add(durationKey);
382
+ return;
383
+ }
384
+ if (nextDurations === durations) nextDurations = { ...durations };
385
+ nextDurations[durationKey] = Math.max(0, (now - startedAt) / 1e3);
386
+ thinkingSegmentStartsRef.current.delete(durationKey);
387
+ });
388
+ }
389
+ return nextDurations;
390
+ });
391
+ setActiveThinkingDurationKeys(activeDurationKeys);
392
+ }, [messages]);
393
+ React.useEffect(() => {
394
+ if (!isLoading) return;
395
+ if (thinkingStartedAtRef.current !== null) return;
396
+ thinkingStartedAtRef.current = Date.now();
397
+ }, [isLoading]);
398
+ React.useEffect(() => {
399
+ const startedAt_0 = thinkingStartedAtRef.current;
400
+ if (startedAt_0 === null) return;
401
+ if (lastMessage?.role !== "assistant") return;
402
+ if (!lastMessageResponseOutputKey) return;
403
+ thinkingStartedAtRef.current = null;
404
+ const durationSeconds = Math.max(0, (Date.now() - startedAt_0) / 1e3);
405
+ setThinkingDurations((durations_0) => ({
406
+ ...durations_0,
407
+ [lastMessage.id]: durations_0[lastMessage.id] ?? durationSeconds
408
+ }));
409
+ }, [
410
+ lastMessage?.id,
411
+ lastMessage?.role,
412
+ lastMessageResponseOutputKey
413
+ ]);
414
+ React.useEffect(() => {
415
+ if (isLoading) return;
416
+ const startedAt_1 = thinkingStartedAtRef.current;
417
+ if (startedAt_1 === null) return;
418
+ thinkingStartedAtRef.current = null;
419
+ if (!lastAssistantMessage) return;
420
+ const durationSeconds_0 = Math.max(0, (Date.now() - startedAt_1) / 1e3);
421
+ setThinkingDurations((durations_1) => ({
422
+ ...durations_1,
423
+ [lastAssistantMessage.id]: durationSeconds_0
424
+ }));
425
+ }, [isLoading, lastAssistantMessage]);
426
+ React.useEffect(() => {
427
+ if (focusKey === 0) return;
428
+ const frame = requestAnimationFrame(() => {
429
+ inputRef.current?.focus();
430
+ });
431
+ return () => cancelAnimationFrame(frame);
432
+ }, [focusKey]);
433
+ React.useEffect(() => {
434
+ const scrollContainer = messagesScrollRef.current;
435
+ if (!scrollContainer) return;
436
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
437
+ }, [messages]);
438
+ const handleApprovalResponse = async (part_1, approved) => {
439
+ const errorText = "User declined tool execution";
440
+ const refocusInput = () => requestAnimationFrame(() => inputRef.current?.focus());
441
+ if (!approved) {
442
+ if (part_1.approval) await addToolApprovalResponse({
443
+ id: part_1.approval.id,
444
+ approved: false
445
+ });
446
+ await addToolResult({
447
+ toolCallId: part_1.id,
448
+ tool: part_1.name,
449
+ output: { error: errorText },
450
+ state: "output-error",
451
+ errorText
452
+ });
453
+ refocusInput();
454
+ return;
455
+ }
456
+ if (source !== "draft") {
457
+ await addToolResult({
458
+ toolCallId: part_1.id,
459
+ tool: part_1.name,
460
+ output: { error: "This tool cannot be approved from the current source." },
461
+ state: "output-error",
462
+ errorText: "This tool cannot be approved from the current source."
463
+ });
464
+ refocusInput();
465
+ return;
466
+ }
467
+ let args = {};
468
+ try {
469
+ args = part_1.input ?? (part_1.arguments ? JSON.parse(part_1.arguments) : {});
470
+ } catch {
471
+ await addToolResult({
472
+ toolCallId: part_1.id,
473
+ tool: part_1.name,
474
+ output: { error: "Could not parse tool arguments." },
475
+ state: "output-error",
476
+ errorText: "Could not parse tool arguments."
477
+ });
478
+ refocusInput();
479
+ return;
480
+ }
481
+ const response = await getApiClient().agent.callTool({
482
+ projectId,
483
+ name: part_1.name,
484
+ arguments: args
485
+ });
486
+ if (response.ok) {
487
+ if (part_1.approval) await addToolApprovalResponse({
488
+ id: part_1.approval.id,
489
+ approved: true
490
+ });
491
+ await addToolResult({
492
+ toolCallId: part_1.id,
493
+ tool: part_1.name,
494
+ output: response.result
495
+ });
496
+ refocusInput();
497
+ return;
498
+ }
499
+ await addToolResult({
500
+ toolCallId: part_1.id,
501
+ tool: part_1.name,
502
+ output: response.error,
503
+ state: "output-error",
504
+ errorText: response.error.message
505
+ });
506
+ refocusInput();
507
+ };
508
+ const handleSubmit = (event) => {
509
+ event.preventDefault();
510
+ const message_2 = input.trim();
511
+ if (!message_2 || isLoading || disabled) return;
512
+ setInput("");
513
+ sendMessage(message_2);
514
+ requestAnimationFrame(() => inputRef.current?.focus());
515
+ };
516
+ return /* @__PURE__ */ jsxs("div", {
517
+ className: "flex min-h-0 flex-1 flex-col",
518
+ children: [/* @__PURE__ */ jsx("div", {
519
+ ref: messagesScrollRef,
520
+ className: "min-h-0 flex-1 overflow-y-auto border-t p-4",
521
+ children: /* @__PURE__ */ jsxs("div", {
522
+ className: "flex min-h-full flex-col justify-end",
523
+ children: [
524
+ messages.map((message_3, index_0) => {
525
+ const previousMessage = messages[index_0 - 1];
526
+ const isConsecutiveAssistantMessage = message_3.role === "assistant" && previousMessage?.role === "assistant";
527
+ return /* @__PURE__ */ jsx("div", {
528
+ className: cn(index_0 > 0 && "mt-4", isConsecutiveAssistantMessage && "mt-3"),
529
+ children: /* @__PURE__ */ jsx(MessageBubble, {
530
+ message: message_3,
531
+ source,
532
+ isThinkingActive: activeThinkingMessageId === message_3.id,
533
+ activeThinkingDurationKeys,
534
+ thinkingDurations,
535
+ toolLabelContext,
536
+ onApprovalResponse: (part_2, approved_0) => void handleApprovalResponse(part_2, approved_0)
537
+ })
538
+ }, message_3.id);
539
+ }),
540
+ shouldShowThinkingFallback && /* @__PURE__ */ jsx("div", {
541
+ className: cn(messages.length > 0 && "mt-3"),
542
+ children: /* @__PURE__ */ jsx(AgentThinkingIndicator, {})
543
+ }),
544
+ error && /* @__PURE__ */ jsx("div", {
545
+ className: cn((messages.length > 0 || shouldShowThinkingFallback) && "mt-4"),
546
+ children: /* @__PURE__ */ jsxs(Alert, {
547
+ variant: "destructive",
548
+ children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Agent Chat failed" }), /* @__PURE__ */ jsx(AlertDescription, { children: error.message })]
549
+ })
550
+ })
551
+ ]
552
+ })
553
+ }), /* @__PURE__ */ jsx("form", {
554
+ onSubmit: handleSubmit,
555
+ className: "border-border border-t p-4",
556
+ children: /* @__PURE__ */ jsxs("div", {
557
+ className: "border-input focus-within:border-ring focus-within:ring-ring/50 flex items-center gap-2 rounded-2xl border px-3 transition-colors focus-within:ring-[3px]",
558
+ children: [/* @__PURE__ */ jsx(Textarea, {
559
+ ref: inputRef,
560
+ value: input,
561
+ onChange: (event_0) => setInput(event_0.target.value),
562
+ onKeyDown: (event_1) => {
563
+ if (event_1.key === "Enter" && !event_1.shiftKey) {
564
+ event_1.preventDefault();
565
+ event_1.currentTarget.form?.requestSubmit();
566
+ }
567
+ },
568
+ disabled,
569
+ placeholder: disabled ? "Switch to draft to chat" : "Ask for changes or inspect this page…",
570
+ className: "max-h-32 min-h-10 resize-none border-0 bg-inherit! px-0 py-4 leading-6 shadow-none focus-visible:ring-0"
571
+ }), isLoading ? /* @__PURE__ */ jsx(Button, {
572
+ type: "button",
573
+ size: "icon",
574
+ className: "shrink-0",
575
+ "aria-label": "Stop response",
576
+ onClick: stop,
577
+ children: /* @__PURE__ */ jsx(Square, { className: "size-4 fill-current" })
578
+ }) : /* @__PURE__ */ jsxs(Button, {
579
+ type: "submit",
580
+ disabled: disabled || !input.trim(),
581
+ size: "icon",
582
+ className: "shrink-0",
583
+ children: [/* @__PURE__ */ jsx("span", {
584
+ className: "sr-only",
585
+ children: "Send message"
586
+ }), /* @__PURE__ */ jsx(ArrowUp, { className: "size-4" })]
587
+ })]
588
+ })
589
+ })]
590
+ });
591
+ };
592
+
593
+ //#endregion
594
+ export { AgentChatThread };